Describe behaviour directly instead of by reference to the Go version
- platform::mode documents the on-disk mode-bit layout (bits 31..19 = d a l T L D p S u g c t ?, low 9 bits = permissions). - The snapshot module documents the LEB128 / zig-zag varints and the RFC 3339 JSON timestamps, and states that snapshots written by mapsize 1.x load unchanged. - Renames: GoTime -> JsonTime, go_duration -> fmt_duration, fixture testdata/go.msz -> testdata/v1.2.0.msz (a snapshot written by mapsize 1.2.0), loads_go_snapshot -> loads_1_2_snapshot. - --log / --log-level help says they are accepted and ignored.
This commit is contained in:
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
//! In-process benchmarks mirroring the Go `go test -bench` suite
|
//! In-process benchmarks of the TUI model (render, navigation, filtering)
|
||||||
//! (internal/tui/bench_test.go, internal/treemap/treemap_test.go):
|
//! and the treemap layout:
|
||||||
//!
|
//!
|
||||||
//! cargo run --release --example bench
|
//! cargo run --release --example bench
|
||||||
//!
|
//!
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ fn start_source(
|
|||||||
Ok((Some(s), t))
|
Ok((Some(s), t))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancels on SIGINT for report mode, like Go's signal.NotifyContext.
|
/// A token cancelled by SIGINT (Ctrl+C), for report mode.
|
||||||
fn interrupt_cancel() -> Cancel {
|
fn interrupt_cancel() -> Cancel {
|
||||||
static CANCEL: std::sync::OnceLock<Cancel> = std::sync::OnceLock::new();
|
static CANCEL: std::sync::OnceLock<Cancel> = std::sync::OnceLock::new();
|
||||||
extern "C" fn on_sigint(_: libc::c_int) {
|
extern "C" fn on_sigint(_: libc::c_int) {
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
//! A clonable cancellation token, the equivalent of a Go context's Done.
|
//! A clonable cancellation token: a flag to poll plus a channel that is
|
||||||
|
//! closed on cancellation, so it can also be waited on in a select.
|
||||||
|
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|||||||
+5
-1
@@ -213,7 +213,11 @@ pub fn summary(w: &mut dyn Write, t: &Tree) -> io::Result<()> {
|
|||||||
textutil::size(r.tot_alloc),
|
textutil::size(r.tot_alloc),
|
||||||
r.tot_alloc
|
r.tot_alloc
|
||||||
)?;
|
)?;
|
||||||
writeln!(w, "Elapsed {}", textutil::go_duration(st.elapsed()))?;
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Elapsed {}",
|
||||||
|
textutil::fmt_duration(st.elapsed())
|
||||||
|
)?;
|
||||||
for (k, &c) in st.err_counts.iter().enumerate() {
|
for (k, &c) in st.err_counts.iter().enumerate() {
|
||||||
if c > 0 {
|
if c > 0 {
|
||||||
writeln!(
|
writeln!(
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ fn tok(kind: TokKind, val: &str, pos: usize) -> Token {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ASCII white space as Go's unicode.IsSpace sees it.
|
/// ASCII white space: space, \t, \n, \v, \f, \r.
|
||||||
fn is_space(c: u8) -> bool {
|
fn is_space(c: u8) -> bool {
|
||||||
matches!(c, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r')
|
matches!(c, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r')
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -150,7 +150,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert!(names(&tr, "[").is_empty()); // malformed bare glob matches nothing
|
assert!(names(&tr, "[").is_empty()); // malformed bare glob matches nothing
|
||||||
assert!(names(&tr, "voilà\u{a0}x").is_empty()); // Go's lexer hangs on byte 0xA0
|
assert!(names(&tr, "voilà\u{a0}x").is_empty()); // regression: a lexer once hung on byte 0xA0 (NBSP)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ fn has_glob(s: &str) -> bool {
|
|||||||
pub(super) fn name_match(v: &str) -> Expr {
|
pub(super) fn name_match(v: &str) -> Expr {
|
||||||
let lv = v.to_lowercase();
|
let lv = v.to_lowercase();
|
||||||
if has_glob(v) {
|
if has_glob(v) {
|
||||||
// Go's filepath.Match never matches a malformed pattern.
|
// A malformed glob never matches.
|
||||||
return if glob_ok(lv.as_bytes()) {
|
return if glob_ok(lv.as_bytes()) {
|
||||||
Expr::Glob(Field::Name, lv)
|
Expr::Glob(Field::Name, lv)
|
||||||
} else {
|
} else {
|
||||||
@@ -319,8 +319,9 @@ pub(super) fn name_match(v: &str) -> Expr {
|
|||||||
Expr::Contains(Field::Name, lv)
|
Expr::Contains(Field::Name, lv)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reports whether pattern is well formed by Go's filepath.Match rules
|
/// Reports whether pattern is a well-formed glob (see
|
||||||
/// (fnmatch, which does the matching, accepts anything).
|
/// `platform::glob_match`): every `[` class is closed and non-empty, ranges
|
||||||
|
/// have both ends, and no escape is dangling.
|
||||||
fn glob_ok(p: &[u8]) -> bool {
|
fn glob_ok(p: &[u8]) -> bool {
|
||||||
// One possibly escaped character of a range; None if malformed.
|
// One possibly escaped character of a range; None if malformed.
|
||||||
let esc = |mut i: usize| -> Option<usize> {
|
let esc = |mut i: usize| -> Option<usize> {
|
||||||
@@ -461,13 +462,12 @@ pub fn parse_age(s: &str) -> Result<Duration, String> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Saturates where Go's time.Duration would overflow.
|
// Saturates beyond u64 nanoseconds (about 584 years).
|
||||||
Ok(Duration::from_nanos((v * secs * 1e9) as u64))
|
Ok(Duration::from_nanos((v * secs * 1e9) as u64))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses "2006-01-02", "2006-01-02T15:04", "2006-01-02 15:04" (local time)
|
/// Parses "2006-01-02", "2006-01-02T15:04", "2006-01-02 15:04" (local time)
|
||||||
/// or RFC 3339, as Go's time.ParseInLocation with time.Local would, into unix
|
/// or RFC 3339 (with its own offset) into unix nanoseconds.
|
||||||
/// nanoseconds.
|
|
||||||
fn parse_date(s: &str) -> Option<i64> {
|
fn parse_date(s: &str) -> Option<i64> {
|
||||||
let b = s.as_bytes();
|
let b = s.as_bytes();
|
||||||
let fixed = |i: usize, n: usize| -> Option<i64> {
|
let fixed = |i: usize, n: usize| -> Option<i64> {
|
||||||
@@ -492,7 +492,7 @@ fn parse_date(s: &str) -> Option<i64> {
|
|||||||
if sep != b'T' && sep != b' ' {
|
if sep != b'T' && sep != b' ' {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Go's "15" hour accepts one or two digits.
|
// The hour may have one or two digits.
|
||||||
let mut i = 11;
|
let mut i = 11;
|
||||||
let hl = if fixed(i, 2).is_some() { 2 } else { 1 };
|
let hl = if fixed(i, 2).is_some() { 2 } else { 1 };
|
||||||
let h = fixed(i, hl)?;
|
let h = fixed(i, hl)?;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ pub struct Node {
|
|||||||
pub files: u32, // recursive counts beneath (dirs excludes self)
|
pub files: u32, // recursive counts beneath (dirs excludes self)
|
||||||
pub dirs: u32,
|
pub dirs: u32,
|
||||||
pub errors: u32, // recursive error count beneath and including self
|
pub errors: u32, // recursive error count beneath and including self
|
||||||
pub mode: u32, // Go fs.FileMode bit layout (see platform::mode)
|
pub mode: u32, // portable mode bits (see platform::mode)
|
||||||
pub uid: u32,
|
pub uid: u32,
|
||||||
pub gid: u32,
|
pub gid: u32,
|
||||||
pub nlink: u32,
|
pub nlink: u32,
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
//! mapsize: interactive terminal disk usage analyser (Rust port).
|
//! mapsize: interactive terminal disk usage analyser.
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod brand;
|
pub mod brand;
|
||||||
|
|||||||
+4
-4
@@ -5,7 +5,7 @@ use mapsize::inventory::SizeMode;
|
|||||||
use mapsize::{brand, config, scan, snapshot, textutil};
|
use mapsize::{brand, config, scan, snapshot, textutil};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Flags taking a value (Go's flag package: `-f v`, `-f=v`, `--f v`, `--f=v`).
|
/// Flags taking a value, written `-f v`, `-f=v`, `--f v` or `--f=v`.
|
||||||
const VALUE_FLAGS: &[&str] = &[
|
const VALUE_FLAGS: &[&str] = &[
|
||||||
"workers",
|
"workers",
|
||||||
"follow-symlinks",
|
"follow-symlinks",
|
||||||
@@ -71,8 +71,8 @@ Flags:
|
|||||||
--theme NAME colour theme: default, dark, high-contrast, mono
|
--theme NAME colour theme: default, dark, high-contrast, mono
|
||||||
--color MODE colour support: auto, truecolor, 256, 16, none
|
--color MODE colour support: auto, truecolor, 256, 16, none
|
||||||
--no-mouse disable mouse support
|
--no-mouse disable mouse support
|
||||||
--log FILE write a debug log to FILE
|
--log FILE accepted for compatibility; ignored
|
||||||
--log-level LEVEL log level: error, warn, info, debug, trace
|
--log-level LEVEL accepted for compatibility (error, warn, info, debug, trace); ignored
|
||||||
--version print version and exit
|
--version print version and exit
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
@@ -171,7 +171,7 @@ fn run(args: Vec<String>) -> i32 {
|
|||||||
return fail(format!("invalid --log-level {l:?}"));
|
return fail(format!("invalid --log-level {l:?}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ponytail: --log is accepted for CLI compatibility; the Rust port has no debug logging yet.
|
// ponytail: --log and --log-level are accepted for compatibility and ignored; there is no debug logging.
|
||||||
|
|
||||||
let (depth, top, largest) = match (int("depth", -1), int("top", 0), int("largest-files", 0)) {
|
let (depth, top, largest) = match (int("depth", -1), int("top", 0), int("largest-files", 0)) {
|
||||||
(Ok(d), Ok(t), Ok(l)) => (d, t.max(0) as usize, l.max(0) as usize),
|
(Ok(d), Ok(t), Ok(l)) => (d, t.max(0) as usize, l.max(0) as usize),
|
||||||
|
|||||||
+2
-1
@@ -651,7 +651,8 @@ impl Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The OS error text without Rust's " (os error N)" suffix, matching Go.
|
/// The OS error text, lower-cased and without the " (os error N)" suffix,
|
||||||
|
/// e.g. "permission denied".
|
||||||
fn err_msg(e: &io::Error) -> String {
|
fn err_msg(e: &io::Error) -> String {
|
||||||
let s = e.to_string();
|
let s = e.to_string();
|
||||||
match s.rfind(" (os error ") {
|
match s.rfind(" (os error ") {
|
||||||
|
|||||||
+70
-56
@@ -21,10 +21,22 @@
|
|||||||
//! sha256 32 bytes over the uncompressed body
|
//! sha256 32 bytes over the uncompressed body
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! Varints are Go's encoding/binary (zig-zag for signed). Nodes are written
|
//! A uvarint is unsigned LEB128: 7 bits per byte, least significant group
|
||||||
//! in ID order; a parent always precedes its children, so a reader can
|
//! first, the high bit set on every byte but the last; at most 10 bytes for
|
||||||
//! rebuild the tree in one pass and malformed input cannot create cycles.
|
//! 64 bits. A varint (signed) is zig-zag encoded first, (v << 1) ^ (v >> 63),
|
||||||
//! Aggregates are recomputed on load, never trusted.
|
//! so small magnitudes of either sign stay short: 0, -1, 1, -2 → 0, 1, 2, 3.
|
||||||
|
//!
|
||||||
|
//! In the meta JSON, timestamps are RFC 3339 strings with nanoseconds
|
||||||
|
//! (trailing zeros trimmed) and the writer's UTC offset, or "Z" for UTC,
|
||||||
|
//! e.g. "2026-10-08T10:40:00.123456789+01:00". An unset time is
|
||||||
|
//! "0001-01-01T00:00:00Z"; null is read as unset.
|
||||||
|
//!
|
||||||
|
//! Compatibility: snapshots written by mapsize 1.x load unchanged, and the
|
||||||
|
//! mode bits (see `platform::mode`) mean the same on every OS.
|
||||||
|
//!
|
||||||
|
//! Nodes are written in ID order; a parent always precedes its children, so
|
||||||
|
//! a reader can rebuild the tree in one pass and malformed input cannot
|
||||||
|
//! create cycles. Aggregates are recomputed on load, never trusted.
|
||||||
|
|
||||||
mod compare;
|
mod compare;
|
||||||
|
|
||||||
@@ -65,9 +77,9 @@ const FLAG_GZIPPED: u16 = 1;
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
struct Meta {
|
struct Meta {
|
||||||
root: String,
|
root: String,
|
||||||
created: GoTime,
|
created: JsonTime,
|
||||||
start: GoTime,
|
start: JsonTime,
|
||||||
end: GoTime,
|
end: JsonTime,
|
||||||
complete: bool,
|
complete: bool,
|
||||||
cancelled: bool,
|
cancelled: bool,
|
||||||
files: i64,
|
files: i64,
|
||||||
@@ -131,7 +143,7 @@ impl<W: Write> Writer<W> {
|
|||||||
self.bytes(&b[..=i])
|
self.bytes(&b[..=i])
|
||||||
}
|
}
|
||||||
fn varint(&mut self, v: i64) -> io::Result<()> {
|
fn varint(&mut self, v: i64) -> io::Result<()> {
|
||||||
self.uvarint(((v << 1) ^ (v >> 63)) as u64) // zig-zag, as encoding/binary.PutVarint
|
self.uvarint(((v << 1) ^ (v >> 63)) as u64) // zig-zag
|
||||||
}
|
}
|
||||||
fn str(&mut self, s: &[u8]) -> io::Result<()> {
|
fn str(&mut self, s: &[u8]) -> io::Result<()> {
|
||||||
self.uvarint(s.len() as u64)?;
|
self.uvarint(s.len() as u64)?;
|
||||||
@@ -154,11 +166,11 @@ pub fn save(out: &mut dyn Write, t: &Tree) -> io::Result<()> {
|
|||||||
let st = &t.stats;
|
let st = &t.stats;
|
||||||
let m = Meta {
|
let m = Meta {
|
||||||
root: String::from_utf8_lossy(&st.root).into_owned(),
|
root: String::from_utf8_lossy(&st.root).into_owned(),
|
||||||
created: GoTime::from_system(SystemTime::now()),
|
created: JsonTime::from_system(SystemTime::now()),
|
||||||
start: GoTime::from_system(st.start).local(),
|
start: JsonTime::from_system(st.start).local(),
|
||||||
end: st
|
end: st
|
||||||
.end
|
.end
|
||||||
.map(|e| GoTime::from_system(e).local())
|
.map(|e| JsonTime::from_system(e).local())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
complete: st.complete,
|
complete: st.complete,
|
||||||
cancelled: st.cancelled,
|
cancelled: st.cancelled,
|
||||||
@@ -233,7 +245,8 @@ pub fn save_file(path: &Path, t: &Tree) -> io::Result<()> {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new 0600 file named .mapsize-*.tmp in dir (like os.CreateTemp).
|
/// Creates a new private (0600 on Unix) file named .mapsize-<random>.tmp in
|
||||||
|
/// dir, retrying on name collisions.
|
||||||
fn create_temp(dir: &Path) -> io::Result<(PathBuf, File)> {
|
fn create_temp(dir: &Path) -> io::Result<(PathBuf, File)> {
|
||||||
let seed = SystemTime::now()
|
let seed = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -303,7 +316,8 @@ impl Body {
|
|||||||
Ok(self.buf[self.pos - 1])
|
Ok(self.buf[self.pos - 1])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fills out completely (errors like io.ReadFull).
|
/// Fills out completely: "EOF" if the stream ends before the first byte,
|
||||||
|
/// "unexpected EOF" if it ends part way.
|
||||||
fn read_full(&mut self, out: &mut [u8]) -> Result<(), String> {
|
fn read_full(&mut self, out: &mut [u8]) -> Result<(), String> {
|
||||||
let mut done = 0;
|
let mut done = 0;
|
||||||
while done < out.len() {
|
while done < out.len() {
|
||||||
@@ -332,7 +346,7 @@ impl Body {
|
|||||||
|
|
||||||
fn varint(&mut self, what: &str) -> Result<i64, String> {
|
fn varint(&mut self, what: &str) -> Result<i64, String> {
|
||||||
let ux = read_uvarint(self).map_err(|e| format!("snapshot: bad {what}: {e}"))?;
|
let ux = read_uvarint(self).map_err(|e| format!("snapshot: bad {what}: {e}"))?;
|
||||||
let v = (ux >> 1) as i64; // zig-zag, as encoding/binary.PutVarint
|
let v = (ux >> 1) as i64; // undo zig-zag
|
||||||
Ok(if ux & 1 != 0 { !v } else { v })
|
Ok(if ux & 1 != 0 { !v } else { v })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +363,7 @@ impl Body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// encoding/binary.ReadUvarint over the body.
|
/// Reads a uvarint (LEB128) from the body; more than 64 bits is an error.
|
||||||
fn read_uvarint(b: &mut Body) -> Result<u64, String> {
|
fn read_uvarint(b: &mut Body) -> Result<u64, String> {
|
||||||
let (mut x, mut s) = (0u64, 0u32);
|
let (mut x, mut s) = (0u64, 0u32);
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
@@ -611,32 +625,32 @@ fn valid_name(s: &[u8]) -> bool {
|
|||||||
!s.is_empty() && s != b"." && s != b".." && !s.iter().any(|&c| c == b'/' || c == 0)
|
!s.is_empty() && s != b"." && s != b".." && !s.iter().any(|&c| c == b'/' || c == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Go time.Time as JSON carries it: unix seconds plus nanoseconds, and
|
/// A timestamp as the meta JSON carries it (see the module docs): unix
|
||||||
/// the UTC offset it is written with. The default is Go's zero time
|
/// seconds plus nanoseconds, and the UTC offset it is written with. The
|
||||||
/// (0001-01-01T00:00:00Z).
|
/// default is the unset time, 0001-01-01T00:00:00Z.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
struct GoTime {
|
struct JsonTime {
|
||||||
secs: i64,
|
secs: i64,
|
||||||
nanos: u32,
|
nanos: u32,
|
||||||
off: i32, // seconds east of UTC; presentation only
|
off: i32, // seconds east of UTC; presentation only
|
||||||
}
|
}
|
||||||
|
|
||||||
const GO_ZERO_SECS: i64 = -62_135_596_800;
|
const ZERO_SECS: i64 = -62_135_596_800;
|
||||||
|
|
||||||
impl Default for GoTime {
|
impl Default for JsonTime {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
GoTime {
|
JsonTime {
|
||||||
secs: GO_ZERO_SECS,
|
secs: ZERO_SECS,
|
||||||
nanos: 0,
|
nanos: 0,
|
||||||
off: 0,
|
off: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GoTime {
|
impl JsonTime {
|
||||||
fn from_system(t: SystemTime) -> GoTime {
|
fn from_system(t: SystemTime) -> JsonTime {
|
||||||
match t.duration_since(UNIX_EPOCH) {
|
match t.duration_since(UNIX_EPOCH) {
|
||||||
Ok(d) => GoTime {
|
Ok(d) => JsonTime {
|
||||||
secs: d.as_secs() as i64,
|
secs: d.as_secs() as i64,
|
||||||
nanos: d.subsec_nanos(),
|
nanos: d.subsec_nanos(),
|
||||||
off: 0,
|
off: 0,
|
||||||
@@ -649,7 +663,7 @@ impl GoTime {
|
|||||||
} else {
|
} else {
|
||||||
(-s - 1, 1_000_000_000 - n)
|
(-s - 1, 1_000_000_000 - n)
|
||||||
};
|
};
|
||||||
GoTime {
|
JsonTime {
|
||||||
secs,
|
secs,
|
||||||
nanos,
|
nanos,
|
||||||
off: 0,
|
off: 0,
|
||||||
@@ -658,14 +672,14 @@ impl GoTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The same instant in the local time zone (as Go's time.Now()).
|
/// The same instant, presented in the local time zone.
|
||||||
fn local(self) -> GoTime {
|
fn local(self) -> JsonTime {
|
||||||
let off = crate::platform::local_time(self.secs).off;
|
let off = crate::platform::local_time(self.secs).off;
|
||||||
GoTime { off, ..self }
|
JsonTime { off, ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_zero(self) -> bool {
|
fn is_zero(self) -> bool {
|
||||||
(self.secs, self.nanos) == (GO_ZERO_SECS, 0)
|
(self.secs, self.nanos) == (ZERO_SECS, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_system(self) -> SystemTime {
|
fn to_system(self) -> SystemTime {
|
||||||
@@ -677,7 +691,7 @@ impl GoTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RFC 3339 with trimmed nanoseconds, like Go's MarshalJSON.
|
/// RFC 3339 with nanoseconds (trailing zeros trimmed) and "Z" for UTC.
|
||||||
fn format(self) -> String {
|
fn format(self) -> String {
|
||||||
let secs = self.secs + self.off as i64;
|
let secs = self.secs + self.off as i64;
|
||||||
let (y, mo, d) = civil_from_days(secs.div_euclid(86400));
|
let (y, mo, d) = civil_from_days(secs.div_euclid(86400));
|
||||||
@@ -704,7 +718,7 @@ impl GoTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parses RFC 3339 ("2026-10-08T10:40:00.123456789+01:00").
|
/// Parses RFC 3339 ("2026-10-08T10:40:00.123456789+01:00").
|
||||||
fn parse(s: &str) -> Option<GoTime> {
|
fn parse(s: &str) -> Option<JsonTime> {
|
||||||
let b = s.as_bytes();
|
let b = s.as_bytes();
|
||||||
let num = |r: std::ops::Range<usize>| -> Option<i64> {
|
let num = |r: std::ops::Range<usize>| -> Option<i64> {
|
||||||
let p = b.get(r)?;
|
let p = b.get(r)?;
|
||||||
@@ -762,7 +776,7 @@ impl GoTime {
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let secs = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se - off;
|
let secs = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se - off;
|
||||||
Some(GoTime {
|
Some(JsonTime {
|
||||||
secs,
|
secs,
|
||||||
nanos,
|
nanos,
|
||||||
off: off as i32,
|
off: off as i32,
|
||||||
@@ -779,18 +793,18 @@ impl GoTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for GoTime {
|
impl Serialize for JsonTime {
|
||||||
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||||
s.serialize_str(&self.format())
|
s.serialize_str(&self.format())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for GoTime {
|
impl<'de> Deserialize<'de> for JsonTime {
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||||
// null leaves the zero time, as in Go.
|
// null leaves the time unset.
|
||||||
match Option::<String>::deserialize(d)? {
|
match Option::<String>::deserialize(d)? {
|
||||||
None => Ok(GoTime::default()),
|
None => Ok(JsonTime::default()),
|
||||||
Some(s) => GoTime::parse(&s)
|
Some(s) => JsonTime::parse(&s)
|
||||||
.ok_or_else(|| serde::de::Error::custom(format!("parsing time {s:?}"))),
|
.ok_or_else(|| serde::de::Error::custom(format!("parsing time {s:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -914,7 +928,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_uvarint_matches_go() {
|
fn varint_encoding() {
|
||||||
for v in [0, 1, 127, 128, 300, 1 << 35, u64::MAX] {
|
for v in [0, 1, 127, 128, 300, 1 << 35, u64::MAX] {
|
||||||
assert_eq!(read_uvarint(&mut body(&put_uvarint(v))), Ok(v));
|
assert_eq!(read_uvarint(&mut body(&put_uvarint(v))), Ok(v));
|
||||||
}
|
}
|
||||||
@@ -923,7 +937,7 @@ mod tests {
|
|||||||
read_uvarint(&mut body(&[0xff; 11])).is_err(),
|
read_uvarint(&mut body(&[0xff; 11])).is_err(),
|
||||||
"overflow not detected"
|
"overflow not detected"
|
||||||
);
|
);
|
||||||
// Zig-zag as Go's PutVarint.
|
// Zig-zag.
|
||||||
for (v, enc) in [(0i64, 0u64), (-1, 1), (1, 2), (-2, 3), (i64::MIN, u64::MAX)] {
|
for (v, enc) in [(0i64, 0u64), (-1, 1), (1, 2), (-2, 3), (i64::MIN, u64::MAX)] {
|
||||||
assert_eq!(((v << 1) ^ (v >> 63)) as u64, enc);
|
assert_eq!(((v << 1) ^ (v >> 63)) as u64, enc);
|
||||||
assert_eq!(body(&put_uvarint(enc)).varint("v"), Ok(v));
|
assert_eq!(body(&put_uvarint(enc)).varint("v"), Ok(v));
|
||||||
@@ -931,16 +945,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn go_time_json() {
|
fn json_time() {
|
||||||
let t = GoTime::parse("2026-10-08T10:40:00.123456789+01:00").unwrap();
|
let t = JsonTime::parse("2026-10-08T10:40:00.123456789+01:00").unwrap();
|
||||||
assert_eq!((t.secs, t.nanos, t.off), (1_791_452_400, 123_456_789, 3600));
|
assert_eq!((t.secs, t.nanos, t.off), (1_791_452_400, 123_456_789, 3600));
|
||||||
assert_eq!(t.format(), "2026-10-08T10:40:00.123456789+01:00");
|
assert_eq!(t.format(), "2026-10-08T10:40:00.123456789+01:00");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GoTime { off: 0, ..t }.format(),
|
JsonTime { off: 0, ..t }.format(),
|
||||||
"2026-10-08T09:40:00.123456789Z"
|
"2026-10-08T09:40:00.123456789Z"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GoTime {
|
JsonTime {
|
||||||
off: -5 * 3600 - 1800,
|
off: -5 * 3600 - 1800,
|
||||||
..t
|
..t
|
||||||
}
|
}
|
||||||
@@ -948,17 +962,17 @@ mod tests {
|
|||||||
"2026-10-08T04:10:00.123456789-05:30"
|
"2026-10-08T04:10:00.123456789-05:30"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GoTime::parse("2026-10-08T09:40:00.1Z").unwrap().format(),
|
JsonTime::parse("2026-10-08T09:40:00.1Z").unwrap().format(),
|
||||||
"2026-10-08T09:40:00.1Z"
|
"2026-10-08T09:40:00.1Z"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GoTime::parse("0001-01-01T00:00:00Z"),
|
JsonTime::parse("0001-01-01T00:00:00Z"),
|
||||||
Some(GoTime::default())
|
Some(JsonTime::default())
|
||||||
);
|
);
|
||||||
assert_eq!(GoTime::default().format(), "0001-01-01T00:00:00Z");
|
assert_eq!(JsonTime::default().format(), "0001-01-01T00:00:00Z");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GoTime::from_system(GoTime::default().to_system()),
|
JsonTime::from_system(JsonTime::default().to_system()),
|
||||||
GoTime::default()
|
JsonTime::default()
|
||||||
);
|
);
|
||||||
for bad in [
|
for bad in [
|
||||||
"",
|
"",
|
||||||
@@ -967,17 +981,17 @@ mod tests {
|
|||||||
"2026-10-08T00:00:00",
|
"2026-10-08T00:00:00",
|
||||||
"2026-10-08T00:00:00.Z",
|
"2026-10-08T00:00:00.Z",
|
||||||
] {
|
] {
|
||||||
assert_eq!(GoTime::parse(bad), None, "{bad}");
|
assert_eq!(JsonTime::parse(bad), None, "{bad}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A tiny tree saved by the Go binary
|
/// A snapshot written by mapsize 1.2.0 of a tiny tree
|
||||||
/// (tree/{a/x.iso 10000 B, a/b/note.txt, c/y.log 3000 B, c/link -> ../a}).
|
/// (tree/{a/x.iso 10000 B, a/b/note.txt, c/y.log 3000 B, c/link -> ../a}).
|
||||||
#[test]
|
#[test]
|
||||||
fn loads_go_snapshot() {
|
fn loads_1_2_snapshot() {
|
||||||
let t = load_file(Path::new(concat!(
|
let t = load_file(Path::new(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
"/src/snapshot/testdata/go.msz"
|
"/src/snapshot/testdata/v1.2.0.msz"
|
||||||
)))
|
)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let r = t.node(0);
|
let r = t.node(0);
|
||||||
@@ -997,7 +1011,7 @@ mod tests {
|
|||||||
assert_eq!(t.node(iso).size, 10000);
|
assert_eq!(t.node(iso).size, 10000);
|
||||||
assert_eq!(t.ext_name(t.node(iso)), "iso");
|
assert_eq!(t.ext_name(t.node(iso)), "iso");
|
||||||
assert!(t.path_string(iso).ends_with("/tree/a/x.iso"));
|
assert!(t.path_string(iso).ends_with("/tree/a/x.iso"));
|
||||||
// And it survives a Rust round trip unchanged.
|
// And it survives a save/load round trip unchanged.
|
||||||
let again = load(&saved(&t)[..]).unwrap();
|
let again = load(&saved(&t)[..]).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(again.node(0).tot_size, again.len(), again.stats.start),
|
(again.node(0).tot_size, again.len(), again.stats.start),
|
||||||
|
|||||||
+6
-6
@@ -357,9 +357,9 @@ pub fn rfc3339_utc(unix_nanos: i64) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a duration like Go's time.Duration.String after rounding to
|
/// Formats a duration rounded to milliseconds: "0s", "250ms", "1.234s",
|
||||||
/// milliseconds, e.g. "1.234s", "250ms", "2m3.5s".
|
/// "2m3.5s", "1h0m5s" (trailing zeros of the seconds trimmed).
|
||||||
pub fn go_duration(d: Duration) -> String {
|
pub fn fmt_duration(d: Duration) -> String {
|
||||||
let ms = (d.as_nanos() + 500_000) / 1_000_000;
|
let ms = (d.as_nanos() + 500_000) / 1_000_000;
|
||||||
if ms == 0 {
|
if ms == 0 {
|
||||||
return "0s".into();
|
return "0s".into();
|
||||||
@@ -393,8 +393,8 @@ mod time_tests {
|
|||||||
rfc3339_utc(1_791_456_000 * 1_000_000_000),
|
rfc3339_utc(1_791_456_000 * 1_000_000_000),
|
||||||
"2026-10-08T10:40:00Z"
|
"2026-10-08T10:40:00Z"
|
||||||
);
|
);
|
||||||
assert_eq!(go_duration(Duration::from_micros(1_234_400)), "1.234s");
|
assert_eq!(fmt_duration(Duration::from_micros(1_234_400)), "1.234s");
|
||||||
assert_eq!(go_duration(Duration::from_millis(250)), "250ms");
|
assert_eq!(fmt_duration(Duration::from_millis(250)), "250ms");
|
||||||
assert_eq!(go_duration(Duration::from_millis(123_500)), "2m3.5s");
|
assert_eq!(fmt_duration(Duration::from_millis(123_500)), "2m3.5s");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -326,7 +326,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Go fuzz target's property, run over fixed seeds.
|
/// Squarify invariants over random partitions, from fixed seeds.
|
||||||
#[test]
|
#[test]
|
||||||
fn squarify_random_partitions() {
|
fn squarify_random_partitions() {
|
||||||
let mut rng = Rng::new(1);
|
let mut rng = Rng::new(1);
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
//! Benchmark hooks: the model driven with no terminal, rendering in
|
//! Benchmark hooks: the model driven with no terminal, rendering in
|
||||||
//! true colour, as the Go benchmarks do.
|
//! true colour.
|
||||||
|
|
||||||
use super::model::{Key, Model, Msg, Options, Profile};
|
use super::model::{Key, Model, Msg, Options, Profile};
|
||||||
use crate::cancel::Cancel;
|
use crate::cancel::Cancel;
|
||||||
|
|||||||
+2
-2
@@ -58,7 +58,7 @@ pub(crate) enum Profile {
|
|||||||
Ascii,
|
Ascii,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A key press. name follows Bubble Tea's keystroke names ("a", "G",
|
/// A key press. name is the keystroke name ("a", "G",
|
||||||
/// "ctrl+c", "shift+tab", "space", "pgdown", …); text is the inserted
|
/// "ctrl+c", "shift+tab", "space", "pgdown", …); text is the inserted
|
||||||
/// text for printable keys.
|
/// text for printable keys.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@@ -69,7 +69,7 @@ pub(crate) struct Key {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Key {
|
||||||
/// A key from its keystroke name, as the Go tests build them: single
|
/// A key from its keystroke name, as tests build them: single
|
||||||
/// characters insert themselves, "space" inserts " ".
|
/// characters insert themselves, "space" inserts " ".
|
||||||
pub fn from_name(s: &str) -> Key {
|
pub fn from_name(s: &str) -> Key {
|
||||||
let text = match s {
|
let text = match s {
|
||||||
|
|||||||
+2
-2
@@ -162,7 +162,7 @@ fn enter_terminal(mouse: bool) -> io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bubble Tea keystroke name for a key event.
|
/// The keystroke name (see `Key`) for a key event.
|
||||||
fn convert_key(ev: KeyEvent) -> Option<Key> {
|
fn convert_key(ev: KeyEvent) -> Option<Key> {
|
||||||
if ev.kind == KeyEventKind::Release {
|
if ev.kind == KeyEventKind::Release {
|
||||||
return None;
|
return None;
|
||||||
@@ -251,7 +251,7 @@ fn convert(ev: Event) -> Option<Msg> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Pastes are ignored, as in the Go version.
|
// Pastes are ignored.
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,4 +1,5 @@
|
|||||||
//! Headless model tests (ports of tui_test.go, nest_test.go, live_test.go).
|
//! Headless model tests: rendering and navigation, nested directories,
|
||||||
|
//! and a model driven by a live scan.
|
||||||
|
|
||||||
use super::bench::opts_for;
|
use super::bench::opts_for;
|
||||||
use super::canvas::rgb;
|
use super::canvas::rgb;
|
||||||
@@ -536,7 +537,7 @@ fn copy_refuses_control_characters() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- nest_test.go -----------------------------------------------------------
|
// ---- Nested directories -----------------------------------------------------
|
||||||
|
|
||||||
fn root_tree() -> Tree {
|
fn root_tree() -> Tree {
|
||||||
let mut t = Tree::new(b"/", Kind::Dir);
|
let mut t = Tree::new(b"/", Kind::Dir);
|
||||||
@@ -611,7 +612,7 @@ fn reveal_headless_zooms_map() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- live_test.go -----------------------------------------------------------
|
// ---- Live scan --------------------------------------------------------------
|
||||||
|
|
||||||
/// Renders, navigates, filters and resizes while the scanner mutates the
|
/// Renders, navigates, filters and resizes while the scanner mutates the
|
||||||
/// inventory, verifying the locking discipline.
|
/// inventory, verifying the locking discipline.
|
||||||
|
|||||||
Reference in New Issue
Block a user