Skip to content

Commit e1dafc4

Browse files
committed
renamed directories should propagate to their children in the inodetable
1 parent af7622e commit e1dafc4

2 files changed

Lines changed: 173 additions & 96 deletions

File tree

RELEASENOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
v0.6.4: (unreleased)
2+
* Fixed a bug in inode table where renamed directories did not propagate to their children in the
3+
table. See #51.
4+
15
v0.6.3: 2025-11-30
26
* Fixed a missing inodes.unlink in rmdir, which could cause a panic in rare circumstances
37

src/inode_table.rs

Lines changed: 169 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// InodeTable :: a bi-directional map of paths to inodes.
22
//
3-
// Copyright (c) 2016-2022 by William R. Fraser
3+
// Copyright (c) 2016-2026 by William R. Fraser
44
//
55

66
use std::borrow::Borrow;
7-
use std::cmp::{Eq, PartialEq};
8-
use std::collections::{HashMap, VecDeque};
9-
use std::collections::hash_map::Entry::*;
10-
use std::hash::{Hash, Hasher};
7+
use std::cmp::{Eq, Ordering, PartialEq};
8+
use std::collections::{BTreeMap, VecDeque};
9+
use std::collections::btree_map::Entry::*;
10+
use std::ops::Bound::{Excluded, Included, Unbounded};
1111
use std::path::{Path, PathBuf};
1212
use std::sync::Arc;
1313

@@ -27,7 +27,7 @@ struct InodeTableEntry {
2727
pub struct InodeTable {
2828
table: Vec<InodeTableEntry>,
2929
free_list: VecDeque<usize>,
30-
by_path: HashMap<Arc<PathBuf>, usize>,
30+
by_path: BTreeMap<Arc<PathBuf>, usize>,
3131
}
3232

3333
impl InodeTable {
@@ -42,7 +42,7 @@ impl InodeTable {
4242
let mut inode_table = InodeTable {
4343
table: Vec::new(),
4444
free_list: VecDeque::new(),
45-
by_path: HashMap::new()
45+
by_path: BTreeMap::new()
4646
};
4747
let root = Arc::new(PathBuf::from("/"));
4848
inode_table.table.push(InodeTableEntry {
@@ -180,9 +180,39 @@ impl InodeTable {
180180
/// Change an inode's path to a different one, without changing the inode number.
181181
/// Lookup counts remain unchanged, even if this is replacing another file.
182182
pub fn rename(&mut self, oldpath: &Path, newpath: Arc<PathBuf>) {
183-
let idx = self.by_path.remove(Pathish::new(oldpath)).unwrap();
184-
self.table[idx].path = Some(newpath.clone());
185-
self.by_path.insert(newpath, idx); // this can replace a path with a new inode
183+
// None means it's a single path being renamed
184+
let mut range = None;
185+
for (candidate, _) in self.by_path.range::<Pathish, _>((Excluded(Pathish::new(oldpath)), Unbounded)) {
186+
// look for children of the path being renamed
187+
if candidate.starts_with(oldpath) {
188+
if let Some((start, _end)) = range {
189+
range = Some((start, Included(candidate.clone())));
190+
} else {
191+
range = Some((Included(Arc::new(oldpath.to_owned())), Included(Arc::clone(candidate))));
192+
}
193+
} else {
194+
break;
195+
}
196+
}
197+
198+
if let Some(range) = range {
199+
let mut new_entries = vec![];
200+
for (path, idx) in self.by_path.extract_if(range, |_, _| true) {
201+
let suffix = path.strip_prefix(oldpath).unwrap();
202+
let new_entry_path = if suffix.as_os_str().is_empty() {
203+
Arc::clone(&newpath)
204+
} else {
205+
Arc::new(newpath.as_path().join(suffix))
206+
};
207+
self.table[idx].path = Some(Arc::clone(&new_entry_path));
208+
new_entries.push((new_entry_path, idx));
209+
}
210+
self.by_path.extend(new_entries);
211+
} else {
212+
let idx = self.by_path.remove(Pathish::new(oldpath)).unwrap();
213+
self.table[idx].path = Some(newpath.clone());
214+
self.by_path.insert(newpath, idx); // this can replace a path with a new inode
215+
}
186216
}
187217

188218
/// Remove the path->inode mapping for a given path, but keep the inode around.
@@ -236,13 +266,19 @@ impl Borrow<Pathish> for Arc<PathBuf> {
236266
}
237267
}
238268

239-
impl Hash for Pathish {
240-
fn hash<H: Hasher>(&self, state: &mut H) {
241-
self.inner.hash(state);
269+
impl Eq for Pathish {
270+
}
271+
272+
impl PartialOrd<Self> for Pathish {
273+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
274+
Some(self.cmp(other))
242275
}
243276
}
244277

245-
impl Eq for Pathish {
278+
impl Ord for Pathish {
279+
fn cmp(&self, other: &Self) -> Ordering {
280+
self.inner.cmp(&other.inner)
281+
}
246282
}
247283

248284
impl PartialEq for Pathish {
@@ -251,93 +287,130 @@ impl PartialEq for Pathish {
251287
}
252288
}
253289

254-
#[test]
255-
fn test_inode_reuse() {
256-
let mut table = InodeTable::new();
257-
let path1 = Arc::new(PathBuf::from("/foo/a"));
258-
let path2 = Arc::new(PathBuf::from("/foo/b"));
259-
260-
// Add a path.
261-
let inode1 = table.add(path1.clone()).0;
262-
assert!(inode1 != 1);
263-
assert_eq!(*path1, *table.get_path(inode1).unwrap());
264-
265-
// Add a second path; verify that the inode number is different.
266-
let inode2 = table.add(path2.clone()).0;
267-
assert!(inode2 != inode1);
268-
assert!(inode2 != 1);
269-
assert_eq!(*path2, *table.get_path(inode2).unwrap());
270-
271-
// Forget the first inode; verify that lookups on it fail.
272-
assert_eq!(0, table.forget(inode1, 1));
273-
assert!(table.get_path(inode1).is_none());
274-
275-
// Add a third path; verify that the inode is reused.
276-
let (inode3, generation3) = table.add(Arc::new(PathBuf::from("/foo/c")));
277-
assert_eq!(inode1, inode3);
278-
assert_eq!(1, generation3);
279-
280-
// Check that lookups on the third path succeed.
281-
assert_eq!(Path::new("/foo/c"), *table.get_path(inode3).unwrap());
282-
}
290+
#[cfg(test)]
291+
mod tests {
292+
use super::*;
293+
294+
#[test]
295+
fn test_inode_reuse() {
296+
let mut table = InodeTable::new();
297+
let path1 = Arc::new(PathBuf::from("/foo/a"));
298+
let path2 = Arc::new(PathBuf::from("/foo/b"));
299+
300+
// Add a path.
301+
let inode1 = table.add(path1.clone()).0;
302+
assert!(inode1 != 1);
303+
assert_eq!(*path1, *table.get_path(inode1).unwrap());
304+
305+
// Add a second path; verify that the inode number is different.
306+
let inode2 = table.add(path2.clone()).0;
307+
assert!(inode2 != inode1);
308+
assert!(inode2 != 1);
309+
assert_eq!(*path2, *table.get_path(inode2).unwrap());
310+
311+
// Forget the first inode; verify that lookups on it fail.
312+
assert_eq!(0, table.forget(inode1, 1));
313+
assert!(table.get_path(inode1).is_none());
314+
315+
// Add a third path; verify that the inode is reused.
316+
let (inode3, generation3) = table.add(Arc::new(PathBuf::from("/foo/c")));
317+
assert_eq!(inode1, inode3);
318+
assert_eq!(1, generation3);
319+
320+
// Check that lookups on the third path succeed.
321+
assert_eq!(Path::new("/foo/c"), *table.get_path(inode3).unwrap());
322+
}
283323

284-
#[test]
285-
fn test_add_or_get() {
286-
let mut table = InodeTable::new();
287-
let path1 = Arc::new(PathBuf::from("/foo/a"));
288-
let path2 = Arc::new(PathBuf::from("/foo/b"));
289-
290-
// add_or_get() a path and verify that get by inode works before lookup() is done.
291-
let inode1 = table.add_or_get(path1.clone()).0;
292-
assert_eq!(*path1, *table.get_path(inode1).unwrap());
293-
table.lookup(inode1);
294-
295-
// add() a second path and verify that get by path and inode work.
296-
let inode2 = table.add(path2.clone()).0;
297-
assert_eq!(*path2, *table.get_path(inode2).unwrap());
298-
assert_eq!(inode2, table.add_or_get(path2).0);
299-
table.lookup(inode2);
300-
301-
// Check the ref counts by doing a single forget.
302-
assert_eq!(0, table.forget(inode1, 1));
303-
assert_eq!(1, table.forget(inode2, 1));
304-
}
324+
#[test]
325+
fn test_add_or_get() {
326+
let mut table = InodeTable::new();
327+
let path1 = Arc::new(PathBuf::from("/foo/a"));
328+
let path2 = Arc::new(PathBuf::from("/foo/b"));
329+
330+
// add_or_get() a path and verify that get by inode works before lookup() is done.
331+
let inode1 = table.add_or_get(path1.clone()).0;
332+
assert_eq!(*path1, *table.get_path(inode1).unwrap());
333+
table.lookup(inode1);
334+
335+
// add() a second path and verify that get by path and inode work.
336+
let inode2 = table.add(path2.clone()).0;
337+
assert_eq!(*path2, *table.get_path(inode2).unwrap());
338+
assert_eq!(inode2, table.add_or_get(path2).0);
339+
table.lookup(inode2);
340+
341+
// Check the ref counts by doing a single forget.
342+
assert_eq!(0, table.forget(inode1, 1));
343+
assert_eq!(1, table.forget(inode2, 1));
344+
}
305345

306-
#[test]
307-
fn test_inode_rename() {
308-
let mut table = InodeTable::new();
309-
let path1 = Arc::new(PathBuf::from("/foo/a"));
310-
let path2 = Arc::new(PathBuf::from("/foo/b"));
311-
312-
// Add a path; verify that get by path and inode work.
313-
let inode = table.add(path1.clone()).0;
314-
assert_eq!(*path1, *table.get_path(inode).unwrap());
315-
assert_eq!(inode, table.get_inode(&path1).unwrap());
316-
317-
// Rename the inode; verify that get by the new path works and old path doesn't, and get by
318-
// inode still works.
319-
table.rename(&path1, path2.clone());
320-
assert!(table.get_inode(&path1).is_none());
321-
assert_eq!(inode, table.get_inode(&path2).unwrap());
322-
assert_eq!(*path2, *table.get_path(inode).unwrap());
323-
}
346+
#[test]
347+
fn test_inode_rename() {
348+
let mut table = InodeTable::new();
349+
let path1 = Arc::new(PathBuf::from("/foo/a"));
350+
let path2 = Arc::new(PathBuf::from("/foo/b"));
351+
352+
// Add a path; verify that get by path and inode work.
353+
let inode = table.add(path1.clone()).0;
354+
assert_eq!(*path1, *table.get_path(inode).unwrap());
355+
assert_eq!(inode, table.get_inode(&path1).unwrap());
356+
357+
// Rename the inode; verify that get by the new path works and old path doesn't, and get by
358+
// inode still works.
359+
table.rename(&path1, path2.clone());
360+
assert!(table.get_inode(&path1).is_none());
361+
assert_eq!(inode, table.get_inode(&path2).unwrap());
362+
assert_eq!(*path2, *table.get_path(inode).unwrap());
363+
}
364+
365+
#[test]
366+
fn test_unlink() {
367+
let mut table = InodeTable::new();
368+
let path = Arc::new(PathBuf::from("/foo/bar"));
324369

325-
#[test]
326-
fn test_unlink() {
327-
let mut table = InodeTable::new();
328-
let path = Arc::new(PathBuf::from("/foo/bar"));
370+
// Add a path.
371+
let inode = table.add(path.clone()).0;
329372

330-
// Add a path.
331-
let inode = table.add(path.clone()).0;
373+
// Unlink it and verify that get by path fails.
374+
table.unlink(&path);
375+
assert!(table.get_inode(&path).is_none());
332376

333-
// Unlink it and verify that get by path fails.
334-
table.unlink(&path);
335-
assert!(table.get_inode(&path).is_none());
377+
// Getting the path for the inode should still return the path.
378+
assert_eq!(*path, *table.get_path(inode).unwrap());
336379

337-
// Getting the path for the inode should still return the path.
338-
assert_eq!(*path, *table.get_path(inode).unwrap());
380+
// Verify that forgetting it once drops the refcount to zero and then lookups by inode fail.
381+
assert_eq!(0, table.forget(inode, 1));
382+
assert!(table.get_path(inode).is_none());
383+
}
339384

340-
// Verify that forgetting it once drops the refcount to zero and then lookups by inode fail.
341-
assert_eq!(0, table.forget(inode, 1));
342-
assert!(table.get_path(inode).is_none());
385+
#[test]
386+
fn test_rename_directory() {
387+
let mut table = InodeTable::new();
388+
let a = table.add(Arc::new(PathBuf::from("/a_file")));
389+
let d = table.add(Arc::new(PathBuf::from("/directory")));
390+
let x = table.add(Arc::new(PathBuf::from("/directory.x"))); // '.' sorts before '/' naïvely!
391+
let d_f1 = table.add(Arc::new(PathBuf::from("/directory/file1")));
392+
let d_f2 = table.add(Arc::new(PathBuf::from("/directory/file2")));
393+
let z = table.add(Arc::new(PathBuf::from("/z_file")));
394+
395+
table.rename(Path::new("/a_file"), Arc::new(PathBuf::from("/a_file_renamed")));
396+
assert_eq!(table.get_inode(Path::new("/a_file")), None);
397+
assert_eq!(table.get_inode(Path::new("/a_file_renamed")), Some(a.0));
398+
399+
table.rename(Path::new("/directory"), Arc::new(PathBuf::from("/new_directory")));
400+
401+
// paths which should be unaffected
402+
assert_eq!(table.get_inode(Path::new("/a_file_renamed")), Some(a.0));
403+
assert_eq!(table.get_inode(Path::new("/z_file")), Some(z.0));
404+
assert_eq!(table.get_inode(Path::new("/directory.x")), Some(x.0));
405+
406+
// paths which should have been renamed and return the same inode as original
407+
assert_eq!(table.get_inode(Path::new("/new_directory")), Some(d.0));
408+
assert_eq!(table.get_inode(Path::new("/new_directory/file1")), Some(d_f1.0));
409+
assert_eq!(table.get_inode(Path::new("/new_directory/file2")), Some(d_f2.0));
410+
411+
// paths which should no longer exist
412+
assert_eq!(table.get_inode(Path::new("/directory")), None);
413+
assert_eq!(table.get_inode(Path::new("/directory/file1")), None);
414+
assert_eq!(table.get_inode(Path::new("/directory/file2")), None);
415+
}
343416
}

0 commit comments

Comments
 (0)