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

negative volume seek

Error message

negative volume seek

What it means

After the overflow checks, seek() converts the computed i128 position back to u64; a negative result means the seek landed before offset 0, which is invalid, so InvalidInput ('negative volume seek') is returned.

Solutions

  1. Clamp the requested position to 0 before seeking
  2. Compute the target in the caller and use SeekFrom::Start(max(0, target))
  3. Verify bookkeeping of how far back you intend to seek (bytes consumed)
  4. Handle the InvalidInput error to detect underflow instead of panicking on assumption

Example fix

// before
reader.seek(std::io::SeekFrom::Current(-1000))?; // cursor is only 10
// after
let target = cursor.saturating_sub(1000);
reader.seek(std::io::SeekFrom::Start(target))?;
Defensive patterns

Strategy: validation

Validate before calling

fn rewind_target(cursor: u64, back: u64) -> u64 { cursor.saturating_sub(back) } // use SeekFrom::Start(rewind_target(...))

Type guard

fn can_rewind(cursor: u64, back: u64) -> bool { back <= cursor }

Try / catch

match reader.seek(SeekFrom::Current(-back)) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {
        reader.seek(std::io::SeekFrom::Start(0))? // clamp to start
    }
    other => other?,
}

Prevention

When it happens

Trigger: SeekFrom::Current(negative delta) or SeekFrom::End(negative delta) whose magnitude exceeds the cursor/region length, e.g. seeking -1000 from Current when cursor is 10.

Common situations: Rewinding past the start due to miscounted bytes read; off-by-one in buffer management; using a recorded delta computed against a different, larger position.

Related errors


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

Appendix: source

Thrown at crates/astrid-storage/src/volume.rs:428

}

impl Seek for VolumeFile {
    fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
        let next = match position {
            SeekFrom::Start(offset) => i128::from(offset),
            SeekFrom::Current(delta) => i128::from(self.cursor)
                .checked_add(i128::from(delta))
                .ok_or_else(|| {
                    io::Error::new(io::ErrorKind::InvalidInput, "volume seek overflow")
                })?,
            SeekFrom::End(delta) => i128::from(self.volume.region_len(&self.region)?)
                .checked_add(i128::from(delta))
                .ok_or_else(|| {
                    io::Error::new(io::ErrorKind::InvalidInput, "volume seek overflow")
                })?,
        };
        self.cursor = u64::try_from(next)
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "negative volume seek"))?;
        Ok(self.cursor)
    }
}

/// Minimal metadata returned for a volume region.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VolumeMetadata {
    length: u64,
}

impl VolumeMetadata {
    /// Return the logical byte length.
    #[must_use]
    pub const fn len(self) -> u64 {
        self.length
    }

    /// A volume region is always a regular byte stream.

View on GitHub (pinned to affd8760f4)