From 888758b8132149e85b65492eb2e57ff97c5ef4b4 Mon Sep 17 00:00:00 2001 From: Reilly Wood <26268125+rgwood@users.noreply.github.com> Date: Fri, 3 Jun 2022 12:37:27 -0400 Subject: [PATCH] Fix `ls` for Windows system files (#5703) * Fix `ls` for Windows system files * Fix non-Windows builds * Make Clippy happy on non-Windows platforms * Fix new test on GitHub runners * Move ls Windows code into its own module --- Cargo.lock | 1 + crates/nu-command/Cargo.toml | 9 ++ crates/nu-command/src/filesystem/ls.rs | 200 +++++++++++++++++++++++++ crates/nu-command/tests/commands/ls.rs | 46 ++++++ 4 files changed, 256 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ce14c331bc..bebc32d7b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2618,6 +2618,7 @@ dependencies = [ "uuid", "wax", "which", + "windows", ] [[package]] diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 8067721560..aa842655ae 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -107,6 +107,15 @@ features = [ "dynamic_groupby" ] +[target.'cfg(windows)'.dependencies.windows] +version = "0.37.0" +features = [ + "alloc", + "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_System_SystemServices", +] + [features] trash-support = ["trash"] which-support = ["which"] diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index a20d50ba1b..d1f91a933a 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -12,6 +12,7 @@ use nu_protocol::{ PipelineMetadata, ShellError, Signature, Span, Spanned, SyntaxShape, Value, }; use pathdiff::diff_paths; + #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; @@ -360,6 +361,11 @@ pub(crate) fn dir_entry_dict( du: bool, ctrl_c: Option>, ) -> Result { + #[cfg(windows)] + if metadata.is_none() { + return windows_helper::dir_entry_dict_windows_fallback(filename, display_name, span, long); + } + let mut cols = vec![]; let mut vals = vec![]; let mut file_type = "unknown"; @@ -568,6 +574,8 @@ pub(crate) fn dir_entry_dict( Ok(Value::Record { cols, vals, span }) } +// TODO: can we get away from local times in `ls`? internals might be cleaner if we worked in UTC +// and left the conversion to local time to the display layer fn try_convert_to_local_date_time(t: SystemTime) -> Option> { // Adapted from https://github.com/chronotope/chrono/blob/v0.4.19/src/datetime.rs#L755-L767. let (sec, nsec) = match t.duration_since(UNIX_EPOCH) { @@ -589,3 +597,195 @@ fn try_convert_to_local_date_time(t: SystemTime) -> Option> { _ => None, } } + +// #[cfg(windows)] is just to make Clippy happy, remove if you ever want to use this on other platforms +#[cfg(windows)] +fn unix_time_to_local_date_time(secs: i64) -> Option> { + match Utc.timestamp_opt(secs, 0) { + LocalResult::Single(t) => Some(t.with_timezone(&Local)), + _ => None, + } +} + +#[cfg(windows)] +mod windows_helper { + use super::*; + + use std::mem::MaybeUninit; + use std::os::windows::prelude::OsStrExt; + use windows::Win32::Foundation::FILETIME; + use windows::Win32::Storage::FileSystem::{ + FindFirstFileW, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY, + FILE_ATTRIBUTE_REPARSE_POINT, WIN32_FIND_DATAW, + }; + use windows::Win32::System::SystemServices::{ + IO_REPARSE_TAG_MOUNT_POINT, IO_REPARSE_TAG_SYMLINK, + }; + + /// A secondary way to get file info on Windows, for when std::fs::symlink_metadata() fails. + /// dir_entry_dict depends on metadata, but that can't be retrieved for some Windows system files: + /// https://github.com/rust-lang/rust/issues/96980 + pub fn dir_entry_dict_windows_fallback( + filename: &Path, + display_name: &str, + span: Span, + long: bool, + ) -> Result { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("name".into()); + vals.push(Value::String { + val: display_name.to_string(), + span, + }); + + let find_data = find_first_file(filename, span)?; + + cols.push("type".into()); + vals.push(Value::String { + val: get_file_type_windows_fallback(&find_data), + span, + }); + + if long { + cols.push("target".into()); + if is_symlink(&find_data) { + if let Ok(path_to_link) = filename.read_link() { + vals.push(Value::String { + val: path_to_link.to_string_lossy().to_string(), + span, + }); + } else { + vals.push(Value::String { + val: "Could not obtain target file's path".to_string(), + span, + }); + } + } else { + vals.push(Value::nothing(span)); + } + + cols.push("readonly".into()); + vals.push(Value::Bool { + val: (find_data.dwFileAttributes & FILE_ATTRIBUTE_READONLY.0 != 0), + span, + }); + } + + cols.push("size".to_string()); + let file_size = (find_data.nFileSizeHigh as u64) << 32 | find_data.nFileSizeLow as u64; + vals.push(Value::Filesize { + val: file_size as i64, + span, + }); + + if long { + cols.push("created".to_string()); + { + let mut val = Value::nothing(span); + let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftCreationTime); + if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) { + val = Value::Date { + val: local.with_timezone(local.offset()), + span, + }; + } + vals.push(val); + } + + cols.push("accessed".to_string()); + { + let mut val = Value::nothing(span); + let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastAccessTime); + if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) { + val = Value::Date { + val: local.with_timezone(local.offset()), + span, + }; + } + vals.push(val); + } + } + + cols.push("modified".to_string()); + { + let mut val = Value::nothing(span); + let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastWriteTime); + if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) { + val = Value::Date { + val: local.with_timezone(local.offset()), + span, + }; + } + vals.push(val); + } + + Ok(Value::Record { cols, vals, span }) + } + + fn unix_time_from_filetime(ft: &FILETIME) -> i64 { + /// January 1, 1970 as Windows file time + const EPOCH_AS_FILETIME: u64 = 116444736000000000; + const HUNDREDS_OF_NANOSECONDS: u64 = 10000000; + + let time_u64 = ((ft.dwHighDateTime as u64) << 32) | (ft.dwLowDateTime as u64); + let rel_to_linux_epoch = time_u64 - EPOCH_AS_FILETIME; + let seconds_since_unix_epoch = rel_to_linux_epoch / HUNDREDS_OF_NANOSECONDS; + + seconds_since_unix_epoch as i64 + } + + // wrapper around the FindFirstFileW Win32 API + fn find_first_file(filename: &Path, span: Span) -> Result { + unsafe { + let mut find_data = MaybeUninit::::uninit(); + // The windows crate really needs a nicer way to do string conversions + let filename_wide: Vec = filename + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + if FindFirstFileW( + windows::core::PCWSTR(filename_wide.as_ptr()), + find_data.as_mut_ptr(), + ) + .is_err() + { + return Err(ShellError::ReadingFile( + "Could not read file metadata".to_string(), + span, + )); + } + + let find_data = find_data.assume_init(); + Ok(find_data) + } + } + + fn get_file_type_windows_fallback(find_data: &WIN32_FIND_DATAW) -> String { + if find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY.0 != 0 { + return "dir".to_string(); + } + + if is_symlink(find_data) { + return "symlink".to_string(); + } + + "file".to_string() + } + + fn is_symlink(find_data: &WIN32_FIND_DATAW) -> bool { + if find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 { + // Follow Golang's lead in treating mount points as symlinks. + // https://github.com/golang/go/blob/016d7552138077741a9c3fdadc73c0179f5d3ff7/src/os/types_windows.go#L104-L105 + if find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK + || find_data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT + { + return true; + } + } + false + } +} diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index 97c1d183eb..4f907478d6 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -391,3 +391,49 @@ fn list_all_columns() { ); }); } + +/// Rust's fs::metadata function is unable to read info for certain system files on Windows, +/// like the `C:\Windows\System32\Configuration` folder. https://github.com/rust-lang/rust/issues/96980 +/// This test confirms that Nu can work around this successfully. +#[test] +#[cfg(windows)] +fn can_list_system_folder() { + // the awkward `ls Configuration* | where name == "Configuration"` thing is for speed; + // listing the entire System32 folder is slow and `ls Configuration*` alone + // might return more than 1 file someday + let file_type = nu!( + cwd: "C:\\Windows\\System32", pipeline( + r#"ls Configuration* | where name == "Configuration" | get type.0"# + )); + assert_eq!(file_type.out, "dir"); + + let file_size = nu!( + cwd: "C:\\Windows\\System32", pipeline( + r#"ls Configuration* | where name == "Configuration" | get size.0"# + )); + assert!(file_size.out.trim() != ""); + + let file_modified = nu!( + cwd: "C:\\Windows\\System32", pipeline( + r#"ls Configuration* | where name == "Configuration" | get modified.0"# + )); + assert!(file_modified.out.trim() != ""); + + let file_accessed = nu!( + cwd: "C:\\Windows\\System32", pipeline( + r#"ls -l Configuration* | where name == "Configuration" | get accessed.0"# + )); + assert!(file_accessed.out.trim() != ""); + + let file_created = nu!( + cwd: "C:\\Windows\\System32", pipeline( + r#"ls -l Configuration* | where name == "Configuration" | get created.0"# + )); + assert!(file_created.out.trim() != ""); + + let ls_with_filter = nu!( + cwd: "C:\\Windows\\System32", pipeline( + r#"ls | where size > 10mb"# + )); + assert_eq!(ls_with_filter.err, ""); +}