Skip to content

Commit 092203d

Browse files
committed
Downloads watcher M2a: Pure-Rust primitives
- `downloads::filter::is_eligible` rejects hidden / partial-suffix / non-file paths. - `downloads::IgnoreSet` holds Cmdr-own-write paths with per-entry TTLs, FIFO-bounded at 1000 entries, scoped to the resolved Downloads root. - `downloads::LatestRing` keeps the last 10 observed downloads in insertion order with re-push moving to the back. - 22 unit tests colocated in each module. No `notify`, no Tauri — M2b wires those in. - `mod downloads;` in `lib.rs` carries an `#[allow(dead_code, reason = …)]` until M2b's watcher becomes the first caller.
1 parent 65c1089 commit 092203d

5 files changed

Lines changed: 621 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//! Eligibility filter for downloads-watcher events.
2+
//!
3+
//! Decides whether a path observed under the watched root looks like a "real"
4+
//! completed download we should surface. The watcher (M2b) passes only paths
5+
//! that are already under the resolved Downloads root, so this function
6+
//! doesn't re-check that boundary.
7+
8+
use std::fs;
9+
use std::path::{Component, Path};
10+
11+
/// Partial-download filename suffixes browsers emit while a transfer is
12+
/// in flight. Case-sensitive: browsers always lowercase these.
13+
const PARTIAL_SUFFIXES: &[&str] = &[".crdownload", ".part", ".download"];
14+
15+
/// Is `path` an eligible download to surface?
16+
///
17+
/// Returns `false` when any of:
18+
///
19+
/// - Any path component (basename or any ancestor) starts with `.` (hidden).
20+
/// - The filename ends with a partial-download suffix
21+
/// (`.crdownload`, `.part`, `.download`), case-sensitive.
22+
/// - `path` refers to a directory.
23+
/// - `path` is a broken symlink (errors swallowed, no propagation).
24+
///
25+
/// Returns `true` for regular files and symlinks resolving to a regular file.
26+
pub fn is_eligible(path: &Path) -> bool {
27+
if has_hidden_component(path) {
28+
return false;
29+
}
30+
if has_partial_suffix(path) {
31+
return false;
32+
}
33+
// `fs::metadata` follows symlinks, so a symlink to a regular file is
34+
// treated as a regular file, and a broken symlink errors out and we
35+
// return `false` without propagating.
36+
match fs::metadata(path) {
37+
Ok(meta) => meta.is_file(),
38+
Err(_) => false,
39+
}
40+
}
41+
42+
fn has_hidden_component(path: &Path) -> bool {
43+
path.components().any(|c| match c {
44+
Component::Normal(s) => s.to_str().is_some_and(|s| s.starts_with('.')),
45+
_ => false,
46+
})
47+
}
48+
49+
fn has_partial_suffix(path: &Path) -> bool {
50+
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
51+
return false;
52+
};
53+
PARTIAL_SUFFIXES.iter().any(|suf| name.ends_with(suf))
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use super::*;
59+
60+
use std::os::unix::fs::symlink;
61+
use std::path::PathBuf;
62+
63+
use tempfile::TempDir;
64+
65+
/// `tempfile::TempDir::new` on macOS creates a `.tmpXXXXX`-named
66+
/// directory inside `$TMPDIR` (the leading dot hides it in Finder).
67+
/// That dot trips our hidden-component check on the full path and
68+
/// shadows every other assertion in this file. Use a non-hidden
69+
/// prefix instead so positive-path assertions actually exercise the
70+
/// codepath we care about.
71+
fn unhidden_tempdir() -> TempDir {
72+
tempfile::Builder::new()
73+
.prefix("cmdr-downloads-test-")
74+
.tempdir()
75+
.unwrap()
76+
}
77+
78+
fn touch(dir: &Path, name: &str) -> PathBuf {
79+
let p = dir.join(name);
80+
fs::write(&p, b"hi").unwrap();
81+
p
82+
}
83+
84+
#[test]
85+
fn rejects_hidden_basename() {
86+
let td = unhidden_tempdir();
87+
let root = td.path();
88+
let p = touch(root, ".DS_Store");
89+
assert!(!is_eligible(&p));
90+
}
91+
92+
#[test]
93+
fn rejects_hidden_ancestor() {
94+
let td = unhidden_tempdir();
95+
let root = td.path();
96+
let sub = root.join(".tmp");
97+
fs::create_dir(&sub).unwrap();
98+
let p = touch(&sub, "foo.zip");
99+
assert!(!is_eligible(&p));
100+
}
101+
102+
#[test]
103+
fn rejects_partial_suffixes() {
104+
let td = unhidden_tempdir();
105+
let root = td.path();
106+
for name in ["foo.crdownload", "bar.part", "baz.download"] {
107+
let p = touch(root, name);
108+
assert!(!is_eligible(&p), "expected {name} to be ineligible");
109+
}
110+
}
111+
112+
#[test]
113+
fn accepts_regular_file() {
114+
let td = unhidden_tempdir();
115+
let root = td.path();
116+
let p = touch(root, "foo.zip");
117+
assert!(is_eligible(&p));
118+
}
119+
120+
#[test]
121+
fn rejects_directory() {
122+
let td = unhidden_tempdir();
123+
let root = td.path();
124+
let sub = root.join("subdir");
125+
fs::create_dir(&sub).unwrap();
126+
assert!(!is_eligible(&sub));
127+
}
128+
129+
#[test]
130+
fn accepts_partial_looking_subname() {
131+
// Only literal trailing suffixes match. A file named
132+
// `foo.crdownload.zip` is final (the partial suffix is in the middle,
133+
// not at the end).
134+
let td = unhidden_tempdir();
135+
let root = td.path();
136+
let p = touch(root, "foo.crdownload.zip");
137+
assert!(is_eligible(&p));
138+
}
139+
140+
#[test]
141+
fn broken_symlink_returns_false_without_panic() {
142+
let td = unhidden_tempdir();
143+
let root = td.path();
144+
let link = root.join("dangling");
145+
symlink(root.join("does-not-exist"), &link).unwrap();
146+
assert!(!is_eligible(&link));
147+
}
148+
149+
#[test]
150+
fn case_sensitive_partial_suffix_check() {
151+
// Browsers emit lowercase only. Anything else is treated as a real
152+
// download (the user named it themselves).
153+
let td = unhidden_tempdir();
154+
let root = td.path();
155+
let p = touch(root, "foo.CRDOWNLOAD");
156+
assert!(is_eligible(&p));
157+
}
158+
}

0 commit comments

Comments
 (0)