diff --git a/Cargo.lock b/Cargo.lock index 1a976a10530..b71facc66d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3320,6 +3320,7 @@ dependencies = [ "codspeed-divan-compat", "fluent", "glob", + "libc", "tempfile", "thiserror 2.0.18", "uucore", diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 192ec9dea15..550cce8f131 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -30,6 +30,7 @@ uucore = { workspace = true, features = [ ] } thiserror = { workspace = true } fluent = { workspace = true } +libc = { workspace = true } [target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] uucore = { workspace = true, features = ["safe-traversal"] } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a70a0269ce1..3c3ff0f4f01 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -3,10 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // -// spell-checker:ignore fstatat openat dirfd +// spell-checker:ignore dedupe dirfd fiemap fstatat openat reflinks -use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; -use glob::Pattern; use std::collections::HashSet; use std::env; use std::ffi::{OsStr, OsString}; @@ -20,27 +18,35 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::mpsc; use std::thread; + +use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; +use glob::Pattern; use thiserror::Error; +#[cfg(windows)] +use windows_sys::Win32::{ + Foundation::HANDLE, + Storage::FileSystem::{ + FILE_ID_128, FILE_ID_INFO, FILE_STANDARD_INFO, FileIdInfo, FileStandardInfo, + GetFileInformationByHandleEx, + }, +}; + use uucore::display::{Quotable, print_verbatim}; use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code}; use uucore::fsext::{MetadataTimeField, metadata_get_time}; use uucore::line_ending::LineEnding; -#[cfg(all(unix, not(target_os = "redox")))] -use uucore::safe_traversal::DirFd; -use uucore::translate; - use uucore::parser::parse_glob; use uucore::parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; +#[cfg(all(unix, not(target_os = "redox")))] +use uucore::safe_traversal::DirFd; use uucore::time::{FormatSystemTimeFallback, format, format_system_time}; +use uucore::translate; use uucore::{format_usage, show, show_error, show_warning}; -#[cfg(windows)] -use windows_sys::Win32::Foundation::HANDLE; -#[cfg(windows)] -use windows_sys::Win32::Storage::FileSystem::{ - FILE_ID_128, FILE_ID_INFO, FILE_STANDARD_INFO, FileIdInfo, FileStandardInfo, - GetFileInformationByHandleEx, -}; + +pub mod fiemap; +#[cfg(target_os = "linux")] +use crate::fiemap::{FIEMAP_EXTENT_ENCODED, FIEMAP_EXTENT_SHARED, walk_fiemap_extents}; mod options { pub const HELP: &str = "help"; @@ -73,12 +79,15 @@ mod options { pub const FILE: &str = "FILE"; } +const POSIX_BLOCK_SIZE: u64 = 512; + struct TraversalOptions { all: bool, separate_dirs: bool, one_file_system: bool, dereference: Deref, count_links: bool, + dedupe_reflinks: bool, verbose: bool, excludes: Vec, } @@ -117,6 +126,13 @@ struct FileInfo { dev_id: u64, } +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct SharedExtentKey { + dev_id: u64, + physical: u64, + length: u64, +} + struct Stat { path: PathBuf, size: u64, @@ -171,9 +187,10 @@ impl Stat { // Create file info from the safe metadata let file_info = safe_metadata.file_info(); + #[allow(clippy::unnecessary_cast)] let file_info_option = Some(FileInfo { file_id: file_info.inode() as u128, - dev_id: file_info.device(), + dev_id: file_info.device() as u64, }); let blocks = safe_metadata.blocks(); @@ -270,13 +287,66 @@ fn get_file_info(path: &Path, _metadata: &Metadata) -> Option { result } +#[cfg(target_os = "linux")] +fn adjust_blocks_for_reflinks( + path: &Path, + dev_id: u64, + blocks: u64, + shared_extents: &mut HashSet, +) -> u64 { + if blocks == 0 { + return blocks; + } + + let Ok(file) = File::open(path) else { + return blocks; + }; + + let mut dedup_bytes = 0_u64; + + if walk_fiemap_extents(&file, 0, |extent| { + if (extent.fe_flags & FIEMAP_EXTENT_SHARED) != 0 + && (extent.fe_flags & FIEMAP_EXTENT_ENCODED) == 0 + && extent.fe_physical != 0 + { + let key = SharedExtentKey { + dev_id, + physical: extent.fe_physical, + length: extent.fe_length, + }; + + if !shared_extents.insert(key) { + dedup_bytes = dedup_bytes.saturating_add(extent.fe_length); + } + } + + true + }) + .is_err() + { + return blocks; + } + + let dedup_blocks = dedup_bytes / POSIX_BLOCK_SIZE; + blocks.saturating_sub(dedup_blocks) +} + +#[cfg(not(target_os = "linux"))] +fn adjust_blocks_for_reflinks( + _path: &Path, + _dev_id: u64, + blocks: u64, + _shared_extents: &mut HashSet, +) -> u64 { + blocks +} + fn block_size_from_env() -> Option { for env_var in ["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { if let Ok(env_size) = env::var(env_var) { return parse_size_non_zero_u64(&env_size).ok(); } } - None } @@ -287,7 +357,7 @@ fn read_block_size(s: Option<&str>) -> UResult { } else if let Some(bytes) = block_size_from_env() { Ok(bytes) } else if env::var("POSIXLY_CORRECT").is_ok() { - Ok(512) + Ok(POSIX_BLOCK_SIZE) } else { Ok(1024) } @@ -301,6 +371,7 @@ fn safe_du( options: &TraversalOptions, depth: usize, seen_inodes: &mut HashSet, + shared_extents: &mut HashSet, print_tx: &mpsc::Sender>, parent_fd: Option<&DirFd>, ) -> Result>>> { @@ -312,9 +383,10 @@ fn safe_du( Ok(safe_metadata) => { // Create Stat from safe metadata let file_info = safe_metadata.file_info(); + #[allow(clippy::unnecessary_cast)] let file_info_option = Some(FileInfo { file_id: file_info.inode() as u128, - dev_id: file_info.device(), + dev_id: file_info.device() as u64, }); let blocks = safe_metadata.blocks(); @@ -391,6 +463,11 @@ fn safe_du( } }; if !my_stat.metadata.is_dir() { + if options.dedupe_reflinks { + let dev_id = my_stat.inode.map_or(0, |inode| inode.dev_id); + my_stat.blocks = + adjust_blocks_for_reflinks(&my_stat.path, dev_id, my_stat.blocks, shared_extents); + } return Ok(my_stat); } @@ -439,6 +516,8 @@ fn safe_du( const S_IFMT: u32 = 0o170_000; const S_IFDIR: u32 = 0o040_000; const S_IFLNK: u32 = 0o120_000; + const S_IFREG: u32 = 0o100_000; + #[allow(clippy::unnecessary_cast)] let is_symlink = (lstat.st_mode as u32 & S_IFMT) == S_IFLNK; @@ -453,6 +532,8 @@ fn safe_du( #[allow(clippy::unnecessary_cast)] let is_dir = (lstat.st_mode as u32 & S_IFMT) == S_IFDIR; + #[allow(clippy::unnecessary_cast)] + let is_regular = (lstat.st_mode as u32 & S_IFMT) == S_IFREG; let entry_stat = lstat; #[allow(clippy::unnecessary_cast)] @@ -463,7 +544,7 @@ fn safe_du( // For safe traversal, we need to handle stats differently // We can't use std::fs::Metadata since that requires the full path - let this_stat = if is_dir { + let mut this_stat = if is_dir { // For directories, recurse using safe_du Stat { path: entry_path.clone(), @@ -513,6 +594,15 @@ fn safe_du( seen_inodes.insert(inode); } + #[allow(clippy::unnecessary_cast)] + if options.dedupe_reflinks && is_regular { + let dev_id = this_stat + .inode + .map_or(entry_stat.st_dev as u64, |inode| inode.dev_id); + this_stat.blocks = + adjust_blocks_for_reflinks(&entry_path, dev_id, this_stat.blocks, shared_extents); + } + // Process directories recursively if is_dir { if options.one_file_system { @@ -528,6 +618,7 @@ fn safe_du( options, depth + 1, seen_inodes, + shared_extents, print_tx, Some(&dir_fd), )?; @@ -561,12 +652,13 @@ fn safe_du( // Only used on non-Linux platforms // Regular traversal using std::fs // Used on non-Linux platforms and as fallback for symlinks on Linux -#[allow(clippy::cognitive_complexity)] +#[allow(clippy::cognitive_complexity, clippy::too_many_arguments)] fn du_regular( mut my_stat: Stat, options: &TraversalOptions, depth: usize, seen_inodes: &mut HashSet, + shared_extents: &mut HashSet, print_tx: &mpsc::Sender>, ancestors: Option<&mut HashSet>, symlink_depth: Option, @@ -577,6 +669,15 @@ fn du_regular( // Maximum symlink depth to prevent infinite loops const MAX_SYMLINK_DEPTH: usize = 40; + if !my_stat.metadata.is_dir() { + if options.dedupe_reflinks { + let dev_id = my_stat.inode.map_or(0, |inode| inode.dev_id); + my_stat.blocks = + adjust_blocks_for_reflinks(&my_stat.path, dev_id, my_stat.blocks, shared_extents); + } + return Ok(my_stat); + } + // Add current directory to ancestors if it's a directory let my_inode = if my_stat.metadata.is_dir() { my_stat.inode @@ -627,7 +728,7 @@ fn du_regular( } match Stat::new(&entry_path, Some(&entry), options) { - Ok(this_stat) => { + Ok(mut this_stat) => { // Check if symlink with -L points to an ancestor (cycle detection) if is_symlink && options.dereference == Deref::All @@ -687,6 +788,7 @@ fn du_regular( options, depth + 1, seen_inodes, + shared_extents, print_tx, Some(ancestors), Some(current_symlink_depth), @@ -702,9 +804,20 @@ fn du_regular( depth: depth + 1, }))?; } else { + if options.dedupe_reflinks { + let dev_id = this_stat.inode.map_or(0, |inode| inode.dev_id); + this_stat.blocks = adjust_blocks_for_reflinks( + &this_stat.path, + dev_id, + this_stat.blocks, + shared_extents, + ); + } + my_stat.size += this_stat.size; my_stat.blocks += this_stat.blocks; my_stat.inodes += 1; + if options.all { print_tx.send(Ok(StatPrintInfo { stat: this_stat, @@ -810,9 +923,10 @@ impl StatPrinter { } else if self.apparent_size { stat.size } else { - // The st_blocks field indicates the number of blocks allocated to the file, 512-byte units. + // The st_blocks field indicates the number of blocks allocated to the file, + // in POSIX_BLOCK_SIZE-byte units. // See: http://linux.die.net/man/2/stat - stat.blocks * 512 + stat.blocks * POSIX_BLOCK_SIZE } } @@ -1023,6 +1137,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { SizeFormat::BlockSize(block_size) }; + let inodes = matches.get_flag(options::INODES); + let apparent_size = + matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES); + let traversal_options = TraversalOptions { all: matches.get_flag(options::ALL), separate_dirs: matches.get_flag(options::SEPARATE_DIRS), @@ -1036,6 +1154,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Deref::None }, count_links, + dedupe_reflinks: !count_links && !apparent_size && !inodes, verbose: matches.get_flag(options::VERBOSE), excludes: build_exclude_patterns(&matches)?, }; @@ -1051,7 +1170,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { size_format, summarize, total: matches.get_flag(options::TOTAL), - inodes: matches.get_flag(options::INODES), + inodes, threshold: matches .get_one::(options::THRESHOLD) .map(|s| { @@ -1060,16 +1179,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }) }) .transpose()?, - apparent_size: matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES), + apparent_size, time, time_format, line_ending: LineEnding::from_zero_flag(matches.get_flag(options::NULL)), total_text: translate!("du-total"), }; - if stat_printer.inodes - && (matches.get_flag(options::APPARENT_SIZE) || matches.get_flag(options::BYTES)) - { + if inodes && apparent_size { show_warning!( "{}", translate!("du-warning-apparent-size-ineffective-with-inodes") @@ -1082,6 +1199,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Check existence of path provided in argument let mut seen_inodes: HashSet = HashSet::new(); + let mut seen_shared_extents: HashSet = HashSet::new(); 'loop_file: for path in files { // Skip if we don't want to ignore anything @@ -1126,6 +1244,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &traversal_options, 0, &mut seen_inodes, + &mut seen_shared_extents, &print_tx, None, ) { @@ -1160,6 +1279,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &traversal_options, 0, &mut seen_inodes, + &mut seen_shared_extents, &print_tx, None, None, diff --git a/src/uu/du/src/fiemap.rs b/src/uu/du/src/fiemap.rs new file mode 100644 index 00000000000..bc9b60dba48 --- /dev/null +++ b/src/uu/du/src/fiemap.rs @@ -0,0 +1,112 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore fiemap iowr + +#![cfg(target_os = "linux")] + +use std::fs::File; +use std::io; +use std::os::unix::io::AsRawFd; + +#[derive(Default)] +#[repr(C)] +pub struct Fiemap { + pub fm_start: u64, + pub fm_length: u64, + pub fm_flags: u32, + pub fm_mapped_extents: u32, + pub fm_extent_count: u32, + fm_reserved: u32, +} + +#[derive(Default)] +#[repr(C)] +pub struct FiemapExtent { + pub fe_logical: u64, + pub fe_physical: u64, + pub fe_length: u64, + fe_reserved64: [u64; 2], + pub fe_flags: u32, + fe_reserved: [u32; 3], +} + +pub const FIEMAP_EXTENT_COUNT: usize = 128; + +#[repr(C)] +pub struct FiemapBuffer { + pub header: Fiemap, + pub extents: [FiemapExtent; FIEMAP_EXTENT_COUNT], +} + +impl Default for FiemapBuffer { + fn default() -> Self { + Self { + header: Fiemap::default(), + extents: std::array::from_fn(|_| FiemapExtent::default()), + } + } +} + +impl FiemapBuffer { + pub fn new(offset: u64) -> Self { + let mut buffer = Self::default(); + buffer.header.fm_start = offset; + buffer.header.fm_length = u64::MAX; + buffer.header.fm_extent_count = FIEMAP_EXTENT_COUNT as u32; + buffer + } +} + +pub const FIEMAP_EXTENT_LAST: u32 = 0x00000001; +pub const FIEMAP_EXTENT_ENCODED: u32 = 0x00000008; +pub const FIEMAP_EXTENT_SHARED: u32 = 0x00002000; +pub const FS_IOC_FIEMAP: libc::Ioctl = libc::_IOWR::(b'f' as u32, 11); + +pub fn walk_fiemap_extents(file: &File, start_offset: u64, mut visit: F) -> io::Result<()> +where + F: FnMut(&FiemapExtent) -> bool, +{ + let mut offset = start_offset; + + loop { + let mut buffer = FiemapBuffer::new(offset); + + let result = unsafe { libc::ioctl(file.as_raw_fd(), FS_IOC_FIEMAP, &mut buffer.header) }; + if result != 0 { + return Err(io::Error::last_os_error()); + } + + let mapped = buffer.header.fm_mapped_extents as usize; + if mapped == 0 { + break; + } + + let mut last_end = offset; + let mut is_last = false; + for extent in &buffer.extents[..mapped] { + if extent.fe_length == 0 { + continue; + } + + last_end = extent.fe_logical.saturating_add(extent.fe_length); + if (extent.fe_flags & FIEMAP_EXTENT_LAST) != 0 { + is_last = true; + } + + if !visit(extent) { + return Ok(()); + } + } + + if is_last || last_end <= offset { + break; + } + + offset = last_end; + } + + Ok(()) +} diff --git a/src/uucore/src/lib/features/safe_traversal.rs b/src/uucore/src/lib/features/safe_traversal.rs index 3b1ac70678f..d7093228a30 100644 --- a/src/uucore/src/lib/features/safe_traversal.rs +++ b/src/uucore/src/lib/features/safe_traversal.rs @@ -20,6 +20,7 @@ use std::os::unix::ffi::OsStrExt; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}; use std::path::{Path, PathBuf}; +use libc::{dev_t, ino_t, stat}; use nix::dir::Dir; use nix::fcntl::{OFlag, openat}; use nix::libc; @@ -296,32 +297,31 @@ impl AsFd for DirFd { /// File information for tracking inodes #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct FileInfo { - pub dev: u64, - pub ino: u64, + pub dev: dev_t, + pub ino: ino_t, } impl FileInfo { - pub fn from_stat(stat: &libc::stat) -> Self { - // Allow unnecessary cast because st_dev and st_ino have different types on different platforms + pub fn from_stat(stat: &stat) -> Self { #[allow(clippy::unnecessary_cast)] Self { - dev: stat.st_dev as u64, - ino: stat.st_ino as u64, + dev: stat.st_dev as dev_t, + ino: stat.st_ino as ino_t, } } /// Create FileInfo from device and inode numbers - pub fn new(dev: u64, ino: u64) -> Self { + pub fn new(dev: dev_t, ino: ino_t) -> Self { Self { dev, ino } } /// Get the device number - pub fn device(&self) -> u64 { + pub fn device(&self) -> dev_t { self.dev } /// Get the inode number - pub fn inode(&self) -> u64 { + pub fn inode(&self) -> ino_t { self.ino } } @@ -389,12 +389,10 @@ impl Metadata { self.stat.st_mode as u32 } + // st_nlink type varies by platform (u16 on FreeBSD, u32/u64 on others) + #[allow(clippy::unnecessary_cast)] pub fn nlink(&self) -> u64 { - // st_nlink type varies by platform (u16 on FreeBSD, u32/u64 on others) - #[allow(clippy::unnecessary_cast)] - { - self.stat.st_nlink as u64 - } + self.stat.st_nlink as u64 } /// Compatibility methods to match std::fs::Metadata interface @@ -419,12 +417,10 @@ impl std::os::unix::fs::MetadataExt for Metadata { self.stat.st_dev as u64 } + // st_ino type varies by platform (u32 on FreeBSD, u64 on Linux) + #[allow(clippy::unnecessary_cast)] fn ino(&self) -> u64 { - // st_ino type varies by platform (u32 on FreeBSD, u64 on Linux) - #[allow(clippy::unnecessary_cast)] - { - self.stat.st_ino as u64 - } + self.stat.st_ino as u64 } // st_mode type varies by platform (u16 on macOS, u32 on Linux) @@ -433,12 +429,10 @@ impl std::os::unix::fs::MetadataExt for Metadata { self.stat.st_mode as u32 } + // st_nlink type varies by platform (u16 on FreeBSD, u32/u64 on others) + #[allow(clippy::unnecessary_cast)] fn nlink(&self) -> u64 { - // st_nlink type varies by platform (u16 on FreeBSD, u32/u64 on others) - #[allow(clippy::unnecessary_cast)] - { - self.stat.st_nlink as u64 - } + self.stat.st_nlink as u64 } fn uid(&self) -> u32 { @@ -703,7 +697,6 @@ mod tests { } #[test] - #[allow(clippy::unnecessary_cast)] fn test_file_info() { let temp_dir = TempDir::new().unwrap(); let file_path = temp_dir.path().join("test_file"); @@ -712,8 +705,8 @@ mod tests { let dir_fd = DirFd::open(temp_dir.path()).unwrap(); let stat = dir_fd.stat_at(OsStr::new("test_file"), true).unwrap(); let file_info = FileInfo::from_stat(&stat); - assert_eq!(file_info.device(), stat.st_dev as u64); - assert_eq!(file_info.inode(), stat.st_ino as u64); + assert_eq!(file_info.device(), stat.st_dev); + assert_eq!(file_info.inode(), stat.st_ino); } #[test] diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index c89cd46678c..700489654fa 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -4,10 +4,18 @@ // file that was distributed with this source code. // spell-checker:ignore (paths) atim sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink testfile1 testfile2 filelist fpath testdir testfile -// spell-checker:ignore selfref ELOOP smallfile +// spell-checker:ignore selfref ELOOP smallfile fiemap #[cfg(not(windows))] use regex::Regex; +#[cfg(target_os = "linux")] +use { + du::fiemap::{FIEMAP_EXTENT_ENCODED, FIEMAP_EXTENT_SHARED, walk_fiemap_extents}, + rand::rngs::StdRng, + rand::{RngCore, SeedableRng}, + std::fs::File, + std::path::Path, +}; #[cfg(not(target_os = "windows"))] use uutests::unwrap_or_return; @@ -532,6 +540,86 @@ fn du_hard_link(s: &str) { } } +#[cfg(target_os = "linux")] +fn reflink_extents_all_shared(path: &Path) -> bool { + let Ok(file) = File::open(path) else { + return false; + }; + let mut saw_extent = false; + let mut all_shared = true; + + let result = walk_fiemap_extents(&file, 0, |extent| { + saw_extent = true; + + if extent.fe_physical == 0 + || (extent.fe_flags & FIEMAP_EXTENT_ENCODED) != 0 + || (extent.fe_flags & FIEMAP_EXTENT_SHARED) == 0 + { + all_shared = false; + return false; + } + + true + }); + + if result.is_err() || !saw_extent { + return false; + } + + all_shared +} + +#[cfg(target_os = "linux")] +fn find_du_size(output: &str, file: &str) -> Option { + let prefixed = format!("./{file}"); + for line in output.lines() { + let mut parts = line.split_whitespace(); + let size = parts.next()?; + let path = parts.next()?; + if path == file || path == prefixed { + return size.parse().ok(); + } + } + None +} + +#[cfg(target_os = "linux")] +#[test] +fn test_du_reflink_dedup() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let file1 = "reflink_src"; + let file2 = "reflink_dst"; + + let file_size = 256 * 1024; + let mut data = vec![0_u8; file_size]; + let mut rng = StdRng::seed_from_u64(0x5eed); + rng.fill_bytes(&mut data); + at.write_bytes(file1, &data); + at.sync_file(file1); + + let cp_result = ts + .ccmd("cp") + .arg("--reflink=always") + .arg(file1) + .arg(file2) + .run(); + + if !cp_result.succeeded() { + // reflink not supported, skip this test + return; + } + + at.sync_file(file2); + assert!(reflink_extents_all_shared(&at.plus(file2))); + + let result = ts.ucmd().arg("--all").arg("--bytes").succeeds(); + let size1 = find_du_size(result.stdout_str(), file1).unwrap(); + let size2 = find_du_size(result.stdout_str(), file2).unwrap(); + assert_eq!(size1, file_size); + assert_eq!(size2, 0); +} + #[test] #[cfg(not(target_os = "openbsd"))] fn test_du_d_flag() { diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 0c4bd25535f..e48ab3cb3c6 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -1041,6 +1041,14 @@ impl AtPath { .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); } + pub fn sync_file(&self, name: &str) { + log_info("sync_file", self.plus_as_string(name)); + let file = File::open(self.plus(name)) + .unwrap_or_else(|e| panic!("Couldn't open {name} for sync: {e}")); + file.sync_all() + .unwrap_or_else(|e| panic!("Couldn't sync {name}: {e}")); + } + pub fn append(&self, name: impl AsRef, contents: &str) { let name = name.as_ref(); log_info("write(append)", self.plus_as_string(name));