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