RyanCodrai/turbovec · error · io::Error

truncated file

Error message

truncated file

What it means

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.

Source

Thrown at turbovec/src/io.rs:629






#[cfg(unix)]
pub(crate) fn read_exact_at(f: &File, buf: &mut [u8], off: u64) -> io::Result<()> {
    use std::os::unix::fs::FileExt;
    f.read_exact_at(buf, off)
}

#[cfg(windows)]
pub(crate) fn read_exact_at(f: &File, mut buf: &mut [u8], mut off: u64) -> io::Result<()> {
    use std::os::windows::fs::FileExt;
    while !buf.is_empty() {
        let n = f.seek_read(buf, off)?;
        if n == 0 {
            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "truncated file"));
        }
        buf = &mut buf[n..];
        off += n as u64;
    }
    Ok(())
}



#[cfg(test)]
mod rename_retry_tests {
    use super::*;

    // #415. The retry existed but whitelisted only ERROR_SHARING_VIOLATION
    // (32), so the ERROR_ACCESS_DENIED (5) that a delete-pending
    // destination returns went straight to the caller as a failed save.
    // The retry loop itself is `cfg(windows)` and cannot run here; the
    // decision table it consults is not, so the part that was actually

View on GitHub (pinned to ccab9f325e)

Solutions

  1. Restore the index from a backup or re-export it from the source vectors
  2. Check the file size against the expected header/declared length before loading
  3. Delete the incomplete file and regenerate the index
  4. Use the v7 fallback-to-previous-generation load if an earlier complete commit exists

Example fix

// before
let hdr = read_exact_at(&f, &mut buf, 0)?; // Err: truncated file
// after
let len = f.metadata()?.len();
if len < HEADER_SIZE as u64 {
    eprintln!("index file too small ({len} bytes), regenerating");
    let idx = rebuild_index()?;
} else {
    let hdr = read_exact_at(&f, &mut buf, 0)?;
}
Defensive patterns

Strategy: try-catch

Validate before calling

let len = std::fs::metadata(path)?.len();
if len < MIN_INDEX_SIZE {
    return Err(anyhow!("index file only {len} bytes; regenerate"));
}

Type guard

fn plausibly_complete(path: &Path, min_len: u64) -> bool {
    std::fs::metadata(path).map(|m| m.len() >= min_len).unwrap_or(false)
}

Try / catch

match Index::load(path) {
    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
        eprintln!("{path:?} truncated; restoring from backup");
        restore_backup(path)?;
        Index::load(path)?
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: Interrupted save left a partial file; another process truncated the index; disk-full during commit; copying the file while it was being written.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of RyanCodrai/turbovec@ccab9f325e (2026-09-06). Data as JSON: /api/errors/d521c0164c81b150. Report an issue: GitHub.