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 flags: u8,
76}
77
78pub fn compact_image(
86 manifest_bytes: &[u8],
87 slab_bytes: &[u8],
88) -> Result<CompactionResult, CompactionError> {
89 let mut cursor = ManifestCursor::new(manifest_bytes);
91 let _header = parse_manifest_header(&mut cursor)?;
92
93 let prefix_end = {
96 let flags_start = cursor.position();
97 let _ = parse_feature_flags_section(&mut cursor)?;
98 let flags_end = cursor.position();
99 let meta_ref_start = cursor.position();
100 let meta_ref = parse_metadata_reference(&mut cursor)?;
101 let _meta_ref_end = cursor.position();
102 let _ = (flags_start, flags_end, meta_ref_start);
103
104 let referenced = find_referenced_drops(&meta_ref)?;
106
107 let slab_index_start = cursor.position();
109 let slab_index = parse_slab_index(&mut cursor)?;
110 let slab_index_end = cursor.position();
111 let _ = (slab_index_start, slab_index_end, &slab_index);
112
113 referenced
114 };
115
116 let view = limnifs_core::parse_slab(slab_bytes)?;
119 let original_count = view.drop_records().len();
120 let win_start = view.solid_window_offset();
121 let mut kept_drops: Vec<ExtractedDrop> = Vec::new();
122
123 for record in view.drop_records() {
124 if prefix_end.contains(record.drop_id.as_bytes()) {
125 let offset = usize::try_from(record.offset_in_window).unwrap_or(0);
126 let len = usize::try_from(record.len_in_window).unwrap_or(0);
127 let start = win_start + offset;
128 let end = start + len;
129 if end > slab_bytes.len() {
130 continue;
131 }
132 kept_drops.push(ExtractedDrop {
133 id: *record.drop_id.as_bytes(),
134 compressed: slab_bytes[start..end].to_vec(),
135 codec: record.representation.codec,
136 plaintext_len: record.plaintext_len,
137 flags: record.flags,
138 });
139 }
140 }
141
142 let compacted_count = kept_drops.len();
143 let reclaimed = original_count.saturating_sub(compacted_count);
144
145 let (new_slab_bytes, new_slab_id) = encode_compacted_slab(&kept_drops);
147
148 let new_manifest = reassemble_manifest(manifest_bytes, &new_slab_bytes, &new_slab_id)?;
150
151 Ok(CompactionResult {
152 manifest_bytes: new_manifest.bytes,
153 merkle_root: new_manifest.merkle_root,
154 slab_bytes: Some(new_slab_bytes),
155 original_drop_count: original_count,
156 compacted_drop_count: compacted_count,
157 reclaimed_drops: reclaimed,
158 })
159}
160
161fn find_referenced_drops(
169 meta_ref: &limnifs_core::MetadataReference,
170) -> Result<HashSet<[u8; 32]>, CompactionError> {
171 let mut referenced = HashSet::new();
172 if let Some(blob_bytes) = &meta_ref.inline_metadata {
173 let mut blob_cursor = ManifestCursor::new(blob_bytes);
174 let blob = parse_metadata_blob(&mut blob_cursor)?;
175 for inode in &blob.inodes {
176 if let ContentHandle::SliceMap(slices) = &inode.content_handle {
177 for slice in slices {
178 referenced.insert(*slice.drop_id.as_bytes());
179 }
180 }
181 }
182 }
183 Ok(referenced)
184}
185
186fn encode_compacted_slab(drops: &[ExtractedDrop]) -> (Vec<u8>, SlabId) {
188 let mut drop_records = Vec::new();
189 let mut solid_window = Vec::new();
190
191 for drop in drops {
192 let win_len = u32::try_from(drop.compressed.len()).unwrap_or(0);
193 let offset = u32::try_from(solid_window.len()).unwrap_or(0);
194 drop_records.extend_from_slice(&drop.id);
195 drop_records.extend_from_slice(&drop.plaintext_len.to_le_bytes());
196 drop_records.extend_from_slice(&[drop.codec, 0x00, 0x00]); drop_records.push(0x00); drop_records.extend_from_slice(&offset.to_le_bytes());
199 drop_records.extend_from_slice(&win_len.to_le_bytes());
200 drop_records.push(limnifs_core::drop_record::NO_DICT); drop_records.push(drop.flags); solid_window.extend_from_slice(&drop.compressed);
203 }
204
205 let slab_content = [&drop_records[..], &solid_window[..]].concat();
206 let slab_hash = hash_section(&slab_content);
207 let slab_id = SlabId::new(0, slab_hash);
208
209 let total_length = 56 + slab_content.len();
210 let mut slab_bytes = Vec::with_capacity(total_length);
211 slab_bytes.extend_from_slice(b"LIM1");
212 slab_bytes.extend_from_slice(&1u16.to_le_bytes()); slab_bytes.extend_from_slice(&slab_id.to_bytes());
214 slab_bytes.extend_from_slice(&(total_length as u64).to_le_bytes());
215 slab_bytes.push(0x00); slab_bytes.push(0x00); slab_bytes.extend_from_slice(&slab_content);
218
219 (slab_bytes, slab_id)
220}
221
222struct ReassembledManifest {
223 bytes: Vec<u8>,
224 merkle_root: ManifestRoot,
225}
226
227fn reassemble_manifest(
229 source: &[u8],
230 _slab_bytes: &[u8],
231 slab_id: &SlabId,
232) -> Result<ReassembledManifest, CompactionError> {
233 let mut cursor = ManifestCursor::new(source);
234
235 let header_start = cursor.position();
237 let _ = parse_manifest_header(&mut cursor)?;
238 let header_end = cursor.position();
239
240 let flags_start = cursor.position();
241 let _ = parse_feature_flags_section(&mut cursor)?;
242 let flags_end = cursor.position();
243
244 let meta_ref_start = cursor.position();
245 let meta_ref = parse_metadata_reference(&mut cursor)?;
246 let meta_ref_end = cursor.position();
247
248 let _ = parse_slab_index(&mut cursor)?;
250 let _ = limnifs_core::parse_history(&mut cursor)?;
251
252 let mut manifest = Vec::new();
254
255 manifest.extend_from_slice(&source[header_start..meta_ref_end]);
257
258 let slab_index_start_new = manifest.len();
260 manifest.push(SLAB_INDEX_SECTION_VERSION);
261 manifest.extend_from_slice(&1u32.to_le_bytes());
262 manifest.extend_from_slice(&slab_id.to_bytes());
263 manifest.extend_from_slice(&1u32.to_le_bytes());
264 let locator = "file:slab-0.bin";
265 manifest.extend_from_slice(&u32::try_from(locator.len()).unwrap_or(0).to_le_bytes());
266 manifest.extend_from_slice(locator.as_bytes());
267 let slab_index_end_new = manifest.len();
268
269 let history_start_new = manifest.len();
271 manifest.push(HISTORY_SECTION_VERSION);
272 manifest.extend_from_slice(&1u32.to_le_bytes());
273 manifest.push(0x04); manifest.extend_from_slice(&0u64.to_le_bytes());
275 manifest.extend_from_slice(&0u32.to_le_bytes());
276 manifest.extend_from_slice(&0u32.to_le_bytes());
277 let history_end_new = manifest.len();
278
279 let hashes = SectionHashes {
280 metadata: meta_ref.metadata_hash,
281 format_header: hash_section(&source[header_start..header_end]),
282 feature_flags: hash_section(&source[flags_start..flags_end]),
283 metadata_reference: hash_section(&source[meta_ref_start..meta_ref_end]),
284 slab_index: hash_section(&manifest[slab_index_start_new..slab_index_end_new]),
285 crypto_params: hash_empty_section(),
286 ec_params: hash_empty_section(),
287 dms_policy: hash_empty_section(),
288 delta_linkage: hash_empty_section(),
289 history: hash_section(&manifest[history_start_new..history_end_new]),
290 };
291 let merkle_root = compute_merkle_root(&hashes);
292
293 Ok(ReassembledManifest {
294 bytes: manifest,
295 merkle_root,
296 })
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn compact_preserves_referenced_drops() {
305 let temp =
306 std::env::temp_dir().join(format!("limnifs-compaction-test-{}", std::process::id()));
307 std::fs::create_dir_all(&temp).expect("create temp");
308 let data = vec![0xABu8; crate::INLINE_THRESHOLD + 100];
310 std::fs::write(temp.join("big.bin"), &data).expect("write big");
311
312 let artifact = crate::write_directory(&temp).expect("write");
313 std::fs::remove_dir_all(&temp).ok();
314
315 let slab_bytes = artifact.slab_bytes().map(Vec::from).unwrap_or_default();
316 if slab_bytes.is_empty() {
317 return;
318 }
319
320 let result = compact_image(&artifact.bytes, &slab_bytes).expect("compact");
321 assert_eq!(result.original_drop_count, result.compacted_drop_count);
322 assert_eq!(result.reclaimed_drops, 0);
323
324 let new_slab = result.slab_bytes.as_ref().expect("slab exists");
326 let view = limnifs_core::parse_slab(new_slab).expect("compacted slab parses");
327 assert_eq!(view.drop_records().len(), result.compacted_drop_count);
328 }
329
330 #[test]
331 fn compact_preserves_drop_plaintext() {
332 let temp =
333 std::env::temp_dir().join(format!("limnifs-compaction-pt-{}", std::process::id()));
334 std::fs::create_dir_all(&temp).expect("create temp");
335 let data = vec![0xCDu8; crate::INLINE_THRESHOLD + 200];
336 std::fs::write(temp.join("data.bin"), &data).expect("write");
337
338 let artifact = crate::write_directory(&temp).expect("write");
339 std::fs::remove_dir_all(&temp).ok();
340
341 let slab_bytes = artifact.slab_bytes().map(Vec::from).unwrap_or_default();
342 if slab_bytes.is_empty() {
343 return;
344 }
345
346 let result = compact_image(&artifact.bytes, &slab_bytes).expect("compact");
347 let new_slab = result.slab_bytes.as_ref().expect("slab");
348
349 let old_view = limnifs_core::parse_slab(&slab_bytes).expect("old slab parses");
351 let new_view = limnifs_core::parse_slab(new_slab).expect("new slab parses");
352
353 for old_record in old_view.drop_records() {
354 let old_pt = old_view
355 .plaintext_for(old_record.drop_id.as_bytes())
356 .expect("old drop exists")
357 .expect("decompress ok");
358 let new_pt = new_view
359 .plaintext_for(old_record.drop_id.as_bytes())
360 .expect("new drop exists")
361 .expect("decompress ok");
362 assert_eq!(old_pt, new_pt, "plaintext must match after compaction");
363 }
364 }
365
366 #[test]
367 fn compact_manifest_parses_correctly() {
368 let temp =
369 std::env::temp_dir().join(format!("limnifs-compaction-mp-{}", std::process::id()));
370 std::fs::create_dir_all(&temp).expect("create temp");
371 let data = vec![0xEFu8; crate::INLINE_THRESHOLD + 50];
372 std::fs::write(temp.join("file.bin"), &data).expect("write");
373
374 let artifact = crate::write_directory(&temp).expect("write");
375 std::fs::remove_dir_all(&temp).ok();
376
377 let slab_bytes = artifact.slab_bytes().map(Vec::from).unwrap_or_default();
378 if slab_bytes.is_empty() {
379 return;
380 }
381
382 let result = compact_image(&artifact.bytes, &slab_bytes).expect("compact");
383
384 let mut cursor = limnifs_core::ManifestCursor::new(&result.manifest_bytes);
386 limnifs_core::parse_manifest_header(&mut cursor).expect("header parses");
387 limnifs_core::parse_feature_flags_section(&mut cursor).expect("flags parse");
388 let meta_ref =
389 limnifs_core::parse_metadata_reference(&mut cursor).expect("metadata ref parses");
390 assert!(meta_ref.is_inlined());
391 limnifs_core::parse_slab_index(&mut cursor).expect("slab index parses");
392 limnifs_core::parse_history(&mut cursor).expect("history parses");
393 }
394}