feat(fileio): move output path construction to Rust

Move the basename-preserving output-directory helper behind the existing Rust utility ABI. Keep the libc allocation contract and platform separator behavior intact while leaving the C file-I/O engines and higher-level naming policy unchanged.

Test Plan:

- cargo test --manifest-path rust/cli/Cargo.toml (93 passed)

- cargo clippy --manifest-path rust/cli/Cargo.toml --all-targets

- make -B -C tests -j2 test-cli-tests (41 passed)
This commit is contained in:
2026-07-18 02:52:26 +02:00
parent 9f85a7ebd7
commit 4171c42448
2 changed files with 74 additions and 36 deletions
+71
View File
@@ -161,6 +161,59 @@ fn path_separator() -> u8 {
}
}
/// Build the basename-preserving output path used by the file-I/O backend.
///
/// The caller owns the returned libc allocation. `suffix_len` is retained in
/// the ABI because the original C helper reserved that extra capacity for its
/// callers, even though it only wrote the directory and basename.
#[no_mangle]
pub unsafe extern "C" fn UTIL_createFilenameFromOutDir(
path: *const c_char,
out_dir: *const c_char,
_suffix_len: usize,
) -> *mut c_char {
if path.is_null() || out_dir.is_null() {
return ptr::null_mut();
}
let path = unsafe { c_bytes(path) };
let out_dir = unsafe { c_bytes(out_dir) };
if out_dir.is_empty() {
std::process::abort();
}
let separator = path_separator();
let mut filename = path;
if let Some(index) = path.iter().rposition(|&byte| byte == separator) {
filename = &path[index + 1..];
}
#[cfg(windows)]
if let Some(index) = filename.iter().rposition(|&byte| byte == b'/') {
filename = &filename[index + 1..];
}
let separator_len = usize::from(out_dir.last().copied() != Some(separator));
let Some(size) = out_dir
.len()
.checked_add(separator_len)
.and_then(|size| size.checked_add(filename.len()))
.and_then(|size| size.checked_add(1))
else {
std::process::abort();
};
let result = unsafe { malloc_bytes(size).cast::<c_char>() };
unsafe {
ptr::copy_nonoverlapping(out_dir.as_ptr(), result.cast(), out_dir.len());
let mut offset = out_dir.len();
if separator_len != 0 {
*result.add(offset) = separator as c_char;
offset += 1;
}
ptr::copy_nonoverlapping(filename.as_ptr(), result.add(offset).cast(), filename.len());
*result.add(offset + filename.len()) = 0;
}
result
}
fn path_from_bytes(bytes: &[u8]) -> PathBuf {
#[cfg(unix)]
{
@@ -1558,6 +1611,24 @@ mod tests {
}
}
#[test]
fn output_path_preserves_basename_and_trailing_separator() {
let path = CString::new("input/nested/file.txt").unwrap();
let out_dir = CString::new("out").unwrap();
unsafe {
let name = UTIL_createFilenameFromOutDir(path.as_ptr(), out_dir.as_ptr(), 4);
assert_eq!(CStr::from_ptr(name).to_bytes(), b"out/file.txt");
free_ptr(name);
}
let out_dir = CString::new(if cfg!(windows) { "out\\" } else { "out/" }).unwrap();
unsafe {
let name = UTIL_createFilenameFromOutDir(path.as_ptr(), out_dir.as_ptr(), 0);
assert_eq!(CStr::from_ptr(name).to_bytes(), b"out/file.txt");
free_ptr(name);
}
}
#[test]
fn human_readable_size_uses_the_c_scaling_policy() {
unsafe {