use super::{Entry, FmtEnv, nanos_of};
use crate::fs::Metadata;
fn type_char(meta: &Metadata) -> char {
if meta.file_type.is_dir() {
'd'
} else if meta.file_type.is_symlink() {
'l'
} else if matches!(meta.file_type, crate::fs::FileType::Fifo) {
'p'
} else {
'f'
}
}
pub(super) fn symbolic_mode(meta: &Metadata) -> String {
let mut s = String::with_capacity(10);
s.push(match type_char(meta) {
'f' => '-',
c => c,
});
let mode = meta.mode;
for (shift, special, set_char) in [(6u32, 0o4000, 's'), (3, 0o2000, 's'), (0, 0o1000, 't')] {
let bits = (mode >> shift) & 7;
s.push(if bits & 4 != 0 { 'r' } else { '-' });
s.push(if bits & 2 != 0 { 'w' } else { '-' });
let x = bits & 1 != 0;
s.push(match (mode & special != 0, x) {
(true, true) => set_char,
(true, false) => set_char.to_ascii_uppercase(),
(false, true) => 'x',
(false, false) => '-',
});
}
s
}
fn dirname(path: &str) -> &str {
match path.rfind('/') {
Some(idx) => &path[..idx],
None => ".",
}
}
fn basename(path: &str) -> String {
if path.chars().all(|c| c == '/') && !path.is_empty() {
return "/".to_string();
}
let trimmed = path.trim_end_matches('/');
let tail = &path[trimmed.len()..];
let base = trimmed.rsplit('/').next().unwrap_or(trimmed);
format!("{base}{tail}")
}
fn time_value(nanos: i128, key: char, env: &FmtEnv) -> String {
let secs = i64::try_from(nanos.div_euclid(1_000_000_000)).unwrap_or(0);
let frac = format!("{:09}0", nanos.rem_euclid(1_000_000_000));
let fmt = |f: &str| env.strftime(secs, f);
match key {
'@' => format!("{secs}.{frac}"),
'S' => format!("{}.{frac}", fmt("%S")),
'T' => format!("{}.{frac}", fmt("%H:%M:%S")),
'+' => format!("{}.{frac}", fmt("%Y-%m-%d+%H:%M:%S")),
k => fmt(&format!("%{k}")),
}
}
fn ctime_value(nanos: i128, env: &FmtEnv) -> String {
let secs = i64::try_from(nanos.div_euclid(1_000_000_000)).unwrap_or(0);
format!(
"{}.{:09}0 {}",
env.strftime(secs, "%a %b %e %H:%M:%S"),
nanos.rem_euclid(1_000_000_000),
env.strftime(secs, "%Y")
)
}
fn blocks_1k(meta: &Metadata) -> u64 {
if meta.file_type.is_symlink() {
0
} else {
meta.size.div_ceil(4096) * 4
}
}
fn pad(value: String, left: bool, width: Option<usize>, precision: Option<usize>) -> String {
let mut value = value;
if let Some(p) = precision {
value = value.chars().take(p).collect();
}
match width {
Some(w) if value.chars().count() < w => {
let fill = " ".repeat(w - value.chars().count());
if left { value + &fill } else { fill + &value }
}
_ => value,
}
}
pub(super) fn render(fmt: &str, entry: &Entry, start: &str, env: &FmtEnv) -> String {
let chars: Vec<char> = fmt.chars().collect();
let mut out = String::new();
let mut i = 0;
let meta = entry.meta();
while i < chars.len() {
let c = chars[i];
i += 1;
match c {
'\\' => {
let Some(&n) = chars.get(i) else {
out.push('\\');
break;
};
i += 1;
match n {
'a' => out.push('\x07'),
'b' => out.push('\x08'),
'c' => return out,
'f' => out.push('\x0c'),
'n' => out.push('\n'),
'r' => out.push('\r'),
't' => out.push('\t'),
'v' => out.push('\x0b'),
'\\' => out.push('\\'),
'0'..='7' => {
let mut v = n.to_digit(8).unwrap_or(0);
let mut digits = 1;
while digits < 3
&& let Some(d) = chars.get(i).and_then(|c| c.to_digit(8))
{
v = v * 8 + d;
i += 1;
digits += 1;
}
out.push(char::from_u32(v & 0xff).unwrap_or('?'));
}
other => {
out.push('\\');
out.push(other);
}
}
}
'%' => {
let spec_start = i - 1;
let mut left = false;
while let Some(&f) = chars.get(i)
&& matches!(f, '-' | '+' | ' ' | '#' | '0')
{
left |= f == '-';
i += 1;
}
let mut width = None;
while let Some(d) = chars.get(i).and_then(|c| c.to_digit(10)) {
width = Some(width.unwrap_or(0usize) * 10 + d as usize);
i += 1;
}
let mut precision = None;
if chars.get(i) == Some(&'.') {
i += 1;
precision = Some(0usize);
while let Some(d) = chars.get(i).and_then(|c| c.to_digit(10)) {
precision = Some(precision.unwrap_or(0) * 10 + d as usize);
i += 1;
}
}
let Some(&d) = chars.get(i) else {
out.extend(&chars[spec_start..]);
break;
};
i += 1;
let value = match d {
'%' => "%".to_string(),
'p' => entry.display.clone(),
'f' => basename(&entry.display),
'h' => dirname(&entry.display).to_string(),
'H' => start.to_string(),
'P' => {
if entry.display == start {
String::new()
} else {
entry.display[start.len().min(entry.display.len())..]
.trim_start_matches('/')
.to_string()
}
}
'd' => entry.depth.to_string(),
's' => meta.size.to_string(),
'k' => blocks_1k(meta).to_string(),
'b' => (blocks_1k(meta) * 2).to_string(),
'm' => format!("{:o}", meta.mode & 0o7777),
'M' => symbolic_mode(meta),
'y' => type_char(&entry.lmeta).to_string(),
'Y' => match (&entry.link_target, &entry.fmeta) {
(None, _) => type_char(&entry.lmeta).to_string(),
(Some(_), Some(m)) => type_char(m).to_string(),
(Some(_), None) => "N".to_string(),
},
'l' => entry.link_target.clone().unwrap_or_default(),
'n' => entry.nlink().to_string(),
'u' | 'g' => env.username.clone(),
'U' | 'G' => "1000".to_string(),
'i' | 'D' => "0".to_string(),
'F' => "vfs".to_string(),
'S' => "1".to_string(),
'Z' => "?".to_string(),
't' | 'a' => ctime_value(nanos_of(meta.modified), env),
'c' => ctime_value(nanos_of(meta.modified), env),
'T' | 'A' | 'C' | 'B' => {
let Some(&key) = chars.get(i) else {
out.extend(&chars[spec_start..]);
break;
};
i += 1;
let t = if d == 'B' {
nanos_of(meta.created)
} else {
nanos_of(meta.modified)
};
time_value(t, key, env)
}
_ => {
out.extend(&chars[spec_start..i]);
continue;
}
};
out.push_str(&pad(value, left, width, precision));
}
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_parts_match_gnu() {
assert_eq!(dirname("d"), ".");
assert_eq!(dirname("/"), "");
assert_eq!(dirname("./d"), ".");
assert_eq!(dirname("d/"), "d");
assert_eq!(basename("d/f"), "f");
assert_eq!(basename("d/"), "d/");
assert_eq!(basename("/"), "/");
assert_eq!(basename("."), ".");
}
#[test]
fn padding() {
assert_eq!(pad("3".into(), true, Some(6), None), "3 ");
assert_eq!(pad("3".into(), false, Some(6), None), " 3");
assert_eq!(pad("abc".into(), false, None, Some(2)), "ab");
}
}