fix: return NotFound when a storage key resolves to a directory

`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 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-13 19:59:51 +02:00
parent c570e0cc37
commit 253d46c7e5
4 changed files with 31 additions and 4 deletions

2
backend/Cargo.lock generated
View File

@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mangalord" name = "mangalord"
version = "0.128.6" version = "0.128.7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "mangalord" name = "mangalord"
version = "0.128.6" version = "0.128.7"
edition = "2021" edition = "2021"
default-run = "mangalord" default-run = "mangalord"

View File

@@ -95,6 +95,11 @@ impl Storage for LocalStorage {
match fs::read(&path).await { match fs::read(&path).await {
Ok(b) => Ok(b), Ok(b) => Ok(b),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(StorageError::NotFound), 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()), Err(e) => Err(e.into()),
} }
} }
@@ -108,7 +113,14 @@ impl Storage for LocalStorage {
} }
Err(e) => return Err(e.into()), 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 // 64 KiB chunks: small enough that a few-MB page emits many frames
// (so streaming is observable), large enough to keep syscalls cheap. // (so streaming is observable), large enough to keep syscalls cheap.
let stream = ReaderStream::with_capacity(file, 64 * 1024); let stream = ReaderStream::with_capacity(file, 64 * 1024);
@@ -244,6 +256,21 @@ mod tests {
assert!(matches!(s.size("adir").await, Err(StorageError::NotFound))); 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] #[tokio::test]
async fn put_stream_writes_full_body_and_removes_temp_on_error() { async fn put_stream_writes_full_body_and_removes_temp_on_error() {
use bytes::Bytes; use bytes::Bytes;

View File

@@ -1,6 +1,6 @@
{ {
"name": "mangalord-frontend", "name": "mangalord-frontend",
"version": "0.128.6", "version": "0.128.7",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {