From 253d46c7e56ece06eb334216dcdd46def317be7f Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 13 Jul 2026 19:59:51 +0200 Subject: [PATCH] fix: return NotFound when a storage key resolves to a directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get` (fs::read → EISDIR) and `get_stream` (File::open succeeds on a Unix directory, then streams garbage that fails mid-read) both mishandled a key resolving to a directory. Mirror `size`'s existing guard: `get` maps IsADirectory to NotFound; `get_stream` checks metadata.is_file() after open. Test: get_on_directory_is_not_found. Co-Authored-By: Claude Opus 4.8 --- backend/Cargo.lock | 2 +- backend/Cargo.toml | 2 +- backend/src/storage/local.rs | 29 ++++++++++++++++++++++++++++- frontend/package.json | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 44287ab..06475d3 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mangalord" -version = "0.128.6" +version = "0.128.7" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 400c712..468249d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.128.6" +version = "0.128.7" edition = "2021" default-run = "mangalord" diff --git a/backend/src/storage/local.rs b/backend/src/storage/local.rs index 04784d4..b97aa20 100644 --- a/backend/src/storage/local.rs +++ b/backend/src/storage/local.rs @@ -95,6 +95,11 @@ impl Storage for LocalStorage { match fs::read(&path).await { Ok(b) => Ok(b), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(StorageError::NotFound), + // A key resolving to a directory isn't a stored blob; `fs::read` + // fails with EISDIR. Treat it as absent, mirroring `size`. + Err(e) if e.kind() == std::io::ErrorKind::IsADirectory => { + Err(StorageError::NotFound) + } Err(e) => Err(e.into()), } } @@ -108,7 +113,14 @@ impl Storage for LocalStorage { } Err(e) => return Err(e.into()), }; - let size_bytes = file.metadata().await?.len(); + let meta = file.metadata().await?; + // Opening a directory succeeds on Unix, but it isn't a stored blob: + // its inode "size" is meaningless and ReaderStream would fail mid-read. + // Treat it as absent, mirroring `size` and `get`. + if !meta.is_file() { + return Err(StorageError::NotFound); + } + let size_bytes = meta.len(); // 64 KiB chunks: small enough that a few-MB page emits many frames // (so streaming is observable), large enough to keep syscalls cheap. let stream = ReaderStream::with_capacity(file, 64 * 1024); @@ -244,6 +256,21 @@ mod tests { assert!(matches!(s.size("adir").await, Err(StorageError::NotFound))); } + #[tokio::test] + async fn get_on_directory_is_not_found() { + // A key resolving to a directory isn't a stored blob. `fs::read` on a + // dir errors with EISDIR (not NotFound), and `File::open` on a dir + // succeeds on Unix then streams garbage — both must surface NotFound. + let dir = tempdir().unwrap(); + let s = LocalStorage::new(dir.path()); + std::fs::create_dir(dir.path().join("adir")).unwrap(); + assert!(matches!(s.get("adir").await, Err(StorageError::NotFound))); + assert!(matches!( + s.get_stream("adir").await.err(), + Some(StorageError::NotFound) + )); + } + #[tokio::test] async fn put_stream_writes_full_body_and_removes_temp_on_error() { use bytes::Bytes; diff --git a/frontend/package.json b/frontend/package.json index 7b4042f..2d46d9e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.128.6", + "version": "0.128.7", "private": true, "type": "module", "scripts": {