use std::{collections::HashMap, sync::Arc};
use bytemuck::{Pod, Zeroable};
pub const NO_INDEX: u32 = u32::MAX;
pub const NO_EXTENSION: &str = "(no extension)";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Pod, Zeroable)]
#[repr(transparent)]
pub struct StringId(pub u32);
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
#[repr(C, align(8))]
pub struct FileNode {
pub name_id: StringId,
pub parent: u32,
pub first_child: u32,
pub next_sibling: u32,
pub size: u64,
pub modified_timestamp: i64,
pub created_timestamp: i64,
pub accessed_timestamp: i64,
pub file_count: u32,
pub flags: u8,
_padding: [u8; 3],
}
impl FileNode {
pub const FLAG_DIRECTORY: u8 = 1 << 0;
pub const FLAG_SYMLINK: u8 = 1 << 1;
#[must_use]
#[inline]
pub fn new(
name_id: StringId,
parent: Option<u32>,
is_dir: bool,
is_symlink: bool,
modified_timestamp: i64,
created_timestamp: i64,
accessed_timestamp: i64,
) -> Self {
let mut flags = 0u8;
if is_dir {
flags |= Self::FLAG_DIRECTORY;
}
if is_symlink {
flags |= Self::FLAG_SYMLINK;
}
Self {
name_id,
parent: parent.unwrap_or(NO_INDEX),
first_child: NO_INDEX,
next_sibling: NO_INDEX,
size: 0,
modified_timestamp,
created_timestamp,
accessed_timestamp,
file_count: 0,
flags,
_padding: [0; 3],
}
}
#[must_use]
#[inline]
pub const fn is_directory(&self) -> bool {
(self.flags & Self::FLAG_DIRECTORY) != 0
}
#[must_use]
#[inline]
pub const fn is_symlink(&self) -> bool {
(self.flags & Self::FLAG_SYMLINK) != 0
}
#[must_use]
#[inline]
pub const fn parent_opt(&self) -> Option<u32> {
if self.parent == NO_INDEX {
None
} else {
Some(self.parent)
}
}
#[must_use]
#[inline]
pub const fn first_child_opt(&self) -> Option<u32> {
if self.first_child == NO_INDEX {
None
} else {
Some(self.first_child)
}
}
#[must_use]
#[inline]
pub const fn next_sibling_opt(&self) -> Option<u32> {
if self.next_sibling == NO_INDEX {
None
} else {
Some(self.next_sibling)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StringPool {
pub data: Vec<u8>,
pub offsets: Vec<StringOffset>,
pub lookup: HashMap<Vec<u8>, StringId>,
}
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct StringOffset {
pub offset: u32,
pub len: u32,
}
impl StringPool {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn get_or_insert(&mut self, s: &[u8]) -> StringId {
if let Some(&id) = self.lookup.get(s) {
return id;
}
let offset = self.data.len() as u32;
let len = s.len() as u32;
self.data.extend_from_slice(s);
let id = StringId(self.offsets.len() as u32);
self.offsets.push(StringOffset { offset, len });
self.lookup.insert(s.to_vec(), id);
id
}
#[must_use]
pub fn get(&self, id: StringId) -> Option<&str> {
let offset_info = self.offsets.get(id.0 as usize)?;
let start = offset_info.offset as usize;
let end = start + offset_info.len as usize;
if end <= self.data.len() {
std::str::from_utf8(&self.data[start..end]).ok()
} else {
None
}
}
}
pub struct FileArenaSnapshot {
pub nodes: Arc<Vec<FileNode>>,
pub string_pool: Arc<StringPool>,
}
impl FileArenaSnapshot {
#[must_use]
pub fn get_full_path(&self, node_idx: u32) -> String {
let mut parts = Vec::new();
let mut curr = Some(node_idx);
while let Some(idx) = curr {
if let Some(node) = self.nodes.get(idx as usize) {
if let Some(name) = self.string_pool.get(node.name_id) {
if !name.is_empty() {
parts.push(name);
}
}
curr = node.parent_opt();
} else {
break;
}
}
parts.reverse();
if parts.is_empty() {
return "/".to_string();
}
let first = parts[0];
if first.starts_with('/') || first.contains(':') {
let mut path = first.to_string();
for part in &parts[1..] {
if !path.ends_with('/') {
path.push('/');
}
path.push_str(part);
}
path
} else {
parts.join("/")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_pool() {
let mut pool = StringPool::new();
let id1 = pool.get_or_insert(b"Cargo.toml");
let id2 = pool.get_or_insert(b"src");
let id3 = pool.get_or_insert(b"Cargo.toml");
assert_eq!(id1, id3); assert_ne!(id1, id2);
assert_eq!(pool.get(id1), Some("Cargo.toml"));
assert_eq!(pool.get(id2), Some("src"));
}
#[test]
fn test_path_reconstruction() {
let mut pool = StringPool::new();
let root_id = pool.get_or_insert(b"/home/tux");
let dir_id = pool.get_or_insert(b"Documents");
let file_id = pool.get_or_insert(b"test.rs");
let nodes = vec![
FileNode::new(root_id, None, true, false, 0, 0, 0),
FileNode::new(dir_id, Some(0), true, false, 0, 0, 0),
FileNode::new(file_id, Some(1), false, false, 0, 0, 0),
];
let snapshot = FileArenaSnapshot {
nodes: Arc::new(nodes),
string_pool: Arc::new(pool),
};
assert_eq!(snapshot.get_full_path(0), "/home/tux");
assert_eq!(snapshot.get_full_path(1), "/home/tux/Documents");
assert_eq!(snapshot.get_full_path(2), "/home/tux/Documents/test.rs");
}
}