fix(vfs): V-02 — missing chunk within expected file size causes error instead of silent truncation

This commit is contained in:
2026-09-19 00:13:43 +02:00
parent 374150e5b3
commit 533764487a
2 changed files with 103 additions and 1 deletions
+98 -1
View File
@@ -228,6 +228,12 @@ impl SanctumFile {
data.zeroize();
}
let expected_chunks = if self.file_size == 0 {
0
} else {
self.file_size.div_ceil(CHUNK_SIZE as u64) as u32
};
let payload = match self.db.read_chunk(self.node_id, chunk_index).map_err(|e| {
error!("Fehler beim Lesen des Chunks #{chunk_index}: {e}");
FsError::GeneralFailure
@@ -245,7 +251,16 @@ impl SanctumFile {
error!("AEAD-Entschlüsselungsfehler bei Chunk #{chunk_index}: {e}");
FsError::GeneralFailure
})?,
None => Vec::new(),
None => {
if chunk_index < expected_chunks {
error!(
"Integritätsfehler: Fehlender Chunk #{} bei Knoten id={} (erwartete Chunks: {})",
chunk_index, self.node_id, expected_chunks
);
return Err(FsError::GeneralFailure);
}
Vec::new()
}
};
self.cached_chunk = Some((chunk_index, payload, false));
@@ -304,6 +319,13 @@ impl DavFile for SanctumFile {
let chunk_data = self.ensure_chunk_loaded(chunk_idx)?;
if offset_in_chunk >= chunk_data.len() {
if self.cursor < self.file_size {
error!(
"Integritätsfehler: Unerwartetes Chunk-Ende bei Offset {} in Chunk #{} (Dateigröße: {}, Cursor: {})",
offset_in_chunk, chunk_idx, self.file_size, self.cursor
);
return Err(FsError::GeneralFailure);
}
break;
}
@@ -314,6 +336,13 @@ impl DavFile for SanctumFile {
count -= available;
if available < to_read {
if self.cursor < self.file_size {
error!(
"Integritätsfehler: Vorzeitiges Chunk-Ende (verfügbar: {}, benötigt: {}, Dateigröße: {}, Cursor: {})",
available, to_read, self.file_size, self.cursor
);
return Err(FsError::GeneralFailure);
}
break;
}
}
@@ -1313,4 +1342,72 @@ mod tests {
let chunk = fs.db.read_chunk(node.id, 0).unwrap().unwrap();
assert!(!chunk.ciphertext.is_empty());
}
#[tokio::test]
async fn test_v02_missing_chunk_returns_error_instead_of_silent_truncation() {
let (fs, _dir) = create_test_fs(true);
let path = DavPath::new("/corrupt_test.bin").unwrap();
// 1. Datei mit 3 Chunks anlegen (3 * 1 MB = 3 MB)
let mut opts = OpenOptions::default();
opts.write = true;
opts.create_new = true;
let mut file = fs.open(&path, opts).await.unwrap();
let chunk_data = vec![0xABu8; CHUNK_SIZE];
for _ in 0..3 {
file.write_bytes(Bytes::copy_from_slice(&chunk_data))
.await
.unwrap();
}
file.flush().await.unwrap();
drop(file);
let node = fs.resolve_path("/corrupt_test.bin").unwrap().unwrap();
assert_eq!(node.size, 3 * CHUNK_SIZE as u64);
// 2. Chunk 1 direkt per SQL aus der chunks-Tabelle löschen (stille Datenkorruption)
{
let conn = fs.db.conn_for_test();
let deleted = conn
.execute(
"DELETE FROM chunks WHERE node_id = ?1 AND chunk_index = 1",
[node.id],
)
.unwrap();
assert_eq!(deleted, 1, "Chunk 1 muss gelöscht worden sein");
}
// 3. Datei im VFS zum Lesen öffnen
let mut read_opts = OpenOptions::default();
read_opts.read = true;
let mut read_file = fs.open(&path, read_opts).await.unwrap();
// Erstes Megabyte (Chunk 0) lesen -> muss klappen
let c0 = read_file.read_bytes(CHUNK_SIZE).await.unwrap();
assert_eq!(c0.len(), CHUNK_SIZE);
// Zweites Megabyte (fehlender Chunk 1) lesen -> V-02 MUSS FsError::GeneralFailure zurückgeben!
let c1_res = read_file.read_bytes(CHUNK_SIZE).await;
assert!(
matches!(c1_res, Err(FsError::GeneralFailure)),
"Lesen eines gelöschten/fehlenden Chunks muss mit GeneralFailure fehlschlagen, nicht stillschweigend gekürzt werden! Erhalten: {:?}",
c1_res
);
// 4. Test jenseits der Dateigröße bei leeren Dateien: kein Regress
let empty_path = DavPath::new("/empty.txt").unwrap();
let mut empty_opts = OpenOptions::default();
empty_opts.create_new = true;
empty_opts.write = true;
let mut empty_file = fs.open(&empty_path, empty_opts).await.unwrap();
empty_file.flush().await.unwrap();
drop(empty_file);
let mut read_empty_opts = OpenOptions::default();
read_empty_opts.read = true;
let mut read_empty = fs.open(&empty_path, read_empty_opts).await.unwrap();
let bytes = read_empty.read_bytes(100).await.unwrap();
assert!(bytes.is_empty(), "Leere Datei liefert 0 Bytes ohne Fehler");
}
}