feat(cli): move destination opening into Rust
FIO_openDstFile still performed destination classification, platform-specific file creation, stdio buffering, and overwrite preparation in C, even though source opening and removal already used Rust filesystem leaves. That made the CLI backend's most important output safety path split across two implementations. Add a Rust destination leaf with a small status ABI. Rust owns sentinel and same-file classification, regular-file detection, platform binary open and fdopen, truncation, and buffering. C keeps the user-facing diagnostics, sparse-mode preference mutation, overwrite prompt, and existing remove-file wrapper; it performs a second Rust open after an accepted overwrite decision so no prompt or policy is duplicated. Existing file contents are left untouched until that C-owned decision is complete. Test Plan: - `cargo test --manifest-path rust/Cargo.toml --lib -- --test-threads=1` -- passed (411 tests, including destination classification/open tests). - `cargo clippy --manifest-path rust/Cargo.toml --lib -- -D warnings` -- passed. - `cargo clippy --manifest-path rust/cli/Cargo.toml --lib --no-default-features --features cli,compression,decompression,benchmark, dict-builder -- -D warnings` -- passed. - Nightly fmt checks for both Rust manifests -- passed. - `make -B -C tests -j2 test-cli-tests` -- passed (41 tests).
This commit is contained in:
+86
-80
@@ -361,6 +361,23 @@ int FIO_rust_openSrcFile(int allowBlockDevices,
|
||||
const char* srcFileName,
|
||||
stat_t* statbuf,
|
||||
FILE** outFile);
|
||||
enum {
|
||||
FIO_RUST_OPEN_DST_SUCCESS = 0,
|
||||
FIO_RUST_OPEN_DST_SETBUF_FAILED = 1,
|
||||
FIO_RUST_OPEN_DST_TEST_MODE = 2,
|
||||
FIO_RUST_OPEN_DST_STDOUT = 3,
|
||||
FIO_RUST_OPEN_DST_SAME_FILE = 4,
|
||||
FIO_RUST_OPEN_DST_EXISTING = 5,
|
||||
FIO_RUST_OPEN_DST_NULL_DEVICE_REGULAR = 6,
|
||||
FIO_RUST_OPEN_DST_OPEN_FAILED = 7,
|
||||
};
|
||||
int FIO_rust_openDstFile(int testMode,
|
||||
int allowExisting,
|
||||
const char* srcFileName,
|
||||
const char* dstFileName,
|
||||
int mode,
|
||||
int* isDstRegFile,
|
||||
FILE** outFile);
|
||||
int FIO_rust_setDictBufferMalloc(const char* fileName,
|
||||
unsigned long long expectedFileSize,
|
||||
size_t maxSize,
|
||||
@@ -478,99 +495,88 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
|
||||
const char* srcFileName, const char* dstFileName,
|
||||
const int mode)
|
||||
{
|
||||
int isDstRegFile;
|
||||
int isDstRegFile = 0;
|
||||
int allowExisting = 0;
|
||||
int sparseAdjusted = 0;
|
||||
int status;
|
||||
FILE* f = NULL;
|
||||
|
||||
if (prefs->testMode) return NULL; /* do not open file in test mode */
|
||||
status = FIO_rust_openDstFile(prefs->testMode, allowExisting,
|
||||
srcFileName, dstFileName, mode,
|
||||
&isDstRegFile, &f);
|
||||
if (status == FIO_RUST_OPEN_DST_TEST_MODE)
|
||||
return NULL; /* do not open file in test mode */
|
||||
|
||||
assert(dstFileName != NULL);
|
||||
if (!strcmp (dstFileName, stdoutmark)) {
|
||||
DISPLAYLEVEL(4,"Using stdout for output \n");
|
||||
SET_BINARY_MODE(stdout);
|
||||
if (prefs->sparseFileSupport == 1) {
|
||||
prefs->sparseFileSupport = 0;
|
||||
DISPLAYLEVEL(4, "Sparse File Support is automatically disabled on stdout ; try --sparse \n");
|
||||
for (;;) {
|
||||
if (status == FIO_RUST_OPEN_DST_STDOUT) {
|
||||
DISPLAYLEVEL(4,"Using stdout for output \n");
|
||||
SET_BINARY_MODE(stdout);
|
||||
if (prefs->sparseFileSupport == 1) {
|
||||
prefs->sparseFileSupport = 0;
|
||||
DISPLAYLEVEL(4, "Sparse File Support is automatically disabled on stdout ; try --sparse \n");
|
||||
}
|
||||
return stdout;
|
||||
}
|
||||
return stdout;
|
||||
}
|
||||
|
||||
/* ensure dst is not the same as src */
|
||||
if (srcFileName != NULL && UTIL_isSameFile(srcFileName, dstFileName)) {
|
||||
DISPLAYLEVEL(1, "zstd: Refusing to open an output file which will overwrite the input file \n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
isDstRegFile = UTIL_isRegularFile(dstFileName); /* invoke once */
|
||||
if (prefs->sparseFileSupport == 1) {
|
||||
prefs->sparseFileSupport = ZSTD_SPARSE_DEFAULT;
|
||||
if (!isDstRegFile) {
|
||||
prefs->sparseFileSupport = 0;
|
||||
DISPLAYLEVEL(4, "Sparse File Support is disabled when output is not a file \n");
|
||||
if (status == FIO_RUST_OPEN_DST_SAME_FILE) {
|
||||
DISPLAYLEVEL(1, "zstd: Refusing to open an output file which will overwrite the input file \n");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDstRegFile) {
|
||||
/* Check if destination file already exists */
|
||||
#if !defined(_WIN32)
|
||||
/* this test does not work on Windows :
|
||||
* `NUL` and `nul` are detected as regular files */
|
||||
if (!strcmp(dstFileName, nulmark)) {
|
||||
/* Keep the C-owned preference mutation and its diagnostics here. Do
|
||||
* this only for the first classification: after an existing file is
|
||||
* removed, the second Rust call must not reclassify it as a missing
|
||||
* destination and emit a different sparse-mode message. */
|
||||
if (!sparseAdjusted) {
|
||||
if (prefs->sparseFileSupport == 1) {
|
||||
prefs->sparseFileSupport = ZSTD_SPARSE_DEFAULT;
|
||||
if (!isDstRegFile) {
|
||||
prefs->sparseFileSupport = 0;
|
||||
DISPLAYLEVEL(4, "Sparse File Support is disabled when output is not a file \n");
|
||||
}
|
||||
}
|
||||
sparseAdjusted = 1;
|
||||
}
|
||||
|
||||
if (status == FIO_RUST_OPEN_DST_NULL_DEVICE_REGULAR) {
|
||||
EXM_THROW(40, "%s is unexpectedly categorized as a regular file",
|
||||
dstFileName);
|
||||
}
|
||||
#endif
|
||||
if (!prefs->overwrite) {
|
||||
if (g_display_prefs.displayLevel <= 1) {
|
||||
/* No interaction possible */
|
||||
DISPLAYLEVEL(1, "zstd: %s already exists; not overwritten \n",
|
||||
dstFileName);
|
||||
return NULL;
|
||||
}
|
||||
DISPLAY("zstd: %s already exists; ", dstFileName);
|
||||
if (UTIL_requireUserConfirmation("overwrite (y/n) ? ", "Not overwritten \n", "yY", fCtx->hasStdinInput))
|
||||
return NULL;
|
||||
}
|
||||
/* need to unlink */
|
||||
FIO_removeFile(dstFileName);
|
||||
}
|
||||
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
/* Windows requires opening the file as a "binary" file to avoid
|
||||
* mangling. This macro doesn't exist on unix. */
|
||||
const int openflags = O_WRONLY|O_CREAT|O_TRUNC|O_BINARY;
|
||||
const int fd = _open(dstFileName, openflags, mode);
|
||||
FILE* f = NULL;
|
||||
if (fd != -1) {
|
||||
f = _fdopen(fd, "wb");
|
||||
}
|
||||
#else
|
||||
const int openflags = O_WRONLY|O_CREAT|O_TRUNC;
|
||||
const int fd = open(dstFileName, openflags, mode);
|
||||
FILE* f = NULL;
|
||||
if (fd != -1) {
|
||||
f = fdopen(fd, "wb");
|
||||
}
|
||||
#endif
|
||||
if (f == NULL) {
|
||||
DISPLAYLEVEL(1, "zstd: %s: %s\n", dstFileName, strerror(errno));
|
||||
} else {
|
||||
/* An increased buffer size can provide a significant performance
|
||||
* boost on some platforms. Note that providing a NULL buf with a
|
||||
* size that's not 0 is not defined in ANSI C, but is defined in an
|
||||
* extension. There are three possibilities here:
|
||||
* 1. Libc supports the extended version and everything is good.
|
||||
* 2. Libc ignores the size when buf is NULL, in which case
|
||||
* everything will continue as if we didn't call `setvbuf()`.
|
||||
* 3. We fail the call and execution continues but a warning
|
||||
* message might be shown.
|
||||
* In all cases due execution continues. For now, I believe that
|
||||
* this is a more cost-effective solution than managing the buffers
|
||||
* allocations ourselves (will require an API change).
|
||||
*/
|
||||
if (setvbuf(f, NULL, _IOFBF, 1 MB)) {
|
||||
DISPLAYLEVEL(2, "Warning: setvbuf failed for %s\n", dstFileName);
|
||||
if (status == FIO_RUST_OPEN_DST_EXISTING) {
|
||||
if (!prefs->overwrite) {
|
||||
if (g_display_prefs.displayLevel <= 1) {
|
||||
/* No interaction possible */
|
||||
DISPLAYLEVEL(1, "zstd: %s already exists; not overwritten \n",
|
||||
dstFileName);
|
||||
return NULL;
|
||||
}
|
||||
DISPLAY("zstd: %s already exists; ", dstFileName);
|
||||
if (UTIL_requireUserConfirmation("overwrite (y/n) ? ", "Not overwritten \n", "yY", fCtx->hasStdinInput))
|
||||
return NULL;
|
||||
}
|
||||
/* Keep the existing C wrapper so its stat/non-regular diagnostics
|
||||
* remain unchanged. The next Rust call opens with O_TRUNC. */
|
||||
FIO_removeFile(dstFileName);
|
||||
allowExisting = 1;
|
||||
f = NULL;
|
||||
status = FIO_rust_openDstFile(prefs->testMode, allowExisting,
|
||||
srcFileName, dstFileName, mode,
|
||||
&isDstRegFile, &f);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status == FIO_RUST_OPEN_DST_OPEN_FAILED) {
|
||||
DISPLAYLEVEL(1, "zstd: %s: %s\n", dstFileName, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (status == FIO_RUST_OPEN_DST_SETBUF_FAILED)
|
||||
DISPLAYLEVEL(2, "Warning: setvbuf failed for %s\n", dstFileName);
|
||||
|
||||
assert(status == FIO_RUST_OPEN_DST_SUCCESS
|
||||
|| status == FIO_RUST_OPEN_DST_SETBUF_FAILED);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
+342
-5
@@ -3,11 +3,11 @@
|
||||
|
||||
//! Rust-owned filesystem leaves for the command-line backend.
|
||||
//!
|
||||
//! The surrounding CLI still owns policy, diagnostics, and stream orchestration
|
||||
//! in C. This module implements the filesystem operations behind
|
||||
//! `FIO_openSrcFile()`, `FIO_removeFile()`, and dictionary loading, returning
|
||||
//! small status codes so the C wrappers can retain their existing messages and
|
||||
//! conventions.
|
||||
//! The surrounding CLI still owns diagnostics and stream orchestration in C.
|
||||
//! This module implements the filesystem operations behind
|
||||
//! `FIO_openDstFile()`, `FIO_openSrcFile()`, `FIO_removeFile()`, and dictionary
|
||||
//! loading, returning small status codes so the C wrappers can retain their
|
||||
//! existing messages, policy fields, and conventions.
|
||||
|
||||
use std::ffi::{c_char, c_void, CStr};
|
||||
use std::fs::File;
|
||||
@@ -44,9 +44,59 @@ const FIO_OPEN_SRC_STAT_FAILED: c_int = 1;
|
||||
const FIO_OPEN_SRC_NON_REGULAR: c_int = 2;
|
||||
const FIO_OPEN_SRC_FOPEN_FAILED: c_int = 3;
|
||||
|
||||
const FIO_OPEN_DST_SUCCESS: c_int = 0;
|
||||
const FIO_OPEN_DST_SETBUF_FAILED: c_int = 1;
|
||||
const FIO_OPEN_DST_TEST_MODE: c_int = 2;
|
||||
const FIO_OPEN_DST_STDOUT: c_int = 3;
|
||||
const FIO_OPEN_DST_SAME_FILE: c_int = 4;
|
||||
const FIO_OPEN_DST_EXISTING: c_int = 5;
|
||||
const FIO_OPEN_DST_NULL_DEVICE_REGULAR: c_int = 6;
|
||||
const FIO_OPEN_DST_OPEN_FAILED: c_int = 7;
|
||||
const FIO_DESTINATION_BUFFER_SIZE: usize = 1 << 20;
|
||||
|
||||
static STDOUT_MARK: &[u8] = b"/*stdout*\\\0";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum DestinationDecision {
|
||||
TestMode,
|
||||
Stdout,
|
||||
SameFile,
|
||||
NullDeviceRegular,
|
||||
ExistingRegular,
|
||||
Create,
|
||||
}
|
||||
|
||||
fn classify_destination(
|
||||
test_mode: bool,
|
||||
is_stdout: bool,
|
||||
same_file: bool,
|
||||
is_regular: bool,
|
||||
is_null_device: bool,
|
||||
allow_existing: bool,
|
||||
) -> DestinationDecision {
|
||||
if test_mode {
|
||||
return DestinationDecision::TestMode;
|
||||
}
|
||||
if is_stdout {
|
||||
return DestinationDecision::Stdout;
|
||||
}
|
||||
if same_file {
|
||||
return DestinationDecision::SameFile;
|
||||
}
|
||||
if is_regular && is_null_device {
|
||||
return DestinationDecision::NullDeviceRegular;
|
||||
}
|
||||
if is_regular && !allow_existing {
|
||||
return DestinationDecision::ExistingRegular;
|
||||
}
|
||||
DestinationDecision::Create
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn UTIL_stat(file_name: *const c_char, stat_buf: *mut libc::stat) -> c_int;
|
||||
fn UTIL_isRegularFileStat(stat_buf: *const libc::stat) -> c_int;
|
||||
fn UTIL_isRegularFile(file_name: *const c_char) -> c_int;
|
||||
fn UTIL_isSameFile(file1: *const c_char, file2: *const c_char) -> c_int;
|
||||
fn UTIL_isFIFOStat(stat_buf: *const libc::stat) -> c_int;
|
||||
fn UTIL_isBlockDevStat(stat_buf: *const libc::stat) -> c_int;
|
||||
fn fopen(file_name: *const c_char, mode: *const c_char) -> *mut c_void;
|
||||
@@ -127,6 +177,126 @@ fn path_from_c(path: *const c_char) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
fn c_string_equals(path: *const c_char, expected: &[u8]) -> bool {
|
||||
if path.is_null() {
|
||||
return false;
|
||||
}
|
||||
unsafe { CStr::from_ptr(path).to_bytes() == &expected[..expected.len() - 1] }
|
||||
}
|
||||
|
||||
unsafe fn open_destination(path: *const c_char, mode: c_int) -> (*mut c_void, c_int) {
|
||||
#[cfg(windows)]
|
||||
let open_flags = libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC | libc::O_BINARY;
|
||||
#[cfg(not(windows))]
|
||||
let open_flags = libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC;
|
||||
|
||||
let fd = unsafe { libc::open(path, open_flags, mode) };
|
||||
if fd == -1 {
|
||||
return (ptr::null_mut(), FIO_OPEN_DST_OPEN_FAILED);
|
||||
}
|
||||
|
||||
// This intentionally follows the original C leaf: if fdopen fails, the
|
||||
// caller observes its errno through the C diagnostic path. In particular,
|
||||
// do not perform another errno-setting operation before returning.
|
||||
let file = unsafe { libc::fdopen(fd, c"wb".as_ptr()) };
|
||||
if file.is_null() {
|
||||
return (ptr::null_mut(), FIO_OPEN_DST_OPEN_FAILED);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
if libc::setvbuf(
|
||||
file,
|
||||
ptr::null_mut(),
|
||||
0, // _IOFBF
|
||||
FIO_DESTINATION_BUFFER_SIZE,
|
||||
) != 0
|
||||
{
|
||||
// The stream remains usable. The caller needs the warning status
|
||||
// so C can preserve the original user-facing diagnostic.
|
||||
return (file.cast::<c_void>(), FIO_OPEN_DST_SETBUF_FAILED);
|
||||
}
|
||||
}
|
||||
(file.cast(), FIO_OPEN_DST_SUCCESS)
|
||||
}
|
||||
|
||||
/// Classifies and opens a non-policy destination path for the C adapter.
|
||||
///
|
||||
/// `allow_existing` is false for the first call. A regular destination then
|
||||
/// returns `FIO_OPEN_DST_EXISTING` without changing it, allowing C to retain
|
||||
/// its overwrite prompt and `FIO_removeFile()` diagnostics. C calls again
|
||||
/// with `allow_existing` true after that policy decision. The returned
|
||||
/// `FILE*` remains owned and closed by C.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn FIO_rust_openDstFile(
|
||||
test_mode: c_int,
|
||||
allow_existing: c_int,
|
||||
src_file_name: *const c_char,
|
||||
dst_file_name: *const c_char,
|
||||
mode: c_int,
|
||||
is_dst_reg_file: *mut c_int,
|
||||
out_file: *mut *mut c_void,
|
||||
) -> c_int {
|
||||
if is_dst_reg_file.is_null() || out_file.is_null() {
|
||||
return FIO_OPEN_DST_OPEN_FAILED;
|
||||
}
|
||||
unsafe {
|
||||
*is_dst_reg_file = 0;
|
||||
*out_file = ptr::null_mut();
|
||||
}
|
||||
|
||||
if test_mode != 0 {
|
||||
return FIO_OPEN_DST_TEST_MODE;
|
||||
}
|
||||
if dst_file_name.is_null() {
|
||||
return FIO_OPEN_DST_OPEN_FAILED;
|
||||
}
|
||||
|
||||
let is_stdout = c_string_equals(dst_file_name, STDOUT_MARK);
|
||||
let same_file = !is_stdout
|
||||
&& !src_file_name.is_null()
|
||||
&& unsafe { UTIL_isSameFile(src_file_name, dst_file_name) != 0 };
|
||||
if same_file {
|
||||
return FIO_OPEN_DST_SAME_FILE;
|
||||
}
|
||||
|
||||
let is_regular = !is_stdout && unsafe { UTIL_isRegularFile(dst_file_name) != 0 };
|
||||
let is_null_device = {
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
is_regular && c_string_equals(dst_file_name, b"/dev/null\0")
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
unsafe { *is_dst_reg_file = is_regular as c_int };
|
||||
match classify_destination(
|
||||
test_mode != 0,
|
||||
is_stdout,
|
||||
same_file,
|
||||
is_regular,
|
||||
is_null_device,
|
||||
allow_existing != 0,
|
||||
) {
|
||||
DestinationDecision::TestMode => FIO_OPEN_DST_TEST_MODE,
|
||||
DestinationDecision::Stdout => FIO_OPEN_DST_STDOUT,
|
||||
DestinationDecision::SameFile => FIO_OPEN_DST_SAME_FILE,
|
||||
DestinationDecision::NullDeviceRegular => FIO_OPEN_DST_NULL_DEVICE_REGULAR,
|
||||
DestinationDecision::ExistingRegular => FIO_OPEN_DST_EXISTING,
|
||||
DestinationDecision::Create => {
|
||||
let (file, status) = unsafe { open_destination(dst_file_name, mode) };
|
||||
if file.is_null() {
|
||||
return status;
|
||||
}
|
||||
|
||||
unsafe { *out_file = file };
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stats a dictionary path into the C-owned `stat_t` buffer.
|
||||
///
|
||||
/// The C wrapper keeps the policy and diagnostics. This leaf only reports
|
||||
@@ -594,6 +764,173 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_classification_preserves_policy_precedence() {
|
||||
assert_eq!(
|
||||
classify_destination(true, true, true, true, true, false),
|
||||
DestinationDecision::TestMode
|
||||
);
|
||||
assert_eq!(
|
||||
classify_destination(false, true, true, true, true, false),
|
||||
DestinationDecision::Stdout
|
||||
);
|
||||
assert_eq!(
|
||||
classify_destination(false, false, true, true, true, false),
|
||||
DestinationDecision::SameFile
|
||||
);
|
||||
assert_eq!(
|
||||
classify_destination(false, false, false, true, true, true),
|
||||
DestinationDecision::NullDeviceRegular
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_classification_requires_explicit_existing_file_permission() {
|
||||
assert_eq!(
|
||||
classify_destination(false, false, false, true, false, false),
|
||||
DestinationDecision::ExistingRegular
|
||||
);
|
||||
assert_eq!(
|
||||
classify_destination(false, false, false, true, false, true),
|
||||
DestinationDecision::Create
|
||||
);
|
||||
assert_eq!(
|
||||
classify_destination(false, false, false, false, true, false),
|
||||
DestinationDecision::Create
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_test_mode_returns_before_path_validation() {
|
||||
let mut is_dst_reg_file = c_int::MAX;
|
||||
let mut out_file = ptr::dangling_mut::<c_void>();
|
||||
let status = unsafe {
|
||||
FIO_rust_openDstFile(
|
||||
1,
|
||||
0,
|
||||
ptr::null(),
|
||||
ptr::null(),
|
||||
0o666,
|
||||
&mut is_dst_reg_file,
|
||||
&mut out_file,
|
||||
)
|
||||
};
|
||||
assert_eq!(status, FIO_OPEN_DST_TEST_MODE);
|
||||
assert_eq!(is_dst_reg_file, 0);
|
||||
assert!(out_file.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destination_stdout_is_classified_without_opening_or_statting() {
|
||||
let dst = CString::new("/*stdout*\\").expect("stdout sentinel contains no NUL");
|
||||
let mut is_dst_reg_file = c_int::MAX;
|
||||
let mut out_file = ptr::dangling_mut::<c_void>();
|
||||
let status = unsafe {
|
||||
FIO_rust_openDstFile(
|
||||
0,
|
||||
0,
|
||||
ptr::null(),
|
||||
dst.as_ptr(),
|
||||
0o666,
|
||||
&mut is_dst_reg_file,
|
||||
&mut out_file,
|
||||
)
|
||||
};
|
||||
assert_eq!(status, FIO_OPEN_DST_STDOUT);
|
||||
assert_eq!(is_dst_reg_file, 0);
|
||||
assert!(out_file.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_destination_is_reported_without_modifying_it() {
|
||||
let path = temp_path("open-existing");
|
||||
let payload = b"existing payload";
|
||||
fs::write(&path, payload).expect("create temporary file");
|
||||
let c_path = c_path(&path);
|
||||
let mut is_dst_reg_file = 0;
|
||||
let mut out_file = ptr::null_mut();
|
||||
|
||||
let status = unsafe {
|
||||
FIO_rust_openDstFile(
|
||||
0,
|
||||
0,
|
||||
ptr::null(),
|
||||
c_path.as_ptr(),
|
||||
0o666,
|
||||
&mut is_dst_reg_file,
|
||||
&mut out_file,
|
||||
)
|
||||
};
|
||||
assert_eq!(status, FIO_OPEN_DST_EXISTING);
|
||||
assert_eq!(is_dst_reg_file, 1);
|
||||
assert!(out_file.is_null());
|
||||
assert_eq!(fs::read(&path).expect("read destination"), payload);
|
||||
|
||||
fs::remove_file(&path).expect("remove temporary file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_source_and_destination_is_refused_before_creation() {
|
||||
let path = temp_path("open-same");
|
||||
fs::write(&path, b"source payload").expect("create temporary file");
|
||||
let c_path = c_path(&path);
|
||||
let mut is_dst_reg_file = 0;
|
||||
let mut out_file = ptr::null_mut();
|
||||
|
||||
let status = unsafe {
|
||||
FIO_rust_openDstFile(
|
||||
0,
|
||||
1,
|
||||
c_path.as_ptr(),
|
||||
c_path.as_ptr(),
|
||||
0o666,
|
||||
&mut is_dst_reg_file,
|
||||
&mut out_file,
|
||||
)
|
||||
};
|
||||
assert_eq!(status, FIO_OPEN_DST_SAME_FILE);
|
||||
assert_eq!(is_dst_reg_file, 0);
|
||||
assert!(out_file.is_null());
|
||||
assert_eq!(fs::read(&path).expect("read source"), b"source payload");
|
||||
|
||||
fs::remove_file(&path).expect("remove temporary file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creates_binary_buffered_destination() {
|
||||
let path = temp_path("open-create");
|
||||
let c_path = c_path(&path);
|
||||
let mut is_dst_reg_file = c_int::MAX;
|
||||
let mut out_file = ptr::null_mut();
|
||||
|
||||
let status = unsafe {
|
||||
FIO_rust_openDstFile(
|
||||
0,
|
||||
0,
|
||||
ptr::null(),
|
||||
c_path.as_ptr(),
|
||||
0o666,
|
||||
&mut is_dst_reg_file,
|
||||
&mut out_file,
|
||||
)
|
||||
};
|
||||
assert!(matches!(
|
||||
status,
|
||||
FIO_OPEN_DST_SUCCESS | FIO_OPEN_DST_SETBUF_FAILED
|
||||
));
|
||||
assert_eq!(is_dst_reg_file, 0);
|
||||
assert!(!out_file.is_null());
|
||||
|
||||
let payload = b"destination payload";
|
||||
let written =
|
||||
unsafe { libc::fwrite(payload.as_ptr().cast(), 1, payload.len(), out_file.cast()) };
|
||||
assert_eq!(written, payload.len());
|
||||
assert_eq!(unsafe { libc::fclose(out_file.cast()) }, 0);
|
||||
assert_eq!(fs::read(&path).expect("read destination"), payload);
|
||||
|
||||
fs::remove_file(&path).expect("remove temporary file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_filename_leaves_empty_outputs() {
|
||||
let mut buffer = ptr::null_mut();
|
||||
|
||||
Reference in New Issue
Block a user