Skip to main content

stuffr_cli/compat/zstd/
mod.rs

1//! The zstd family (`zstd`, `unzstd`, `zstdcat`, `zstdmt`): stuffr
2//! behaving as zstd 1.5.7's command line tool.
3//!
4//! Ported from zstd 1.5.7 `programs/` by Yann Collet and the zstd
5//! contributors (Meta Platforms, Inc.; BSD-3-Clause, the licence ships as
6//! `LICENSE-zstd`), one module per C file: `cli.rs` (zstdcli.c's argument
7//! parsing, help texts and parameter printers), `dibio.rs` (dibio.c,
8//! `--train*`), `util.rs` (util.c),
9//! `display.rs` (fileio_common.h's display macros) and this module
10//! (zstdcli.c's `main` after the arguments). Every string is in
11//! `messages.rs`. libzstd itself is reached through
12//! `stuffr::zstd_opts`.
13//!
14//! **Names** (spec section 1): the four map to this family, and the
15//! presets come from the invoked name by zstdcli.c's `exeNameMatch` (the
16//! name, then the end or a `.`): `unzstd` decompresses, `zstdcat`
17//! decompresses to standard output with pass-through at display level 1,
18//! `zstdmt` compresses with `-T0`. zstdcli.c's gzip, xz, lzma and lz4
19//! presets are ported too, though dispatch never reaches them (those names
20//! belong to other families).
21//!
22//! **The machine** (R3): the real process counts its cores as util.c does
23//! (`host::cores`, shared with the lz4 family since 0.15.0); a sandboxed run ([`run_in_counted`]) sees 8
24//! logical and 4 physical cores.
25//!
26//! Known deviations (the source of truth; the README and the spec mirror
27//! this list):
28//!
29//! 1. The banner names stuffr wherever zstd prints `WELCOME_MESSAGE`
30//!    (`-V`, `-vV`, `-vvV`, the first line of `-H`, and `-v`'s welcome
31//!    line on standard error): `*** Zstandard CLI (stuffr X.Y.Z; zstd
32//!    1.5.7 compatible) (64-bit) v1.5.7, by Yann Collet ***`. `-qV` prints
33//!    exactly `1.5.7`, and `-vV`'s "supports:" line is zstd's. `-vvV`
34//!    prints zstd's POSIX lines for the platform, but not its three
35//!    library-version lines (the zlib, lz4 and lzma zstd links), which
36//!    describe zstd's own build.
37//! 2. The `-v` summary's timing fields vary from run to run, as they do
38//!    between two runs of zstd.
39//! 3. An input that is also the output is never lost.
40//! 4. Closed standard streams and a parent that ignores SIGPIPE: the Rust
41//!    runtime reopens a closed standard descriptor on `/dev/null` and
42//!    resets SIGPIPE before `main`, so neither is seen.
43//! 5. Out of scope, refused as a zstd built with `ZSTD_NOBENCH` and
44//!    `ZSTD_NOTRACE` refuses them: `-b`, `-e#`, `-i#` ("Incorrect
45//!    parameter: -b", exit 1) and `--trace` ("Incorrect parameter:
46//!    --trace"); `-H` omits their lines. `-B#`, `-S` and `-p#` keep the
47//!    reference's meaning. `--[no-]asyncio` is accepted and has no visible
48//!    effect. `--trace-file-stat` is accepted and prints no trace.
49//!    - `-l` on two malformed skippable frames (ruling T7-a): a size field
50//!      of `0xFFFFFFF8` makes zstd loop forever, and stuffr answers exit 1
51//!      with `could not find end of skippable frame`; a skippable frame cut
52//!      after 5 to 7 bytes makes zstd read uninitialised stack, and stuffr
53//!      reads a deterministic header. Neither upstream defect is
54//!      reproduced.
55//!    - At display level 5 and above, `EXM_THROW`'s `Error defined at
56//!      <file>, line <n>` line is not printed (ruling T4-b): it names the C
57//!      implementation's source lines.
58//! 6. `--train-cover` (ruling T1-a): libzstd 1.5.7's cover sort breaks ties
59//!    by comparing addresses (`cover.c:300`, `:316`), so a cover dictionary
60//!    depends on heap layout, in zstd itself too. It is promised valid and
61//!    usable by zstd, not byte for byte. `--train`, `--train-fastcover` and
62//!    `--train-legacy` write zstd's bytes. `--adapt` changes the level by
63//!    timing, so its frames are compatible, not identical.
64//!
65//! Spec deviation 7, the pure tier (`zstd-pure` without `zstd-c`), as far
66//! as compressing and decompressing go (`fileio.rs`):
67//!
68//! - Every level compresses with `ruzstd`'s `Fastest`, so the sizes and
69//!   ratios in the summaries and in `-vvvvvv`'s trace are the tier's own;
70//!   the reference decodes every frame. The compression parameters are
71//!   range-checked as libzstd checks them, then ignored: `-T`,
72//!   `--auto-threads` and `zstdmt` run single-threaded, and the LDM,
73//!   `--rsyncable`, block-size and overlap options have no effect.
74//! - Content size and window: our frames never record a content size, so
75//!   `--content-size` has no effect and `-l` on our own output says the
76//!   decompressed size is unknown. The window is 128 KiB whatever `--long`
77//!   asks.
78//! - Each input is compressed as one frame built in memory: memory use
79//!   grows with the input's size (a piped standard input is unbounded),
80//!   and a run that exhausts memory ends outside zstd's exit codes.
81//! - Compressing with `-D` or `--patch-from` is refused, after everything
82//!   zstd prints first: `zstd: error 1 : dictionary compression is not
83//!   available in this build (stuffr pure tier)`. Decompressing with either
84//!   works (a `--patch-from` prefix through a hand-built raw-content
85//!   dictionary, Task 1's spike S1).
86//! - `--train*` answers as a zstd built with `ZSTD_NODICT`: `--train`
87//!   prints `training mode not available`, exit 1, and `--train-cover`,
88//!   `--train-fastcover` and `--train-legacy` are no options there
89//!   (`Incorrect parameter`). `-H` still lists the dictionary builder.
90//! - `--adapt` is accepted and the level never moves (the encoder has no
91//!   jobs to measure).
92//! - A legacy frame (v0.5 to v0.7) is `unsupported format`, what a zstd
93//!   built without legacy support says.
94//! - A corrupt frame whose blocks overflow the output libzstd sized from
95//!   its header can be named `Data corruption detected` where libzstd says
96//!   `Destination buffer is too small`.
97//! - `--format=gzip|xz|lzma` write flate2's and `xz_opts`' pure encoders'
98//!   bytes, which the reference decodes (compatible, not identical; the
99//!   summaries' sizes are the tier's own).
100//! - A gzip member's deflate data goes through flate2 (miniz_oxide), which
101//!   accepts some streams zlib refuses (a distance reaching before the
102//!   output, a code with no end of block): where the reference says
103//!   `inflate error -3` and writes nothing, the pure tier may write bytes
104//!   and then fail (`premature gz end`), or, when the trailer's CRC and
105//!   size match what miniz_oxide decoded, succeed with exit 0 and that
106//!   output. With `zlib-c` it is zlib's own `inflate`.
107//! - An xz or lzma frame that fails to decode (the xz family's P6 and P9):
108//!   what is written before the error can be a strict prefix of what
109//!   liblzma writes, and corruption in the last bytes of a `.lzma` reads as
110//!   the input ending there (`premature lzma end`, the jobs before it
111//!   written) where liblzma says `lzma_code decoding error 9`.
112//!
113//! The foreign formats on both tiers (`formats.rs`; spec "Promises"):
114//! `--format=gzip` is zlib's bytes with `zlib-c`, `--format=xz|lzma`
115//! liblzma's with `xz-c`, `--format=lz4` liblz4 1.10.0's with `lz4-c`
116//! (`stuffr::lz4_opts`, plain `LZ4F` streaming: 64 KiB linked blocks, content
117//! size when known, content checksum, as fileio.c sets them; byte-identical
118//! from files, to standard output and from standard input). Without `lz4-c`
119//! the pure tier's `--format=lz4` is compatible: `lz4_flex`'s block
120//! compressor at every level, inside the same frame descriptor.
121//!
122//! 8. Hardening (ruling T5 of the 2026-10-31 security audit): a symlink at
123//!    an output name is never written through. zstd checks for an existing
124//!    output with `stat`, so a dangling symlink looks absent and is opened
125//!    `fopen(.., "wb")` through, creating the file it names, and a link to
126//!    a device or a directory is opened in place; `--train` writes its
127//!    dictionary with `fopen` whatever is there. stuffr checks with
128//!    `lstat`: a symlink, dangling or not, is an existing file (`already
129//!    exists; not overwritten`, or the question); overwriting it (`-f`, or
130//!    `y`) unlinks the link and writes a fresh file, its target untouched;
131//!    an output written in place is opened `O_NOFOLLOW`, and its times are
132//!    set with `AT_SYMLINK_NOFOLLOW` (on Apple's platforms too, whole
133//!    seconds as zstd's `utime`). `--train` still overwrites a regular
134//!    file without `-f`, as zstd does, but refuses a symlink at the
135//!    dictionary's name (`cannot open`, exit 3) unless `-f`, which replaces
136//!    the link. Why: a planted link must not redirect a write.
137
138use std::collections::BTreeMap;
139use std::ffi::{OsStr, OsString};
140use std::io::{self, Read, Write};
141use std::path::Path;
142use std::process::ExitCode;
143use std::sync::atomic::AtomicU64;
144use std::sync::{Arc, Mutex};
145
146use stuffr::zstd_opts::{self, CParamKey};
147
148use super::CountedRun;
149use super::host::{self, Cores, Host, SANDBOX_OUTPUT_CAP, Sandbox, reset_sigpipe};
150
151pub(crate) mod cli;
152mod dibio;
153pub(crate) mod display;
154pub(crate) mod fileio;
155mod formats;
156mod list;
157mod messages;
158pub(crate) mod util;
159
160use cli::{Command, Limits, Operation, Progress, STDINMARK, STDOUTMARK, Stop};
161use display::{Display, exm_throw};
162use messages::{self as m, printf};
163
164/// Entry point for the four names; `name` is the canonical invoked name,
165/// `argv0` the program as invoked (its last component is zstd's
166/// `programName`) and `args` the arguments after it.
167pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
168    // zstd dies of SIGPIPE writing to a closed pipe.
169    reset_sigpipe();
170    let argv0 = if argv0.is_empty() {
171        name.as_bytes()
172    } else {
173        argv0.as_encoded_bytes()
174    };
175    let env = |k: &str| std::env::var_os(k);
176    let mut err = io::stderr();
177    let code = main(
178        argv0,
179        &args,
180        &env,
181        &mut Host::Real,
182        host::cores(&Host::Real),
183        None,
184        &mut err,
185    );
186    // The process's exit status is the code's low byte, as `exit()` makes
187    // it.
188    ExitCode::from(code as u8)
189}
190
191/// [`run`] without the process: operands resolve against `dir`, standard
192/// input is `stdin` (a pipe), standard output and error are captured and
193/// returned with the exit code, neither is a terminal, `env` stands in for
194/// the environment (`ZSTD_CLEVEL`, `ZSTD_NBTHREADS`) and `SIGPIPE` is left
195/// alone. The machine is R3's fixed 8 logical and 4 physical cores.
196///
197/// R9's input-independent bounds hold whatever the arguments say: the
198/// decoder's window log is at most 24 (16 MiB; a lower `--memory` or
199/// `--long` stands), `--train*` loads at most 256 KiB of samples with at
200/// most 4 optimiser steps, and compression runs at most 2 workers; xz and
201/// lzma frames decode under a 256 MiB memory limit and compress at preset
202/// 6 at most (`formats.rs`). Standard output and output files share the [`SANDBOX_OUTPUT_CAP`]
203/// budget. No signal handler is installed. For the fuzz target; not a
204/// stable interface.
205#[doc(hidden)]
206pub fn run_in_counted(
207    dir: &Path,
208    name: &'static str,
209    args: &[OsString],
210    stdin: &[u8],
211    env: &dyn Fn(&str) -> Option<OsString>,
212) -> CountedRun {
213    let stdout = Arc::new(Mutex::new(Vec::new()));
214    let reads = Arc::new(Mutex::new(BTreeMap::new()));
215    let mut host = Host::Sandbox(Sandbox {
216        dir: dir.to_path_buf(),
217        stdin: Some(stdin.to_vec()),
218        stdout: stdout.clone(),
219        budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
220        reads: reads.clone(),
221    });
222    let mut err = Vec::new();
223    let code = main(
224        name.as_bytes(),
225        args,
226        env,
227        &mut host,
228        Cores::SANDBOX,
229        Some(Limits::SANDBOX),
230        &mut err,
231    );
232    drop(host);
233    let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
234    let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
235    (code, out, err, reads)
236}
237
238/// zstdcli.c: main, below the process boundary. Returns the exit code.
239fn main(
240    argv0: &[u8],
241    args: &[OsString],
242    env: &dyn Fn(&str) -> Option<OsString>,
243    host: &mut Host,
244    cores: Cores,
245    limits: Option<Limits>,
246    err: &mut dyn Write,
247) -> i32 {
248    let program = String::from_utf8_lossy(cli::last_name_from_path(argv0)).into_owned();
249    let mut out = host.out_writer(None);
250    let stop = match cli::parse(&program, env, args, cores, &mut out, err) {
251        Ok(mut cmd) => {
252            cmd.limits = limits;
253            execute(&mut cmd, host, &mut out, err)
254        }
255        Err(stop) => stop,
256    };
257    let _ = out.flush();
258    if stop.clean && stop.pause {
259        wait_enter(host, err);
260    }
261    stop.code
262}
263
264/// zstdcli.c: waitEnter, for `-p`.
265fn wait_enter(host: &mut Host, err: &mut dyn Write) {
266    printf(err, m::PRESS_ENTER, &[]);
267    let mut byte = [0u8; 1];
268    let _ = host.stdin().read(&mut byte);
269}
270
271/// UTIL_isLink and UTIL_isFIFO: whether `name` is a symbolic link (lstat)
272/// that does not lead to a FIFO (stat). Never on Windows, as util.c.
273fn is_link_not_fifo(host: &Host, name: &[u8]) -> bool {
274    util::is_link(host, name) && !util::is_fifo(host, name)
275}
276
277/// UTIL_getFileSize: a regular file's size (following links), else
278/// unknown.
279fn file_size(host: &Host, name: &[u8]) -> Option<u64> {
280    Some(util::file_size(host, name)).filter(|&s| s != util::FILESIZE_UNKNOWN)
281}
282
283/// An operation's result as `main` ends it: `CLEAN_RETURN` with the
284/// result, or the `EXM_THROW` exit.
285fn finish(r: fileio::Fio<i32>, pause: bool) -> Stop {
286    match r {
287        Ok(code) => Stop {
288            code,
289            clean: true,
290            pause,
291        },
292        Err(fileio::Thrown(code)) => Stop {
293            code,
294            clean: false,
295            pause,
296        },
297    }
298}
299
300/// zstdcli.c: main, from the symbolic-link filter to the operation.
301fn execute(cmd: &mut Command, host: &mut Host, out: &mut dyn Write, err: &mut dyn Write) -> Stop {
302    let pause = cmd.main_pause;
303    let clean = |code| Stop {
304        code,
305        clean: true,
306        pause,
307    };
308    let exit = |code| Stop {
309        code,
310        clean: false,
311        pause,
312    };
313    let mut d = Display::new(cmd.display_level);
314    // g_utilDisplayLevel.
315    cmd.prefs.util_level = cmd.display_level;
316    let util_level = cmd.display_level;
317
318    if !cmd.follow_links {
319        let total = cmd.files.len();
320        let mut kept = Vec::with_capacity(total);
321        for name in std::mem::take(&mut cmd.files) {
322            if is_link_not_fifo(host, &name) {
323                d.at(2, err, m::SYMLINK_IGNORED, &[&name]);
324            } else {
325                kept.push(name);
326            }
327        }
328        cmd.files = kept;
329        if cmd.files.is_empty() && total > 0 {
330            return clean(1);
331        }
332    }
333    for list in std::mem::take(&mut cmd.filelists) {
334        match util::names_from_file(host, &list, util_level, err) {
335            Some(names) => cmd.files.extend(names),
336            None => {
337                d.at(1, err, m::ERROR_READING, &[&list]);
338                return clean(1);
339            }
340        }
341    }
342    cmd.nb_input_files = cmd.files.len();
343    if cmd.recursive {
344        let names = std::mem::take(&mut cmd.files);
345        cmd.files = util::expand_names(host, names, cmd.follow_links, util_level, err);
346    }
347
348    match cmd.op {
349        Operation::List => return clean(list::list_files(cmd, host, out, err)),
350        Operation::Train => return finish(dibio::train_files(cmd, host, err), pause),
351        Operation::Test => {
352            cmd.prefs.test_mode = true;
353            cmd.out_file = Some(cli::NULMARK.to_vec());
354            cmd.prefs.remove_src = false;
355        }
356        Operation::Compress | Operation::Decompress => {}
357    }
358
359    if cmd.files.is_empty() {
360        if cmd.nb_input_files > 0 {
361            d.at(1, err, m::NO_INPUT_LEFT, &[]);
362            return clean(0);
363        }
364        cmd.files.push(STDINMARK.to_vec());
365    }
366    if cmd.files.len() == 1 && cmd.files[0] == STDINMARK && cmd.out_file.is_none() {
367        cmd.out_file = Some(STDOUTMARK.to_vec());
368    }
369
370    let has_stdin = cmd.files.iter().any(|f| f == STDINMARK);
371    if !cmd.force_stdin && has_stdin && (cmd.prefs.fake_stdin_console || host.stdin_is_tty()) {
372        d.at(1, err, m::STDIN_CONSOLE, &[]);
373        return clean(1);
374    }
375    if cmd.out_file.as_deref().is_none_or(|o| o == STDOUTMARK)
376        && (cmd.prefs.fake_stdout_console || host.stdout_is_tty())
377        && has_stdin
378        && !cmd.force_stdout
379        && cmd.op != Operation::Decompress
380    {
381        d.at(1, err, m::STDOUT_CONSOLE, &[]);
382        return clean(1);
383    }
384
385    let max_level = if cmd.ultra {
386        zstd_opts::MAX_CLEVEL
387    } else {
388        cli::CLEVEL_MAX
389    };
390    if cmd.level > max_level {
391        d.at(
392            2,
393            err,
394            m::LEVEL_REDUCED,
395            &[max_level.to_string().as_bytes()],
396        );
397        cmd.level = max_level;
398    }
399
400    if cmd.show_default_cparams && cmd.op == Operation::Decompress {
401        d.at(1, err, m::CPARAMS_DECOMPRESS, &[]);
402        return clean(1);
403    }
404    if cmd.dict_file.is_some() && cmd.patch_from.is_some() {
405        d.at(1, err, m::DICT_AND_PATCH, &[]);
406        return clean(1);
407    }
408    if cmd.patch_from.is_some() && cmd.files.len() > 1 {
409        d.at(1, err, m::PATCH_MULTIPLE, &[]);
410        return clean(1);
411    }
412
413    let has_stdout = cmd.out_file.as_deref() == Some(STDOUTMARK);
414    if has_stdout && d.level == 2 {
415        d.level = 1;
416    }
417    cmd.display_level = d.level;
418    let stderr_console = cmd.prefs.fake_stderr_console || host.stderr_is_tty();
419    if !stderr_console && cmd.prefs.progress != Progress::Always {
420        cmd.prefs.progress = Progress::Never;
421    }
422    if has_stdout && cmd.prefs.remove_src {
423        d.at(3, err, m::SRC_KEPT_STDOUT, &[]);
424        cmd.prefs.remove_src = false;
425    }
426    cmd.prefs.has_stdout_output = has_stdout;
427    cmd.prefs.nb_files_total = cmd.files.len();
428    cmd.prefs.has_stdin_input = has_stdin;
429    cmd.prefs.patch_from_mode = cmd.patch_from.is_some();
430    if cmd.prefs.memory_limit == 0 {
431        let wlog = cmd.cparam(CParamKey::WindowLog);
432        cmd.prefs.memory_limit = if wlog == 0 {
433            1 << cli::DEFAULT_MAX_WINDOW_LOG
434        } else {
435            1u32 << (wlog & 31)
436        };
437    }
438    if cmd.patch_from.is_some() {
439        cmd.dict_file = cmd.patch_from.clone();
440    }
441
442    if cmd.op != Operation::Compress {
443        if let Some(l) = cmd.limits {
444            // R9: the decoder's window log at most 24.
445            cmd.prefs.memory_limit = cmd.prefs.memory_limit.min(1 << l.window_log_max);
446        }
447        return finish(fileio::decompress_files(cmd, host, out, err), pause);
448    }
449
450    // FIO_setCompressionType .. FIO_setSparseWrite, with their messages.
451    if let Some(l) = cmd.limits
452        && cmd.prefs.workers > l.workers
453    {
454        // R9: at most 2 workers in the sandbox.
455        cmd.prefs.workers = l.workers;
456    }
457    let workers = cmd.prefs.workers;
458    // FIO_setBlockSize takes `(int)blockSize`.
459    if cmd.prefs.job_size as i32 != 0 && workers == 0 {
460        d.at(2, err, m::BLOCK_SIZE_USELESS, &[]);
461    }
462    if let Some(ovlog) = cmd.prefs.overlap_log
463        && ovlog != 0
464        && workers == 0
465    {
466        d.at(2, err, m::OVERLAP_LOG_USELESS, &[]);
467    }
468    if cmd.prefs.adapt && workers == 0 {
469        return exit(exm_throw(&d, err, 1, m::ADAPT_SINGLE_THREAD.as_bytes()));
470    }
471    if cmd.prefs.rsyncable && workers == 0 {
472        return exit(exm_throw(&d, err, 1, m::RSYNCABLE_SINGLE_THREAD.as_bytes()));
473    }
474    cmd.prefs.sparse = 0;
475    if cmd.prefs.adapt_min > cmd.level {
476        cmd.level = cmd.prefs.adapt_min;
477    }
478    if cmd.prefs.adapt_max < cmd.level {
479        cmd.level = cmd.prefs.adapt_max;
480    }
481
482    if cmd.show_default_cparams || d.level >= 4 {
483        let dict_size = cmd
484            .dict_file
485            .as_deref()
486            .map_or(0, |f| file_size(host, f).unwrap_or(u64::MAX));
487        for name in &cmd.files {
488            let size = file_size(host, name);
489            if cmd.show_default_cparams {
490                cli::print_default_cparams(err, name, size, dict_size, cmd.level);
491            }
492            if d.level >= 4 {
493                cli::print_actual_cparams(err, size, dict_size, cmd.level, cmd);
494            }
495        }
496    }
497    if d.level >= 4 {
498        display_compression_parameters(cmd, err);
499    }
500    finish(fileio::compress_files(cmd, host, out, err), pause)
501}
502
503/// fileio.c: FIO_displayCompressionParameters (display level 4).
504fn display_compression_parameters(cmd: &Command, err: &mut dyn Write) {
505    let p = &cmd.prefs;
506    let n = |v: i64| v.to_string();
507    printf(
508        err,
509        m::DCP_FORMAT,
510        &[m::FORMAT_OPTIONS[p.ctype.index()].as_bytes()],
511    );
512    printf(err, m::DCP_SPARSE[p.sparse.clamp(0, 2) as usize], &[]);
513    if !p.dict_id {
514        printf(err, m::DCP_NO_DICT_ID, &[]);
515    }
516    printf(err, m::DCP_CHECKSUM[p.checksum.clamp(0, 2) as usize], &[]);
517    printf(
518        err,
519        m::DCP_BLOCK_SIZE,
520        &[n(i64::from(p.job_size as i32)).as_bytes()],
521    );
522    if p.adapt {
523        printf(
524            err,
525            m::DCP_ADAPT,
526            &[
527                n(p.adapt_min.into()).as_bytes(),
528                n(p.adapt_max.into()).as_bytes(),
529            ],
530        );
531    }
532    printf(
533        err,
534        m::DCP_ROW_MATCH_FINDER[p.row_match_finder.value() as usize],
535        &[],
536    );
537    if p.rsyncable {
538        printf(err, m::DCP_RSYNCABLE, &[]);
539    }
540    if p.stream_size != 0 {
541        printf(
542            err,
543            m::DCP_STREAM_SIZE,
544            &[n((p.stream_size as u32).into()).as_bytes()],
545        );
546    }
547    if p.size_hint != 0 {
548        printf(err, m::DCP_SIZE_HINT, &[n(p.size_hint.into()).as_bytes()]);
549    }
550    if p.target_cblock_size != 0 {
551        printf(
552            err,
553            m::DCP_TARGET_CBLOCK,
554            &[n((p.target_cblock_size as u32).into()).as_bytes()],
555        );
556    }
557    printf(
558        err,
559        m::DCP_LITERALS[p.literal_compression.value() as usize],
560        &[],
561    );
562    let memory = if p.memory_limit != 0 {
563        p.memory_limit
564    } else {
565        128 << 20
566    };
567    printf(err, m::DCP_MEMORY, &[n(memory.into()).as_bytes()]);
568    printf(err, m::DCP_THREADS, &[n(p.workers.into()).as_bytes()]);
569    if p.exclude_compressed {
570        printf(err, m::DCP_EXCLUDE, &[]);
571    }
572    printf(
573        err,
574        m::DCP_CONTENT_SIZE,
575        &[if p.content_size { "" } else { m::DCP_NO }.as_bytes()],
576    );
577}
578
579#[cfg(test)]
580mod tests;