Skip to content

Commit 936fe91

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

2 files changed

Lines changed: 175 additions & 95 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: 171 additions & 95 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::btree_map::Entry::*;
9+
use std::collections::{BTreeMap, VecDeque};
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,42 @@ 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+
// Look for children of the path being renamed and fix them up too.
184+
// Note that we use range() to find the bounds of the map first, and only then use
185+
// extract_if to remove that range, because:
186+
// 1. extract_if does not provide a way to stop iterating early
187+
// 2. extract_if does not utilize Borrow in its range, so here we have to clone the
188+
// start and end paths because the range type must match the key type exactly.
189+
if let Some((last_child, _)) = self
190+
.by_path
191+
.range::<Pathish, _>((Excluded(Pathish::new(oldpath)), Unbounded))
192+
.take_while(|(path, _)| path.starts_with(oldpath))
193+
.last()
194+
{
195+
let mut new_entries = vec![];
196+
for (path, idx) in self.by_path.extract_if(
197+
(
198+
Included(Arc::new(oldpath.to_owned())),
199+
Included(Arc::clone(last_child)),
200+
),
201+
|_, _| true,
202+
) {
203+
let suffix = path.strip_prefix(oldpath).unwrap();
204+
let new_entry_path = if suffix.as_os_str().is_empty() {
205+
// this is the entry for parent path itself
206+
Arc::clone(&newpath)
207+
} else {
208+
Arc::new(newpath.as_path().join(suffix))
209+
};
210+
self.table[idx].path = Some(Arc::clone(&new_entry_path));
211+
new_entries.push((new_entry_path, idx));
212+
}
213+
self.by_path.extend(new_entries);
214+
} else {
215+
let idx = self.by_path.remove(Pathish::new(oldpath)).unwrap();
216+
self.table[idx].path = Some(Arc::clone(&newpath));
217+
self.by_path.insert(newpath, idx); // this can replace a path with a new inode
218+
}
186219
}
187220

188221
/// Remove the path->inode mapping for a given path, but keep the inode around.
@@ -236,9 +269,15 @@ impl Borrow<Pathish> for Arc<PathBuf> {
236269
}
237270
}
238271

239-
impl Hash for Pathish {
240-
fn hash<H: Hasher>(&self, state: &mut H) {
241-
self.inner.hash(state);
272+
impl PartialOrd<Self> for Pathish {
273+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
274+
Some(self.cmp(other))
275+
}
276+
}
277+
278+
impl Ord for Pathish {
279+
fn cmp(&self, other: &Self) -> Ordering {
280+
self.inner.cmp(&other.inner)
242281
}
243282
}
244283

@@ -251,93 +290,130 @@ impl PartialEq for Pathish {
251290
}
252291
}
253292

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-
}
293+
#[cfg(test)]
294+
mod tests {
295+
use super::*;
296+
297+
#[test]
298+
fn test_inode_reuse() {
299+
let mut table = InodeTable::new();
300+
let path1 = Arc::new(PathBuf::from("/foo/a"));
301+
let path2 = Arc::new(PathBuf::from("/foo/b"));
302+
303+
// Add a path.
304+
let inode1 = table.add(path1.clone()).0;
305+
assert!(inode1 != 1);
306+
assert_eq!(*path1, *table.get_path(inode1).unwrap());
307+
308+
// Add a second path; verify that the inode number is different.
309+
let inode2 = table.add(path2.clone()).0;
310+
assert!(inode2 != inode1);
311+
assert!(inode2 != 1);
312+
assert_eq!(*path2, *table.get_path(inode2).unwrap());
313+
314+
// Forget the first inode; verify that lookups on it fail.
315+
assert_eq!(0, table.forget(inode1, 1));
316+
assert!(table.get_path(inode1).is_none());
317+
318+
// Add a third path; verify that the inode is reused.
319+
let (inode3, generation3) = table.add(Arc::new(PathBuf::from("/foo/c")));
320+
assert_eq!(inode1, inode3);
321+
assert_eq!(1, generation3);
322+
323+
// Check that lookups on the third path succeed.
324+
assert_eq!(Path::new("/foo/c"), *table.get_path(inode3).unwrap());
325+
}
283326

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-
}
327+
#[test]
328+
fn test_add_or_get() {
329+
let mut table = InodeTable::new();
330+
let path1 = Arc::new(PathBuf::from("/foo/a"));
331+
let path2 = Arc::new(PathBuf::from("/foo/b"));
332+
333+
// add_or_get() a path and verify that get by inode works before lookup() is done.
334+
let inode1 = table.add_or_get(path1.clone()).0;
335+
assert_eq!(*path1, *table.get_path(inode1).unwrap());
336+
table.lookup(inode1);
337+
338+
// add() a second path and verify that get by path and inode work.
339+
let inode2 = table.add(path2.clone()).0;
340+
assert_eq!(*path2, *table.get_path(inode2).unwrap());
341+
assert_eq!(inode2, table.add_or_get(path2).0);
342+
table.lookup(inode2);
343+
344+
// Check the ref counts by doing a single forget.
345+
assert_eq!(0, table.forget(inode1, 1));
346+
assert_eq!(1, table.forget(inode2, 1));
347+
}
305348

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-
}
349+
#[test]
350+
fn test_inode_rename() {
351+
let mut table = InodeTable::new();
352+
let path1 = Arc::new(PathBuf::from("/foo/a"));
353+
let path2 = Arc::new(PathBuf::from("/foo/b"));
354+
355+
// Add a path; verify that get by path and inode work.
356+
let inode = table.add(path1.clone()).0;
357+
assert_eq!(*path1, *table.get_path(inode).unwrap());
358+
assert_eq!(inode, table.get_inode(&path1).unwrap());
359+
360+
// Rename the inode; verify that get by the new path works and old path doesn't, and get by
361+
// inode still works.
362+
table.rename(&path1, path2.clone());
363+
assert!(table.get_inode(&path1).is_none());
364+
assert_eq!(inode, table.get_inode(&path2).unwrap());
365+
assert_eq!(*path2, *table.get_path(inode).unwrap());
366+
}
324367

325-
#[test]
326-
fn test_unlink() {
327-
let mut table = InodeTable::new();
328-
let path = Arc::new(PathBuf::from("/foo/bar"));
368+
#[test]
369+
fn test_unlink() {
370+
let mut table = InodeTable::new();
371+
let path = Arc::new(PathBuf::from("/foo/bar"));
329372

330-
// Add a path.
331-
let inode = table.add(path.clone()).0;
373+
// Add a path.
374+
let inode = table.add(path.clone()).0;
332375

333-
// Unlink it and verify that get by path fails.
334-
table.unlink(&path);
335-
assert!(table.get_inode(&path).is_none());
376+
// Unlink it and verify that get by path fails.
377+
table.unlink(&path);
378+
assert!(table.get_inode(&path).is_none());
336379

337-
// Getting the path for the inode should still return the path.
338-
assert_eq!(*path, *table.get_path(inode).unwrap());
380+
// Getting the path for the inode should still return the path.
381+
assert_eq!(*path, *table.get_path(inode).unwrap());
339382

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

0 commit comments

Comments
 (0)