astrid-runtime/astrid · error · io::Error

positional read reached end of file

Error message

positional read reached end of file

What it means

read_exact_at performs positional (pread-style) reads in a loop until the requested buffer is filled. If positioned_read returns 0 bytes — meaning the file ends before the requested range — it raises UnexpectedEof with this message. Callers include visit_indexed_objects and read_frame_at, which both expect fully readable frames at known offsets.

Solutions

  1. Verify the file's integrity/rebuild the index — the offset likely points past EOF after truncation
  2. Check available file size before reading: ensure file_len >= offset + buffer_len
  3. Restore the durable file from backup or snapshot if it was truncated by a crash
  4. Re-scan/rebuild visit_indexed_objects against the current file so offsets match reality

Example fix

// before
let frame = reader.read_frame_at(&mut file, offset, len)?;
// after
let file_len = file.metadata()?.len();
if offset + len as u64 > file_len {
    return Err(StorageError::TruncatedFile { offset });
}
let frame = reader.read_frame_at(&mut file, offset, len)?;
Defensive patterns

Strategy: validation

Validate before calling

fn can_read_at(file: &File, offset: u64, len: u64) -> io::Result<bool> {
    Ok(file.metadata()?.len() >= offset.saturating_add(len))
}
if !can_read_at(&file, offset, buf_len)? {
    return Err(StorageError::TruncatedFile { offset });
}

Try / catch

match result {
    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof
        && e.to_string() == "positional read reached end of file" => {
        eprintln!("durable file truncated or index stale at offset {offset}; rebuild index or restore backup");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Reading a frame or indexed object at a recorded offset where the backing file is shorter than offset+length: truncated/corrupted durable log, index entries pointing past EOF, or concurrent truncation of the storage file.

Common situations: Durable storage file truncated by a crash or disk-full event; stale index built against a longer earlier version of the file; reading a file copied incompletely; offset overflow bugs leading to reads at bogus positions (though those get the separate overflow error).

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/db28e29eccd7c038. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-storage/src/engine/durable/format/mod.rs:207

        .try_into()
        .map_err(|_| DurableError::EncodingOverflow)?;
    if frame_checksum(magic, payload_len, &payload) != checksum {
        return Err(corrupt(file_name, offset, "frame checksum mismatch"));
    }
    Ok(payload)
}

fn read_exact_at<F: DurableIo>(file: &F, buffer: &mut [u8], offset: u64) -> io::Result<()> {
    let mut filled = 0_usize;
    while filled != buffer.len() {
        let relative = u64::try_from(filled)
            .map_err(|_| io::Error::other("positional read offset overflow"))?;
        let position = offset
            .checked_add(relative)
            .ok_or_else(|| io::Error::other("positional read offset overflow"))?;
        let read = positioned_read(file, &mut buffer[filled..], position)?;
        if read == 0 {
            return Err(io::Error::new(
                io::ErrorKind::UnexpectedEof,
                "positional read reached end of file",
            ));
        }
        filled = filled
            .checked_add(read)
            .ok_or_else(|| io::Error::other("positional read length overflow"))?;
    }
    Ok(())
}

fn positioned_read<F: DurableIo>(file: &F, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
    file.durable_read_at(buffer, offset)
}

#[cfg(test)]
pub(super) fn open_rw(path: &Path) -> Result<File, DurableError> {
    let mut options = OpenOptions::new();

View on GitHub (pinned to affd8760f4)