1use corium_core::chunk;
14
15use crate::{BlobId, StoreError};
16
17pub const INDEX_MANIFEST_MAGIC: &str = "corium-index-manifest-v1";
21
22#[must_use]
24pub fn is_index_manifest(bytes: &[u8]) -> bool {
25 bytes.starts_with(INDEX_MANIFEST_MAGIC.as_bytes())
26}
27
28#[must_use]
30pub fn encode_index_manifest(children: &[BlobId]) -> Vec<u8> {
31 let mut out = String::from(INDEX_MANIFEST_MAGIC);
32 out.push('\n');
33 for child in children {
34 out.push_str(child.as_str());
35 out.push('\n');
36 }
37 out.into_bytes()
38}
39
40pub fn decode_index_manifest(bytes: &[u8]) -> Result<Vec<BlobId>, StoreError> {
46 let corrupt = |detail: &str| {
47 StoreError::Io(std::io::Error::other(format!(
48 "malformed index manifest: {detail}"
49 )))
50 };
51 let text = std::str::from_utf8(bytes).map_err(|_| corrupt("not UTF-8"))?;
52 let mut lines = text.lines();
53 if lines.next() != Some(INDEX_MANIFEST_MAGIC) {
54 return Err(corrupt("missing magic"));
55 }
56 lines
57 .map(|line| BlobId::from_hex(line).ok_or_else(|| corrupt("invalid child id")))
58 .collect()
59}
60
61pub fn index_blob_children(bytes: &[u8]) -> Result<Vec<BlobId>, StoreError> {
68 if is_index_manifest(bytes) {
69 decode_index_manifest(bytes)
70 } else {
71 Ok(Vec::new())
72 }
73}
74
75pub fn decode_segment_keys(bytes: &[u8]) -> Result<Vec<Vec<u8>>, StoreError> {
82 let truncated = || StoreError::Io(std::io::Error::other("truncated segment"));
83 let mut keys = Vec::new();
84 let mut input = bytes;
85 while !input.is_empty() {
86 let (len_bytes, rest) = input.split_at_checked(8).ok_or_else(truncated)?;
87 let len = usize::try_from(u64::from_be_bytes(len_bytes.try_into().unwrap_or_default()))
88 .map_err(|_| StoreError::Io(std::io::Error::other("segment key too large")))?;
89 let (key, rest) = rest.split_at_checked(len).ok_or_else(truncated)?;
90 keys.push(key.to_vec());
91 input = rest;
92 }
93 Ok(keys)
94}
95
96#[must_use]
104pub fn encode_segment_chunk<'a>(keys: impl IntoIterator<Item = &'a [u8]>) -> Vec<u8> {
105 let mut out = Vec::new();
106 for key in keys {
107 out.extend_from_slice(&(key.len() as u64).to_be_bytes());
108 out.extend_from_slice(key);
109 }
110 out
111}
112
113#[must_use]
120pub fn chunk_segment_keys<'a>(keys: impl IntoIterator<Item = &'a [u8]>) -> Vec<Vec<u8>> {
121 let mut chunks = Vec::new();
122 let mut chunk = Vec::new();
123 let mut entries = 0usize;
124 for key in keys {
125 chunk.extend_from_slice(&(key.len() as u64).to_be_bytes());
126 chunk.extend_from_slice(key);
127 entries += 1;
128 if chunk::cut_after(key, entries) {
129 chunks.push(std::mem::take(&mut chunk));
130 entries = 0;
131 }
132 }
133 if !chunk.is_empty() {
134 chunks.push(chunk);
135 }
136 chunks
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::digest;
143
144 fn keys(count: u64) -> Vec<Vec<u8>> {
145 (0..count).map(|n| n.to_be_bytes().to_vec()).collect()
146 }
147
148 #[test]
149 fn manifest_round_trips() {
150 let children = vec![digest(b"a"), digest(b"b")];
151 let encoded = encode_index_manifest(&children);
152 assert!(is_index_manifest(&encoded));
153 assert_eq!(decode_index_manifest(&encoded).unwrap(), children);
154 assert_eq!(index_blob_children(&encoded).unwrap(), children);
155 }
156
157 #[test]
158 fn empty_manifest_round_trips() {
159 let encoded = encode_index_manifest(&[]);
160 assert!(decode_index_manifest(&encoded).unwrap().is_empty());
161 }
162
163 #[test]
164 fn flat_key_stream_is_not_a_manifest_and_has_no_children() {
165 let mut flat = Vec::new();
166 flat.extend_from_slice(&4u64.to_be_bytes());
167 flat.extend_from_slice(b"key0");
168 assert!(!is_index_manifest(&flat));
169 assert!(index_blob_children(&flat).unwrap().is_empty());
170 assert!(index_blob_children(&[]).unwrap().is_empty());
171 }
172
173 #[test]
174 fn corrupt_manifest_is_an_error_not_a_flat_blob() {
175 let mut encoded = encode_index_manifest(&[digest(b"a")]);
176 encoded.extend_from_slice(b"not-a-blob-id\n");
177 assert!(decode_index_manifest(&encoded).is_err());
178 assert!(index_blob_children(&encoded).is_err());
179 }
180
181 #[test]
182 fn chunks_concatenate_back_to_the_input() {
183 let keys = keys(10_000);
184 let chunks = chunk_segment_keys(keys.iter().map(Vec::as_slice));
185 assert!(chunks.len() > 1, "10k keys should span several chunks");
186 let mut decoded = Vec::new();
187 for chunk in &chunks {
188 let mut input = chunk.as_slice();
189 while !input.is_empty() {
190 let (len, rest) = input.split_at(8);
191 let len = usize::try_from(u64::from_be_bytes(len.try_into().unwrap())).unwrap();
192 let (key, rest) = rest.split_at(len);
193 decoded.push(key.to_vec());
194 input = rest;
195 }
196 }
197 assert_eq!(decoded, keys);
198 }
199
200 #[test]
201 fn encoding_a_chunks_keys_matches_the_chunker() {
202 let keys = keys(10_000);
203 let chunks = chunk_segment_keys(keys.iter().map(Vec::as_slice));
204 let mut offset = 0;
205 for chunk in &chunks {
206 let count = decode_segment_keys(chunk).unwrap().len();
207 let encoded =
208 encode_segment_chunk(keys[offset..offset + count].iter().map(Vec::as_slice));
209 assert_eq!(&encoded, chunk);
210 offset += count;
211 }
212 assert_eq!(offset, keys.len());
213 }
214
215 #[test]
216 fn chunking_is_deterministic() {
217 let keys = keys(5_000);
218 let first = chunk_segment_keys(keys.iter().map(Vec::as_slice));
219 let second = chunk_segment_keys(keys.iter().map(Vec::as_slice));
220 assert_eq!(first, second);
221 }
222
223 #[test]
224 fn empty_input_yields_no_chunks() {
225 assert!(chunk_segment_keys(std::iter::empty()).is_empty());
226 }
227
228 #[test]
229 fn appending_keys_reuses_every_settled_chunk() {
230 let original = keys(10_000);
231 let extended = keys(10_100);
232 let before: Vec<BlobId> = chunk_segment_keys(original.iter().map(Vec::as_slice))
233 .iter()
234 .map(|chunk| digest(chunk))
235 .collect();
236 let after: Vec<BlobId> = chunk_segment_keys(extended.iter().map(Vec::as_slice))
237 .iter()
238 .map(|chunk| digest(chunk))
239 .collect();
240 let shared = after.iter().filter(|id| before.contains(id)).count();
242 assert!(
243 shared >= before.len() - 1,
244 "expected at least {} shared chunks, found {shared}",
245 before.len() - 1
246 );
247 }
248
249 #[test]
250 fn inserting_a_key_rechunks_only_its_neighborhood() {
251 let original = keys(10_000);
252 let mut modified = original.clone();
253 modified.insert(4_000, b"inserted-key".to_vec());
254 let before: Vec<BlobId> = chunk_segment_keys(original.iter().map(Vec::as_slice))
255 .iter()
256 .map(|chunk| digest(chunk))
257 .collect();
258 let after: Vec<BlobId> = chunk_segment_keys(modified.iter().map(Vec::as_slice))
259 .iter()
260 .map(|chunk| digest(chunk))
261 .collect();
262 let fresh = after.iter().filter(|id| !before.contains(id)).count();
263 assert!(
264 fresh <= 2,
265 "one insertion should rewrite at most two chunks, rewrote {fresh} of {}",
266 after.len()
267 );
268 }
269}