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, 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 #[allow(deprecated)]
235 data_source: DataSource::Ls,
236 path_columns: vec!["name".to_string()],
237 ..Default::default()
238 },
239 ),
240 ),
241 Some(pattern) => {
242 let mut result_iters = vec![];
243 for pat in pattern {
244 result_iters.push(ls_for_one_pattern(
245 Some(pat),
246 args,
247 engine_state.signals().clone(),
248 cwd.clone(),
249 )?)
250 }
251
252 Ok(result_iters
255 .into_iter()
256 .flatten()
257 .into_pipeline_data_with_metadata(
258 call_span,
259 engine_state.signals().clone(),
260 PipelineMetadata {
261 #[allow(deprecated)]
262 data_source: DataSource::Ls,
263 path_columns: vec!["name".to_string()],
264 ..Default::default()
265 },
266 ))
267 }
268 }
269 }
270
271 fn examples(&self) -> Vec<Example<'_>> {
272 vec![
273 Example {
274 description: "List visible files in the current directory.",
275 example: "ls",
276 result: None,
277 },
278 Example {
279 description: "List visible files in a subdirectory.",
280 example: "ls subdir",
281 result: None,
282 },
283 Example {
284 description: "List visible files with full path in the parent directory.",
285 example: "ls -f ..",
286 result: None,
287 },
288 Example {
289 description: "List Rust files.",
290 example: "ls *.rs",
291 result: None,
292 },
293 Example {
294 description: "List files and directories whose name do not contain 'bar'.",
295 example: "ls | where name !~ bar",
296 result: None,
297 },
298 Example {
299 description: "List the full path of all dirs in your home directory.",
300 example: "ls -a ~ | where type == dir",
301 result: None,
302 },
303 Example {
304 description: "List only the names (not paths) of all dirs in your home directory which have not been modified in 7 days.",
305 example: "ls -as ~ | where type == dir and modified < ((date now) - 7day)",
306 result: None,
307 },
308 Example {
309 description: "Recursively list all files and subdirectories under the current directory using a glob pattern.",
310 example: "ls -a **/*",
311 result: None,
312 },
313 Example {
314 description: "Recursively list *.rs and *.toml files using the glob command.",
315 example: "ls ...(glob **/*.{rs,toml})",
316 result: None,
317 },
318 Example {
319 description: "List given paths and show directories themselves.",
320 example: "['/path/to/directory' '/path/to/file'] | each {|| ls -D $in } | flatten",
321 result: None,
322 },
323 ]
324 }
325}
326
327fn ls_for_one_pattern(
328 pattern_arg: Option<Spanned<NuGlob>>,
329 args: Args,
330 signals: Signals,
331 cwd: PathBuf,
332) -> Result<PipelineData, ShellError> {
333 fn create_pool(num_threads: usize, call_span: Span) -> Result<rayon::ThreadPool, ShellError> {
334 match rayon::ThreadPoolBuilder::new()
335 .num_threads(num_threads)
336 .build()
337 {
338 Err(e) => Err(e).map_err(|e| {
339 ShellError::Generic(GenericError::new(
340 "Error creating thread pool",
341 e.to_string(),
342 call_span,
343 ))
344 }),
345 Ok(pool) => Ok(pool),
346 }
347 }
348
349 let (tx, rx) = mpsc::channel();
350
351 let Args {
352 all,
353 long,
354 short_names,
355 full_paths,
356 du,
357 directory,
358 use_mime_type,
359 use_threads,
360 call_span,
361 } = args;
362 let pattern_arg = {
363 if let Some(path) = pattern_arg {
364 if path.item.as_ref().is_empty() {
366 return Err(ShellError::Io(IoError::new_with_additional_context(
367 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::NotFound),
368 path.span,
369 PathBuf::from(path.item.to_string()),
370 "empty string('') directory or file does not exist",
371 )));
372 }
373 match path.item {
374 NuGlob::DoNotExpand(p) => Some(Spanned {
375 item: NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(p)),
376 span: path.span,
377 }),
378 NuGlob::Expand(p) => Some(Spanned {
379 item: NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(p)),
380 span: path.span,
381 }),
382 }
383 } else {
384 pattern_arg
385 }
386 };
387
388 let mut just_read_dir = false;
389 let p_tag: Span = pattern_arg.as_ref().map(|p| p.span).unwrap_or(call_span);
390 let (pattern_arg, absolute_path) = match pattern_arg {
391 Some(pat) => {
392 let tmp_expanded =
394 nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand());
395 if !directory && tmp_expanded.is_dir() {
397 if read_dir(tmp_expanded, p_tag, use_threads, signals.clone())?
398 .next()
399 .is_none()
400 {
401 return Ok(Value::test_nothing().into_pipeline_data());
402 }
403 just_read_dir = !(pat.item.is_expand() && nu_glob::is_glob(pat.item.as_ref()));
404 }
405
406 let absolute_path = Path::new(pat.item.as_ref()).is_absolute()
412 || (pat.item.is_expand() && expand_to_real_path(pat.item.as_ref()).is_absolute());
413 (pat.item, absolute_path)
414 }
415 None => {
416 if directory {
418 (NuGlob::Expand(".".to_string()), false)
419 } else if read_dir(cwd.clone(), p_tag, use_threads, signals.clone())?
420 .next()
421 .is_none()
422 {
423 return Ok(Value::test_nothing().into_pipeline_data());
424 } else {
425 (NuGlob::Expand("*".to_string()), false)
426 }
427 }
428 };
429
430 let hidden_dir_specified = is_hidden_dir(pattern_arg.as_ref());
431
432 let path = pattern_arg.into_spanned(p_tag);
433 let (prefix, paths): (
434 Option<PathBuf>,
435 Box<dyn Iterator<Item = Result<LsEntry, ShellError>> + Send>,
436 ) = if just_read_dir {
437 let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand());
438 let paths = read_dir(expanded.clone(), p_tag, use_threads, signals.clone())?;
439 (Some(expanded), paths)
441 } else {
442 let glob_options = if all {
443 None
444 } else {
445 let glob_options = MatchOptions {
446 recursive_match_hidden_dir: false,
447 ..Default::default()
448 };
449 Some(glob_options)
450 };
451 let (prefix, glob_paths) =
452 glob_from(&path, &cwd, call_span, glob_options, signals.clone())?;
453 let paths = glob_paths.map(|r| r.map(LsEntry::from_path));
455 (prefix, Box::new(paths))
456 };
457
458 let mut paths_peek = paths.peekable();
459 let no_matches = paths_peek.peek().is_none();
460 signals.check(&call_span)?;
461 if no_matches {
462 return Err(ShellError::Generic(
463 GenericError::new(
464 format!("No matches found for {:?}", path.item),
465 "Pattern, file or folder not found",
466 p_tag,
467 )
468 .with_help("no matches found"),
469 ));
470 }
471
472 let hidden_dirs = Arc::new(Mutex::new(Vec::new()));
473
474 let signals_clone = signals.clone();
475
476 let pool = if use_threads {
477 let count = std::thread::available_parallelism()
478 .map_err(|err| {
479 IoError::new_with_additional_context(
480 err,
481 call_span,
482 None,
483 "Could not get available parallelism",
484 )
485 })?
486 .get();
487 create_pool(count, call_span)?
488 } else {
489 create_pool(1, call_span)?
490 };
491
492 pool.install(|| {
493 rayon::spawn(move || {
494 let result = paths_peek
495 .par_bridge()
496 .filter_map(move |x| match x {
497 Ok(entry) => {
498 let hidden_dir_clone = Arc::clone(&hidden_dirs);
499 let mut hidden_dir_mutex = hidden_dir_clone
500 .lock()
501 .expect("Unable to acquire lock for hidden_dirs");
502 if path_contains_hidden_folder(&entry.path, &hidden_dir_mutex) {
503 return None;
504 }
505
506 if !all && !hidden_dir_specified && entry.is_hidden() {
507 if entry.is_dir() {
508 hidden_dir_mutex.push(entry.path.clone());
509 drop(hidden_dir_mutex);
510 }
511 return None;
512 }
513 let path = &entry.path;
515
516 let display_name = if short_names {
517 path.file_name().map(|os| os.to_string_lossy().to_string())
518 } else if full_paths || absolute_path {
519 Some(path.to_string_lossy().to_string())
520 } else if let Some(prefix) = &prefix {
521 if let Ok(remainder) = path.strip_prefix(prefix) {
522 if directory {
523 let path_diff = if let Some(path_diff_not_dot) =
525 diff_paths(path, &cwd)
526 {
527 let path_diff_not_dot = path_diff_not_dot.to_string_lossy();
528 if path_diff_not_dot.is_empty() {
529 ".".to_string()
530 } else {
531 path_diff_not_dot.to_string()
532 }
533 } else {
534 path.to_string_lossy().to_string()
535 };
536
537 Some(path_diff)
538 } else {
539 let new_prefix = if let Some(pfx) = diff_paths(prefix, &cwd) {
540 pfx
541 } else {
542 prefix.to_path_buf()
543 };
544
545 Some(new_prefix.join(remainder).to_string_lossy().to_string())
546 }
547 } else {
548 Some(path.to_string_lossy().to_string())
549 }
550 } else {
551 Some(path.to_string_lossy().to_string())
552 }
553 .ok_or_else(|| {
554 ShellError::Generic(GenericError::new(
555 format!("Invalid file name: {:}", path.to_string_lossy()),
556 "invalid file name",
557 call_span,
558 ))
559 });
560
561 match display_name {
562 Ok(name) => {
563 let metadata = entry.get_metadata();
566 let path_for_dict = if full_paths && !path.is_absolute() {
568 std::borrow::Cow::Owned(cwd.join(path))
569 } else {
570 std::borrow::Cow::Borrowed(path)
571 };
572 let result = dir_entry_dict(
573 &path_for_dict,
574 &name,
575 metadata.as_ref(),
576 call_span,
577 long,
578 du,
579 &signals_clone,
580 use_mime_type,
581 full_paths,
582 );
583 match result {
584 Ok(value) => Some(value),
585 Err(err) => Some(Value::error(err, call_span)),
586 }
587 }
588 Err(err) => Some(Value::error(err, call_span)),
589 }
590 }
591 Err(err) => Some(Value::error(err, call_span)),
592 })
593 .try_for_each(|stream| {
594 tx.send(stream).map_err(|e| {
595 ShellError::Generic(GenericError::new(
596 "Error streaming data",
597 e.to_string(),
598 call_span,
599 ))
600 })
601 })
602 .map_err(|err| {
603 ShellError::Generic(GenericError::new(
604 "Unable to create a rayon pool",
605 err.to_string(),
606 call_span,
607 ))
608 });
609
610 if let Err(error) = result {
611 let _ = tx.send(Value::error(error, call_span));
612 }
613 });
614 });
615
616 Ok(rx
617 .into_iter()
618 .into_pipeline_data(call_span, signals.clone()))
619}
620
621fn is_hidden_dir(dir: impl AsRef<Path>) -> bool {
622 #[cfg(windows)]
623 {
624 use std::os::windows::fs::MetadataExt;
625
626 if let Ok(metadata) = dir.as_ref().metadata() {
627 let attributes = metadata.file_attributes();
628 (attributes & 0x2) != 0
630 } else {
631 false
632 }
633 }
634
635 #[cfg(not(windows))]
636 {
637 dir.as_ref()
638 .file_name()
639 .map(|name| name.to_string_lossy().starts_with('.'))
640 .unwrap_or(false)
641 }
642}
643
644fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool {
645 if folders.iter().any(|p| path.starts_with(p.as_path())) {
646 return true;
647 }
648 false
649}
650
651#[cfg(unix)]
652use std::os::unix::fs::FileTypeExt;
653use std::path::Path;
654
655pub fn get_file_type(md: &std::fs::Metadata, display_name: &str, use_mime_type: bool) -> String {
656 let ft = md.file_type();
657 let mut file_type = "unknown";
658 if ft.is_dir() {
659 file_type = "dir";
660 } else if ft.is_file() {
661 file_type = "file";
662 } else if ft.is_symlink() {
663 file_type = "symlink";
664 } else {
665 #[cfg(unix)]
666 {
667 if ft.is_block_device() {
668 file_type = "block device";
669 } else if ft.is_char_device() {
670 file_type = "char device";
671 } else if ft.is_fifo() {
672 file_type = "pipe";
673 } else if ft.is_socket() {
674 file_type = "socket";
675 }
676 }
677 }
678 if use_mime_type {
679 let guess = mime_guess::from_path(display_name);
680 let mime_guess = match guess.first() {
681 Some(mime_type) => mime_type.essence_str().to_string(),
682 None => "unknown".to_string(),
683 };
684 if file_type == "file" {
685 mime_guess
686 } else {
687 file_type.to_string()
688 }
689 } else {
690 file_type.to_string()
691 }
692}
693
694fn escape_filename_control_chars(name: &str) -> String {
697 if !name.chars().any(|c| c.is_control()) {
698 return name.to_string();
699 }
700
701 let mut buf = String::with_capacity(name.len());
702 for c in name.chars() {
703 if c.is_control() {
704 buf.extend(c.escape_unicode());
705 } else {
706 buf.push(c);
707 }
708 }
709 buf
710}
711
712#[allow(clippy::too_many_arguments)]
713pub(crate) fn dir_entry_dict(
714 filename: &std::path::Path, display_name: &str, metadata: Option<&std::fs::Metadata>,
717 span: Span,
718 long: bool,
719 du: bool,
720 signals: &Signals,
721 use_mime_type: bool,
722 full_symlink_target: bool,
723) -> Result<Value, ShellError> {
724 #[cfg(windows)]
725 if metadata.is_none() {
726 return Ok(windows_helper::dir_entry_dict_windows_fallback(
727 filename,
728 display_name,
729 span,
730 long,
731 ));
732 }
733
734 let mut record = Record::new();
735 let mut file_type = "unknown".to_string();
736
737 record.push(
738 "name",
739 Value::string(escape_filename_control_chars(display_name), span),
740 );
741
742 if let Some(md) = metadata {
743 file_type = get_file_type(md, display_name, use_mime_type);
744 record.push("type", Value::string(file_type.clone(), span));
745 } else {
746 record.push("type", Value::nothing(span));
747 }
748
749 if long && let Some(md) = metadata {
750 record.push(
751 "target",
752 if md.file_type().is_symlink() {
753 if let Ok(path_to_link) = filename.read_link() {
754 if full_symlink_target && filename.parent().is_some() {
757 Value::string(
758 expand_path_with(
759 path_to_link,
760 filename
761 .parent()
762 .expect("already check the filename have a parent"),
763 true,
764 )
765 .to_string_lossy(),
766 span,
767 )
768 } else {
769 Value::string(path_to_link.to_string_lossy(), span)
770 }
771 } else {
772 Value::string("Could not obtain target file's path", span)
773 }
774 } else {
775 Value::nothing(span)
776 },
777 )
778 }
779
780 if long && let Some(md) = metadata {
781 record.push("readonly", Value::bool(md.permissions().readonly(), span));
782
783 #[cfg(unix)]
784 {
785 use nu_utils::filesystem::users;
786 use std::os::unix::fs::MetadataExt;
787
788 let mode = md.permissions().mode();
789 record.push(
790 "mode",
791 Value::string(umask::Mode::from(mode).to_string(), span),
792 );
793
794 let nlinks = md.nlink();
795 record.push("num_links", Value::int(nlinks as i64, span));
796
797 let inode = md.ino();
798 record.push("inode", Value::int(inode as i64, span));
799
800 record.push(
801 "user",
802 if let Some(user) = users::get_user_by_uid(md.uid().into()) {
803 Value::string(user.name, span)
804 } else {
805 Value::int(md.uid().into(), span)
806 },
807 );
808
809 record.push(
810 "group",
811 if let Some(group) = users::get_group_by_gid(md.gid().into()) {
812 Value::string(group.name, span)
813 } else {
814 Value::int(md.gid().into(), span)
815 },
816 );
817 }
818 }
819
820 record.push(
821 "size",
822 if let Some(md) = metadata {
823 let zero_sized = file_type == "pipe"
824 || file_type == "socket"
825 || file_type == "char device"
826 || file_type == "block device";
827
828 if md.is_dir() {
829 if du {
830 let params = DirBuilder::new(Span::new(0, 2), None, false, None, false);
831 let dir_size = DirInfo::new(filename, ¶ms, None, span, signals)?.get_size();
832
833 Value::filesize(dir_size as i64, span)
834 } else {
835 let dir_size: u64 = md.len();
836
837 Value::filesize(dir_size as i64, span)
838 }
839 } else if md.is_file() {
840 Value::filesize(md.len() as i64, span)
841 } else if md.file_type().is_symlink() {
842 if let Ok(symlink_md) = filename.symlink_metadata() {
843 Value::filesize(symlink_md.len() as i64, span)
844 } else {
845 Value::nothing(span)
846 }
847 } else if zero_sized {
848 Value::filesize(0, span)
849 } else {
850 Value::nothing(span)
851 }
852 } else {
853 Value::nothing(span)
854 },
855 );
856
857 if let Some(md) = metadata {
858 if long {
859 record.push("created", {
860 let mut val = Value::nothing(span);
861 if let Ok(c) = md.created()
862 && let Some(local) = try_convert_to_local_date_time(c)
863 {
864 val = Value::date(local.with_timezone(local.offset()), span);
865 }
866 val
867 });
868
869 record.push("accessed", {
870 let mut val = Value::nothing(span);
871 if let Ok(a) = md.accessed()
872 && let Some(local) = try_convert_to_local_date_time(a)
873 {
874 val = Value::date(local.with_timezone(local.offset()), span)
875 }
876 val
877 });
878 }
879
880 record.push("modified", {
881 let mut val = Value::nothing(span);
882 if let Ok(m) = md.modified()
883 && let Some(local) = try_convert_to_local_date_time(m)
884 {
885 val = Value::date(local.with_timezone(local.offset()), span);
886 }
887 val
888 })
889 } else {
890 if long {
891 record.push("created", Value::nothing(span));
892 record.push("accessed", Value::nothing(span));
893 }
894
895 record.push("modified", Value::nothing(span));
896 }
897
898 Ok(Value::record(record, span))
899}
900
901fn try_convert_to_local_date_time(t: SystemTime) -> Option<DateTime<Local>> {
904 let (sec, nsec) = match t.duration_since(UNIX_EPOCH) {
906 Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
907 Err(e) => {
908 let dur = e.duration();
910 let (sec, nsec) = (dur.as_secs() as i64, dur.subsec_nanos());
911 if nsec == 0 {
912 (-sec, 0)
913 } else {
914 (-sec - 1, 1_000_000_000 - nsec)
915 }
916 }
917 };
918
919 const NEG_UNIX_EPOCH: i64 = -11644473600; if sec == NEG_UNIX_EPOCH {
921 return None;
923 }
924 match Utc.timestamp_opt(sec, nsec) {
925 LocalResult::Single(t) => Some(t.with_timezone(&Local)),
926 _ => None,
927 }
928}
929
930#[cfg(windows)]
932fn unix_time_to_local_date_time(secs: i64) -> Option<DateTime<Local>> {
933 match Utc.timestamp_opt(secs, 0) {
934 LocalResult::Single(t) => Some(t.with_timezone(&Local)),
935 _ => None,
936 }
937}
938
939#[cfg(windows)]
940mod windows_helper {
941 use super::*;
942
943 use nu_protocol::shell_error;
944 use std::os::windows::prelude::OsStrExt;
945 use windows::Win32::Foundation::FILETIME;
946 use windows::Win32::Storage::FileSystem::{
947 FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_REPARSE_POINT, FindClose,
948 FindFirstFileW, WIN32_FIND_DATAW,
949 };
950 use windows::Win32::System::SystemServices::{
951 IO_REPARSE_TAG_MOUNT_POINT, IO_REPARSE_TAG_SYMLINK,
952 };
953
954 pub fn dir_entry_dict_windows_fallback(
958 filename: &Path,
959 display_name: &str,
960 span: Span,
961 long: bool,
962 ) -> Value {
963 let mut record = Record::new();
964
965 record.push(
966 "name",
967 Value::string(escape_filename_control_chars(display_name), span),
968 );
969
970 let find_data = match find_first_file(filename, span) {
971 Ok(fd) => fd,
972 Err(e) => {
973 log::error!("ls: '{}' {}", filename.to_string_lossy(), e);
977 return Value::record(record, span);
978 }
979 };
980
981 record.push(
982 "type",
983 Value::string(get_file_type_windows_fallback(&find_data), span),
984 );
985
986 if long {
987 record.push(
988 "target",
989 if is_symlink(&find_data) {
990 if let Ok(path_to_link) = filename.read_link() {
991 Value::string(path_to_link.to_string_lossy(), span)
992 } else {
993 Value::string("Could not obtain target file's path", span)
994 }
995 } else {
996 Value::nothing(span)
997 },
998 );
999
1000 record.push(
1001 "readonly",
1002 Value::bool(
1003 find_data.dwFileAttributes & FILE_ATTRIBUTE_READONLY.0 != 0,
1004 span,
1005 ),
1006 );
1007 }
1008
1009 let file_size = ((find_data.nFileSizeHigh as u64) << 32) | find_data.nFileSizeLow as u64;
1010 record.push("size", Value::filesize(file_size as i64, span));
1011
1012 if long {
1013 record.push("created", {
1014 let mut val = Value::nothing(span);
1015 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftCreationTime);
1016 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
1017 val = Value::date(local.with_timezone(local.offset()), span);
1018 }
1019 val
1020 });
1021
1022 record.push("accessed", {
1023 let mut val = Value::nothing(span);
1024 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastAccessTime);
1025 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
1026 val = Value::date(local.with_timezone(local.offset()), span);
1027 }
1028 val
1029 });
1030 }
1031
1032 record.push("modified", {
1033 let mut val = Value::nothing(span);
1034 let seconds_since_unix_epoch = unix_time_from_filetime(&find_data.ftLastWriteTime);
1035 if let Some(local) = unix_time_to_local_date_time(seconds_since_unix_epoch) {
1036 val = Value::date(local.with_timezone(local.offset()), span);
1037 }
1038 val
1039 });
1040
1041 Value::record(record, span)
1042 }
1043
1044 fn unix_time_from_filetime(ft: &FILETIME) -> i64 {
1045 const EPOCH_AS_FILETIME: u64 = 116444736000000000;
1047 const HUNDREDS_OF_NANOSECONDS: u64 = 10000000;
1048
1049 let time_u64 = ((ft.dwHighDateTime as u64) << 32) | (ft.dwLowDateTime as u64);
1050 if time_u64 > 0 {
1051 let rel_to_linux_epoch = time_u64.saturating_sub(EPOCH_AS_FILETIME);
1052 let seconds_since_unix_epoch = rel_to_linux_epoch / HUNDREDS_OF_NANOSECONDS;
1053 return seconds_since_unix_epoch as i64;
1054 }
1055 0
1056 }
1057
1058 fn find_first_file(filename: &Path, span: Span) -> Result<WIN32_FIND_DATAW, ShellError> {
1060 unsafe {
1061 let mut find_data = WIN32_FIND_DATAW::default();
1062 let filename_wide: Vec<u16> = filename
1064 .as_os_str()
1065 .encode_wide()
1066 .chain(std::iter::once(0))
1067 .collect();
1068
1069 match FindFirstFileW(
1070 windows::core::PCWSTR(filename_wide.as_ptr()),
1071 &mut find_data,
1072 ) {
1073 Ok(handle) => {
1074 let _ = FindClose(handle);
1079 Ok(find_data)
1080 }
1081 Err(e) => Err(ShellError::Io(IoError::new_with_additional_context(
1082 shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Other),
1083 span,
1084 PathBuf::from(filename),
1085 format!("Could not read metadata: {e}"),
1086 ))),
1087 }
1088 }
1089 }
1090
1091 fn get_file_type_windows_fallback(find_data: &WIN32_FIND_DATAW) -> String {
1092 if find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY.0 != 0 {
1093 return "dir".to_string();
1094 }
1095
1096 if is_symlink(find_data) {
1097 return "symlink".to_string();
1098 }
1099
1100 "file".to_string()
1101 }
1102
1103 fn is_symlink(find_data: &WIN32_FIND_DATAW) -> bool {
1104 if find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 {
1105 if find_data.dwReserved0 == IO_REPARSE_TAG_SYMLINK
1108 || find_data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT
1109 {
1110 return true;
1111 }
1112 }
1113 false
1114 }
1115}
1116
1117#[allow(clippy::type_complexity)]
1118fn read_dir(
1119 f: PathBuf,
1120 span: Span,
1121 use_threads: bool,
1122 signals: Signals,
1123) -> Result<Box<dyn Iterator<Item = Result<LsEntry, ShellError>> + Send>, ShellError> {
1124 let signals_clone = signals.clone();
1125 let items = f
1126 .read_dir()
1127 .map_err(|err| IoError::new(err, span, f.clone()))?
1128 .map(move |d| {
1129 signals_clone.check(&span)?;
1130 d.map(|entry| LsEntry::from_dir_entry(&entry))
1131 .map_err(|err| IoError::new(err, span, f.clone()))
1132 .map_err(ShellError::from)
1133 });
1134 if !use_threads {
1135 let mut collected = items.collect::<Vec<_>>();
1136 signals.check(&span)?;
1137 collected.sort_by(|a, b| match (a, b) {
1138 (Ok(a), Ok(b)) => a.path.cmp(&b.path),
1139 (Ok(_), Err(_)) => Ordering::Greater,
1140 (Err(_), Ok(_)) => Ordering::Less,
1141 (Err(_), Err(_)) => Ordering::Equal,
1142 });
1143 return Ok(Box::new(collected.into_iter()));
1144 }
1145 Ok(Box::new(items))
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::escape_filename_control_chars;
1151
1152 #[test]
1153 fn escape_filename_control_chars_renders_control_chars_visibly() {
1154 assert_eq!(escape_filename_control_chars("hello.txt"), "hello.txt");
1156 assert_eq!(escape_filename_control_chars("hooks\x1bE"), "hooks\\u{1b}E");
1158 assert_eq!(
1160 escape_filename_control_chars("file\x00name"),
1161 "file\\u{0}name"
1162 );
1163 assert_eq!(
1165 escape_filename_control_chars("\x01a\x02b"),
1166 "\\u{1}a\\u{2}b"
1167 );
1168 }
1169}