use std::io::Read;
use crate::error::{Error, Result};
use crate::matrix::RectangularBinaryMatrix;
pub const FORMAT_BINARY_SORTED: &str = "binary/sorted";
pub const FORMAT_TEXT_SORTED: &str = "text/sorted";
pub const FORMAT_BLOOM_COUNTER: &str = "bloomcounter";
const HEADER_LENGTH_DIGITS: usize = 9;
#[derive(Debug, Clone)]
pub struct FileHeader {
raw: serde_json::Value,
offset: usize,
}
impl FileHeader {
pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
let mut len_buf = [0u8; HEADER_LENGTH_DIGITS];
reader
.read_exact(&mut len_buf)
.map_err(|_| Error::InvalidHeader("could not read header length prefix".to_string()))?;
let len_str = std::str::from_utf8(&len_buf)
.map_err(|_| Error::InvalidHeader("header length is not valid ASCII".to_string()))?;
let header_len: usize = len_str
.trim()
.parse()
.map_err(|_| Error::InvalidHeader(format!("invalid header length: {len_str:?}")))?;
let mut json_buf = vec![0u8; header_len];
reader
.read_exact(&mut json_buf)
.map_err(|_| Error::InvalidHeader("could not read header JSON content".to_string()))?;
let json_end = json_buf
.iter()
.position(|&b| b == 0)
.unwrap_or(json_buf.len());
let json_str = std::str::from_utf8(&json_buf[..json_end])
.map_err(|_| Error::InvalidHeader("header JSON is not valid UTF-8".to_string()))?;
let raw: serde_json::Value = serde_json::from_str(json_str)?;
let offset = HEADER_LENGTH_DIGITS + header_len;
Ok(Self { raw, offset })
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn format(&self) -> &str {
self.raw
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("")
}
pub fn is_binary(&self) -> bool {
self.format() == FORMAT_BINARY_SORTED
}
pub fn is_text(&self) -> bool {
self.format() == FORMAT_TEXT_SORTED
}
pub fn size(&self) -> Option<u64> {
self.raw.get("size").and_then(|v| v.as_u64())
}
pub fn key_len(&self) -> Option<u64> {
self.raw.get("key_len").and_then(|v| v.as_u64())
}
pub fn k(&self) -> Option<usize> {
self.key_len().map(|kl| (kl / 2) as usize)
}
pub fn val_len(&self) -> Option<u64> {
self.raw.get("val_len").and_then(|v| v.as_u64())
}
pub fn counter_len(&self) -> Option<u64> {
self.raw.get("counter_len").and_then(|v| v.as_u64())
}
pub fn max_reprobe(&self) -> Option<u64> {
self.raw.get("max_reprobe").and_then(|v| v.as_u64())
}
pub fn canonical(&self) -> bool {
self.raw
.get("canonical")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub fn nb_hashes(&self) -> Option<u64> {
self.raw.get("nb_hashes").and_then(|v| v.as_u64())
}
pub fn fpr(&self) -> Option<f64> {
self.raw.get("fpr").and_then(|v| v.as_f64())
}
pub fn hostname(&self) -> Option<&str> {
self.raw.get("hostname").and_then(|v| v.as_str())
}
pub fn pwd(&self) -> Option<&str> {
self.raw.get("pwd").and_then(|v| v.as_str())
}
pub fn time(&self) -> Option<&str> {
self.raw.get("time").and_then(|v| v.as_str())
}
pub fn exe_path(&self) -> Option<&str> {
self.raw.get("exe_path").and_then(|v| v.as_str())
}
pub fn cmdline(&self) -> Option<Vec<&str>> {
self.raw.get("cmdline").and_then(|v| {
v.as_array()
.map(|arr| arr.iter().filter_map(|s| s.as_str()).collect())
})
}
pub fn matrix(&self, index: usize) -> Result<RectangularBinaryMatrix> {
let key = format!("matrix{index}");
match self.raw.get(&key) {
Some(v) => RectangularBinaryMatrix::from_json(v),
None => {
if index == 0 {
match self.raw.get("matrix") {
Some(v) => RectangularBinaryMatrix::from_json(v),
None => Ok(RectangularBinaryMatrix::identity(64)),
}
} else {
Err(Error::MissingField(key))
}
}
}
}
pub fn reprobes(&self) -> Option<Vec<u64>> {
self.raw.get("reprobes").and_then(|v| {
v.as_array().map(|arr| {
arr.iter()
.filter_map(|v| v.as_u64().or_else(|| v.as_i64().map(|i| i as u64)))
.collect()
})
})
}
pub fn raw_json(&self) -> &serde_json::Value {
&self.raw
}
pub fn key_bytes(&self) -> Option<usize> {
self.key_len().map(|bits| {
let bits = bits as usize;
bits.div_ceil(8)
})
}
pub fn data_val_len(&self) -> Option<usize> {
self.counter_len()
.or_else(|| self.val_len())
.map(|v| v as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn make_header_bytes(json: &serde_json::Value) -> Vec<u8> {
let json_str = serde_json::to_string(json).unwrap();
let len = json_str.len();
let mut buf = Vec::new();
buf.extend_from_slice(format!("{:09}", len).as_bytes());
buf.extend_from_slice(json_str.as_bytes());
buf
}
#[test]
fn test_parse_minimal_header() {
let json = serde_json::json!({
"format": "binary/sorted",
"key_len": 50,
"val_len": 4,
"size": 1024,
"canonical": true
});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.format(), "binary/sorted");
assert!(header.is_binary());
assert!(!header.is_text());
assert_eq!(header.key_len(), Some(50));
assert_eq!(header.k(), Some(25));
assert_eq!(header.val_len(), Some(4));
assert_eq!(header.size(), Some(1024));
assert!(header.canonical());
}
#[test]
fn test_parse_text_format_header() {
let json = serde_json::json!({
"format": "text/sorted",
"key_len": 42,
"counter_len": 8
});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert!(header.is_text());
assert!(!header.is_binary());
assert_eq!(header.k(), Some(21));
}
#[test]
fn test_header_offset() {
let json = serde_json::json!({"format": "binary/sorted"});
let json_str = serde_json::to_string(&json).unwrap();
let expected_offset = 9 + json_str.len();
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.offset(), expected_offset);
}
#[test]
fn test_header_with_padding() {
let json = serde_json::json!({"format": "binary/sorted", "key_len": 50});
let json_str = serde_json::to_string(&json).unwrap();
let padded_len = json_str.len() + 10; let mut buf = Vec::new();
buf.extend_from_slice(format!("{:09}", padded_len).as_bytes());
buf.extend_from_slice(json_str.as_bytes());
buf.extend_from_slice(&vec![0u8; 10]);
let mut cursor = Cursor::new(buf);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.format(), "binary/sorted");
assert_eq!(header.key_len(), Some(50));
}
#[test]
fn test_header_metadata_fields() {
let json = serde_json::json!({
"format": "binary/sorted",
"hostname": "testhost",
"pwd": "/tmp",
"time": "2024-01-01",
"exe_path": "/usr/bin/jellyfish",
"cmdline": ["jellyfish", "count", "-m", "25", "input.fa"],
"key_len": 50,
"val_len": 4,
"counter_len": 4,
"max_reprobe": 126,
"canonical": false
});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.hostname(), Some("testhost"));
assert_eq!(header.pwd(), Some("/tmp"));
assert_eq!(header.time(), Some("2024-01-01"));
assert_eq!(header.exe_path(), Some("/usr/bin/jellyfish"));
assert_eq!(
header.cmdline(),
Some(vec!["jellyfish", "count", "-m", "25", "input.fa"])
);
assert_eq!(header.max_reprobe(), Some(126));
assert!(!header.canonical());
}
#[test]
fn test_header_key_bytes() {
let json = serde_json::json!({"format": "binary/sorted", "key_len": 50});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.key_bytes(), Some(7));
}
#[test]
fn test_header_key_bytes_aligned() {
let json = serde_json::json!({"format": "binary/sorted", "key_len": 64});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.key_bytes(), Some(8));
}
#[test]
fn test_invalid_header_too_short() {
let mut cursor = Cursor::new(b"123");
assert!(FileHeader::read(&mut cursor).is_err());
}
#[test]
fn test_invalid_header_bad_length() {
let mut cursor = Cursor::new(b"not_a_num");
assert!(FileHeader::read(&mut cursor).is_err());
}
#[test]
fn test_invalid_header_bad_json() {
let mut buf = Vec::new();
let bad_json = b"not json!";
buf.extend_from_slice(format!("{:09}", bad_json.len()).as_bytes());
buf.extend_from_slice(bad_json);
let mut cursor = Cursor::new(buf);
assert!(FileHeader::read(&mut cursor).is_err());
}
#[test]
fn test_missing_optional_fields() {
let json = serde_json::json!({"format": "binary/sorted"});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.size(), None);
assert_eq!(header.key_len(), None);
assert_eq!(header.val_len(), None);
assert_eq!(header.hostname(), None);
assert_eq!(header.pwd(), None);
assert_eq!(header.time(), None);
assert_eq!(header.exe_path(), None);
assert_eq!(header.cmdline(), None);
assert!(!header.canonical()); }
#[test]
fn test_data_val_len_prefers_counter_len() {
let json = serde_json::json!({
"format": "binary/sorted",
"val_len": 4,
"counter_len": 8
});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.data_val_len(), Some(8)); }
#[test]
fn test_data_val_len_fallback_to_val_len() {
let json = serde_json::json!({
"format": "binary/sorted",
"val_len": 4
});
let bytes = make_header_bytes(&json);
let mut cursor = Cursor::new(bytes);
let header = FileHeader::read(&mut cursor).unwrap();
assert_eq!(header.data_val_len(), Some(4));
}
}