Preview disk cache: ignore symlinked shard directories (#1504)

This commit is contained in:
MarekSpodBiedry committed 2026-10-13 16:36:39 +02:00
1 parent b3d392b499
commit 935fabeb18
3 files changed
+45 -2

No files matched your search

+12 -2
View File
@@ -9,6 +9,7 @@
//! (`<2 hex>/<32 hex>.jpg` and the `<32 hex>.tmp…` temp files of [`DiskCache::put`], see
//! [`is_cache_file`]): the directory may be shared with other files (e.g. a library opened on a
//! photo folder that already has a `thumbs/` folder), and those are never touched (issue #98).
//! A shard directory that is a symlink or junction is not followed (issue #1504).
use std::path::{Path, PathBuf};
use std::sync::Mutex;
@@ -65,7 +66,7 @@ impl DiskCache {
pub fn get(&self, key: Hash128) -> Option<Rgba8> {
let p = self.path(key);
let Ok(bytes) = std::fs::read(&p) else {
let Some(bytes) = p.parent().filter(|d| is_real_dir(d)).and_then(|_| std::fs::read(&p).ok()) else {
self.misses.fetch_add(1, Ordering::Relaxed);
return None;
};
@@ -91,7 +92,7 @@ impl DiskCache {
let Some(bytes) = encode_jpeg(img) else { return };
let p = self.path(key);
let Some(parent) = p.parent() else { return };
if std::fs::create_dir_all(parent).is_err() {
if std::fs::create_dir_all(parent).is_err() || !is_real_dir(parent) {
return;
}
// Serialize the file replacement and its size accounting. A repeated render of
@@ -124,6 +125,10 @@ impl DiskCache {
if !is_shard_name(sub_name) {
continue;
}
// a shard that is a symlink (or junction) leads outside the cache: never walk it
if !is_real_dir(&sub.path()) {
continue;
}
let Ok(files) = std::fs::read_dir(sub.path()) else { continue };
for f in files.flatten() {
let name = f.file_name();
@@ -173,6 +178,11 @@ impl DiskCache {
}
}
/// Whether `p` is a directory itself, not a symlink or Windows reparse point to one.
fn is_real_dir(p: &Path) -> bool {
std::fs::symlink_metadata(p).is_ok_and(|m| m.is_dir())
}
fn is_lower_hex(s: &str) -> bool {
s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
+27
View File
@@ -380,6 +380,33 @@ fn disk_cache_never_touches_foreign_files() {
let _ = std::fs::remove_dir_all(&dir);
}
/// A shard directory that is a symlink to a folder outside the cache is neither listed, cleared,
/// pruned, read nor written (issue #1504).
#[cfg(unix)]
#[test]
fn disk_cache_ignores_symlinked_shard_directories() {
let dir = temp_dir("symlink-shard");
let outside = temp_dir("symlink-outside");
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(&outside).unwrap();
let key = hash_bytes(&[9, 9]);
let hex = key.to_string();
let sentinel = outside.join(format!("{hex}.jpg"));
std::fs::write(&sentinel, vec![7u8; 50_000]).unwrap();
std::os::unix::fs::symlink(&outside, dir.join(&hex[..2])).unwrap();
let d = DiskCache::new(&dir, 10);
assert_eq!(d.size(), 0, "files behind the link are not the cache's");
d.prune();
assert!(sentinel.exists(), "prune followed the link");
assert!(d.get(key).is_none(), "get read through the link");
d.put(key, &gradient(16, 16, 1));
assert_eq!(std::fs::metadata(&sentinel).unwrap().len(), 50_000, "put wrote through the link");
d.clear();
assert!(sentinel.exists(), "clear followed the link");
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_dir_all(&outside);
}
/// [`JobPool::wait`] returns once a job finishes, which [`JobPool::try_recv`] then hands out, or
/// after the timeout.
#[test]
+6
View File
@@ -2,6 +2,12 @@
## 2026-10-13
### Thumbnail cache stays in its folder
- The thumbnail cache no longer follows a shard folder (`thumbs/ab/`) that is a symlink or junction to somewhere
else (issue #1504): trimming or clearing the cache can't delete files outside it, and thumbnails are never read
from or written through such a link.
## 2026-10-13
### Slider drags on large raws
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's