1use std::path::Path;
16
17use kernel::ledger::chain_hash;
18use kernel::{AxError, B3Hash, Seq};
19
20use crate::error::StorageError;
21use crate::jsonl::LineCheck;
22use crate::real_fs::RealFs;
23use crate::vfs::Vfs;
24
25mod start;
26
27pub use start::{SnapshotStart, WholeFold, start_from_snapshot, tail_after};
28
29const MAGIC: &[u8; 8] = b"SPRSNAP1";
30const FILE: &str = "chain.snap";
31const STAGED: &str = "chain.snap.staged";
34
35#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ChainSnapshot {
39 fold_version: u32,
40 seq: Seq,
41 line_hash: B3Hash,
42 views: Vec<u8>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SnapshotFit {
49 Fits,
50 Stale,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum StoredSnapshot {
58 Absent,
59 Damaged(String),
62 Present(ChainSnapshot),
63}
64
65impl ChainSnapshot {
66 pub fn cut(fold_version: u32, seq: Seq, line: &[u8], views: Vec<u8>) -> ChainSnapshot {
68 ChainSnapshot {
69 fold_version,
70 seq,
71 line_hash: chain_hash(line),
72 views,
73 }
74 }
75
76 pub fn fold_version(&self) -> u32 {
77 self.fold_version
78 }
79
80 pub fn seq(&self) -> Seq {
81 self.seq
82 }
83
84 pub fn views(&self) -> &[u8] {
85 &self.views
86 }
87
88 pub fn same_line(&self, other: &ChainSnapshot) -> bool {
91 self.seq == other.seq && self.line_hash == other.line_hash
92 }
93
94 pub fn fit(&self, line_at_seq: &[u8]) -> SnapshotFit {
96 if chain_hash(line_at_seq) == self.line_hash {
97 SnapshotFit::Fits
98 } else {
99 SnapshotFit::Stale
100 }
101 }
102
103 pub fn resume(&self) -> Result<LineCheck, AxError> {
109 Ok(LineCheck::after(self.line_hash, self.seq.next()?))
110 }
111
112 fn encode(&self) -> Vec<u8> {
114 let digest = B3Hash::digest(&self.views);
115 [
116 MAGIC.as_slice(),
117 &self.fold_version.to_le_bytes(),
118 &self.seq.value().to_le_bytes(),
119 self.line_hash.as_bytes(),
120 digest.as_bytes(),
121 &self.views,
122 ]
123 .concat()
124 }
125
126 fn decode(bytes: &[u8]) -> Result<ChainSnapshot, String> {
127 let short = || "the file ends inside its header".to_owned();
128 let (magic, rest) = bytes.split_first_chunk::<8>().ok_or_else(short)?;
129 if magic != MAGIC {
130 return Err("the file does not start with the snapshot magic".to_owned());
131 }
132 let (fold_version, rest) = rest.split_first_chunk::<4>().ok_or_else(short)?;
133 let (seq, rest) = rest.split_first_chunk::<8>().ok_or_else(short)?;
134 let (line_hash, rest) = rest.split_first_chunk::<32>().ok_or_else(short)?;
135 let (digest, views) = rest.split_first_chunk::<32>().ok_or_else(short)?;
136 if B3Hash::digest(views).as_bytes() != digest {
137 return Err("the views do not hash to the digest written beside them".to_owned());
138 }
139 Ok(ChainSnapshot {
140 fold_version: u32::from_le_bytes(*fold_version),
141 seq: Seq::new(u64::from_le_bytes(*seq)),
142 line_hash: B3Hash::from_bytes(*line_hash),
143 views: views.to_vec(),
144 })
145 }
146}
147
148pub fn write_snapshot(dir: &Path, snapshot: &ChainSnapshot) -> Result<(), StorageError> {
154 let mut vfs = RealFs::new();
155 let (file, staged) = (dir.join(FILE), dir.join(STAGED));
156 vfs.create_dir_all(dir)
157 .map_err(refused("create snapshot dir", dir))?;
158 if vfs.exists(&staged) {
159 vfs.remove_file(&staged)
160 .map_err(refused("remove staged snapshot", &staged))?;
161 }
162 vfs.append(&staged, &snapshot.encode())
163 .map_err(refused("write staged snapshot", &staged))?;
164 vfs.sync_data(&staged)
165 .map_err(refused("sync staged snapshot", &staged))?;
166 vfs.rename(&staged, &file)
167 .map_err(refused("rename staged snapshot", &file))?;
168 vfs.sync_dir(dir).map_err(refused("sync snapshot dir", dir))
169}
170
171pub fn read_snapshot(dir: &Path) -> Result<StoredSnapshot, StorageError> {
177 let file = dir.join(FILE);
178 match RealFs::new().read(&file) {
179 Ok(bytes) => Ok(ChainSnapshot::decode(&bytes)
180 .map_or_else(StoredSnapshot::Damaged, StoredSnapshot::Present)),
181 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(StoredSnapshot::Absent),
182 Err(e) => Err(refused("read snapshot", &file)(e)),
183 }
184}
185
186fn refused(op: &'static str, path: &Path) -> impl FnOnce(std::io::Error) -> StorageError {
189 let path = path.to_path_buf();
190 move |source| StorageError::Snapshot { op, path, source }
191}
192
193#[cfg(test)]
194#[allow(
195 clippy::unwrap_used,
196 clippy::expect_used,
197 clippy::panic,
198 clippy::indexing_slicing,
199 clippy::arithmetic_side_effects,
200 reason = "test code"
201)]
202mod tests;