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
This commit is contained in:
Reilly Wood 2022-06-03 12:37:27 -04:00 committed by GitHub
parent cb909f810e
commit 888758b813
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 256 additions and 0 deletions

1
Cargo.lock generated
View file

@ -2618,6 +2618,7 @@ dependencies = [
"uuid",
"wax",
"which",
"windows",
]
[[package]]

View file

@ -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"]

View file

@ -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<Arc<AtomicBool>>,
) -> Result<Value, ShellError> {
#[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<DateTime<Local>> {
// 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<DateTime<Local>> {
_ => 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<DateTime<Local>> {
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<Value, ShellError> {
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<WIN32_FIND_DATAW, ShellError> {
unsafe {
let mut find_data = MaybeUninit::<WIN32_FIND_DATAW>::uninit();
// The windows crate really needs a nicer way to do string conversions
let filename_wide: Vec<u16> = 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
}
}

View file

@ -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, "");
}