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