1use std::collections::HashSet;
17
18use limnifs_core::{
19 compute_merkle_root, hash_empty_section, hash_section, parse_feature_flags_section,
20 parse_manifest_header, parse_metadata_blob, parse_metadata_reference, parse_slab_index,
21 ContentHandle, CoreError, ManifestCursor, SectionHashes, HISTORY_SECTION_VERSION,
22 SLAB_INDEX_SECTION_VERSION,
23};
24use limnifs_format::{ManifestRoot, SlabId};
25
26#[derive(Clone, Debug)]
28pub struct CompactionResult {
29 pub manifest_bytes: Vec<u8>,
30 pub merkle_root: ManifestRoot,
31 pub slab_bytes: Option<Vec<u8>>,
32 pub original_drop_count: usize,
33 pub compacted_drop_count: usize,
34 pub reclaimed_drops: usize,
35}
36
37#[derive(Debug)]
39pub enum CompactionError {
40 Core(CoreError),
41 Io(std::io::Error),
42}
43
44impl std::fmt::Display for CompactionError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::Core(e) => write!(f, "{e}"),
48 Self::Io(e) => write!(f, "I/O: {e}"),
49 }
50 }
51}
52
53impl std::error::Error for CompactionError {}
54
55impl From<CoreError> for CompactionError {
56 fn from(e: CoreError) -> Self {
57 Self::Core(e)
58 }
59}
60
61impl From<std::io::Error> for CompactionError {
62 fn from(e: std::io::Error) -> Self {
63 Self::Io(e)
64 }
65}
66
67struct ExtractedDrop {
69 id: [u8; 32],
70 compressed: Vec<u8>,
71 codec: u8,
72 plaintext_len: u32,
73}
74
75pub fn compact_image(
83 manifest_bytes: &[u8],
84 slab_bytes: &[u8],
85) -> Result<CompactionResult, CompactionError> {
86 let mut cursor = ManifestCursor::new(manifest_bytes);
88 let _header = parse_manifest_header(&mut cursor)?;
89
90 let prefix_end = {
93 let flags_start = cursor.position();
94 let _ = parse_feature_flags_section(&mut cursor)?;
95 let flags_end = cursor.position();
96 let meta_ref_start = cursor.position();
97 let meta_ref = parse_metadata_reference(&mut cursor)?;
98 let _meta_ref_end = cursor.position();
99 let _ = (flags_start, flags_end, meta_ref_start);
100
101 let referenced = find_referenced_drops(&meta_ref)?;
103
104 let slab_index_start = cursor.position();
106 let slab_index = parse_slab_index(&mut cursor)?;
107 let slab_index_end = cursor.position();
108 let _ = (slab_index_start, slab_index_end, &slab_index);
109
110 referenced
111 };
112
113 let view = limnifs_core::parse_slab(slab_bytes)?;
116 let original_count = view.drop_records().len();
117 let win_start = view.solid_window_offset();
118 let mut kept_drops: Vec<ExtractedDrop> = Vec::new();
119
120 for record in view.drop_records() {
121 if prefix_end.contains(record.drop_id.as_bytes()) {
122 let offset = usize::try_from(record.offset_in_window).unwrap_or(0);
123 let len = usize::try_from(record.len_in_window).unwrap_or(0);
124 let start = win_start + offset;
125 let end = start + len;
126 if end > slab_bytes.len() {
127 continue;
128 }
129 kept_drops.push(ExtractedDrop {
130 id: *record.drop_id.as_bytes(),
131 compressed: slab_bytes[start..end].to_vec(),
132 codec: record.representation.codec,
133 plaintext_len: record.plaintext_len,
134 });
135 }
136 }
137
138 let compacted_count = kept_drops.len();
139 let reclaimed = original_count.saturating_sub(compacted_count);
140
141 let (new_slab_bytes, new_slab_id) = encode_compacted_slab(&kept_drops);
143
144 let new_manifest = reassemble_manifest(manifest_bytes, &new_slab_bytes, &new_slab_id)?;
146
147 Ok(CompactionResult {
148 manifest_bytes: new_manifest.bytes,
149 merkle_root: new_manifest.merkle_root,
150 slab_bytes: Some(new_slab_bytes),
151 original_drop_count: original_count,
152 compacted_drop_count: compacted_count,
153 reclaimed_drops: reclaimed,
154 })
155}
156
157fn find_referenced_drops(
159 meta_ref: &limnifs_core::MetadataReference,
160) -> Result<HashSet<[u8; 32]>, CompactionError> {
161 let mut referenced = HashSet::new();
162 if let Some(blob_bytes) = &meta_ref.inline_metadata {
163 let mut blob_cursor = ManifestCursor::new(blob_bytes);
164 let blob = parse_metadata_blob(&mut blob_cursor)?;
165 for inode in &blob.inodes {
166 if let ContentHandle::SliceMap(slices) = &inode.content_handle {
167 for slice in slices {
168 referenced.insert(*slice.drop_id.as_bytes());
169 }
170 }
171 }
172 }
173 Ok(referenced)
174}
175
176fn encode_compacted_slab(drops: &[ExtractedDrop]) -> (Vec<u8>, SlabId) {
178 let mut drop_records = Vec::new();
179 let mut solid_window = Vec::new();
180
181 for drop in drops {
182 let win_len = u32::try_from(drop.compressed.len()).unwrap_or(0);
183 let offset = u32::try_from(solid_window.len()).unwrap_or(0);
184 drop_records.extend_from_slice(&drop.id);
185 drop_records.extend_from_slice(&drop.plaintext_len.to_le_bytes());
186 drop_records.extend_from_slice(&[drop.codec, 0x00, 0x00]); drop_records.push(0x00); drop_records.extend_from_slice(&offset.to_le_bytes());
189 drop_records.extend_from_slice(&win_len.to_le_bytes());
190 drop_records.push(limnifs_core::drop_record::NO_DICT); solid_window.extend_from_slice(&drop.compressed);
192 }
193
194 let slab_content = [&drop_records[..], &solid_window[..]].concat();
195 let slab_hash = hash_section(&slab_content);
196 let slab_id = SlabId::new(0, slab_hash);
197
198 let total_length = 56 + slab_content.len();
199 let mut slab_bytes = Vec::with_capacity(total_length);
200 slab_bytes.extend_from_slice(b"LIM1");
201 slab_bytes.extend_from_slice(&1u16.to_le_bytes());
202 slab_bytes.extend_from_slice(&slab_id.to_bytes());
203 slab_bytes.extend_from_slice(&(total_length as u64).to_le_bytes());
204 slab_bytes.push(0x00); slab_bytes.push(0x00); slab_bytes.extend_from_slice(&slab_content);
207
208 (slab_bytes, slab_id)
209}
210
211struct ReassembledManifest {
212 bytes: Vec<u8>,
213 merkle_root: ManifestRoot,
214}
215
216fn reassemble_manifest(
218 source: &[u8],
219 _slab_bytes: &[u8],
220 slab_id: &SlabId,
221) -> Result<ReassembledManifest, CompactionError> {
222 let mut cursor = ManifestCursor::new(source);
223
224 let header_start = cursor.position();
226 let _ = parse_manifest_header(&mut cursor)?;
227 let header_end = cursor.position();
228
229 let flags_start = cursor.position();
230 let _ = parse_feature_flags_section(&mut cursor)?;
231 let flags_end = cursor.position();
232
233 let meta_ref_start = cursor.position();
234 let meta_ref = parse_metadata_reference(&mut cursor)?;
235 let meta_ref_end = cursor.position();
236
237 let _ = parse_slab_index(&mut cursor)?;
239 let _ = limnifs_core::parse_history(&mut cursor)?;
240
241 let mut manifest = Vec::new();
243
244 manifest.extend_from_slice(&source[header_start..meta_ref_end]);
246
247 let slab_index_start_new = manifest.len();
249 manifest.push(SLAB_INDEX_SECTION_VERSION);
250 manifest.extend_from_slice(&1u32.to_le_bytes());
251 manifest.extend_from_slice(&slab_id.to_bytes());
252 manifest.extend_from_slice(&1u32.to_le_bytes());
253 let locator = "file:slab-0.bin";
254 manifest.extend_from_slice(&u32::try_from(locator.len()).unwrap_or(0).to_le_bytes());
255 manifest.extend_from_slice(locator.as_bytes());
256 let slab_index_end_new = manifest.len();
257
258 let history_start_new = manifest.len();
260 manifest.push(HISTORY_SECTION_VERSION);
261 manifest.extend_from_slice(&1u32.to_le_bytes());
262 manifest.push(0x04); manifest.extend_from_slice(&0u64.to_le_bytes());
264 manifest.extend_from_slice(&0u32.to_le_bytes());
265 manifest.extend_from_slice(&0u32.to_le_bytes());
266 let history_end_new = manifest.len();
267
268 let hashes = SectionHashes {
269 metadata: meta_ref.metadata_hash,
270 format_header: hash_section(&source[header_start..header_end]),
271 feature_flags: hash_section(&source[flags_start..flags_end]),
272 metadata_reference: hash_section(&source[meta_ref_start..meta_ref_end]),
273 slab_index: hash_section(&manifest[slab_index_start_new..slab_index_end_new]),
274 crypto_params: hash_empty_section(),
275 ec_params: hash_empty_section(),
276 dms_policy: hash_empty_section(),
277 delta_linkage: hash_empty_section(),
278 history: hash_section(&manifest[history_start_new..history_end_new]),
279 };
280 let merkle_root = compute_merkle_root(&hashes);
281
282 Ok(ReassembledManifest {
283 bytes: manifest,
284 merkle_root,
285 })
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn compact_preserves_referenced_drops() {
294 let temp =
295 std::env::temp_dir().join(format!("limnifs-compaction-test-{}", std::process::id()));
296 std::fs::create_dir_all(&temp).expect("create temp");
297 let data = vec![0xABu8; crate::INLINE_THRESHOLD + 100];
299 std::fs::write(temp.join("big.bin"), &data).expect("write big");
300
301 let artifact = crate::write_directory(&temp).expect("write");
302 std::fs::remove_dir_all(&temp).ok();
303
304 let slab_bytes = artifact.slab_bytes().map(Vec::from).unwrap_or_default();
305 if slab_bytes.is_empty() {
306 return;
307 }
308
309 let result = compact_image(&artifact.bytes, &slab_bytes).expect("compact");
310 assert_eq!(result.original_drop_count, result.compacted_drop_count);
311 assert_eq!(result.reclaimed_drops, 0);
312
313 let new_slab = result.slab_bytes.as_ref().expect("slab exists");
315 let view = limnifs_core::parse_slab(new_slab).expect("compacted slab parses");
316 assert_eq!(view.drop_records().len(), result.compacted_drop_count);
317 }
318
319 #[test]
320 fn compact_preserves_drop_plaintext() {
321 let temp =
322 std::env::temp_dir().join(format!("limnifs-compaction-pt-{}", std::process::id()));
323 std::fs::create_dir_all(&temp).expect("create temp");
324 let data = vec![0xCDu8; crate::INLINE_THRESHOLD + 200];
325 std::fs::write(temp.join("data.bin"), &data).expect("write");
326
327 let artifact = crate::write_directory(&temp).expect("write");
328 std::fs::remove_dir_all(&temp).ok();
329
330 let slab_bytes = artifact.slab_bytes().map(Vec::from).unwrap_or_default();
331 if slab_bytes.is_empty() {
332 return;
333 }
334
335 let result = compact_image(&artifact.bytes, &slab_bytes).expect("compact");
336 let new_slab = result.slab_bytes.as_ref().expect("slab");
337
338 let old_view = limnifs_core::parse_slab(&slab_bytes).expect("old slab parses");
340 let new_view = limnifs_core::parse_slab(new_slab).expect("new slab parses");
341
342 for old_record in old_view.drop_records() {
343 let old_pt = old_view
344 .plaintext_for(old_record.drop_id.as_bytes())
345 .expect("old drop exists")
346 .expect("decompress ok");
347 let new_pt = new_view
348 .plaintext_for(old_record.drop_id.as_bytes())
349 .expect("new drop exists")
350 .expect("decompress ok");
351 assert_eq!(old_pt, new_pt, "plaintext must match after compaction");
352 }
353 }
354
355 #[test]
356 fn compact_manifest_parses_correctly() {
357 let temp =
358 std::env::temp_dir().join(format!("limnifs-compaction-mp-{}", std::process::id()));
359 std::fs::create_dir_all(&temp).expect("create temp");
360 let data = vec![0xEFu8; crate::INLINE_THRESHOLD + 50];
361 std::fs::write(temp.join("file.bin"), &data).expect("write");
362
363 let artifact = crate::write_directory(&temp).expect("write");
364 std::fs::remove_dir_all(&temp).ok();
365
366 let slab_bytes = artifact.slab_bytes().map(Vec::from).unwrap_or_default();
367 if slab_bytes.is_empty() {
368 return;
369 }
370
371 let result = compact_image(&artifact.bytes, &slab_bytes).expect("compact");
372
373 let mut cursor = limnifs_core::ManifestCursor::new(&result.manifest_bytes);
375 limnifs_core::parse_manifest_header(&mut cursor).expect("header parses");
376 limnifs_core::parse_feature_flags_section(&mut cursor).expect("flags parse");
377 let meta_ref =
378 limnifs_core::parse_metadata_reference(&mut cursor).expect("metadata ref parses");
379 assert!(meta_ref.is_inlined());
380 limnifs_core::parse_slab_index(&mut cursor).expect("slab index parses");
381 limnifs_core::parse_history(&mut cursor).expect("history parses");
382 }
383}