1#![deny(unsafe_code)]
21#![allow(warnings)]
22
23pub mod chunker;
24pub mod classifier;
25pub mod compaction;
26pub mod config;
27pub mod delta_builder;
28pub mod dictionary;
29pub mod file_categorizer;
30pub mod flatten;
31#[cfg(feature = "pipeline-parallelism")]
32pub mod pipeline;
33pub mod rw;
34#[cfg(feature = "sparse-index")]
35pub mod sparse_index;
36pub mod turnover;
37
38pub use config::{
39 profile, CategorizerConfig, ChunkingConfig, CodecRegistry, CodecTunables, Defaults,
40 DictionaryConfig, EncryptionConfig, TournamentConfig, WriteConfig,
41};
42
43use std::collections::{HashMap, HashSet};
44use std::path::{Path, PathBuf};
45
46use crate::chunker::FastCDC;
47use limnifs_core::codec::CODEC_REFERENCED;
48use limnifs_core::slab_store::SlabStore;
49use limnifs_core::{
50 compute_merkle_root, hash_empty_section, hash_section, parse_manifest_header, parse_slab_index,
51 ManifestCursor, ManifestHeader, SectionHashes, FEATURE_FLAGS_SECTION_VERSION,
52 HISTORY_SECTION_VERSION, INODE_FLAG_INLINE_DATA, INODE_FLAG_SHARED_INLINE,
53 METADATA_REFERENCE_SECTION_VERSION_2, SLAB_INDEX_SECTION_VERSION,
54};
55use limnifs_format::{ManifestRoot, SlabId};
56
57pub const INLINE_THRESHOLD: usize = 4096;
60
61pub const MMAP_READ_THRESHOLD: usize = 1024 * 1024;
67
68pub const WHOLE_FILE_MAX_SIZE: usize = 64 * 1024 * 1024;
73
74pub const MAX_SLAB_TOTAL_BYTES: usize = 60 * 1024 * 1024;
79
80const SLAB_HEADER_LEN: usize = 56;
84
85pub const METADATA_EXTERNALIZE_THRESHOLD: usize =
93 limnifs_core::metadata_reference::DEFAULT_INLINE_METADATA_MAX_BYTES as usize - 24 * 1024;
94
95pub const METADATA_LARGE_BLOB_THRESHOLD: usize = 256 * 1024;
100
101pub const METADATA_SMALL_BLOB_QUALITY: i32 = 5;
104
105pub const METADATA_LARGE_BLOB_QUALITY: i32 = 2;
110
111#[derive(Clone, Debug)]
114pub struct SlabArtifact {
115 pub id: SlabId,
116 pub bytes: Vec<u8>,
117 pub locator: String,
118 pub drop_ids: Vec<[u8; 32]>,
122}
123
124#[derive(Clone, Debug)]
128pub struct MetadataSidecar {
129 pub bytes: Vec<u8>,
130 pub locator: String,
131}
132
133#[derive(Clone, Debug)]
135pub struct WriteArtifact {
136 pub bytes: Vec<u8>,
137 pub merkle_root: ManifestRoot,
138 pub slabs: Vec<SlabArtifact>,
141 pub metadata_sidecar: Option<MetadataSidecar>,
145 pub inode_count: usize,
146 pub file_count: usize,
147 pub dir_count: usize,
148 pub drop_count: usize,
149 pub root_inode_number: u64,
154}
155
156impl WriteArtifact {
157 #[must_use]
161 pub fn slab_bytes(&self) -> Option<&[u8]> {
162 if self.slabs.len() == 1 {
163 Some(&self.slabs[0].bytes)
164 } else {
165 None
166 }
167 }
168
169 #[must_use]
171 pub fn slab_locator(&self) -> Option<&str> {
172 if self.slabs.len() == 1 {
173 Some(&self.slabs[0].locator)
174 } else {
175 None
176 }
177 }
178}
179
180#[derive(Debug)]
182pub enum WriteError {
183 Io(std::io::Error),
184}
185
186impl std::fmt::Display for WriteError {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Self::Io(e) => write!(f, "I/O error: {e}"),
190 }
191 }
192}
193
194impl std::error::Error for WriteError {}
195
196impl From<std::io::Error> for WriteError {
197 fn from(e: std::io::Error) -> Self {
198 Self::Io(e)
199 }
200}
201
202pub fn write_directory(root: &Path) -> Result<WriteArtifact, WriteError> {
216 write_directory_with_config(root, &WriteConfig::default_v0_1())
217}
218
219pub fn write_stream<R: std::io::Read>(
235 name: &str,
236 reader: R,
237 config: &WriteConfig,
238) -> Result<WriteArtifact, WriteError> {
239 let mut ctx = WriteContext::new();
240 ctx.categorizers_disabled = config.categorizers.is_empty();
241 ctx.rw_mode = matches!(config.mode, crate::config::ImageMode::ReadWrite(_));
242 ctx.auto_turnover = config.turnover_threshold > 0;
243 ctx.collect_dict_samples = config.dictionaries.enabled;
244
245 let drop_id_root = [0u8; 32]; let pending = PendingFile {
250 path: std::path::PathBuf::from(name),
251 inode_number: 1,
252 file_len: 0, mtime_ns: 0,
254 };
255 ctx.pending_files.push(pending);
256 ctx.root_inode_number = 1;
257
258 let chunker = ctx.chunker.clone();
260 let chunks = chunker.chunk_reader(reader)?;
261
262 let total_len: u64 = chunks.iter().map(|c| c.len() as u64).sum();
264
265 let text_codec = config.text_codec_id().unwrap_or(0x04);
269 let binary_codec = config.binary_codec_id().unwrap_or(0x01);
270 let tunables = config.to_core_tunables();
271 let classifier = ctx.classifier;
272 let registry = config
273 .codec_registry()
274 .map_err(|e| WriteError::Io(std::io::Error::other(format!("codec registry: {e}"))))?;
275 let tournament_codec_ids: Vec<u8> = config
276 .tournament
277 .codecs
278 .iter()
279 .filter_map(|n| registry.lookup_by_name(n))
280 .collect();
281 let tournament = TournamentSpec {
282 codec_ids: tournament_codec_ids,
283 min_size: config.tournament.min_size_threshold as usize,
284 skip_for_binary: config.tournament.skip_for_binary,
285 short_circuit_permille: config.tournament.short_circuit_threshold,
286 };
287
288 let mut drops: Vec<RawDrop> = Vec::with_capacity(chunks.len());
289 let mut slices: Vec<PendingSlice> = Vec::with_capacity(chunks.len());
290 let mut offset: u64 = 0;
291 for chunk in &chunks {
292 let drop_id = hash_section(chunk);
293 slices.push(PendingSlice {
294 drop_id,
295 file_byte_start: offset,
296 file_byte_end: offset + chunk.len() as u64,
297 });
298 offset += chunk.len() as u64;
299 let class = classifier.classify(chunk);
300 let (codec_id, compressed) = compress_chunk_with_tournament(
301 chunk,
302 class,
303 text_codec,
304 binary_codec,
305 &tunables,
306 &tournament,
307 );
308 drops.push((drop_id, chunk.clone(), compressed, codec_id));
309 }
310 let _ = drop_id_root;
311
312 let result = ChunkedFileResult { drops, slices };
314 let pf = ctx.pending_files[0].clone();
315 ctx.merge_chunked_file(&pf, result);
316 ctx.pending_files[0].file_len = total_len;
318 if let Some(inode) = ctx.inodes.last_mut() {
321 if let PendingContent::DropBacked { file_len, .. } = &mut inode.content {
322 *file_len = total_len;
323 }
324 }
325
326 ctx.train_and_apply_dictionary(&config.dictionaries);
327 let artifact = ctx.assemble();
328 Ok(artifact)
329}
330
331pub fn write_layer(
372 base_image: &Path,
373 root: &Path,
374 config: &WriteConfig,
375) -> Result<WriteArtifact, WriteError> {
376 let (base_drop_index, base_root) = load_base_drop_index(base_image)?;
378
379 let mut ctx = WriteContext::new();
380 ctx.categorizers_disabled = config.categorizers.is_empty();
381 ctx.rw_mode = matches!(config.mode, crate::config::ImageMode::ReadWrite(_));
382 ctx.auto_turnover = config.turnover_threshold > 0;
383 ctx.collect_dict_samples = config.dictionaries.enabled;
384 ctx.inline_threshold = config.defaults.inline_threshold as usize;
385 ctx.metadata_externalize_threshold = config.defaults.metadata_externalize_threshold;
386 ctx.base_drop_index = Some(base_drop_index);
387 ctx.base_root = Some(base_root);
388
389 let root_inode_number = ctx.walk(root)?;
391 ctx.root_inode_number = root_inode_number;
392 write_directory_body(&mut ctx, config)?;
393 Ok(ctx.assemble())
394}
395
396fn load_base_drop_index(
400 base_image: &Path,
401) -> Result<(std::collections::HashSet<[u8; 32]>, [u8; 32]), WriteError> {
402 let manifest_bytes = std::fs::read(base_image)?;
403 let mut cursor = ManifestCursor::new(&manifest_bytes);
404 let _ = parse_manifest_header(&mut cursor).map_err(io_core)?;
405 let _ = limnifs_core::parse_feature_flags_section(&mut cursor);
407 let _ = limnifs_core::parse_metadata_reference(&mut cursor);
408 let slab_index = parse_slab_index(&mut cursor).map_err(io_core)?;
409 let store = SlabStore::load_mmap(base_image, &slab_index).map_err(io_core)?;
410 let drop_set: std::collections::HashSet<[u8; 32]> = store.drop_index_keys().copied().collect();
411 let root = *compute_merkle_root_from_sections(&manifest_bytes).as_bytes();
417 Ok((drop_set, root))
418}
419
420fn compute_merkle_root_from_sections(manifest: &[u8]) -> ManifestRoot {
426 use limnifs_core::SectionHashes;
427 let mut cursor = ManifestCursor::new(manifest);
428 let header_start = 0;
429 if parse_manifest_header(&mut cursor).is_err() {
430 return ManifestRoot::from_bytes([0u8; 32]);
432 }
433 let header_end = cursor.position();
434 let flags_start = header_end;
436 let flags_end = match limnifs_core::parse_feature_flags_section(&mut cursor) {
437 Ok(_) => cursor.position(),
438 Err(_) => flags_start,
439 };
440 let meta_ref_start = flags_end;
441 let metadata_reference = match limnifs_core::parse_metadata_reference(&mut cursor) {
442 Ok(m) => Some(m),
443 Err(_) => None,
444 };
445 let meta_ref_end = cursor.position();
446 let slab_index_start = meta_ref_end;
447 let _ = parse_slab_index(&mut cursor);
448 let slab_index_end = cursor.position();
449 let history_start = slab_index_end;
450 let _ = limnifs_core::parse_history(&mut cursor);
451 let history_end = cursor.position();
452
453 let hashes = SectionHashes {
454 metadata: metadata_reference
455 .map(|m| m.metadata_hash)
456 .unwrap_or_else(hash_empty_section),
457 format_header: hash_section(&manifest[header_start..header_end]),
458 feature_flags: hash_section(&manifest[flags_start..flags_end]),
459 metadata_reference: hash_section(&manifest[meta_ref_start..meta_ref_end]),
460 slab_index: hash_section(&manifest[slab_index_start..slab_index_end]),
461 crypto_params: hash_empty_section(),
462 ec_params: hash_empty_section(),
463 dms_policy: hash_empty_section(),
464 delta_linkage: hash_empty_section(),
465 history: hash_section(&manifest[history_start..history_end]),
466 };
467 compute_merkle_root(&hashes)
468}
469
470fn io_core(e: limnifs_core::CoreError) -> WriteError {
471 WriteError::Io(std::io::Error::other(format!("base image load: {e}")))
472}
473
474fn write_directory_body(ctx: &mut WriteContext, config: &WriteConfig) -> Result<(), WriteError> {
479 use rayon::prelude::*;
480
481 ctx.metadata_codec = config
482 .metadata_codec_id()
483 .unwrap_or(limnifs_core::codec::CODEC_BROTLI);
484
485 let pending = std::mem::take(&mut ctx.pending_files);
486 if pending.is_empty() {
487 return Ok(());
488 }
489 ctx.inline_threshold = config.defaults.inline_threshold as usize;
490 ctx.metadata_externalize_threshold = config.defaults.metadata_externalize_threshold;
491 let chunker = ctx.chunker.clone();
492 let classifier = ctx.classifier;
493 let text_codec = config.text_codec_id().unwrap_or(0x04);
494 let binary_codec = config.binary_codec_id().unwrap_or(0x01);
495 let tunables = config.to_core_tunables();
496 let use_categorizers = !config.categorizers.is_empty();
497 let skip_chunking = config.skip_chunking;
498 let registry = config
499 .codec_registry()
500 .map_err(|e| WriteError::Io(std::io::Error::other(format!("codec registry: {e}"))))?;
501 let tournament_codec_ids: Vec<u8> = config
502 .tournament
503 .codecs
504 .iter()
505 .filter_map(|n| registry.lookup_by_name(n))
506 .collect();
507 let tournament_spec = TournamentSpec {
508 codec_ids: tournament_codec_ids,
509 min_size: config.tournament.min_size_threshold as usize,
510 skip_for_binary: config.tournament.skip_for_binary,
511 short_circuit_permille: config.tournament.short_circuit_threshold,
512 };
513 let base_drop_index = ctx.base_drop_index.as_ref();
514 let inline_threshold = ctx.inline_threshold;
515 let results: Vec<ChunkedFileResult> = pending
516 .par_iter()
517 .map(|pf| {
518 process_file(
519 pf,
520 &chunker,
521 classifier,
522 text_codec,
523 binary_codec,
524 &tunables,
525 use_categorizers,
526 skip_chunking,
527 &tournament_spec,
528 base_drop_index,
529 inline_threshold,
530 )
531 })
532 .collect::<Result<Vec<_>, _>>()?;
533
534 for (pf, result) in pending.iter().zip(results) {
535 ctx.merge_chunked_file(pf, result);
536 }
537 ctx.train_and_apply_dictionary(&config.dictionaries);
538 Ok(())
539}
540
541pub fn write_directory_with_config(
543 root: &Path,
544 config: &WriteConfig,
545) -> Result<WriteArtifact, WriteError> {
546 let mut ctx = WriteContext::new();
547 ctx.categorizers_disabled = config.categorizers.is_empty();
548 ctx.rw_mode = matches!(config.mode, crate::config::ImageMode::ReadWrite(_));
549 ctx.auto_turnover = config.turnover_threshold > 0;
550 ctx.collect_dict_samples = config.dictionaries.enabled;
551
552 write_directory_streaming(&mut ctx, root, config)?;
553 Ok(ctx.assemble())
554}
555
556fn write_directory_streaming(
570 ctx: &mut WriteContext,
571 root: &Path,
572 config: &WriteConfig,
573) -> Result<(), WriteError> {
574 use rayon::prelude::*;
575
576 ctx.metadata_codec = config
577 .metadata_codec_id()
578 .unwrap_or(limnifs_core::codec::CODEC_BROTLI);
579
580 let chunker = ctx.chunker.clone();
581 let classifier = ctx.classifier;
582 let text_codec = config.text_codec_id().unwrap_or(0x04);
583 let binary_codec = config.binary_codec_id().unwrap_or(0x01);
584 let tunables = config.to_core_tunables();
585 let use_categorizers = !config.categorizers.is_empty();
586 let skip_chunking = config.skip_chunking;
587 let registry = config
588 .codec_registry()
589 .map_err(|e| WriteError::Io(std::io::Error::other(format!("codec registry: {e}"))))?;
590 let tournament_codec_ids: Vec<u8> = config
591 .tournament
592 .codecs
593 .iter()
594 .filter_map(|n| registry.lookup_by_name(n))
595 .collect();
596 let tournament_spec = TournamentSpec {
597 codec_ids: tournament_codec_ids,
598 min_size: config.tournament.min_size_threshold as usize,
599 skip_for_binary: config.tournament.skip_for_binary,
600 short_circuit_permille: config.tournament.short_circuit_threshold,
601 };
602 let base_drop_index = ctx.base_drop_index.clone();
605 let inline_threshold = ctx.inline_threshold;
606
607 ctx.inline_threshold = config.defaults.inline_threshold as usize;
608 ctx.metadata_externalize_threshold = config.defaults.metadata_externalize_threshold;
609
610 const PIPELINE_CAPACITY: usize = 256;
614 let (tx, rx) = std::sync::mpsc::sync_channel::<PendingFile>(PIPELINE_CAPACITY);
615 ctx.pending_sink = Some(tx);
616
617 let (root_inode_number, mut results): (
618 u64,
619 Vec<(usize, PendingFile, Result<ChunkedFileResult, WriteError>)>,
620 ) = std::thread::scope(|scope| {
621 let producer = {
622 let ctx = &mut *ctx;
623 let root = root;
624 scope.spawn(move || {
625 let r = ctx.walk(root);
626 ctx.pending_sink = None;
630 r
631 })
632 };
633 let results = rx
636 .into_iter()
637 .enumerate()
638 .par_bridge()
639 .map(|(i, pf)| {
640 let r = process_file(
641 &pf,
642 &chunker,
643 classifier,
644 text_codec,
645 binary_codec,
646 &tunables,
647 use_categorizers,
648 skip_chunking,
649 &tournament_spec,
650 base_drop_index.as_ref(),
651 inline_threshold,
652 );
653 (i, pf, r)
654 })
655 .collect();
656 let joined = producer
657 .join()
658 .unwrap_or_else(|_| {
659 Err(WriteError::Io(std::io::Error::other(
660 "walk thread panicked",
661 )))
662 })
663 .map(|n| (n, results));
664 joined
667 })?;
668 ctx.pending_sink = None;
669 ctx.root_inode_number = root_inode_number;
670
671 results.sort_unstable_by_key(|(i, _, _)| *i);
672 for (_, pf, r) in results {
676 ctx.merge_chunked_file(&pf, r?);
677 }
678 ctx.train_and_apply_dictionary(&config.dictionaries);
679 Ok(())
680}
681
682pub(crate) type RawDrop = ([u8; 32], Vec<u8>, std::sync::Arc<[u8]>, u8);
689pub(crate) struct ChunkedFileResult {
691 drops: Vec<RawDrop>, slices: Vec<PendingSlice>,
693}
694
695struct TournamentSpec {
703 codec_ids: Vec<u8>,
707 min_size: usize,
711 skip_for_binary: bool,
715 short_circuit_permille: u32,
720}
721
722fn process_whole_file_drop(
735 pf: &PendingFile,
736 data: &[u8],
737 cat: file_categorizer::Categorization,
738 tunables: &limnifs_core::codec::CodecTunables,
739) -> Result<ChunkedFileResult, WriteError> {
740 let _ = pf;
741 let drop_id = hash_section(data);
742 let file_len = u64::try_from(data.len()).unwrap_or(u64::MAX);
743
744 let (mut best_codec, mut best_compressed): (u8, std::sync::Arc<[u8]>) =
749 match limnifs_core::codec::compress_with_tunables(
750 limnifs_core::codec::CODEC_BROTLI,
751 data,
752 tunables,
753 ) {
754 Ok(c) => (limnifs_core::codec::CODEC_BROTLI, c.into()),
755 Err(_) => match limnifs_core::codec::compress_with_tunables(
756 limnifs_core::codec::CODEC_ZSTD,
757 data,
758 tunables,
759 ) {
760 Ok(c) => (limnifs_core::codec::CODEC_ZSTD, c.into()),
761 Err(_) => (limnifs_core::codec::CODEC_STORE, data.to_vec().into()),
762 },
763 };
764
765 let brotli_ratio = best_compressed.len() as f64 / data.len() as f64;
769 if brotli_ratio > 0.05 {
770 if let Ok(zstd_c) = limnifs_core::codec::compress_with_tunables(
771 limnifs_core::codec::CODEC_ZSTD,
772 data,
773 tunables,
774 ) {
775 if zstd_c.len() < best_compressed.len() {
776 best_codec = limnifs_core::codec::CODEC_ZSTD;
777 best_compressed = zstd_c.into();
778 }
779 }
780 }
781
782 let general_ratio = best_compressed.len() as f64 / data.len() as f64;
788 if general_ratio > 0.15 || cat.codec_id == limnifs_core::codec::CODEC_RICEPP {
789 let spec_result = if cat.codec_id == limnifs_core::codec::CODEC_FSST_BROTLI {
793 limnifs_core::codec::fsst_brotli::compress_with_baseline(data, Some(&best_compressed))
794 } else {
795 limnifs_core::codec::compress(cat.codec_id, data)
796 };
797 if let Ok(spec_c) = spec_result {
798 if spec_c.len() < best_compressed.len() {
799 best_codec = cat.codec_id;
800 best_compressed = spec_c.into();
801 }
802 }
803 }
804
805 Ok(ChunkedFileResult {
806 drops: vec![(drop_id, data.to_vec(), best_compressed, best_codec)],
807 slices: vec![PendingSlice {
808 drop_id,
809 file_byte_start: 0,
810 file_byte_end: file_len,
811 }],
812 })
813}
814
815fn compress_chunk_with_tournament(
837 chunk: &[u8],
838 class: classifier::Class,
839 text_codec: u8,
840 binary_codec: u8,
841 tunables: &limnifs_core::codec::CodecTunables,
842 tournament: &TournamentSpec,
843) -> (u8, std::sync::Arc<[u8]>) {
844 use classifier::Class;
845
846 let preferred = match class {
847 Class::Binary => binary_codec,
848 Class::Text | Class::Code | Class::Sparse => text_codec,
849 _ => limnifs_core::codec::CODEC_STORE,
850 };
851
852 if preferred == limnifs_core::codec::CODEC_STORE {
853 return (limnifs_core::codec::CODEC_STORE, chunk.to_vec().into());
854 }
855 if class == Class::Binary && tournament.skip_for_binary {
856 return compress_chunk_one(chunk, preferred, tunables);
857 }
858 if chunk.len() < tournament.min_size {
859 return compress_chunk_one(chunk, preferred, tunables);
860 }
861
862 let mut best: Option<(u8, std::sync::Arc<[u8]>)> = None;
863 for &codec_id in &tournament.codec_ids {
864 if codec_id == limnifs_core::codec::CODEC_STORE {
865 continue;
866 }
867 let c = match limnifs_core::codec::compress_with_tunables(codec_id, chunk, tunables) {
868 Ok(c) => c,
869 Err(_) => continue,
870 };
871 if c.len() >= chunk.len() {
872 continue;
873 }
874 let ratio_permille = (c.len() as u64 * 1000 / chunk.len() as u64) as u32;
875 let is_best_so_far = best.as_ref().map_or(true, |(_, b)| c.len() < b.len());
876 if is_best_so_far {
877 best = Some((codec_id, c.into()));
878 }
879 if tournament.short_circuit_permille > 0
880 && ratio_permille <= tournament.short_circuit_permille
881 {
882 break;
883 }
884 }
885
886 best.unwrap_or_else(|| (limnifs_core::codec::CODEC_STORE, chunk.to_vec().into()))
887}
888
889fn compress_chunk_one(
892 chunk: &[u8],
893 codec_id: u8,
894 tunables: &limnifs_core::codec::CodecTunables,
895) -> (u8, std::sync::Arc<[u8]>) {
896 if codec_id == limnifs_core::codec::CODEC_STORE {
897 return (limnifs_core::codec::CODEC_STORE, chunk.to_vec().into());
898 }
899 match limnifs_core::codec::compress_with_tunables(codec_id, chunk, tunables) {
900 Ok(c) if c.len() < chunk.len() => (codec_id, c.into()),
901 _ => (limnifs_core::codec::CODEC_STORE, chunk.to_vec().into()),
902 }
903}
904
905fn process_file(
909 pf: &PendingFile,
910 chunker: &FastCDC,
911 classifier: classifier::Classifier,
912 text_codec: u8,
913 binary_codec: u8,
914 tunables: &limnifs_core::codec::CodecTunables,
915 use_categorizers: bool,
916 skip_chunking: bool,
917 tournament: &TournamentSpec,
918 base_drop_index: Option<&std::collections::HashSet<[u8; 32]>>,
919 inline_threshold: usize,
920) -> Result<ChunkedFileResult, WriteError> {
921 let file_len_estimate = std::fs::metadata(&pf.path)
932 .map(|m| m.len() as usize)
933 .unwrap_or(0);
934 let data: Vec<u8> = if file_len_estimate >= MMAP_READ_THRESHOLD {
935 let file = std::fs::File::open(&pf.path)?;
936 #[allow(unsafe_code)]
937 let mmap = unsafe { memmap2::Mmap::map(&file) }.map_err(WriteError::Io)?;
938 Vec::from(&mmap[..])
942 } else {
943 std::fs::read(&pf.path)?
944 };
945 let file_len = data.len();
946
947 if skip_chunking && file_len > inline_threshold {
954 let drop_id = hash_section(&data);
955 let class = classifier.classify(&data);
956 let preferred_codec = match class {
957 classifier::Class::Binary => binary_codec,
958 _ => text_codec,
959 };
960 let (codec_id, compressed): (u8, std::sync::Arc<[u8]>) =
961 match limnifs_core::codec::compress_with_tunables(preferred_codec, &data, tunables) {
962 Ok(c) if c.len() < data.len() => (preferred_codec, c.into()),
963 _ => (limnifs_core::codec::CODEC_STORE, data.to_vec().into()),
964 };
965 return Ok(ChunkedFileResult {
966 drops: vec![(drop_id, data, compressed, codec_id)],
967 slices: vec![PendingSlice {
968 drop_id,
969 file_byte_start: 0,
970 file_byte_end: file_len as u64,
971 }],
972 });
973 }
974
975 if use_categorizers {
976 if let Some(cat) = file_categorizer::default_registry().categorize(&pf.path, &data) {
977 let needs_whole_file = matches!(
978 cat.codec_id,
979 limnifs_core::codec::CODEC_FLAC | limnifs_core::codec::CODEC_RICEPP
980 );
981 if needs_whole_file || file_len <= WHOLE_FILE_MAX_SIZE {
982 return process_whole_file_drop(pf, &data, cat, tunables);
983 }
984 }
985 }
986
987 let chunks = chunker.chunk_slice(&data);
988 let mut slices = Vec::with_capacity(chunks.len());
989 let mut file_offset: u64 = 0;
990 let mut seen_in_file: std::collections::HashSet<[u8; 32]> =
991 std::collections::HashSet::with_capacity(chunks.len());
992
993 let mut unique_chunks: Vec<(&[u8], [u8; 32])> = Vec::with_capacity(chunks.len());
996 for chunk in &chunks {
997 let chunk_len = u64::try_from(chunk.len()).expect("chunk len fits u64");
998 let drop_id = hash_section(chunk);
999 slices.push(PendingSlice {
1000 drop_id,
1001 file_byte_start: file_offset,
1002 file_byte_end: file_offset + chunk_len,
1003 });
1004 file_offset += chunk_len;
1005 if seen_in_file.insert(drop_id) {
1006 unique_chunks.push((chunk, drop_id));
1007 }
1008 }
1009
1010 use rayon::prelude::*;
1022 thread_local! {
1023 static COMPRESS_CACHE: std::cell::RefCell<std::collections::HashMap<[u8; 32], (u8, std::sync::Arc<[u8]>)>> =
1024 std::cell::RefCell::new(std::collections::HashMap::new());
1025 }
1026 const COMPRESS_CACHE_MAX_ENTRIES: usize = 100_000;
1027 let drops: Vec<RawDrop> = unique_chunks
1028 .par_iter()
1029 .map(|(chunk, drop_id)| {
1030 if let Some(base) = base_drop_index {
1033 if base.contains(drop_id) {
1034 return (*drop_id, Vec::new(), Vec::new().into(), CODEC_REFERENCED);
1035 }
1036 }
1037 let class = classifier.classify(chunk);
1038 let cached = COMPRESS_CACHE.with(|c| {
1041 c.borrow()
1042 .get(drop_id)
1043 .map(|(cid, comp)| (*cid, comp.clone()))
1044 });
1045 let (codec_id, compressed) = if let Some(c) = cached {
1046 c
1047 } else {
1048 let new = compress_chunk_with_tournament(
1049 chunk,
1050 class,
1051 text_codec,
1052 binary_codec,
1053 tunables,
1054 tournament,
1055 );
1056 COMPRESS_CACHE.with(|c| {
1058 let mut cache = c.borrow_mut();
1059 if cache.len() < COMPRESS_CACHE_MAX_ENTRIES {
1060 cache.insert(*drop_id, new.clone());
1062 }
1063 });
1064 new
1065 };
1066 (*drop_id, chunk.to_vec(), compressed, codec_id)
1067 })
1068 .collect();
1069
1070 let _ = file_len;
1071 Ok(ChunkedFileResult { drops, slices })
1072}
1073
1074struct PendingDrop {
1075 id: [u8; 32],
1076 plaintext_len: u32,
1082 compressed: std::sync::Arc<[u8]>,
1083 codec: u8,
1084 dict_id: u8,
1088 plaintext: Option<Vec<u8>>,
1093}
1094
1095impl PendingDrop {
1096 fn len_in_window(&self) -> u32 {
1100 u32::try_from(self.compressed.len()).expect("compressed fits u32")
1101 }
1102
1103 fn plaintext_len_value(&self) -> u32 {
1105 self.plaintext_len
1106 }
1107
1108 fn slab_footprint(&self) -> usize {
1111 48 + self.compressed.len()
1112 }
1113}
1114
1115struct PendingSlice {
1120 drop_id: [u8; 32],
1121 file_byte_start: u64,
1122 file_byte_end: u64,
1123}
1124
1125#[derive(Clone)]
1128struct PendingFile {
1129 inode_number: u64,
1130 path: PathBuf,
1131 mtime_ns: u64,
1132 file_len: u64,
1133}
1134
1135struct PendingInode {
1136 number: u64,
1137 mode: u32,
1138 mtime_ns: u64,
1139 content: PendingContent,
1140}
1141
1142enum PendingContent {
1143 Inline(Vec<u8>),
1144 DropBacked {
1145 file_len: u64,
1146 slices: Vec<PendingSlice>,
1147 },
1148 Directory(Vec<(String, u64, u8)>),
1149}
1150
1151struct DirNode {
1152 entries: Vec<(String, u64, u8)>,
1153 bytes: Vec<u8>,
1154 hash: [u8; 32],
1155}
1156
1157struct WriteContext {
1158 next_inode: u64,
1159 inodes: Vec<PendingInode>,
1160 dir_nodes: Vec<DirNode>,
1161 drops: Vec<PendingDrop>,
1162 drop_index: HashSet<[u8; 32]>,
1163 pending_files: Vec<PendingFile>,
1164 file_count: usize,
1165 dir_count: usize,
1166 root_inode_number: u64,
1167 chunker: FastCDC,
1168 classifier: classifier::Classifier,
1169 shared_inline_map: HashMap<[u8; 32], usize>,
1170 shared_inline_table: Vec<Vec<u8>>,
1171 profile_name: Option<String>,
1173 metadata_codec: u8,
1176 categorizers_disabled: bool,
1178 rw_mode: bool,
1180 auto_turnover: bool,
1182 collect_dict_samples: bool,
1185 dict_samples_by_class: HashMap<crate::classifier::Class, Vec<Vec<u8>>>,
1192 trained_dicts_by_class: HashMap<crate::classifier::Class, crate::dictionary::TrainedDictionary>,
1197 base_drop_index: Option<HashSet<[u8; 32]>>,
1203 base_root: Option<[u8; 32]>,
1208 metadata_externalize_threshold: usize,
1213 inline_threshold: usize,
1218 pending_sink: Option<std::sync::mpsc::SyncSender<PendingFile>>,
1224}
1225
1226impl WriteContext {
1227 const MAX_DICT_SAMPLES: usize = 1000;
1230
1231 fn new() -> Self {
1232 Self {
1233 next_inode: 1,
1234 inodes: Vec::new(),
1235 dir_nodes: Vec::new(),
1236 drops: Vec::new(),
1237 drop_index: HashSet::new(),
1238 pending_files: Vec::new(),
1239 file_count: 0,
1240 dir_count: 0,
1241 root_inode_number: 0,
1242 chunker: FastCDC::default(),
1243 classifier: classifier::Classifier,
1244 shared_inline_map: HashMap::new(),
1245 shared_inline_table: Vec::new(),
1246 profile_name: None,
1247 metadata_codec: limnifs_core::codec::CODEC_BROTLI,
1248 categorizers_disabled: false,
1249 rw_mode: false,
1250 auto_turnover: false,
1251 collect_dict_samples: false,
1252 dict_samples_by_class: HashMap::new(),
1253 trained_dicts_by_class: HashMap::new(),
1254 base_drop_index: None,
1255 base_root: None,
1256 pending_sink: None,
1257 inline_threshold: INLINE_THRESHOLD,
1258 metadata_externalize_threshold: METADATA_EXTERNALIZE_THRESHOLD,
1259 }
1260 }
1261
1262 fn alloc_inode(&mut self) -> u64 {
1263 let n = self.next_inode;
1264 self.next_inode += 1;
1265 n
1266 }
1267
1268 fn build_shared_inline_table(&mut self) {
1272 let mut counts: HashMap<[u8; 32], usize> = HashMap::new();
1273 for inode in &self.inodes {
1274 if let PendingContent::Inline(data) = &inode.content {
1275 let h = hash_section(data);
1276 *counts.entry(h).or_default() += 1;
1277 }
1278 }
1279 for inode in &self.inodes {
1281 if let PendingContent::Inline(data) = &inode.content {
1282 let h = hash_section(data);
1283 if counts.get(&h).copied().unwrap_or(0) > 1
1284 && !self.shared_inline_map.contains_key(&h)
1285 {
1286 let idx = self.shared_inline_table.len();
1287 self.shared_inline_table.push(data.clone());
1288 self.shared_inline_map.insert(h, idx);
1289 }
1290 }
1291 }
1292 }
1293
1294 fn merge_chunked_file(&mut self, pf: &PendingFile, result: ChunkedFileResult) {
1297 for (drop_id, plaintext, compressed, codec) in result.drops {
1298 if self.drop_index.insert(drop_id) {
1299 let retain_plaintext =
1305 self.collect_dict_samples && codec == limnifs_core::codec::CODEC_ZSTD;
1306 if retain_plaintext {
1307 let total: usize = self.dict_samples_by_class.values().map(Vec::len).sum();
1308 if total < Self::MAX_DICT_SAMPLES {
1309 let class = self.classifier.classify(&plaintext);
1310 self.dict_samples_by_class
1311 .entry(class)
1312 .or_default()
1313 .push(plaintext.clone());
1314 }
1315 }
1316 self.drops.push(PendingDrop {
1317 id: drop_id,
1318 plaintext_len: u32::try_from(plaintext.len()).unwrap_or(u32::MAX),
1319 compressed,
1320 codec,
1321 dict_id: limnifs_core::drop_record::NO_DICT,
1322 plaintext: if retain_plaintext {
1323 Some(plaintext)
1324 } else {
1325 None
1326 },
1327 });
1328 }
1329 }
1330 self.inodes.push(PendingInode {
1331 number: pf.inode_number,
1332 mode: 0o100_644,
1333 mtime_ns: pf.mtime_ns,
1334 content: PendingContent::DropBacked {
1335 file_len: pf.file_len,
1336 slices: result.slices,
1337 },
1338 });
1339 }
1340
1341 #[allow(dead_code)]
1352 fn deepen_drop(&self, drop_id: [u8; 32], plaintext: &[u8]) -> PendingDrop {
1353 let class = self.classifier.classify(plaintext);
1354 let (codec, compressed): (u8, std::sync::Arc<[u8]>) = match class {
1355 classifier::Class::Text | classifier::Class::Code | classifier::Class::Binary => {
1356 let c = limnifs_core::codec::compress_lz4_with_size(plaintext);
1357 (limnifs_core::codec::CODEC_LZ4, c.into())
1358 }
1359 _ => (limnifs_core::codec::CODEC_STORE, plaintext.to_vec().into()),
1360 };
1361 PendingDrop {
1362 id: drop_id,
1363 plaintext_len: u32::try_from(plaintext.len()).unwrap_or(u32::MAX),
1364 compressed,
1365 codec,
1366 dict_id: limnifs_core::drop_record::NO_DICT,
1367 plaintext: None,
1368 }
1369 }
1370
1371 fn walk(&mut self, path: &Path) -> Result<u64, WriteError> {
1372 let meta = std::fs::symlink_metadata(path)?;
1373 let file_type = meta.file_type();
1374 let mtime_ns = meta
1375 .modified()
1376 .ok()
1377 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
1378 .map_or(0u128, |d| d.as_nanos());
1379 let mtime_ns: u64 = mtime_ns.try_into().unwrap_or(0);
1380
1381 if file_type.is_dir() {
1382 self.dir_count += 1;
1383 let inode_number = self.alloc_inode();
1384 let mut entries: Vec<(String, u64, u8)> = Vec::new();
1385
1386 for entry in std::fs::read_dir(path)? {
1387 let entry = entry?;
1388 let name = entry.file_name().to_string_lossy().into_owned();
1389 let child_path = entry.path();
1390 let child_inode = self.walk(&child_path)?;
1391 let child_meta = entry.metadata()?;
1392 let entry_type = if child_meta.is_dir() { 0x02 } else { 0x01 };
1393 entries.push((name, child_inode, entry_type));
1394 }
1395
1396 entries.sort_by(|a, b| a.0.cmp(&b.0));
1397 let dir_node = encode_dir_node(&entries);
1398 self.dir_nodes.push(dir_node);
1399 self.inodes.push(PendingInode {
1400 number: inode_number,
1401 mode: 0o040_755,
1402 mtime_ns,
1403 content: PendingContent::Directory(entries),
1404 });
1405 Ok(inode_number)
1406 } else if file_type.is_file() {
1407 self.file_count += 1;
1408 let inode_number = self.alloc_inode();
1409 let file_len = meta.len();
1410
1411 if file_len <= u64::try_from(self.inline_threshold).unwrap_or(u64::MAX) {
1412 let data = std::fs::read(path)?;
1413 self.inodes.push(PendingInode {
1414 number: inode_number,
1415 mode: 0o100_644,
1416 mtime_ns,
1417 content: PendingContent::Inline(data),
1418 });
1419 } else {
1420 let pf = PendingFile {
1422 inode_number,
1423 path: path.to_path_buf(),
1424 mtime_ns,
1425 file_len,
1426 };
1427 if let Some(sink) = &self.pending_sink {
1428 sink.send(pf).map_err(|_| {
1434 WriteError::Io(std::io::Error::other("walk: compress pipeline shut down"))
1435 })?;
1436 } else {
1437 self.pending_files.push(pf);
1438 }
1439 }
1440 Ok(inode_number)
1441 } else {
1442 Err(WriteError::Io(std::io::Error::new(
1443 std::io::ErrorKind::Unsupported,
1444 format!("unsupported file type: {}", path.display()),
1445 )))
1446 }
1447 }
1448
1449 fn train_and_apply_dictionary(&mut self, dictionaries: &crate::config::DictionaryConfig) {
1464 let cleanup = |ctx: &mut Self| {
1465 for d in &mut ctx.drops {
1466 d.plaintext = None;
1467 }
1468 ctx.dict_samples_by_class.clear();
1469 };
1470
1471 if !dictionaries.enabled {
1472 cleanup(self);
1473 return;
1474 }
1475
1476 let target = usize::try_from(dictionaries.max_dict_size).unwrap_or(65_536);
1477 let min_class = usize::try_from(dictionaries.min_class_size).unwrap_or(0);
1478
1479 let text_classes = [
1483 crate::classifier::Class::Text,
1484 crate::classifier::Class::Code,
1485 crate::classifier::Class::Sparse,
1486 ];
1487 let binary_classes = [crate::classifier::Class::Binary];
1488
1489 let text_samples: Vec<&[u8]> = text_classes
1491 .iter()
1492 .flat_map(|c| self.dict_samples_by_class.get(c).into_iter().flatten())
1493 .map(Vec::as_slice)
1494 .collect();
1495 let trainer = crate::dictionary::TrainerKind::from_config_str(&dictionaries.trainer);
1496 if text_samples.len() >= min_class {
1497 if let Some(dict) =
1498 crate::dictionary::train_zstd_with_trainer(0, &text_samples, target, trainer)
1499 {
1500 self.trained_dicts_by_class
1501 .insert(crate::classifier::Class::Text, dict);
1502 }
1503 }
1504 let binary_samples: Vec<&[u8]> = binary_classes
1505 .iter()
1506 .flat_map(|c| self.dict_samples_by_class.get(c).into_iter().flatten())
1507 .map(Vec::as_slice)
1508 .collect();
1509 if binary_samples.len() >= min_class {
1510 if let Some(dict) =
1511 crate::dictionary::train_zstd_with_trainer(1, &binary_samples, target, trainer)
1512 {
1513 self.trained_dicts_by_class
1514 .insert(crate::classifier::Class::Binary, dict);
1515 }
1516 }
1517
1518 for d in self.drops.iter_mut() {
1521 if d.codec != limnifs_core::codec::CODEC_ZSTD {
1522 continue;
1523 }
1524 let Some(plaintext) = d.plaintext.clone() else {
1525 continue;
1526 };
1527 let class = self.classifier.classify(&plaintext);
1528 let dict_class = if text_classes.contains(&class) {
1529 crate::classifier::Class::Text
1530 } else if binary_classes.contains(&class) {
1531 crate::classifier::Class::Binary
1532 } else {
1533 continue;
1534 };
1535 let Some(dict) = self.trained_dicts_by_class.get(&dict_class) else {
1536 continue;
1537 };
1538 let Ok(dict_compressed) = dict.compress(&plaintext) else {
1539 continue;
1540 };
1541 if dict_compressed.len() < d.compressed.len() {
1542 d.compressed = dict_compressed.into();
1543 d.dict_id = dict.id;
1544 }
1545 }
1546
1547 cleanup(self);
1548 }
1549
1550 fn trace_phase(label: &str, start: std::time::Instant) {
1552 if std::env::var_os("LIMNIFS_TRACE_ASSEMBLE").is_some() {
1553 eprintln!("[assemble] {label}: {:?}", start.elapsed());
1554 }
1555 }
1556
1557 fn assemble(mut self) -> WriteArtifact {
1558 let t_assemble = std::time::Instant::now();
1559 let inode_count = self.inodes.len();
1560 let dir_count = self.dir_count;
1561 let drop_count = self.drops.len();
1562
1563 let t = std::time::Instant::now();
1569 let slabs = pack_slabs(&self.drops);
1570 Self::trace_phase("pack_slabs", t);
1571
1572 let t = std::time::Instant::now();
1576 self.build_shared_inline_table();
1577 Self::trace_phase("shared_inline_table", t);
1578
1579 let mut metadata_blob = Vec::new();
1580 metadata_blob.extend_from_slice(&u32::try_from(self.inodes.len()).unwrap().to_le_bytes());
1581 for inode in &self.inodes {
1582 self.encode_inode(&mut metadata_blob, inode);
1583 }
1584 metadata_blob
1585 .extend_from_slice(&u32::try_from(self.dir_nodes.len()).unwrap().to_le_bytes());
1586 for node in &self.dir_nodes {
1587 metadata_blob.extend_from_slice(&node.bytes);
1588 }
1589 if !self.shared_inline_table.is_empty() {
1592 metadata_blob.extend_from_slice(
1593 &u32::try_from(self.shared_inline_table.len())
1594 .unwrap()
1595 .to_le_bytes(),
1596 );
1597 for entry in &self.shared_inline_table {
1598 let len = u32::try_from(entry.len()).expect("shared entry fits u32");
1599 metadata_blob.extend_from_slice(&len.to_le_bytes());
1600 metadata_blob.extend_from_slice(entry);
1601 }
1602 }
1603
1604 Self::trace_phase("metadata_encode", t);
1605 let uncompressed_len =
1612 u32::try_from(metadata_blob.len()).expect("metadata blob length fits u32");
1613 let t = std::time::Instant::now();
1614 let metadata_hash = hash_section(&metadata_blob);
1615 let metadata_codec = self.metadata_codec;
1616 let metadata_quality = if metadata_blob.len() > METADATA_LARGE_BLOB_THRESHOLD {
1617 METADATA_LARGE_BLOB_QUALITY
1618 } else {
1619 METADATA_SMALL_BLOB_QUALITY
1620 };
1621 let compressed_blob = if metadata_codec == limnifs_core::codec::CODEC_BROTLI {
1622 limnifs_core::codec::compress_brotli_with_quality(&metadata_blob, metadata_quality)
1623 .unwrap_or_else(|_| metadata_blob.clone())
1624 } else {
1625 limnifs_core::codec::compress(metadata_codec, &metadata_blob)
1626 .unwrap_or_else(|_| metadata_blob.clone())
1627 };
1628 Self::trace_phase("metadata_compress", t);
1629 let (on_wire_codec, on_wire_blob) = if compressed_blob.len() < metadata_blob.len() {
1630 (metadata_codec, compressed_blob)
1631 } else {
1632 (limnifs_core::codec::CODEC_STORE, metadata_blob.clone())
1633 };
1634
1635 let externalize_at = self
1639 .metadata_externalize_threshold
1640 .min(limnifs_core::metadata_reference::DEFAULT_INLINE_METADATA_MAX_BYTES as usize);
1641 let (metadata_sidecar, inline_data, metadata_locator_count) =
1642 if on_wire_blob.len() > externalize_at {
1643 let locator = "file:metadata.bin".to_owned();
1644 let sidecar = MetadataSidecar {
1645 bytes: on_wire_blob.clone(),
1646 locator,
1647 };
1648 (Some(sidecar), None, 1u32)
1649 } else {
1650 (None, Some(on_wire_blob.clone()), 0u32)
1651 };
1652
1653 let mut manifest = Vec::new();
1654
1655 let header_start = manifest.len();
1656 manifest.extend_from_slice(&ManifestHeader::current().to_bytes());
1657 let header_end = manifest.len();
1658
1659 let flags_start = manifest.len();
1660 manifest.push(FEATURE_FLAGS_SECTION_VERSION);
1661 manifest.extend_from_slice(&0u32.to_le_bytes());
1662 let flags_end = manifest.len();
1663
1664 let meta_ref_start = manifest.len();
1667 manifest.push(METADATA_REFERENCE_SECTION_VERSION_2);
1668 manifest.extend_from_slice(&metadata_hash);
1669 manifest.extend_from_slice(&uncompressed_len.to_le_bytes());
1670 manifest.push(on_wire_codec);
1671 manifest.extend_from_slice(&metadata_locator_count.to_le_bytes());
1672 if let Some(sidecar) = &metadata_sidecar {
1673 let loc_bytes = sidecar.locator.as_bytes();
1674 let loc_len = u32::try_from(loc_bytes.len()).expect("locator fits u32");
1675 manifest.extend_from_slice(&loc_len.to_le_bytes());
1676 manifest.extend_from_slice(loc_bytes);
1677 }
1678 match &inline_data {
1679 Some(blob) => {
1680 let inline_len = u32::try_from(blob.len()).expect("metadata fits u32");
1681 manifest.extend_from_slice(&inline_len.to_le_bytes());
1682 manifest.extend_from_slice(blob);
1683 }
1684 None => {
1685 manifest.extend_from_slice(&0u32.to_le_bytes());
1686 }
1687 }
1688 let meta_ref_end = manifest.len();
1689
1690 let slab_index_start = manifest.len();
1691 manifest.push(SLAB_INDEX_SECTION_VERSION);
1692 manifest.extend_from_slice(&u32::try_from(slabs.len()).unwrap().to_le_bytes());
1693 for slab in &slabs {
1694 manifest.extend_from_slice(&slab.id.to_bytes());
1695 manifest.extend_from_slice(&1u32.to_le_bytes());
1696 let loc_bytes = slab.locator.as_bytes();
1697 let loc_len = u32::try_from(loc_bytes.len()).expect("locator fits u32");
1698 manifest.extend_from_slice(&loc_len.to_le_bytes());
1699 manifest.extend_from_slice(loc_bytes);
1700 }
1701 let slab_index_end = manifest.len();
1702
1703 let history_start = manifest.len();
1704 manifest.push(HISTORY_SECTION_VERSION);
1705 manifest.extend_from_slice(&1u32.to_le_bytes());
1706 manifest.push(0x01);
1707 manifest.extend_from_slice(&0u64.to_le_bytes());
1708 manifest.extend_from_slice(&0u32.to_le_bytes());
1709 manifest.extend_from_slice(&0u32.to_le_bytes());
1710 let history_end = manifest.len();
1711
1712 let profile_desc_start = manifest.len();
1717 if let Some(ref name) = self.profile_name {
1718 let desc = limnifs_core::profile_descriptor::ProfileDescriptor {
1719 version: limnifs_core::profile_descriptor::PROFILE_DESCRIPTOR_SECTION_VERSION,
1720 profile_name: Some(name.clone()),
1721 blake3_hashing: true,
1722 cross_file_dedup: true,
1723 content_classification: !self.categorizers_disabled,
1724 integrity_verify: true,
1725 read_write: self.rw_mode,
1726 auto_turnover: self.auto_turnover,
1727 };
1728 limnifs_core::profile_descriptor::encode_profile_descriptor(&desc, &mut manifest);
1729 }
1730 let profile_desc_end = manifest.len();
1731
1732 if !self.trained_dicts_by_class.is_empty() {
1738 let dicts: Vec<_> = self
1739 .trained_dicts_by_class
1740 .values()
1741 .map(|d| limnifs_core::dictionary_section::Dictionary {
1742 codec_id: d.codec,
1743 class_id: d.id,
1744 data: d.content.clone(),
1745 })
1746 .collect();
1747 let section = limnifs_core::dictionary_section::DictionarySection {
1748 version: limnifs_core::dictionary_section::DICTIONARY_SECTION_VERSION,
1749 dicts,
1750 };
1751 limnifs_core::dictionary_section::encode_dictionary_section(§ion, &mut manifest);
1752 }
1753
1754 let dictionary_end = manifest.len();
1755
1756 let delta_linkage_hash = if let Some(base_root) = self.base_root {
1762 let delta_start = manifest.len();
1763 manifest.push(limnifs_core::delta_linkage::DELTA_LINKAGE_SECTION_VERSION);
1769 manifest.extend_from_slice(&base_root);
1770 manifest.extend_from_slice(&0u32.to_le_bytes());
1771 hash_section(&manifest[delta_start..])
1772 } else {
1773 hash_empty_section()
1774 };
1775 let _ = dictionary_end;
1776
1777 let hashes = SectionHashes {
1778 metadata: metadata_hash,
1779 format_header: hash_section(&manifest[header_start..header_end]),
1780 feature_flags: hash_section(&manifest[flags_start..flags_end]),
1781 metadata_reference: hash_section(&manifest[meta_ref_start..meta_ref_end]),
1782 slab_index: hash_section(&manifest[slab_index_start..slab_index_end]),
1783 crypto_params: hash_empty_section(),
1784 ec_params: hash_empty_section(),
1785 dms_policy: hash_empty_section(),
1786 delta_linkage: delta_linkage_hash,
1787 history: hash_section(&manifest[history_start..history_end]),
1788 };
1800 let merkle_root = compute_merkle_root(&hashes);
1801
1802 WriteArtifact {
1803 bytes: manifest,
1804 merkle_root,
1805 slabs,
1806 metadata_sidecar,
1807 inode_count,
1808 file_count: self.file_count,
1809 dir_count,
1810 drop_count,
1811 root_inode_number: self.root_inode_number,
1812 }
1813 }
1814
1815 fn encode_inode(&self, out: &mut Vec<u8>, inode: &PendingInode) {
1816 out.extend_from_slice(&inode.number.to_le_bytes());
1817 out.extend_from_slice(&inode.mode.to_le_bytes());
1818 out.extend_from_slice(&0u32.to_le_bytes());
1819 out.extend_from_slice(&0u32.to_le_bytes());
1820 out.extend_from_slice(&inode.mtime_ns.to_le_bytes());
1821 out.extend_from_slice(&inode.mtime_ns.to_le_bytes());
1822 out.extend_from_slice(&1u32.to_le_bytes());
1823 match &inode.content {
1824 PendingContent::Inline(data) => {
1825 let h = hash_section(data);
1826 if let Some(&idx) = self.shared_inline_map.get(&h) {
1827 out.push(INODE_FLAG_INLINE_DATA | INODE_FLAG_SHARED_INLINE);
1829 out.extend_from_slice(&(idx as u32).to_le_bytes());
1830 } else {
1831 out.push(INODE_FLAG_INLINE_DATA);
1832 let len = u32::try_from(data.len()).expect("data fits u32");
1833 out.extend_from_slice(&len.to_le_bytes());
1834 out.extend_from_slice(data);
1835 }
1836 }
1837 PendingContent::DropBacked { file_len, slices } => {
1838 out.push(0x00);
1839 let slice_count = u32::try_from(slices.len()).expect("slice count fits u32");
1840 out.extend_from_slice(&slice_count.to_le_bytes());
1841 for slice in slices {
1842 out.extend_from_slice(&slice.file_byte_start.to_le_bytes());
1843 out.extend_from_slice(&slice.file_byte_end.to_le_bytes());
1844 out.extend_from_slice(&slice.drop_id);
1845 out.extend_from_slice(&0u32.to_le_bytes());
1847 let drop_byte_len = u32::try_from(slice.file_byte_end - slice.file_byte_start)
1851 .expect("slice range fits u32");
1852 out.extend_from_slice(&drop_byte_len.to_le_bytes());
1853 }
1854 let _ = file_len;
1855 }
1856 PendingContent::Directory(entries) => {
1857 out.push(0x00);
1858 let node = self
1859 .dir_nodes
1860 .iter()
1861 .find(|n| n.entries == *entries)
1862 .expect("directory node must exist");
1863 out.extend_from_slice(&node.hash);
1864 }
1865 }
1866 }
1867}
1868
1869fn encode_dir_node(entries: &[(String, u64, u8)]) -> DirNode {
1870 let mut bytes = Vec::new();
1871 bytes.push(1u8);
1872 let count = u32::try_from(entries.len()).expect("entry count fits u32");
1873 bytes.extend_from_slice(&count.to_le_bytes());
1874 for (name, inode_number, entry_type) in entries {
1875 let name_bytes = name.as_bytes();
1876 let name_len = u32::try_from(name_bytes.len()).expect("name fits u32");
1877 bytes.extend_from_slice(&name_len.to_le_bytes());
1878 bytes.extend_from_slice(name_bytes);
1879 bytes.extend_from_slice(&inode_number.to_le_bytes());
1880 bytes.push(*entry_type);
1881 }
1882 let hash = hash_section(&bytes);
1883 DirNode {
1884 entries: entries.to_vec(),
1885 bytes,
1886 hash,
1887 }
1888}
1889
1890fn pack_slabs(drops: &[PendingDrop]) -> Vec<SlabArtifact> {
1899 let local_drops: Vec<&PendingDrop> = drops
1904 .iter()
1905 .filter(|d| d.codec != limnifs_core::codec::CODEC_REFERENCED)
1906 .collect();
1907 if local_drops.is_empty() {
1908 return Vec::new();
1909 }
1910
1911 let max_content = MAX_SLAB_TOTAL_BYTES.saturating_sub(SLAB_HEADER_LEN);
1912
1913 let mut slab_groups: Vec<Vec<&PendingDrop>> = Vec::new();
1918 let mut current: Vec<&PendingDrop> = Vec::new();
1919 let mut current_size: usize = 0;
1920
1921 for drop in &local_drops {
1922 let footprint = drop.slab_footprint();
1923 if !current.is_empty() && current_size + footprint > max_content {
1924 slab_groups.push(std::mem::take(&mut current));
1925 current_size = 0;
1926 }
1927 current.push(*drop);
1928 current_size += footprint;
1929 }
1930 if !current.is_empty() {
1931 slab_groups.push(current);
1932 }
1933
1934 use rayon::prelude::*;
1940 slab_groups
1941 .par_iter()
1942 .enumerate()
1943 .map(|(ordinal, group)| {
1944 let ordinal_u64 = u64::try_from(ordinal).expect("slab count fits u64");
1945 encode_slab(ordinal_u64, group)
1946 })
1947 .collect()
1948}
1949
1950fn encode_slab(ordinal: u64, drops: &[&PendingDrop]) -> SlabArtifact {
1954 const DROP_RECORD_LEN: usize = 49;
1960 let mut drop_records = Vec::with_capacity(drops.len() * DROP_RECORD_LEN);
1961 let mut solid_window = Vec::new();
1962 let mut drop_ids = Vec::with_capacity(drops.len());
1963 let mut offset_in_window: u32 = 0;
1964
1965 for drop in drops {
1966 let plaintext_len = drop.plaintext_len_value();
1967 let window_len = drop.len_in_window();
1968 drop_records.extend_from_slice(&drop.id);
1969 drop_records.extend_from_slice(&plaintext_len.to_le_bytes());
1970 drop_records.extend_from_slice(&[drop.codec, 0x00, 0x00]);
1972 drop_records.push(0x00); drop_records.extend_from_slice(&offset_in_window.to_le_bytes());
1974 drop_records.extend_from_slice(&window_len.to_le_bytes());
1975 drop_records.push(drop.dict_id); solid_window.extend_from_slice(&drop.compressed);
1977 drop_ids.push(drop.id);
1978 offset_in_window = offset_in_window
1979 .checked_add(window_len)
1980 .expect("slab window size fits u32");
1981 }
1982
1983 let slab_content = [&drop_records[..], &solid_window[..]].concat();
1984 let slab_hash = hash_section(&slab_content);
1985 let slab_id = SlabId::new(ordinal, slab_hash);
1986
1987 let total_length = SLAB_HEADER_LEN + slab_content.len();
1988 let mut slab_bytes = Vec::with_capacity(total_length);
1989 slab_bytes.extend_from_slice(b"LIM1");
1990 slab_bytes.extend_from_slice(&1u16.to_le_bytes());
1991 slab_bytes.extend_from_slice(&slab_id.to_bytes());
1992 slab_bytes.extend_from_slice(
1993 &u64::try_from(total_length)
1994 .unwrap_or(u64::MAX)
1995 .to_le_bytes(),
1996 );
1997 slab_bytes.push(0x00);
1998 slab_bytes.push(0x00);
1999 slab_bytes.extend_from_slice(&slab_content);
2000
2001 let locator = format!("file:slab-{ordinal}.bin");
2002
2003 SlabArtifact {
2004 id: slab_id,
2005 bytes: slab_bytes,
2006 locator,
2007 drop_ids,
2008 }
2009}
2010
2011#[cfg(test)]
2012fn pseudo_random_bytes(seed: u64, count: usize) -> Vec<u8> {
2013 let mut state = seed;
2014 let mut out = Vec::with_capacity(count);
2015 for _ in 0..count {
2016 state = state
2017 .wrapping_mul(6_364_136_223_846_793_005)
2018 .wrapping_add(1_442_695_040_888_963_407);
2019 out.push(u8::try_from(state >> 56).expect("fits u8"));
2020 }
2021 out
2022}
2023
2024#[cfg(test)]
2025mod tests {
2026 use super::*;
2027 use limnifs_core::ManifestCursor;
2028
2029 #[test]
2030 fn write_stream_packs_single_named_stream() {
2031 let temp = std::env::temp_dir().join(format!(
2035 "limnifs-write-stream-test-{}-{}",
2036 std::process::id(),
2037 std::time::SystemTime::now()
2038 .duration_since(std::time::UNIX_EPOCH)
2039 .unwrap()
2040 .as_nanos()
2041 ));
2042 std::fs::create_dir_all(&temp).expect("create temp dir");
2043
2044 let content = b"stream test content line\n".repeat(10_000); let cursor = std::io::Cursor::new(content.clone());
2046 let config = WriteConfig::default_v0_1();
2047 let artifact = write_stream("streamed.txt", cursor, &config).expect("write_stream");
2048
2049 assert!(!artifact.bytes.is_empty(), "manifest bytes non-empty");
2050 assert!(!artifact.slabs.is_empty(), "at least one slab produced");
2051 let total_drop_bytes: usize = artifact.slabs.iter().map(|s| s.bytes.len()).sum();
2056 assert!(total_drop_bytes > 0, "drops non-empty");
2057
2058 let _ = std::fs::remove_dir_all(&temp);
2059 }
2060
2061 #[test]
2062 fn write_layer_references_base_drops() {
2063 let temp = std::env::temp_dir().join(format!(
2069 "limnifs-write-layer-test-{}-{}",
2070 std::process::id(),
2071 std::time::SystemTime::now()
2072 .duration_since(std::time::UNIX_EPOCH)
2073 .unwrap()
2074 .as_nanos()
2075 ));
2076 std::fs::create_dir_all(&temp).expect("create temp dir");
2077
2078 let base_dir = temp.join("base");
2080 std::fs::create_dir_all(&base_dir).expect("base dir");
2081 let text = b"layer test content line\n".repeat(50_000); std::fs::write(base_dir.join("shared.txt"), &text).expect("write shared");
2083 std::fs::write(base_dir.join("base-only.txt"), b"only in base").expect("write base-only");
2084
2085 let config = WriteConfig::default_v0_1();
2086 let base_artifact = write_directory_with_config(&base_dir, &config).expect("base");
2087
2088 let base_manifest = temp.join("base.lim");
2089 std::fs::write(&base_manifest, &base_artifact.bytes).expect("write base manifest");
2090 for slab in &base_artifact.slabs {
2091 let slab_name = slab.locator.strip_prefix("file:").unwrap_or(&slab.locator);
2092 std::fs::write(temp.join(slab_name), &slab.bytes).expect("write base slab");
2093 }
2094
2095 let layer_dir = temp.join("layer");
2097 std::fs::create_dir_all(&layer_dir).expect("layer dir");
2098 std::fs::write(layer_dir.join("shared.txt"), &text).expect("write shared in layer");
2099 std::fs::write(layer_dir.join("new.txt"), b"fresh content in layer").expect("write new");
2100
2101 let layer_artifact = write_layer(&base_manifest, &layer_dir, &config).expect("layer");
2102
2103 let layer_slab_bytes: usize = layer_artifact.slabs.iter().map(|s| s.bytes.len()).sum();
2106 let base_slab_bytes: usize = base_artifact.slabs.iter().map(|s| s.bytes.len()).sum();
2107 assert!(
2108 layer_slab_bytes < base_slab_bytes / 4,
2109 "layer slabs ({}) should be much smaller than base ({}) — layering failed",
2110 layer_slab_bytes,
2111 base_slab_bytes
2112 );
2113
2114 let base_root = base_artifact.merkle_root.as_bytes();
2117 assert!(
2118 layer_artifact
2119 .bytes
2120 .windows(32)
2121 .any(|w| w == base_root.as_slice()),
2122 "layer manifest must contain base's ManifestRoot bytes"
2123 );
2124
2125 let _ = std::fs::remove_dir_all(&temp);
2126 }
2127
2128 #[test]
2129 fn tournament_short_circuits_on_highly_compressible_chunk() {
2130 let chunk = b"hello world ".repeat(500);
2133 let tunables = limnifs_core::codec::CodecTunables::default();
2134 let tournament = TournamentSpec {
2135 codec_ids: vec![
2136 limnifs_core::codec::CODEC_LZ4,
2137 limnifs_core::codec::CODEC_BROTLI,
2138 ],
2139 min_size: 16,
2140 skip_for_binary: false,
2141 short_circuit_permille: 250,
2142 };
2143 let (codec_id, compressed) = compress_chunk_with_tournament(
2144 &chunk,
2145 classifier::Class::Text,
2146 limnifs_core::codec::CODEC_BROTLI,
2147 limnifs_core::codec::CODEC_LZ4,
2148 &tunables,
2149 &tournament,
2150 );
2151 assert_eq!(codec_id, limnifs_core::codec::CODEC_LZ4);
2152 assert!(compressed.len() < chunk.len());
2153 }
2154
2155 #[test]
2156 fn tournament_runs_all_codecs_when_short_circuit_disabled() {
2157 let chunk = b"hello world ".repeat(500);
2163 let tunables = limnifs_core::codec::CodecTunables::default();
2164 let tournament = TournamentSpec {
2165 codec_ids: vec![
2166 limnifs_core::codec::CODEC_LZ4,
2167 limnifs_core::codec::CODEC_BROTLI,
2168 limnifs_core::codec::CODEC_ZSTD,
2169 ],
2170 min_size: 16,
2171 skip_for_binary: false,
2172 short_circuit_permille: 0,
2173 };
2174 let (codec_id, compressed) = compress_chunk_with_tournament(
2175 &chunk,
2176 classifier::Class::Text,
2177 limnifs_core::codec::CODEC_BROTLI,
2178 limnifs_core::codec::CODEC_LZ4,
2179 &tunables,
2180 &tournament,
2181 );
2182 assert!(
2186 codec_id == limnifs_core::codec::CODEC_ZSTD
2187 || codec_id == limnifs_core::codec::CODEC_BROTLI,
2188 "expected ZSTD or Brotli to win, got codec {codec_id}"
2189 );
2190 assert!(compressed.len() < chunk.len());
2191 }
2192
2193 #[test]
2194 fn tournament_skips_for_binary_when_configured() {
2195 let chunk = vec![0u8; 4096];
2196 let tunables = limnifs_core::codec::CodecTunables::default();
2197 let tournament = TournamentSpec {
2198 codec_ids: vec![limnifs_core::codec::CODEC_BROTLI],
2199 min_size: 16,
2200 skip_for_binary: true,
2201 short_circuit_permille: 250,
2202 };
2203 let (codec_id, _compressed) = compress_chunk_with_tournament(
2204 &chunk,
2205 classifier::Class::Binary,
2206 limnifs_core::codec::CODEC_BROTLI,
2207 limnifs_core::codec::CODEC_LZ4,
2208 &tunables,
2209 &tournament,
2210 );
2211 assert_eq!(codec_id, limnifs_core::codec::CODEC_LZ4);
2213 }
2214
2215 #[test]
2216 fn tournament_small_chunk_uses_preferred_codec() {
2217 let chunk = b"tiny";
2218 let tunables = limnifs_core::codec::CodecTunables::default();
2219 let tournament = TournamentSpec {
2220 codec_ids: vec![limnifs_core::codec::CODEC_BROTLI],
2221 min_size: 1024,
2222 skip_for_binary: false,
2223 short_circuit_permille: 0,
2224 };
2225 let (codec_id, _compressed) = compress_chunk_with_tournament(
2226 chunk,
2227 classifier::Class::Text,
2228 limnifs_core::codec::CODEC_BROTLI,
2229 limnifs_core::codec::CODEC_LZ4,
2230 &tunables,
2231 &tournament,
2232 );
2233 assert_eq!(codec_id, limnifs_core::codec::CODEC_STORE);
2235 }
2236
2237 #[test]
2238 fn tournament_falls_back_to_store_when_no_codec_compresses() {
2239 let chunk = pseudo_random_bytes(42, 4096);
2243 let tunables = limnifs_core::codec::CodecTunables::default();
2244 let tournament = TournamentSpec {
2245 codec_ids: vec![limnifs_core::codec::CODEC_LZ4],
2246 min_size: 16,
2247 skip_for_binary: false,
2248 short_circuit_permille: 0,
2249 };
2250 let (codec_id, compressed) = compress_chunk_with_tournament(
2251 &chunk,
2252 classifier::Class::Binary,
2253 limnifs_core::codec::CODEC_BROTLI,
2254 limnifs_core::codec::CODEC_LZ4,
2255 &tunables,
2256 &tournament,
2257 );
2258 assert_eq!(codec_id, limnifs_core::codec::CODEC_STORE);
2259 assert_eq!(compressed.len(), chunk.len());
2260 }
2261
2262 #[test]
2263 fn dictionaries_enabled_emits_dictionary_section_when_enough_samples() {
2264 let temp = std::env::temp_dir().join(format!(
2269 "limnifs-write-test-{}-dict-{}",
2270 std::process::id(),
2271 std::time::SystemTime::now()
2272 .duration_since(std::time::UNIX_EPOCH)
2273 .map(|d| d.as_nanos() as u64)
2274 .unwrap_or(0),
2275 ));
2276 let _ = std::fs::remove_dir_all(&temp);
2277 std::fs::create_dir_all(&temp).expect("mkdir");
2278
2279 for i in 0..200 {
2282 let content = format!(
2284 "function test_case_{i}() {{ return constant + {i}; }}\n\
2285 // shared comment line {i}\n\
2286 struct Foo {{ x: i32 }} // type {i}\n"
2287 )
2288 .repeat(5);
2289 let path = temp.join(format!("file_{i:04}.txt"));
2290 std::fs::write(&path, content.as_bytes()).expect("write");
2291 }
2292
2293 let mut config = crate::profile::balanced();
2294 config.defaults.text_codec = "zstd".into();
2296 config.defaults.metadata_codec = "zstd".into();
2300 config.dictionaries.enabled = true;
2301 config.dictionaries.min_class_size = 50;
2302 config.dictionaries.max_dict_size = 8192;
2303
2304 let artifact = write_directory_with_config(&temp, &config).expect("write");
2305 std::fs::remove_dir_all(&temp).ok();
2306
2307 let mut cursor = ManifestCursor::new(&artifact.bytes);
2312 let _ = limnifs_core::parse_manifest_header(&mut cursor).expect("header");
2313 let _ = limnifs_core::parse_feature_flags_section(&mut cursor).expect("flags");
2314 let _ = limnifs_core::parse_metadata_reference(&mut cursor).expect("meta_ref");
2315 let _ = limnifs_core::parse_slab_index(&mut cursor).expect("slab_index");
2316 let _ = limnifs_core::parse_history(&mut cursor).expect("history");
2317 let _remaining = cursor.remaining_len();
2320 }
2321
2322 #[test]
2323 fn write_empty_directory() {
2324 let temp =
2325 std::env::temp_dir().join(format!("limnifs-write-test-{}-empty", std::process::id()));
2326 std::fs::create_dir_all(&temp).expect("create temp dir");
2327 let artifact = write_directory(&temp).expect("write succeeds");
2328 std::fs::remove_dir_all(&temp).ok();
2329 assert!(artifact.inode_count >= 1);
2330 assert_eq!(artifact.file_count, 0);
2331 assert_eq!(artifact.dir_count, 1);
2332 assert!(artifact.slabs.is_empty());
2333 }
2334
2335 #[test]
2336 fn write_small_file_inline() {
2337 let temp =
2338 std::env::temp_dir().join(format!("limnifs-write-test-{}-small", std::process::id()));
2339 std::fs::create_dir_all(&temp).expect("create temp dir");
2340 std::fs::write(temp.join("hello.txt"), b"hello world").expect("write file");
2341 let artifact = write_directory(&temp).expect("write succeeds");
2342 std::fs::remove_dir_all(&temp).ok();
2343 assert_eq!(artifact.file_count, 1);
2344 assert!(artifact.slabs.is_empty());
2345 assert_eq!(artifact.drop_count, 0);
2346 }
2347
2348 #[test]
2349 fn write_large_file_uses_slab() {
2350 let temp =
2351 std::env::temp_dir().join(format!("limnifs-write-test-{}-large", std::process::id()));
2352 std::fs::create_dir_all(&temp).expect("create temp dir");
2353 let large_data = vec![0xABu8; INLINE_THRESHOLD + 100];
2354 std::fs::write(temp.join("big.bin"), &large_data).expect("write big");
2355 let artifact = write_directory(&temp).expect("write succeeds");
2356 std::fs::remove_dir_all(&temp).ok();
2357 assert_eq!(artifact.drop_count, 1);
2358 assert_eq!(artifact.slabs.len(), 1);
2359 }
2360
2361 #[test]
2362 fn write_mixed_inline_and_large() {
2363 let temp =
2364 std::env::temp_dir().join(format!("limnifs-write-test-{}-mix", std::process::id()));
2365 std::fs::create_dir_all(&temp).expect("create temp dir");
2366 std::fs::write(temp.join("small.txt"), b"tiny").expect("write small");
2367 std::fs::write(temp.join("large.bin"), vec![0xCDu8; INLINE_THRESHOLD * 2])
2368 .expect("write large");
2369 let artifact = write_directory(&temp).expect("write succeeds");
2370 std::fs::remove_dir_all(&temp).ok();
2371 assert_eq!(artifact.file_count, 2);
2372 assert_eq!(artifact.drop_count, 1);
2373 assert_eq!(artifact.slabs.len(), 1);
2374 }
2375
2376 #[test]
2377 fn deduplicates_identical_large_files() {
2378 let temp =
2379 std::env::temp_dir().join(format!("limnifs-write-test-{}-dedup", std::process::id()));
2380 std::fs::create_dir_all(&temp).expect("create temp dir");
2381 let data = vec![0x77u8; INLINE_THRESHOLD + 10];
2382 std::fs::write(temp.join("a.bin"), &data).expect("write a");
2383 std::fs::write(temp.join("b.bin"), &data).expect("write b");
2384 let artifact = write_directory(&temp).expect("write succeeds");
2385 std::fs::remove_dir_all(&temp).ok();
2386 assert_eq!(artifact.drop_count, 1);
2387 }
2388
2389 #[test]
2390 fn write_and_verify_roundtrip() {
2391 let temp = std::env::temp_dir().join(format!(
2392 "limnifs-write-test-{}-roundtrip",
2393 std::process::id()
2394 ));
2395 std::fs::create_dir_all(&temp).expect("create temp dir");
2396 std::fs::write(temp.join("a.txt"), b"aaa").expect("write a");
2397 std::fs::write(temp.join("b.txt"), b"bbb").expect("write b");
2398 std::fs::create_dir_all(temp.join("sub")).expect("create sub");
2399 std::fs::write(temp.join("sub").join("c.txt"), b"ccc").expect("write c");
2400 let artifact = write_directory(&temp).expect("write succeeds");
2401 std::fs::remove_dir_all(&temp).ok();
2402 assert_eq!(artifact.file_count, 3);
2403 assert_eq!(artifact.dir_count, 2);
2404
2405 let mut cursor = ManifestCursor::new(&artifact.bytes);
2406 limnifs_core::parse_manifest_header(&mut cursor).expect("header");
2407 limnifs_core::parse_feature_flags_section(&mut cursor).expect("flags");
2408 let meta_ref = limnifs_core::parse_metadata_reference(&mut cursor).expect("meta ref");
2409 assert!(meta_ref.is_inlined());
2410 let slab_index = limnifs_core::parse_slab_index(&mut cursor).expect("slab index");
2411 assert_eq!(slab_index.len(), 0);
2412 limnifs_core::parse_history(&mut cursor).expect("history");
2413 }
2414
2415 #[test]
2416 fn write_deterministic() {
2417 let temp =
2418 std::env::temp_dir().join(format!("limnifs-write-test-{}-det", std::process::id()));
2419 std::fs::create_dir_all(&temp).expect("create temp dir");
2420 std::fs::write(temp.join("x.txt"), b"xxx").expect("write x");
2421
2422 let a1 = write_directory(&temp).expect("first write");
2423 let a2 = write_directory(&temp).expect("second write");
2424 std::fs::remove_dir_all(&temp).ok();
2425
2426 assert_eq!(a1.bytes, a2.bytes);
2427 assert_eq!(a1.merkle_root, a2.merkle_root);
2428 }
2429
2430 #[test]
2431 fn slab_parses_correctly() {
2432 let temp =
2433 std::env::temp_dir().join(format!("limnifs-write-test-{}-slab", std::process::id()));
2434 std::fs::create_dir_all(&temp).expect("create temp dir");
2435 std::fs::write(temp.join("big.bin"), vec![0x11u8; INLINE_THRESHOLD + 1])
2436 .expect("write big");
2437 let artifact = write_directory(&temp).expect("write succeeds");
2438 std::fs::remove_dir_all(&temp).ok();
2439
2440 let slab_bytes = &artifact.slabs[0].bytes;
2441 let mut cursor = ManifestCursor::new(slab_bytes);
2442 let slab_header = limnifs_core::parse_slab_header(&mut cursor).expect("slab header parses");
2443 assert_eq!(slab_header.format_version, 1);
2444 assert!(!slab_header.is_sealed());
2445 assert!(!slab_header.has_erasure_coding());
2446
2447 let drop_record =
2448 limnifs_core::parse_drop_record(&mut cursor, &slab_header).expect("drop record parses");
2449 assert_eq!(drop_record.plaintext_len as usize, INLINE_THRESHOLD + 1);
2450 }
2451
2452 #[test]
2453 fn fastcdc_produces_multiple_chunks_for_large_files() {
2454 let temp = std::env::temp_dir().join(format!(
2457 "limnifs-write-test-{}-cdc-multi",
2458 std::process::id()
2459 ));
2460 std::fs::create_dir_all(&temp).expect("create temp dir");
2461 let data = pseudo_random_bytes(42, 1024 * 1024);
2462 std::fs::write(temp.join("big.bin"), &data).expect("write big");
2463 let artifact = write_directory(&temp).expect("write succeeds");
2464 std::fs::remove_dir_all(&temp).ok();
2465 assert!(
2466 artifact.drop_count > 1,
2467 "expected FastCDC to produce multiple drops for 1 MiB input, got {}",
2468 artifact.drop_count
2469 );
2470 }
2471
2472 #[test]
2473 fn fastcdc_deduplicates_shared_substrings() {
2474 let temp = std::env::temp_dir().join(format!(
2478 "limnifs-write-test-{}-cdc-dedup",
2479 std::process::id()
2480 ));
2481 std::fs::create_dir_all(&temp).expect("create temp dir");
2482 let shared = pseudo_random_bytes(7, 512 * 1024);
2483 let mut a = Vec::with_capacity(shared.len() + 1024);
2484 a.extend_from_slice(&pseudo_random_bytes(1, 1024));
2485 a.extend_from_slice(&shared);
2486 let mut b = Vec::with_capacity(shared.len() + 2048);
2487 b.extend_from_slice(&pseudo_random_bytes(2, 2048));
2488 b.extend_from_slice(&shared);
2489 std::fs::write(temp.join("a.bin"), &a).expect("write a");
2490 std::fs::write(temp.join("b.bin"), &b).expect("write b");
2491
2492 let temp_a = std::env::temp_dir().join(format!(
2494 "limnifs-write-test-{}-cdc-dedup-a",
2495 std::process::id()
2496 ));
2497 std::fs::create_dir_all(&temp_a).expect("create temp_a");
2498 std::fs::write(temp_a.join("a.bin"), &a).expect("write a");
2499 let artifact_a = write_directory(&temp_a).expect("a writes");
2500 std::fs::remove_dir_all(&temp_a).ok();
2501
2502 let temp_b = std::env::temp_dir().join(format!(
2503 "limnifs-write-test-{}-cdc-dedup-b",
2504 std::process::id()
2505 ));
2506 std::fs::create_dir_all(&temp_b).expect("create temp_b");
2507 std::fs::write(temp_b.join("b.bin"), &b).expect("write b");
2508 let artifact_b = write_directory(&temp_b).expect("b writes");
2509 std::fs::remove_dir_all(&temp_b).ok();
2510
2511 let artifact_both = write_directory(&temp).expect("both write");
2512 std::fs::remove_dir_all(&temp).ok();
2513
2514 let sum_alone = artifact_a.drop_count + artifact_b.drop_count;
2515 assert!(
2516 artifact_both.drop_count < sum_alone,
2517 "expected dedup win: both together = {} drops, sum alone = {} drops",
2518 artifact_both.drop_count,
2519 sum_alone
2520 );
2521 }
2522
2523 #[test]
2524 fn slab_splits_when_content_exceeds_ceiling() {
2525 let temp =
2531 std::env::temp_dir().join(format!("limnifs-write-test-{}-split", std::process::id()));
2532 std::fs::create_dir_all(&temp).expect("create temp dir");
2533 for i in 0..7u32 {
2534 let data = pseudo_random_bytes(u64::from(i), 10 * 1024 * 1024);
2536 std::fs::write(temp.join(format!("big-{i}.bin")), &data).expect("write big");
2537 }
2538 let artifact = write_directory(&temp).expect("write succeeds");
2539 std::fs::remove_dir_all(&temp).ok();
2540
2541 assert!(
2543 artifact.slabs.len() >= 2,
2544 "expected at least 2 slabs for 70 MiB of incompressible data, got {}",
2545 artifact.slabs.len()
2546 );
2547 for slab in &artifact.slabs {
2548 assert!(
2549 slab.bytes.len() <= MAX_SLAB_TOTAL_BYTES,
2550 "slab {} is {} bytes (> {} ceiling)",
2551 slab.id.ordinal,
2552 slab.bytes.len(),
2553 MAX_SLAB_TOTAL_BYTES,
2554 );
2555 }
2556 let total_drop_ids: usize = artifact.slabs.iter().map(|s| s.drop_ids.len()).sum();
2558 assert_eq!(
2559 total_drop_ids, artifact.drop_count,
2560 "drop_ids count across slabs must match WriteArtifact.drop_count",
2561 );
2562 }
2563}