Files
mapsize/rust/src/inventory/mod.rs
T

482 lines
15 KiB
Rust

//! The in-memory filesystem model: an append-only arena of nodes addressed
//! by stable NodeIds, with incrementally maintained directory aggregates.
//!
//! Concurrency: a Tree has exactly one writer (the scan controller or a
//! snapshot loader), shared as `Arc<RwLock<Tree>>`. Nodes are never removed,
//! so a NodeId stays valid for the Tree's lifetime.
mod category;
mod query;
mod stats;
pub use category::*;
pub use query::*;
pub use stats::*;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
/// Addresses a node within one Tree. The root is always 0.
pub type NodeId = u32;
/// The nil NodeId.
pub const NO_NODE: NodeId = u32::MAX;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[repr(u8)]
pub enum Kind {
#[default]
File,
Dir,
Symlink,
Other, // devices, sockets, fifos, reparse points
}
impl Kind {
pub fn from_u8(v: u8) -> Kind {
[Kind::File, Kind::Dir, Kind::Symlink, Kind::Other][(v & 3) as usize]
}
pub fn name(self) -> &'static str {
["File", "Directory", "Symlink", "Special"][self as usize]
}
}
/// Per-node facts (bit set).
pub type Flags = u16;
pub const FLAG_HARDLINK_DUP: Flags = 1 << 0; // additional link to an inode counted elsewhere
pub const FLAG_MOUNT_POINT: Flags = 1 << 1; // directory on a different device than its parent
pub const FLAG_VIRTUAL_FS: Flags = 1 << 2; // virtual filesystem, not descended
pub const FLAG_SKIPPED_FS: Flags = 1 << 3; // other filesystem, not descended (--one-file-system)
pub const FLAG_SPARSE: Flags = 1 << 4; // allocated < logical (sparse or compressed)
pub const FLAG_ERROR: Flags = 1 << 5; // stat or read failed
pub const FLAG_INCOMPLETE: Flags = 1 << 6; // directory contents not (fully) read
pub const FLAG_SCANNED: Flags = 1 << 7; // directory fully read
pub const FLAG_LOOP: Flags = 1 << 8; // directory already visited via another path
pub const FLAG_FOLLOWED: Flags = 1 << 9; // reached through a followed symlink
pub const FLAG_ALLOC_UNKNOWN: Flags = 1 << 10; // platform cannot report allocated size
pub const FLAG_BROKEN_LINK: Flags = 1 << 11; // symlink target does not exist
pub const FLAG_HARDLINKED: Flags = 1 << 12; // regular file with link count > 1
pub const FLAG_DELETED: Flags = 1 << 13; // moved to trash from the UI
/// One filesystem object. Sizes are own sizes; tot_* fields are aggregates
/// including the node itself and (for directories) everything beneath it.
#[derive(Clone, Debug)]
pub struct Node {
pub parent: NodeId,
pub first_child: NodeId,
pub next_sibling: NodeId,
dir: u32, // index into dir side table (directories only)
/// Single path component (raw bytes); the root holds the full root path.
pub name: Box<[u8]>,
pub size: i64, // own logical bytes
pub alloc: i64, // own allocated bytes
pub tot_size: i64, // aggregates (hard-link duplicates contribute 0)
pub tot_alloc: i64,
pub mtime: i64, // unix nanoseconds
pub files: u32, // recursive counts beneath (dirs excludes self)
pub dirs: u32,
pub errors: u32, // recursive error count beneath and including self
pub mode: u32, // Go fs.FileMode bit layout (see platform::mode)
pub uid: u32,
pub gid: u32,
pub nlink: u32,
pub ext: u16,
pub flags: Flags,
pub kind: Kind,
pub cat: Category,
}
impl Node {
pub fn is_dir(&self) -> bool {
self.kind == Kind::Dir
}
pub fn has(&self, f: Flags) -> bool {
self.flags & f != 0
}
/// The name as (lossy) UTF-8, unsanitised.
pub fn name_str(&self) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(&self.name)
}
}
/// An aggregate change applied to a node and all its ancestors.
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
pub struct Delta {
pub size: i64,
pub alloc: i64,
pub files: i64,
pub dirs: i64,
pub errors: i64,
pub cat: [i64; NUM_CATEGORIES],
}
const CHUNK_BITS: u32 = 16;
const CHUNK_SIZE: usize = 1 << CHUNK_BITS;
/// A growable array that never moves existing elements, so growth never
/// doubles peak memory.
#[derive(Clone)]
struct Chunked<T> {
c: Vec<Vec<T>>,
n: usize,
}
impl<T> Chunked<T> {
fn new() -> Self {
Chunked { c: Vec::new(), n: 0 }
}
fn add(&mut self, v: T) -> u32 {
if self.n & (CHUNK_SIZE - 1) == 0 && self.n >> CHUNK_BITS == self.c.len() {
self.c.push(Vec::with_capacity(CHUNK_SIZE));
}
self.c[self.n >> CHUNK_BITS].push(v);
self.n += 1;
(self.n - 1) as u32
}
#[inline]
fn at(&self, i: u32) -> &T {
&self.c[(i >> CHUNK_BITS) as usize][i as usize & (CHUNK_SIZE - 1)]
}
#[inline]
fn at_mut(&mut self, i: u32) -> &mut T {
&mut self.c[(i >> CHUNK_BITS) as usize][i as usize & (CHUNK_SIZE - 1)]
}
}
/// The inventory for one scan root.
#[derive(Clone)]
pub struct Tree {
nodes: Chunked<Node>,
dirs: Chunked<[i64; NUM_CATEGORIES]>,
exts: Vec<String>,
ext_idx: HashMap<String, u16>,
pub stats: Stats,
}
impl Tree {
/// Creates a tree whose root has the given full path and kind.
pub fn new(root_path: &[u8], kind: Kind) -> Tree {
let mut t = Tree {
nodes: Chunked::new(),
dirs: Chunked::new(),
exts: vec![String::new()],
ext_idx: HashMap::from([(String::new(), 0)]),
stats: Stats::default(),
};
t.stats.root = root_path.to_vec();
t.stats.start = std::time::SystemTime::now();
t.add(NO_NODE, root_path, kind);
t
}
pub fn len(&self) -> usize {
self.nodes.n
}
pub fn is_empty(&self) -> bool {
self.nodes.n == 0
}
#[inline]
pub fn node(&self, id: NodeId) -> &Node {
self.nodes.at(id)
}
#[inline]
pub fn node_mut(&mut self, id: NodeId) -> &mut Node {
self.nodes.at_mut(id)
}
pub fn root(&self) -> NodeId {
0
}
/// Appends a child node and links it under parent. It does not touch
/// aggregates; use propagate.
pub fn add(&mut self, parent: NodeId, name: &[u8], kind: Kind) -> NodeId {
let mut n = Node {
parent,
first_child: NO_NODE,
next_sibling: NO_NODE,
dir: 0,
name: name.into(),
size: 0, alloc: 0, tot_size: 0, tot_alloc: 0, mtime: 0,
files: 0, dirs: 0, errors: 0, mode: 0, uid: 0, gid: 0, nlink: 0,
ext: 0, flags: 0, kind, cat: Category::Other,
};
if kind == Kind::Dir {
n.dir = self.dirs.add([0; NUM_CATEGORIES]);
} else {
n.ext = self.intern_ext(&ext(name));
}
let id = self.nodes.add(n);
if parent != NO_NODE {
let first = self.node(parent).first_child;
self.node_mut(id).next_sibling = first;
self.node_mut(parent).first_child = id;
}
id
}
/// Interns an extension string; used by loaders rebuilding a tree.
pub fn intern_ext(&mut self, e: &str) -> u16 {
if let Some(&i) = self.ext_idx.get(e) {
return i;
}
if self.exts.len() >= (1 << 16) - 1 {
return 0;
}
self.exts.push(e.to_string());
let i = (self.exts.len() - 1) as u16;
self.ext_idx.insert(e.to_string(), i);
i
}
/// The extension string for a node ("" if none).
pub fn ext_name(&self, n: &Node) -> &str {
&self.exts[n.ext as usize]
}
pub fn ext_count(&self) -> usize {
self.exts.len()
}
pub fn ext_by_index(&self, i: usize) -> &str {
&self.exts[i]
}
/// Per-category allocated bytes beneath a directory.
pub fn cat_sizes(&self, id: NodeId) -> Option<&[i64; NUM_CATEGORIES]> {
let n = self.node(id);
(n.kind == Kind::Dir).then(|| self.dirs.at(n.dir))
}
/// Adds d to id and every ancestor. Cost is O(depth); callers batch many
/// entries into one Delta.
pub fn propagate(&mut self, mut id: NodeId, d: &Delta) {
while id != NO_NODE {
let n = self.nodes.at_mut(id);
n.tot_size += d.size;
n.tot_alloc += d.alloc;
n.files = add_u32(n.files, d.files);
n.dirs = add_u32(n.dirs, d.dirs);
n.errors = add_u32(n.errors, d.errors);
let (is_dir, dir, parent) = (n.kind == Kind::Dir, n.dir, n.parent);
if is_dir {
let cs = self.dirs.at_mut(dir);
for (c, v) in cs.iter_mut().zip(d.cat) {
*c += v;
}
}
id = parent;
}
}
/// Iterates the children of id (in reverse insertion order).
pub fn children(&self, id: NodeId) -> Children<'_> {
Children { t: self, cur: self.node(id).first_child }
}
pub fn child_count(&self, id: NodeId) -> usize {
self.children(id).count()
}
/// Reconstructs the full path of id as raw bytes.
pub fn path_bytes(&self, mut id: NodeId) -> Vec<u8> {
let mut parts: Vec<&[u8]> = Vec::new();
while id != NO_NODE {
let n = self.node(id);
parts.push(&n.name);
id = n.parent;
}
let mut out = Vec::with_capacity(parts.iter().map(|p| p.len() + 1).sum());
for p in parts.iter().rev() {
if !out.is_empty() && out.last() != Some(&b'/') {
out.push(b'/');
}
out.extend_from_slice(p);
}
out
}
/// Reconstructs the full path of id.
pub fn path(&self, id: NodeId) -> PathBuf {
PathBuf::from(OsStr::from_bytes(&self.path_bytes(id)))
}
/// The full path as (lossy) UTF-8, unsanitised.
pub fn path_string(&self, id: NodeId) -> String {
String::from_utf8_lossy(&self.path_bytes(id)).into_owned()
}
/// The chain root..id inclusive.
pub fn ancestors(&self, mut id: NodeId) -> Vec<NodeId> {
let mut out = Vec::new();
while id != NO_NODE {
out.push(id);
id = self.node(id).parent;
}
out.reverse();
out
}
/// Reports whether a is id or an ancestor of id.
pub fn is_ancestor(&self, a: NodeId, mut id: NodeId) -> bool {
while id != NO_NODE {
if id == a {
return true;
}
id = self.node(id).parent;
}
false
}
/// Number of ancestors of id.
pub fn depth(&self, id: NodeId) -> usize {
let mut d = 0;
let mut id = self.node(id).parent;
while id != NO_NODE {
d += 1;
id = self.node(id).parent;
}
d
}
/// Rebuilds all aggregates bottom-up. Relies on a parent's ID being
/// smaller than its children's, which both the scanner and the snapshot
/// loader guarantee. Used after loading a snapshot.
pub fn recompute(&mut self) {
for i in 0..self.len() as NodeId {
let n = self.nodes.at_mut(i);
n.tot_size = 0;
n.tot_alloc = 0;
n.files = 0;
n.dirs = 0;
n.errors = (n.flags & FLAG_ERROR != 0) as u32;
if n.flags & FLAG_HARDLINK_DUP == 0 {
n.tot_size = n.size;
n.tot_alloc = n.alloc;
}
if n.kind == Kind::Dir {
let d = n.dir;
*self.dirs.at_mut(d) = [0; NUM_CATEGORIES];
}
}
// Sums saturate: snapshot data is untrusted and must not wrap to
// negative totals.
for i in (1..self.len() as NodeId).rev() {
let n = self.nodes.at(i).clone_shallow();
let p = self.nodes.at_mut(n.parent);
p.tot_size = sat_add(p.tot_size, n.tot_size);
p.tot_alloc = sat_add(p.tot_alloc, n.tot_alloc);
p.errors = add_u32(p.errors, n.errors as i64);
let pdir = p.dir;
if n.kind == Kind::Dir {
p.files = add_u32(p.files, n.files as i64);
p.dirs = add_u32(p.dirs, n.dirs as i64 + 1);
let child = *self.dirs.at(n.dir);
let cs = self.dirs.at_mut(pdir);
for (c, v) in cs.iter_mut().zip(child) {
*c = sat_add(*c, v);
}
} else {
p.files = add_u32(p.files, 1);
if n.flags & FLAG_HARDLINK_DUP == 0 {
let cs = self.dirs.at_mut(pdir);
cs[n.cat as usize] = sat_add(cs[n.cat as usize], n.alloc);
}
}
}
}
}
/// The numeric fields recompute needs, without cloning the name.
struct Shallow {
parent: NodeId,
dir: u32,
tot_size: i64,
tot_alloc: i64,
alloc: i64,
files: u32,
dirs: u32,
errors: u32,
flags: Flags,
kind: Kind,
cat: Category,
}
impl Node {
fn clone_shallow(&self) -> Shallow {
Shallow {
parent: self.parent, dir: self.dir, tot_size: self.tot_size, tot_alloc: self.tot_alloc,
alloc: self.alloc, files: self.files, dirs: self.dirs, errors: self.errors,
flags: self.flags, kind: self.kind, cat: self.cat,
}
}
}
pub struct Children<'a> {
t: &'a Tree,
cur: NodeId,
}
impl<'a> Iterator for Children<'a> {
type Item = (NodeId, &'a Node);
fn next(&mut self) -> Option<Self::Item> {
if self.cur == NO_NODE {
return None;
}
let id = self.cur;
let n = self.t.node(id);
self.cur = n.next_sibling;
Some((id, n))
}
}
/// Adds non-negative i64s, saturating at the maximum.
pub fn sat_add(a: i64, b: i64) -> i64 {
if b > 0 && a > i64::MAX - b {
return i64::MAX;
}
a + b
}
fn add_u32(a: u32, d: i64) -> u32 {
(a as i64 + d).clamp(0, u32::MAX as i64) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn topk_and_recompute() {
let mut tr = Tree::new(b"/r", Kind::Dir);
let d = tr.add(0, b"d", Kind::Dir);
for i in 0..100 {
let id = tr.add(d, b"f", Kind::File);
let n = tr.node_mut(id);
n.size = i;
n.alloc = i;
}
tr.recompute();
assert_eq!(tr.node(0).tot_size, 4950);
assert_eq!((tr.node(0).files, tr.node(0).dirs), (100, 1));
let top = tr.top(0, 3, |n| n.kind == Kind::File, |n| n.size);
assert_eq!(top.len(), 3);
assert_eq!(tr.node(top[0]).size, 99);
assert_eq!(tr.node(top[2]).size, 97);
assert_eq!(tr.path_string(top[0]), "/r/d/f");
}
#[test]
fn ext_and_category() {
for (input, want) in [("a.QCOW2", "qcow2"), (".bashrc", ""), ("noext", ""), ("x.tar.gz", "gz"), ("trail.", "")] {
assert_eq!(ext(input.as_bytes()), want, "{input}");
}
assert_eq!(category_for("qcow2", false, false), Category::DiskImage);
assert_eq!(category_for("bin", true, false), Category::Executable);
assert_eq!(category_for("", true, false), Category::Cache);
}
}