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 NuGlob, PipelineMetadata, Signals,
8 shell_error::{self, generic::GenericError, 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 path_columns: vec!["name".to_string()],
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 path_columns: vec!["name".to_string()],
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, call_span: Span) -> 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| {
335 ShellError::Generic(GenericError::new(
336 "Error creating thread pool",
337 e.to_string(),
338 call_span,
339 ))
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 Some(path.map(NuGlob::strip_ansi_string_unlikely))
370 } else {
371 pattern_arg
372 }
373 };
374
375 let mut just_read_dir = false;
376 let p_tag: Span = pattern_arg.as_ref().map(|p| p.span).unwrap_or(call_span);
377 let (pattern_arg, absolute_path) = match pattern_arg {
378 Some(pat) => {
379 let tmp_expanded =
381 nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand());
382 if !directory && tmp_expanded.is_dir() {
384 if read_dir(tmp_expanded, p_tag, use_threads, signals.clone())?
385 .next()
386 .is_none()
387 {
388 return Ok(Value::test_nothing().into_pipeline_data());
389 }
390 just_read_dir = !(pat.item.is_expand() && nu_glob::is_glob(pat.item.as_ref()));
391 }
392
393 let absolute_path = Path::new(pat.item.as_ref()).is_absolute()
399 || (pat.item.is_expand() && expand_to_real_path(pat.item.as_ref()).is_absolute());
400 (pat.item, absolute_path)
401 }
402 None => {
403 if directory {
405 (NuGlob::Expand(".".to_string()), false)
406 } else if read_dir(cwd.clone(), p_tag, use_threads, signals.clone())?
407 .next()
408 .is_none()
409 {
410 return Ok(Value::test_nothing().into_pipeline_data());
411 } else {
412 (NuGlob::Expand("*".to_string()), false)
413 }
414 }
415 };
416
417 let hidden_dir_specified = is_hidden_dir(pattern_arg.as_ref());
418
419 let path = pattern_arg.into_spanned(p_tag);
420 let (prefix, paths): (
421 Option<PathBuf>,
422 Box<dyn Iterator<Item = Result<LsEntry, ShellError>> + Send>,
423 ) = if just_read_dir {
424 let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand());
425 let paths = read_dir(expanded.clone(), p_tag, use_threads, signals.clone())?;
426 (Some(expanded), paths)
428 } else {
429 let glob_options = if all {
430 None
431 } else {
432 let glob_options = MatchOptions {
433 recursive_match_hidden_dir: false,
434 ..Default::default()
435 };
436 Some(glob_options)
437 };
438 let (prefix, glob_paths) =
439 glob_from(&path, &cwd, call_span, glob_options, signals.clone())?;
440 let paths = glob_paths.map(|r| r.map(LsEntry::from_path));
442 (prefix, Box::new(paths))
443 };
444
445 let mut paths_peek = paths.peekable();
446 let no_matches = paths_peek.peek().is_none();
447 signals.check(&call_span)?;
448 if no_matches {
449 return Err(ShellError::Generic(
450 GenericError::new(
451 format!("No matches found for {:?}", path.item),
452 "Pattern, file or folder not found",
453 p_tag,
454 )
455 .with_help("no matches found"),
456 ));
457 }
458
459 let hidden_dirs = Arc::new(Mutex::new(Vec::new()));
460
461 let signals_clone = signals.clone();
462
463 let pool = if use_threads {
464 let count = std::thread::available_parallelism()
465 .map_err(|err| {
466 IoError::new_with_additional_context(
467 err,
468 call_span,
469 None,
470 "Could not get available parallelism",
471 )
472 })?
473 .get();
474 create_pool(count, call_span)?
475 } else {
476 create_pool(1, call_span)?
477 };
478
479 pool.install(|| {
480 rayon::spawn(move || {
481 let result = paths_peek
482 .par_bridge()
483 .filter_map(move |x| match x {
484 Ok(entry) => {
485 let hidden_dir_clone = Arc::clone(&hidden_dirs);
486 let mut hidden_dir_mutex = hidden_dir_clone
487 .lock()
488 .expect("Unable to acquire lock for hidden_dirs");
489 if path_contains_hidden_folder(&entry.path, &hidden_dir_mutex) {
490 return None;
491 }
492
493 if !all && !hidden_dir_specified && entry.is_hidden() {
494 if entry.is_dir() {
495 hidden_dir_mutex.push(entry.path.clone());
496 drop(hidden_dir_mutex);
497 }
498 return None;
499 }
500 let path = &entry.path;
502
503 let display_name = if short_names {
504 path.file_name().map(|os| os.to_string_lossy().to_string())
505 } else if full_paths || absolute_path {
506 Some(path.to_string_lossy().to_string())
507 } else if let Some(prefix) = &prefix {
508 if let Ok(remainder) = path.strip_prefix(prefix) {
509 if directory {
510 let path_diff = if let Some(path_diff_not_dot) =
512 diff_paths(path, &cwd)
513 {
514 let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
515 if path_diff_not_dot.is_empty() {
516 ".".to_string()
517 } else {
518 path_diff_not_dot.to_string()
519 }
520 } else {
521 path.to_string_lossy().to_string()
522 };
523
524 Some(path_diff)
525 } else {
526 let new_prefix = if let Some(pfx) = diff_paths(prefix, &cwd) {
527 pfx
528 } else {
529 prefix.to_path_buf()
530 };
531
532 Some(new_prefix.join(remainder).to_string_lossy().to_string())
533 }
534 } else {
535 Some(path.to_string_lossy().to_string())
536 }
537 } else {
538 Some(path.to_string_lossy().to_string())
539 }
540 .ok_or_else(|| {
541 ShellError::Generic(GenericError::new(
542 format!("Invalid file name: {:}", path.to_string_lossy()),
543 "invalid file name",
544 call_span,
545 ))
546 });
547
548 match display_name {
549 Ok(name) => {
550 let metadata = entry.get_metadata();
553 let path_for_dict = if full_paths && !path.is_absolute() {
555 std::borrow::Cow::Owned(cwd.join(path))
556 } else {
557 std::borrow::Cow::Borrowed(path)
558 };
559 let result = dir_entry_dict(
560 &path_for_dict,
561 &name,
562 metadata.as_ref(),
563 call_span,
564 long,
565 du,
566 &signals_clone,
567 use_mime_type,
568 full_paths,
569 );
570 match result {
571 Ok(value) => Some(value),
572 Err(err) => Some(Value::error(err, call_span)),
573 }
574 }
575 Err(err) => Some(Value::error(err, call_span)),
576 }
577 }
578 Err(err) => Some(Value::error(err, call_span)),
579 })
580 .try_for_each(|stream| {
581 tx.send(stream).map_err(|e| {
582 ShellError::Generic(GenericError::new(
583 "Error streaming data",
584 e.to_string(),
585 call_span,
586 ))
587 })
588 })
589 .map_err(|err| {
590 ShellError::Generic(GenericError::new(
591 "Unable to create a rayon pool",
592 err.to_string(),
593 call_span,
594 ))
595 });
596
597 if let Err(error) = result {
598 let _ = tx.send(Value::error(error, call_span));
599 }
600 });
601 });
602
603 Ok(rx
604 .into_iter()
605 .into_pipeline_data(call_span, signals.clone()))
606}
607
608fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
609 #[cfg(windows)]
610 {
611 use std::os::windows::fs::MetadataExt;
612
613 if let Ok(metadata) = dir.as_ref().metadata() {
614 let attributes = metadata.file_attributes();
615 (attributes & 0x2) != 0
617 } else {
618 false
619 }
620 }
621
622 #[cfg(not(windows))]
623 {
624 dir.as_ref()
625 .file_name()
626 .map(|name| name.to_string_lossy().starts_with('.'))
627 .unwrap_or(false)
628 }
629}
630
631fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool {
632 if folders.iter().any(|p| path.starts_with(p.as_path())) {
633 return true;
634 }
635 false
636}
637
638#[cfg(unix)]
639use std::os::unix::fs::FileTypeExt;
640use std::path::Path;
641
642pub fn get_file_type(md: &std::fs::Metadata, display_name: &str, use_mime_type: bool) -> String {
643 let ft = md.file_type();
644 let mut file_type = "unknown";
645 if ft.is_dir() {
646 file_type = "dir";
647 } else if ft.is_file() {
648 file_type = "file";
649 } else if ft.is_symlink() {
650 file_type = "symlink";
651 } else {
652 #[cfg(unix)]
653 {
654 if ft.is_block_device() {
655 file_type = "block device";
656 } else if ft.is_char_device() {
657 file_type = "char device";
658 } else if ft.is_fifo() {
659 file_type = "pipe";
660 } else if ft.is_socket() {
661 file_type = "socket";
662 }
663 }
664 }
665 if use_mime_type {
666 let guess = mime_guess::from_path(display_name);
667 let mime_guess = match guess.first() {
668 Some(mime_type) => mime_type.essence_str().to_string(),
669 None => "unknown".to_string(),
670 };
671 if file_type == "file" {
672 mime_guess
673 } else {
674 file_type.to_string()
675 }
676 } else {
677 file_type.to_string()
678 }
679}
680
681fn escape_filename_control_chars(name: &str) -> String {
684 if !name.chars().any(|c| c.is_control()) {
685 return name.to_string();
686 }
687
688 let mut buf = String::with_capacity(name.len());
689 for c in name.chars() {
690 if c.is_control() {
691 buf.extend(c.escape_unicode());
692 } else {
693 buf.push(c);
694 }
695 }
696 buf
697}
698
699#[allow(clippy::too_many_arguments)]
700pub(crate) fn dir_entry_dict(
701 filename: &std::path::Path, display_name: &str, metadata: Option<&std::fs::Metadata>,
704 span: Span,
705 long: bool,
706 du: bool,
707 signals: &Signals,
708 use_mime_type: bool,
709 full_symlink_target: bool,
710) -> Result<Value, ShellError> {
711 #[cfg(windows)]
712 if metadata.is_none() {
713 return Ok(windows_helper::dir_entry_dict_windows_fallback(
714 filename,
715 display_name,
716 span,
717 long,
718 ));
719 }
720
721 let mut record = Record::new();
722 let mut file_type = "unknown".to_string();
723
724 record.push(
725 "name",
726 Value::string(escape_filename_control_chars(display_name), span),
727 );
728
729 if let Some(md) = metadata {
730 file_type = get_file_type(md, display_name, use_mime_type);
731 record.push("type", Value::string(file_type.clone(), span));
732 } else {
733 record.push("type", Value::nothing(span));
734 }
735
736 if long && let Some(md) = metadata {
737 record.push(
738 "target",
739 if md.file_type().is_symlink() {
740 if let Ok(path_to_link) = filename.read_link() {
741 if full_symlink_target && filename.parent().is_some() {
744 Value::string(
745 expand_path_with(
746 path_to_link,
747 filename
748 .parent()
749 .expect("already check the filename have a parent"),
750 true,
751 )
752 .to_string_lossy(),
753 span,
754 )
755 } else {
756 Value::string(path_to_link.to_string_lossy(), span)
757 }
758 } else {
759 Value::string("Could not obtain target file's path", span)
760 }
761 } else {
762 Value::nothing(span)
763 },
764 )
765 }
766
767 if long && let Some(md) = metadata {
768 record.push("readonly", Value::bool(md.permissions().readonly(), span));
769
770 #[cfg(unix)]
771 {
772 use nu_utils::filesystem::users;
773 use std::os::unix::fs::MetadataExt;
774
775 let mode = md.permissions().mode();
776 record.push(
777 "mode",
778 Value::string(umask::Mode::from(mode).to_string(), span),
779 );
780
781 let nlinks = md.nlink();
782 record.push("num_links", Value::int(nlinks as i64, span));
783
784 let inode = md.ino();
785 record.push("inode", Value::int(inode as i64, span));
786
787 record.push(
788 "user",
789 if let Some(user) = users::get_user_by_uid(md.uid().into()) {
790 Value::string(user.name, span)
791 } else {
792 Value::int(md.uid().into(), span)
793 },
794 );
795
796 record.push(
797 "group",
798 if let Some(group) = users::get_group_by_gid(md.gid().into()) {
799 Value::string(group.name, span)
800 } else {
801 Value::int(md.gid().into(), span)
802 },
803 );
804 }
805 }
806
807 record.push(
808 "size",
809 if let Some(md) = metadata {
810 let zero_sized = file_type == "pipe"
811 || file_type == "socket"
812 || file_type == "char device"
813 || file_type == "block device";
814
815 if md.is_dir() {
816 if du {
817 let params = DirBuilder::new(Span::new(0, 2), None, false, None, false);
818 let dir_size = DirInfo::new(filename, ¶ms, None, span, signals)?.get_size();
819
820 Value::filesize(dir_size as i64, span)
821 } else {
822 let dir_size: u64 = md.len();
823
824 Value::filesize(dir_size as i64, span)
825 }
826 } else if md.is_file() {
827 Value::filesize(md.len() as i64, span)
828 } else if md.file_type().is_symlink() {
829 if let Ok(symlink_md) = filename.symlink_metadata() {
830 Value::filesize(symlink_md.len() as i64, span)
831 } else {
832 Value::nothing(span)
833 }
834 } else if zero_sized {
835 Value::filesize(0, span)
836 } else {
837 Value::nothing(span)
838 }
839 } else {
840 Value::nothing(span)
841 },
842 );
843
844 if let Some(md) = metadata {
845 if long {
846 record.push("created", {
847 let mut val = Value::nothing(span);
848 if let Ok(c) = md.created()
849 && let Some(local) = try_convert_to_local_date_time(c)
850 {
851 val = Value::date(local.with_timezone(local.offset()), span);
852 }
853 val
854 });
855
856 record.push("accessed", {
857 let mut val = Value::nothing(span);
858 if let Ok(a) = md.accessed()
859 && let Some(local) = try_convert_to_local_date_time(a)
860 {
861 val = Value::date(local.with_timezone(local.offset()), span)
862 }
863 val
864 });
865 }
866
867 record.push("modified", {
868 let mut val = Value::nothing(span);
869 if let Ok(m) = md.modified()
870 && let Some(local) = try_convert_to_local_date_time(m)
871 {
872 val = Value::date(local.with_timezone(local.offset()), span);
873 }
874 val
875 })
876 } else {
877 if long {
878 record.push("created", Value::nothing(span));
879 record.push("accessed", Value::nothing(span));
880 }
881
882 record.push("modified", Value::nothing(span));
883 }
884
885 Ok(Value::record(record, span))
886}
887
888fn try_convert_to_local_date_time(t: SystemTime) -> Option<DateTime<Local>> {
891 let (sec, nsec) = match t.duration_since(UNIX_EPOCH) {
893 Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
894 Err(e) => {
895 let dur = e.duration();
897 let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos());
898 if nsec == 0 {
899 (-sec, 0)
900 } else {
901 (-sec - 1, 1_000_000_000 - nsec)
902 }
903 }
904 };
905
906 const NEG_UNIX_EPOCH: i64 = -11644473600; if sec == NEG_UNIX_EPOCH {
908 return None;
910 }
911 match Utc.timestamp_opt(sec, nsec) {
912 LocalResult::Single(t) => Some(t.with_timezone(&Local)),
913 _ => None,
914 }
915}
916
917#[cfg(windows)]
919fn unix_time_to_local_date_time(secs: i64) -> Option<DateTime<Local>> {
920 match Utc.timestamp_opt(secs, 0) {
921 LocalResult::Single(t) => Some(t.with_timezone(&Local)),
922 _ => None,
923 }
924}
925
926#[cfg(windows)]
927mod windows_helper {
928 use super::*;
929
930 use nu_protocol::shell_error;
931 use std::os::windows::prelude::OsStrExt;
932 use windows::Win32::Foundation::FILETIME;
933 use windows::Win32::Storage::FileSystem::{
934 FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_REPARSE_POINT, FindClose,
935 FindFirstFileW, WIN32_FIND_DATAW,
936 };
937 use windows::Win32::System::SystemServices::{
938 IO_REPARSE_TAG_MOUNT_POINT, IO_REPARSE_TAG_SYMLINK,
939 };
940
941 pub fn dir_entry_dict_windows_fallback(
945 filename: &Path,
946 display_name: &str,
947 span: Span,
948 long: bool,
949 ) -> Value {
950 let mut record = Record::new();
951
952 record.push(
953 "name",
954 Value::string(escape_filename_control_chars(display_name), span),
955 );
956
957 let find_data = match find_first_file(filename, span) {
958 Ok(fd) => fd,
959 Err(e) => {
960 log::error!("ls: '{}' {}", filename.to_string_lossy(), e);
964 return Value::record(record, span);
965 }
966 };
967
968 record.push(
969 "type",
970 Value::string(get_file_type_windows_fallback(&find_data), span),
971 );
972
973 if long {
974 record.push(
975 "target",
976 if is_symlink(&find_data) {
977 if let Ok(path_to_link) = filename.read_link() {
978 Value::string(path_to_link.to_string_lossy(), span)
979 } else {
980 Value::string("Could not obtain target file's path", span)
981 }
982 } else {
983 Value::nothing(span)
984 },
985 );
986
987 record.push(
988 "readonly",
989 Value::bool(
990 find_data.dwFileAttributes & FILE_ATTRIBUTE_READONLY.0 != 0,
991 span,
992 ),
993 );
994 }
995
996 let file_size = ((find_data.nFileSizeHigh as u64) << 32) | find_data.nFileSizeLow as u64;
997 record.push("size", Value::filesize(file_size as i64, span));
998
999 if long {
1000 record.push("created", {
1001 let mut val = Value::nothing(span);
1002 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftCreationTime);
1003 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
1004 val = Value::date(local.with_timezone(local.offset()), span);
1005 }
1006 val
1007 });
1008
1009 record.push("accessed", {
1010 let mut val = Value::nothing(span);
1011 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastAccessTime);
1012 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
1013 val = Value::date(local.with_timezone(local.offset()), span);
1014 }
1015 val
1016 });
1017 }
1018
1019 record.push("modified", {
1020 let mut val = Value::nothing(span);
1021 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastWriteTime);
1022 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
1023 val = Value::date(local.with_timezone(local.offset()), span);
1024 }
1025 val
1026 });
1027
1028 Value::record(record, span)
1029 }
1030
1031 fn unix_time_from_filetime(ft: &FILETIME) -> i64 {
1032 const EPOCH_AS_FILETIME: u64 = 116444736000000000;
1034 const HUNDREDS_OF_NANOSECONDS: u64 = 10000000;
1035
1036 let time_u64 = ((ft.dwHighDateTime as u64) << 32) | (ft.dwLowDateTime as u64);
1037 if time_u64 > 0 {
1038 let rel_to_linux_epoch = time_u64.saturating_sub(EPOCH_AS_FILETIME);
1039 let seconds_since_unix_epoch = rel_to_linux_epoch / HUNDREDS_OF_NANOSECONDS;
1040 return seconds_since_unix_epoch as i64;
1041 }
1042 0
1043 }
1044
1045 fn find_first_file(filename: &Path, span: Span) -> Result<WIN32_FIND_DATAW, ShellError> {
1047 unsafe {
1048 let mut find_data = WIN32_FIND_DATAW::default();
1049 let filename_wide: Vec<u16> = filename
1051 .as_os_str()
1052 .encode_wide()
1053 .chain(std::iter::once(0))
1054 .collect();
1055
1056 match FindFirstFileW(
1057 windows::core::PCWSTR(filename_wide.as_ptr()),
1058 &mut find_data,
1059 ) {
1060 Ok(handle) => {
1061 let _ = FindClose(handle);
1066 Ok(find_data)
1067 }
1068 Err(e) => Err(ShellError::Io(IoError::new_with_additional_context(
1069 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Other),
1070 span,
1071 PathBuf::from(filename),
1072 format!("Could not read metadata: {e}"),
1073 ))),
1074 }
1075 }
1076 }
1077
1078 fn get_file_type_windows_fallback(find_data: &WIN32_FIND_DATAW) -> String {
1079 if find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY.0 != 0 {
1080 return "dir".to_string();
1081 }
1082
1083 if is_symlink(find_data) {
1084 return "symlink".to_string();
1085 }
1086
1087 "file".to_string()
1088 }
1089
1090 fn is_symlink(find_data: &WIN32_FIND_DATAW) -> bool {
1091 if find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 {
1092 if find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK
1095 || find_data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT
1096 {
1097 return true;
1098 }
1099 }
1100 false
1101 }
1102}
1103
1104#[allow(clippy::type_complexity)]
1105fn read_dir(
1106 f: PathBuf,
1107 span: Span,
1108 use_threads: bool,
1109 signals: Signals,
1110) -> Result<Box<dyn Iterator<Item = Result<LsEntry, ShellError>> + Send>, ShellError> {
1111 let signals_clone = signals.clone();
1112 let items = f
1113 .read_dir()
1114 .map_err(|err| IoError::new(err, span, f.clone()))?
1115 .map(move |d| {
1116 signals_clone.check(&span)?;
1117 d.map(|entry| LsEntry::from_dir_entry(&entry))
1118 .map_err(|err| IoError::new(err, span, f.clone()))
1119 .map_err(ShellError::from)
1120 });
1121 if !use_threads {
1122 let mut collected = items.collect::<Vec<_>>();
1123 signals.check(&span)?;
1124 collected.sort_by(|a, b| match (a, b) {
1125 (Ok(a), Ok(b)) => a.path.cmp(&b.path),
1126 (Ok(_), Err(_)) => Ordering::Greater,
1127 (Err(_), Ok(_)) => Ordering::Less,
1128 (Err(_), Err(_)) => Ordering::Equal,
1129 });
1130 return Ok(Box::new(collected.into_iter()));
1131 }
1132 Ok(Box::new(items))
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::escape_filename_control_chars;
1138
1139 #[test]
1140 fn escape_filename_control_chars_renders_control_chars_visibly() {
1141 assert_eq!(escape_filename_control_chars("hello.txt"), "hello.txt");
1143 assert_eq!(escape_filename_control_chars("hooks\x1bE"), "hooks\\u{1b}E");
1145 assert_eq!(
1147 escape_filename_control_chars("file\x00name"),
1148 "file\\u{0}name"
1149 );
1150 assert_eq!(
1152 escape_filename_control_chars("\x01a\x02b"),
1153 "\\u{1}a\\u{2}b"
1154 );
1155 }
1156}