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