1use std::cmp::Ordering;
2use std::collections::HashMap;
3use std::ffi::CString;
4use std::fs::{self, DirEntry, Metadata};
5use std::io::{self, BufWriter, Write};
6use std::os::unix::fs::MetadataExt;
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
9use std::time::SystemTime;
10
11static IS_C_LOCALE: AtomicBool = AtomicBool::new(false);
14
15pub fn detect_c_locale() {
18 let lc = unsafe { libc::setlocale(libc::LC_COLLATE, std::ptr::null()) };
19 if lc.is_null() {
20 IS_C_LOCALE.store(true, AtomicOrdering::Relaxed);
21 return;
22 }
23 let s = unsafe { std::ffi::CStr::from_ptr(lc) }.to_bytes();
24 let is_c = s == b"C" || s == b"POSIX";
25 IS_C_LOCALE.store(is_c, AtomicOrdering::Relaxed);
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum SortBy {
35 Name,
36 Size,
37 Time,
38 Extension,
39 Version,
40 None,
41 Width,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum OutputFormat {
47 Long,
48 SingleColumn,
49 Columns,
50 Comma,
51 Across,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ColorMode {
57 Always,
58 Auto,
59 Never,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum TimeField {
65 Mtime,
66 Atime,
67 Ctime,
68 Birth,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TimeStyle {
74 FullIso,
75 LongIso,
76 Iso,
77 Locale,
78 Custom(String),
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum IndicatorStyle {
84 None,
85 Slash,
86 FileType,
87 Classify,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum ClassifyMode {
93 Always,
94 Auto,
95 Never,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum QuotingStyle {
101 Literal,
102 Locale,
103 Shell,
104 ShellAlways,
105 ShellEscape,
106 ShellEscapeAlways,
107 C,
108 Escape,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum HyperlinkMode {
114 Always,
115 Auto,
116 Never,
117}
118
119#[derive(Debug, Clone)]
121pub struct LsConfig {
122 pub all: bool,
123 pub almost_all: bool,
124 pub long_format: bool,
125 pub human_readable: bool,
126 pub si: bool,
127 pub reverse: bool,
128 pub recursive: bool,
129 pub sort_by: SortBy,
130 pub format: OutputFormat,
131 pub classify: ClassifyMode,
132 pub color: ColorMode,
133 pub group_directories_first: bool,
134 pub show_inode: bool,
135 pub show_size: bool,
136 pub show_owner: bool,
137 pub show_group: bool,
138 pub numeric_ids: bool,
139 pub dereference: bool,
140 pub directory: bool,
141 pub time_field: TimeField,
142 pub time_style: TimeStyle,
143 pub ignore_patterns: Vec<String>,
144 pub ignore_backups: bool,
145 pub width: usize,
146 pub quoting_style: QuotingStyle,
147 pub hide_control_chars: bool,
148 pub kibibytes: bool,
149 pub indicator_style: IndicatorStyle,
150 pub tab_size: usize,
151 pub hyperlink: HyperlinkMode,
152 pub context: bool,
153 pub literal: bool,
154 pub zero: bool,
156 pub block_size: Option<u64>,
159 pub block_size_suffix: String,
161}
162
163impl Default for LsConfig {
164 fn default() -> Self {
165 LsConfig {
166 all: false,
167 almost_all: false,
168 long_format: false,
169 human_readable: false,
170 si: false,
171 reverse: false,
172 recursive: false,
173 sort_by: SortBy::Name,
174 format: OutputFormat::Columns,
175 classify: ClassifyMode::Never,
176 color: ColorMode::Auto,
177 group_directories_first: false,
178 show_inode: false,
179 show_size: false,
180 show_owner: true,
181 show_group: true,
182 numeric_ids: false,
183 dereference: false,
184 directory: false,
185 time_field: TimeField::Mtime,
186 time_style: TimeStyle::Locale,
187 ignore_patterns: Vec::new(),
188 ignore_backups: false,
189 width: 80,
190 quoting_style: QuotingStyle::Literal,
191 hide_control_chars: false,
192 kibibytes: false,
193 indicator_style: IndicatorStyle::None,
194 tab_size: 8,
195 hyperlink: HyperlinkMode::Never,
196 context: false,
197 literal: false,
198 zero: false,
199 block_size: None,
200 block_size_suffix: String::new(),
201 }
202 }
203}
204
205#[derive(Debug, Clone)]
211pub struct ColorDb {
212 pub map: HashMap<String, String>,
213 pub dir: String,
214 pub link: String,
215 pub exec: String,
216 pub pipe: String,
217 pub socket: String,
218 pub block_dev: String,
219 pub char_dev: String,
220 pub orphan: String,
221 pub setuid: String,
222 pub setgid: String,
223 pub sticky: String,
224 pub other_writable: String,
225 pub sticky_other_writable: String,
226 pub reset: String,
227}
228
229impl Default for ColorDb {
230 fn default() -> Self {
231 ColorDb {
232 map: HashMap::new(),
233 dir: "\x1b[01;34m".to_string(), link: "\x1b[01;36m".to_string(), exec: "\x1b[01;32m".to_string(), pipe: "\x1b[33m".to_string(), socket: "\x1b[01;35m".to_string(), block_dev: "\x1b[01;33m".to_string(), char_dev: "\x1b[01;33m".to_string(), orphan: "\x1b[01;31m".to_string(), setuid: "\x1b[37;41m".to_string(), setgid: "\x1b[30;43m".to_string(), sticky: "\x1b[37;44m".to_string(), other_writable: "\x1b[34;42m".to_string(), sticky_other_writable: "\x1b[30;42m".to_string(), reset: "\x1b[0m".to_string(),
247 }
248 }
249}
250
251impl ColorDb {
252 pub fn from_env() -> Self {
254 let mut db = ColorDb::default();
255 if let Ok(val) = std::env::var("LS_COLORS") {
256 for item in val.split(':') {
257 if let Some((key, code)) = item.split_once('=') {
258 let esc = format!("\x1b[{}m", code);
259 match key {
260 "di" => db.dir = esc,
261 "ln" => db.link = esc,
262 "ex" => db.exec = esc,
263 "pi" | "fi" if key == "pi" => db.pipe = esc,
264 "so" => db.socket = esc,
265 "bd" => db.block_dev = esc,
266 "cd" => db.char_dev = esc,
267 "or" => db.orphan = esc,
268 "su" => db.setuid = esc,
269 "sg" => db.setgid = esc,
270 "st" => db.sticky = esc,
271 "ow" => db.other_writable = esc,
272 "tw" => db.sticky_other_writable = esc,
273 "rs" => db.reset = esc,
274 _ => {
275 if key.starts_with('*') {
276 db.map.insert(key[1..].to_string(), esc);
277 }
278 }
279 }
280 }
281 }
282 }
283 db
284 }
285
286 fn color_for(&self, entry: &FileEntry) -> &str {
288 let mode = entry.mode;
289 let ft = mode & (libc::S_IFMT as u32);
290
291 if ft == libc::S_IFLNK as u32 {
293 if entry.link_target_ok {
294 return &self.link;
295 } else {
296 return &self.orphan;
297 }
298 }
299
300 if ft == libc::S_IFDIR as u32 {
302 let sticky = mode & (libc::S_ISVTX as u32) != 0;
303 let ow = mode & (libc::S_IWOTH as u32) != 0;
304 if sticky && ow {
305 return &self.sticky_other_writable;
306 }
307 if ow {
308 return &self.other_writable;
309 }
310 if sticky {
311 return &self.sticky;
312 }
313 return &self.dir;
314 }
315
316 if ft == libc::S_IFIFO as u32 {
318 return &self.pipe;
319 }
320 if ft == libc::S_IFSOCK as u32 {
321 return &self.socket;
322 }
323 if ft == libc::S_IFBLK as u32 {
324 return &self.block_dev;
325 }
326 if ft == libc::S_IFCHR as u32 {
327 return &self.char_dev;
328 }
329
330 if mode & (libc::S_ISUID as u32) != 0 {
332 return &self.setuid;
333 }
334 if mode & (libc::S_ISGID as u32) != 0 {
335 return &self.setgid;
336 }
337
338 if let Some(ext_pos) = entry.name.rfind('.') {
340 let ext = &entry.name[ext_pos..];
341 if let Some(c) = self.map.get(ext) {
342 return c;
343 }
344 }
345
346 if ft == libc::S_IFREG as u32
348 && mode & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32) != 0
349 {
350 return &self.exec;
351 }
352
353 ""
354 }
355}
356
357#[derive(Debug, Clone)]
363pub struct FileEntry {
364 pub name: String,
365 pub path: PathBuf,
366 pub sort_key: CString,
368 pub ino: u64,
369 pub nlink: u64,
370 pub mode: u32,
371 pub uid: u32,
372 pub gid: u32,
373 pub size: u64,
374 pub blocks: u64,
375 pub mtime: i64,
376 pub mtime_nsec: i64,
377 pub atime: i64,
378 pub atime_nsec: i64,
379 pub ctime: i64,
380 pub ctime_nsec: i64,
381 pub rdev_major: u32,
382 pub rdev_minor: u32,
383 pub is_dir: bool,
384 pub link_target: Option<String>,
385 pub link_target_ok: bool,
386 pub link_target_is_dir: bool,
388 pub link_target_mode: Option<u32>,
390}
391
392impl FileEntry {
393 fn from_dir_entry(de: &DirEntry, config: &LsConfig) -> io::Result<Self> {
395 let name = de.file_name().to_string_lossy().into_owned();
396 let path = de.path();
397
398 let meta = if config.dereference {
399 match fs::metadata(&path) {
400 Ok(m) => m,
401 Err(e) => {
402 if let Ok(lmeta) = fs::symlink_metadata(&path) {
404 if lmeta.file_type().is_symlink() {
405 if config.long_format {
408 eprintln!(
409 "ls: cannot access '{}': {}",
410 name,
411 crate::common::io_error_msg(&e)
412 );
413 return Ok(Self::broken_deref(name, path));
414 }
415 return Self::from_metadata(name, path, &lmeta, config);
417 }
418 }
419 return Err(e);
420 }
421 }
422 } else {
423 fs::symlink_metadata(&path)?
424 };
425
426 Self::from_metadata(name, path, &meta, config)
427 }
428
429 pub fn from_path_with_name(name: String, path: &Path, config: &LsConfig) -> io::Result<Self> {
432 let meta = if config.dereference {
433 fs::metadata(path).or_else(|_| fs::symlink_metadata(path))?
434 } else {
435 fs::symlink_metadata(path)?
436 };
437 Self::from_metadata(name, path.to_path_buf(), &meta, config)
438 }
439
440 fn from_metadata(
441 name: String,
442 path: PathBuf,
443 meta: &Metadata,
444 _config: &LsConfig,
445 ) -> io::Result<Self> {
446 let file_type = meta.file_type();
447 let is_symlink = file_type.is_symlink();
448
449 let (link_target, link_target_ok, link_target_is_dir, link_target_mode) = if is_symlink {
450 match fs::read_link(&path) {
451 Ok(target) => match fs::metadata(&path) {
452 Ok(target_meta) => {
453 let tmode = target_meta.mode();
454 (
455 Some(target.to_string_lossy().into_owned()),
456 true,
457 target_meta.is_dir(),
458 Some(tmode),
459 )
460 }
461 Err(_) => (
462 Some(target.to_string_lossy().into_owned()),
463 false,
464 false,
465 None,
466 ),
467 },
468 Err(_) => (None, false, false, None),
469 }
470 } else {
471 (None, true, false, None)
472 };
473
474 let rdev = meta.rdev();
475 let sort_key = CString::new(name.as_str()).unwrap_or_default();
476
477 Ok(FileEntry {
478 name,
479 path,
480 sort_key,
481 ino: meta.ino(),
482 nlink: meta.nlink(),
483 mode: meta.mode(),
484 uid: meta.uid(),
485 gid: meta.gid(),
486 size: meta.size(),
487 blocks: meta.blocks(),
488 mtime: meta.mtime(),
489 mtime_nsec: meta.mtime_nsec(),
490 atime: meta.atime(),
491 atime_nsec: meta.atime_nsec(),
492 ctime: meta.ctime(),
493 ctime_nsec: meta.ctime_nsec(),
494 rdev_major: ((rdev >> 8) & 0xfff) as u32,
495 rdev_minor: (rdev & 0xff) as u32,
496 is_dir: meta.is_dir(),
497 link_target,
498 link_target_ok,
499 link_target_is_dir,
500 link_target_mode,
501 })
502 }
503
504 fn time_secs(&self, field: TimeField) -> i64 {
506 match field {
507 TimeField::Mtime => self.mtime,
508 TimeField::Atime => self.atime,
509 TimeField::Ctime | TimeField::Birth => self.ctime,
510 }
511 }
512
513 fn time_nsec(&self, field: TimeField) -> i64 {
514 match field {
515 TimeField::Mtime => self.mtime_nsec,
516 TimeField::Atime => self.atime_nsec,
517 TimeField::Ctime | TimeField::Birth => self.ctime_nsec,
518 }
519 }
520
521 fn extension(&self) -> &str {
523 match self.name.rfind('.') {
524 Some(pos) if pos > 0 => &self.name[pos + 1..],
525 _ => "",
526 }
527 }
528
529 fn is_directory(&self) -> bool {
531 self.is_dir
532 }
533
534 fn indicator(&self, style: IndicatorStyle) -> &'static str {
536 let ft = self.mode & (libc::S_IFMT as u32);
537 match style {
538 IndicatorStyle::None => "",
539 IndicatorStyle::Slash => {
540 if ft == libc::S_IFDIR as u32 {
541 "/"
542 } else {
543 ""
544 }
545 }
546 IndicatorStyle::FileType => match ft {
547 x if x == libc::S_IFDIR as u32 => "/",
548 x if x == libc::S_IFLNK as u32 => "@",
549 x if x == libc::S_IFIFO as u32 => "|",
550 x if x == libc::S_IFSOCK as u32 => "=",
551 _ => "",
552 },
553 IndicatorStyle::Classify => match ft {
554 x if x == libc::S_IFDIR as u32 => "/",
555 x if x == libc::S_IFLNK as u32 => "@",
556 x if x == libc::S_IFIFO as u32 => "|",
557 x if x == libc::S_IFSOCK as u32 => "=",
558 _ => {
559 if ft == libc::S_IFREG as u32
560 && self.mode
561 & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32)
562 != 0
563 {
564 "*"
565 } else {
566 ""
567 }
568 }
569 },
570 }
571 }
572
573 pub fn broken_deref(name: String, path: PathBuf) -> Self {
576 let sort_key = CString::new(name.as_str()).unwrap_or_default();
577 FileEntry {
578 name,
579 path,
580 sort_key,
581 ino: 0,
582 nlink: 0, mode: libc::S_IFLNK as u32,
584 uid: 0,
585 gid: 0,
586 size: 0,
587 blocks: 0,
588 mtime: 0,
589 mtime_nsec: 0,
590 atime: 0,
591 atime_nsec: 0,
592 ctime: 0,
593 ctime_nsec: 0,
594 rdev_major: 0,
595 rdev_minor: 0,
596 is_dir: false,
597 link_target: None,
598 link_target_ok: false,
599 link_target_is_dir: false,
600 link_target_mode: None,
601 }
602 }
603
604 pub fn is_broken_deref(&self) -> bool {
606 self.nlink == 0 && (self.mode & libc::S_IFMT as u32) == libc::S_IFLNK as u32
607 }
608
609 fn display_width(&self, config: &LsConfig) -> usize {
611 let quoted = quote_name(&self.name, config);
612 let ind = self.indicator(config.indicator_style);
613 quoted.len() + ind.len()
614 }
615}
616
617pub fn quote_name(name: &str, config: &LsConfig) -> String {
623 match config.quoting_style {
624 QuotingStyle::Literal => {
625 if config.hide_control_chars {
626 hide_control(name)
627 } else {
628 name.to_string()
629 }
630 }
631 QuotingStyle::Escape => escape_name(name),
632 QuotingStyle::C => c_quote(name),
633 QuotingStyle::Shell => shell_quote(name, false, false),
634 QuotingStyle::ShellAlways => shell_quote(name, true, false),
635 QuotingStyle::ShellEscape => shell_quote(name, false, true),
636 QuotingStyle::ShellEscapeAlways => shell_quote(name, true, true),
637 QuotingStyle::Locale => locale_quote(name),
638 }
639}
640
641fn hide_control(name: &str) -> String {
642 name.chars()
643 .map(|c| if c.is_control() { '?' } else { c })
644 .collect()
645}
646
647fn escape_name(name: &str) -> String {
648 let mut out = String::with_capacity(name.len());
649 for c in name.chars() {
650 match c {
651 '\\' => out.push_str("\\\\"),
652 '\n' => out.push_str("\\n"),
653 '\r' => out.push_str("\\r"),
654 '\t' => out.push_str("\\t"),
655 ' ' => out.push_str("\\ "),
656 c if c.is_control() => {
657 out.push_str(&format!("\\{:03o}", c as u32));
658 }
659 c => out.push(c),
660 }
661 }
662 out
663}
664
665fn c_quote(name: &str) -> String {
666 let mut out = String::with_capacity(name.len() + 2);
667 out.push('"');
668 for c in name.chars() {
669 match c {
670 '"' => out.push_str("\\\""),
671 '\\' => out.push_str("\\\\"),
672 '\n' => out.push_str("\\n"),
673 '\r' => out.push_str("\\r"),
674 '\t' => out.push_str("\\t"),
675 '\x07' => out.push_str("\\a"),
676 '\x08' => out.push_str("\\b"),
677 '\x0C' => out.push_str("\\f"),
678 '\x0B' => out.push_str("\\v"),
679 c if c.is_control() => {
680 out.push_str(&format!("\\{:03o}", c as u32));
681 }
682 c => out.push(c),
683 }
684 }
685 out.push('"');
686 out
687}
688
689fn shell_quote(name: &str, always: bool, escape: bool) -> String {
690 let needs_quoting = name.is_empty()
691 || name
692 .chars()
693 .any(|c| " \t\n'\"\\|&;()<>!$`#~{}[]?*".contains(c) || c.is_control());
694
695 if !needs_quoting && !always {
696 return name.to_string();
697 }
698
699 if escape {
700 let has_control = name.chars().any(|c| c.is_control());
702 if has_control {
703 let mut out = String::with_capacity(name.len() + 4);
704 out.push_str("$'");
705 for c in name.chars() {
706 match c {
707 '\'' => out.push_str("\\'"),
708 '\\' => out.push_str("\\\\"),
709 '\n' => out.push_str("\\n"),
710 '\r' => out.push_str("\\r"),
711 '\t' => out.push_str("\\t"),
712 c if c.is_control() => {
713 out.push_str(&format!("\\{:03o}", c as u32));
714 }
715 c => out.push(c),
716 }
717 }
718 out.push('\'');
719 return out;
720 }
721 }
722
723 let mut out = String::with_capacity(name.len() + 2);
725 out.push('\'');
726 for c in name.chars() {
727 if c == '\'' {
728 out.push_str("'\\''");
729 } else {
730 out.push(c);
731 }
732 }
733 out.push('\'');
734 out
735}
736
737fn locale_quote(name: &str) -> String {
738 let mut out = String::with_capacity(name.len() + 2);
740 out.push('\u{2018}');
741 for c in name.chars() {
742 match c {
743 '\\' => out.push_str("\\\\"),
744 '\n' => out.push_str("\\n"),
745 '\r' => out.push_str("\\r"),
746 '\t' => out.push_str("\\t"),
747 c if c.is_control() => {
748 out.push_str(&format!("\\{:03o}", c as u32));
749 }
750 c => out.push(c),
751 }
752 }
753 out.push('\u{2019}');
754 out
755}
756
757pub(crate) fn version_cmp(a: &str, b: &str) -> Ordering {
763 let ab = a.as_bytes();
764 let bb = b.as_bytes();
765 let mut ai = 0;
766 let mut bi = 0;
767 while ai < ab.len() && bi < bb.len() {
768 let ac = ab[ai];
769 let bc = bb[bi];
770 if ac.is_ascii_digit() && bc.is_ascii_digit() {
771 let a_start = ai;
773 let b_start = bi;
774 while ai < ab.len() && ab[ai] == b'0' {
775 ai += 1;
776 }
777 while bi < bb.len() && bb[bi] == b'0' {
778 bi += 1;
779 }
780 let a_num_start = ai;
781 let b_num_start = bi;
782 while ai < ab.len() && ab[ai].is_ascii_digit() {
783 ai += 1;
784 }
785 while bi < bb.len() && bb[bi].is_ascii_digit() {
786 bi += 1;
787 }
788 let a_len = ai - a_num_start;
789 let b_len = bi - b_num_start;
790 if a_len != b_len {
791 return a_len.cmp(&b_len);
792 }
793 let ord = ab[a_num_start..ai].cmp(&bb[b_num_start..bi]);
794 if ord != Ordering::Equal {
795 return ord;
796 }
797 let a_zeros = a_num_start - a_start;
799 let b_zeros = b_num_start - b_start;
800 if a_zeros != b_zeros {
801 return a_zeros.cmp(&b_zeros);
802 }
803 } else {
804 let ord = ac.cmp(&bc);
805 if ord != Ordering::Equal {
806 return ord;
807 }
808 ai += 1;
809 bi += 1;
810 }
811 }
812 ab.len().cmp(&bb.len())
813}
814
815fn sort_entries(entries: &mut [FileEntry], config: &LsConfig) {
816 if config.group_directories_first {
817 entries.sort_by(|a, b| {
819 let a_dir = a.is_directory();
820 let b_dir = b.is_directory();
821 match (a_dir, b_dir) {
822 (true, false) => Ordering::Less,
823 (false, true) => Ordering::Greater,
824 _ => compare_entries(a, b, config),
825 }
826 });
827 } else {
828 entries.sort_by(|a, b| compare_entries(a, b, config));
829 }
830}
831
832#[inline]
836fn locale_cmp_cstr(a: &CString, b: &CString) -> Ordering {
837 if IS_C_LOCALE.load(AtomicOrdering::Relaxed) {
838 a.as_bytes().cmp(b.as_bytes())
839 } else {
840 let result = unsafe { libc::strcoll(a.as_ptr(), b.as_ptr()) };
841 result.cmp(&0)
842 }
843}
844
845fn locale_cmp(a: &str, b: &str) -> Ordering {
847 if IS_C_LOCALE.load(AtomicOrdering::Relaxed) {
848 a.cmp(b)
849 } else {
850 let ca = CString::new(a).unwrap_or_default();
851 let cb = CString::new(b).unwrap_or_default();
852 let result = unsafe { libc::strcoll(ca.as_ptr(), cb.as_ptr()) };
853 result.cmp(&0)
854 }
855}
856
857fn compare_entries(a: &FileEntry, b: &FileEntry, config: &LsConfig) -> Ordering {
858 let ord = match config.sort_by {
860 SortBy::Name => locale_cmp_cstr(&a.sort_key, &b.sort_key),
861 SortBy::Size => {
862 let size_ord = b.size.cmp(&a.size);
863 if size_ord == Ordering::Equal {
864 locale_cmp_cstr(&a.sort_key, &b.sort_key)
865 } else {
866 size_ord
867 }
868 }
869 SortBy::Time => {
870 let ta = a.time_secs(config.time_field);
871 let tb = b.time_secs(config.time_field);
872 let ord = tb.cmp(&ta);
873 if ord == Ordering::Equal {
874 let na = a.time_nsec(config.time_field);
875 let nb = b.time_nsec(config.time_field);
876 let nsec_ord = nb.cmp(&na);
877 if nsec_ord == Ordering::Equal {
878 locale_cmp_cstr(&a.sort_key, &b.sort_key)
879 } else {
880 nsec_ord
881 }
882 } else {
883 ord
884 }
885 }
886 SortBy::Extension => {
887 let ea = a.extension();
888 let eb = b.extension();
889 let ord = locale_cmp(ea, eb);
890 if ord == Ordering::Equal {
891 locale_cmp_cstr(&a.sort_key, &b.sort_key)
892 } else {
893 ord
894 }
895 }
896 SortBy::Version => version_cmp(&a.name, &b.name),
897 SortBy::None => Ordering::Equal,
898 SortBy::Width => {
899 let wa = a.display_width(config);
900 let wb = b.display_width(config);
901 wa.cmp(&wb)
902 }
903 };
904
905 if config.reverse { ord.reverse() } else { ord }
906}
907
908pub fn format_permissions(mode: u32) -> String {
914 let mut s = String::with_capacity(10);
915
916 s.push(match mode & (libc::S_IFMT as u32) {
918 x if x == libc::S_IFDIR as u32 => 'd',
919 x if x == libc::S_IFLNK as u32 => 'l',
920 x if x == libc::S_IFBLK as u32 => 'b',
921 x if x == libc::S_IFCHR as u32 => 'c',
922 x if x == libc::S_IFIFO as u32 => 'p',
923 x if x == libc::S_IFSOCK as u32 => 's',
924 _ => '-',
925 });
926
927 s.push(if mode & (libc::S_IRUSR as u32) != 0 {
929 'r'
930 } else {
931 '-'
932 });
933 s.push(if mode & (libc::S_IWUSR as u32) != 0 {
934 'w'
935 } else {
936 '-'
937 });
938 s.push(if mode & (libc::S_ISUID as u32) != 0 {
939 if mode & (libc::S_IXUSR as u32) != 0 {
940 's'
941 } else {
942 'S'
943 }
944 } else if mode & (libc::S_IXUSR as u32) != 0 {
945 'x'
946 } else {
947 '-'
948 });
949
950 s.push(if mode & (libc::S_IRGRP as u32) != 0 {
952 'r'
953 } else {
954 '-'
955 });
956 s.push(if mode & (libc::S_IWGRP as u32) != 0 {
957 'w'
958 } else {
959 '-'
960 });
961 s.push(if mode & (libc::S_ISGID as u32) != 0 {
962 if mode & (libc::S_IXGRP as u32) != 0 {
963 's'
964 } else {
965 'S'
966 }
967 } else if mode & (libc::S_IXGRP as u32) != 0 {
968 'x'
969 } else {
970 '-'
971 });
972
973 s.push(if mode & (libc::S_IROTH as u32) != 0 {
975 'r'
976 } else {
977 '-'
978 });
979 s.push(if mode & (libc::S_IWOTH as u32) != 0 {
980 'w'
981 } else {
982 '-'
983 });
984 s.push(if mode & (libc::S_ISVTX as u32) != 0 {
985 if mode & (libc::S_IXOTH as u32) != 0 {
986 't'
987 } else {
988 'T'
989 }
990 } else if mode & (libc::S_IXOTH as u32) != 0 {
991 'x'
992 } else {
993 '-'
994 });
995
996 s
997}
998
999pub fn parse_block_size(s: &str) -> Result<(u64, String), String> {
1013 let s = s.trim();
1014 if s.is_empty() {
1015 return Err("empty block size".to_string());
1016 }
1017
1018 let digit_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
1023
1024 let (num_str, suffix_str) = s.split_at(digit_end);
1025
1026 let suffix_upper = suffix_str.to_uppercase();
1027
1028 let (multiplier, display_suffix) = match suffix_upper.as_str() {
1030 "" => (1u64, String::new()),
1031 "K" => (1024, "K".to_string()),
1032 "M" => (1024 * 1024, "M".to_string()),
1033 "G" => (1024 * 1024 * 1024, "G".to_string()),
1034 "T" => (1024u64 * 1024 * 1024 * 1024, "T".to_string()),
1035 "P" => (1024u64 * 1024 * 1024 * 1024 * 1024, "P".to_string()),
1036 "E" => (1024u64 * 1024 * 1024 * 1024 * 1024 * 1024, "E".to_string()),
1037 "KB" => (1000, "kB".to_string()),
1038 "MB" => (1000 * 1000, "MB".to_string()),
1039 "GB" => (1000 * 1000 * 1000, "GB".to_string()),
1040 "TB" => (1000u64 * 1000 * 1000 * 1000, "TB".to_string()),
1041 "PB" => (1000u64 * 1000 * 1000 * 1000 * 1000, "PB".to_string()),
1042 "EB" => (1000u64 * 1000 * 1000 * 1000 * 1000 * 1000, "EB".to_string()),
1043 _ => {
1044 return Err(format!("invalid suffix in block size '{}'", s));
1045 }
1046 };
1047
1048 let numeric = if num_str.is_empty() {
1049 1u64
1050 } else {
1051 num_str
1052 .parse::<u64>()
1053 .map_err(|_| format!("invalid block size '{}'", s))?
1054 };
1055
1056 let block_size = numeric
1057 .checked_mul(multiplier)
1058 .ok_or_else(|| format!("block size '{}' is too large", s))?;
1059
1060 if block_size == 0 {
1061 return Err(format!("invalid block size '{}'", s));
1062 }
1063
1064 let suffix = if num_str.is_empty() {
1068 display_suffix
1069 } else {
1070 String::new()
1071 };
1072
1073 Ok((block_size, suffix))
1074}
1075
1076pub fn format_size(size: u64, config: &LsConfig) -> String {
1082 if let Some(bs) = config.block_size {
1084 let scaled = if bs == 0 { size } else { (size + bs - 1) / bs };
1085 return format!("{}{}", scaled, config.block_size_suffix);
1086 }
1087 if config.human_readable || config.si {
1088 let base: f64 = if config.si { 1000.0 } else { 1024.0 };
1089 let suffixes = ["", "K", "M", "G", "T", "P", "E"];
1090
1091 if size == 0 {
1092 return "0".to_string();
1093 }
1094
1095 let mut val = size as f64;
1096 let mut idx = 0;
1097 while val >= base && idx < suffixes.len() - 1 {
1098 val /= base;
1099 idx += 1;
1100 }
1101
1102 if idx == 0 {
1103 format!("{}", size)
1104 } else if val >= 10.0 {
1105 format!("{:.0}{}", val, suffixes[idx])
1106 } else {
1107 format!("{:.1}{}", val, suffixes[idx])
1108 }
1109 } else if config.kibibytes {
1110 let blocks_k = (size + 1023) / 1024;
1112 format!("{}", blocks_k)
1113 } else {
1114 format!("{}", size)
1115 }
1116}
1117
1118pub fn format_blocks(blocks_512: u64, config: &LsConfig) -> String {
1120 let bytes = blocks_512 * 512;
1121 if let Some(bs) = config.block_size {
1122 let scaled = if bs == 0 {
1123 bytes
1124 } else {
1125 (bytes + bs - 1) / bs
1126 };
1127 return format!("{}{}", scaled, config.block_size_suffix);
1128 }
1129 if config.human_readable || config.si {
1130 format_size(bytes, config)
1131 } else if config.kibibytes {
1132 let k = (bytes + 1023) / 1024;
1133 format!("{}", k)
1134 } else {
1135 let k = (bytes + 1023) / 1024;
1137 format!("{}", k)
1138 }
1139}
1140
1141pub fn format_time(secs: i64, nsec: i64, style: &TimeStyle) -> String {
1147 let now_sys = SystemTime::now();
1149 let now_secs = now_sys
1150 .duration_since(SystemTime::UNIX_EPOCH)
1151 .map(|d| d.as_secs() as i64)
1152 .unwrap_or(0);
1153 let six_months_ago = now_secs - 6 * 30 * 24 * 3600;
1154
1155 let tm = time_from_epoch(secs);
1157
1158 match style {
1159 TimeStyle::FullIso => {
1160 format!(
1161 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {}",
1162 tm.year,
1163 tm.month,
1164 tm.day,
1165 tm.hour,
1166 tm.min,
1167 tm.sec,
1168 nsec,
1169 format_tz_offset(tm.utc_offset_secs)
1170 )
1171 }
1172 TimeStyle::LongIso => {
1173 format!(
1174 "{:04}-{:02}-{:02} {:02}:{:02}",
1175 tm.year, tm.month, tm.day, tm.hour, tm.min
1176 )
1177 }
1178 TimeStyle::Iso => {
1179 if secs > six_months_ago && secs <= now_secs {
1180 format!("{:02}-{:02} {:02}:{:02}", tm.month, tm.day, tm.hour, tm.min)
1181 } else {
1182 format!("{:02}-{:02} {:04}", tm.month, tm.day, tm.year)
1183 }
1184 }
1185 TimeStyle::Locale | TimeStyle::Custom(_) => {
1186 let month_names = [
1187 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
1188 ];
1189 let mon = if tm.month >= 1 && tm.month <= 12 {
1190 month_names[(tm.month - 1) as usize]
1191 } else {
1192 "???"
1193 };
1194
1195 if secs > six_months_ago && secs <= now_secs {
1196 format!("{} {:>2} {:02}:{:02}", mon, tm.day, tm.hour, tm.min)
1197 } else {
1198 format!("{} {:>2} {:04}", mon, tm.day, tm.year)
1199 }
1200 }
1201 }
1202}
1203
1204fn format_tz_offset(offset_secs: i32) -> String {
1205 let sign = if offset_secs >= 0 { '+' } else { '-' };
1206 let abs = offset_secs.unsigned_abs();
1207 let hours = abs / 3600;
1208 let mins = (abs % 3600) / 60;
1209 format!("{}{:02}{:02}", sign, hours, mins)
1210}
1211
1212struct BrokenDownTime {
1213 year: i32,
1214 month: u32,
1215 day: u32,
1216 hour: u32,
1217 min: u32,
1218 sec: u32,
1219 utc_offset_secs: i32,
1220}
1221
1222fn time_from_epoch(secs: i64) -> BrokenDownTime {
1224 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
1225 let time_t = secs as libc::time_t;
1226 unsafe {
1227 libc::localtime_r(&time_t, &mut tm);
1228 }
1229 BrokenDownTime {
1230 year: tm.tm_year + 1900,
1231 month: (tm.tm_mon + 1) as u32,
1232 day: tm.tm_mday as u32,
1233 hour: tm.tm_hour as u32,
1234 min: tm.tm_min as u32,
1235 sec: tm.tm_sec as u32,
1236 utc_offset_secs: tm.tm_gmtoff as i32,
1237 }
1238}
1239
1240fn lookup_user(uid: u32) -> String {
1247 use std::cell::RefCell;
1248 thread_local! {
1249 static CACHE: RefCell<HashMap<u32, String>> = RefCell::new(HashMap::new());
1250 }
1251 CACHE.with(|c| {
1252 let mut cache = c.borrow_mut();
1253 if let Some(name) = cache.get(&uid) {
1254 return name.clone();
1255 }
1256 let name = lookup_user_uncached(uid);
1257 cache.insert(uid, name.clone());
1258 name
1259 })
1260}
1261
1262fn lookup_user_uncached(uid: u32) -> String {
1263 let mut buf = vec![0u8; 1024];
1264 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
1265 let mut result: *mut libc::passwd = std::ptr::null_mut();
1266 let ret = unsafe {
1267 libc::getpwuid_r(
1268 uid,
1269 &mut pwd,
1270 buf.as_mut_ptr() as *mut libc::c_char,
1271 buf.len(),
1272 &mut result,
1273 )
1274 };
1275 if ret == 0 && !result.is_null() {
1276 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
1277 cstr.to_string_lossy().into_owned()
1278 } else {
1279 uid.to_string()
1280 }
1281}
1282
1283fn lookup_group(gid: u32) -> String {
1285 use std::cell::RefCell;
1286 thread_local! {
1287 static CACHE: RefCell<HashMap<u32, String>> = RefCell::new(HashMap::new());
1288 }
1289 CACHE.with(|c| {
1290 let mut cache = c.borrow_mut();
1291 if let Some(name) = cache.get(&gid) {
1292 return name.clone();
1293 }
1294 let name = lookup_group_uncached(gid);
1295 cache.insert(gid, name.clone());
1296 name
1297 })
1298}
1299
1300fn lookup_group_uncached(gid: u32) -> String {
1301 let mut buf = vec![0u8; 1024];
1302 let mut grp: libc::group = unsafe { std::mem::zeroed() };
1303 let mut result: *mut libc::group = std::ptr::null_mut();
1304 let ret = unsafe {
1305 libc::getgrgid_r(
1306 gid,
1307 &mut grp,
1308 buf.as_mut_ptr() as *mut libc::c_char,
1309 buf.len(),
1310 &mut result,
1311 )
1312 };
1313 if ret == 0 && !result.is_null() {
1314 let cstr = unsafe { std::ffi::CStr::from_ptr(grp.gr_name) };
1315 cstr.to_string_lossy().into_owned()
1316 } else {
1317 gid.to_string()
1318 }
1319}
1320
1321pub fn glob_match(pattern: &str, name: &str) -> bool {
1327 let pat = pattern.as_bytes();
1328 let txt = name.as_bytes();
1329 let mut pi = 0;
1330 let mut ti = 0;
1331 let mut star_p = usize::MAX;
1332 let mut star_t = 0;
1333
1334 while ti < txt.len() {
1335 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
1336 pi += 1;
1337 ti += 1;
1338 } else if pi < pat.len() && pat[pi] == b'*' {
1339 star_p = pi;
1340 star_t = ti;
1341 pi += 1;
1342 } else if star_p != usize::MAX {
1343 pi = star_p + 1;
1344 star_t += 1;
1345 ti = star_t;
1346 } else {
1347 return false;
1348 }
1349 }
1350 while pi < pat.len() && pat[pi] == b'*' {
1351 pi += 1;
1352 }
1353 pi == pat.len()
1354}
1355
1356fn should_ignore(name: &str, config: &LsConfig) -> bool {
1357 if config.ignore_backups && name.ends_with('~') {
1358 return true;
1359 }
1360 for pat in &config.ignore_patterns {
1361 if glob_match(pat, name) {
1362 return true;
1363 }
1364 }
1365 false
1366}
1367
1368pub fn read_entries(path: &Path, config: &LsConfig) -> io::Result<Vec<FileEntry>> {
1374 let mut entries = Vec::new();
1375
1376 let show_all = config.all && !config.almost_all;
1378 let show_hidden = config.all || config.almost_all;
1379
1380 if show_all {
1381 if let Ok(e) = FileEntry::from_path_with_name(".".to_string(), path, config) {
1383 entries.push(e);
1384 }
1385 let parent = path.parent().unwrap_or(path);
1386 if let Ok(e) = FileEntry::from_path_with_name("..".to_string(), parent, config) {
1387 entries.push(e);
1388 }
1389 }
1390
1391 for entry in fs::read_dir(path)? {
1392 let entry = entry?;
1393 let name = entry.file_name().to_string_lossy().into_owned();
1394
1395 if !show_hidden && name.starts_with('.') {
1397 continue;
1398 }
1399
1400 if should_ignore(&name, config) {
1402 continue;
1403 }
1404
1405 match FileEntry::from_dir_entry(&entry, config) {
1406 Ok(fe) => entries.push(fe),
1407 Err(e) => {
1408 eprintln!("ls: cannot access '{}': {}", entry.path().display(), e);
1409 }
1410 }
1411 }
1412
1413 Ok(entries)
1414}
1415
1416fn print_long(
1422 out: &mut impl Write,
1423 entries: &[FileEntry],
1424 config: &LsConfig,
1425 color_db: Option<&ColorDb>,
1426) -> io::Result<()> {
1427 if entries.is_empty() {
1428 return Ok(());
1429 }
1430
1431 let max_nlink = entries
1433 .iter()
1434 .map(|e| count_digits(e.nlink))
1435 .max()
1436 .unwrap_or(1);
1437 let max_owner = if config.show_owner {
1438 entries
1439 .iter()
1440 .map(|e| {
1441 if config.numeric_ids {
1442 e.uid.to_string().len()
1443 } else {
1444 lookup_user(e.uid).len()
1445 }
1446 })
1447 .max()
1448 .unwrap_or(0)
1449 } else {
1450 0
1451 };
1452 let max_group = if config.show_group {
1453 entries
1454 .iter()
1455 .map(|e| {
1456 if config.numeric_ids {
1457 e.gid.to_string().len()
1458 } else {
1459 lookup_group(e.gid).len()
1460 }
1461 })
1462 .max()
1463 .unwrap_or(0)
1464 } else {
1465 0
1466 };
1467
1468 let has_device = entries.iter().any(|e| {
1470 let ft = e.mode & (libc::S_IFMT as u32);
1471 ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32
1472 });
1473 let max_size = if has_device {
1474 entries
1476 .iter()
1477 .map(|e| {
1478 let ft = e.mode & (libc::S_IFMT as u32);
1479 if ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32 {
1480 format!("{}, {}", e.rdev_major, e.rdev_minor).len()
1481 } else {
1482 format_size(e.size, config).len()
1483 }
1484 })
1485 .max()
1486 .unwrap_or(1)
1487 } else {
1488 entries
1489 .iter()
1490 .map(|e| format_size(e.size, config).len())
1491 .max()
1492 .unwrap_or(1)
1493 };
1494
1495 let max_inode = if config.show_inode {
1496 entries
1497 .iter()
1498 .map(|e| count_digits(e.ino))
1499 .max()
1500 .unwrap_or(1)
1501 } else {
1502 0
1503 };
1504
1505 let max_blocks = if config.show_size {
1506 entries
1507 .iter()
1508 .map(|e| format_blocks(e.blocks, config).len())
1509 .max()
1510 .unwrap_or(1)
1511 } else {
1512 0
1513 };
1514
1515 let ts_width = entries
1517 .iter()
1518 .filter(|e| !e.is_broken_deref())
1519 .map(|e| {
1520 format_time(
1521 e.time_secs(config.time_field),
1522 e.time_nsec(config.time_field),
1523 &config.time_style,
1524 )
1525 .len()
1526 })
1527 .max()
1528 .unwrap_or(12);
1529
1530 for entry in entries {
1531 if entry.is_broken_deref() {
1533 let quoted = quote_name(&entry.name, config);
1534 if config.show_inode {
1535 write!(out, "{:>width$} ", "?", width = max_inode)?;
1536 }
1537 if config.show_size {
1538 write!(out, "{:>width$} ", "?", width = max_blocks)?;
1539 }
1540 write!(out, "l????????? ")?;
1541 write!(out, "{:>width$} ", "?", width = max_nlink)?;
1542 if config.show_owner {
1543 write!(out, "{:<width$} ", "?", width = max_owner)?;
1544 }
1545 if config.show_group {
1546 write!(out, "{:<width$} ", "?", width = max_group)?;
1547 }
1548 write!(out, "{:>width$} ", "?", width = max_size)?;
1549 write!(out, "{:>width$} ", "?", width = ts_width)?;
1550 writeln!(out, "{}", quoted)?;
1551 continue;
1552 }
1553
1554 if config.show_inode {
1556 write!(out, "{:>width$} ", entry.ino, width = max_inode)?;
1557 }
1558
1559 if config.show_size {
1561 let bs = format_blocks(entry.blocks, config);
1562 write!(out, "{:>width$} ", bs, width = max_blocks)?;
1563 }
1564
1565 write!(out, "{} ", format_permissions(entry.mode))?;
1567
1568 write!(out, "{:>width$} ", entry.nlink, width = max_nlink)?;
1570
1571 if config.show_owner {
1573 let owner = if config.numeric_ids {
1574 entry.uid.to_string()
1575 } else {
1576 lookup_user(entry.uid)
1577 };
1578 write!(out, "{:<width$} ", owner, width = max_owner)?;
1579 }
1580
1581 if config.show_group {
1583 let group = if config.numeric_ids {
1584 entry.gid.to_string()
1585 } else {
1586 lookup_group(entry.gid)
1587 };
1588 write!(out, "{:<width$} ", group, width = max_group)?;
1589 }
1590
1591 let ft = entry.mode & (libc::S_IFMT as u32);
1593 if ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32 {
1594 let dev = format!("{}, {}", entry.rdev_major, entry.rdev_minor);
1595 write!(out, "{:>width$} ", dev, width = max_size)?;
1596 } else {
1597 let sz = format_size(entry.size, config);
1598 write!(out, "{:>width$} ", sz, width = max_size)?;
1599 }
1600
1601 let ts = format_time(
1603 entry.time_secs(config.time_field),
1604 entry.time_nsec(config.time_field),
1605 &config.time_style,
1606 );
1607 write!(out, "{} ", ts)?;
1608
1609 let quoted = quote_name(&entry.name, config);
1611 if let Some(db) = color_db {
1612 let c = db.color_for(entry);
1613 if c.is_empty() {
1614 write!(out, "{}", quoted)?;
1615 } else {
1616 write!(out, "{}{}{}", c, quoted, db.reset)?;
1617 }
1618 } else {
1619 write!(out, "{}", quoted)?;
1620 }
1621
1622 let is_symlink = (entry.mode & libc::S_IFMT as u32) == libc::S_IFLNK as u32;
1626 if !is_symlink {
1627 let ind = entry.indicator(config.indicator_style);
1628 if !ind.is_empty() {
1629 write!(out, "{}", ind)?;
1630 }
1631 }
1632
1633 if let Some(ref target) = entry.link_target {
1635 let quoted_target = quote_name(target, config);
1636 write!(out, " -> {}", quoted_target)?;
1637 if config.indicator_style == IndicatorStyle::Classify
1639 || config.indicator_style == IndicatorStyle::FileType
1640 || config.indicator_style == IndicatorStyle::Slash
1641 {
1642 if let Some(tmode) = entry.link_target_mode {
1643 let tft = tmode & libc::S_IFMT as u32;
1644 let target_ind = if tft == libc::S_IFDIR as u32 {
1645 "/"
1646 } else if config.indicator_style == IndicatorStyle::Classify
1647 && tft == libc::S_IFREG as u32
1648 && tmode
1649 & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32)
1650 != 0
1651 {
1652 "*"
1653 } else if (config.indicator_style == IndicatorStyle::Classify
1654 || config.indicator_style == IndicatorStyle::FileType)
1655 && tft == libc::S_IFIFO as u32
1656 {
1657 "|"
1658 } else if (config.indicator_style == IndicatorStyle::Classify
1659 || config.indicator_style == IndicatorStyle::FileType)
1660 && tft == libc::S_IFSOCK as u32
1661 {
1662 "="
1663 } else {
1664 ""
1665 };
1666 if !target_ind.is_empty() {
1667 write!(out, "{}", target_ind)?;
1668 }
1669 }
1670 }
1671 }
1672
1673 if config.zero {
1674 out.write_all(&[0u8])?;
1675 } else {
1676 writeln!(out)?;
1677 }
1678 }
1679
1680 Ok(())
1681}
1682
1683fn count_digits(n: u64) -> usize {
1684 if n == 0 {
1685 return 1;
1686 }
1687 let mut count = 0;
1688 let mut v = n;
1689 while v > 0 {
1690 count += 1;
1691 v /= 10;
1692 }
1693 count
1694}
1695
1696fn indent(out: &mut impl Write, from: usize, to: usize, tab: usize) -> io::Result<()> {
1703 let mut pos = from;
1704 while pos < to {
1705 if tab != 0 && to / tab > (pos + 1) / tab {
1706 out.write_all(b"\t")?;
1707 pos += tab - pos % tab;
1708 } else {
1709 out.write_all(b" ")?;
1710 pos += 1;
1711 }
1712 }
1713 Ok(())
1714}
1715
1716fn write_entry_prefix(
1718 out: &mut impl Write,
1719 entry: &FileEntry,
1720 config: &LsConfig,
1721 max_inode_w: usize,
1722 max_blocks_w: usize,
1723) -> io::Result<()> {
1724 if config.show_inode {
1725 write!(out, "{:>width$} ", entry.ino, width = max_inode_w)?;
1726 }
1727 if config.show_size {
1728 let bs = format_blocks(entry.blocks, config);
1729 write!(out, "{:>width$} ", bs, width = max_blocks_w)?;
1730 }
1731 Ok(())
1732}
1733
1734fn write_entry_name(
1736 out: &mut impl Write,
1737 display: &str,
1738 entry: &FileEntry,
1739 config: &LsConfig,
1740 color_db: Option<&ColorDb>,
1741) -> io::Result<()> {
1742 if let Some(db) = color_db {
1743 let c = db.color_for(entry);
1744 let quoted = quote_name(&entry.name, config);
1745 let ind = entry.indicator(config.indicator_style);
1746 if c.is_empty() {
1747 write!(out, "{}{}", quoted, ind)?;
1748 } else {
1749 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
1750 }
1751 } else {
1752 write!(out, "{}", display)?;
1753 }
1754 Ok(())
1755}
1756
1757fn print_with_separator(
1761 out: &mut impl Write,
1762 entries: &[FileEntry],
1763 config: &LsConfig,
1764 color_db: Option<&ColorDb>,
1765 sep: u8,
1766 eol: u8,
1767) -> io::Result<()> {
1768 let line_length = config.width;
1769
1770 let max_inode_w = if config.show_inode {
1771 entries
1772 .iter()
1773 .map(|e| count_digits(e.ino))
1774 .max()
1775 .unwrap_or(1)
1776 } else {
1777 0
1778 };
1779 let max_blocks_w = if config.show_size {
1780 entries
1781 .iter()
1782 .map(|e| format_blocks(e.blocks, config).len())
1783 .max()
1784 .unwrap_or(1)
1785 } else {
1786 0
1787 };
1788
1789 let prefix_width = if config.show_inode && config.show_size {
1790 max_inode_w + 1 + max_blocks_w + 1
1791 } else if config.show_inode {
1792 max_inode_w + 1
1793 } else if config.show_size {
1794 max_blocks_w + 1
1795 } else {
1796 0
1797 };
1798
1799 let mut pos: usize = 0;
1800
1801 for (i, entry) in entries.iter().enumerate() {
1802 let quoted = quote_name(&entry.name, config);
1803 let ind = entry.indicator(config.indicator_style);
1804 let len = if line_length > 0 {
1805 quoted.len() + ind.len() + prefix_width
1806 } else {
1807 0
1808 };
1809
1810 if i > 0 {
1811 let fits =
1814 line_length == 0 || (pos + len + 2 < line_length && pos <= usize::MAX - len - 2);
1815 let separator: u8 = if fits { b' ' } else { eol };
1816
1817 out.write_all(&[sep, separator])?;
1818 if fits {
1819 pos += 2;
1820 } else {
1821 pos = 0;
1822 }
1823 }
1824
1825 write_entry_prefix(out, entry, config, max_inode_w, max_blocks_w)?;
1826 if let Some(db) = color_db {
1827 let c = db.color_for(entry);
1828 if c.is_empty() {
1829 write!(out, "{}{}", quoted, ind)?;
1830 } else {
1831 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
1832 }
1833 } else {
1834 write!(out, "{}{}", quoted, ind)?;
1835 }
1836 pos += len;
1837 }
1838 if !entries.is_empty() {
1839 out.write_all(&[eol])?;
1840 }
1841 Ok(())
1842}
1843
1844fn print_columns(
1846 out: &mut impl Write,
1847 entries: &[FileEntry],
1848 config: &LsConfig,
1849 color_db: Option<&ColorDb>,
1850) -> io::Result<()> {
1851 if entries.is_empty() {
1852 return Ok(());
1853 }
1854
1855 let eol: u8 = if config.zero { 0 } else { b'\n' };
1856
1857 if config.width == 0 {
1861 return print_with_separator(out, entries, config, color_db, b' ', eol);
1862 }
1863
1864 let by_columns = config.format == OutputFormat::Columns;
1865 let tab = config.tab_size;
1866 let term_width = config.width;
1867
1868 let max_inode_w = if config.show_inode {
1869 entries
1870 .iter()
1871 .map(|e| count_digits(e.ino))
1872 .max()
1873 .unwrap_or(1)
1874 } else {
1875 0
1876 };
1877 let max_blocks_w = if config.show_size {
1878 entries
1879 .iter()
1880 .map(|e| format_blocks(e.blocks, config).len())
1881 .max()
1882 .unwrap_or(1)
1883 } else {
1884 0
1885 };
1886
1887 let prefix_width = if config.show_inode && config.show_size {
1888 max_inode_w + 1 + max_blocks_w + 1
1889 } else if config.show_inode {
1890 max_inode_w + 1
1891 } else if config.show_size {
1892 max_blocks_w + 1
1893 } else {
1894 0
1895 };
1896
1897 let items: Vec<(String, usize, &FileEntry)> = entries
1899 .iter()
1900 .map(|e| {
1901 let quoted = quote_name(&e.name, config);
1902 let ind = e.indicator(config.indicator_style);
1903 let display = format!("{}{}", quoted, ind);
1904 let w = display.len() + prefix_width;
1905 (display, w, e)
1906 })
1907 .collect();
1908
1909 let n = items.len();
1910
1911 let min_col_w: usize = 3;
1914 let max_possible_cols = if term_width < min_col_w {
1915 1
1916 } else {
1917 let base = term_width / min_col_w;
1918 let extra = if !term_width.is_multiple_of(min_col_w) {
1919 1
1920 } else {
1921 0
1922 };
1923 std::cmp::min(base + extra, n)
1924 };
1925
1926 let mut col_arrs: Vec<Vec<usize>> = (0..max_possible_cols)
1928 .map(|i| vec![min_col_w; i + 1])
1929 .collect();
1930 let mut line_lens: Vec<usize> = (0..max_possible_cols)
1931 .map(|i| (i + 1) * min_col_w)
1932 .collect();
1933 let mut valid: Vec<bool> = vec![true; max_possible_cols];
1934
1935 for filesno in 0..n {
1936 let name_length = items[filesno].1;
1937
1938 for i in 0..max_possible_cols {
1939 if !valid[i] {
1940 continue;
1941 }
1942 let ncols = i + 1;
1943 let idx = if by_columns {
1944 filesno / ((n + i) / ncols)
1945 } else {
1946 filesno % ncols
1947 };
1948 let real_length = name_length + if idx == i { 0 } else { 2 };
1950
1951 if col_arrs[i][idx] < real_length {
1952 line_lens[i] += real_length - col_arrs[i][idx];
1953 col_arrs[i][idx] = real_length;
1954 valid[i] = line_lens[i] < term_width;
1955 }
1956 }
1957 }
1958
1959 let mut num_cols = 1;
1961 for cols in (1..=max_possible_cols).rev() {
1962 if valid[cols - 1] {
1963 num_cols = cols;
1964 break;
1965 }
1966 }
1967
1968 if num_cols <= 1 {
1969 return print_single_column(out, entries, config, color_db);
1970 }
1971
1972 let col_arr = &col_arrs[num_cols - 1];
1973
1974 if by_columns {
1975 let num_rows = (n + num_cols - 1) / num_cols;
1977 for row in 0..num_rows {
1978 let mut pos = 0;
1979 let mut col = 0;
1980 let mut filesno = row;
1981
1982 loop {
1983 let (ref display, w, entry) = items[filesno];
1984 let max_w = col_arr[col];
1985
1986 write_entry_prefix(out, entry, config, max_inode_w, max_blocks_w)?;
1987 write_entry_name(out, display, entry, config, color_db)?;
1988
1989 if n.saturating_sub(num_rows) <= filesno {
1990 break;
1991 }
1992 filesno += num_rows;
1993
1994 indent(out, pos + w, pos + max_w, tab)?;
1995 pos += max_w;
1996 col += 1;
1997 }
1998 out.write_all(&[eol])?;
1999 }
2000 } else {
2001 let (ref display0, w0, entry0) = items[0];
2003 write_entry_prefix(out, entry0, config, max_inode_w, max_blocks_w)?;
2004 write_entry_name(out, display0, entry0, config, color_db)?;
2005
2006 let mut pos: usize = 0;
2007 let mut prev_w = w0;
2008 let mut prev_max_w = col_arr[0];
2009
2010 for filesno in 1..n {
2011 let col_idx = filesno % num_cols;
2012
2013 if col_idx == 0 {
2014 out.write_all(&[eol])?;
2015 pos = 0;
2016 } else {
2017 indent(out, pos + prev_w, pos + prev_max_w, tab)?;
2018 pos += prev_max_w;
2019 }
2020
2021 let (ref display, w, entry) = items[filesno];
2022 write_entry_prefix(out, entry, config, max_inode_w, max_blocks_w)?;
2023 write_entry_name(out, display, entry, config, color_db)?;
2024
2025 prev_w = w;
2026 prev_max_w = col_arr[col_idx];
2027 }
2028 out.write_all(&[eol])?;
2029 }
2030
2031 Ok(())
2032}
2033
2034fn print_single_column(
2039 out: &mut impl Write,
2040 entries: &[FileEntry],
2041 config: &LsConfig,
2042 color_db: Option<&ColorDb>,
2043) -> io::Result<()> {
2044 let max_inode_w = if config.show_inode {
2045 entries
2046 .iter()
2047 .map(|e| count_digits(e.ino))
2048 .max()
2049 .unwrap_or(1)
2050 } else {
2051 0
2052 };
2053 let max_blocks_w = if config.show_size {
2054 entries
2055 .iter()
2056 .map(|e| format_blocks(e.blocks, config).len())
2057 .max()
2058 .unwrap_or(1)
2059 } else {
2060 0
2061 };
2062
2063 for entry in entries {
2064 if config.show_inode {
2065 write!(out, "{:>width$} ", entry.ino, width = max_inode_w)?;
2066 }
2067 if config.show_size {
2068 let bs = format_blocks(entry.blocks, config);
2069 write!(out, "{:>width$} ", bs, width = max_blocks_w)?;
2070 }
2071
2072 let quoted = quote_name(&entry.name, config);
2073 if let Some(db) = color_db {
2074 let c = db.color_for(entry);
2075 if c.is_empty() {
2076 write!(out, "{}", quoted)?;
2077 } else {
2078 write!(out, "{}{}{}", c, quoted, db.reset)?;
2079 }
2080 } else {
2081 write!(out, "{}", quoted)?;
2082 }
2083
2084 let ind = entry.indicator(config.indicator_style);
2085 if !ind.is_empty() {
2086 write!(out, "{}", ind)?;
2087 }
2088
2089 if config.zero {
2090 out.write_all(&[0u8])?;
2091 } else {
2092 writeln!(out)?;
2093 }
2094 }
2095 Ok(())
2096}
2097
2098pub fn print_comma(
2103 out: &mut impl Write,
2104 entries: &[FileEntry],
2105 config: &LsConfig,
2106 color_db: Option<&ColorDb>,
2107) -> io::Result<()> {
2108 let eol: u8 = if config.zero { 0 } else { b'\n' };
2109 let line_length = config.width;
2110 let mut pos: usize = 0;
2111
2112 for (i, entry) in entries.iter().enumerate() {
2113 let quoted = quote_name(&entry.name, config);
2114 let ind = entry.indicator(config.indicator_style);
2115 let name_len = if line_length > 0 {
2116 quoted.len() + ind.len()
2117 } else {
2118 0
2119 };
2120
2121 if i > 0 {
2122 let fits = line_length == 0
2125 || (pos + name_len + 2 < line_length && pos <= usize::MAX - name_len - 2);
2126 if fits {
2127 write!(out, ", ")?;
2128 pos += 2;
2129 } else {
2130 write!(out, ",")?;
2131 out.write_all(&[eol])?;
2132 pos = 0;
2133 }
2134 }
2135
2136 if let Some(db) = color_db {
2137 let c = db.color_for(entry);
2138 if c.is_empty() {
2139 write!(out, "{}{}", quoted, ind)?;
2140 } else {
2141 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
2142 }
2143 } else {
2144 write!(out, "{}{}", quoted, ind)?;
2145 }
2146 pos += name_len;
2147 }
2148 if !entries.is_empty() {
2149 out.write_all(&[eol])?;
2150 }
2151 Ok(())
2152}
2153
2154fn print_total(out: &mut impl Write, entries: &[FileEntry], config: &LsConfig) -> io::Result<()> {
2159 let total_blocks: u64 = entries.iter().map(|e| e.blocks).sum();
2160 let formatted = format_blocks(total_blocks, config);
2161 write!(out, "total {}", formatted)?;
2162 if config.zero {
2163 out.write_all(&[0u8])
2164 } else {
2165 writeln!(out)
2166 }
2167}
2168
2169pub fn ls_dir(
2175 out: &mut impl Write,
2176 path: &Path,
2177 config: &LsConfig,
2178 color_db: Option<&ColorDb>,
2179 show_header: bool,
2180) -> io::Result<bool> {
2181 if show_header {
2182 writeln!(out, "{}:", path.display())?;
2183 }
2184
2185 let mut entries = read_entries(path, config)?;
2186 sort_entries(&mut entries, config);
2187
2188 let has_broken_deref = entries.iter().any(|e| e.is_broken_deref());
2190
2191 if config.long_format || config.show_size {
2193 print_total(out, &entries, config)?;
2194 }
2195
2196 match config.format {
2197 OutputFormat::Long => print_long(out, &entries, config, color_db)?,
2198 OutputFormat::SingleColumn => print_single_column(out, &entries, config, color_db)?,
2199 OutputFormat::Columns | OutputFormat::Across => {
2200 print_columns(out, &entries, config, color_db)?
2201 }
2202 OutputFormat::Comma => print_comma(out, &entries, config, color_db)?,
2203 }
2204
2205 if config.recursive {
2207 let dirs: Vec<PathBuf> = entries
2208 .iter()
2209 .filter(|e| {
2210 e.is_directory()
2211 && e.name != "."
2212 && e.name != ".."
2213 && (e.mode & (libc::S_IFMT as u32)) != libc::S_IFLNK as u32
2214 })
2215 .map(|e| e.path.clone())
2216 .collect();
2217
2218 for dir in dirs {
2219 writeln!(out)?;
2220 ls_dir(out, &dir, config, color_db, true)?;
2221 }
2222 }
2223
2224 Ok(!has_broken_deref)
2225}
2226
2227pub fn ls_main(paths: &[String], config: &LsConfig) -> io::Result<bool> {
2231 let stdout = io::stdout();
2232 let is_tty = atty_stdout();
2233 #[cfg(target_os = "linux")]
2238 if !is_tty {
2239 unsafe {
2240 libc::fcntl(1, 1031 , 4096i32)
2241 };
2242 }
2243 let buf_cap = if is_tty { 64 * 1024 } else { 4 * 1024 };
2244 let mut out = BufWriter::with_capacity(buf_cap, stdout.lock());
2245
2246 let color_db = match config.color {
2247 ColorMode::Always => Some(ColorDb::from_env()),
2248 ColorMode::Auto => {
2249 if atty_stdout() {
2250 Some(ColorDb::from_env())
2251 } else {
2252 None
2253 }
2254 }
2255 ColorMode::Never => None,
2256 };
2257
2258 let mut had_error = false;
2259
2260 let mut file_args: Vec<FileEntry> = Vec::new();
2262 let mut dir_args: Vec<PathBuf> = Vec::new();
2263
2264 for p in paths {
2265 let path = PathBuf::from(p);
2266 let meta_result = if config.dereference {
2267 match fs::metadata(&path) {
2268 Ok(m) => Ok(m),
2269 Err(e) => {
2270 if let Ok(lmeta) = fs::symlink_metadata(&path) {
2272 if lmeta.file_type().is_symlink() {
2273 eprintln!(
2275 "ls: cannot access '{}': {}",
2276 p,
2277 crate::common::io_error_msg(&e)
2278 );
2279 had_error = true;
2280 file_args.push(FileEntry::broken_deref(p.to_string(), path));
2281 continue;
2282 }
2283 }
2284 Err(e)
2285 }
2286 }
2287 } else {
2288 fs::symlink_metadata(&path)
2289 };
2290
2291 match meta_result {
2292 Ok(meta) => {
2293 if config.directory || !meta.is_dir() {
2294 match FileEntry::from_path_with_name(p.to_string(), &path, config) {
2295 Ok(fe) => file_args.push(fe),
2296 Err(e) => {
2297 eprintln!("ls: cannot access '{}': {}", p, e);
2298 had_error = true;
2299 }
2300 }
2301 } else {
2302 dir_args.push(path);
2303 }
2304 }
2305 Err(e) => {
2306 eprintln!(
2307 "ls: cannot access '{}': {}",
2308 p,
2309 crate::common::io_error_msg(&e)
2310 );
2311 had_error = true;
2312 }
2313 }
2314 }
2315
2316 sort_entries(&mut file_args, config);
2318
2319 if !file_args.is_empty() {
2321 match config.format {
2322 OutputFormat::Long => print_long(&mut out, &file_args, config, color_db.as_ref())?,
2323 OutputFormat::SingleColumn => {
2324 print_single_column(&mut out, &file_args, config, color_db.as_ref())?
2325 }
2326 OutputFormat::Columns | OutputFormat::Across => {
2327 print_columns(&mut out, &file_args, config, color_db.as_ref())?
2328 }
2329 OutputFormat::Comma => print_comma(&mut out, &file_args, config, color_db.as_ref())?,
2330 }
2331 }
2332
2333 dir_args.sort_by(|a, b| {
2335 let an = a.to_string_lossy();
2336 let bn = b.to_string_lossy();
2337 let ord = locale_cmp(&an, &bn);
2338 if config.reverse { ord.reverse() } else { ord }
2339 });
2340
2341 let show_header =
2342 dir_args.len() > 1 || (!file_args.is_empty() && !dir_args.is_empty()) || config.recursive;
2343
2344 for (i, dir) in dir_args.iter().enumerate() {
2345 if i > 0 || !file_args.is_empty() {
2346 writeln!(out)?;
2347 }
2348 match ls_dir(&mut out, dir, config, color_db.as_ref(), show_header) {
2349 Ok(true) => {}
2350 Ok(false) => {
2351 had_error = true;
2352 }
2353 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => return Err(e),
2354 Err(e) => {
2355 eprintln!(
2356 "ls: cannot open directory '{}': {}",
2357 dir.display(),
2358 crate::common::io_error_msg(&e)
2359 );
2360 had_error = true;
2361 }
2362 }
2363 }
2364
2365 out.flush()?;
2366
2367 Ok(!had_error)
2368}
2369
2370pub fn atty_stdout() -> bool {
2372 unsafe { libc::isatty(1) != 0 }
2373}
2374
2375pub fn collect_entries(path: &Path, config: &LsConfig) -> io::Result<Vec<FileEntry>> {
2381 let mut entries = read_entries(path, config)?;
2382 sort_entries(&mut entries, config);
2383 Ok(entries)
2384}
2385
2386pub fn render_long(entries: &[FileEntry], config: &LsConfig) -> io::Result<String> {
2388 let mut buf = Vec::new();
2389 print_long(&mut buf, entries, config, None)?;
2390 Ok(String::from_utf8_lossy(&buf).into_owned())
2391}
2392
2393pub fn render_single_column(entries: &[FileEntry], config: &LsConfig) -> io::Result<String> {
2395 let mut buf = Vec::new();
2396 print_single_column(&mut buf, entries, config, None)?;
2397 Ok(String::from_utf8_lossy(&buf).into_owned())
2398}
2399
2400pub fn render_dir(path: &Path, config: &LsConfig) -> io::Result<String> {
2402 let mut buf = Vec::new();
2403 ls_dir(&mut buf, path, config, None, false)?;
2404 Ok(String::from_utf8_lossy(&buf).into_owned())
2405}