harden obiskio error handling with explicit variants and bounds checking

Extend `SKError` with `BadMagic`, `Truncated`, and `InvalidData` variants to replace `expect()` calls and unsafe slice indexing. Implement proper error chaining via `source()` and update `Display` formatting. Improve magic byte validation and serde error mapping for clearer debugging. Documents a broader roadmap for further crate hardening, including LRU eviction, concurrency, and benchmarking.
This commit is contained in:
Eric Coissac
2026-05-09 18:26:08 +08:00
parent 5169f65dc9
commit e008a8beb4
4 changed files with 126 additions and 11 deletions
+14
View File
@@ -5,6 +5,12 @@ use std::io;
pub enum SKError {
Io(io::Error),
Compression(niffler::Error),
/// Binary file has unrecognized magic bytes.
BadMagic { expected: &'static str, got: [u8; 4] },
/// File is too short to contain a required field.
Truncated { context: &'static str },
/// A field value is outside its valid range or cannot be parsed.
InvalidData { context: &'static str, detail: String },
}
pub type SKResult<T> = Result<T, SKError>;
@@ -14,6 +20,13 @@ impl fmt::Display for SKError {
match self {
SKError::Io(e) => write!(f, "I/O error: {e}"),
SKError::Compression(e) => write!(f, "compression error: {e}"),
SKError::BadMagic { expected, got } => {
write!(f, "bad magic in {expected}: expected {:?}, got {:?}", expected.as_bytes(), got)
}
SKError::Truncated { context } => write!(f, "truncated file: {context}"),
SKError::InvalidData { context, detail } => {
write!(f, "invalid data in {context}: {detail}")
}
}
}
}
@@ -23,6 +36,7 @@ impl std::error::Error for SKError {
match self {
SKError::Io(e) => Some(e),
SKError::Compression(e) => Some(e),
_ => None,
}
}
}
+9 -3
View File
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::SKResult;
use crate::error::{SKError, SKResult};
/// Statistics sidecar written alongside each SuperKmer file on close.
///
@@ -31,7 +31,10 @@ impl SKFileMeta {
pub fn read(sk_path: &Path) -> SKResult<Option<Self>> {
let path = Self::sidecar_path(sk_path);
match std::fs::File::open(&path) {
Ok(f) => Ok(Some(serde_json::from_reader(f).map_err(std::io::Error::other)?)),
Ok(f) => Ok(Some(serde_json::from_reader(f).map_err(|e| SKError::InvalidData {
context: "SKFileMeta sidecar",
detail: e.to_string(),
})?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
@@ -40,7 +43,10 @@ impl SKFileMeta {
pub(crate) fn write(&self, sk_path: &Path) -> SKResult<()> {
let path = Self::sidecar_path(sk_path);
let f = std::fs::File::create(&path)?;
serde_json::to_writer_pretty(f, self).map_err(std::io::Error::other)?;
serde_json::to_writer_pretty(f, self).map_err(|e| SKError::InvalidData {
context: "SKFileMeta sidecar",
detail: e.to_string(),
})?;
Ok(())
}
}
+16 -8
View File
@@ -218,23 +218,31 @@ fn read_idx(path: &Path) -> SKResult<(Vec<u8>, Vec<u32>)> {
let data = std::fs::read(path).map_err(SKError::Io)?;
let mut pos = 0;
if &data[pos..pos + 4] != &MAGIC {
return Err(SKError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"unitig index: bad magic",
)));
let magic_bytes = data.get(pos..pos + 4)
.ok_or(SKError::Truncated { context: "unitig index: magic" })?;
if magic_bytes != &MAGIC {
return Err(SKError::BadMagic {
expected: "UIDX",
got: magic_bytes.try_into().unwrap(),
});
}
pos += 4;
let n = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
let n_bytes = data.get(pos..pos + 4)
.ok_or(SKError::Truncated { context: "unitig index: n_unitigs" })?;
let n = u32::from_le_bytes(n_bytes.try_into().unwrap()) as usize;
pos += 4;
let seqls = data[pos..pos + n].to_vec();
let seqls = data.get(pos..pos + n)
.ok_or(SKError::Truncated { context: "unitig index: seqls" })?
.to_vec();
pos += n;
let mut packed_offsets = Vec::with_capacity(n + 1);
for _ in 0..=n {
packed_offsets.push(u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()));
let off_bytes = data.get(pos..pos + 4)
.ok_or(SKError::Truncated { context: "unitig index: packed_offsets" })?;
packed_offsets.push(u32::from_le_bytes(off_bytes.try_into().unwrap()));
pos += 4;
}