fix(vfs): V-06 — sparse writes zero-fill gaps between previous end of file and seek offset

This commit is contained in:
2026-09-19 09:19:56 +02:00
parent 17ac4f9358
commit e300a764c9
2 changed files with 108 additions and 0 deletions
+28
View File
@@ -1057,6 +1057,34 @@ impl DavFile for CarrierFile {
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
self.touch();
Box::pin(async move {
// V-06: Sparse Writes — Lücke zwischen bisherigem Dateiende und Cursor mit Nullen füllen
if self.cursor > self.file_size {
let target = self.cursor;
while self.file_size < target {
let block_idx = (self.file_size / CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
let offset_in_block = (self.file_size % CARRIER_BLOCK_PAYLOAD_SIZE as u64) as usize;
let space_in_block = CARRIER_BLOCK_PAYLOAD_SIZE - offset_in_block;
let to_pad = ((target - self.file_size) as usize).min(space_in_block);
let block_data = self.ensure_block_loaded(block_idx)?;
if block_data.len() < offset_in_block + to_pad {
block_data.resize(offset_in_block + to_pad, 0);
}
let mut is_full = false;
if let Some((_, ref d, ref mut dirty)) = self.cached_block {
*dirty = true;
is_full = d.len() >= CARRIER_BLOCK_PAYLOAD_SIZE;
}
self.file_size += to_pad as u64;
if is_full {
self.flush_cached_block()?;
}
}
}
let mut src = &buf[..];
while !src.is_empty() {
+80
View File
@@ -396,6 +396,34 @@ impl DavFile for SanctumFile {
fn write_bytes(&mut self, buf: Bytes) -> FsFuture<'_, ()> {
self.touch();
Box::pin(async move {
// V-06: Sparse Writes — Lücke zwischen bisherigem Dateiende und Cursor mit Nullen füllen
if self.cursor > self.file_size {
let target = self.cursor;
while self.file_size < target {
let chunk_idx = (self.file_size / CHUNK_SIZE as u64) as u32;
let offset_in_chunk = (self.file_size % CHUNK_SIZE as u64) as usize;
let space_in_chunk = CHUNK_SIZE - offset_in_chunk;
let to_pad = ((target - self.file_size) as usize).min(space_in_chunk);
let chunk_data = self.ensure_chunk_loaded(chunk_idx)?;
if chunk_data.len() < offset_in_chunk + to_pad {
chunk_data.resize(offset_in_chunk + to_pad, 0);
}
let mut is_full = false;
if let Some((_, ref d, ref mut dirty)) = self.cached_chunk {
*dirty = true;
is_full = d.len() >= CHUNK_SIZE;
}
self.file_size += to_pad as u64;
if is_full {
self.flush_cached_chunk_and_size()?;
}
}
}
let mut src = &buf[..];
while !src.is_empty() {
@@ -1488,4 +1516,56 @@ mod tests {
let read_bytes = read_file.read_bytes(test_data.len()).await.unwrap();
assert_eq!(&read_bytes[..], test_data);
}
#[tokio::test]
async fn test_v06_sparse_write_zero_fills_gap() {
use std::io::SeekFrom;
let (fs, _dir) = create_test_fs(true);
let path = DavPath::new("/sparse_test.bin").unwrap();
let mut opts = OpenOptions::default();
opts.write = true;
opts.create_new = true;
let mut file = fs.open(&path, opts).await.unwrap();
// 1. Schreibe 5 Bytes am Anfang (Chunk 0)
file.write_bytes(Bytes::copy_from_slice(b"START"))
.await
.unwrap();
// 2. Springe weit über das Dateiende hinaus in Chunk 2 (2,5 MB)
let seek_pos = 2 * CHUNK_SIZE as u64 + 500;
file.seek(SeekFrom::Start(seek_pos)).await.unwrap();
// 3. Schreibe 3 Bytes an Position seek_pos
file.write_bytes(Bytes::copy_from_slice(b"END"))
.await
.unwrap();
file.flush().await.unwrap();
drop(file);
// 4. Verifiziere Dateigröße
let node = fs.resolve_path("/sparse_test.bin").unwrap().unwrap();
assert_eq!(node.size, seek_pos + 3);
// 5. Lese Datei vollständig ein und prüfe Nullen in der Lücke
let mut read_opts = OpenOptions::default();
read_opts.read = true;
let mut read_file = fs.open(&path, read_opts).await.unwrap();
// Anfang lesen
let start_bytes = read_file.read_bytes(5).await.unwrap();
assert_eq!(&start_bytes[..], b"START");
// Lücke in Chunk 0 und 1 muss mit Nullen gefüllt sein
let zero_gap = read_file.read_bytes(1000).await.unwrap();
assert_eq!(zero_gap.len(), 1000);
assert!(zero_gap.iter().all(|&b| b == 0));
// Springe zum Ende und lies die geschriebenen Daten
read_file.seek(SeekFrom::Start(seek_pos)).await.unwrap();
let end_bytes = read_file.read_bytes(3).await.unwrap();
assert_eq!(&end_bytes[..], b"END");
}
}