Skip to main content

limnifs_write/config/
profile.rs

1//! Compression profiles — predefined codec strategies for different goals.
2//!
3//! A [`CompressionProfile`] bundles codec selection, parameters, tournament
4//! behavior, and chunking into a single named configuration. Four built-in
5//! profiles cover the main use cases; users can define custom profiles via
6//! TOML.
7//!
8//! ## Built-in profiles
9//!
10//! | Profile | Goal | Create speed | Ratio | vs SquashFS | vs DwarFS |
11//! |---------|------|-------------|-------|------------|-----------|
12//! | `max-ratio` | Smallest output | Slow | Best | Wins ratio | Ties ratio |
13//! | `max-speed` | Fastest create | Match SquashFS | OK | Ties speed | Wins speed |
14//! | `balanced` | Good trade-off | Medium | Good | Wins ratio | Wins speed |
15//! | `competitive` | Beat both | Fast | Best-of-both | **Wins both** | **Wins both** |
16//!
17//! ## Usage
18//!
19//! ```toml
20//! # Use a built-in profile
21//! profile = "competitive"
22//!
23//! # Or define a custom profile inline
24//! [profile]
25//! name = "my-custom"
26//! text_codec = "brotli"
27//! brotli_quality = 7
28//! binary_codec = "lz4"
29//! chunk_avg_size = 32768
30//! tournament = "none"
31//! ```
32
33#![allow(warnings)]
34
35use serde::{Deserialize, Serialize};
36
37use crate::config::{
38    ChunkingConfig, CodecTunables, Defaults, DictionaryConfig, EncryptionConfig, TournamentConfig,
39    WriteConfig,
40};
41
42/// Built-in profile names.
43pub const MAX_RATIO: &str = "max-ratio";
44pub const MAX_SPEED: &str = "max-speed";
45pub const BALANCED: &str = "balanced";
46pub const COMPETITIVE: &str = "competitive";
47pub const MAX_READ: &str = "max-read";
48pub const MAX_WRITE: &str = "max-write";
49pub const MAX_WRITE_RW: &str = "max-write-rw";
50pub const MAX_READ_RW: &str = "max-read-rw";
51pub const BALANCED_RW: &str = "balanced-rw";
52
53/// Select a built-in profile by name. Returns a complete [`WriteConfig`]
54/// configured for that profile's strategy.
55#[must_use]
56pub fn select(name: &str) -> Option<WriteConfig> {
57    match name {
58        MAX_RATIO => Some(max_ratio()),
59        MAX_SPEED => Some(max_speed()),
60        BALANCED => Some(balanced()),
61        COMPETITIVE => Some(competitive()),
62        MAX_READ => Some(max_read()),
63        MAX_WRITE => Some(max_write()),
64        MAX_WRITE_RW => Some(max_write_rw()),
65        MAX_READ_RW => Some(max_read_rw()),
66        BALANCED_RW => Some(balanced_rw()),
67        _ => None,
68    }
69}
70
71/// Maximum compression ratio. Tries every applicable codec per drop,
72/// picks the smallest. Slowest create, smallest output.
73///
74/// - Text: Brotli q11 + LZMA + PPMd7 (256 MB budget) tournament
75/// - Binary: ZSTD L19 + LZMA tournament
76/// - Categorizers: all enabled (FLAC, Rice++, FSST+Brotli)
77/// - Chunks: 64 KB (better cross-chunk pattern matching)
78/// - Whole-file max: 256 MB
79#[must_use]
80pub fn max_ratio() -> WriteConfig {
81    WriteConfig {
82        defaults: Defaults {
83            text_codec: "brotli".into(),
84            binary_codec: "zstd".into(),
85            metadata_codec: "brotli".into(),
86            metadata_quality: 11,
87            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
88            shared_inline: true,
89            // Ratio over read-cost: keep whole-file categorizer
90            // claims at any size (0 = unlimited) and monolithic (a
91            // container's independent frames cost 1-3%).
92            max_drop_size: 0,
93            seekable_drops: false,
94            inline_threshold: 8192,
95        },
96        categorizers: crate::config::defaults::all_v0_1(),
97        chunking: ChunkingConfig {
98            name: "fastcdc".into(),
99            avg_chunk_size: 65_536,
100            min_chunk_size: 8192,
101            max_chunk_size: 262_144,
102        },
103        tournament: TournamentConfig {
104            codecs: vec![
105                "store".into(),
106                "lz4".into(),
107                "lz4-hc".into(),
108                "zstd".into(),
109                "brotli".into(),
110                "ppmd".into(),
111                "bzip2".into(),
112            ],
113            min_size_threshold: 256,
114            skip_for_binary: false,
115            short_circuit_threshold: 0,
116        },
117        codec_tunables: CodecTunables {
118            ppmd7: crate::config::Ppmd7Tunables {
119                order: 6,
120                memory_budget_mb: 256,
121            },
122            ppmd8: crate::config::Ppmd8Tunables {
123                order: 8,
124                memory_budget_mb: 128,
125            },
126            brotli: crate::config::BrotliTunables {
127                quality: 11,
128                window: 24,
129            },
130            lzma: crate::config::LzmaTunables {
131                lc: 3,
132                lp: 0,
133                pb: 2,
134                dict_size_mb: 64,
135                use_optimal_parser: true,
136            },
137            bzip2: crate::config::Bzip2Tunables { block_size_kb: 900 },
138        },
139        encryption: EncryptionConfig {
140            aead: "chacha20-poly1305".into(),
141            key_wrap: "x25519-hkdf".into(),
142        },
143        dictionaries: DictionaryConfig {
144            enabled: true,
145            min_class_size: 50,
146            max_dict_size: 131_072,
147            trainer: "frequency".into(),
148        },
149        mode: crate::config::ImageMode::ReadOnly,
150        write_codec: "lz4".into(),
151        turnover_threshold: 0,
152        skip_chunking: false,
153    }
154}
155
156/// Maximum speed. Single-codec per content class, no tournament.
157/// Matches SquashFS LZ4 speed on binary data.
158///
159/// - Text: LZ4 (instant)
160/// - Binary: LZ4 (instant)
161/// - Categorizers: disabled (no FLAC, no Rice++)
162/// - Tournament: none (classify once, compress once)
163/// - Chunks: 4 KB (maximum parallelism)
164#[must_use]
165pub fn max_speed() -> WriteConfig {
166    WriteConfig {
167        defaults: Defaults {
168            text_codec: "lz4".into(),
169            binary_codec: "lz4".into(),
170            metadata_codec: "lz4".into(),
171            metadata_quality: 1,
172            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
173            shared_inline: true,
174            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
175            seekable_drops: true,
176            inline_threshold: 4096,
177        },
178        categorizers: vec![],
179        chunking: ChunkingConfig {
180            name: "fastcdc".into(),
181            avg_chunk_size: 4096,
182            min_chunk_size: 512,
183            max_chunk_size: 16_384,
184        },
185        tournament: TournamentConfig {
186            codecs: vec!["store".into(), "lz4".into()],
187            min_size_threshold: 0,
188            skip_for_binary: true,
189            short_circuit_threshold: 500,
190        },
191        codec_tunables: CodecTunables {
192            brotli: crate::config::BrotliTunables {
193                quality: 0,
194                window: 10,
195            },
196            ..CodecTunables::default()
197        },
198        encryption: EncryptionConfig {
199            aead: "chacha20-poly1305".into(),
200            key_wrap: "x25519-hkdf".into(),
201        },
202        dictionaries: DictionaryConfig {
203            enabled: false,
204            min_class_size: 0,
205            max_dict_size: 0,
206            trainer: "frequency".into(),
207        },
208        mode: crate::config::ImageMode::ReadOnly,
209        write_codec: "lz4".into(),
210        turnover_threshold: 0,
211        skip_chunking: false,
212    }
213}
214
215/// Balanced profile. Good ratio + good speed for general use.
216///
217/// - Text: Brotli q5 (fast, good ratio)
218/// - Binary: LZ4 (fast)
219/// - Categorizers: FLAC for small audio, skip large
220/// - Tournament: Brotli + ZSTD only
221/// - Chunks: 16 KB
222#[must_use]
223pub fn balanced() -> WriteConfig {
224    WriteConfig {
225        defaults: Defaults {
226            text_codec: "brotli".into(),
227            binary_codec: "lz4".into(),
228            metadata_codec: "zstd".into(),
229            metadata_quality: 3,
230            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
231            shared_inline: true,
232            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
233            seekable_drops: true,
234            inline_threshold: 4096,
235        },
236        categorizers: crate::config::defaults::all_v0_1(),
237        chunking: ChunkingConfig {
238            name: "fastcdc".into(),
239            avg_chunk_size: 16_384,
240            min_chunk_size: 2048,
241            max_chunk_size: 65_536,
242        },
243        tournament: TournamentConfig {
244            codecs: vec!["store".into(), "lz4".into(), "brotli".into()],
245            min_size_threshold: 256,
246            skip_for_binary: true,
247            short_circuit_threshold: 250,
248        },
249        codec_tunables: CodecTunables {
250            brotli: crate::config::BrotliTunables {
251                quality: 5,
252                window: 22,
253            },
254            ..CodecTunables::default()
255        },
256        encryption: EncryptionConfig {
257            aead: "chacha20-poly1305".into(),
258            key_wrap: "x25519-hkdf".into(),
259        },
260        dictionaries: DictionaryConfig {
261            enabled: true,
262            min_class_size: 100,
263            max_dict_size: 65_536,
264            trainer: "frequency".into(),
265        },
266        mode: crate::config::ImageMode::ReadOnly,
267        write_codec: "lz4".into(),
268        turnover_threshold: 0,
269        skip_chunking: false,
270    }
271}
272
273/// Competitive profile — beat SquashFS on ratio AND DwarFS on speed.
274///
275/// Uses ZSTD L1 for text (5x faster compress than Brotli, 3x faster
276/// decompress, 3x better ratio than SquashFS LZ4). LZ4 for binary.
277#[must_use]
278pub fn competitive() -> WriteConfig {
279    WriteConfig {
280        defaults: Defaults {
281            text_codec: "zstd".into(),
282            binary_codec: "lz4".into(),
283            metadata_codec: "zstd".into(),
284            metadata_quality: 3,
285            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
286            shared_inline: true,
287            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
288            seekable_drops: true,
289            inline_threshold: 4096,
290        },
291        categorizers: crate::config::defaults::all_v0_1(),
292        chunking: ChunkingConfig {
293            name: "fastcdc".into(),
294            avg_chunk_size: 8192,
295            min_chunk_size: 1024,
296            max_chunk_size: 65_536,
297        },
298        tournament: TournamentConfig {
299            codecs: vec!["store".into(), "lz4".into(), "brotli".into()],
300            min_size_threshold: 0,
301            skip_for_binary: true,
302            short_circuit_threshold: 250,
303        },
304        codec_tunables: CodecTunables {
305            brotli: crate::config::BrotliTunables {
306                quality: 5,
307                window: 22,
308            },
309            ..CodecTunables::default()
310        },
311        encryption: EncryptionConfig {
312            aead: "chacha20-poly1305".into(),
313            key_wrap: "x25519-hkdf".into(),
314        },
315        dictionaries: DictionaryConfig {
316            enabled: false,
317            min_class_size: 0,
318            max_dict_size: 0,
319            trainer: "frequency".into(),
320        },
321        mode: crate::config::ImageMode::ReadOnly,
322        write_codec: "lz4".into(),
323        turnover_threshold: 0,
324        skip_chunking: false,
325    }
326}
327
328/// Maximum read profile — optimized for read-heavy workloads (write
329/// once, read many). Uses codecs with the best ratio that still
330/// decompresses quickly. Write cost is amortised over many reads.
331///
332/// - Text/Binary: ZSTD L19 (best ratio among fast-decompress codecs;
333///   ZSTD decompresses at ~1500 MB/s vs Brotli's ~500 MB/s)
334/// - Metadata: ZSTD L19
335/// - Categorizers: enabled (FLAC, Rice++ for best ratio per file type)
336/// - Chunks: 64 KB (fewer drops = fewer slab lookups during extract)
337/// - Inline threshold: 8192 (more inline = fewer slab reads)
338#[must_use]
339pub fn max_read() -> WriteConfig {
340    WriteConfig {
341        defaults: Defaults {
342            text_codec: "zstd".into(),
343            binary_codec: "zstd".into(),
344            metadata_codec: "zstd".into(),
345            metadata_quality: 11,
346            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
347            shared_inline: true,
348            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
349            seekable_drops: true,
350            inline_threshold: 8192,
351        },
352        categorizers: crate::config::defaults::all_v0_1(),
353        chunking: ChunkingConfig {
354            name: "fastcdc".into(),
355            avg_chunk_size: 65_536,
356            min_chunk_size: 8192,
357            max_chunk_size: 262_144,
358        },
359        tournament: TournamentConfig {
360            codecs: vec!["store".into(), "lz4".into(), "zstd".into(), "brotli".into()],
361            min_size_threshold: 256,
362            skip_for_binary: false,
363            short_circuit_threshold: 200,
364        },
365        codec_tunables: CodecTunables {
366            brotli: crate::config::BrotliTunables {
367                quality: 11,
368                window: 22,
369            },
370            lzma: crate::config::LzmaTunables {
371                dict_size_mb: 64,
372                use_optimal_parser: true,
373                ..crate::config::LzmaTunables::default()
374            },
375            ..CodecTunables::default()
376        },
377        encryption: EncryptionConfig {
378            aead: "chacha20-poly1305".into(),
379            key_wrap: "x25519-hkdf".into(),
380        },
381        dictionaries: DictionaryConfig {
382            enabled: true,
383            min_class_size: 50,
384            max_dict_size: 131_072,
385            trainer: "frequency".into(),
386        },
387        mode: crate::config::ImageMode::ReadOnly,
388        write_codec: "lz4".into(),
389        turnover_threshold: 0,
390        skip_chunking: false,
391    }
392}
393
394/// Maximum write profile — optimized for write-heavy workloads where
395/// write latency matters more than ratio. Uses the fastest possible
396/// compression (LZ4 at ~1 GB/s) and skips all categorization/tournament
397/// overhead.
398///
399/// - Text/Binary/Metadata: LZ4 (fastest compress AND decompress)
400/// - Categorizers: disabled (zero categorization overhead)
401/// - Tournament: none (classify once, compress once)
402/// - Chunks: 128 KB (minimal per-chunk overhead)
403#[must_use]
404pub fn max_write() -> WriteConfig {
405    WriteConfig {
406        defaults: Defaults {
407            text_codec: "lz4".into(),
408            binary_codec: "lz4".into(),
409            metadata_codec: "lz4".into(),
410            metadata_quality: 1,
411            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
412            shared_inline: true,
413            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
414            seekable_drops: true,
415            inline_threshold: 4096,
416        },
417        categorizers: vec![],
418        chunking: ChunkingConfig {
419            name: "fastcdc".into(),
420            avg_chunk_size: 131_072,
421            min_chunk_size: 16_384,
422            max_chunk_size: 524_288,
423        },
424        tournament: TournamentConfig {
425            codecs: vec!["store".into(), "lz4".into()],
426            min_size_threshold: 0,
427            skip_for_binary: true,
428            short_circuit_threshold: 500,
429        },
430        codec_tunables: CodecTunables::default(),
431        encryption: EncryptionConfig {
432            aead: "chacha20-poly1305".into(),
433            key_wrap: "x25519-hkdf".into(),
434        },
435        dictionaries: DictionaryConfig {
436            enabled: false,
437            min_class_size: 0,
438            max_dict_size: 0,
439            trainer: "frequency".into(),
440        },
441        mode: crate::config::ImageMode::ReadOnly,
442        write_codec: "lz4".into(),
443        turnover_threshold: 0,
444        skip_chunking: true,
445    }
446}
447
448/// Maximum write profile for RW images — optimized for write-heavy
449/// live filesystems where write latency per operation matters most.
450///
451/// - Write codec: LZ4 (instant compress, minimal write latency)
452/// - Turnover codec: ZSTD L12 (re-compaction with decent ratio)
453/// - Mode: CopyOnWrite (fast updates, unreferenced blocks reclaimed)
454/// - Chunks: 128 KB (minimal per-chunk overhead per write)
455/// - Turnover threshold: 500 updates
456#[must_use]
457pub fn max_write_rw() -> WriteConfig {
458    WriteConfig {
459        defaults: Defaults {
460            text_codec: "lz4".into(),
461            binary_codec: "lz4".into(),
462            metadata_codec: "lz4".into(),
463            metadata_quality: 1,
464            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
465            shared_inline: true,
466            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
467            seekable_drops: true,
468            inline_threshold: 4096,
469        },
470        categorizers: vec![],
471        chunking: ChunkingConfig {
472            name: "fastcdc".into(),
473            avg_chunk_size: 131_072,
474            min_chunk_size: 16_384,
475            max_chunk_size: 524_288,
476        },
477        tournament: TournamentConfig {
478            codecs: vec!["store".into(), "lz4".into()],
479            min_size_threshold: 0,
480            skip_for_binary: true,
481            short_circuit_threshold: 500,
482        },
483        codec_tunables: CodecTunables::default(),
484        mode: crate::config::ImageMode::ReadWrite(crate::config::RWMode::CopyOnWrite),
485        write_codec: "lz4".into(),
486        turnover_threshold: 500,
487        skip_chunking: true,
488        encryption: EncryptionConfig {
489            aead: "chacha20-poly1305".into(),
490            key_wrap: "x25519-hkdf".into(),
491        },
492        dictionaries: DictionaryConfig {
493            enabled: false,
494            min_class_size: 0,
495            max_dict_size: 0,
496            trainer: "frequency".into(),
497        },
498    }
499}
500
501/// Maximum read profile for RW images — optimized for read-heavy
502/// live filesystems where read throughput and integrity matter.
503///
504/// - Write codec: ZSTD L6 (good ratio, decent compress speed)
505/// - Turnover codec: ZSTD L19 (best ratio for compaction)
506/// - Mode: UpdateInPlace (full history for audit trail)
507/// - Chunks: 64 KB (fewer drops to traverse during reads)
508/// - Turnover threshold: 1000 updates
509#[must_use]
510pub fn max_read_rw() -> WriteConfig {
511    WriteConfig {
512        defaults: Defaults {
513            text_codec: "zstd".into(),
514            binary_codec: "zstd".into(),
515            metadata_codec: "zstd".into(),
516            metadata_quality: 6,
517            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
518            shared_inline: true,
519            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
520            seekable_drops: true,
521            inline_threshold: 8192,
522        },
523        categorizers: crate::config::defaults::all_v0_1(),
524        chunking: ChunkingConfig {
525            name: "fastcdc".into(),
526            avg_chunk_size: 65_536,
527            min_chunk_size: 8192,
528            max_chunk_size: 262_144,
529        },
530        tournament: TournamentConfig {
531            codecs: vec!["store".into(), "lz4".into(), "zstd".into(), "brotli".into()],
532            min_size_threshold: 256,
533            skip_for_binary: false,
534            short_circuit_threshold: 200,
535        },
536        codec_tunables: CodecTunables {
537            brotli: crate::config::BrotliTunables {
538                quality: 11,
539                window: 22,
540            },
541            ..CodecTunables::default()
542        },
543        mode: crate::config::ImageMode::ReadWrite(crate::config::RWMode::UpdateInPlace),
544        write_codec: "zstd".into(),
545        turnover_threshold: 1000,
546        skip_chunking: false,
547        encryption: EncryptionConfig {
548            aead: "chacha20-poly1305".into(),
549            key_wrap: "x25519-hkdf".into(),
550        },
551        dictionaries: DictionaryConfig {
552            enabled: true,
553            min_class_size: 50,
554            max_dict_size: 131_072,
555            trainer: "frequency".into(),
556        },
557    }
558}
559
560/// Balanced RW profile — general-purpose read-write image.
561///
562/// - Write codec: ZSTD L1 (fast, decent ratio per write)
563/// - Turnover codec: Brotli q5 (good ratio compaction)
564/// - Mode: UpdateInPlace
565/// - Chunks: 16 KB
566/// - Turnover threshold: 1000 updates
567#[must_use]
568pub fn balanced_rw() -> WriteConfig {
569    WriteConfig {
570        defaults: Defaults {
571            text_codec: "zstd".into(),
572            binary_codec: "lz4".into(),
573            metadata_codec: "zstd".into(),
574            metadata_quality: 3,
575            metadata_externalize_threshold: crate::METADATA_EXTERNALIZE_THRESHOLD,
576            shared_inline: true,
577            max_drop_size: crate::config::DEFAULT_MAX_DROP_SIZE,
578            seekable_drops: true,
579            inline_threshold: 4096,
580        },
581        categorizers: crate::config::defaults::all_v0_1(),
582        chunking: ChunkingConfig {
583            name: "fastcdc".into(),
584            avg_chunk_size: 16_384,
585            min_chunk_size: 2048,
586            max_chunk_size: 65_536,
587        },
588        tournament: TournamentConfig {
589            codecs: vec!["store".into(), "lz4".into(), "zstd".into()],
590            min_size_threshold: 256,
591            skip_for_binary: true,
592            short_circuit_threshold: 250,
593        },
594        codec_tunables: CodecTunables {
595            brotli: crate::config::BrotliTunables {
596                quality: 5,
597                window: 22,
598            },
599            ..CodecTunables::default()
600        },
601        mode: crate::config::ImageMode::ReadWrite(crate::config::RWMode::UpdateInPlace),
602        write_codec: "zstd".into(),
603        turnover_threshold: 1000,
604        skip_chunking: false,
605        encryption: EncryptionConfig {
606            aead: "chacha20-poly1305".into(),
607            key_wrap: "x25519-hkdf".into(),
608        },
609        dictionaries: DictionaryConfig {
610            enabled: true,
611            min_class_size: 100,
612            max_dict_size: 65_536,
613            trainer: "frequency".into(),
614        },
615    }
616}
617
618/// TOML-representable profile selector. Either a built-in name or
619/// an inline custom profile.
620#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
621#[serde(untagged)]
622pub enum ProfileSpec {
623    /// Use a built-in profile by name.
624    Preset(String),
625    /// Define a custom profile inline.
626    Custom(CustomProfile),
627}
628
629/// User-defined profile fields. Any field not specified inherits from
630/// the `balanced` profile.
631#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
632pub struct CustomProfile {
633    pub name: String,
634    #[serde(default = "default_text")]
635    pub text_codec: String,
636    #[serde(default = "default_binary")]
637    pub binary_codec: String,
638    #[serde(default = "default_quality")]
639    pub brotli_quality: u8,
640    #[serde(default)]
641    pub chunk_avg_size: u32,
642    #[serde(default)]
643    pub skip_tournament_for_binary: bool,
644    #[serde(default = "default_true")]
645    pub enable_flac: bool,
646    #[serde(default = "default_true")]
647    pub enable_ricepp: bool,
648}
649
650fn default_text() -> String {
651    "brotli".into()
652}
653fn default_binary() -> String {
654    "lz4".into()
655}
656fn default_quality() -> u8 {
657    5
658}
659fn default_true() -> bool {
660    true
661}
662
663/// Resolve a [`ProfileSpec`] into a concrete [`WriteConfig`].
664pub fn resolve(spec: &ProfileSpec) -> Option<WriteConfig> {
665    match spec {
666        ProfileSpec::Preset(name) => select(name),
667        ProfileSpec::Custom(custom) => {
668            let mut config = balanced();
669            if !custom.text_codec.is_empty() {
670                config.defaults.text_codec = custom.text_codec.clone();
671            }
672            if !custom.binary_codec.is_empty() {
673                config.defaults.binary_codec = custom.binary_codec.clone();
674            }
675            if custom.brotli_quality > 0 {
676                config.codec_tunables.brotli.quality = custom.brotli_quality;
677            }
678            if custom.chunk_avg_size > 0 {
679                config.chunking.avg_chunk_size = custom.chunk_avg_size;
680            }
681            config.tournament.skip_for_binary = custom.skip_tournament_for_binary;
682            if !custom.enable_flac {
683                config.categorizers.retain(|c| c.name != "pcm_audio");
684            }
685            if !custom.enable_ricepp {
686                config.categorizers.retain(|c| c.name != "fits");
687            }
688            Some(config)
689        }
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[test]
698    fn all_builtins_resolve() {
699        for name in [
700            MAX_RATIO,
701            MAX_SPEED,
702            BALANCED,
703            COMPETITIVE,
704            MAX_READ,
705            MAX_WRITE,
706            MAX_WRITE_RW,
707            MAX_READ_RW,
708            BALANCED_RW,
709        ] {
710            let config = select(name).expect("profile exists");
711            config.validate().expect("validates");
712        }
713    }
714
715    #[test]
716    fn competitive_uses_lz4_for_binary() {
717        let config = competitive();
718        assert_eq!(config.binary_codec_id().unwrap(), 0x01); // LZ4
719    }
720
721    #[test]
722    fn competitive_uses_zstd_for_text() {
723        let config = competitive();
724        assert_eq!(config.text_codec_id().unwrap(), 0x02); // ZSTD
725    }
726
727    #[test]
728    fn max_speed_disables_categorizers() {
729        let config = max_speed();
730        assert!(config.categorizers.is_empty());
731    }
732
733    #[test]
734    fn max_ratio_enables_ppmd() {
735        let config = max_ratio();
736        assert_eq!(config.codec_tunables.ppmd7.memory_budget_mb, 256);
737    }
738
739    #[test]
740    fn custom_profile_inherits_balanced() {
741        let spec = ProfileSpec::Custom(CustomProfile {
742            name: "test".into(),
743            brotli_quality: 9,
744            ..CustomProfile::default()
745        });
746        let config = resolve(&spec).expect("resolves");
747        assert_eq!(config.codec_tunables.brotli.quality, 9);
748        // Inherited from balanced
749        assert_eq!(config.defaults.text_codec, "brotli");
750    }
751}