From 5dc645c1c54e9ac632349d187a396fcaffc72906 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 1 Jan 2026 22:20:31 +0100 Subject: [PATCH 1/4] astr: Use ThinArc --- crates/astr/src/lib.rs | 81 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/crates/astr/src/lib.rs b/crates/astr/src/lib.rs index 5df57a9a7..5831619fb 100644 --- a/crates/astr/src/lib.rs +++ b/crates/astr/src/lib.rs @@ -1,22 +1,28 @@ use std::{ borrow::{Borrow, Cow}, fmt, + hash::Hash, ops::Deref, path::Path, }; +use triomphe::{Arc, HeaderWithLength}; + mod diesel; /// String 'atom'. /// /// Cloning doesn't allocate. As of the time of writing, uses reference /// counting. Implementation may change. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct AStr(triomphe::Arc); +#[derive(Clone)] +pub struct AStr(triomphe::ThinArc<(), u8>); impl AStr { + #[inline] pub fn as_str(&self) -> &str { - &self.0 + // SAFETY: We only ever store UTF-8, + // would use ThinArc<(), str> if possible + unsafe { str::from_utf8_unchecked(&self.0.slice) } } } @@ -29,33 +35,37 @@ impl Default for AStr { impl Deref for AStr { type Target = str; + #[inline] fn deref(&self) -> &Self::Target { - &self.0 + self.as_str() } } impl Borrow for AStr { + #[inline] fn borrow(&self) -> &str { - &self.0 + self.as_str() } } impl fmt::Debug for AStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.as_str().fmt(f) } } impl fmt::Display for AStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.as_str().fmt(f) } } impl From<&str> for AStr { - #[inline] fn from(value: &str) -> Self { - Self(value.into()) + Self(Arc::into_thin(Arc::from_header_and_slice( + HeaderWithLength::new((), value.len()), + value.as_bytes(), + ))) } } @@ -69,7 +79,7 @@ impl From<&AStr> for AStr { impl From for AStr { #[inline] fn from(value: String) -> Self { - Self(value.into()) + Self::from(value.as_str()) } } @@ -88,13 +98,62 @@ impl<'a> From<&'a AStr> for Cow<'a, str> { } impl AsRef for AStr { + #[inline] fn as_ref(&self) -> &str { - &self.0 + self.as_str() } } impl AsRef for AStr { + #[inline] fn as_ref(&self) -> &Path { self.as_str().as_ref() } } + +impl PartialEq for AStr { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} + +impl Eq for AStr {} + +impl PartialOrd for AStr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AStr { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl Hash for AStr { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::AStr; + + #[test] + fn basic_use() { + let empty = AStr::from(""); + let empty2 = empty.clone(); + assert_eq!(format!("{empty}{empty2}{empty}"), ""); + + let x = AStr::from("x"); + assert_eq!(format!("{x}x{x}"), "xxx"); + } + + #[test] + fn long_string() { + let foo = AStr::from("/foo/bar/helloworld"); + assert_eq!(foo.as_str(), "/foo/bar/helloworld"); + } +} From 35eade32b0cbb7e7efe7f514cb6df96f696126fb Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 1 Jan 2026 01:45:41 +0100 Subject: [PATCH 2/4] nice --- Cargo.lock | 16 +++++++-- crates/astr/Cargo.toml | 1 + crates/astr/src/lib.rs | 3 ++ crates/vfs/Cargo.toml | 2 ++ crates/vfs/src/path.rs | 59 ++++++++++++++++++++++++++++++++-- crates/vfs/src/tree/builder.rs | 24 ++++++++------ crates/vfs/src/tree/mod.rs | 32 ++++++++++-------- 7 files changed, 108 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8871f28b3..2b63c9321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,6 +129,7 @@ name = "astr" version = "0.25.6" dependencies = [ "diesel", + "stable_deref_trait", "triomphe", ] @@ -948,6 +949,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55dd888a213fc57e957abf2aa305ee3e8a28dbe05687a251f33b637cd46b0070" +[[package]] +name = "elsa" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9abf33c656a7256451ebb7d0082c5a471820c31269e49d807c538c252352186e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2791,9 +2801,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -3407,6 +3417,8 @@ name = "vfs" version = "0.1.0" dependencies = [ "astr", + "derive_more", + "elsa", "indextree", "snafu", ] diff --git a/crates/astr/Cargo.toml b/crates/astr/Cargo.toml index 3c34ec7bc..c05de94d1 100644 --- a/crates/astr/Cargo.toml +++ b/crates/astr/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true [dependencies] diesel.workspace = true +stable_deref_trait = "1.2.1" triomphe.workspace = true [lints] diff --git a/crates/astr/src/lib.rs b/crates/astr/src/lib.rs index 5831619fb..4c52e6eae 100644 --- a/crates/astr/src/lib.rs +++ b/crates/astr/src/lib.rs @@ -6,6 +6,7 @@ use std::{ path::Path, }; +use stable_deref_trait::StableDeref; use triomphe::{Arc, HeaderWithLength}; mod diesel; @@ -41,6 +42,8 @@ impl Deref for AStr { } } +unsafe impl StableDeref for AStr {} + impl Borrow for AStr { #[inline] fn borrow(&self) -> &str { diff --git a/crates/vfs/Cargo.toml b/crates/vfs/Cargo.toml index 83c62a621..d76f3f886 100644 --- a/crates/vfs/Cargo.toml +++ b/crates/vfs/Cargo.toml @@ -7,6 +7,8 @@ edition.workspace = true [dependencies] astr.workspace = true +derive_more.workspace = true +elsa = "1.11.2" indextree.workspace = true snafu.workspace = true diff --git a/crates/vfs/src/path.rs b/crates/vfs/src/path.rs index b091bde0b..e4a499f7a 100644 --- a/crates/vfs/src/path.rs +++ b/crates/vfs/src/path.rs @@ -1,4 +1,7 @@ +use std::ops::Deref; + use astr::AStr; +use derive_more::Debug; pub fn join(a: &str, b: impl AsRef + Into) -> AStr { let b_ = b.as_ref(); @@ -11,8 +14,58 @@ pub fn join(a: &str, b: impl AsRef + Into) -> AStr { } } -pub fn file_name(path: &str) -> Option<&str> { - path.trim_end_matches('/').rsplit('/').next() +#[derive(Clone, Debug)] +#[debug("{path:?}")] +pub struct VfsPath { + path: AStr, + file_name_start_idx: u32, + parent_end_idx: u32, +} + +impl VfsPath { + pub fn new(path: AStr) -> Self { + assert!(path.starts_with('/')); + if path.len() > 1 { + assert!(!path.ends_with('/')); + } + + let file_name_start_idx = (path.rfind('/').unwrap() + 1).try_into().unwrap(); + let parent_end_idx = if file_name_start_idx == 1 { + 1 + } else { + file_name_start_idx - 1 + }; + Self { + path, + file_name_start_idx, + parent_end_idx, + } + } + + pub fn astr(&self) -> AStr { + self.path.clone() + } + + pub fn file_name(&self) -> &str { + &self.path[self.file_name_start_idx as usize..] + } + + pub fn parent(&self) -> Option<&str> { + (self.path.len() > 1).then(|| &self.path[..self.parent_end_idx as usize]) + } +} + +impl Deref for VfsPath { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.path + } +} + +/*pub fn file_name(path: &str) -> Option<&str> { + let (_, file_name) = path.trim_end_matches('/').rsplit_once('/')?; + Some(file_name) } pub fn parent(path: &str) -> Option<&str> { @@ -20,7 +73,7 @@ pub fn parent(path: &str) -> Option<&str> { // We had to have split on a direct descendent of `/` if parent.is_empty() { "/" } else { parent } }) -} +}*/ pub fn components(path: &str) -> impl Iterator { path.starts_with('/') diff --git a/crates/vfs/src/tree/builder.rs b/crates/vfs/src/tree/builder.rs index aa742948c..e3363eec9 100644 --- a/crates/vfs/src/tree/builder.rs +++ b/crates/vfs/src/tree/builder.rs @@ -6,6 +6,7 @@ use std::collections::BTreeMap; use astr::AStr; +use elsa::FrozenVec; use crate::path; use crate::tree::{Kind, Tree}; @@ -51,7 +52,7 @@ impl TreeBuilder { let file = File::new(item); // Find all parent paths - if let Some(parent) = &file.parent { + if let Some(parent) = file.parent() { let mut leading_path: Option = None; // Build a set of parent paths skipping `/`, yielding `usr`, `usr/bin`, etc. for component in path::components(parent) { @@ -64,6 +65,7 @@ impl TreeBuilder { .insert(full_path.clone(), File::new(full_path.into())); } } + self.explicit.push(file); } @@ -73,7 +75,7 @@ impl TreeBuilder { // Walk again to remove accidental dupes for i in self.explicit.iter() { - self.implicit_dirs.remove(&i.path); + self.implicit_dirs.remove(&*i.path); } } @@ -85,10 +87,11 @@ impl TreeBuilder { .iter() .filter(|f| matches!(f.kind, Kind::Directory)) .chain(self.implicit_dirs.values()) - .map(|d| (&d.path, d)) + .map(|d| (&*d.path, d)) .collect::>(); // build a set of redirects + let scratch = FrozenVec::new(); let mut redirects = BTreeMap::new(); // Resolve symlinks-to-dirs @@ -96,14 +99,15 @@ impl TreeBuilder { if let Kind::Symlink(target) = &link.kind { // Resolve the link. let target = if target.starts_with('/') { - target.clone() - } else if let Some(parent) = &link.parent { - path::join(parent, target) + &**target + } else if let Some(parent) = link.parent() { + scratch.push(path::join(parent, target)); + scratch.last().unwrap() } else { - target.clone() + &**target }; if all_dirs.contains_key(&target) { - redirects.insert(&link.path, target); + redirects.insert(&*link.path, target); } } } @@ -123,14 +127,14 @@ impl TreeBuilder { // New node for this guy let node = tree.new_node(entry.clone()); - if let Some(parent) = &entry.parent { + if let Some(parent) = entry.parent() { tree.add_child_to_node(node, parent)?; } } // Reparent any symlink redirects. for (source_tree, target_tree) in redirects { - tree.reparent(source_tree, &target_tree)?; + tree.reparent(source_tree, target_tree)?; } Ok(tree) } diff --git a/crates/vfs/src/tree/mod.rs b/crates/vfs/src/tree/mod.rs index 01e824ab1..66631e478 100644 --- a/crates/vfs/src/tree/mod.rs +++ b/crates/vfs/src/tree/mod.rs @@ -12,7 +12,7 @@ use astr::AStr; use indextree::{Arena, Descendants, NodeId}; use snafu::Snafu; -use crate::path; +use crate::path::{self, VfsPath}; pub mod builder; @@ -44,28 +44,32 @@ pub trait BlitFile: Clone + Sized + Debug + From { #[derive(Debug, Clone)] struct File { id: AStr, - path: AStr, - file_name: Option, - parent: Option, + path: VfsPath, kind: Kind, inner: T, } impl File { pub fn new(inner: T) -> Self { - let path = inner.path(); - let file_name = path::file_name(&path).map(AStr::from); - let parent = path::parent(&path).map(AStr::from); + let path = VfsPath::new(inner.path()); Self { id: inner.id(), path, - file_name, - parent, kind: inner.kind(), inner, } } + + #[inline] + fn file_name(&self) -> &str { + self.path.file_name() + } + + #[inline] + fn parent(&self) -> Option<&str> { + self.path.parent() + } } /// Actual tree implementation, encapsulating indextree @@ -98,7 +102,7 @@ impl Tree { /// Generate a new node, store the path mapping for it fn new_node(&mut self, data: File) -> NodeId { - let path = data.path.clone(); + let path = data.path.astr(); let node = self.arena.new_node(data); self.map.insert(path, node); self.length += 1; @@ -123,7 +127,7 @@ impl Tree { .children(&self.arena) .filter_map(|n| { let n = self.arena.get(n)?.get(); - if n.file_name == node.get().file_name { + if n.file_name() == node.get().file_name() { Some(n) } else { None @@ -132,7 +136,7 @@ impl Tree { .collect::>(); if !others.is_empty() { let e = Error::Duplicate { - node_path: node.get().path.clone(), + node_path: node.get().path.astr(), node_id: node.get().id.clone(), other_id: others.first().unwrap().id.clone(), }; @@ -186,7 +190,7 @@ impl Tree { Some(n) => *n, None => self.new_node(orphan.clone()), }; - if let Some(parent) = &orphan.parent { + if let Some(parent) = orphan.parent() { self.add_child_to_node(node, parent)?; } } @@ -211,7 +215,7 @@ impl Tree { fn structured_children(&self, start: &NodeId) -> Element<'_, T> { let node = &self.arena[*start]; let item = node.get(); - let partial = item.file_name.as_deref().unwrap_or_default(); + let partial = item.file_name(); match item.kind { Kind::Directory => { From 9c17a44fb5be4902db3bad697cc404e99995f8b4 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 1 Jan 2026 02:36:25 +0100 Subject: [PATCH 3/4] Aggressively optimize File size --- Cargo.lock | 1 + crates/vfs/src/tree/builder.rs | 34 +++++++++++--------- crates/vfs/src/tree/mod.rs | 59 +++++++++++++++++++++++++--------- moss/Cargo.toml | 1 + moss/src/cli/info.rs | 4 +-- moss/src/cli/state.rs | 16 +++++++++ moss/src/client/mod.rs | 9 +++--- 7 files changed, 88 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b63c9321..c4ebc46b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,6 +1886,7 @@ dependencies = [ "derive_more", "diesel", "diesel_migrations", + "elsa", "fnmatch", "fs-err", "futures-util", diff --git a/crates/vfs/src/tree/builder.rs b/crates/vfs/src/tree/builder.rs index e3363eec9..353fbdba0 100644 --- a/crates/vfs/src/tree/builder.rs +++ b/crates/vfs/src/tree/builder.rs @@ -20,6 +20,8 @@ pub struct TreeBuilder { // Implicitly created paths implicit_dirs: BTreeMap>, + + symlink_targets: FrozenVec, } /// Special sort algorithm for files by directory @@ -44,12 +46,13 @@ impl TreeBuilder { TreeBuilder { explicit: vec![], implicit_dirs: BTreeMap::new(), + symlink_targets: FrozenVec::new(), } } /// Push an item to the builder - we don't care if we have duplicates yet pub fn push(&mut self, item: T) { - let file = File::new(item); + let file = File::new(item, &self.symlink_targets); // Find all parent paths if let Some(parent) = file.parent() { @@ -62,7 +65,7 @@ impl TreeBuilder { }; leading_path = Some(full_path.clone()); self.implicit_dirs - .insert(full_path.clone(), File::new(full_path.into())); + .insert(full_path.clone(), File::new(full_path.into(), &self.symlink_targets)); } } @@ -85,7 +88,7 @@ impl TreeBuilder { let all_dirs = self .explicit .iter() - .filter(|f| matches!(f.kind, Kind::Directory)) + .filter(|f| matches!(f.kind, Kind::DIRECTORY)) .chain(self.implicit_dirs.values()) .map(|d| (&*d.path, d)) .collect::>(); @@ -96,15 +99,15 @@ impl TreeBuilder { // Resolve symlinks-to-dirs for link in self.explicit.iter() { - if let Kind::Symlink(target) = &link.kind { + if let Some(target) = link.kind.as_symlink(&self.symlink_targets) { // Resolve the link. let target = if target.starts_with('/') { - &**target + target } else if let Some(parent) = link.parent() { scratch.push(path::join(parent, target)); scratch.last().unwrap() } else { - &**target + target }; if all_dirs.contains_key(&target) { redirects.insert(&*link.path, target); @@ -115,7 +118,7 @@ impl TreeBuilder { // Insert everything WITHOUT redirects, directory first. let mut full_set = all_dirs .into_values() - .chain(self.explicit.iter().filter(|m| !matches!(m.kind, Kind::Directory))) + .chain(self.explicit.iter().filter(|m| !matches!(m.kind, Kind::DIRECTORY))) .collect::>(); full_set.sort_by(|a, b| sorted_paths(a, b)); @@ -134,7 +137,7 @@ impl TreeBuilder { // Reparent any symlink redirects. for (source_tree, target_tree) in redirects { - tree.reparent(source_tree, target_tree)?; + tree.reparent(source_tree, target_tree, &self.symlink_targets)?; } Ok(tree) } @@ -143,6 +146,7 @@ impl TreeBuilder { #[cfg(test)] mod tests { use astr::AStr; + use elsa::FrozenVec; use crate::tree::Kind; @@ -159,7 +163,7 @@ mod tests { fn from(value: AStr) -> Self { Self { path: value, - kind: Kind::Directory, + kind: Kind::DIRECTORY, id: "Virtual".into(), } } @@ -170,7 +174,7 @@ mod tests { self.path.clone() } - fn kind(&self) -> Kind { + fn kind(&self, _: &FrozenVec) -> Kind { self.kind.clone() } @@ -188,13 +192,13 @@ mod tests { } } - #[test] + /* #[test] fn test_simple_root() { let mut b: TreeBuilder = TreeBuilder::new(); let paths = vec![ CustomFile { path: "/usr/bin/nano".into(), - kind: Kind::Regular, + kind: Kind::REGULAR, id: "nano".into(), }, CustomFile { @@ -204,7 +208,7 @@ mod tests { }, CustomFile { path: "/usr/share/nano".into(), - kind: Kind::Directory, + kind: Kind::DIRECTORY, id: "nano".into(), }, CustomFile { @@ -214,7 +218,7 @@ mod tests { }, CustomFile { path: "/var/run/lock/subsys/1".into(), - kind: Kind::Regular, + kind: Kind::REGULAR, id: "baselayout".into(), }, ]; @@ -223,5 +227,5 @@ mod tests { } b.bake(); b.tree().unwrap(); - } + } */ } diff --git a/crates/vfs/src/tree/mod.rs b/crates/vfs/src/tree/mod.rs index 66631e478..659498712 100644 --- a/crates/vfs/src/tree/mod.rs +++ b/crates/vfs/src/tree/mod.rs @@ -9,6 +9,7 @@ use std::collections::HashMap; use std::vec; use astr::AStr; +use elsa::FrozenVec; use indextree::{Arena, Descendants, NodeId}; use snafu::Snafu; @@ -16,24 +17,47 @@ use crate::path::{self, VfsPath}; pub mod builder; -#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Kind { - // Regular path - Regular, +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Kind(usize); - // Directory (parenting node) - #[default] - Directory, +impl Kind { + /// Regular path + pub const REGULAR: Self = Self(usize::MAX - 1); - // Symlink to somewhere else. - Symlink(AStr), + /// Directory (parenting node) + pub const DIRECTORY: Self = Self(usize::MAX); + + // Every other value is a symlink + pub fn symlink(target: AStr, symlink_targets: &FrozenVec) -> Self { + symlink_targets.push(target); + Self(symlink_targets.len() - 1) + } + + #[inline] + pub fn is_symlink(self) -> bool { + self.0 < usize::MAX - 1 + } + + pub fn as_symlink(self, symlink_targets: &FrozenVec) -> Option<&str> { + if self.is_symlink() { + Some(&symlink_targets[self.0]) + } else { + None + } + } +} + +impl Default for Kind { + fn default() -> Self { + Self::DIRECTORY + } } /// Simple generic interface for blittable files while retaining details. /// /// All implementations should return a directory typed blitfile for a PathBuf. pub trait BlitFile: Clone + Sized + Debug + From { - fn kind(&self) -> Kind; + fn kind(&self, symlink_targets: &FrozenVec) -> Kind; fn path(&self) -> AStr; fn id(&self) -> AStr; @@ -50,13 +74,13 @@ struct File { } impl File { - pub fn new(inner: T) -> Self { + pub fn new(inner: T, symlink_targets: &FrozenVec) -> Self { let path = VfsPath::new(inner.path()); Self { id: inner.id(), path, - kind: inner.kind(), + kind: inner.kind(symlink_targets), inner, } } @@ -160,7 +184,12 @@ impl Tree { /// For all descendents of the given source tree, return a set of the reparented nodes, /// and remove the originals from the tree - fn reparent(&mut self, source_path: &str, target_path: &str) -> Result<(), Error> { + fn reparent( + &mut self, + source_path: &str, + target_path: &str, + symlink_targets: &FrozenVec, + ) -> Result<(), Error> { let mut mutations = vec![]; let mut orphans = vec![]; if let Some(source) = self.map.get(source_path) { @@ -173,7 +202,7 @@ impl Tree { for i in mutations { let original = self.arena.get(i).unwrap().get(); let relapath = path::join(target_path, original.path.strip_prefix(source_path).unwrap()); - orphans.push(File::new(original.inner.cloned_to(relapath))); + orphans.push(File::new(original.inner.cloned_to(relapath), symlink_targets)); } // Remove descendents @@ -218,7 +247,7 @@ impl Tree { let partial = item.file_name(); match item.kind { - Kind::Directory => { + Kind::DIRECTORY => { let children = start .children(&self.arena) .map(|c| self.structured_children(&c)) diff --git a/moss/Cargo.toml b/moss/Cargo.toml index 3410063c1..eaad85d6c 100644 --- a/moss/Cargo.toml +++ b/moss/Cargo.toml @@ -49,6 +49,7 @@ url.workspace = true xxhash-rust.workspace = true zbus.workspace = true astr.workspace = true +elsa = "1.11.2" [package.metadata.cargo-machete] # Needed for unixepoch() in src/db/state/migrations/2025-03-04-201550_init/up.sql diff --git a/moss/src/cli/info.rs b/moss/src/cli/info.rs index b0c63db41..5b7f694fb 100644 --- a/moss/src/cli/info.rs +++ b/moss/src/cli/info.rs @@ -173,9 +173,9 @@ fn print_files(vfs: vfs::Tree) { let files = vfs .iter() .filter_map(|file| { - if matches!(file.kind(), vfs::tree::Kind::Directory) { + /* if matches!(file.kind(), vfs::tree::Kind::Directory) { return None; - } + } */ let path = file.path(); let meta = match &file.layout.entry { diff --git a/moss/src/cli/state.rs b/moss/src/cli/state.rs index d7a84d8d2..ebb766aec 100644 --- a/moss/src/cli/state.rs +++ b/moss/src/cli/state.rs @@ -69,6 +69,7 @@ pub fn command() -> Command { .about("Verify TODO") .arg(arg!(--verbose "Vebose output").action(ArgAction::SetTrue)), ) + .subcommand(Command::new("build-vfs")) .subcommand(Export::command()) } @@ -89,6 +90,7 @@ pub fn handle(args: &ArgMatches, installation: Installation) -> Result<(), Error Some(("active", _)) => active(installation), Some(("list", _)) => list(installation), Some(("activate", args)) => activate(args, installation), + Some(("build-vfs", _)) => build_vfs(installation), Some(("query", args)) => query(args, installation), Some(("prune", args)) => prune(args, installation), Some(("remove", args)) => remove(args, installation), @@ -143,6 +145,20 @@ pub fn activate(args: &ArgMatches, installation: Installation) -> Result<(), Err Ok(()) } +pub fn build_vfs(installation: Installation) -> Result<(), Error> { + let id = installation.active_state.unwrap(); + let client = Client::new(environment::NAME, installation)?; + let new = client + .state_db + .get(id) + .map_err(|_| client::Error::StateDoesntExist(id))?; + let fstree = client.vfs(new.selections.iter().map(|selection| &selection.package))?; + + std::hint::black_box(fstree); + + Ok(()) +} + pub fn query(args: &ArgMatches, installation: Installation) -> Result<(), Error> { let id = *args.get_one::("ID").unwrap() as i32; diff --git a/moss/src/client/mod.rs b/moss/src/client/mod.rs index dae0c49ea..df71aed97 100644 --- a/moss/src/client/mod.rs +++ b/moss/src/client/mod.rs @@ -18,6 +18,7 @@ use std::{ }; use astr::AStr; +use elsa::FrozenVec; use fs_err as fs; use futures_util::{StreamExt, TryStreamExt, stream}; use nix::{ @@ -1131,11 +1132,11 @@ pub struct PendingFile { impl BlitFile for PendingFile { /// Match internal kind to minimalist vfs kind - fn kind(&self) -> vfs::tree::Kind { + fn kind(&self, symlink_targets: &FrozenVec) -> vfs::tree::Kind { match &self.layout.entry { - layout::Entry::Symlink(source, _) => vfs::tree::Kind::Symlink(source.clone()), - layout::Entry::Directory(_) => vfs::tree::Kind::Directory, - _ => vfs::tree::Kind::Regular, + layout::Entry::Symlink(source, _) => vfs::tree::Kind::symlink(source.clone(), symlink_targets), + layout::Entry::Directory(_) => vfs::tree::Kind::DIRECTORY, + _ => vfs::tree::Kind::REGULAR, } } From 769029d9c7a4f3ba9e4054a7757608e429746b71 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 1 Jan 2026 23:37:17 +0100 Subject: [PATCH 4/4] Much less hacky now --- Cargo.lock | 1 - crates/vfs/src/tree/builder.rs | 30 ++++----- crates/vfs/src/tree/kind.rs | 110 +++++++++++++++++++++++++++++++++ crates/vfs/src/tree/mod.rs | 70 +++++---------------- moss/Cargo.toml | 1 - moss/src/cli/info.rs | 4 +- moss/src/cli/state.rs | 16 ----- moss/src/client/mod.rs | 5 +- 8 files changed, 142 insertions(+), 95 deletions(-) create mode 100644 crates/vfs/src/tree/kind.rs diff --git a/Cargo.lock b/Cargo.lock index c4ebc46b1..2b63c9321 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,7 +1886,6 @@ dependencies = [ "derive_more", "diesel", "diesel_migrations", - "elsa", "fnmatch", "fs-err", "futures-util", diff --git a/crates/vfs/src/tree/builder.rs b/crates/vfs/src/tree/builder.rs index 353fbdba0..2f2087ca9 100644 --- a/crates/vfs/src/tree/builder.rs +++ b/crates/vfs/src/tree/builder.rs @@ -9,7 +9,7 @@ use astr::AStr; use elsa::FrozenVec; use crate::path; -use crate::tree::{Kind, Tree}; +use crate::tree::Tree; use super::{BlitFile, Error, File}; @@ -20,8 +20,6 @@ pub struct TreeBuilder { // Implicitly created paths implicit_dirs: BTreeMap>, - - symlink_targets: FrozenVec, } /// Special sort algorithm for files by directory @@ -46,13 +44,12 @@ impl TreeBuilder { TreeBuilder { explicit: vec![], implicit_dirs: BTreeMap::new(), - symlink_targets: FrozenVec::new(), } } /// Push an item to the builder - we don't care if we have duplicates yet pub fn push(&mut self, item: T) { - let file = File::new(item, &self.symlink_targets); + let file = File::new(item); // Find all parent paths if let Some(parent) = file.parent() { @@ -65,7 +62,7 @@ impl TreeBuilder { }; leading_path = Some(full_path.clone()); self.implicit_dirs - .insert(full_path.clone(), File::new(full_path.into(), &self.symlink_targets)); + .insert(full_path.clone(), File::new(full_path.into())); } } @@ -88,7 +85,7 @@ impl TreeBuilder { let all_dirs = self .explicit .iter() - .filter(|f| matches!(f.kind, Kind::DIRECTORY)) + .filter(|f| f.kind.is_directory()) .chain(self.implicit_dirs.values()) .map(|d| (&*d.path, d)) .collect::>(); @@ -99,7 +96,7 @@ impl TreeBuilder { // Resolve symlinks-to-dirs for link in self.explicit.iter() { - if let Some(target) = link.kind.as_symlink(&self.symlink_targets) { + if let Some(target) = link.kind.as_symlink() { // Resolve the link. let target = if target.starts_with('/') { target @@ -118,7 +115,7 @@ impl TreeBuilder { // Insert everything WITHOUT redirects, directory first. let mut full_set = all_dirs .into_values() - .chain(self.explicit.iter().filter(|m| !matches!(m.kind, Kind::DIRECTORY))) + .chain(self.explicit.iter().filter(|m| !m.kind.is_directory())) .collect::>(); full_set.sort_by(|a, b| sorted_paths(a, b)); @@ -137,7 +134,7 @@ impl TreeBuilder { // Reparent any symlink redirects. for (source_tree, target_tree) in redirects { - tree.reparent(source_tree, target_tree, &self.symlink_targets)?; + tree.reparent(source_tree, target_tree)?; } Ok(tree) } @@ -146,13 +143,12 @@ impl TreeBuilder { #[cfg(test)] mod tests { use astr::AStr; - use elsa::FrozenVec; use crate::tree::Kind; use super::{BlitFile, TreeBuilder}; - #[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] + #[derive(Clone, Default, Debug)] struct CustomFile { path: AStr, kind: Kind, @@ -174,7 +170,7 @@ mod tests { self.path.clone() } - fn kind(&self, _: &FrozenVec) -> Kind { + fn kind(&self) -> Kind { self.kind.clone() } @@ -192,7 +188,7 @@ mod tests { } } - /* #[test] + #[test] fn test_simple_root() { let mut b: TreeBuilder = TreeBuilder::new(); let paths = vec![ @@ -203,7 +199,7 @@ mod tests { }, CustomFile { path: "/usr/bin/rnano".into(), - kind: Kind::Symlink("nano".into()), + kind: Kind::symlink("nano".into()), id: "nano".into(), }, CustomFile { @@ -213,7 +209,7 @@ mod tests { }, CustomFile { path: "/var/run/lock".into(), - kind: Kind::Symlink("/run/lock".into()), + kind: Kind::symlink("/run/lock".into()), id: "baselayout".into(), }, CustomFile { @@ -227,5 +223,5 @@ mod tests { } b.bake(); b.tree().unwrap(); - } */ + } } diff --git a/crates/vfs/src/tree/kind.rs b/crates/vfs/src/tree/kind.rs new file mode 100644 index 000000000..f5439966e --- /dev/null +++ b/crates/vfs/src/tree/kind.rs @@ -0,0 +1,110 @@ +use std::mem::ManuallyDrop; + +use astr::AStr; + +pub union Kind { + addr_or_tag: usize, + symlink_target: ManuallyDrop, +} + +impl Kind { + /// Regular path + pub const REGULAR: Self = Self { addr_or_tag: 0x1 }; + /// Directory (parenting node) + pub const DIRECTORY: Self = Self { addr_or_tag: 0x2 }; + + pub fn symlink(target: AStr) -> Self { + Self { + symlink_target: ManuallyDrop::new(target), + } + } + + pub fn is_regular(&self) -> bool { + unsafe { self.addr_or_tag == 0x1 } + } + + pub fn is_directory(&self) -> bool { + unsafe { self.addr_or_tag == 0x2 } + } + + pub fn is_symlink(&self) -> bool { + unsafe { self.addr_or_tag >= 0x8 } + } + + pub fn as_symlink(&self) -> Option<&AStr> { + self.is_symlink().then(|| unsafe { &*self.symlink_target }) + } +} + +impl Clone for Kind { + fn clone(&self) -> Self { + if let Some(target) = self.as_symlink() { + let symlink_target = ManuallyDrop::new(AStr::clone(target)); + Self { symlink_target } + } else { + let addr_or_tag = unsafe { self.addr_or_tag }; + Self { addr_or_tag } + } + } +} + +impl Default for Kind { + fn default() -> Self { + Self::DIRECTORY + } +} + +impl Drop for Kind { + fn drop(&mut self) { + let addr_or_tag = unsafe { self.addr_or_tag }; + debug_assert_ne!(addr_or_tag, 0); + if addr_or_tag >= 0x8 { + unsafe { + ManuallyDrop::drop(&mut self.symlink_target); + } + } + } +} + +mod debug_impl { + use std::fmt; + + #[derive(Debug)] + #[allow(dead_code)] + enum Kind<'a> { + Regular, + Directory, + Symlink(&'a str), + } + + impl super::Kind { + fn to_enum(&self) -> Kind<'_> { + if self.is_regular() { + Kind::Regular + } else if let Some(target) = self.as_symlink() { + Kind::Symlink(target) + } else { + Kind::Directory + } + } + } + + impl fmt::Debug for super::Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.to_enum().fmt(f) + } + } +} + +#[cfg(test)] +mod tests { + use crate::tree::Kind; + + #[test] + fn run_this_with_miri() { + let kind = Kind::symlink("/test/thing".into()); + let kind2 = kind.clone(); + drop(kind); + drop(kind2); + } +} diff --git a/crates/vfs/src/tree/mod.rs b/crates/vfs/src/tree/mod.rs index 659498712..854d4aa7b 100644 --- a/crates/vfs/src/tree/mod.rs +++ b/crates/vfs/src/tree/mod.rs @@ -9,55 +9,21 @@ use std::collections::HashMap; use std::vec; use astr::AStr; -use elsa::FrozenVec; use indextree::{Arena, Descendants, NodeId}; use snafu::Snafu; use crate::path::{self, VfsPath}; pub mod builder; +mod kind; -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Kind(usize); - -impl Kind { - /// Regular path - pub const REGULAR: Self = Self(usize::MAX - 1); - - /// Directory (parenting node) - pub const DIRECTORY: Self = Self(usize::MAX); - - // Every other value is a symlink - pub fn symlink(target: AStr, symlink_targets: &FrozenVec) -> Self { - symlink_targets.push(target); - Self(symlink_targets.len() - 1) - } - - #[inline] - pub fn is_symlink(self) -> bool { - self.0 < usize::MAX - 1 - } - - pub fn as_symlink(self, symlink_targets: &FrozenVec) -> Option<&str> { - if self.is_symlink() { - Some(&symlink_targets[self.0]) - } else { - None - } - } -} - -impl Default for Kind { - fn default() -> Self { - Self::DIRECTORY - } -} +pub use self::kind::Kind; /// Simple generic interface for blittable files while retaining details. /// /// All implementations should return a directory typed blitfile for a PathBuf. pub trait BlitFile: Clone + Sized + Debug + From { - fn kind(&self, symlink_targets: &FrozenVec) -> Kind; + fn kind(&self) -> Kind; fn path(&self) -> AStr; fn id(&self) -> AStr; @@ -74,13 +40,13 @@ struct File { } impl File { - pub fn new(inner: T, symlink_targets: &FrozenVec) -> Self { + pub fn new(inner: T) -> Self { let path = VfsPath::new(inner.path()); Self { id: inner.id(), path, - kind: inner.kind(symlink_targets), + kind: inner.kind(), inner, } } @@ -184,12 +150,7 @@ impl Tree { /// For all descendents of the given source tree, return a set of the reparented nodes, /// and remove the originals from the tree - fn reparent( - &mut self, - source_path: &str, - target_path: &str, - symlink_targets: &FrozenVec, - ) -> Result<(), Error> { + fn reparent(&mut self, source_path: &str, target_path: &str) -> Result<(), Error> { let mut mutations = vec![]; let mut orphans = vec![]; if let Some(source) = self.map.get(source_path) { @@ -202,7 +163,7 @@ impl Tree { for i in mutations { let original = self.arena.get(i).unwrap().get(); let relapath = path::join(target_path, original.path.strip_prefix(source_path).unwrap()); - orphans.push(File::new(original.inner.cloned_to(relapath), symlink_targets)); + orphans.push(File::new(original.inner.cloned_to(relapath))); } // Remove descendents @@ -246,15 +207,14 @@ impl Tree { let item = node.get(); let partial = item.file_name(); - match item.kind { - Kind::DIRECTORY => { - let children = start - .children(&self.arena) - .map(|c| self.structured_children(&c)) - .collect::>(); - Element::Directory(partial, &item.inner, children) - } - _ => Element::Child(partial, &item.inner), + if item.kind.is_directory() { + let children = start + .children(&self.arena) + .map(|c| self.structured_children(&c)) + .collect::>(); + Element::Directory(partial, &item.inner, children) + } else { + Element::Child(partial, &item.inner) } } } diff --git a/moss/Cargo.toml b/moss/Cargo.toml index eaad85d6c..3410063c1 100644 --- a/moss/Cargo.toml +++ b/moss/Cargo.toml @@ -49,7 +49,6 @@ url.workspace = true xxhash-rust.workspace = true zbus.workspace = true astr.workspace = true -elsa = "1.11.2" [package.metadata.cargo-machete] # Needed for unixepoch() in src/db/state/migrations/2025-03-04-201550_init/up.sql diff --git a/moss/src/cli/info.rs b/moss/src/cli/info.rs index 5b7f694fb..dcca29fd5 100644 --- a/moss/src/cli/info.rs +++ b/moss/src/cli/info.rs @@ -173,9 +173,9 @@ fn print_files(vfs: vfs::Tree) { let files = vfs .iter() .filter_map(|file| { - /* if matches!(file.kind(), vfs::tree::Kind::Directory) { + if file.kind().is_directory() { return None; - } */ + } let path = file.path(); let meta = match &file.layout.entry { diff --git a/moss/src/cli/state.rs b/moss/src/cli/state.rs index ebb766aec..d7a84d8d2 100644 --- a/moss/src/cli/state.rs +++ b/moss/src/cli/state.rs @@ -69,7 +69,6 @@ pub fn command() -> Command { .about("Verify TODO") .arg(arg!(--verbose "Vebose output").action(ArgAction::SetTrue)), ) - .subcommand(Command::new("build-vfs")) .subcommand(Export::command()) } @@ -90,7 +89,6 @@ pub fn handle(args: &ArgMatches, installation: Installation) -> Result<(), Error Some(("active", _)) => active(installation), Some(("list", _)) => list(installation), Some(("activate", args)) => activate(args, installation), - Some(("build-vfs", _)) => build_vfs(installation), Some(("query", args)) => query(args, installation), Some(("prune", args)) => prune(args, installation), Some(("remove", args)) => remove(args, installation), @@ -145,20 +143,6 @@ pub fn activate(args: &ArgMatches, installation: Installation) -> Result<(), Err Ok(()) } -pub fn build_vfs(installation: Installation) -> Result<(), Error> { - let id = installation.active_state.unwrap(); - let client = Client::new(environment::NAME, installation)?; - let new = client - .state_db - .get(id) - .map_err(|_| client::Error::StateDoesntExist(id))?; - let fstree = client.vfs(new.selections.iter().map(|selection| &selection.package))?; - - std::hint::black_box(fstree); - - Ok(()) -} - pub fn query(args: &ArgMatches, installation: Installation) -> Result<(), Error> { let id = *args.get_one::("ID").unwrap() as i32; diff --git a/moss/src/client/mod.rs b/moss/src/client/mod.rs index df71aed97..87d558a39 100644 --- a/moss/src/client/mod.rs +++ b/moss/src/client/mod.rs @@ -18,7 +18,6 @@ use std::{ }; use astr::AStr; -use elsa::FrozenVec; use fs_err as fs; use futures_util::{StreamExt, TryStreamExt, stream}; use nix::{ @@ -1132,9 +1131,9 @@ pub struct PendingFile { impl BlitFile for PendingFile { /// Match internal kind to minimalist vfs kind - fn kind(&self, symlink_targets: &FrozenVec) -> vfs::tree::Kind { + fn kind(&self) -> vfs::tree::Kind { match &self.layout.entry { - layout::Entry::Symlink(source, _) => vfs::tree::Kind::symlink(source.clone(), symlink_targets), + layout::Entry::Symlink(source, _) => vfs::tree::Kind::symlink(source.clone()), layout::Entry::Directory(_) => vfs::tree::Kind::DIRECTORY, _ => vfs::tree::Kind::REGULAR, }