lz4r 1.10.0

LZ4 CLI programs — Rust port of lz4-1.10.0/programs
Documentation
//! CLI initialization and binary-alias detection.
//!
//! When LZ4 is installed under multiple names (`lz4cat`, `unlz4`, `lz4c`)
//! via hard or symbolic links, the program detects which operation mode to
//! enter from `argv[0]` before any flag parsing takes place.
//!
//! [`detect_alias`] encapsulates that detection and returns a [`CliInit`]
//! carrying the pre-parsed defaults.  The argument parser in [`crate::cli`]
//! then layers explicit flags on top of these values.

use crate::cli::arg_utils::{exe_name_match, last_name_from_path};
use crate::cli::constants::{
    set_display_level, set_lz4c_legacy_commands, LZ4CAT, LZ4_LEGACY, UNLZ4,
};
use crate::cli::op_mode::{init_c_level, init_nb_workers, OpMode};
use crate::io::file_io::STDOUT_MARK;
use crate::io::prefs::{Prefs, LZ4IO_BLOCKSIZEID_DEFAULT};

/// Initial CLI state derived from the binary name and environment.
///
/// Built by [`detect_alias`] before argument parsing begins; the argument
/// parser layers explicit flags on top of these defaults.
#[derive(Debug, Clone)]
pub struct CliInit {
    /// Compression preferences initialised to library defaults.
    pub prefs: Prefs,
    /// Initial operation mode — overridden by alias detection before argument parsing.
    pub op_mode: OpMode,
    /// Whether the binary was invoked as `lz4c`, enabling legacy option spellings.
    pub lz4c_legacy: bool,
    /// Whether multiple input files should be concatenated (set by `lz4cat` alias).
    pub multiple_inputs: bool,
    /// Initial compression level from `LZ4_CLEVEL` env var (or default).
    pub c_level: i32,
    /// Initial worker count from `LZ4_NBWORKERS` env var (or default).
    pub nb_workers: usize,
    /// When `true`, output is forced to stdout regardless of the file argument.
    pub force_stdout: bool,
    /// Explicit output filename — set to `Some(STDOUT_MARK)` by the `lz4cat` alias.
    pub output_filename: Option<String>,
    /// Display level override applied by the alias (e.g. `lz4cat` sets level 1).
    /// The caller should apply this via `set_display_level` after `detect_alias` returns.
    pub display_level_override: Option<u32>,
}

/// Detect the operation mode and initial settings from `argv[0]`.
///
/// LZ4 ships as several alias binaries that each select a different default
/// mode before any flags are parsed:
///
/// | Binary name | Effect                                                          |
/// |-------------|----------------------------------------------------------------|
/// | `lz4cat`    | Decompress + pass-through + force stdout + multiple inputs     |
/// | `unlz4`     | Decompress only                                                |
/// | `lz4c`      | Enable legacy option spellings                                 |
///
/// `argv0` may be a full path; the basename is extracted internally.
///
/// As a side effect, the `LZ4C_LEGACY_COMMANDS` atomic and the display-level
/// atomic are updated to match any alias-specific global state, so that code
/// reading those atomics directly sees consistent values.
pub fn detect_alias(argv0: &str) -> CliInit {
    let exe_name = last_name_from_path(argv0);

    let mut prefs = Prefs::default();
    // `Prefs::default()` sets overwrite to true; the normal invocation path starts
    // with it disabled.  The lz4cat branch below re-enables it explicitly.
    prefs.overwrite = false;
    prefs.set_block_size_id(LZ4IO_BLOCKSIZEID_DEFAULT);

    let mut op_mode = OpMode::Auto;
    let mut lz4c_legacy = false;
    let mut multiple_inputs = false;
    let mut force_stdout = false;
    let mut output_filename: Option<String> = None;
    let mut display_level_override: Option<u32> = None;

    // lz4cat: decompress to stdout, accept multiple inputs, quiet verbosity (level 1).
    if exe_name_match(exe_name, LZ4CAT) {
        op_mode = OpMode::Decompress;
        prefs.set_overwrite(true);
        prefs.set_pass_through(true);
        prefs.set_remove_src_file(false);
        force_stdout = true;
        output_filename = Some(STDOUT_MARK.to_owned());
        display_level_override = Some(1);
        multiple_inputs = true;
        // Sync the global display-level atomic so callers that read it directly
        // also observe level 1 without waiting for the caller to apply the override.
        set_display_level(1);
    }

    // unlz4: decompress only; all other settings remain at their defaults.
    if exe_name_match(exe_name, UNLZ4) {
        op_mode = OpMode::Decompress;
    }

    // lz4c: keep default compress mode but enable legacy-style option parsing.
    if exe_name_match(exe_name, LZ4_LEGACY) {
        lz4c_legacy = true;
        // Sync the global atomic so callers that read LZ4C_LEGACY_COMMANDS directly
        // also observe legacy mode.
        set_lz4c_legacy_commands(true);
    }

    CliInit {
        prefs,
        op_mode,
        lz4c_legacy,
        multiple_inputs,
        c_level: init_c_level(),
        nb_workers: init_nb_workers(),
        force_stdout,
        output_filename,
        display_level_override,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cli::constants::{
        lz4c_legacy_commands, set_display_level, set_lz4c_legacy_commands,
    };
    use crate::cli::op_mode::OpMode;

    // Reset shared atomics before each test to prevent inter-test interference.
    fn reset_globals() {
        set_display_level(2);
        set_lz4c_legacy_commands(false);
    }

    // ── lz4cat alias ────────────────────────────────────────────────────────

    #[test]
    fn lz4cat_sets_decompress_mode() {
        reset_globals();
        let init = detect_alias("lz4cat");
        assert_eq!(init.op_mode, OpMode::Decompress);
    }

    #[test]
    fn lz4cat_sets_multiple_inputs() {
        reset_globals();
        let init = detect_alias("lz4cat");
        assert!(init.multiple_inputs);
    }

    #[test]
    fn lz4cat_sets_force_stdout() {
        reset_globals();
        let init = detect_alias("lz4cat");
        assert!(init.force_stdout);
    }

    #[test]
    fn lz4cat_sets_output_filename_to_stdout_mark() {
        reset_globals();
        let init = detect_alias("lz4cat");
        assert_eq!(init.output_filename.as_deref(), Some(STDOUT_MARK));
    }

    #[test]
    fn lz4cat_sets_display_level_override_to_1() {
        reset_globals();
        let init = detect_alias("lz4cat");
        assert_eq!(init.display_level_override, Some(1));
    }

    #[test]
    fn lz4cat_sets_overwrite_and_pass_through() {
        reset_globals();
        let init = detect_alias("lz4cat");
        assert!(init.prefs.overwrite);
        assert!(init.prefs.pass_through);
        assert!(!init.prefs.remove_src_file);
    }

    #[test]
    fn lz4cat_with_path_prefix() {
        // argv[0] may include a path — last_name_from_path should strip it.
        reset_globals();
        let init = detect_alias("/usr/bin/lz4cat");
        assert_eq!(init.op_mode, OpMode::Decompress);
        assert!(init.multiple_inputs);
    }

    #[test]
    fn lz4cat_with_exe_extension() {
        // On Windows argv[0] may have ".exe".
        reset_globals();
        let init = detect_alias("lz4cat.exe");
        assert_eq!(init.op_mode, OpMode::Decompress);
    }

    // ── unlz4 alias ─────────────────────────────────────────────────────────

    #[test]
    fn unlz4_sets_decompress_mode() {
        reset_globals();
        let init = detect_alias("unlz4");
        assert_eq!(init.op_mode, OpMode::Decompress);
    }

    #[test]
    fn unlz4_does_not_set_multiple_inputs() {
        reset_globals();
        let init = detect_alias("unlz4");
        assert!(!init.multiple_inputs);
    }

    #[test]
    fn unlz4_does_not_set_force_stdout() {
        reset_globals();
        let init = detect_alias("unlz4");
        assert!(!init.force_stdout);
    }

    // ── lz4c legacy alias ───────────────────────────────────────────────────

    #[test]
    fn lz4c_sets_lz4c_legacy() {
        reset_globals();
        let init = detect_alias("lz4c");
        assert!(init.lz4c_legacy);
    }

    #[test]
    #[ignore = "global atomic state is racy in parallel test runner; field-level check in lz4c_sets_lz4c_legacy covers the acceptance criterion"]
    fn lz4c_updates_global_atomic() {
        // Verify that detect_alias("lz4c") drives set_lz4c_legacy_commands(true).
        // We read the atomic immediately after our own call to avoid races with
        // other parallel tests that reset globals; we do not reset before the call
        // so we are checking only our write, then we clean up afterwards.
        detect_alias("lz4c");
        let observed = lz4c_legacy_commands();
        // Restore before asserting to minimise window for other tests.
        set_lz4c_legacy_commands(false);
        assert!(
            observed,
            "detect_alias(\"lz4c\") should have set LZ4C_LEGACY_COMMANDS to true"
        );
    }

    #[test]
    fn lz4c_op_mode_is_auto() {
        // lz4c only sets legacy flag, not a specific operation mode.
        reset_globals();
        let init = detect_alias("lz4c");
        assert_eq!(init.op_mode, OpMode::Auto);
    }

    // ── plain lz4 (no alias) ────────────────────────────────────────────────

    #[test]
    fn lz4_returns_defaults() {
        reset_globals();
        let init = detect_alias("lz4");
        assert_eq!(init.op_mode, OpMode::Auto);
        assert!(!init.lz4c_legacy);
        assert!(!init.multiple_inputs);
        assert!(!init.force_stdout);
        assert!(init.output_filename.is_none());
        assert!(init.display_level_override.is_none());
    }

    #[test]
    fn lz4_overwrite_is_false() {
        // C main() sets LZ4IO_setOverwrite(prefs, 0) for the common path.
        reset_globals();
        let init = detect_alias("lz4");
        assert!(!init.prefs.overwrite);
    }

    // ── unrecognised binary name ─────────────────────────────────────────────

    #[test]
    fn unknown_binary_returns_defaults() {
        reset_globals();
        let init = detect_alias("my-lz4-wrapper");
        assert_eq!(init.op_mode, OpMode::Auto);
        assert!(!init.lz4c_legacy);
        assert!(!init.multiple_inputs);
    }
}