Skip to content

Commit 2844751

Browse files
authored
feat: filter OS junk files (.DS_Store, Thumbs.db, etc.) (#15)
## Summary - Reject creation of OS-generated junk files (`.DS_Store`, `._*`, `.Spotlight-V100`, `.Trashes`, `.fseventsd`, `__MACOSX`, `Thumbs.db`, `desktop.ini`) with `EACCES` - Guards applied in `create()`, `mkdir()`, and `rename()` (destination name) at the VirtualFs level, covering both FUSE and NFS - Enabled by default, can be disabled with `--no-filter-os-files` Closes #9
1 parent 6ef3d8a commit 2844751

4 files changed

Lines changed: 65 additions & 0 deletions

File tree

src/setup.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ pub struct Args {
101101
/// within this time regardless of ongoing writes resetting the debounce.
102102
#[arg(long, default_value_t = 30_000)]
103103
pub flush_max_batch_window_ms: u64,
104+
105+
/// Disable filtering of OS junk files (.DS_Store, Thumbs.db, etc.).
106+
/// By default these files are rejected on create/mkdir/rename.
107+
#[arg(long, default_value_t = false)]
108+
pub no_filter_os_files: bool,
104109
}
105110

106111
/// Everything needed to run a mount backend (FUSE or NFS).
@@ -261,6 +266,7 @@ pub fn setup(is_nfs: bool) -> MountSetup {
261266
args.poll_interval_secs,
262267
metadata_ttl,
263268
!args.metadata_ttl_minimal,
269+
!args.no_filter_os_files,
264270
std::time::Duration::from_millis(args.flush_debounce_ms),
265271
std::time::Duration::from_millis(args.flush_max_batch_window_ms),
266272
);

src/test_mocks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ pub fn make_test_vfs(
440440
0, // poll_interval_secs = 0 (disabled)
441441
opts.metadata_ttl,
442442
opts.serve_lookup_from_cache,
443+
true, // filter_os_files
443444
Duration::from_millis(100), // fast debounce for tests
444445
Duration::from_secs(1), // fast batch window for tests
445446
)

src/virtual_fs/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ type Invalidator = Arc<Mutex<Option<Box<dyn Fn(u64) + Send + Sync>>>>;
2929
type CommitHookTx = tokio::sync::watch::Sender<Option<Result<(), i32>>>;
3030
type CommitHookRx = tokio::sync::watch::Receiver<Option<Result<(), i32>>>;
3131

32+
/// Returns `true` for OS-generated junk files that should not be synced to remote storage.
33+
fn is_os_junk(name: &str) -> bool {
34+
matches!(
35+
name,
36+
".DS_Store" | ".Spotlight-V100" | ".Trashes" | ".fseventsd" | "__MACOSX" | "Thumbs.db" | "desktop.ini"
37+
) || name.starts_with("._")
38+
}
39+
3240
// ── VirtualFs ──────────────────────────────────────────────────────────
3341

3442
pub struct VirtualFs {
@@ -66,6 +74,8 @@ pub struct VirtualFs {
6674
/// of `last_revalidated`.
6775
/// When true, lookups within the metadata TTL window skip HEAD.
6876
serve_lookup_from_cache: bool,
77+
/// When true, reject creation of OS junk files (.DS_Store, Thumbs.db, etc.).
78+
filter_os_files: bool,
6979
}
7080

7181
/// Where to read file content from when opening read-only.
@@ -83,6 +93,7 @@ impl VirtualFs {
8393
poll_interval_secs: u64,
8494
metadata_ttl: Duration,
8595
serve_lookup_from_cache: bool,
96+
filter_os_files: bool,
8697
flush_debounce: Duration,
8798
flush_max_batch_window: Duration,
8899
) -> Arc<Self> {
@@ -146,6 +157,7 @@ impl VirtualFs {
146157
invalidator,
147158
metadata_ttl,
148159
serve_lookup_from_cache,
160+
filter_os_files,
149161
});
150162

151163
// Set root inode mtime (repos use the last commit date).
@@ -1799,6 +1811,10 @@ impl VirtualFs {
17991811
if self.read_only {
18001812
return Err(libc::EROFS);
18011813
}
1814+
if self.filter_os_files && is_os_junk(name) {
1815+
debug!("create: rejecting OS junk file: {}", name);
1816+
return Err(libc::EACCES);
1817+
}
18021818

18031819
debug!("create: parent={}, name={}", parent, name);
18041820

@@ -1907,6 +1923,10 @@ impl VirtualFs {
19071923
if self.read_only {
19081924
return Err(libc::EROFS);
19091925
}
1926+
if self.filter_os_files && is_os_junk(name) {
1927+
debug!("mkdir: rejecting OS junk directory: {}", name);
1928+
return Err(libc::EACCES);
1929+
}
19101930

19111931
debug!("mkdir: parent={}, name={}", parent, name);
19121932

@@ -2052,6 +2072,10 @@ impl VirtualFs {
20522072
if self.read_only {
20532073
return Err(libc::EROFS);
20542074
}
2075+
if self.filter_os_files && is_os_junk(newname) {
2076+
debug!("rename: rejecting rename to OS junk name: {}", newname);
2077+
return Err(libc::EACCES);
2078+
}
20552079

20562080
debug!(
20572081
"rename: parent={}, name={}, newparent={}, newname={}",

src/virtual_fs/tests.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,40 @@ fn concurrent_create_eexist() {
647647
});
648648
}
649649

650+
// ── OS junk file filtering ──────────────────────────────────────────
651+
652+
/// create/mkdir/rename reject OS junk file names with EACCES.
653+
#[test]
654+
fn os_junk_files_rejected() {
655+
let hub = MockHub::new();
656+
hub.add_file("legit.txt", 10, Some("h"), None);
657+
let xet = MockXet::new();
658+
let (rt, vfs) = vfs_simple(&hub, &xet);
659+
660+
rt.block_on(async {
661+
for name in [".DS_Store", "._metadata", "Thumbs.db", "desktop.ini", "__MACOSX"] {
662+
assert_eq!(
663+
vfs.create(ROOT_INODE, name, Some(1)).await.unwrap_err(),
664+
libc::EACCES,
665+
"create({name})"
666+
);
667+
assert_eq!(
668+
vfs.mkdir(ROOT_INODE, name).await.unwrap_err(),
669+
libc::EACCES,
670+
"mkdir({name})"
671+
);
672+
}
673+
// rename to a junk destination name is also blocked
674+
let _ = vfs.lookup(ROOT_INODE, "legit.txt").await.unwrap();
675+
assert_eq!(
676+
vfs.rename(ROOT_INODE, "legit.txt", ROOT_INODE, ".DS_Store", false)
677+
.await
678+
.unwrap_err(),
679+
libc::EACCES
680+
);
681+
});
682+
}
683+
650684
// ── Lookup / revalidation / poll ────────────────────────────────────
651685

652686
/// HEAD failure is silently ignored (graceful degradation, cached data served).

0 commit comments

Comments
 (0)