use std::path::{Path, PathBuf};
use linkrs_core::{StorageError, StorageResult};
use crate::edge::ImmutableCsr;
pub(crate) const SNAPSHOT_MAGIC: u32 = 0x52534347;
pub(crate) const SNAPSHOT_HEADER_LEN: usize = 4 + 32 + 7 * 16;
pub(crate) const SNAPSHOT_CRC_LEN: usize = 4;
pub(crate) const SNAPSHOT_FLAG_VALUED: u64 = 1;
pub(crate) fn snapshot_error(message: String) -> StorageError {
StorageError::deserialize_error(message)
}
pub fn snapshot_path_for(base: &Path) -> PathBuf {
let mut name = base.as_os_str().to_owned();
name.push(".snapshot");
base.with_file_name(name)
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct ColumnRange {
pub(crate) start: usize,
pub(crate) len: usize,
}
impl ColumnRange {
pub(crate) fn end(&self) -> usize {
self.start.saturating_add(self.len)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct SnapshotColumns {
pub(crate) degrees: ColumnRange,
pub(crate) endpoints: ColumnRange,
pub(crate) ranks: ColumnRange,
pub(crate) edge_ids: ColumnRange,
pub(crate) deletes: ColumnRange,
pub(crate) values: ColumnRange,
pub(crate) validity: ColumnRange,
}
pub(crate) fn read_u32_le_at(bytes: &[u8], offset: usize) -> StorageResult<u32> {
let end = offset.saturating_add(4);
let chunk = bytes
.get(offset..end)
.ok_or_else(|| snapshot_error(format!("snapshot file truncated at byte {offset}")))?;
Ok(u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
}
pub(crate) fn read_u64_le_at(bytes: &[u8], offset: usize) -> StorageResult<u64> {
let end = offset.saturating_add(8);
let chunk = bytes
.get(offset..end)
.ok_or_else(|| snapshot_error(format!("snapshot file truncated at byte {offset}")))?;
Ok(u64::from_le_bytes([
chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
]))
}
pub(crate) fn parse_header(bytes: &[u8]) -> StorageResult<(usize, usize, u64, SnapshotColumns)> {
if bytes.len() < SNAPSHOT_HEADER_LEN + SNAPSHOT_CRC_LEN {
return Err(snapshot_error(format!(
"snapshot file too short for header: {} bytes",
bytes.len()
)));
}
let (body, trailer) = bytes.split_at(bytes.len() - SNAPSHOT_CRC_LEN);
let stored = u32::from_le_bytes([trailer[0], trailer[1], trailer[2], trailer[3]]);
let computed = crc32fast::hash(body);
if stored != computed {
return Err(snapshot_error(format!(
"snapshot file CRC mismatch: stored={stored:#x} computed={computed:#x}"
)));
}
let bytes = body;
let magic = read_u32_le_at(bytes, 0)?;
if magic != SNAPSHOT_MAGIC {
return Err(snapshot_error(format!(
"snapshot file magic mismatch: {magic:#010x}"
)));
}
let rows = read_u64_le_at(bytes, 4)? as usize;
let entries = read_u64_le_at(bytes, 12)? as usize;
let edge_count = read_u64_le_at(bytes, 20)?;
let flags = read_u64_le_at(bytes, 28)?;
if flags & !SNAPSHOT_FLAG_VALUED != 0 {
return Err(snapshot_error(format!(
"snapshot file flags out of range: {flags:#x}"
)));
}
let valued = flags & SNAPSHOT_FLAG_VALUED != 0;
let mut cursor = 36usize;
let mut ranges = [ColumnRange::default(); 7];
for range in &mut ranges {
let start = read_u64_le_at(bytes, cursor)? as usize;
let len = read_u64_le_at(bytes, cursor + 8)? as usize;
*range = ColumnRange { start, len };
cursor += 16;
}
let expected = [
rows.saturating_mul(4),
entries.saturating_mul(4),
entries.saturating_mul(8),
entries.saturating_mul(8),
entries.saturating_mul(8),
if valued { entries.saturating_mul(8) } else { 0 },
if valued { entries.div_ceil(8) } else { 0 },
];
let mut poll_end = SNAPSHOT_HEADER_LEN;
for (range, want) in ranges.iter().zip(expected) {
if range.len != want {
return Err(snapshot_error(format!(
"snapshot column length mismatch: holds {} bytes, layout needs {want}",
range.len
)));
}
if range.start != poll_end {
return Err(snapshot_error(format!(
"snapshot column gap or overlap at byte {}",
range.start
)));
}
poll_end = range.end();
}
if bytes.len() != poll_end {
return Err(snapshot_error(format!(
"snapshot file size mismatch: holds {} bytes, layout needs {poll_end}",
bytes.len()
)));
}
let columns = SnapshotColumns {
degrees: ranges[0],
endpoints: ranges[1],
ranks: ranges[2],
edge_ids: ranges[3],
deletes: ranges[4],
values: ranges[5],
validity: ranges[6],
};
Ok((rows, entries, edge_count, columns))
}
pub fn write_snapshot_file(frozen: &ImmutableCsr, path: &Path) -> StorageResult<()> {
let hot = frozen.packed_hot();
let cold = frozen.packed_cold();
let degrees = frozen.packed_degrees();
debug_assert_eq!(hot.len(), cold.len());
let rows = degrees.len();
let entries = hot.len();
let valued = frozen.has_valued_entries();
let flags = if valued { SNAPSHOT_FLAG_VALUED } else { 0 };
let mut ranges = [ColumnRange::default(); 7];
let mut cursor = SNAPSHOT_HEADER_LEN;
let lens = [
rows.saturating_mul(4),
entries.saturating_mul(4),
entries.saturating_mul(8),
entries.saturating_mul(8),
entries.saturating_mul(8),
if valued { entries.saturating_mul(8) } else { 0 },
if valued { entries.div_ceil(8) } else { 0 },
];
for (range, len) in ranges.iter_mut().zip(lens) {
*range = ColumnRange { start: cursor, len };
cursor += len;
}
let mut bytes = Vec::with_capacity(cursor);
bytes.extend_from_slice(&SNAPSHOT_MAGIC.to_le_bytes());
bytes.extend_from_slice(&(rows as u64).to_le_bytes());
bytes.extend_from_slice(&(entries as u64).to_le_bytes());
bytes.extend_from_slice(&frozen.edge_count().to_le_bytes());
bytes.extend_from_slice(&flags.to_le_bytes());
for range in &ranges {
bytes.extend_from_slice(&(range.start as u64).to_le_bytes());
bytes.extend_from_slice(&(range.len as u64).to_le_bytes());
}
for degree in degrees {
bytes.extend_from_slice(°ree.to_le_bytes());
}
for hot_nbr in hot {
bytes.extend_from_slice(&hot_nbr.endpoint.to_le_bytes());
}
for hot_nbr in hot {
bytes.extend_from_slice(&hot_nbr.rank.to_le_bytes());
}
for hot_nbr in hot {
bytes.extend_from_slice(&hot_nbr.edge_id.0.to_le_bytes());
}
for cold_stamps in cold {
bytes.extend_from_slice(&cold_stamps.delete_ts.to_le_bytes());
}
if valued {
for value in frozen.packed_values() {
bytes.extend_from_slice(&value.to_le_bytes());
}
bytes.extend_from_slice(frozen.packed_valid_bytes());
}
debug_assert_eq!(bytes.len(), cursor);
let crc = crc32fast::hash(&bytes);
bytes.extend_from_slice(&crc.to_le_bytes());
let tmp = path.with_extension("snapshot.tmp");
std::fs::write(&tmp, &bytes)
.map_err(|e| StorageError::io_error(format!("snapshot file write failed: {e}")))?;
std::fs::rename(&tmp, path)
.map_err(|e| StorageError::io_error(format!("snapshot file swap failed: {e}")))?;
Ok(())
}