Skip to content

Commit 20ae0fc

Browse files
committed
feat(storage/vfs): use HashMaps to improve efficiency of the VFS
add hashbrown as a dependency to provide fast HashMaps
1 parent 44b2c7d commit 20ae0fc

8 files changed

Lines changed: 194 additions & 91 deletions

File tree

Cargo.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beskar-core/src/video.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ impl Pixel {
155155

156156
#[must_use]
157157
#[inline]
158-
fn new_rgb(components: PixelComponents) -> Self {
158+
pub fn new_rgb(components: PixelComponents) -> Self {
159159
Self(
160160
((u32::from(components.blue)) << 16)
161161
| ((u32::from(components.green)) << 8)
@@ -165,7 +165,7 @@ impl Pixel {
165165

166166
#[must_use]
167167
#[inline]
168-
fn new_bgr(components: PixelComponents) -> Self {
168+
pub fn new_bgr(components: PixelComponents) -> Self {
169169
Self(
170170
((u32::from(components.red)) << 16)
171171
| ((u32::from(components.green)) << 8)
@@ -185,7 +185,7 @@ impl Pixel {
185185

186186
#[must_use]
187187
#[inline]
188-
fn components_bgr(self) -> PixelComponents {
188+
pub fn components_bgr(self) -> PixelComponents {
189189
let red = u8::try_from((self.0 >> 16) & 0xFF).unwrap();
190190
let green = u8::try_from((self.0 >> 8) & 0xFF).unwrap();
191191
let blue = u8::try_from(self.0 & 0xFF).unwrap();
@@ -194,7 +194,7 @@ impl Pixel {
194194

195195
#[must_use]
196196
#[inline]
197-
fn components_rgb(self) -> PixelComponents {
197+
pub fn components_rgb(self) -> PixelComponents {
198198
let blue = u8::try_from((self.0 >> 16) & 0xFF).unwrap();
199199
let green = u8::try_from((self.0 >> 8) & 0xFF).unwrap();
200200
let red = u8::try_from(self.0 & 0xFF).unwrap();

kernel/foundry/storage/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ edition = "2024"
55

66
[dependencies]
77
beskar-core = { workspace = true }
8+
hashbrown = { version = "0.16.1" }
89
hyperdrive = { workspace = true }
910
thiserror = { workspace = true }

kernel/foundry/storage/src/fs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ pub trait FileSystem {
8484
fn read_dir(&mut self, path: Path) -> FileResult<Vec<PathBuf>>;
8585
}
8686

87-
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
87+
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
8888
pub struct PathBuf(String);
8989

90-
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
90+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
9191
pub struct Path<'a>(&'a str);
9292

9393
impl PathBuf {

kernel/foundry/storage/src/vfs.rs

Lines changed: 96 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use super::fs::{FileError, FileResult, FileSystem, PathBuf};
22
use crate::fs::Path;
3-
use alloc::{boxed::Box, collections::BTreeMap};
3+
use alloc::{boxed::Box, vec::Vec};
44
use core::{
55
marker::PhantomData,
66
sync::atomic::{AtomicI64, Ordering},
77
};
8+
use hashbrown::HashMap;
89
use hyperdrive::locks::rw::RwLock;
910

1011
pub trait VfsHelper {
@@ -13,7 +14,7 @@ pub trait VfsHelper {
1314
fn get_current_process_id() -> u64;
1415
}
1516

16-
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
17+
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
1718
pub struct Handle {
1819
id: i64,
1920
}
@@ -31,14 +32,10 @@ impl Handle {
3132

3233
#[must_use]
3334
#[inline]
34-
#[expect(clippy::missing_panics_doc, reason = "Never panics")]
3535
pub fn new() -> Self {
36-
let id = HANDLE_COUNTER.fetch_add(1, Ordering::Relaxed);
37-
3836
// By opening 1 000 files a second, it would take 3 000 000 centuries to overflow,
3937
// so we can deliberately not handle the overflow.
40-
assert!(id >= 0, "Handle ID overflow");
41-
38+
let id = HANDLE_COUNTER.fetch_add(1, Ordering::Relaxed);
4239
Self { id }
4340
}
4441

@@ -53,7 +50,7 @@ impl Handle {
5350
///
5451
/// The given ID should be positive.
5552
pub const unsafe fn from_raw(id: i64) -> Self {
56-
debug_assert!(id >= 0);
53+
debug_assert!(id >= 1);
5754
Self { id }
5855
}
5956

@@ -64,12 +61,11 @@ impl Handle {
6461
}
6562
}
6663

67-
type Mounts = BTreeMap<PathBuf, RwLock<Box<dyn FileSystem + Send + Sync>>>;
68-
type OpenFiles = BTreeMap<Handle, OpenFileInfo>;
64+
type OpenFiles = HashMap<Handle, OpenFileInfo>;
6965

7066
#[derive(Default)]
7167
pub struct Vfs<H: VfsHelper> {
72-
mounts: RwLock<Mounts>,
68+
mounts: RwLock<MountIndex>,
7369
open_handles: RwLock<OpenFiles>,
7470
_helper: PhantomData<H>,
7571
}
@@ -79,51 +75,92 @@ struct OpenFileInfo {
7975
path: PathBuf,
8076
}
8177

78+
/// Cached sorted mount list for efficient path matching.
79+
/// Stored as `(path_length, path, filesystem)` sorted by length descending.
80+
#[derive(Default)]
81+
struct MountIndex {
82+
/// Sorted list of mounts by path length (longest first) for prefix matching
83+
sorted_mounts: Vec<(usize, PathBuf)>,
84+
/// `HashMap` for filesystem lookup
85+
filesystems: HashMap<PathBuf, RwLock<Box<dyn FileSystem + Send + Sync>>>,
86+
}
87+
8288
impl<H: VfsHelper> Vfs<H> {
8389
#[must_use]
84-
#[inline]
8590
/// Creates a new VFS instance.
86-
pub const fn new() -> Self {
91+
pub fn new() -> Self {
8792
Self {
88-
mounts: RwLock::new(BTreeMap::new()),
89-
open_handles: RwLock::new(BTreeMap::new()),
93+
mounts: RwLock::new(MountIndex::default()),
94+
open_handles: RwLock::new(HashMap::new()),
9095
_helper: PhantomData,
9196
}
9297
}
9398

9499
/// Mounts a filesystem at the given path.
95100
pub fn mount(&self, path: PathBuf, fs: Box<dyn FileSystem + Send + Sync>) {
96-
self.mounts.write().insert(path, RwLock::new(fs));
101+
let mut mounts = self.mounts.write();
102+
let path_len = path.as_path().len();
103+
104+
// Insert into hashmap
105+
mounts.filesystems.insert(path.clone(), RwLock::new(fs));
106+
107+
// Insert into sorted list maintaining descending order by length
108+
match mounts
109+
.sorted_mounts
110+
.binary_search_by(|&(len, _)| len.cmp(&path_len).reverse())
111+
{
112+
Ok(idx) => {
113+
// Find the correct position (may have duplicates)
114+
mounts.sorted_mounts.insert(idx, (path_len, path));
115+
}
116+
Err(idx) => mounts.sorted_mounts.insert(idx, (path_len, path)),
117+
}
97118
}
98119

99120
/// Unmounts the filesystem at the given path.
100-
pub fn unmount(&self, path: &str) -> FileResult<Box<dyn FileSystem + Send + Sync>> {
101-
self.mounts
102-
.write()
103-
.remove(path)
104-
.map(RwLock::into_inner)
105-
.ok_or(FileError::NotFound)
106-
}
107-
108-
/// Checks if a file is opened.
109-
fn check_file_opened(&self, path: Path) -> bool {
110-
let current_pid = H::get_current_process_id();
111-
self.open_handles.read().values().any(|open_file| {
112-
open_file.path.as_path() == path && open_file.process_id == current_pid
121+
pub fn unmount(&self, path: Path) -> FileResult<Box<dyn FileSystem + Send + Sync>> {
122+
let mut mounts = self.mounts.write();
123+
124+
// Remove from sorted list
125+
mounts.sorted_mounts.retain(|(_, p)| p.as_path() != path);
126+
127+
// Remove from hashmap and return the filesystem
128+
// Try to find matching PathBuf by string comparison
129+
let path_buf = mounts
130+
.filesystems
131+
.iter()
132+
.find(|(p, _)| p.as_path() == path)
133+
.map(|(p, _)| p.clone());
134+
135+
path_buf.map_or(Err(FileError::NotFound), |path_buf| {
136+
mounts
137+
.filesystems
138+
.remove(&path_buf)
139+
.map(RwLock::into_inner)
140+
.ok_or(FileError::NotFound)
113141
})
114142
}
115143

116144
/// Creates a new handle.
117145
///
118146
/// This function performs checks and adds the handle to the open handles list.
119147
fn new_handle(&self, path: Path) -> FileResult<Handle> {
120-
if self.check_file_opened(path) {
121-
return Err(FileError::PermissionDenied);
148+
let current_pid = H::get_current_process_id();
149+
150+
// Check if already opened by this process
151+
{
152+
let open_handles = self.open_handles.read();
153+
if open_handles.values().any(|open_file| {
154+
open_file.path.as_path() == path && open_file.process_id == current_pid
155+
}) {
156+
return Err(FileError::PermissionDenied);
157+
}
122158
}
159+
123160
let handle = Handle::new();
124161
let open_file_info = OpenFileInfo {
125162
path: path.to_owned(),
126-
process_id: H::get_current_process_id(),
163+
process_id: current_pid,
127164
};
128165
self.open_handles.write().insert(handle, open_file_info);
129166
Ok(handle)
@@ -156,37 +193,33 @@ impl<H: VfsHelper> Vfs<H> {
156193
f: impl FnOnce(&mut (dyn FileSystem + Send + Sync), Path) -> FileResult<T>,
157194
) -> FileResult<T> {
158195
let mounts = self.mounts.read();
159-
160-
let mut best_match: Option<(&RwLock<Box<dyn FileSystem + Send + Sync>>, usize)> = None;
161-
let mut best_len = 0;
162-
163196
let path_str = path.as_str();
164197

165-
for (mount_path, fs) in mounts.iter() {
166-
let mount_len = mount_path.as_path().len();
167-
if mount_len <= best_len {
198+
for &(mount_len, ref mount_path) in &mounts.sorted_mounts {
199+
if mount_len > path_str.len() {
168200
continue;
169201
}
170202

171203
// Check if path starts with mount point
172-
if path_str.len() >= mount_len
173-
&& &path_str[..mount_len] == mount_path.as_path().as_str()
174-
{
204+
if &path_str[..mount_len] == mount_path.as_path().as_str() {
175205
// Ensure we match at path boundaries to avoid partial matches
176206
// e.g., /dev should not match /device
177207
if mount_len == path_str.len()
178208
|| path_str.as_bytes().get(mount_len) == Some(&b'/')
179209
|| mount_path.as_path().as_str().ends_with('/')
180210
{
181-
best_match = Some((fs, mount_len));
182-
best_len = mount_len;
211+
// Found match - get filesystem from hashmap for O(1) lookup
212+
let fs = mounts
213+
.filesystems
214+
.get(mount_path)
215+
.ok_or(FileError::InvalidPath)?;
216+
let rel_path = Path::from(&path_str[mount_len..]);
217+
return f(&mut **fs.write(), rel_path);
183218
}
184219
}
185220
}
186221

187-
let (fs, mount_len) = best_match.ok_or(FileError::InvalidPath)?;
188-
let rel_path = Path::from(&path_str[mount_len..]);
189-
f(&mut **fs.write(), rel_path)
222+
Err(FileError::InvalidPath)
190223
}
191224

192225
#[inline]
@@ -220,19 +253,29 @@ impl<H: VfsHelper> Vfs<H> {
220253
self.open_handles.write().retain(|_handle, open_file| {
221254
let retained = open_file.process_id != pid;
222255
if !retained {
223-
self.path_to_fs(open_file.path.as_path(), |fs, rel_path| fs.close(rel_path))
224-
.unwrap();
256+
let res =
257+
self.path_to_fs(open_file.path.as_path(), |fs, rel_path| fs.close(rel_path));
258+
debug_assert!(res.is_ok(), "Failed to close file during process cleanup");
225259
}
226260
retained
227261
});
228262
}
229263

230264
/// Deletes a file at the given path.
231265
pub fn delete(&self, path: Path) -> FileResult<()> {
232-
if self.check_file_opened(path) {
233-
return Err(FileError::PermissionDenied);
266+
let current_pid = H::get_current_process_id();
267+
268+
// Check if file is opened with lock to prevent TOCTOU issues
269+
{
270+
let open_handles = self.open_handles.read();
271+
if open_handles.values().any(|open_file| {
272+
open_file.path.as_path() == path && open_file.process_id == current_pid
273+
}) {
274+
return Err(FileError::PermissionDenied);
275+
}
234276
}
235-
// FIXME: TOCTOU vulnerability?
277+
278+
// Delete the file from the filesystem
236279
self.path_to_fs(path, |fs, rel_path| fs.delete(rel_path))
237280
}
238281

@@ -261,7 +304,7 @@ impl<H: VfsHelper> Vfs<H> {
261304
self.path_to_fs(path, |fs, rel_path| fs.metadata(rel_path))
262305
}
263306

264-
pub fn read_dir(&self, path: Path) -> FileResult<alloc::vec::Vec<PathBuf>> {
307+
pub fn read_dir(&self, path: Path) -> FileResult<Vec<PathBuf>> {
265308
self.path_to_fs(path, |fs, rel_path| fs.read_dir(rel_path))
266309
}
267310
}

0 commit comments

Comments
 (0)