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