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}
157
158impl Default for LsConfig {
159 fn default() -> Self {
160 LsConfig {
161 all: false,
162 almost_all: false,
163 long_format: false,
164 human_readable: false,
165 si: false,
166 reverse: false,
167 recursive: false,
168 sort_by: SortBy::Name,
169 format: OutputFormat::Columns,
170 classify: ClassifyMode::Never,
171 color: ColorMode::Auto,
172 group_directories_first: false,
173 show_inode: false,
174 show_size: false,
175 show_owner: true,
176 show_group: true,
177 numeric_ids: false,
178 dereference: false,
179 directory: false,
180 time_field: TimeField::Mtime,
181 time_style: TimeStyle::Locale,
182 ignore_patterns: Vec::new(),
183 ignore_backups: false,
184 width: 80,
185 quoting_style: QuotingStyle::Literal,
186 hide_control_chars: false,
187 kibibytes: false,
188 indicator_style: IndicatorStyle::None,
189 tab_size: 8,
190 hyperlink: HyperlinkMode::Never,
191 context: false,
192 literal: false,
193 zero: false,
194 }
195 }
196}
197
198#[derive(Debug, Clone)]
204pub struct ColorDb {
205 pub map: HashMap<String, String>,
206 pub dir: String,
207 pub link: String,
208 pub exec: String,
209 pub pipe: String,
210 pub socket: String,
211 pub block_dev: String,
212 pub char_dev: String,
213 pub orphan: String,
214 pub setuid: String,
215 pub setgid: String,
216 pub sticky: String,
217 pub other_writable: String,
218 pub sticky_other_writable: String,
219 pub reset: String,
220}
221
222impl Default for ColorDb {
223 fn default() -> Self {
224 ColorDb {
225 map: HashMap::new(),
226 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(),
240 }
241 }
242}
243
244impl ColorDb {
245 pub fn from_env() -> Self {
247 let mut db = ColorDb::default();
248 if let Ok(val) = std::env::var("LS_COLORS") {
249 for item in val.split(':') {
250 if let Some((key, code)) = item.split_once('=') {
251 let esc = format!("\x1b[{}m", code);
252 match key {
253 "di" => db.dir = esc,
254 "ln" => db.link = esc,
255 "ex" => db.exec = esc,
256 "pi" | "fi" if key == "pi" => db.pipe = esc,
257 "so" => db.socket = esc,
258 "bd" => db.block_dev = esc,
259 "cd" => db.char_dev = esc,
260 "or" => db.orphan = esc,
261 "su" => db.setuid = esc,
262 "sg" => db.setgid = esc,
263 "st" => db.sticky = esc,
264 "ow" => db.other_writable = esc,
265 "tw" => db.sticky_other_writable = esc,
266 "rs" => db.reset = esc,
267 _ => {
268 if key.starts_with('*') {
269 db.map.insert(key[1..].to_string(), esc);
270 }
271 }
272 }
273 }
274 }
275 }
276 db
277 }
278
279 fn color_for(&self, entry: &FileEntry) -> &str {
281 let mode = entry.mode;
282 let ft = mode & (libc::S_IFMT as u32);
283
284 if ft == libc::S_IFLNK as u32 {
286 if entry.link_target_ok {
287 return &self.link;
288 } else {
289 return &self.orphan;
290 }
291 }
292
293 if ft == libc::S_IFDIR as u32 {
295 let sticky = mode & (libc::S_ISVTX as u32) != 0;
296 let ow = mode & (libc::S_IWOTH as u32) != 0;
297 if sticky && ow {
298 return &self.sticky_other_writable;
299 }
300 if ow {
301 return &self.other_writable;
302 }
303 if sticky {
304 return &self.sticky;
305 }
306 return &self.dir;
307 }
308
309 if ft == libc::S_IFIFO as u32 {
311 return &self.pipe;
312 }
313 if ft == libc::S_IFSOCK as u32 {
314 return &self.socket;
315 }
316 if ft == libc::S_IFBLK as u32 {
317 return &self.block_dev;
318 }
319 if ft == libc::S_IFCHR as u32 {
320 return &self.char_dev;
321 }
322
323 if mode & (libc::S_ISUID as u32) != 0 {
325 return &self.setuid;
326 }
327 if mode & (libc::S_ISGID as u32) != 0 {
328 return &self.setgid;
329 }
330
331 if let Some(ext_pos) = entry.name.rfind('.') {
333 let ext = &entry.name[ext_pos..];
334 if let Some(c) = self.map.get(ext) {
335 return c;
336 }
337 }
338
339 if ft == libc::S_IFREG as u32
341 && mode & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32) != 0
342 {
343 return &self.exec;
344 }
345
346 ""
347 }
348}
349
350#[derive(Debug, Clone)]
356pub struct FileEntry {
357 pub name: String,
358 pub path: PathBuf,
359 pub sort_key: CString,
361 pub ino: u64,
362 pub nlink: u64,
363 pub mode: u32,
364 pub uid: u32,
365 pub gid: u32,
366 pub size: u64,
367 pub blocks: u64,
368 pub mtime: i64,
369 pub mtime_nsec: i64,
370 pub atime: i64,
371 pub atime_nsec: i64,
372 pub ctime: i64,
373 pub ctime_nsec: i64,
374 pub rdev_major: u32,
375 pub rdev_minor: u32,
376 pub is_dir: bool,
377 pub link_target: Option<String>,
378 pub link_target_ok: bool,
379}
380
381impl FileEntry {
382 fn from_dir_entry(de: &DirEntry, config: &LsConfig) -> io::Result<Self> {
384 let name = de.file_name().to_string_lossy().into_owned();
385 let path = de.path();
386
387 let meta = if config.dereference {
388 match fs::metadata(&path) {
389 Ok(m) => m,
390 Err(e) => {
391 if let Ok(lmeta) = fs::symlink_metadata(&path) {
393 if lmeta.file_type().is_symlink() {
394 eprintln!(
395 "ls: cannot access '{}': {}",
396 name,
397 crate::common::io_error_msg(&e)
398 );
399 return Ok(Self::broken_deref(name, path));
400 }
401 }
402 return Err(e);
403 }
404 }
405 } else {
406 fs::symlink_metadata(&path)?
407 };
408
409 Self::from_metadata(name, path, &meta, config)
410 }
411
412 pub fn from_path_with_name(name: String, path: &Path, config: &LsConfig) -> io::Result<Self> {
415 let meta = if config.dereference {
416 fs::metadata(path).or_else(|_| fs::symlink_metadata(path))?
417 } else {
418 fs::symlink_metadata(path)?
419 };
420 Self::from_metadata(name, path.to_path_buf(), &meta, config)
421 }
422
423 fn from_metadata(
424 name: String,
425 path: PathBuf,
426 meta: &Metadata,
427 _config: &LsConfig,
428 ) -> io::Result<Self> {
429 let file_type = meta.file_type();
430 let is_symlink = file_type.is_symlink();
431
432 let (link_target, link_target_ok) = if is_symlink {
433 match fs::read_link(&path) {
434 Ok(target) => {
435 let ok = fs::metadata(&path).is_ok();
436 (Some(target.to_string_lossy().into_owned()), ok)
437 }
438 Err(_) => (None, false),
439 }
440 } else {
441 (None, true)
442 };
443
444 let rdev = meta.rdev();
445 let sort_key = CString::new(name.as_str()).unwrap_or_default();
446
447 Ok(FileEntry {
448 name,
449 path,
450 sort_key,
451 ino: meta.ino(),
452 nlink: meta.nlink(),
453 mode: meta.mode(),
454 uid: meta.uid(),
455 gid: meta.gid(),
456 size: meta.size(),
457 blocks: meta.blocks(),
458 mtime: meta.mtime(),
459 mtime_nsec: meta.mtime_nsec(),
460 atime: meta.atime(),
461 atime_nsec: meta.atime_nsec(),
462 ctime: meta.ctime(),
463 ctime_nsec: meta.ctime_nsec(),
464 rdev_major: ((rdev >> 8) & 0xfff) as u32,
465 rdev_minor: (rdev & 0xff) as u32,
466 is_dir: meta.is_dir(),
467 link_target,
468 link_target_ok,
469 })
470 }
471
472 fn time_secs(&self, field: TimeField) -> i64 {
474 match field {
475 TimeField::Mtime => self.mtime,
476 TimeField::Atime => self.atime,
477 TimeField::Ctime | TimeField::Birth => self.ctime,
478 }
479 }
480
481 fn time_nsec(&self, field: TimeField) -> i64 {
482 match field {
483 TimeField::Mtime => self.mtime_nsec,
484 TimeField::Atime => self.atime_nsec,
485 TimeField::Ctime | TimeField::Birth => self.ctime_nsec,
486 }
487 }
488
489 fn extension(&self) -> &str {
491 match self.name.rfind('.') {
492 Some(pos) if pos > 0 => &self.name[pos + 1..],
493 _ => "",
494 }
495 }
496
497 fn is_directory(&self) -> bool {
499 self.is_dir
500 }
501
502 fn indicator(&self, style: IndicatorStyle) -> &'static str {
504 let ft = self.mode & (libc::S_IFMT as u32);
505 match style {
506 IndicatorStyle::None => "",
507 IndicatorStyle::Slash => {
508 if ft == libc::S_IFDIR as u32 {
509 "/"
510 } else {
511 ""
512 }
513 }
514 IndicatorStyle::FileType => match ft {
515 x if x == libc::S_IFDIR as u32 => "/",
516 x if x == libc::S_IFLNK as u32 => "@",
517 x if x == libc::S_IFIFO as u32 => "|",
518 x if x == libc::S_IFSOCK as u32 => "=",
519 _ => "",
520 },
521 IndicatorStyle::Classify => match ft {
522 x if x == libc::S_IFDIR as u32 => "/",
523 x if x == libc::S_IFLNK as u32 => "@",
524 x if x == libc::S_IFIFO as u32 => "|",
525 x if x == libc::S_IFSOCK as u32 => "=",
526 _ => {
527 if ft == libc::S_IFREG as u32
528 && self.mode
529 & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32)
530 != 0
531 {
532 "*"
533 } else {
534 ""
535 }
536 }
537 },
538 }
539 }
540
541 pub fn broken_deref(name: String, path: PathBuf) -> Self {
544 let sort_key = CString::new(name.as_str()).unwrap_or_default();
545 FileEntry {
546 name,
547 path,
548 sort_key,
549 ino: 0,
550 nlink: 0, mode: libc::S_IFLNK as u32,
552 uid: 0,
553 gid: 0,
554 size: 0,
555 blocks: 0,
556 mtime: 0,
557 mtime_nsec: 0,
558 atime: 0,
559 atime_nsec: 0,
560 ctime: 0,
561 ctime_nsec: 0,
562 rdev_major: 0,
563 rdev_minor: 0,
564 is_dir: false,
565 link_target: None,
566 link_target_ok: false,
567 }
568 }
569
570 fn is_broken_deref(&self) -> bool {
572 self.nlink == 0 && (self.mode & libc::S_IFMT as u32) == libc::S_IFLNK as u32
573 }
574
575 fn display_width(&self, config: &LsConfig) -> usize {
577 let quoted = quote_name(&self.name, config);
578 let ind = self.indicator(config.indicator_style);
579 quoted.len() + ind.len()
580 }
581}
582
583pub fn quote_name(name: &str, config: &LsConfig) -> String {
589 match config.quoting_style {
590 QuotingStyle::Literal => {
591 if config.hide_control_chars {
592 hide_control(name)
593 } else {
594 name.to_string()
595 }
596 }
597 QuotingStyle::Escape => escape_name(name),
598 QuotingStyle::C => c_quote(name),
599 QuotingStyle::Shell => shell_quote(name, false, false),
600 QuotingStyle::ShellAlways => shell_quote(name, true, false),
601 QuotingStyle::ShellEscape => shell_quote(name, false, true),
602 QuotingStyle::ShellEscapeAlways => shell_quote(name, true, true),
603 QuotingStyle::Locale => locale_quote(name),
604 }
605}
606
607fn get_link_target_indicator(symlink_path: &Path, style: IndicatorStyle) -> &'static str {
610 if style == IndicatorStyle::None || style == IndicatorStyle::Slash {
611 return "";
612 }
613 let meta = match fs::metadata(symlink_path) {
615 Ok(m) => m,
616 Err(_) => return "", };
618 let mode = meta.mode();
619 let ft = mode & (libc::S_IFMT as u32);
620 match style {
621 IndicatorStyle::FileType => match ft {
622 x if x == libc::S_IFDIR as u32 => "/",
623 x if x == libc::S_IFLNK as u32 => "@",
624 x if x == libc::S_IFIFO as u32 => "|",
625 x if x == libc::S_IFSOCK as u32 => "=",
626 _ => "",
627 },
628 IndicatorStyle::Classify => match ft {
629 x if x == libc::S_IFDIR as u32 => "/",
630 x if x == libc::S_IFLNK as u32 => "@",
631 x if x == libc::S_IFIFO as u32 => "|",
632 x if x == libc::S_IFSOCK as u32 => "=",
633 _ => {
634 if ft == libc::S_IFREG as u32
635 && mode & (libc::S_IXUSR as u32 | libc::S_IXGRP as u32 | libc::S_IXOTH as u32)
636 != 0
637 {
638 "*"
639 } else {
640 ""
641 }
642 }
643 },
644 _ => "",
645 }
646}
647
648fn hide_control(name: &str) -> String {
649 name.chars()
650 .map(|c| if c.is_control() { '?' } else { c })
651 .collect()
652}
653
654fn escape_name(name: &str) -> String {
655 let mut out = String::with_capacity(name.len());
656 for c in name.chars() {
657 match c {
658 '\\' => out.push_str("\\\\"),
659 '\n' => out.push_str("\\n"),
660 '\r' => out.push_str("\\r"),
661 '\t' => out.push_str("\\t"),
662 ' ' => out.push_str("\\ "),
663 c if c.is_control() => {
664 out.push_str(&format!("\\{:03o}", c as u32));
665 }
666 c => out.push(c),
667 }
668 }
669 out
670}
671
672fn c_quote(name: &str) -> String {
673 let mut out = String::with_capacity(name.len() + 2);
674 out.push('"');
675 for c in name.chars() {
676 match c {
677 '"' => out.push_str("\\\""),
678 '\\' => out.push_str("\\\\"),
679 '\n' => out.push_str("\\n"),
680 '\r' => out.push_str("\\r"),
681 '\t' => out.push_str("\\t"),
682 '\x07' => out.push_str("\\a"),
683 '\x08' => out.push_str("\\b"),
684 '\x0C' => out.push_str("\\f"),
685 '\x0B' => out.push_str("\\v"),
686 c if c.is_control() => {
687 out.push_str(&format!("\\{:03o}", c as u32));
688 }
689 c => out.push(c),
690 }
691 }
692 out.push('"');
693 out
694}
695
696fn shell_quote(name: &str, always: bool, escape: bool) -> String {
697 let needs_quoting = name.is_empty()
698 || name
699 .chars()
700 .any(|c| " \t\n'\"\\|&;()<>!$`#~{}[]?*".contains(c) || c.is_control());
701
702 if !needs_quoting && !always {
703 return name.to_string();
704 }
705
706 if escape {
707 let has_control = name.chars().any(|c| c.is_control());
709 if has_control {
710 let mut out = String::with_capacity(name.len() + 4);
711 out.push_str("$'");
712 for c in name.chars() {
713 match c {
714 '\'' => out.push_str("\\'"),
715 '\\' => out.push_str("\\\\"),
716 '\n' => out.push_str("\\n"),
717 '\r' => out.push_str("\\r"),
718 '\t' => out.push_str("\\t"),
719 c if c.is_control() => {
720 out.push_str(&format!("\\{:03o}", c as u32));
721 }
722 c => out.push(c),
723 }
724 }
725 out.push('\'');
726 return out;
727 }
728 }
729
730 let mut out = String::with_capacity(name.len() + 2);
732 out.push('\'');
733 for c in name.chars() {
734 if c == '\'' {
735 out.push_str("'\\''");
736 } else {
737 out.push(c);
738 }
739 }
740 out.push('\'');
741 out
742}
743
744fn locale_quote(name: &str) -> String {
745 let mut out = String::with_capacity(name.len() + 2);
747 out.push('\u{2018}');
748 for c in name.chars() {
749 match c {
750 '\\' => out.push_str("\\\\"),
751 '\n' => out.push_str("\\n"),
752 '\r' => out.push_str("\\r"),
753 '\t' => out.push_str("\\t"),
754 c if c.is_control() => {
755 out.push_str(&format!("\\{:03o}", c as u32));
756 }
757 c => out.push(c),
758 }
759 }
760 out.push('\u{2019}');
761 out
762}
763
764pub(crate) fn version_cmp(a: &str, b: &str) -> Ordering {
770 let ab = a.as_bytes();
771 let bb = b.as_bytes();
772 let mut ai = 0;
773 let mut bi = 0;
774 while ai < ab.len() && bi < bb.len() {
775 let ac = ab[ai];
776 let bc = bb[bi];
777 if ac.is_ascii_digit() && bc.is_ascii_digit() {
778 let a_start = ai;
780 let b_start = bi;
781 while ai < ab.len() && ab[ai] == b'0' {
782 ai += 1;
783 }
784 while bi < bb.len() && bb[bi] == b'0' {
785 bi += 1;
786 }
787 let a_num_start = ai;
788 let b_num_start = bi;
789 while ai < ab.len() && ab[ai].is_ascii_digit() {
790 ai += 1;
791 }
792 while bi < bb.len() && bb[bi].is_ascii_digit() {
793 bi += 1;
794 }
795 let a_len = ai - a_num_start;
796 let b_len = bi - b_num_start;
797 if a_len != b_len {
798 return a_len.cmp(&b_len);
799 }
800 let ord = ab[a_num_start..ai].cmp(&bb[b_num_start..bi]);
801 if ord != Ordering::Equal {
802 return ord;
803 }
804 let a_zeros = a_num_start - a_start;
806 let b_zeros = b_num_start - b_start;
807 if a_zeros != b_zeros {
808 return a_zeros.cmp(&b_zeros);
809 }
810 } else {
811 let ord = ac.cmp(&bc);
812 if ord != Ordering::Equal {
813 return ord;
814 }
815 ai += 1;
816 bi += 1;
817 }
818 }
819 ab.len().cmp(&bb.len())
820}
821
822fn sort_entries(entries: &mut [FileEntry], config: &LsConfig) {
823 if config.group_directories_first {
824 entries.sort_by(|a, b| {
826 let a_dir = a.is_directory();
827 let b_dir = b.is_directory();
828 match (a_dir, b_dir) {
829 (true, false) => Ordering::Less,
830 (false, true) => Ordering::Greater,
831 _ => compare_entries(a, b, config),
832 }
833 });
834 } else {
835 entries.sort_by(|a, b| compare_entries(a, b, config));
836 }
837}
838
839#[inline]
843fn locale_cmp_cstr(a: &CString, b: &CString) -> Ordering {
844 if IS_C_LOCALE.load(AtomicOrdering::Relaxed) {
845 a.as_bytes().cmp(b.as_bytes())
846 } else {
847 let result = unsafe { libc::strcoll(a.as_ptr(), b.as_ptr()) };
848 result.cmp(&0)
849 }
850}
851
852fn locale_cmp(a: &str, b: &str) -> Ordering {
854 if IS_C_LOCALE.load(AtomicOrdering::Relaxed) {
855 a.cmp(b)
856 } else {
857 let ca = CString::new(a).unwrap_or_default();
858 let cb = CString::new(b).unwrap_or_default();
859 let result = unsafe { libc::strcoll(ca.as_ptr(), cb.as_ptr()) };
860 result.cmp(&0)
861 }
862}
863
864fn compare_entries(a: &FileEntry, b: &FileEntry, config: &LsConfig) -> Ordering {
865 let ord = match config.sort_by {
867 SortBy::Name => locale_cmp_cstr(&a.sort_key, &b.sort_key),
868 SortBy::Size => {
869 let size_ord = b.size.cmp(&a.size);
870 if size_ord == Ordering::Equal {
871 locale_cmp_cstr(&a.sort_key, &b.sort_key)
872 } else {
873 size_ord
874 }
875 }
876 SortBy::Time => {
877 let ta = a.time_secs(config.time_field);
878 let tb = b.time_secs(config.time_field);
879 let ord = tb.cmp(&ta);
880 if ord == Ordering::Equal {
881 let na = a.time_nsec(config.time_field);
882 let nb = b.time_nsec(config.time_field);
883 let nsec_ord = nb.cmp(&na);
884 if nsec_ord == Ordering::Equal {
885 locale_cmp_cstr(&a.sort_key, &b.sort_key)
886 } else {
887 nsec_ord
888 }
889 } else {
890 ord
891 }
892 }
893 SortBy::Extension => {
894 let ea = a.extension();
895 let eb = b.extension();
896 let ord = locale_cmp(ea, eb);
897 if ord == Ordering::Equal {
898 locale_cmp_cstr(&a.sort_key, &b.sort_key)
899 } else {
900 ord
901 }
902 }
903 SortBy::Version => version_cmp(&a.name, &b.name),
904 SortBy::None => Ordering::Equal,
905 SortBy::Width => {
906 let wa = a.display_width(config);
907 let wb = b.display_width(config);
908 wa.cmp(&wb)
909 }
910 };
911
912 if config.reverse { ord.reverse() } else { ord }
913}
914
915pub fn format_permissions(mode: u32) -> String {
921 let mut s = String::with_capacity(10);
922
923 s.push(match mode & (libc::S_IFMT as u32) {
925 x if x == libc::S_IFDIR as u32 => 'd',
926 x if x == libc::S_IFLNK as u32 => 'l',
927 x if x == libc::S_IFBLK as u32 => 'b',
928 x if x == libc::S_IFCHR as u32 => 'c',
929 x if x == libc::S_IFIFO as u32 => 'p',
930 x if x == libc::S_IFSOCK as u32 => 's',
931 _ => '-',
932 });
933
934 s.push(if mode & (libc::S_IRUSR as u32) != 0 {
936 'r'
937 } else {
938 '-'
939 });
940 s.push(if mode & (libc::S_IWUSR as u32) != 0 {
941 'w'
942 } else {
943 '-'
944 });
945 s.push(if mode & (libc::S_ISUID as u32) != 0 {
946 if mode & (libc::S_IXUSR as u32) != 0 {
947 's'
948 } else {
949 'S'
950 }
951 } else if mode & (libc::S_IXUSR as u32) != 0 {
952 'x'
953 } else {
954 '-'
955 });
956
957 s.push(if mode & (libc::S_IRGRP as u32) != 0 {
959 'r'
960 } else {
961 '-'
962 });
963 s.push(if mode & (libc::S_IWGRP as u32) != 0 {
964 'w'
965 } else {
966 '-'
967 });
968 s.push(if mode & (libc::S_ISGID as u32) != 0 {
969 if mode & (libc::S_IXGRP as u32) != 0 {
970 's'
971 } else {
972 'S'
973 }
974 } else if mode & (libc::S_IXGRP as u32) != 0 {
975 'x'
976 } else {
977 '-'
978 });
979
980 s.push(if mode & (libc::S_IROTH as u32) != 0 {
982 'r'
983 } else {
984 '-'
985 });
986 s.push(if mode & (libc::S_IWOTH as u32) != 0 {
987 'w'
988 } else {
989 '-'
990 });
991 s.push(if mode & (libc::S_ISVTX as u32) != 0 {
992 if mode & (libc::S_IXOTH as u32) != 0 {
993 't'
994 } else {
995 'T'
996 }
997 } else if mode & (libc::S_IXOTH as u32) != 0 {
998 'x'
999 } else {
1000 '-'
1001 });
1002
1003 s
1004}
1005
1006pub fn format_size(size: u64, human: bool, si: bool, kibibytes: bool) -> String {
1012 if human || si {
1013 let base: f64 = if si { 1000.0 } else { 1024.0 };
1014 let suffixes = ["", "K", "M", "G", "T", "P", "E"];
1015
1016 if size == 0 {
1017 return "0".to_string();
1018 }
1019
1020 let mut val = size as f64;
1021 let mut idx = 0;
1022 while val >= base && idx < suffixes.len() - 1 {
1023 val /= base;
1024 idx += 1;
1025 }
1026
1027 if idx == 0 {
1028 format!("{}", size)
1029 } else if val >= 10.0 {
1030 format!("{:.0}{}", val, suffixes[idx])
1031 } else {
1032 format!("{:.1}{}", val, suffixes[idx])
1033 }
1034 } else if kibibytes {
1035 let blocks_k = (size + 1023) / 1024;
1037 format!("{}", blocks_k)
1038 } else {
1039 format!("{}", size)
1040 }
1041}
1042
1043pub fn format_blocks(blocks_512: u64, human: bool, si: bool, kibibytes: bool) -> String {
1045 let bytes = blocks_512 * 512;
1046 if human || si {
1047 format_size(bytes, human, si, false)
1048 } else if kibibytes {
1049 let k = (bytes + 1023) / 1024;
1050 format!("{}", k)
1051 } else {
1052 let k = (bytes + 1023) / 1024;
1054 format!("{}", k)
1055 }
1056}
1057
1058pub fn format_time(secs: i64, nsec: i64, style: &TimeStyle) -> String {
1064 let now_sys = SystemTime::now();
1066 let now_secs = now_sys
1067 .duration_since(SystemTime::UNIX_EPOCH)
1068 .map(|d| d.as_secs() as i64)
1069 .unwrap_or(0);
1070 let six_months_ago = now_secs - 6 * 30 * 24 * 3600;
1071
1072 let tm = time_from_epoch(secs);
1074
1075 match style {
1076 TimeStyle::FullIso => {
1077 format!(
1078 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {}",
1079 tm.year,
1080 tm.month,
1081 tm.day,
1082 tm.hour,
1083 tm.min,
1084 tm.sec,
1085 nsec,
1086 format_tz_offset(tm.utc_offset_secs)
1087 )
1088 }
1089 TimeStyle::LongIso => {
1090 format!(
1091 "{:04}-{:02}-{:02} {:02}:{:02}",
1092 tm.year, tm.month, tm.day, tm.hour, tm.min
1093 )
1094 }
1095 TimeStyle::Iso => {
1096 if secs > six_months_ago && secs <= now_secs {
1097 format!("{:02}-{:02} {:02}:{:02}", tm.month, tm.day, tm.hour, tm.min)
1098 } else {
1099 format!("{:02}-{:02} {:04}", tm.month, tm.day, tm.year)
1100 }
1101 }
1102 TimeStyle::Locale | TimeStyle::Custom(_) => {
1103 let month_names = [
1104 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
1105 ];
1106 let mon = if tm.month >= 1 && tm.month <= 12 {
1107 month_names[(tm.month - 1) as usize]
1108 } else {
1109 "???"
1110 };
1111
1112 if secs > six_months_ago && secs <= now_secs {
1113 format!("{} {:>2} {:02}:{:02}", mon, tm.day, tm.hour, tm.min)
1114 } else {
1115 format!("{} {:>2} {:04}", mon, tm.day, tm.year)
1116 }
1117 }
1118 }
1119}
1120
1121fn format_tz_offset(offset_secs: i32) -> String {
1122 let sign = if offset_secs >= 0 { '+' } else { '-' };
1123 let abs = offset_secs.unsigned_abs();
1124 let hours = abs / 3600;
1125 let mins = (abs % 3600) / 60;
1126 format!("{}{:02}{:02}", sign, hours, mins)
1127}
1128
1129struct BrokenDownTime {
1130 year: i32,
1131 month: u32,
1132 day: u32,
1133 hour: u32,
1134 min: u32,
1135 sec: u32,
1136 utc_offset_secs: i32,
1137}
1138
1139fn time_from_epoch(secs: i64) -> BrokenDownTime {
1141 let mut tm: libc::tm = unsafe { std::mem::zeroed() };
1142 let time_t = secs as libc::time_t;
1143 unsafe {
1144 libc::localtime_r(&time_t, &mut tm);
1145 }
1146 BrokenDownTime {
1147 year: tm.tm_year + 1900,
1148 month: (tm.tm_mon + 1) as u32,
1149 day: tm.tm_mday as u32,
1150 hour: tm.tm_hour as u32,
1151 min: tm.tm_min as u32,
1152 sec: tm.tm_sec as u32,
1153 utc_offset_secs: tm.tm_gmtoff as i32,
1154 }
1155}
1156
1157fn lookup_user(uid: u32) -> String {
1164 use std::cell::RefCell;
1165 thread_local! {
1166 static CACHE: RefCell<HashMap<u32, String>> = RefCell::new(HashMap::new());
1167 }
1168 CACHE.with(|c| {
1169 let mut cache = c.borrow_mut();
1170 if let Some(name) = cache.get(&uid) {
1171 return name.clone();
1172 }
1173 let name = lookup_user_uncached(uid);
1174 cache.insert(uid, name.clone());
1175 name
1176 })
1177}
1178
1179fn lookup_user_uncached(uid: u32) -> String {
1180 let mut buf = vec![0u8; 1024];
1181 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
1182 let mut result: *mut libc::passwd = std::ptr::null_mut();
1183 let ret = unsafe {
1184 libc::getpwuid_r(
1185 uid,
1186 &mut pwd,
1187 buf.as_mut_ptr() as *mut libc::c_char,
1188 buf.len(),
1189 &mut result,
1190 )
1191 };
1192 if ret == 0 && !result.is_null() {
1193 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
1194 cstr.to_string_lossy().into_owned()
1195 } else {
1196 uid.to_string()
1197 }
1198}
1199
1200fn lookup_group(gid: u32) -> String {
1202 use std::cell::RefCell;
1203 thread_local! {
1204 static CACHE: RefCell<HashMap<u32, String>> = RefCell::new(HashMap::new());
1205 }
1206 CACHE.with(|c| {
1207 let mut cache = c.borrow_mut();
1208 if let Some(name) = cache.get(&gid) {
1209 return name.clone();
1210 }
1211 let name = lookup_group_uncached(gid);
1212 cache.insert(gid, name.clone());
1213 name
1214 })
1215}
1216
1217fn lookup_group_uncached(gid: u32) -> String {
1218 let mut buf = vec![0u8; 1024];
1219 let mut grp: libc::group = unsafe { std::mem::zeroed() };
1220 let mut result: *mut libc::group = std::ptr::null_mut();
1221 let ret = unsafe {
1222 libc::getgrgid_r(
1223 gid,
1224 &mut grp,
1225 buf.as_mut_ptr() as *mut libc::c_char,
1226 buf.len(),
1227 &mut result,
1228 )
1229 };
1230 if ret == 0 && !result.is_null() {
1231 let cstr = unsafe { std::ffi::CStr::from_ptr(grp.gr_name) };
1232 cstr.to_string_lossy().into_owned()
1233 } else {
1234 gid.to_string()
1235 }
1236}
1237
1238pub fn glob_match(pattern: &str, name: &str) -> bool {
1244 let pat = pattern.as_bytes();
1245 let txt = name.as_bytes();
1246 let mut pi = 0;
1247 let mut ti = 0;
1248 let mut star_p = usize::MAX;
1249 let mut star_t = 0;
1250
1251 while ti < txt.len() {
1252 if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == txt[ti]) {
1253 pi += 1;
1254 ti += 1;
1255 } else if pi < pat.len() && pat[pi] == b'*' {
1256 star_p = pi;
1257 star_t = ti;
1258 pi += 1;
1259 } else if star_p != usize::MAX {
1260 pi = star_p + 1;
1261 star_t += 1;
1262 ti = star_t;
1263 } else {
1264 return false;
1265 }
1266 }
1267 while pi < pat.len() && pat[pi] == b'*' {
1268 pi += 1;
1269 }
1270 pi == pat.len()
1271}
1272
1273fn should_ignore(name: &str, config: &LsConfig) -> bool {
1274 if config.ignore_backups && name.ends_with('~') {
1275 return true;
1276 }
1277 for pat in &config.ignore_patterns {
1278 if glob_match(pat, name) {
1279 return true;
1280 }
1281 }
1282 false
1283}
1284
1285pub fn read_entries(path: &Path, config: &LsConfig) -> io::Result<Vec<FileEntry>> {
1291 let mut entries = Vec::new();
1292
1293 let show_all = config.all && !config.almost_all;
1295 let show_hidden = config.all || config.almost_all;
1296
1297 if show_all {
1298 if let Ok(e) = FileEntry::from_path_with_name(".".to_string(), path, config) {
1300 entries.push(e);
1301 }
1302 let parent = path.parent().unwrap_or(path);
1303 if let Ok(e) = FileEntry::from_path_with_name("..".to_string(), parent, config) {
1304 entries.push(e);
1305 }
1306 }
1307
1308 for entry in fs::read_dir(path)? {
1309 let entry = entry?;
1310 let name = entry.file_name().to_string_lossy().into_owned();
1311
1312 if !show_hidden && name.starts_with('.') {
1314 continue;
1315 }
1316
1317 if should_ignore(&name, config) {
1319 continue;
1320 }
1321
1322 match FileEntry::from_dir_entry(&entry, config) {
1323 Ok(fe) => entries.push(fe),
1324 Err(e) => {
1325 eprintln!("ls: cannot access '{}': {}", entry.path().display(), e);
1326 }
1327 }
1328 }
1329
1330 Ok(entries)
1331}
1332
1333fn print_long(
1339 out: &mut impl Write,
1340 entries: &[FileEntry],
1341 config: &LsConfig,
1342 color_db: Option<&ColorDb>,
1343) -> io::Result<()> {
1344 if entries.is_empty() {
1345 return Ok(());
1346 }
1347
1348 let max_nlink = entries
1350 .iter()
1351 .map(|e| count_digits(e.nlink))
1352 .max()
1353 .unwrap_or(1);
1354 let max_owner = if config.show_owner {
1355 entries
1356 .iter()
1357 .map(|e| {
1358 if config.numeric_ids {
1359 e.uid.to_string().len()
1360 } else {
1361 lookup_user(e.uid).len()
1362 }
1363 })
1364 .max()
1365 .unwrap_or(0)
1366 } else {
1367 0
1368 };
1369 let max_group = if config.show_group {
1370 entries
1371 .iter()
1372 .map(|e| {
1373 if config.numeric_ids {
1374 e.gid.to_string().len()
1375 } else {
1376 lookup_group(e.gid).len()
1377 }
1378 })
1379 .max()
1380 .unwrap_or(0)
1381 } else {
1382 0
1383 };
1384
1385 let has_device = entries.iter().any(|e| {
1387 let ft = e.mode & (libc::S_IFMT as u32);
1388 ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32
1389 });
1390 let max_size = if has_device {
1391 entries
1393 .iter()
1394 .map(|e| {
1395 let ft = e.mode & (libc::S_IFMT as u32);
1396 if ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32 {
1397 format!("{}, {}", e.rdev_major, e.rdev_minor).len()
1398 } else {
1399 format_size(e.size, config.human_readable, config.si, config.kibibytes).len()
1400 }
1401 })
1402 .max()
1403 .unwrap_or(1)
1404 } else {
1405 entries
1406 .iter()
1407 .map(|e| format_size(e.size, config.human_readable, config.si, config.kibibytes).len())
1408 .max()
1409 .unwrap_or(1)
1410 };
1411
1412 let max_inode = if config.show_inode {
1413 entries
1414 .iter()
1415 .map(|e| count_digits(e.ino))
1416 .max()
1417 .unwrap_or(1)
1418 } else {
1419 0
1420 };
1421
1422 let max_blocks = if config.show_size {
1423 entries
1424 .iter()
1425 .map(|e| {
1426 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1427 })
1428 .max()
1429 .unwrap_or(1)
1430 } else {
1431 0
1432 };
1433
1434 for entry in entries {
1435 if entry.is_broken_deref() {
1437 let quoted = quote_name(&entry.name, config);
1438 writeln!(out, "l????????? ? ? ? ? ? {}", quoted)?;
1439 continue;
1440 }
1441
1442 if config.show_inode {
1444 write!(out, "{:>width$} ", entry.ino, width = max_inode)?;
1445 }
1446
1447 if config.show_size {
1449 let bs = format_blocks(
1450 entry.blocks,
1451 config.human_readable,
1452 config.si,
1453 config.kibibytes,
1454 );
1455 write!(out, "{:>width$} ", bs, width = max_blocks)?;
1456 }
1457
1458 write!(out, "{} ", format_permissions(entry.mode))?;
1460
1461 write!(out, "{:>width$} ", entry.nlink, width = max_nlink)?;
1463
1464 if config.show_owner {
1466 let owner = if config.numeric_ids {
1467 entry.uid.to_string()
1468 } else {
1469 lookup_user(entry.uid)
1470 };
1471 write!(out, "{:<width$} ", owner, width = max_owner)?;
1472 }
1473
1474 if config.show_group {
1476 let group = if config.numeric_ids {
1477 entry.gid.to_string()
1478 } else {
1479 lookup_group(entry.gid)
1480 };
1481 write!(out, "{:<width$} ", group, width = max_group)?;
1482 }
1483
1484 let ft = entry.mode & (libc::S_IFMT as u32);
1486 if ft == libc::S_IFBLK as u32 || ft == libc::S_IFCHR as u32 {
1487 let dev = format!("{}, {}", entry.rdev_major, entry.rdev_minor);
1488 write!(out, "{:>width$} ", dev, width = max_size)?;
1489 } else {
1490 let sz = format_size(
1491 entry.size,
1492 config.human_readable,
1493 config.si,
1494 config.kibibytes,
1495 );
1496 write!(out, "{:>width$} ", sz, width = max_size)?;
1497 }
1498
1499 let ts = format_time(
1501 entry.time_secs(config.time_field),
1502 entry.time_nsec(config.time_field),
1503 &config.time_style,
1504 );
1505 write!(out, "{} ", ts)?;
1506
1507 let quoted = quote_name(&entry.name, config);
1509 if let Some(db) = color_db {
1510 let c = db.color_for(entry);
1511 if c.is_empty() {
1512 write!(out, "{}", quoted)?;
1513 } else {
1514 write!(out, "{}{}{}", c, quoted, db.reset)?;
1515 }
1516 } else {
1517 write!(out, "{}", quoted)?;
1518 }
1519
1520 let is_symlink = (entry.mode & libc::S_IFMT as u32) == libc::S_IFLNK as u32;
1523 if !is_symlink {
1524 let ind = entry.indicator(config.indicator_style);
1525 if !ind.is_empty() {
1526 write!(out, "{}", ind)?;
1527 }
1528 }
1529
1530 if let Some(ref target) = entry.link_target {
1532 let target_quoted = quote_name(target, config);
1533 if entry.link_target_ok
1534 && config.indicator_style != IndicatorStyle::None
1535 && config.indicator_style != IndicatorStyle::Slash
1536 {
1537 let target_ind = get_link_target_indicator(&entry.path, config.indicator_style);
1539 write!(out, " -> {}{}", target_quoted, target_ind)?;
1540 } else {
1541 write!(out, " -> {}", target_quoted)?;
1542 }
1543 }
1544
1545 if config.zero {
1546 out.write_all(&[0u8])?;
1547 } else {
1548 writeln!(out)?;
1549 }
1550 }
1551
1552 Ok(())
1553}
1554
1555fn count_digits(n: u64) -> usize {
1556 if n == 0 {
1557 return 1;
1558 }
1559 let mut count = 0;
1560 let mut v = n;
1561 while v > 0 {
1562 count += 1;
1563 v /= 10;
1564 }
1565 count
1566}
1567
1568fn indent(out: &mut impl Write, from: usize, to: usize, tab: usize) -> io::Result<()> {
1575 let mut pos = from;
1576 while pos < to {
1577 if tab != 0 && to / tab > (pos + 1) / tab {
1578 out.write_all(b"\t")?;
1579 pos += tab - pos % tab;
1580 } else {
1581 out.write_all(b" ")?;
1582 pos += 1;
1583 }
1584 }
1585 Ok(())
1586}
1587
1588fn write_entry_prefix(
1590 out: &mut impl Write,
1591 entry: &FileEntry,
1592 config: &LsConfig,
1593 max_inode_w: usize,
1594 max_blocks_w: usize,
1595) -> io::Result<()> {
1596 if config.show_inode {
1597 write!(out, "{:>width$} ", entry.ino, width = max_inode_w)?;
1598 }
1599 if config.show_size {
1600 let bs = format_blocks(
1601 entry.blocks,
1602 config.human_readable,
1603 config.si,
1604 config.kibibytes,
1605 );
1606 write!(out, "{:>width$} ", bs, width = max_blocks_w)?;
1607 }
1608 Ok(())
1609}
1610
1611fn write_entry_name(
1613 out: &mut impl Write,
1614 display: &str,
1615 entry: &FileEntry,
1616 config: &LsConfig,
1617 color_db: Option<&ColorDb>,
1618) -> io::Result<()> {
1619 if let Some(db) = color_db {
1620 let c = db.color_for(entry);
1621 let quoted = quote_name(&entry.name, config);
1622 let ind = entry.indicator(config.indicator_style);
1623 if c.is_empty() {
1624 write!(out, "{}{}", quoted, ind)?;
1625 } else {
1626 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
1627 }
1628 } else {
1629 write!(out, "{}", display)?;
1630 }
1631 Ok(())
1632}
1633
1634fn print_with_separator(
1638 out: &mut impl Write,
1639 entries: &[FileEntry],
1640 config: &LsConfig,
1641 color_db: Option<&ColorDb>,
1642 sep: u8,
1643 eol: u8,
1644) -> io::Result<()> {
1645 let line_length = config.width;
1646
1647 let max_inode_w = if config.show_inode {
1648 entries
1649 .iter()
1650 .map(|e| count_digits(e.ino))
1651 .max()
1652 .unwrap_or(1)
1653 } else {
1654 0
1655 };
1656 let max_blocks_w = if config.show_size {
1657 entries
1658 .iter()
1659 .map(|e| {
1660 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1661 })
1662 .max()
1663 .unwrap_or(1)
1664 } else {
1665 0
1666 };
1667
1668 let prefix_width = if config.show_inode && config.show_size {
1669 max_inode_w + 1 + max_blocks_w + 1
1670 } else if config.show_inode {
1671 max_inode_w + 1
1672 } else if config.show_size {
1673 max_blocks_w + 1
1674 } else {
1675 0
1676 };
1677
1678 let mut pos: usize = 0;
1679
1680 for (i, entry) in entries.iter().enumerate() {
1681 let quoted = quote_name(&entry.name, config);
1682 let ind = entry.indicator(config.indicator_style);
1683 let len = if line_length > 0 {
1684 quoted.len() + ind.len() + prefix_width
1685 } else {
1686 0
1687 };
1688
1689 if i > 0 {
1690 let fits =
1693 line_length == 0 || (pos + len + 2 < line_length && pos <= usize::MAX - len - 2);
1694 let separator: u8 = if fits { b' ' } else { eol };
1695
1696 out.write_all(&[sep, separator])?;
1697 if fits {
1698 pos += 2;
1699 } else {
1700 pos = 0;
1701 }
1702 }
1703
1704 write_entry_prefix(out, entry, config, max_inode_w, max_blocks_w)?;
1705 if let Some(db) = color_db {
1706 let c = db.color_for(entry);
1707 if c.is_empty() {
1708 write!(out, "{}{}", quoted, ind)?;
1709 } else {
1710 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
1711 }
1712 } else {
1713 write!(out, "{}{}", quoted, ind)?;
1714 }
1715 pos += len;
1716 }
1717 if !entries.is_empty() {
1718 out.write_all(&[eol])?;
1719 }
1720 Ok(())
1721}
1722
1723fn print_columns(
1725 out: &mut impl Write,
1726 entries: &[FileEntry],
1727 config: &LsConfig,
1728 color_db: Option<&ColorDb>,
1729) -> io::Result<()> {
1730 if entries.is_empty() {
1731 return Ok(());
1732 }
1733
1734 let eol: u8 = if config.zero { 0 } else { b'\n' };
1735
1736 if config.width == 0 {
1740 return print_with_separator(out, entries, config, color_db, b' ', eol);
1741 }
1742
1743 let by_columns = config.format == OutputFormat::Columns;
1744 let tab = config.tab_size;
1745 let term_width = config.width;
1746
1747 let max_inode_w = if config.show_inode {
1748 entries
1749 .iter()
1750 .map(|e| count_digits(e.ino))
1751 .max()
1752 .unwrap_or(1)
1753 } else {
1754 0
1755 };
1756 let max_blocks_w = if config.show_size {
1757 entries
1758 .iter()
1759 .map(|e| {
1760 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1761 })
1762 .max()
1763 .unwrap_or(1)
1764 } else {
1765 0
1766 };
1767
1768 let prefix_width = if config.show_inode && config.show_size {
1769 max_inode_w + 1 + max_blocks_w + 1
1770 } else if config.show_inode {
1771 max_inode_w + 1
1772 } else if config.show_size {
1773 max_blocks_w + 1
1774 } else {
1775 0
1776 };
1777
1778 let items: Vec<(String, usize, &FileEntry)> = entries
1780 .iter()
1781 .map(|e| {
1782 let quoted = quote_name(&e.name, config);
1783 let ind = e.indicator(config.indicator_style);
1784 let display = format!("{}{}", quoted, ind);
1785 let w = display.len() + prefix_width;
1786 (display, w, e)
1787 })
1788 .collect();
1789
1790 let n = items.len();
1791
1792 let min_col_w: usize = 3;
1795 let max_possible_cols = if term_width < min_col_w {
1796 1
1797 } else {
1798 let base = term_width / min_col_w;
1799 let extra = if !term_width.is_multiple_of(min_col_w) {
1800 1
1801 } else {
1802 0
1803 };
1804 std::cmp::min(base + extra, n)
1805 };
1806
1807 let mut col_arrs: Vec<Vec<usize>> = (0..max_possible_cols)
1809 .map(|i| vec![min_col_w; i + 1])
1810 .collect();
1811 let mut line_lens: Vec<usize> = (0..max_possible_cols)
1812 .map(|i| (i + 1) * min_col_w)
1813 .collect();
1814 let mut valid: Vec<bool> = vec![true; max_possible_cols];
1815
1816 for filesno in 0..n {
1817 let name_length = items[filesno].1;
1818
1819 for i in 0..max_possible_cols {
1820 if !valid[i] {
1821 continue;
1822 }
1823 let ncols = i + 1;
1824 let idx = if by_columns {
1825 filesno / ((n + i) / ncols)
1826 } else {
1827 filesno % ncols
1828 };
1829 let real_length = name_length + if idx == i { 0 } else { 2 };
1831
1832 if col_arrs[i][idx] < real_length {
1833 line_lens[i] += real_length - col_arrs[i][idx];
1834 col_arrs[i][idx] = real_length;
1835 valid[i] = line_lens[i] < term_width;
1836 }
1837 }
1838 }
1839
1840 let mut num_cols = 1;
1842 for cols in (1..=max_possible_cols).rev() {
1843 if valid[cols - 1] {
1844 num_cols = cols;
1845 break;
1846 }
1847 }
1848
1849 if num_cols <= 1 {
1850 return print_single_column(out, entries, config, color_db);
1851 }
1852
1853 let col_arr = &col_arrs[num_cols - 1];
1854
1855 if by_columns {
1856 let num_rows = (n + num_cols - 1) / num_cols;
1858 for row in 0..num_rows {
1859 let mut pos = 0;
1860 let mut col = 0;
1861 let mut filesno = row;
1862
1863 loop {
1864 let (ref display, w, entry) = items[filesno];
1865 let max_w = col_arr[col];
1866
1867 write_entry_prefix(out, entry, config, max_inode_w, max_blocks_w)?;
1868 write_entry_name(out, display, entry, config, color_db)?;
1869
1870 if n.saturating_sub(num_rows) <= filesno {
1871 break;
1872 }
1873 filesno += num_rows;
1874
1875 indent(out, pos + w, pos + max_w, tab)?;
1876 pos += max_w;
1877 col += 1;
1878 }
1879 out.write_all(&[eol])?;
1880 }
1881 } else {
1882 let (ref display0, w0, entry0) = items[0];
1884 write_entry_prefix(out, entry0, config, max_inode_w, max_blocks_w)?;
1885 write_entry_name(out, display0, entry0, config, color_db)?;
1886
1887 let mut pos: usize = 0;
1888 let mut prev_w = w0;
1889 let mut prev_max_w = col_arr[0];
1890
1891 for filesno in 1..n {
1892 let col_idx = filesno % num_cols;
1893
1894 if col_idx == 0 {
1895 out.write_all(&[eol])?;
1896 pos = 0;
1897 } else {
1898 indent(out, pos + prev_w, pos + prev_max_w, tab)?;
1899 pos += prev_max_w;
1900 }
1901
1902 let (ref display, w, entry) = items[filesno];
1903 write_entry_prefix(out, entry, config, max_inode_w, max_blocks_w)?;
1904 write_entry_name(out, display, entry, config, color_db)?;
1905
1906 prev_w = w;
1907 prev_max_w = col_arr[col_idx];
1908 }
1909 out.write_all(&[eol])?;
1910 }
1911
1912 Ok(())
1913}
1914
1915fn print_single_column(
1920 out: &mut impl Write,
1921 entries: &[FileEntry],
1922 config: &LsConfig,
1923 color_db: Option<&ColorDb>,
1924) -> io::Result<()> {
1925 let max_inode_w = if config.show_inode {
1926 entries
1927 .iter()
1928 .map(|e| count_digits(e.ino))
1929 .max()
1930 .unwrap_or(1)
1931 } else {
1932 0
1933 };
1934 let max_blocks_w = if config.show_size {
1935 entries
1936 .iter()
1937 .map(|e| {
1938 format_blocks(e.blocks, config.human_readable, config.si, config.kibibytes).len()
1939 })
1940 .max()
1941 .unwrap_or(1)
1942 } else {
1943 0
1944 };
1945
1946 for entry in entries {
1947 if config.show_inode {
1948 write!(out, "{:>width$} ", entry.ino, width = max_inode_w)?;
1949 }
1950 if config.show_size {
1951 let bs = format_blocks(
1952 entry.blocks,
1953 config.human_readable,
1954 config.si,
1955 config.kibibytes,
1956 );
1957 write!(out, "{:>width$} ", bs, width = max_blocks_w)?;
1958 }
1959
1960 let quoted = quote_name(&entry.name, config);
1961 if let Some(db) = color_db {
1962 let c = db.color_for(entry);
1963 if c.is_empty() {
1964 write!(out, "{}", quoted)?;
1965 } else {
1966 write!(out, "{}{}{}", c, quoted, db.reset)?;
1967 }
1968 } else {
1969 write!(out, "{}", quoted)?;
1970 }
1971
1972 let ind = entry.indicator(config.indicator_style);
1973 if !ind.is_empty() {
1974 write!(out, "{}", ind)?;
1975 }
1976
1977 if config.zero {
1978 out.write_all(&[0u8])?;
1979 } else {
1980 writeln!(out)?;
1981 }
1982 }
1983 Ok(())
1984}
1985
1986pub fn print_comma(
1991 out: &mut impl Write,
1992 entries: &[FileEntry],
1993 config: &LsConfig,
1994 color_db: Option<&ColorDb>,
1995) -> io::Result<()> {
1996 let eol: u8 = if config.zero { 0 } else { b'\n' };
1997 let line_length = config.width;
1998 let mut pos: usize = 0;
1999
2000 for (i, entry) in entries.iter().enumerate() {
2001 let quoted = quote_name(&entry.name, config);
2002 let ind = entry.indicator(config.indicator_style);
2003 let name_len = if line_length > 0 {
2004 quoted.len() + ind.len()
2005 } else {
2006 0
2007 };
2008
2009 if i > 0 {
2010 let fits = line_length == 0
2013 || (pos + name_len + 2 < line_length && pos <= usize::MAX - name_len - 2);
2014 if fits {
2015 write!(out, ", ")?;
2016 pos += 2;
2017 } else {
2018 write!(out, ",")?;
2019 out.write_all(&[eol])?;
2020 pos = 0;
2021 }
2022 }
2023
2024 if let Some(db) = color_db {
2025 let c = db.color_for(entry);
2026 if c.is_empty() {
2027 write!(out, "{}{}", quoted, ind)?;
2028 } else {
2029 write!(out, "{}{}{}{}", c, quoted, db.reset, ind)?;
2030 }
2031 } else {
2032 write!(out, "{}{}", quoted, ind)?;
2033 }
2034 pos += name_len;
2035 }
2036 if !entries.is_empty() {
2037 out.write_all(&[eol])?;
2038 }
2039 Ok(())
2040}
2041
2042fn print_total(out: &mut impl Write, entries: &[FileEntry], config: &LsConfig) -> io::Result<()> {
2047 let total_blocks: u64 = entries.iter().map(|e| e.blocks).sum();
2048 let formatted = format_blocks(
2049 total_blocks,
2050 config.human_readable,
2051 config.si,
2052 config.kibibytes,
2053 );
2054 write!(out, "total {}", formatted)?;
2055 if config.zero {
2056 out.write_all(&[0u8])
2057 } else {
2058 writeln!(out)
2059 }
2060}
2061
2062pub fn ls_dir(
2068 out: &mut impl Write,
2069 path: &Path,
2070 config: &LsConfig,
2071 color_db: Option<&ColorDb>,
2072 show_header: bool,
2073) -> io::Result<bool> {
2074 if show_header {
2075 writeln!(out, "{}:", path.display())?;
2076 }
2077
2078 let mut entries = read_entries(path, config)?;
2079 sort_entries(&mut entries, config);
2080
2081 let has_broken_deref = entries.iter().any(|e| e.is_broken_deref());
2083
2084 if config.long_format || config.show_size {
2086 print_total(out, &entries, config)?;
2087 }
2088
2089 match config.format {
2090 OutputFormat::Long => print_long(out, &entries, config, color_db)?,
2091 OutputFormat::SingleColumn => print_single_column(out, &entries, config, color_db)?,
2092 OutputFormat::Columns | OutputFormat::Across => {
2093 print_columns(out, &entries, config, color_db)?
2094 }
2095 OutputFormat::Comma => print_comma(out, &entries, config, color_db)?,
2096 }
2097
2098 if config.recursive {
2100 let dirs: Vec<PathBuf> = entries
2101 .iter()
2102 .filter(|e| {
2103 e.is_directory()
2104 && e.name != "."
2105 && e.name != ".."
2106 && (e.mode & (libc::S_IFMT as u32)) != libc::S_IFLNK as u32
2107 })
2108 .map(|e| e.path.clone())
2109 .collect();
2110
2111 for dir in dirs {
2112 writeln!(out)?;
2113 ls_dir(out, &dir, config, color_db, true)?;
2114 }
2115 }
2116
2117 Ok(!has_broken_deref)
2118}
2119
2120pub fn ls_main(paths: &[String], config: &LsConfig) -> io::Result<bool> {
2124 let stdout = io::stdout();
2125 let is_tty = atty_stdout();
2126 #[cfg(target_os = "linux")]
2131 if !is_tty {
2132 unsafe {
2133 libc::fcntl(1, 1031 , 4096i32)
2134 };
2135 }
2136 let buf_cap = if is_tty { 64 * 1024 } else { 4 * 1024 };
2137 let mut out = BufWriter::with_capacity(buf_cap, stdout.lock());
2138
2139 let color_db = match config.color {
2140 ColorMode::Always => Some(ColorDb::from_env()),
2141 ColorMode::Auto => {
2142 if atty_stdout() {
2143 Some(ColorDb::from_env())
2144 } else {
2145 None
2146 }
2147 }
2148 ColorMode::Never => None,
2149 };
2150
2151 let mut had_error = false;
2152
2153 let mut file_args: Vec<FileEntry> = Vec::new();
2155 let mut dir_args: Vec<PathBuf> = Vec::new();
2156
2157 for p in paths {
2158 let path = PathBuf::from(p);
2159 let meta_result = if config.dereference {
2160 match fs::metadata(&path) {
2161 Ok(m) => Ok(m),
2162 Err(e) => {
2163 if let Ok(lmeta) = fs::symlink_metadata(&path) {
2165 if lmeta.file_type().is_symlink() {
2166 eprintln!(
2168 "ls: cannot access '{}': {}",
2169 p,
2170 crate::common::io_error_msg(&e)
2171 );
2172 had_error = true;
2173 file_args.push(FileEntry::broken_deref(p.to_string(), path));
2174 continue;
2175 }
2176 }
2177 Err(e)
2178 }
2179 }
2180 } else {
2181 fs::symlink_metadata(&path)
2182 };
2183
2184 match meta_result {
2185 Ok(meta) => {
2186 if config.directory || !meta.is_dir() {
2187 match FileEntry::from_path_with_name(p.to_string(), &path, config) {
2188 Ok(fe) => file_args.push(fe),
2189 Err(e) => {
2190 eprintln!("ls: cannot access '{}': {}", p, e);
2191 had_error = true;
2192 }
2193 }
2194 } else {
2195 dir_args.push(path);
2196 }
2197 }
2198 Err(e) => {
2199 eprintln!(
2200 "ls: cannot access '{}': {}",
2201 p,
2202 crate::common::io_error_msg(&e)
2203 );
2204 had_error = true;
2205 }
2206 }
2207 }
2208
2209 sort_entries(&mut file_args, config);
2211
2212 if !file_args.is_empty() {
2214 match config.format {
2215 OutputFormat::Long => print_long(&mut out, &file_args, config, color_db.as_ref())?,
2216 OutputFormat::SingleColumn => {
2217 print_single_column(&mut out, &file_args, config, color_db.as_ref())?
2218 }
2219 OutputFormat::Columns | OutputFormat::Across => {
2220 print_columns(&mut out, &file_args, config, color_db.as_ref())?
2221 }
2222 OutputFormat::Comma => print_comma(&mut out, &file_args, config, color_db.as_ref())?,
2223 }
2224 }
2225
2226 dir_args.sort_by(|a, b| {
2228 let an = a.to_string_lossy();
2229 let bn = b.to_string_lossy();
2230 let ord = locale_cmp(&an, &bn);
2231 if config.reverse { ord.reverse() } else { ord }
2232 });
2233
2234 let show_header =
2235 dir_args.len() > 1 || (!file_args.is_empty() && !dir_args.is_empty()) || config.recursive;
2236
2237 for (i, dir) in dir_args.iter().enumerate() {
2238 if i > 0 || !file_args.is_empty() {
2239 writeln!(out)?;
2240 }
2241 match ls_dir(&mut out, dir, config, color_db.as_ref(), show_header) {
2242 Ok(true) => {}
2243 Ok(false) => {
2244 had_error = true;
2245 }
2246 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => return Err(e),
2247 Err(e) => {
2248 eprintln!(
2249 "ls: cannot open directory '{}': {}",
2250 dir.display(),
2251 crate::common::io_error_msg(&e)
2252 );
2253 had_error = true;
2254 }
2255 }
2256 }
2257
2258 out.flush()?;
2259
2260 Ok(!had_error)
2261}
2262
2263pub fn atty_stdout() -> bool {
2265 unsafe { libc::isatty(1) != 0 }
2266}
2267
2268pub fn collect_entries(path: &Path, config: &LsConfig) -> io::Result<Vec<FileEntry>> {
2274 let mut entries = read_entries(path, config)?;
2275 sort_entries(&mut entries, config);
2276 Ok(entries)
2277}
2278
2279pub fn render_long(entries: &[FileEntry], config: &LsConfig) -> io::Result<String> {
2281 let mut buf = Vec::new();
2282 print_long(&mut buf, entries, config, None)?;
2283 Ok(String::from_utf8_lossy(&buf).into_owned())
2284}
2285
2286pub fn render_single_column(entries: &[FileEntry], config: &LsConfig) -> io::Result<String> {
2288 let mut buf = Vec::new();
2289 print_single_column(&mut buf, entries, config, None)?;
2290 Ok(String::from_utf8_lossy(&buf).into_owned())
2291}
2292
2293pub fn render_dir(path: &Path, config: &LsConfig) -> io::Result<String> {
2295 let mut buf = Vec::new();
2296 ls_dir(&mut buf, path, config, None, false)?;
2297 Ok(String::from_utf8_lossy(&buf).into_owned())
2298}