diff --git a/Cargo.lock b/Cargo.lock index f7e1187b4..95635159c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1735,6 +1735,7 @@ dependencies = [ "time", "uucore", "uucore_procs", + "winapi 0.3.9", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9136b5d64..1562fcfb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ feat_common_core = [ "df", "dircolors", "dirname", + "du", "echo", "env", "expand", @@ -149,7 +150,6 @@ feat_require_unix = [ "chmod", "chown", "chroot", - "du", "groups", "hostid", "id", diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 912eef17e..bb46c299c 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -18,6 +18,7 @@ path = "src/du.rs" time = "0.1.40" uucore = { version=">=0.0.7", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } +winapi = { version="0.3", features=[] } [[bin]] name = "du" diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 4ed80f18b..2eb7bd658 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -15,9 +15,24 @@ use std::env; use std::fs; use std::io::{stderr, Result, Write}; use std::iter; +#[cfg(not(windows))] use std::os::unix::fs::MetadataExt; +#[cfg(windows)] +use std::os::windows::fs::MetadataExt; +#[cfg(windows)] +use std::os::windows::io::AsRawHandle; use std::path::PathBuf; use time::Timespec; +#[cfg(windows)] +use winapi::shared::minwindef::{DWORD, LPVOID}; +#[cfg(windows)] +use winapi::um::fileapi::{FILE_STANDARD_INFO, FILE_ID_INFO}; +#[cfg(windows)] +use winapi::um::minwinbase::{FileStandardInfo, FileIdInfo}; +#[cfg(windows)] +use winapi::um::winbase::GetFileInformationByHandleEx; +#[cfg(windows)] +use winapi::um::winnt::FILE_ID_128; const NAME: &str = "du"; const SUMMARY: &str = "estimate file space usage"; @@ -48,7 +63,7 @@ struct Stat { is_dir: bool, size: u64, blocks: u64, - inode: u64, + inode: Option, created: u64, accessed: u64, modified: u64, @@ -57,19 +72,106 @@ struct Stat { impl Stat { fn new(path: PathBuf) -> Result { let metadata = fs::symlink_metadata(&path)?; - Ok(Stat { + + #[cfg(not(windows))] + return Ok(Stat { path, is_dir: metadata.is_dir(), size: metadata.len(), blocks: metadata.blocks() as u64, - inode: metadata.ino() as u64, + inode: Some(metadata.ino() as u128), created: metadata.mtime() as u64, accessed: metadata.atime() as u64, modified: metadata.mtime() as u64, + }); + + #[cfg(windows)] + let size_on_disk = get_size_on_disk(&path); + #[cfg(windows)] + let inode = get_inode(&path); + #[cfg(windows)] + Ok(Stat { + path, + is_dir: metadata.is_dir(), + size: metadata.len(), + blocks: size_on_disk / 1024 * 2, + inode: inode, + created: windows_time_to_unix_time(metadata.creation_time()), + accessed: windows_time_to_unix_time(metadata.last_access_time()), + modified: windows_time_to_unix_time(metadata.last_write_time()), }) } } +#[cfg(windows)] +// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html#tymethod.creation_time +// "The returned 64-bit value [...] which represents the number of 100-nanosecond intervals since January 1, 1601 (UTC)." +fn windows_time_to_unix_time(win_time: u64) -> u64 { + win_time / 10_000 - 11_644_473_600_000 +} + +#[cfg(windows)] +fn get_size_on_disk(path: &PathBuf) -> u64 { + let mut size_on_disk = 0; + + // bind file so it stays in scope until end of function + // if it goes out of scope the handle below becomes invalid + let file = match fs::File::open(path) { + Ok(file) => file, + Err(_) => return size_on_disk, // opening directories will fail + }; + + let handle = file.as_raw_handle(); + + unsafe { + let mut file_info: FILE_STANDARD_INFO = core::mem::zeroed(); + let file_info_ptr: *mut FILE_STANDARD_INFO = &mut file_info; + + let success = GetFileInformationByHandleEx( + handle, + FileStandardInfo, + file_info_ptr as LPVOID, + std::mem::size_of::() as DWORD, + ); + + if success != 0 { + size_on_disk = *file_info.AllocationSize.QuadPart() as u64; + } + } + + size_on_disk +} + +#[cfg(windows)] +fn get_inode(path: &PathBuf) -> Option { + let mut inode = None; + + let file = match fs::File::open(path) { + Ok(file) => file, + Err(_) => return inode, + }; + + let handle = file.as_raw_handle(); + + unsafe { + let mut file_info: FILE_ID_INFO = core::mem::zeroed(); + let file_info_ptr: *mut FILE_ID_INFO = &mut file_info; + + let success = GetFileInformationByHandleEx( + handle, + FileIdInfo, + file_info_ptr as LPVOID, + std::mem::size_of::() as DWORD, + ); + + if success != 0 { + inode = Some(std::mem::transmute::(file_info.FileId)); + } + } + + inode +} + fn unit_string_to_number(s: &str) -> Option { let mut offset = 0; let mut s_chars = s.chars().rev(); @@ -137,7 +239,7 @@ fn du( mut my_stat: Stat, options: &Options, depth: usize, - inodes: &mut HashSet, + inodes: &mut HashSet, ) -> Box> { let mut stats = vec![]; let mut futures = vec![]; @@ -164,10 +266,13 @@ fn du( if this_stat.is_dir { futures.push(du(this_stat, options, depth + 1, inodes)); } else { - if inodes.contains(&this_stat.inode) { - continue; + if this_stat.inode.is_some() { + let inode = this_stat.inode.unwrap(); + if inodes.contains(&inode) { + continue; + } + inodes.insert(inode); } - inodes.insert(this_stat.inode); my_stat.size += this_stat.size; my_stat.blocks += this_stat.blocks; if options.all { @@ -418,7 +523,7 @@ Try '{} --help' for more information.", let path = PathBuf::from(&path_str); match Stat::new(path) { Ok(stat) => { - let mut inodes: HashSet = HashSet::new(); + let mut inodes: HashSet = HashSet::new(); let iter = du(stat, &options, 0, &mut inodes); let (_, len) = iter.size_hint(); diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index a79f820fb..c810bd395 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -45,7 +45,11 @@ fn test_du_basics_subdir() { fn _du_basics_subdir(s: String) { assert_eq!(s, "4\tsubdir/deeper\n"); } -#[cfg(not(target_vendor = "apple"))] +#[cfg(target_os = "windows")] +fn _du_basics_subdir(s: String) { + assert_eq!(s, "0\tsubdir/deeper\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_basics_subdir(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -71,7 +75,7 @@ fn test_du_basics_bad_name() { fn test_du_soft_link() { let ts = TestScenario::new("du"); - let link = ts.cmd("ln").arg("-s").arg(SUB_FILE).arg(SUB_LINK).run(); + let link = ts.ccmd("ln").arg("-s").arg(SUB_FILE).arg(SUB_LINK).run(); assert!(link.success); let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); @@ -85,7 +89,11 @@ fn _du_soft_link(s: String) { // 'macos' host variants may have `du` output variation for soft links assert!((s == "12\tsubdir/links\n") || (s == "16\tsubdir/links\n")); } -#[cfg(not(target_vendor = "apple"))] +#[cfg(target_os = "windows")] +fn _du_soft_link(s: String) { + assert_eq!(s, "8\tsubdir/links\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_soft_link(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -99,7 +107,7 @@ fn _du_soft_link(s: String) { fn test_du_hard_link() { let ts = TestScenario::new("du"); - let link = ts.cmd("ln").arg(SUB_FILE).arg(SUB_LINK).run(); + let link = ts.ccmd("ln").arg(SUB_FILE).arg(SUB_LINK).run(); assert!(link.success); let result = ts.ucmd().arg(SUB_DIR_LINKS).run(); @@ -113,7 +121,11 @@ fn test_du_hard_link() { fn _du_hard_link(s: String) { assert_eq!(s, "12\tsubdir/links\n") } -#[cfg(not(target_vendor = "apple"))] +#[cfg(target_os = "windows")] +fn _du_hard_link(s: String) { + assert_eq!(s, "8\tsubdir/links\n") +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_hard_link(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { @@ -137,7 +149,11 @@ fn test_du_d_flag() { fn _du_d_flag(s: String) { assert_eq!(s, "16\t./subdir\n20\t./\n"); } -#[cfg(not(target_vendor = "apple"))] +#[cfg(target_os = "windows")] +fn _du_d_flag(s: String) { + assert_eq!(s, "8\t./subdir\n8\t./\n"); +} +#[cfg(all(not(target_vendor = "apple"), not(target_os = "windows")))] fn _du_d_flag(s: String) { // MS-WSL linux has altered expected output if !is_wsl() { diff --git a/tests/common/util.rs b/tests/common/util.rs index e4b452289..7aea8dc26 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -589,6 +589,14 @@ impl TestScenario { UCommand::new_from_tmp(bin, self.tmpd.clone(), true) } + /// Returns builder for invoking any uutils command. Paths given are treated + /// relative to the environment's unique temporary test directory. + pub fn ccmd>(&self, bin: S) -> UCommand { + let mut cmd = self.cmd(&self.bin_path); + cmd.arg(bin); + cmd + } + // different names are used rather than an argument // because the need to keep the environment is exceedingly rare. pub fn ucmd_keepenv(&self) -> UCommand {