#![allow(non_camel_case_types)] #![allow(non_snake_case)] #![allow(clippy::missing_safety_doc)] //! Rust implementation of the program utility layer from `programs/util.c`. //! //! The C header remains the public ABI. Functions in this module therefore //! use the C allocator for objects which are returned to C, keep all exported //! structures `repr(C)`, and use the target's `libc::stat` representation for //! the `stat_t` pointer passed by `util.h`. use std::ffi::{CStr, CString, OsString}; use std::fs; use std::mem::{offset_of, size_of, MaybeUninit}; use std::os::raw::{c_char, c_int, c_uint, c_void}; use std::path::PathBuf; use std::ptr; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::OnceLock; #[cfg(unix)] use std::os::unix::ffi::OsStringExt; type Stat = libc::stat; #[cfg(unix)] type UtilMode = libc::mode_t; #[cfg(windows)] type UtilMode = c_int; #[cfg(not(any(unix, windows)))] type UtilMode = c_uint; const LIST_SIZE_INCREASE: usize = 8 * 1024; const MAX_FILE_OF_FILE_NAMES_SIZE: u64 = 50 * (1 << 20); const UTIL_FILESIZE_UNKNOWN: u64 = u64::MAX; const DIR_DEFAULT_MODE: u32 = 0o755; /// `g_utilDisplayLevel` is a public C global, not a Rust-owned preference. #[no_mangle] pub static mut g_utilDisplayLevel: c_int = 0; /// The two structs below mirror the declarations in `programs/util.h`. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq)] pub struct UTIL_HumanReadableSize_t { pub value: f64, pub precision: c_int, pub suffix: *const c_char, } #[repr(C)] #[derive(Debug)] pub struct FileNamesTable { pub fileNames: *mut *const c_char, pub buf: *mut c_char, pub tableSize: usize, pub tableCapacity: usize, } const _: () = assert!(offset_of!(UTIL_HumanReadableSize_t, value) == 0); const _: () = assert!(offset_of!(UTIL_HumanReadableSize_t, precision) == size_of::()); const _: () = assert!( offset_of!(UTIL_HumanReadableSize_t, suffix) >= offset_of!(UTIL_HumanReadableSize_t, precision) + size_of::() ); const _: () = assert!( size_of::() == offset_of!(UTIL_HumanReadableSize_t, suffix) + size_of::<*const c_char>() ); const _: () = assert!(offset_of!(FileNamesTable, fileNames) == 0); const _: () = assert!(offset_of!(FileNamesTable, buf) == size_of::<*const c_char>()); const _: () = assert!( offset_of!(FileNamesTable, tableSize) == offset_of!(FileNamesTable, buf) + size_of::<*mut c_char>() ); const _: () = assert!( offset_of!(FileNamesTable, tableCapacity) == offset_of!(FileNamesTable, tableSize) + size_of::() ); const _: () = assert!( size_of::() == offset_of!(FileNamesTable, tableCapacity) + size_of::() ); static EMPTY_EXTENSION: [u8; 1] = [0]; static SUFFIX_B: &[u8] = b" B\0"; static SUFFIX_KIB: &[u8] = b" KiB\0"; static SUFFIX_MIB: &[u8] = b" MiB\0"; static SUFFIX_GIB: &[u8] = b" GiB\0"; static SUFFIX_TIB: &[u8] = b" TiB\0"; static SUFFIX_PIB: &[u8] = b" PiB\0"; static SUFFIX_EIB: &[u8] = b" EiB\0"; static FAKE_STDIN_IS_CONSOLE: AtomicBool = AtomicBool::new(false); static FAKE_STDOUT_IS_CONSOLE: AtomicBool = AtomicBool::new(false); static FAKE_STDERR_IS_CONSOLE: AtomicBool = AtomicBool::new(false); static TRACE_FILE_STAT: AtomicBool = AtomicBool::new(false); static TRACE_DEPTH: AtomicI32 = AtomicI32::new(0); static CORE_COUNT: OnceLock = OnceLock::new(); #[cfg(windows)] unsafe extern "C" { fn UTIL_rust_utime(filename: *const c_char, statbuf: *const Stat) -> c_int; } #[inline] fn display_level() -> c_int { // The C API intentionally exposes this mutable global. All reads mirror // the unsynchronized reads in the original implementation. unsafe { g_utilDisplayLevel } } fn display(message: &str) { eprint!("{message}"); } unsafe fn c_string(value: *const c_char) -> String { if value.is_null() { "(null)".to_owned() } else { String::from_utf8_lossy(CStr::from_ptr(value).to_bytes()).into_owned() } } unsafe fn display_c_string(message: *const c_char) { if !message.is_null() { display(&String::from_utf8_lossy(CStr::from_ptr(message).to_bytes())); } } fn trace_call(message: &str) { if TRACE_FILE_STAT.load(Ordering::Relaxed) { let depth = TRACE_DEPTH.fetch_add(1, Ordering::Relaxed).max(0) as usize; eprintln!("Trace:FileStat: {:width$}> {message}", "", width = depth); } } fn trace_return(ret: c_int) { if TRACE_FILE_STAT.load(Ordering::Relaxed) { let depth = TRACE_DEPTH .fetch_sub(1, Ordering::Relaxed) .saturating_sub(1); eprintln!( "Trace:FileStat: {:width$}< {ret}", "", width = depth as usize ); } } #[inline] unsafe fn c_bytes<'a>(value: *const c_char) -> &'a [u8] { CStr::from_ptr(value).to_bytes() } #[inline] fn path_separator() -> u8 { if cfg!(windows) { b'\\' } else { b'/' } } /// 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::() }; 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)] { PathBuf::from(OsString::from_vec(bytes.to_vec())) } #[cfg(not(unix))] { PathBuf::from(String::from_utf8_lossy(bytes).into_owned()) } } fn os_string_bytes(value: OsString) -> Vec { #[cfg(unix)] { value.into_vec() } #[cfg(not(unix))] { value.to_string_lossy().into_owned().into_bytes() } } fn c_path(bytes: &[u8]) -> CString { CString::new(bytes).unwrap_or_else(|_| std::process::abort()) } unsafe fn malloc_bytes(size: usize) -> *mut u8 { let ptr = libc::malloc(size).cast::(); if ptr.is_null() { std::process::abort(); } ptr } unsafe fn malloc_array(count: usize) -> *mut T { let Some(size) = count.checked_mul(size_of::()) else { return ptr::null_mut(); }; libc::malloc(size).cast::() } unsafe fn allocate_struct() -> *mut T { let result = malloc_bytes(size_of::()).cast::(); // The allocation helper is used only for C functions whose original // CONTROL() path terminates the program on allocation failure. ptr::write_bytes(result.cast::(), 0, size_of::()); result } unsafe fn free_ptr(value: *mut T) { libc::free(value.cast::()); } unsafe fn table_entries<'a>(table: *const FileNamesTable) -> &'a [*const c_char] { if (*table).fileNames.is_null() || (*table).tableSize == 0 { &[] } else { std::slice::from_raw_parts((*table).fileNames, (*table).tableSize) } } unsafe fn table_names_size(table: *const FileNamesTable) -> usize { let mut total = 0usize; for &name in table_entries(table) { if name.is_null() { break; } total = total.saturating_add(c_bytes(name).len().saturating_add(1)); } total } unsafe fn make_table( file_names: *mut *const c_char, table_size: usize, table_capacity: usize, buf: *mut c_char, ) -> *mut FileNamesTable { let table = allocate_struct::(); (*table).fileNames = file_names; (*table).buf = buf; (*table).tableSize = table_size; (*table).tableCapacity = table_capacity; table } unsafe fn make_table_from_buffer( buf: *mut u8, buffer_len: usize, table_size: usize, table_capacity: usize, ) -> *mut FileNamesTable { let Some(pointer_count) = table_capacity.checked_mul(size_of::<*const c_char>()) else { free_ptr(buf); return ptr::null_mut(); }; let file_names = malloc_bytes(pointer_count.max(1)).cast::<*const c_char>(); let mut pos = 0usize; for index in 0..table_size { if pos > buffer_len { free_ptr(file_names); free_ptr(buf); return ptr::null_mut(); } *file_names.add(index) = buf.add(pos).cast::(); let name_len = CStr::from_ptr(buf.add(pos).cast::()) .to_bytes() .len(); pos = pos.saturating_add(name_len + 1); } make_table(file_names, table_size, table_capacity, buf.cast::()) } #[inline] unsafe fn stat_mode(statbuf: *const Stat) -> u64 { (*statbuf).st_mode as u64 } #[cfg(unix)] #[inline] unsafe fn mode_is(statbuf: *const Stat, kind: libc::mode_t) -> bool { stat_mode(statbuf) & libc::S_IFMT as u64 == kind as u64 } #[cfg(windows)] #[inline] unsafe fn mode_is(statbuf: *const Stat, kind: c_int) -> bool { // util.c intentionally uses `(st_mode & S_IFREG) != 0` for the CRT. stat_mode(statbuf) & kind as u64 != 0 } #[cfg(not(any(unix, windows)))] #[inline] unsafe fn mode_is(_statbuf: *const Stat, _kind: c_uint) -> bool { false } #[no_mangle] pub unsafe extern "C" fn UTIL_fstat( fd: c_int, filename: *const c_char, statbuf: *mut Stat, ) -> c_int { trace_call(&format!("UTIL_stat({fd}, {})", unsafe { c_string(filename) })); let result = if fd >= 0 { libc::fstat(fd, statbuf) } else { libc::stat(filename, statbuf) }; let ret = (result == 0) as c_int; trace_return(ret); ret } #[no_mangle] pub unsafe extern "C" fn UTIL_stat(filename: *const c_char, statbuf: *mut Stat) -> c_int { UTIL_fstat(-1, filename, statbuf) } #[no_mangle] pub unsafe extern "C" fn UTIL_isRegularFileStat(statbuf: *const Stat) -> c_int { #[cfg(any(unix, windows))] { mode_is(statbuf, libc::S_IFREG) as c_int } #[cfg(not(any(unix, windows)))] { 0 } } #[no_mangle] pub unsafe extern "C" fn UTIL_isDirectoryStat(statbuf: *const Stat) -> c_int { trace_call("UTIL_isDirectoryStat()"); #[cfg(any(unix, windows))] { let ret = mode_is(statbuf, libc::S_IFDIR) as c_int; trace_return(ret); ret } #[cfg(not(any(unix, windows)))] { trace_return(0); 0 } } #[no_mangle] pub unsafe extern "C" fn UTIL_isFIFOStat(statbuf: *const Stat) -> c_int { #[cfg(unix)] { mode_is(statbuf, libc::S_IFIFO) as c_int } #[cfg(not(unix))] { let _ = statbuf; 0 } } #[no_mangle] pub unsafe extern "C" fn UTIL_isBlockDevStat(statbuf: *const Stat) -> c_int { #[cfg(unix)] { mode_is(statbuf, libc::S_IFBLK) as c_int } #[cfg(not(unix))] { let _ = statbuf; 0 } } #[no_mangle] pub unsafe extern "C" fn UTIL_isRegularFile(infilename: *const c_char) -> c_int { trace_call(&format!("UTIL_isRegularFile({})", unsafe { c_string(infilename) })); let mut statbuf = MaybeUninit::::uninit(); let ret = if UTIL_stat(infilename, statbuf.as_mut_ptr()) == 0 { 0 } else { UTIL_isRegularFileStat(statbuf.as_ptr()) }; trace_return(ret); ret } #[no_mangle] pub unsafe extern "C" fn UTIL_isDirectory(infilename: *const c_char) -> c_int { trace_call(&format!("UTIL_isDirectory({})", unsafe { c_string(infilename) })); let mut statbuf = MaybeUninit::::uninit(); let ret = if UTIL_stat(infilename, statbuf.as_mut_ptr()) == 0 { 0 } else { UTIL_isDirectoryStat(statbuf.as_ptr()) }; trace_return(ret); ret } #[no_mangle] pub unsafe extern "C" fn UTIL_isFIFO(infilename: *const c_char) -> c_int { #[cfg(unix)] { let mut statbuf = MaybeUninit::::uninit(); if UTIL_stat(infilename, statbuf.as_mut_ptr()) != 0 { return UTIL_isFIFOStat(statbuf.as_ptr()); } } let _ = infilename; 0 } #[no_mangle] pub unsafe extern "C" fn UTIL_isLink(infilename: *const c_char) -> c_int { trace_call(&format!("UTIL_isLink({})", unsafe { c_string(infilename) })); #[cfg(unix)] { let mut statbuf = MaybeUninit::::uninit(); if libc::lstat(infilename, statbuf.as_mut_ptr()) == 0 { let ret = mode_is(statbuf.as_ptr(), libc::S_IFLNK) as c_int; trace_return(ret); return ret; } } let _ = infilename; trace_return(0); 0 } #[no_mangle] pub unsafe extern "C" fn UTIL_isSameFile(file1: *const c_char, file2: *const c_char) -> c_int { assert!(!file1.is_null() && !file2.is_null()); trace_call(&format!( "UTIL_isSameFile({}, {})", unsafe { c_string(file1) }, unsafe { c_string(file2) } )); #[cfg(windows)] { let ret = (c_bytes(file1) == c_bytes(file2)) as c_int; trace_return(ret); return ret; } #[cfg(not(windows))] { let mut file1_stat = MaybeUninit::::uninit(); let mut file2_stat = MaybeUninit::::uninit(); let ret = if UTIL_stat(file1, file1_stat.as_mut_ptr()) == 0 || UTIL_stat(file2, file2_stat.as_mut_ptr()) == 0 { 0 } else { UTIL_isSameFileStat(file1, file2, file1_stat.as_ptr(), file2_stat.as_ptr()) }; trace_return(ret); ret } } #[no_mangle] pub unsafe extern "C" fn UTIL_isSameFileStat( file1: *const c_char, file2: *const c_char, file1_stat: *const Stat, file2_stat: *const Stat, ) -> c_int { assert!(!file1.is_null() && !file2.is_null()); #[cfg(windows)] { let _ = (file1_stat, file2_stat); return (c_bytes(file1) == c_bytes(file2)) as c_int; } #[cfg(not(windows))] { ((*file1_stat).st_dev == (*file2_stat).st_dev && (*file1_stat).st_ino == (*file2_stat).st_ino) as c_int } } #[no_mangle] pub unsafe extern "C" fn UTIL_getFileSizeStat(statbuf: *const Stat) -> u64 { if UTIL_isRegularFileStat(statbuf) == 0 { return UTIL_FILESIZE_UNKNOWN; } (*statbuf).st_size as u64 } #[no_mangle] pub unsafe extern "C" fn UTIL_getFileSize(infilename: *const c_char) -> u64 { trace_call(&format!("UTIL_getFileSize({})", unsafe { c_string(infilename) })); let mut statbuf = MaybeUninit::::uninit(); if UTIL_stat(infilename, statbuf.as_mut_ptr()) == 0 { trace_return(-1); return UTIL_FILESIZE_UNKNOWN; } let size = UTIL_getFileSizeStat(statbuf.as_ptr()); trace_return(size as c_int); size } #[no_mangle] pub unsafe extern "C" fn UTIL_getTotalFileSize( file_names: *const *const c_char, nb_files: c_uint, ) -> u64 { let mut total = 0u64; for index in 0..nb_files as usize { let size = UTIL_getFileSize(*file_names.add(index)); if size == UTIL_FILESIZE_UNKNOWN { return UTIL_FILESIZE_UNKNOWN; } total = total.wrapping_add(size); } total } #[no_mangle] pub unsafe extern "C" fn UTIL_chmod( filename: *const c_char, statbuf: *const Stat, permissions: UtilMode, ) -> c_int { UTIL_fchmod(-1, filename, statbuf, permissions) } #[no_mangle] pub unsafe extern "C" fn UTIL_fchmod( fd: c_int, filename: *const c_char, statbuf: *const Stat, permissions: UtilMode, ) -> c_int { trace_call(&format!( "UTIL_chmod({}, {:04o})", unsafe { c_string(filename) }, permissions )); let mut local_stat = MaybeUninit::::uninit(); let stat_ptr = if statbuf.is_null() { if UTIL_fstat(fd, filename, local_stat.as_mut_ptr()) == 0 { trace_return(0); return 0; } local_stat.as_ptr() } else { statbuf }; if UTIL_isRegularFileStat(stat_ptr) == 0 { trace_return(0); return 0; } #[cfg(unix)] { if fd >= 0 { trace_call("fchmod"); let ret = libc::fchmod(fd, permissions); trace_return(ret); trace_return(ret); return ret; } trace_call("chmod"); let ret = libc::chmod(filename, permissions); trace_return(ret); trace_return(ret); ret } #[cfg(windows)] { let _ = fd; let ret = libc::chmod(filename, permissions); trace_return(ret); ret } #[cfg(not(any(unix, windows)))] { let _ = (fd, filename, permissions); trace_return(0); 0 } } #[cfg(any(target_os = "linux", target_os = "android"))] unsafe fn set_mtime(filename: *const c_char, statbuf: *const Stat) -> c_int { let now = libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_NOW, }; let mtime = libc::timespec { tv_sec: (*statbuf).st_mtime, tv_nsec: (*statbuf).st_mtime_nsec as _, }; let times = [now, mtime]; libc::utimensat(libc::AT_FDCWD, filename, times.as_ptr(), 0) } #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))] unsafe fn set_mtime(filename: *const c_char, statbuf: *const Stat) -> c_int { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |duration| duration.as_secs()) as libc::time_t; let times = libc::utimbuf { actime: now, modtime: (*statbuf).st_mtime, }; libc::utime(filename, ×) } #[cfg(windows)] unsafe fn set_mtime(filename: *const c_char, statbuf: *const Stat) -> c_int { UTIL_rust_utime(filename, statbuf) } #[cfg(not(any(unix, windows)))] unsafe fn set_mtime(_filename: *const c_char, _statbuf: *const Stat) -> c_int { -1 } #[no_mangle] pub unsafe extern "C" fn UTIL_utime(filename: *const c_char, statbuf: *const Stat) -> c_int { trace_call(&format!("UTIL_utime({})", unsafe { c_string(filename) })); let ret = set_mtime(filename, statbuf); trace_return(ret); ret } #[no_mangle] pub unsafe extern "C" fn UTIL_setFileStat(filename: *const c_char, statbuf: *const Stat) -> c_int { UTIL_setFDStat(-1, filename, statbuf) } #[no_mangle] pub unsafe extern "C" fn UTIL_setFDStat( fd: c_int, filename: *const c_char, statbuf: *const Stat, ) -> c_int { trace_call(&format!("UTIL_setFileStat({fd}, {})", unsafe { c_string(filename) })); let mut current_stat = MaybeUninit::::uninit(); if UTIL_fstat(fd, filename, current_stat.as_mut_ptr()) == 0 || UTIL_isRegularFileStat(current_stat.as_ptr()) == 0 { trace_return(-1); return -1; } let mut result = 0; #[cfg(unix)] { let no_uid = !0 as libc::uid_t; if fd >= 0 { result += libc::fchown(fd, no_uid, (*statbuf).st_gid); } else { result += libc::chown(filename, no_uid, (*statbuf).st_gid); } } let permissions = ((*statbuf).st_mode as u64 & 0o777) as UtilMode; result += UTIL_fchmod(fd, filename, current_stat.as_ptr(), permissions); #[cfg(unix)] { let no_gid = !0 as libc::gid_t; if fd >= 0 { result += libc::fchown(fd, (*statbuf).st_uid, no_gid); } else { result += libc::chown(filename, (*statbuf).st_uid, no_gid); } } let ret = -result; trace_return(ret); ret } #[no_mangle] pub unsafe extern "C" fn UTIL_requireUserConfirmation( prompt: *const c_char, abort_msg: *const c_char, acceptable_letters: *const c_char, has_stdin_input: c_int, ) -> c_int { if has_stdin_input != 0 { display("stdin is an input - not proceeding.\n"); return 1; } display_c_string(prompt); let ch = libc::getchar(); let accepted = if acceptable_letters.is_null() { false } else { c_bytes(acceptable_letters).contains(&(ch as u8)) }; let result = if accepted { 0 } else { 1 }; if result != 0 { display_c_string(abort_msg); display(" \n"); } let mut next = ch; while next != libc::EOF && next != b'\n' as c_int { next = libc::getchar(); } result } #[no_mangle] pub extern "C" fn UTIL_traceFileStat() { TRACE_FILE_STAT.store(true, Ordering::Relaxed); } #[no_mangle] pub unsafe extern "C" fn UTIL_isConsole(file: *mut libc::FILE) -> c_int { if file.is_null() { return 0; } let fd = libc::fileno(file); trace_call(&format!("UTIL_isConsole({fd})")); let ret = if fd == 0 && FAKE_STDIN_IS_CONSOLE.load(Ordering::Relaxed) || fd == 1 && FAKE_STDOUT_IS_CONSOLE.load(Ordering::Relaxed) || fd == 2 && FAKE_STDERR_IS_CONSOLE.load(Ordering::Relaxed) { 1 } else if fd < 0 { 0 } else { libc::isatty(fd) }; trace_return(ret); ret } #[no_mangle] pub extern "C" fn UTIL_fakeStdinIsConsole() { FAKE_STDIN_IS_CONSOLE.store(true, Ordering::Relaxed); } #[no_mangle] pub extern "C" fn UTIL_fakeStdoutIsConsole() { FAKE_STDOUT_IS_CONSOLE.store(true, Ordering::Relaxed); } #[no_mangle] pub extern "C" fn UTIL_fakeStderrIsConsole() { FAKE_STDERR_IS_CONSOLE.store(true, Ordering::Relaxed); } fn suffix_ptr(suffix: &'static [u8]) -> *const c_char { suffix.as_ptr().cast::() } #[no_mangle] pub extern "C" fn UTIL_makeHumanReadableSize(size: u64) -> UTIL_HumanReadableSize_t { let result; if display_level() > 3 { if size >= 1u64 << 53 { result = UTIL_HumanReadableSize_t { value: size as f64 / (1u64 << 20) as f64, precision: 2, suffix: suffix_ptr(SUFFIX_MIB), }; } else { result = UTIL_HumanReadableSize_t { value: size as f64, precision: 0, suffix: suffix_ptr(SUFFIX_B), }; } } else { let (value, suffix) = if size >= 1u64 << 60 { (size as f64 / (1u64 << 60) as f64, SUFFIX_EIB) } else if size >= 1u64 << 50 { (size as f64 / (1u64 << 50) as f64, SUFFIX_PIB) } else if size >= 1u64 << 40 { (size as f64 / (1u64 << 40) as f64, SUFFIX_TIB) } else if size >= 1u64 << 30 { (size as f64 / (1u64 << 30) as f64, SUFFIX_GIB) } else if size >= 1u64 << 20 { (size as f64 / (1u64 << 20) as f64, SUFFIX_MIB) } else if size >= 1u64 << 10 { (size as f64 / (1u64 << 10) as f64, SUFFIX_KIB) } else { (size as f64, SUFFIX_B) }; let precision = if value >= 100.0 || value as u64 == size { 0 } else if value >= 10.0 { 1 } else if value > 1.0 { 2 } else { 3 }; result = UTIL_HumanReadableSize_t { value, precision, suffix: suffix_ptr(suffix), }; } result } #[no_mangle] pub unsafe extern "C" fn UTIL_compareStr(p1: *const c_void, p2: *const c_void) -> c_int { let left = *(p1.cast::<*const c_char>()); let right = *(p2.cast::<*const c_char>()); let left = c_bytes(left); let right = c_bytes(right); for (&a, &b) in left.iter().zip(right.iter()) { if a != b { return a as c_int - b as c_int; } } left.len() as c_int - right.len() as c_int } #[no_mangle] pub unsafe extern "C" fn UTIL_getFileExtension(infilename: *const c_char) -> *const c_char { let bytes = c_bytes(infilename); let Some(position) = bytes.iter().rposition(|&byte| byte == b'.') else { return EMPTY_EXTENSION.as_ptr().cast::(); }; if position == 0 { EMPTY_EXTENSION.as_ptr().cast::() } else { infilename.add(position) } } #[no_mangle] pub unsafe extern "C" fn UTIL_isCompressedFile( input_name: *const c_char, extension_list: *const *const c_char, ) -> c_int { let extension = CStr::from_ptr(UTIL_getFileExtension(input_name)).to_bytes(); if extension_list.is_null() { return 0; } let mut current = extension_list; loop { let candidate = *current; if candidate.is_null() { return 0; } if c_bytes(candidate) == extension { return 1; } current = current.add(1); } } unsafe fn pathname_has_two_dots(pathname: &[u8]) -> bool { let separator = path_separator(); for (index, pair) in pathname.windows(2).enumerate() { if pair != b".." { continue; } let left_boundary = index == 0 || pathname[index - 1] == separator; let right_index = index + 2; let right_boundary = right_index == pathname.len() || pathname[right_index] == separator; if left_boundary && right_boundary { return true; } } false } fn trim_path(pathname: &[u8]) -> &[u8] { let separator = path_separator(); let mut path = pathname; if path.first() == Some(&separator) { path = &path[1..]; } if path.len() >= 2 && path[0] == b'.' && path[1] == separator { path = &path[2..]; } path } fn join_two_dirs(dir1: &[u8], dir2: &[u8]) -> Vec { let separator = path_separator(); let mut result = Vec::with_capacity(dir1.len() + dir2.len() + 1); result.extend_from_slice(dir1); if !dir1.is_empty() && dir1.last() != Some(&separator) { result.push(separator); } result.extend_from_slice(dir2); result } fn convert_pathname_to_dir_name(pathname: &mut Vec) { let separator = path_separator(); if let Some(position) = pathname.iter().rposition(|&byte| byte == separator) { pathname.truncate(position); } else { pathname.clear(); pathname.push(b'.'); } } unsafe fn get_dir_mode(dir_name: &[u8]) -> u32 { let path = c_path(dir_name); let mut statbuf = MaybeUninit::::uninit(); if UTIL_stat(path.as_ptr(), statbuf.as_mut_ptr()) == 0 { if display_level() >= 1 { eprintln!( "zstd: failed to get DIR stats {}", String::from_utf8_lossy(dir_name) ); } return DIR_DEFAULT_MODE; } if UTIL_isDirectoryStat(statbuf.as_ptr()) == 0 { if display_level() >= 1 { eprintln!( "zstd: expected directory: {}", String::from_utf8_lossy(dir_name) ); } return DIR_DEFAULT_MODE; } stat_mode(statbuf.as_ptr()) as u32 } unsafe fn make_dir(dir: &[u8], mode: u32) -> c_int { let path = c_path(dir); #[cfg(unix)] let result = libc::mkdir(path.as_ptr(), mode as libc::mode_t); #[cfg(windows)] let result = { let _ = mode; libc::mkdir(path.as_ptr()) }; #[cfg(not(any(unix, windows)))] let result = { let _ = (path, mode); -1 }; if result != 0 { if std::io::Error::last_os_error().raw_os_error() == Some(libc::EEXIST) { return 0; } if display_level() >= 1 { eprintln!( "zstd: failed to create DIR {}: {}", String::from_utf8_lossy(dir), std::io::Error::last_os_error() ); } } result } unsafe fn mirror_src_dir(src_dir_name: &[u8], out_dir_name: &[u8]) -> c_int { let new_dir = join_two_dirs(out_dir_name, trim_path(src_dir_name)); let mode = get_dir_mode(src_dir_name); make_dir(&new_dir, mode) } fn mirror_src_dir_recursive(src_dir_name: &[u8], out_dir_name: &[u8]) { let separator = path_separator(); let start = if src_dir_name.len() >= 2 && src_dir_name[0] == b'.' && src_dir_name[1] == separator { 2 } else { 0 }; let mut previous = start; for (position, &byte) in src_dir_name.iter().enumerate().skip(start) { if byte != separator { continue; } if position != previous { unsafe { let _ = mirror_src_dir(&src_dir_name[..position], out_dir_name); } } previous = position + 1; } unsafe { let _ = mirror_src_dir(src_dir_name, out_dir_name); } } fn first_is_parent_or_same_dir(first: &[u8], second: &[u8]) -> bool { let separator = path_separator(); first.len() <= second.len() && (second.get(first.len()) == Some(&separator) || second.get(first.len()).is_none()) && second.starts_with(first) } #[no_mangle] pub unsafe extern "C" fn UTIL_createMirroredDestDirName( src_file_name: *const c_char, out_dir_root_name: *const c_char, ) -> *mut c_char { let src = c_bytes(src_file_name); if pathname_has_two_dots(src) { return ptr::null_mut(); } let mut pathname = join_two_dirs(c_bytes(out_dir_root_name), trim_path(src)); convert_pathname_to_dir_name(&mut pathname); let result = malloc_bytes(pathname.len() + 1).cast::(); ptr::copy_nonoverlapping(pathname.as_ptr().cast::(), result, pathname.len()); *result.add(pathname.len()) = 0; result } #[no_mangle] pub unsafe extern "C" fn UTIL_mirrorSourceFilesDirectories( file_names: *const *const c_char, nb_files: c_uint, out_dir_name: *const c_char, ) { let out_dir = c_bytes(out_dir_name).to_vec(); let mut source_dirs = Vec::new(); for index in 0..nb_files as usize { let source = c_bytes(*file_names.add(index)); if !pathname_has_two_dots(source) { let mut source_dir = source.to_vec(); convert_pathname_to_dir_name(&mut source_dir); source_dirs.push(source_dir); } } if source_dirs.is_empty() { return; } let _ = make_dir(&out_dir, DIR_DEFAULT_MODE); source_dirs.sort_by(|left, right| trim_path(left).cmp(trim_path(right))); let mut unique_dirs: Vec<&[u8]> = Vec::new(); unique_dirs.push(source_dirs[0].as_slice()); for index in 1..source_dirs.len() { let previous = trim_path(&source_dirs[index - 1]); let current = trim_path(&source_dirs[index]); if first_is_parent_or_same_dir(previous, current) { *unique_dirs.last_mut().unwrap() = source_dirs[index].as_slice(); } else { unique_dirs.push(source_dirs[index].as_slice()); } } for source_dir in unique_dirs { mirror_src_dir_recursive(source_dir, &out_dir); } } #[no_mangle] pub unsafe extern "C" fn UTIL_createFileNamesTable_fromFileName( input_file_name: *const c_char, ) -> *mut FileNamesTable { let input_name = c_bytes(input_file_name); let path = path_from_bytes(input_name); let c_input_name = c_path(input_name); let mut statbuf = MaybeUninit::::uninit(); if UTIL_stat(c_input_name.as_ptr(), statbuf.as_mut_ptr()) == 0 || UTIL_isRegularFileStat(statbuf.as_ptr()) == 0 { return ptr::null_mut(); } let file_size = UTIL_getFileSizeStat(statbuf.as_ptr()); if file_size > MAX_FILE_OF_FILE_NAMES_SIZE { return ptr::null_mut(); } let data = match fs::read(path) { Ok(data) => data, Err(error) => { if display_level() >= 1 { eprintln!("zstd:util:readLinesFromFile: {error}"); } return ptr::null_mut(); } }; if data.is_empty() { return ptr::null_mut(); } let line_count = data.iter().filter(|&&byte| byte == b'\n').count() + usize::from(data.last() != Some(&b'\n')); let buffer = malloc_bytes(data.len() + 1); ptr::copy_nonoverlapping(data.as_ptr(), buffer, data.len()); for index in 0..data.len() { if *buffer.add(index) == b'\n' { *buffer.add(index) = 0; } } *buffer.add(data.len()) = 0; make_table_from_buffer(buffer, data.len() + 1, line_count, line_count) } #[no_mangle] pub unsafe extern "C" fn UTIL_assembleFileNamesTable( filenames: *mut *const c_char, table_size: usize, buf: *mut c_char, ) -> *mut FileNamesTable { make_table(filenames, table_size, table_size, buf) } #[no_mangle] pub unsafe extern "C" fn UTIL_freeFileNamesTable(table: *mut FileNamesTable) { if table.is_null() { return; } free_ptr((*table).fileNames); free_ptr((*table).buf); free_ptr(table); } #[no_mangle] pub unsafe extern "C" fn UTIL_allocateFileNamesTable(table_size: usize) -> *mut FileNamesTable { let names = malloc_array::<*const c_char>(table_size); if names.is_null() { return ptr::null_mut(); } make_table(names, 0, table_size, ptr::null_mut()) } #[no_mangle] pub unsafe extern "C" fn UTIL_searchFileNamesTable( table: *mut FileNamesTable, name: *const c_char, ) -> c_int { for (index, &candidate) in table_entries(table).iter().enumerate() { if !candidate.is_null() && c_bytes(candidate) == c_bytes(name) { return index as c_int; } } -1 } #[no_mangle] pub unsafe extern "C" fn UTIL_refFilename(table: *mut FileNamesTable, filename: *const c_char) { if table.is_null() || (*table).tableSize >= (*table).tableCapacity { std::process::abort(); } *(*table).fileNames.add((*table).tableSize) = filename; (*table).tableSize += 1; } #[no_mangle] pub unsafe extern "C" fn UTIL_mergeFileNamesTable( table1: *mut FileNamesTable, table2: *mut FileNamesTable, ) -> *mut FileNamesTable { let total_size = table_names_size(table1).saturating_add(table_names_size(table2)); let new_size = (*table1).tableSize.saturating_add((*table2).tableSize); let buffer = malloc_bytes(total_size.max(1)); ptr::write_bytes(buffer, 0, total_size.max(1)); let names = malloc_bytes(new_size.saturating_mul(size_of::<*const c_char>()).max(1)) .cast::<*const c_char>(); ptr::write_bytes( names.cast::(), 0, new_size.saturating_mul(size_of::<*const c_char>()), ); let mut new_index = 0usize; let mut position = 0usize; for source in [table1, table2] { for &name in table_entries(source) { if name.is_null() || position >= total_size { break; } let name_bytes = c_bytes(name); ptr::copy_nonoverlapping(name_bytes.as_ptr(), buffer.add(position), name_bytes.len()); *names.add(new_index) = buffer.add(position).cast::(); position += name_bytes.len() + 1; new_index += 1; } } let result = make_table(names, new_index, 0, buffer.cast::()); UTIL_freeFileNamesTable(table1); UTIL_freeFileNamesTable(table2); result } fn append_name(buffer: &mut Vec, name: &[u8]) { buffer.extend_from_slice(name); buffer.push(0); } fn join_entry_path(dir_name: &[u8], entry_name: &[u8]) -> Vec { let mut path = Vec::with_capacity(dir_name.len() + entry_name.len() + 1); path.extend_from_slice(dir_name); path.push(path_separator()); path.extend_from_slice(entry_name); path } unsafe fn is_directory_bytes(name: &[u8]) -> bool { let path = c_path(name); let mut statbuf = MaybeUninit::::uninit(); UTIL_stat(path.as_ptr(), statbuf.as_mut_ptr()) != 0 && UTIL_isDirectoryStat(statbuf.as_ptr()) != 0 } unsafe fn is_link_bytes(name: &[u8]) -> bool { #[cfg(unix)] { let path = c_path(name); let mut statbuf = MaybeUninit::::uninit(); libc::lstat(path.as_ptr(), statbuf.as_mut_ptr()) == 0 && mode_is(statbuf.as_ptr(), libc::S_IFLNK) } #[cfg(not(unix))] { let _ = name; false } } fn walk_directory( dir_name: &[u8], buffer: &mut Vec, nb_files: &mut usize, follow_links: bool, ) -> Result<(), ()> { let entries = match fs::read_dir(path_from_bytes(dir_name)) { Ok(entries) => entries, Err(error) => { if display_level() >= 1 { eprintln!( "Cannot open directory '{}': {error}", String::from_utf8_lossy(dir_name) ); } return Ok(()); } }; for entry in entries { let entry = entry.map_err(|_| ())?; let entry_name = os_string_bytes(entry.file_name()); if entry_name == b"." || entry_name == b".." { continue; } let path = join_entry_path(dir_name, &entry_name); if !follow_links && unsafe { is_link_bytes(&path) } { if display_level() >= 2 { eprintln!( "Warning : {} is a symbolic link, ignoring", String::from_utf8_lossy(&path) ); } continue; } if unsafe { is_directory_bytes(&path) } { walk_directory(&path, buffer, nb_files, follow_links)?; } else { append_name(buffer, &path); *nb_files += 1; } } Ok(()) } #[no_mangle] pub unsafe extern "C" fn UTIL_createExpandedFNT( input_names: *const *const c_char, nb_input_names: usize, follow_links: c_int, ) -> *mut FileNamesTable { let mut buffer = Vec::with_capacity(LIST_SIZE_INCREASE); let mut nb_files = 0usize; for index in 0..nb_input_names { let input = *input_names.add(index); let input_bytes = c_bytes(input); if !is_directory_bytes(input_bytes) { append_name(&mut buffer, input_bytes); nb_files += 1; } else if walk_directory(input_bytes, &mut buffer, &mut nb_files, follow_links != 0) .is_err() { return ptr::null_mut(); } } let table_capacity = nb_files.saturating_add(1); let allocated_len = buffer.len().max(LIST_SIZE_INCREASE); let output_buffer = malloc_bytes(allocated_len); if !buffer.is_empty() { ptr::copy_nonoverlapping(buffer.as_ptr(), output_buffer, buffer.len()); } make_table_from_buffer(output_buffer, buffer.len(), nb_files, table_capacity) } #[no_mangle] pub unsafe extern "C" fn UTIL_expandFNT(table: *mut *mut FileNamesTable, follow_links: c_int) { let old_table = *table; let new_table = UTIL_createExpandedFNT((*old_table).fileNames, (*old_table).tableSize, follow_links); if new_table.is_null() { std::process::abort(); } UTIL_freeFileNamesTable(old_table); *table = new_table; } #[no_mangle] pub unsafe extern "C" fn UTIL_createFNT_fromROTable( filenames: *const *const c_char, nb_filenames: usize, ) -> *mut FileNamesTable { let new_names = malloc_array::<*const c_char>(nb_filenames); if new_names.is_null() { return ptr::null_mut(); } ptr::copy_nonoverlapping(filenames, new_names, nb_filenames); UTIL_assembleFileNamesTable(new_names, nb_filenames, ptr::null_mut()) } #[cfg(windows)] #[no_mangle] pub extern "system" fn CountSetBits(bit_mask: usize) -> u32 { bit_mask.count_ones() } #[cfg(target_os = "linux")] fn count_cores_platform(logical: c_int) -> c_int { let online = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) }; let mut count = if online < 0 { 1 } else { online as c_int }; if logical != 0 { return count; } let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") else { return count; }; let mut siblings = 0; let mut cpu_cores = 0; for line in cpuinfo.lines() { if line.starts_with("siblings") { let Some((_, value)) = line.split_once(':') else { return count; }; let Ok(value) = value.trim().parse::() else { return count; }; siblings = value; } if line.starts_with("cpu cores") { let Some((_, value)) = line.split_once(':') else { return count; }; let Ok(value) = value.trim().parse::() else { return count; }; cpu_cores = value; } } if siblings > cpu_cores && cpu_cores > 0 { let ratio = siblings / cpu_cores; if ratio > 0 && count > ratio { count /= ratio; } } count } #[cfg(all(unix, not(target_os = "linux"), not(target_vendor = "apple")))] fn count_cores_platform(_logical: c_int) -> c_int { let count = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) }; if count < 0 { 1 } else { count as c_int } } #[cfg(target_vendor = "apple")] fn count_cores_platform(logical: c_int) -> c_int { let name = if logical != 0 { CString::new("hw.logicalcpu").unwrap() } else { CString::new("hw.physicalcpu").unwrap() }; let mut count = 0i32; let mut size = size_of::(); let result = unsafe { libc::sysctlbyname( name.as_ptr(), (&mut count as *mut i32).cast::(), &mut size, ptr::null_mut(), 0, ) }; if result != 0 { if std::io::Error::last_os_error().raw_os_error() == Some(libc::ENOENT) { 1 } else { std::process::abort() } } else { count } } #[cfg(windows)] fn count_cores_platform(_logical: c_int) -> c_int { std::thread::available_parallelism() .map_or(1, |count| count.get().min(c_int::MAX as usize) as c_int) } #[cfg(not(any(unix, windows)))] fn count_cores_platform(_logical: c_int) -> c_int { 1 } #[no_mangle] pub extern "C" fn UTIL_countCores(logical: c_int) -> c_int { *CORE_COUNT.get_or_init(|| count_cores_platform(logical)) } #[no_mangle] pub extern "C" fn UTIL_countPhysicalCores() -> c_int { UTIL_countCores(0) } #[no_mangle] pub extern "C" fn UTIL_countLogicalCores() -> c_int { UTIL_countCores(1) } #[cfg(test)] mod tests { use super::*; use std::ffi::CString; use std::fs::{self, File}; use std::io::Write; #[test] fn public_struct_layout_is_pointer_and_c_int_stable() { assert_eq!( std::mem::align_of::(), std::mem::align_of::() ); assert_eq!(offset_of!(UTIL_HumanReadableSize_t, value), 0); assert_eq!( offset_of!(UTIL_HumanReadableSize_t, precision), size_of::() ); assert_eq!(offset_of!(FileNamesTable, fileNames), 0); assert_eq!(offset_of!(FileNamesTable, buf), size_of::<*const c_char>()); assert_eq!( offset_of!(FileNamesTable, tableSize), 2 * size_of::<*const c_char>() ); assert_eq!( size_of::(), 2 * size_of::<*const c_char>() + 2 * size_of::() ); } #[test] fn stat_extension_and_size_helpers_follow_c_contract() { let root = std::env::temp_dir().join(format!("zstd-util-{}", std::process::id())); let _ = fs::remove_dir_all(&root); fs::create_dir(&root).unwrap(); let file_path = root.join("sample.txt"); fs::write(&file_path, b"hello").unwrap(); let path = CString::new(file_path.to_string_lossy().as_bytes()).unwrap(); let mut statbuf = MaybeUninit::::uninit(); unsafe { assert_eq!(UTIL_stat(path.as_ptr(), statbuf.as_mut_ptr()), 1); assert_eq!(UTIL_isRegularFileStat(statbuf.as_ptr()), 1); assert_eq!(UTIL_getFileSizeStat(statbuf.as_ptr()), 5); assert_eq!( CStr::from_ptr(UTIL_getFileExtension(path.as_ptr())).to_bytes(), b".txt" ); let extension = CString::new(".txt").unwrap(); let extensions = [extension.as_ptr(), ptr::null()]; assert_eq!(UTIL_isCompressedFile(path.as_ptr(), extensions.as_ptr()), 1); } fs::remove_dir_all(root).unwrap(); } #[test] fn filename_tables_own_c_allocations_and_merge_in_order() { let first = CString::new("first").unwrap(); let second = CString::new("second").unwrap(); let names = [first.as_ptr(), second.as_ptr()]; unsafe { let table = UTIL_createFNT_fromROTable(names.as_ptr(), names.len()); assert!(!table.is_null()); assert_eq!((*table).tableSize, 2); assert_eq!(UTIL_searchFileNamesTable(table, second.as_ptr()), 1); let empty = UTIL_allocateFileNamesTable(1); assert!(!empty.is_null()); UTIL_refFilename(empty, first.as_ptr()); let merged = UTIL_mergeFileNamesTable(table, empty); assert_eq!((*merged).tableSize, 3); assert_eq!( CStr::from_ptr(*(*merged).fileNames.add(2)).to_bytes(), b"first" ); UTIL_freeFileNamesTable(merged); } } #[test] fn file_list_loading_and_directory_expansion_return_nul_tables() { let mut root = std::env::temp_dir(); root.push(format!("zstd-util-list-{}", std::process::id())); let _ = fs::remove_dir_all(&root); fs::create_dir(&root).unwrap(); let file_list = root.join("list"); let nested = root.join("nested"); fs::create_dir(&nested).unwrap(); File::create(nested.join("one")).unwrap(); let mut list = File::create(&file_list).unwrap(); writeln!(list, "alpha").unwrap(); writeln!(list, "beta").unwrap(); drop(list); let list_name = CString::new(file_list.to_string_lossy().as_bytes()).unwrap(); let nested_name = CString::new(nested.to_string_lossy().as_bytes()).unwrap(); unsafe { let table = UTIL_createFileNamesTable_fromFileName(list_name.as_ptr()); assert!(!table.is_null()); assert_eq!((*table).tableSize, 2); assert_eq!(CStr::from_ptr(*(*table).fileNames).to_bytes(), b"alpha"); UTIL_freeFileNamesTable(table); let input = [nested_name.as_ptr()]; let expanded = UTIL_createExpandedFNT(input.as_ptr(), 1, 1); assert!(!expanded.is_null()); assert_eq!((*expanded).tableSize, 1); assert!(CStr::from_ptr(*(*expanded).fileNames) .to_bytes() .ends_with(b"/one")); UTIL_freeFileNamesTable(expanded); } fs::remove_dir_all(root).unwrap(); } #[test] fn mirrored_directory_name_rejects_parent_components() { let source = CString::new("a/../b/file").unwrap(); let root = CString::new("out").unwrap(); unsafe { assert!(UTIL_createMirroredDestDirName(source.as_ptr(), root.as_ptr()).is_null()); } let source = CString::new("a/b/file.txt").unwrap(); unsafe { let name = UTIL_createMirroredDestDirName(source.as_ptr(), root.as_ptr()); assert_eq!(CStr::from_ptr(name).to_bytes(), b"out/a/b"); free_ptr(name); } } #[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 { g_utilDisplayLevel = 0; } let size = UTIL_makeHumanReadableSize(1536); assert_eq!(size.precision, 2); unsafe { assert_eq!(CStr::from_ptr(size.suffix).to_bytes(), b" KiB"); } assert_eq!(size.value, 1.5); } #[test] fn core_count_helpers_never_report_zero() { assert!(UTIL_countPhysicalCores() > 0); assert!(UTIL_countLogicalCores() > 0); } }