1use prikk_error::{PrikkError, Result};
7
8use crate::path::{RepoPath, validate_no_path_collisions};
9
10const SNAPSHOT_MAGIC: &[u8] = b"PRIKK-SNAPSHOT-MANIFEST-v1\n";
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SnapshotEntry {
15 pub path: RepoPath,
17 pub bytes: Vec<u8>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct SnapshotManifest {
24 pub files: Vec<SnapshotEntry>,
26}
27
28impl SnapshotManifest {
29 pub fn decode(bytes: &[u8]) -> Result<Self> {
31 let Some(mut rest) = bytes.get(SNAPSHOT_MAGIC.len()..) else {
32 return Err(PrikkError::MalformedData(
33 "snapshot manifest is shorter than magic".to_string(),
34 ));
35 };
36 if !bytes.starts_with(SNAPSHOT_MAGIC) {
37 return Err(PrikkError::MalformedData(
38 "snapshot manifest magic mismatch".to_string(),
39 ));
40 }
41 let mut files = Vec::new();
42 while !rest.is_empty() {
43 let (path_len, after_path_len) = read_u32(rest)?;
44 rest = after_path_len;
45 let path_len = path_len as usize;
46 if path_len == 0 {
47 return Err(PrikkError::MalformedData(
48 "snapshot path must not be empty".to_string(),
49 ));
50 }
51 let (path_bytes, after_path) = read_exact(rest, path_len)?;
52 rest = after_path;
53 let path_text = std::str::from_utf8(path_bytes)
54 .map_err(|_| PrikkError::MalformedData("snapshot path is not UTF-8".to_string()))?;
55 let path = RepoPath::parse(path_text)?;
56 let (content_len, after_content_len) = read_u64(rest)?;
57 rest = after_content_len;
58 let content_len = usize::try_from(content_len).map_err(|_| {
59 PrikkError::MalformedData("snapshot content length does not fit usize".to_string())
60 })?;
61 let (content, after_content) = read_exact(rest, content_len)?;
62 rest = after_content;
63 files.push(SnapshotEntry {
64 path,
65 bytes: content.to_vec(),
66 });
67 }
68 let manifest = Self { files };
69 manifest.validate_order_and_collisions()?;
70 Ok(manifest)
71 }
72
73 pub fn encode(&self) -> Result<Vec<u8>> {
75 self.validate_order_and_collisions()?;
76 let mut out = Vec::new();
77 out.extend_from_slice(SNAPSHOT_MAGIC);
78 for file in &self.files {
79 let path = file.path.as_str().as_bytes();
80 let path_len = u32::try_from(path.len()).map_err(|_| {
81 PrikkError::MalformedData("snapshot path length exceeds u32".to_string())
82 })?;
83 out.extend_from_slice(&path_len.to_be_bytes());
84 out.extend_from_slice(path);
85 out.extend_from_slice(&(file.bytes.len() as u64).to_be_bytes());
86 out.extend_from_slice(&file.bytes);
87 }
88 Ok(out)
89 }
90
91 #[must_use]
93 pub fn total_content_bytes(&self) -> u64 {
94 self.files
95 .iter()
96 .map(|entry| entry.bytes.len() as u64)
97 .sum()
98 }
99
100 fn validate_order_and_collisions(&self) -> Result<()> {
101 let paths: Vec<RepoPath> = self.files.iter().map(|entry| entry.path.clone()).collect();
102 validate_no_path_collisions(&paths)?;
103 if !paths.windows(2).all(|pair| {
104 let mut items = pair.iter();
105 match (items.next(), items.next()) {
106 (Some(left), Some(right)) => left < right,
107 _ => true,
108 }
109 }) {
110 return Err(PrikkError::MalformedData(
111 "snapshot paths must be sorted by repository path".to_string(),
112 ));
113 }
114 Ok(())
115 }
116}
117
118fn read_u32(bytes: &[u8]) -> Result<(u32, &[u8])> {
119 let (raw, rest) = read_exact(bytes, 4)?;
120 let mut out = [0_u8; 4];
121 out.copy_from_slice(raw);
122 Ok((u32::from_be_bytes(out), rest))
123}
124
125fn read_u64(bytes: &[u8]) -> Result<(u64, &[u8])> {
126 let (raw, rest) = read_exact(bytes, 8)?;
127 let mut out = [0_u8; 8];
128 out.copy_from_slice(raw);
129 Ok((u64::from_be_bytes(out), rest))
130}
131
132fn read_exact(bytes: &[u8], len: usize) -> Result<(&[u8], &[u8])> {
133 let Some(value) = bytes.get(..len) else {
134 return Err(PrikkError::MalformedData(
135 "unexpected end of snapshot manifest".to_string(),
136 ));
137 };
138 let Some(rest) = bytes.get(len..) else {
139 return Err(PrikkError::MalformedData(
140 "snapshot manifest range overflow".to_string(),
141 ));
142 };
143 Ok((value, rest))
144}
145
146#[cfg(test)]
147mod tests;