use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
pub const HEADER: u64 = 8;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Entry {
pub height: u32,
pub hash: String,
pub offset: u64,
pub size: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Index {
pub network: String,
pub from: i64,
pub to: i64,
pub blocks: Vec<Entry>,
}
impl Index {
pub fn new(network: &str) -> Self {
Self {
network: network.to_string(),
from: 0,
to: -1,
blocks: Vec::new(),
}
}
}
pub fn read_index(path: impl AsRef<Path>) -> Result<Option<Index>> {
let path = path.as_ref();
if !path.exists() {
return Ok(None);
}
Ok(Some(serde_json::from_str(&fs::read_to_string(path)?)?))
}
pub fn write_index(path: impl AsRef<Path>, index: &Index) -> Result<()> {
fs::write(path, serde_json::to_string(index)?)?;
Ok(())
}
pub fn append_block(
dat: impl AsRef<Path>,
index: &mut Index,
height: u32,
hash: &str,
bytes: &[u8],
) -> Result<()> {
let dat = dat.as_ref();
let offset = if dat.exists() {
fs::metadata(dat)?.len()
} else {
0
};
let mut f = OpenOptions::new().create(true).append(true).open(dat)?;
f.write_all(&height.to_le_bytes())?;
f.write_all(&(bytes.len() as u32).to_le_bytes())?;
f.write_all(bytes)?;
index.blocks.push(Entry {
height,
hash: hash.to_string(),
offset,
size: bytes.len() as u32,
});
index.to = i64::from(height);
Ok(())
}
pub fn truncate_from(dat: impl AsRef<Path>, index: &mut Index, height: u32) -> Result<()> {
let Some(i) = index.blocks.iter().position(|b| b.height == height) else {
return Ok(());
};
let f = OpenOptions::new().write(true).open(dat)?;
f.set_len(index.blocks[i].offset)?;
index.blocks.truncate(i);
index.to = if i > 0 {
i64::from(index.blocks[i - 1].height)
} else {
index.from - 1
};
Ok(())
}
pub fn read_block(dat: impl AsRef<Path>, entry: &Entry) -> Result<Vec<u8>> {
let mut f = File::open(dat)?;
let len = f.metadata()?.len();
let end = entry
.offset
.checked_add(HEADER)
.and_then(|o| o.checked_add(u64::from(entry.size)))
.ok_or_else(|| {
Error::BlockFile(format!(
"index entry for height {} overflows: offset {} size {}",
entry.height, entry.offset, entry.size
))
})?;
if end > len {
return Err(Error::BlockFile(format!(
"index entry for height {} runs past the file: offset {} + 8 + size {} = {end} > {len} bytes",
entry.height, entry.offset, entry.size
)));
}
f.seek(SeekFrom::Start(entry.offset))?;
let mut prefix = [0u8; HEADER as usize];
f.read_exact(&mut prefix)?;
let height = u32::from_le_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]);
let size = u32::from_le_bytes([prefix[4], prefix[5], prefix[6], prefix[7]]);
if height != entry.height || size != entry.size {
return Err(Error::BlockFile(format!(
"record at offset {} is height {height} size {size}; the index entry says height {} size {}",
entry.offset, entry.height, entry.size
)));
}
let mut buf = vec![0u8; entry.size as usize];
f.read_exact(&mut buf)?;
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn append_read_truncate() {
let dir = std::env::temp_dir().join(format!("sidestr-blockfile-{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let dat = dir.join("blocks.dat");
let idx = dir.join("blocks.json");
assert_eq!(read_index(&idx).unwrap(), None);
let mut index = Index::new("sidestr:test");
append_block(&dat, &mut index, 0, "aa", b"genesis").unwrap();
append_block(&dat, &mut index, 1, "bb", b"one").unwrap();
write_index(&idx, &index).unwrap();
let text = fs::read_to_string(&idx).unwrap();
assert_eq!(
text,
r#"{"network":"sidestr:test","from":0,"to":1,"blocks":[{"height":0,"hash":"aa","offset":0,"size":7},{"height":1,"hash":"bb","offset":15,"size":3}]}"#
);
let again = read_index(&idx).unwrap().unwrap();
assert_eq!(again, index);
assert_eq!(read_block(&dat, &again.blocks[1]).unwrap(), b"one");
for (name, entry) in [
(
"height",
Entry {
height: 2,
..again.blocks[1].clone()
},
),
(
"size",
Entry {
size: 2,
..again.blocks[1].clone()
},
),
(
"past the end",
Entry {
size: 4,
..again.blocks[1].clone()
},
),
(
"offset wraps",
Entry {
offset: u64::MAX - 4,
..again.blocks[1].clone()
},
),
] {
let e = read_block(&dat, &entry).unwrap_err();
assert!(matches!(e, Error::BlockFile(_)), "{name}: {e}");
}
truncate_from(&dat, &mut index, 1).unwrap();
assert_eq!(
(
index.to,
index.blocks.len(),
fs::metadata(&dat).unwrap().len()
),
(0, 1, 15)
);
truncate_from(&dat, &mut index, 0).unwrap();
assert_eq!((index.to, fs::metadata(&dat).unwrap().len()), (-1, 0));
fs::remove_dir_all(&dir).unwrap();
}
}