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