use std::fmt::Write as _;
use std::path::Path;
use ferrosys::ext::ondisk::Timestamp;
use crate::args::os;
const DAY: i64 = 86_400;
#[must_use]
pub fn uuid(bytes: &[u8; 16]) -> String {
let mut out = String::with_capacity(36);
for (i, b) in bytes.iter().enumerate() {
if matches!(i, 4 | 6 | 8 | 10) {
out.push('-');
}
let _ = write!(out, "{b:02x}");
}
out
}
#[must_use]
pub fn label(name: &[u8; 16]) -> Option<String> {
let end = name.iter().position(|&b| b == 0).unwrap_or(name.len());
(end != 0).then(|| printable(&name[..end]))
}
#[must_use]
pub fn printable(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len());
for c in String::from_utf8_lossy(bytes).chars() {
if c.is_control() {
out.push_str(&format!("\\x{:02x}", c as u32));
} else {
out.push(c);
}
}
out
}
#[must_use]
pub fn mode(mode: u16) -> String {
let kind = match mode & 0o170000 {
0o140000 => 's',
0o120000 => 'l',
0o100000 => '-',
0o060000 => 'b',
0o040000 => 'd',
0o020000 => 'c',
0o010000 => 'p',
_ => '?',
};
let mut out = String::with_capacity(10);
out.push(kind);
let triple = |shift: u32, special: bool, special_set: char, special_clear: char| {
let bits = (mode >> shift) & 0o7;
let mut t = String::with_capacity(3);
t.push(if bits & 4 != 0 { 'r' } else { '-' });
t.push(if bits & 2 != 0 { 'w' } else { '-' });
t.push(match (bits & 1 != 0, special) {
(true, true) => special_set,
(false, true) => special_clear,
(true, false) => 'x',
(false, false) => '-',
});
t
};
out.push_str(&triple(6, mode & 0o4000 != 0, 's', 'S'));
out.push_str(&triple(3, mode & 0o2000 != 0, 's', 'S'));
out.push_str(&triple(0, mode & 0o1000 != 0, 't', 'T'));
out
}
#[must_use]
pub fn uri_reference(path: &Path) -> String {
let bytes = os::bytes(path.as_os_str());
let mut out = String::new();
if bytes.first() == Some(&b'/') {
out.push_str("file://");
}
for &b in bytes {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'/') {
out.push(char::from(b));
} else {
let _ = write!(out, "%{b:02X}");
}
}
out
}
#[must_use]
pub fn iso8601(secs: i64) -> String {
let days = secs.div_euclid(DAY);
let rem = secs.rem_euclid(DAY);
let (y, m, d) = civil_from_days(days);
let (h, min, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);
format!("{y:04}-{m:02}-{d:02}T{h:02}:{min:02}:{s:02}Z")
}
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097); let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; let y = yoe + era * 400 + i64::from(m <= 2);
(y, m, d)
}
#[must_use]
pub fn pax_time(t: Timestamp) -> String {
if t.nanos == 0 {
return t.secs.to_string();
}
if t.secs < 0 {
let whole = t.secs + 1;
let frac = 1_000_000_000 - t.nanos;
if whole == 0 {
return format!("-0.{frac:09}");
}
return format!("{whole}.{frac:09}");
}
format!("{}.{:09}", t.secs, t.nanos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_uuid_is_written_in_the_canonical_dashed_form() {
assert_eq!(
uuid(&[
0xf0, 0xe1, 0x70, 0x55, 0, 0, 0x40, 0, 0x80, 0, 0, 0, 0, 0, 0, 0
]),
"f0e17055-0000-4000-8000-000000000000"
);
assert_eq!(uuid(&[0; 16]), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn a_label_reads_up_to_its_first_nul() {
assert_eq!(
label(b"rootfs\0\0\0\0\0\0\0\0\0\0").as_deref(),
Some("rootfs")
);
assert_eq!(
label(b"0123456789abcdef").as_deref(),
Some("0123456789abcdef")
);
assert_eq!(label(&[0u8; 16]), None);
assert_eq!(
label(b"a\xffb\0\0\0\0\0\0\0\0\0\0\0\0\0").as_deref(),
Some("a\u{fffd}b")
);
assert_eq!(
label(b"a\x1bb\0\0\0\0\0\0\0\0\0\0\0\0\0").as_deref(),
Some("a\\x1bb")
);
}
#[test]
fn printable_escapes_control_bytes_and_keeps_the_rest() {
assert_eq!(printable(b"/etc/passwd"), "/etc/passwd");
assert_eq!(
printable(b"safe\x1b[31mred\rgone"),
"safe\\x1b[31mred\\x0dgone"
);
assert_eq!(printable(b"a\0b\x7fc"), "a\\x00b\\x7fc");
assert_eq!(printable(b"a\xffb"), "a\u{fffd}b");
}
#[test]
fn a_path_renders_as_a_uri_reference_that_decodes_back_to_it() {
assert_eq!(
uri_reference(Path::new("/var/tmp/disk.img")),
"file:///var/tmp/disk.img"
);
assert_eq!(
uri_reference(Path::new("a b#c?d%e:f")),
"a%20b%23c%3Fd%25e%3Af"
);
assert_eq!(uri_reference(Path::new("café.img")), "caf%C3%A9.img");
assert_eq!(
uri_reference(Path::new("/a-b/c.d/e_f/g~h")),
"file:///a-b/c.d/e_f/g~h"
);
assert_eq!(uri_reference(Path::new("./sub/disk.img")), "./sub/disk.img");
}
#[test]
fn a_mode_reads_as_it_does_on_a_terminal() {
assert_eq!(mode(0o040755), "drwxr-xr-x");
assert_eq!(mode(0o100644), "-rw-r--r--");
assert_eq!(mode(0o120777), "lrwxrwxrwx");
assert_eq!(mode(0o020666), "crw-rw-rw-");
assert_eq!(mode(0o060660), "brw-rw----");
assert_eq!(mode(0o010600), "prw-------");
assert_eq!(mode(0o140666), "srw-rw-rw-");
assert_eq!(mode(0o104755), "-rwsr-xr-x");
assert_eq!(mode(0o104644), "-rwSr--r--");
assert_eq!(mode(0o041777), "drwxrwxrwt");
assert_eq!(mode(0o041666), "drw-rw-rwT");
}
#[test]
fn a_time_renders_as_utc_without_a_calendar_to_consult() {
assert_eq!(iso8601(0), "1970-01-01T00:00:00Z");
assert_eq!(iso8601(1_700_000_000), "2023-11-14T22:13:20Z");
assert_eq!(iso8601(951_782_400), "2000-02-29T00:00:00Z");
assert_eq!(iso8601(-1), "1969-12-31T23:59:59Z");
assert_eq!(iso8601(-2_147_483_648), "1901-12-13T20:45:52Z");
assert_eq!(iso8601(15_032_385_535), "2446-05-10T22:38:55Z");
}
#[test]
fn a_pax_time_is_the_instant_it_names() {
assert_eq!(pax_time(Timestamp::from_secs(1_700_000_000)), "1700000000");
assert_eq!(
pax_time(Timestamp {
secs: 1_700_000_000,
nanos: 123_456_789
}),
"1700000000.123456789"
);
assert_eq!(
pax_time(Timestamp {
secs: -6,
nanos: 750_000_000
}),
"-5.250000000"
);
assert_eq!(
pax_time(Timestamp {
secs: -1,
nanos: 500_000_000
}),
"-0.500000000"
);
assert_eq!(pax_time(Timestamp::from_secs(-1)), "-1");
}
}