{"record":{"id":"d521c0164c81b150","repo":"RyanCodrai/turbovec","slug":"truncated-file","errorCode":null,"errorMessage":"truncated file","messagePattern":"truncated file","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"turbovec/src/io.rs","lineNumber":629,"sourceCode":"\n\n\n\n\n\n#[cfg(unix)]\npub(crate) fn read_exact_at(f: &File, buf: &mut [u8], off: u64) -> io::Result<()> {\n    use std::os::unix::fs::FileExt;\n    f.read_exact_at(buf, off)\n}\n\n#[cfg(windows)]\npub(crate) fn read_exact_at(f: &File, mut buf: &mut [u8], mut off: u64) -> io::Result<()> {\n    use std::os::windows::fs::FileExt;\n    while !buf.is_empty() {\n        let n = f.seek_read(buf, off)?;\n        if n == 0 {\n            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, \"truncated file\"));\n        }\n        buf = &mut buf[n..];\n        off += n as u64;\n    }\n    Ok(())\n}\n\n\n\n#[cfg(test)]\nmod rename_retry_tests {\n    use super::*;\n\n    // #415. The retry existed but whitelisted only ERROR_SHARING_VIOLATION\n    // (32), so the ERROR_ACCESS_DENIED (5) that a delete-pending\n    // destination returns went straight to the caller as a failed save.\n    // The retry loop itself is `cfg(windows)` and cannot run here; the\n    // decision table it consults is not, so the part that was actually","sourceCodeStart":611,"sourceCodeEnd":647,"githubUrl":"https://github.com/RyanCodrai/turbovec/blob/ccab9f325e6ce2a270a87daf01ae4e443bcf2d49/turbovec/src/io.rs#L611-L647","documentation":"read_exact_at (Windows path) loops on FileExt::seek_read until the caller's buffer is filled; if the file returns 0 bytes before the buffer is full, it raises UnexpectedEof with 'truncated file'. This means the file ended before the expected bytes at the requested offset could be read — the on-disk file is shorter than the index structures expect.","triggerScenarios":"Any pread-style read (v7 header, commit record, vector blocks) whose target region extends past the physical end of the file, e.g. reading a header from a 0-byte or partially written file.","commonSituations":"Interrupted save left a partial file; another process truncated the index; disk-full during commit; copying the file while it was being written.","solutions":["Restore the index from a backup or re-export it from the source vectors","Check the file size against the expected header/declared length before loading","Delete the incomplete file and regenerate the index","Use the v7 fallback-to-previous-generation load if an earlier complete commit exists"],"exampleFix":"// before\nlet hdr = read_exact_at(&f, &mut buf, 0)?; // Err: truncated file\n// after\nlet len = f.metadata()?.len();\nif len < HEADER_SIZE as u64 {\n    eprintln!(\"index file too small ({len} bytes), regenerating\");\n    let idx = rebuild_index()?;\n} else {\n    let hdr = read_exact_at(&f, &mut buf, 0)?;\n}","handlingStrategy":"try-catch","validationCode":"let len = std::fs::metadata(path)?.len();\nif len < MIN_INDEX_SIZE {\n    return Err(anyhow!(\"index file only {len} bytes; regenerate\"));\n}","typeGuard":"fn plausibly_complete(path: &Path, min_len: u64) -> bool {\n    std::fs::metadata(path).map(|m| m.len() >= min_len).unwrap_or(false)\n}","tryCatchPattern":"match Index::load(path) {\n    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {\n        eprintln!(\"{path:?} truncated; restoring from backup\");\n        restore_backup(path)?;\n        Index::load(path)?\n    }\n    other => other?,\n}","preventionTips":["Write index files atomically (temp file + rename)","Avoid killing writers mid-save; use graceful shutdown","Check file sizes against expected minimums before loading"],"tags":["io","truncated-file","unexpected-eof"],"backgroundTag":"file-read-failed","analyzedSha":"ccab9f325e6ce2a270a87daf01ae4e443bcf2d49","analyzedAt":"2026-09-06T08:39:18.516Z","contentChangedAt":"2026-09-06T08:39:18.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}