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