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
- Verify the file's integrity/rebuild the index — the offset likely points past EOF after truncation
- Check available file size before reading: ensure file_len >= offset + buffer_len
- Restore the durable file from backup or snapshot if it was truncated by a crash
- 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
- Validate index offsets against current file length before traversal
- Use fsync/fdatasync on write paths so crashes never leave silently truncated files
- Detect disk-full conditions during writes and treat truncation as corruption needing recovery
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
- Astrid volume is already open
- Astrid volume is not a regular file
- Astrid volume path has no file name
- Astrid volume record checksum mismatch
- audit read failed
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)