1use crate::{DirBuilder, DirInfo};
2use chrono::{DateTime, Local, LocalResult, TimeZone, Utc};
3use nu_engine::glob_from;
4#[allow(deprecated)]
5use nu_engine::{command_prelude::*, env::current_dir};
6use nu_glob::MatchOptions;
7use nu_path::{expand_path_with, expand_to_real_path};
8use nu_protocol::{
9 DataSource, NuGlob, PipelineMetadata, Signals,
10 shell_error::{self, io::IoError},
11};
12use pathdiff::diff_paths;
13use rayon::prelude::*;
14#[cfg(unix)]
15use std::os::unix::fs::PermissionsExt;
16use std::{
17 cmp::Ordering,
18 path::PathBuf,
19 sync::{Arc, Mutex, mpsc},
20 time::{SystemTime, UNIX_EPOCH},
21};
22
23#[derive(Clone)]
24pub struct Ls;
25
26#[derive(Clone, Copy)]
27struct Args {
28 all: bool,
29 long: bool,
30 short_names: bool,
31 full_paths: bool,
32 du: bool,
33 directory: bool,
34 use_mime_type: bool,
35 use_threads: bool,
36 call_span: Span,
37}
38
39impl Command for Ls {
40 fn name(&self) -> &str {
41 "ls"
42 }
43
44 fn description(&self) -> &str {
45 "List the filenames, sizes, and modification times of items in a directory."
46 }
47
48 fn search_terms(&self) -> Vec<&str> {
49 vec!["dir"]
50 }
51
52 fn signature(&self) -> nu_protocol::Signature {
53 Signature::build("ls")
54 .input_output_types(vec![(Type::Nothing, Type::table())])
55 .rest("pattern", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The glob pattern to use.")
58 .switch("all", "Show hidden files", Some('a'))
59 .switch(
60 "long",
61 "Get all available columns for each entry (slower; columns are platform-dependent)",
62 Some('l'),
63 )
64 .switch(
65 "short-names",
66 "Only print the file names, and not the path",
67 Some('s'),
68 )
69 .switch("full-paths", "display paths as absolute paths", Some('f'))
70 .switch(
71 "du",
72 "Display the apparent directory size (\"disk usage\") in place of the directory metadata size",
73 Some('d'),
74 )
75 .switch(
76 "directory",
77 "List the specified directory itself instead of its contents",
78 Some('D'),
79 )
80 .switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m'))
81 .switch("threads", "Use multiple threads to list contents. Output will be non-deterministic.", Some('t'))
82 .category(Category::FileSystem)
83 }
84
85 fn run(
86 &self,
87 engine_state: &EngineState,
88 stack: &mut Stack,
89 call: &Call,
90 _input: PipelineData,
91 ) -> Result<PipelineData, ShellError> {
92 let all = call.has_flag(engine_state, stack, "all")?;
93 let long = call.has_flag(engine_state, stack, "long")?;
94 let short_names = call.has_flag(engine_state, stack, "short-names")?;
95 let full_paths = call.has_flag(engine_state, stack, "full-paths")?;
96 let du = call.has_flag(engine_state, stack, "du")?;
97 let directory = call.has_flag(engine_state, stack, "directory")?;
98 let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?;
99 let use_threads = call.has_flag(engine_state, stack, "threads")?;
100 let call_span = call.head;
101 #[allow(deprecated)]
102 let cwd = current_dir(engine_state, stack)?;
103
104 let args = Args {
105 all,
106 long,
107 short_names,
108 full_paths,
109 du,
110 directory,
111 use_mime_type,
112 use_threads,
113 call_span,
114 };
115
116 let pattern_arg = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
117 let input_pattern_arg = if !call.has_positional_args(stack, 0) {
118 None
119 } else {
120 Some(pattern_arg)
121 };
122 match input_pattern_arg {
123 None => Ok(
124 ls_for_one_pattern(None, args, engine_state.signals().clone(), cwd)?
125 .into_pipeline_data_with_metadata(
126 call_span,
127 engine_state.signals().clone(),
128 PipelineMetadata {
129 data_source: DataSource::Ls,
130 content_type: None,
131 },
132 ),
133 ),
134 Some(pattern) => {
135 let mut result_iters = vec![];
136 for pat in pattern {
137 result_iters.push(ls_for_one_pattern(
138 Some(pat),
139 args,
140 engine_state.signals().clone(),
141 cwd.clone(),
142 )?)
143 }
144
145 Ok(result_iters
148 .into_iter()
149 .flatten()
150 .into_pipeline_data_with_metadata(
151 call_span,
152 engine_state.signals().clone(),
153 PipelineMetadata {
154 data_source: DataSource::Ls,
155 content_type: None,
156 },
157 ))
158 }
159 }
160 }
161
162 fn examples(&self) -> Vec<Example> {
163 vec![
164 Example {
165 description: "List visible files in the current directory",
166 example: "ls",
167 result: None,
168 },
169 Example {
170 description: "List visible files in a subdirectory",
171 example: "ls subdir",
172 result: None,
173 },
174 Example {
175 description: "List visible files with full path in the parent directory",
176 example: "ls -f ..",
177 result: None,
178 },
179 Example {
180 description: "List Rust files",
181 example: "ls *.rs",
182 result: None,
183 },
184 Example {
185 description: "List files and directories whose name do not contain 'bar'",
186 example: "ls | where name !~ bar",
187 result: None,
188 },
189 Example {
190 description: "List the full path of all dirs in your home directory",
191 example: "ls -a ~ | where type == dir",
192 result: None,
193 },
194 Example {
195 description: "List only the names (not paths) of all dirs in your home directory which have not been modified in 7 days",
196 example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)",
197 result: None,
198 },
199 Example {
200 description: "Recursively list all files and subdirectories under the current directory using a glob pattern",
201 example: "ls -a **/*",
202 result: None,
203 },
204 Example {
205 description: "Recursively list *.rs and *.toml files using the glob command",
206 example: "ls ...(glob **/*.{rs,toml})",
207 result: None,
208 },
209 Example {
210 description: "List given paths and show directories themselves",
211 example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten",
212 result: None,
213 },
214 ]
215 }
216}
217
218fn ls_for_one_pattern(
219 pattern_arg: Option<Spanned<NuGlob>>,
220 args: Args,
221 signals: Signals,
222 cwd: PathBuf,
223) -> Result<PipelineData, ShellError> {
224 fn create_pool(num_threads: usize) -> Result<rayon::ThreadPool, ShellError> {
225 match rayon::ThreadPoolBuilder::new()
226 .num_threads(num_threads)
227 .build()
228 {
229 Err(e) => Err(e).map_err(|e| ShellError::GenericError {
230 error: "Error creating thread pool".into(),
231 msg: e.to_string(),
232 span: Some(Span::unknown()),
233 help: None,
234 inner: vec![],
235 }),
236 Ok(pool) => Ok(pool),
237 }
238 }
239
240 let (tx, rx) = mpsc::channel();
241
242 let Args {
243 all,
244 long,
245 short_names,
246 full_paths,
247 du,
248 directory,
249 use_mime_type,
250 use_threads,
251 call_span,
252 } = args;
253 let pattern_arg = {
254 if let Some(path) = pattern_arg {
255 if path.item.as_ref().is_empty() {
257 return Err(ShellError::Io(IoError::new_with_additional_context(
258 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::NotFound),
259 path.span,
260 PathBuf::from(path.item.to_string()),
261 "empty string('') directory or file does not exist",
262 )));
263 }
264 match path.item {
265 NuGlob::DoNotExpand(p) => Some(Spanned {
266 item: NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(p)),
267 span: path.span,
268 }),
269 NuGlob::Expand(p) => Some(Spanned {
270 item: NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(p)),
271 span: path.span,
272 }),
273 }
274 } else {
275 pattern_arg
276 }
277 };
278
279 let mut just_read_dir = false;
280 let p_tag: Span = pattern_arg.as_ref().map(|p| p.span).unwrap_or(call_span);
281 let (pattern_arg, absolute_path) = match pattern_arg {
282 Some(pat) => {
283 let tmp_expanded =
285 nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand());
286 if !directory && tmp_expanded.is_dir() {
288 if read_dir(tmp_expanded, p_tag, use_threads, signals.clone())?
289 .next()
290 .is_none()
291 {
292 return Ok(Value::test_nothing().into_pipeline_data());
293 }
294 just_read_dir = !(pat.item.is_expand() && nu_glob::is_glob(pat.item.as_ref()));
295 }
296
297 let absolute_path = Path::new(pat.item.as_ref()).is_absolute()
303 || (pat.item.is_expand() && expand_to_real_path(pat.item.as_ref()).is_absolute());
304 (pat.item, absolute_path)
305 }
306 None => {
307 if directory {
309 (NuGlob::Expand(".".to_string()), false)
310 } else if read_dir(cwd.clone(), p_tag, use_threads, signals.clone())?
311 .next()
312 .is_none()
313 {
314 return Ok(Value::test_nothing().into_pipeline_data());
315 } else {
316 (NuGlob::Expand("*".to_string()), false)
317 }
318 }
319 };
320
321 let hidden_dir_specified = is_hidden_dir(pattern_arg.as_ref());
322
323 let path = pattern_arg.into_spanned(p_tag);
324 let (prefix, paths) = if just_read_dir {
325 let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand());
326 let paths = read_dir(expanded.clone(), p_tag, use_threads, signals.clone())?;
327 (Some(expanded), paths)
329 } else {
330 let glob_options = if all {
331 None
332 } else {
333 let glob_options = MatchOptions {
334 recursive_match_hidden_dir: false,
335 ..Default::default()
336 };
337 Some(glob_options)
338 };
339 glob_from(&path, &cwd, call_span, glob_options, signals.clone())?
340 };
341
342 let mut paths_peek = paths.peekable();
343 let no_matches = paths_peek.peek().is_none();
344 signals.check(call_span)?;
345 if no_matches {
346 return Err(ShellError::GenericError {
347 error: format!("No matches found for {:?}", path.item),
348 msg: "Pattern, file or folder not found".into(),
349 span: Some(p_tag),
350 help: Some("no matches found".into()),
351 inner: vec![],
352 });
353 }
354
355 let hidden_dirs = Arc::new(Mutex::new(Vec::new()));
356
357 let signals_clone = signals.clone();
358
359 let pool = if use_threads {
360 let count = std::thread::available_parallelism()
361 .map_err(|err| {
362 IoError::new_with_additional_context(
363 err,
364 call_span,
365 None,
366 "Could not get available parallelism",
367 )
368 })?
369 .get();
370 create_pool(count)?
371 } else {
372 create_pool(1)?
373 };
374
375 pool.install(|| {
376 rayon::spawn(move || {
377 let result = paths_peek
378 .par_bridge()
379 .filter_map(move |x| match x {
380 Ok(path) => {
381 let metadata = std::fs::symlink_metadata(&path).ok();
382 let hidden_dir_clone = Arc::clone(&hidden_dirs);
383 let mut hidden_dir_mutex = hidden_dir_clone
384 .lock()
385 .expect("Unable to acquire lock for hidden_dirs");
386 if path_contains_hidden_folder(&path, &hidden_dir_mutex) {
387 return None;
388 }
389
390 if !all && !hidden_dir_specified && is_hidden_dir(&path) {
391 if path.is_dir() {
392 hidden_dir_mutex.push(path);
393 drop(hidden_dir_mutex);
394 }
395 return None;
396 }
397
398 let display_name = if short_names {
399 path.file_name().map(|os| os.to_string_lossy().to_string())
400 } else if full_paths || absolute_path {
401 Some(path.to_string_lossy().to_string())
402 } else if let Some(prefix) = &prefix {
403 if let Ok(remainder) = path.strip_prefix(prefix) {
404 if directory {
405 let path_diff = if let Some(path_diff_not_dot) =
407 diff_paths(&path, &cwd)
408 {
409 let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
410 if path_diff_not_dot.is_empty() {
411 ".".to_string()
412 } else {
413 path_diff_not_dot.to_string()
414 }
415 } else {
416 path.to_string_lossy().to_string()
417 };
418
419 Some(path_diff)
420 } else {
421 let new_prefix = if let Some(pfx) = diff_paths(prefix, &cwd) {
422 pfx
423 } else {
424 prefix.to_path_buf()
425 };
426
427 Some(new_prefix.join(remainder).to_string_lossy().to_string())
428 }
429 } else {
430 Some(path.to_string_lossy().to_string())
431 }
432 } else {
433 Some(path.to_string_lossy().to_string())
434 }
435 .ok_or_else(|| ShellError::GenericError {
436 error: format!("Invalid file name: {:}", path.to_string_lossy()),
437 msg: "invalid file name".into(),
438 span: Some(call_span),
439 help: None,
440 inner: vec![],
441 });
442
443 match display_name {
444 Ok(name) => {
445 let entry = dir_entry_dict(
446 &path,
447 &name,
448 metadata.as_ref(),
449 call_span,
450 long,
451 du,
452 &signals_clone,
453 use_mime_type,
454 args.full_paths,
455 );
456 match entry {
457 Ok(value) => Some(value),
458 Err(err) => Some(Value::error(err, call_span)),
459 }
460 }
461 Err(err) => Some(Value::error(err, call_span)),
462 }
463 }
464 Err(err) => Some(Value::error(err, call_span)),
465 })
466 .try_for_each(|stream| {
467 tx.send(stream).map_err(|e| ShellError::GenericError {
468 error: "Error streaming data".into(),
469 msg: e.to_string(),
470 span: Some(call_span),
471 help: None,
472 inner: vec![],
473 })
474 })
475 .map_err(|err| ShellError::GenericError {
476 error: "Unable to create a rayon pool".into(),
477 msg: err.to_string(),
478 span: Some(call_span),
479 help: None,
480 inner: vec![],
481 });
482
483 if let Err(error) = result {
484 let _ = tx.send(Value::error(error, call_span));
485 }
486 });
487 });
488
489 Ok(rx
490 .into_iter()
491 .into_pipeline_data(call_span, signals.clone()))
492}
493
494fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
495 #[cfg(windows)]
496 {
497 use std::os::windows::fs::MetadataExt;
498
499 if let Ok(metadata) = dir.as_ref().metadata() {
500 let attributes = metadata.file_attributes();
501 (attributes & 0x2) != 0
503 } else {
504 false
505 }
506 }
507
508 #[cfg(not(windows))]
509 {
510 dir.as_ref()
511 .file_name()
512 .map(|name| name.to_string_lossy().starts_with('.'))
513 .unwrap_or(false)
514 }
515}
516
517fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool {
518 if folders.iter().any(|p| path.starts_with(p.as_path())) {
519 return true;
520 }
521 false
522}
523
524#[cfg(unix)]
525use std::os::unix::fs::FileTypeExt;
526use std::path::Path;
527
528pub fn get_file_type(md: &std::fs::Metadata, display_name: &str, use_mime_type: bool) -> String {
529 let ft = md.file_type();
530 let mut file_type = "unknown";
531 if ft.is_dir() {
532 file_type = "dir";
533 } else if ft.is_file() {
534 file_type = "file";
535 } else if ft.is_symlink() {
536 file_type = "symlink";
537 } else {
538 #[cfg(unix)]
539 {
540 if ft.is_block_device() {
541 file_type = "block device";
542 } else if ft.is_char_device() {
543 file_type = "char device";
544 } else if ft.is_fifo() {
545 file_type = "pipe";
546 } else if ft.is_socket() {
547 file_type = "socket";
548 }
549 }
550 }
551 if use_mime_type {
552 let guess = mime_guess::from_path(display_name);
553 let mime_guess = match guess.first() {
554 Some(mime_type) => mime_type.essence_str().to_string(),
555 None => "unknown".to_string(),
556 };
557 if file_type == "file" {
558 mime_guess
559 } else {
560 file_type.to_string()
561 }
562 } else {
563 file_type.to_string()
564 }
565}
566
567#[allow(clippy::too_many_arguments)]
568pub(crate) fn dir_entry_dict(
569 filename: &std::path::Path, display_name: &str, metadata: Option<&std::fs::Metadata>,
572 span: Span,
573 long: bool,
574 du: bool,
575 signals: &Signals,
576 use_mime_type: bool,
577 full_symlink_target: bool,
578) -> Result<Value, ShellError> {
579 #[cfg(windows)]
580 if metadata.is_none() {
581 return Ok(windows_helper::dir_entry_dict_windows_fallback(
582 filename,
583 display_name,
584 span,
585 long,
586 ));
587 }
588
589 let mut record = Record::new();
590 let mut file_type = "unknown".to_string();
591
592 record.push("name", Value::string(display_name, span));
593
594 if let Some(md) = metadata {
595 file_type = get_file_type(md, display_name, use_mime_type);
596 record.push("type", Value::string(file_type.clone(), span));
597 } else {
598 record.push("type", Value::nothing(span));
599 }
600
601 if long {
602 if let Some(md) = metadata {
603 record.push(
604 "target",
605 if md.file_type().is_symlink() {
606 if let Ok(path_to_link) = filename.read_link() {
607 if full_symlink_target && filename.parent().is_some() {
610 Value::string(
611 expand_path_with(
612 path_to_link,
613 filename
614 .parent()
615 .expect("already check the filename have a parent"),
616 true,
617 )
618 .to_string_lossy(),
619 span,
620 )
621 } else {
622 Value::string(path_to_link.to_string_lossy(), span)
623 }
624 } else {
625 Value::string("Could not obtain target file's path", span)
626 }
627 } else {
628 Value::nothing(span)
629 },
630 )
631 }
632 }
633
634 if long {
635 if let Some(md) = metadata {
636 record.push("readonly", Value::bool(md.permissions().readonly(), span));
637
638 #[cfg(unix)]
639 {
640 use nu_utils::filesystem::users;
641 use std::os::unix::fs::MetadataExt;
642
643 let mode = md.permissions().mode();
644 record.push(
645 "mode",
646 Value::string(umask::Mode::from(mode).to_string(), span),
647 );
648
649 let nlinks = md.nlink();
650 record.push("num_links", Value::int(nlinks as i64, span));
651
652 let inode = md.ino();
653 record.push("inode", Value::int(inode as i64, span));
654
655 record.push(
656 "user",
657 if let Some(user) = users::get_user_by_uid(md.uid().into()) {
658 Value::string(user.name, span)
659 } else {
660 Value::int(md.uid().into(), span)
661 },
662 );
663
664 record.push(
665 "group",
666 if let Some(group) = users::get_group_by_gid(md.gid().into()) {
667 Value::string(group.name, span)
668 } else {
669 Value::int(md.gid().into(), span)
670 },
671 );
672 }
673 }
674 }
675
676 record.push(
677 "size",
678 if let Some(md) = metadata {
679 let zero_sized = file_type == "pipe"
680 || file_type == "socket"
681 || file_type == "char device"
682 || file_type == "block device";
683
684 if md.is_dir() {
685 if du {
686 let params = DirBuilder::new(Span::new(0, 2), None, false, None, false);
687 let dir_size = DirInfo::new(filename, ¶ms, None, span, signals)?.get_size();
688
689 Value::filesize(dir_size as i64, span)
690 } else {
691 let dir_size: u64 = md.len();
692
693 Value::filesize(dir_size as i64, span)
694 }
695 } else if md.is_file() {
696 Value::filesize(md.len() as i64, span)
697 } else if md.file_type().is_symlink() {
698 if let Ok(symlink_md) = filename.symlink_metadata() {
699 Value::filesize(symlink_md.len() as i64, span)
700 } else {
701 Value::nothing(span)
702 }
703 } else if zero_sized {
704 Value::filesize(0, span)
705 } else {
706 Value::nothing(span)
707 }
708 } else {
709 Value::nothing(span)
710 },
711 );
712
713 if let Some(md) = metadata {
714 if long {
715 record.push("created", {
716 let mut val = Value::nothing(span);
717 if let Ok(c) = md.created() {
718 if let Some(local) = try_convert_to_local_date_time(c) {
719 val = Value::date(local.with_timezone(local.offset()), span);
720 }
721 }
722 val
723 });
724
725 record.push("accessed", {
726 let mut val = Value::nothing(span);
727 if let Ok(a) = md.accessed() {
728 if let Some(local) = try_convert_to_local_date_time(a) {
729 val = Value::date(local.with_timezone(local.offset()), span)
730 }
731 }
732 val
733 });
734 }
735
736 record.push("modified", {
737 let mut val = Value::nothing(span);
738 if let Ok(m) = md.modified() {
739 if let Some(local) = try_convert_to_local_date_time(m) {
740 val = Value::date(local.with_timezone(local.offset()), span);
741 }
742 }
743 val
744 })
745 } else {
746 if long {
747 record.push("created", Value::nothing(span));
748 record.push("accessed", Value::nothing(span));
749 }
750
751 record.push("modified", Value::nothing(span));
752 }
753
754 Ok(Value::record(record, span))
755}
756
757fn try_convert_to_local_date_time(t: SystemTime) -> Option<DateTime<Local>> {
760 let (sec, nsec) = match t.duration_since(UNIX_EPOCH) {
762 Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
763 Err(e) => {
764 let dur = e.duration();
766 let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos());
767 if nsec == 0 {
768 (-sec, 0)
769 } else {
770 (-sec - 1, 1_000_000_000 - nsec)
771 }
772 }
773 };
774
775 const NEG_UNIX_EPOCH: i64 = -11644473600; if sec == NEG_UNIX_EPOCH {
777 return None;
779 }
780 match Utc.timestamp_opt(sec, nsec) {
781 LocalResult::Single(t) => Some(t.with_timezone(&Local)),
782 _ => None,
783 }
784}
785
786#[cfg(windows)]
788fn unix_time_to_local_date_time(secs: i64) -> Option<DateTime<Local>> {
789 match Utc.timestamp_opt(secs, 0) {
790 LocalResult::Single(t) => Some(t.with_timezone(&Local)),
791 _ => None,
792 }
793}
794
795#[cfg(windows)]
796mod windows_helper {
797 use super::*;
798
799 use nu_protocol::shell_error;
800 use std::os::windows::prelude::OsStrExt;
801 use windows::Win32::Foundation::FILETIME;
802 use windows::Win32::Storage::FileSystem::{
803 FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_REPARSE_POINT, FindClose,
804 FindFirstFileW, WIN32_FIND_DATAW,
805 };
806 use windows::Win32::System::SystemServices::{
807 IO_REPARSE_TAG_MOUNT_POINT, IO_REPARSE_TAG_SYMLINK,
808 };
809
810 pub fn dir_entry_dict_windows_fallback(
814 filename: &Path,
815 display_name: &str,
816 span: Span,
817 long: bool,
818 ) -> Value {
819 let mut record = Record::new();
820
821 record.push("name", Value::string(display_name, span));
822
823 let find_data = match find_first_file(filename, span) {
824 Ok(fd) => fd,
825 Err(e) => {
826 log::error!("ls: '{}' {}", filename.to_string_lossy(), e);
830 return Value::record(record, span);
831 }
832 };
833
834 record.push(
835 "type",
836 Value::string(get_file_type_windows_fallback(&find_data), span),
837 );
838
839 if long {
840 record.push(
841 "target",
842 if is_symlink(&find_data) {
843 if let Ok(path_to_link) = filename.read_link() {
844 Value::string(path_to_link.to_string_lossy(), span)
845 } else {
846 Value::string("Could not obtain target file's path", span)
847 }
848 } else {
849 Value::nothing(span)
850 },
851 );
852
853 record.push(
854 "readonly",
855 Value::bool(
856 find_data.dwFileAttributes & FILE_ATTRIBUTE_READONLY.0 != 0,
857 span,
858 ),
859 );
860 }
861
862 let file_size = ((find_data.nFileSizeHigh as u64) << 32) | find_data.nFileSizeLow as u64;
863 record.push("size", Value::filesize(file_size as i64, span));
864
865 if long {
866 record.push("created", {
867 let mut val = Value::nothing(span);
868 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftCreationTime);
869 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
870 val = Value::date(local.with_timezone(local.offset()), span);
871 }
872 val
873 });
874
875 record.push("accessed", {
876 let mut val = Value::nothing(span);
877 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastAccessTime);
878 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
879 val = Value::date(local.with_timezone(local.offset()), span);
880 }
881 val
882 });
883 }
884
885 record.push("modified", {
886 let mut val = Value::nothing(span);
887 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastWriteTime);
888 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
889 val = Value::date(local.with_timezone(local.offset()), span);
890 }
891 val
892 });
893
894 Value::record(record, span)
895 }
896
897 fn unix_time_from_filetime(ft: &FILETIME) -> i64 {
898 const EPOCH_AS_FILETIME: u64 = 116444736000000000;
900 const HUNDREDS_OF_NANOSECONDS: u64 = 10000000;
901
902 let time_u64 = ((ft.dwHighDateTime as u64) << 32) | (ft.dwLowDateTime as u64);
903 if time_u64 > 0 {
904 let rel_to_linux_epoch = time_u64.saturating_sub(EPOCH_AS_FILETIME);
905 let seconds_since_unix_epoch = rel_to_linux_epoch / HUNDREDS_OF_NANOSECONDS;
906 return seconds_since_unix_epoch as i64;
907 }
908 0
909 }
910
911 fn find_first_file(filename: &Path, span: Span) -> Result<WIN32_FIND_DATAW, ShellError> {
913 unsafe {
914 let mut find_data = WIN32_FIND_DATAW::default();
915 let filename_wide: Vec<u16> = filename
917 .as_os_str()
918 .encode_wide()
919 .chain(std::iter::once(0))
920 .collect();
921
922 match FindFirstFileW(
923 windows::core::PCWSTR(filename_wide.as_ptr()),
924 &mut find_data,
925 ) {
926 Ok(handle) => {
927 let _ = FindClose(handle);
932 Ok(find_data)
933 }
934 Err(e) => Err(ShellError::Io(IoError::new_with_additional_context(
935 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Other),
936 span,
937 PathBuf::from(filename),
938 format!("Could not read metadata: {e}"),
939 ))),
940 }
941 }
942 }
943
944 fn get_file_type_windows_fallback(find_data: &WIN32_FIND_DATAW) -> String {
945 if find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY.0 != 0 {
946 return "dir".to_string();
947 }
948
949 if is_symlink(find_data) {
950 return "symlink".to_string();
951 }
952
953 "file".to_string()
954 }
955
956 fn is_symlink(find_data: &WIN32_FIND_DATAW) -> bool {
957 if find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 {
958 if find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK
961 || find_data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT
962 {
963 return true;
964 }
965 }
966 false
967 }
968}
969
970#[allow(clippy::type_complexity)]
971fn read_dir(
972 f: PathBuf,
973 span: Span,
974 use_threads: bool,
975 signals: Signals,
976) -> Result<Box<dyn Iterator<Item = Result<PathBuf, ShellError>> + Send>, ShellError> {
977 let signals_clone = signals.clone();
978 let items = f
979 .read_dir()
980 .map_err(|err| IoError::new(err, span, f.clone()))?
981 .map(move |d| {
982 signals_clone.check(span)?;
983 d.map(|r| r.path())
984 .map_err(|err| IoError::new(err, span, f.clone()))
985 .map_err(ShellError::from)
986 });
987 if !use_threads {
988 let mut collected = items.collect::<Vec<_>>();
989 signals.check(span)?;
990 collected.sort_by(|a, b| match (a, b) {
991 (Ok(a), Ok(b)) => a.cmp(b),
992 (Ok(_), Err(_)) => Ordering::Greater,
993 (Err(_), Ok(_)) => Ordering::Less,
994 (Err(_), Err(_)) => Ordering::Equal,
995 });
996 return Ok(Box::new(collected.into_iter()));
997 }
998 Ok(Box::new(items))
999}