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:
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1558,7 +1558,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mangalord"
|
||||
version = "0.128.6"
|
||||
version = "0.128.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mangalord"
|
||||
version = "0.128.6"
|
||||
version = "0.128.7"
|
||||
edition = "2021"
|
||||
default-run = "mangalord"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user