Skip to main content

ryg_rans_rs_cli/
lib.rs

1//! # ryg-rans-rs-cli — Production-grade rANS compression CLI
2//!
3//! This library implements the `ryg-rans` command-line tool for encoding,
4//! decoding, inspecting, verifying, comparing, and benchmarking rANS
5//! compressed data using the versioned RYGRANS container format.
6//!
7//! ## Safety
8//!
9//! This crate uses `#![forbid(unsafe_code)]`.  All SIMD acceleration is
10//! accessed through safe facade APIs that perform runtime feature detection.
11
12#![forbid(unsafe_code)]
13
14pub mod container;
15pub mod error;
16pub mod exit;
17pub mod limits;
18
19use clap::{Arg, Command};
20use error::AppError;
21use error::{FormatError, InternalInvariantError, IoError};
22use std::io::{Read, Write};
23
24/// Top-level CLI entry point.  Returns the exit code.
25pub fn run(
26    args: impl IntoIterator<Item = std::ffi::OsString>,
27    _stdin: &mut dyn Read,
28    _stdout: &mut dyn Write,
29    _stderr: &mut dyn Write,
30) -> i32 {
31    match cli_entry(args) {
32        Ok(()) => exit::codes::SUCCESS,
33        Err(e) => {
34            let code = exit::error_to_exit_code(&e);
35            let _ = writeln!(_stderr, "error: {}", e);
36            code
37        }
38    }
39}
40
41fn cli_entry<I>(args: I) -> Result<(), error::AppError>
42where
43    I: IntoIterator<Item = std::ffi::OsString>,
44{
45    let app = build_cli();
46    let matches = app.try_get_matches_from(args).map_err(|e| {
47        AppError::Io(error::IoError {
48            path: None,
49            detail: e.to_string(),
50        })
51    })?;
52
53    match matches.subcommand() {
54        Some(("encode", _m)) => Err(error::AppError::InternalInvariant(
55            error::InternalInvariantError {
56                detail: "encode not yet implemented".into(),
57            },
58        )),
59        Some(("decode", _m)) => Err(error::AppError::InternalInvariant(
60            error::InternalInvariantError {
61                detail: "decode not yet implemented".into(),
62            },
63        )),
64        Some(("inspect", _m)) => Err(error::AppError::InternalInvariant(
65            error::InternalInvariantError {
66                detail: "inspect not yet implemented".into(),
67            },
68        )),
69        Some(("verify", _m)) => Err(error::AppError::InternalInvariant(
70            error::InternalInvariantError {
71                detail: "verify not yet implemented".into(),
72            },
73        )),
74        Some(("model", model_matches)) => {
75            match model_matches.subcommand() {
76                Some(("build", _mm)) => Err(error::AppError::InternalInvariant(
77                    error::InternalInvariantError {
78                        detail: "model build not yet implemented".into(),
79                    },
80                )),
81                Some(("inspect", _mm)) => Err(error::AppError::InternalInvariant(
82                    error::InternalInvariantError {
83                        detail: "model inspect not yet implemented".into(),
84                    },
85                )),
86                Some(("validate", _mm)) => Err(error::AppError::InternalInvariant(
87                    error::InternalInvariantError {
88                        detail: "model validate not yet implemented".into(),
89                    },
90                )),
91                Some(("compare", _mm)) => Err(error::AppError::InternalInvariant(
92                    error::InternalInvariantError {
93                        detail: "model compare not yet implemented".into(),
94                    },
95                )),
96                _ => {
97                    // Print model help
98                    let _ = print_help();
99                    Ok(())
100                }
101            }
102        }
103        Some(("trace", _m)) => Err(error::AppError::InternalInvariant(
104            error::InternalInvariantError {
105                detail: "trace not yet implemented".into(),
106            },
107        )),
108        Some(("compare", cmp_matches)) => match cmp_matches.subcommand() {
109            Some(("arithmetic", _mm)) => Err(error::AppError::InternalInvariant(
110                error::InternalInvariantError {
111                    detail: "compare arithmetic not yet implemented".into(),
112                },
113            )),
114            Some(("backends", _mm)) => Err(error::AppError::InternalInvariant(
115                error::InternalInvariantError {
116                    detail: "compare backends not yet implemented".into(),
117                },
118            )),
119            Some(("files", _mm)) => Err(error::AppError::InternalInvariant(
120                error::InternalInvariantError {
121                    detail: "compare files not yet implemented".into(),
122                },
123            )),
124            _ => Ok(()),
125        },
126        Some(("bench", _m)) => Err(error::AppError::InternalInvariant(
127            error::InternalInvariantError {
128                detail: "bench not yet implemented".into(),
129            },
130        )),
131        Some(("capabilities", _m)) => print_capabilities(),
132        Some(("completions", m)) => {
133            let shell = m
134                .get_one::<String>("shell")
135                .map(|s| s.as_str())
136                .unwrap_or("bash");
137            let mut app = build_cli();
138            let mut stdout = std::io::stdout();
139            completions(shell, &mut app, &mut stdout)?;
140            Ok(())
141        }
142        _ => {
143            // Print help
144            let _ = print_help();
145            Ok(())
146        }
147    }
148}
149
150fn print_help() -> Result<(), error::AppError> {
151    build_cli().print_long_help().map_err(|e| {
152        error::AppError::Io(error::IoError {
153            path: None,
154            detail: e.to_string(),
155        })
156    })
157}
158
159fn print_capabilities() -> Result<(), error::AppError> {
160    let json = serde_json::json!({
161        "schema_version": 1,
162        "command": "capabilities",
163        "success": true,
164        "tool_version": env!("CARGO_PKG_VERSION"),
165        "container_versions": ["1.0"],
166        "supported_codecs": [
167            {"id": 1, "name": "BYTE_SINGLE"},
168            {"id": 2, "name": "BYTE_INTERLEAVED2"},
169            {"id": 3, "name": "R64_SINGLE"},
170            {"id": 4, "name": "R64_INTERLEAVED2"},
171            {"id": 5, "name": "WORD_SINGLE"},
172            {"id": 6, "name": "WORD_INTERLEAVED2"},
173            {"id": 7, "name": "WORD_INTERLEAVED8"},
174            {"id": 8, "name": "WORD_INTERLEAVED16"},
175            {"id": 9, "name": "ALIAS_SINGLE"},
176            {"id": 10, "name": "ALIAS_INTERLEAVED2"},
177        ],
178        "supported_backends": [
179            {"name": "scalar-8way", "available": true},
180            {"name": "sse41-8way", "available": cfg!(target_feature = "sse4.1")},
181            {"name": "avx512vl-8way", "available": cfg!(all(
182                target_feature = "avx512f",
183                target_feature = "avx512vl",
184                target_feature = "avx512bw"
185            ))},
186            {"name": "scalar-16way", "available": true},
187            {"name": "avx512-16way", "available": cfg!(all(
188                target_feature = "avx512f",
189                target_feature = "avx512bw"
190            ))},
191        ],
192        "default_codec": "byte-interleaved2",
193        "default_block_size": 1048576,
194        "default_scale_bits": 12,
195        "msrv": "1.85",
196    });
197    println!(
198        "{}",
199        serde_json::to_string_pretty(&json).map_err(|e| {
200            error::AppError::Io(error::IoError {
201                path: None,
202                detail: e.to_string(),
203            })
204        })?
205    );
206    Ok(())
207}
208
209fn build_cli() -> Command {
210    Command::new("ryg-rans")
211        .version(env!("CARGO_PKG_VERSION"))
212        .about("rANS entropy coding tool — encode, decode, inspect, verify, compare, benchmark")
213        .subcommand(
214            Command::new("encode")
215                .about("Encode input into a versioned .rygr container")
216                .arg(
217                    Arg::new("input")
218                        .short('i')
219                        .long("input")
220                        .value_name("PATH")
221                        .help("Input file path (use '-' for stdin)")
222                        .default_value("-"),
223                )
224                .arg(
225                    Arg::new("output")
226                        .short('o')
227                        .long("output")
228                        .value_name("PATH")
229                        .help("Output .rygr file path (use '-' for stdout)")
230                        .default_value("-"),
231                )
232                .arg(
233                    Arg::new("codec")
234                        .long("codec")
235                        .value_name("CODEC")
236                        .help("Codec format")
237                        .default_value("byte-interleaved2"),
238                )
239                .arg(
240                    Arg::new("model")
241                        .long("model")
242                        .value_name("MODE")
243                        .help("Model mode: per-block, global, uniform, external")
244                        .default_value("per-block"),
245                )
246                .arg(
247                    Arg::new("scale-bits")
248                        .long("scale-bits")
249                        .value_name("N")
250                        .help("Frequency scale bits")
251                        .default_value("12"),
252                )
253                .arg(
254                    Arg::new("block-size")
255                        .long("block-size")
256                        .value_name("SIZE")
257                        .help("Block size (e.g. 1MiB)")
258                        .default_value("1MiB"),
259                )
260                .arg(
261                    Arg::new("arithmetic")
262                        .long("arithmetic")
263                        .value_name("PATH")
264                        .help("Arithmetic: division, reciprocal, auto")
265                        .default_value("auto"),
266                )
267                .arg(
268                    Arg::new("always-compress")
269                        .long("always-compress")
270                        .help("Always use RANS blocks, never RAW")
271                        .action(clap::ArgAction::SetTrue),
272                )
273                .arg(
274                    Arg::new("force")
275                        .long("force")
276                        .help("Overwrite existing output file")
277                        .action(clap::ArgAction::SetTrue),
278                )
279                .arg(
280                    Arg::new("force-tty")
281                        .long("force-tty")
282                        .help("Allow binary output to terminal")
283                        .action(clap::ArgAction::SetTrue),
284                ),
285        )
286        .subcommand(
287            Command::new("decode")
288                .about("Strictly decode and verify a .rygr container")
289                .arg(
290                    Arg::new("input")
291                        .short('i')
292                        .long("input")
293                        .value_name("PATH")
294                        .help("Input .rygr file path (use '-' for stdin)")
295                        .default_value("-"),
296                )
297                .arg(
298                    Arg::new("output")
299                        .short('o')
300                        .long("output")
301                        .value_name("PATH")
302                        .help("Output file path (use '-' for stdout)")
303                        .default_value("-"),
304                )
305                .arg(
306                    Arg::new("backend")
307                        .long("backend")
308                        .value_name("BACKEND")
309                        .help("Decode backend: auto, scalar, sse41, avx512vl, avx512")
310                        .default_value("auto"),
311                )
312                .arg(
313                    Arg::new("force")
314                        .long("force")
315                        .help("Overwrite existing output file")
316                        .action(clap::ArgAction::SetTrue),
317                )
318                .arg(
319                    Arg::new("force-tty")
320                        .long("force-tty")
321                        .help("Allow binary output to terminal")
322                        .action(clap::ArgAction::SetTrue),
323                ),
324        )
325        .subcommand(
326            Command::new("inspect")
327                .about("Inspect container structure and metadata")
328                .arg(
329                    Arg::new("input")
330                        .short('i')
331                        .long("input")
332                        .value_name("PATH")
333                        .help("Input .rygr file path")
334                        .default_value("-"),
335                )
336                .arg(
337                    Arg::new("output-format")
338                        .long("output-format")
339                        .value_name("FMT")
340                        .help("Output format: human, json")
341                        .default_value("human"),
342                )
343                .arg(
344                    Arg::new("blocks")
345                        .long("blocks")
346                        .help("Show block details")
347                        .action(clap::ArgAction::SetTrue),
348                )
349                .arg(
350                    Arg::new("deep")
351                        .long("deep")
352                        .help("Verify payload and decoded hashes")
353                        .action(clap::ArgAction::SetTrue),
354                ),
355        )
356        .subcommand(
357            Command::new("verify")
358                .about("Fully verify without writing decoded output")
359                .arg(
360                    Arg::new("input")
361                        .short('i')
362                        .long("input")
363                        .value_name("PATH")
364                        .help("Input .rygr file path")
365                        .default_value("-"),
366                )
367                .arg(
368                    Arg::new("backend")
369                        .long("backend")
370                        .value_name("BACKEND")
371                        .help("Backend: auto, scalar, all-available")
372                        .default_value("auto"),
373                )
374                .arg(
375                    Arg::new("output-format")
376                        .long("output-format")
377                        .value_name("FMT")
378                        .help("Output format: human, json")
379                        .default_value("human"),
380                ),
381        )
382        .subcommand(
383            Command::new("model")
384                .about("Build, inspect, validate, and compare models")
385                .subcommand(
386                    Command::new("build")
387                        .about("Build a deterministic normalized model from input")
388                        .arg(
389                            Arg::new("input")
390                                .short('i')
391                                .long("input")
392                                .value_name("PATH")
393                                .help("Input file path")
394                                .default_value("-"),
395                        )
396                        .arg(
397                            Arg::new("scale-bits")
398                                .long("scale-bits")
399                                .value_name("N")
400                                .help("Scale bits")
401                                .default_value("12"),
402                        )
403                        .arg(
404                            Arg::new("output")
405                                .short('o')
406                                .long("output")
407                                .value_name("PATH")
408                                .help("Output path")
409                                .default_value("-"),
410                        )
411                        .arg(
412                            Arg::new("output-format")
413                                .long("output-format")
414                                .value_name("FMT")
415                                .help("Output format: binary, json")
416                                .default_value("json"),
417                        ),
418                )
419                .subcommand(Command::new("inspect").about("Display model contents"))
420                .subcommand(Command::new("validate").about("Validate a model file"))
421                .subcommand(Command::new("compare").about("Compare two models")),
422        )
423        .subcommand(
424            Command::new("trace")
425                .about("Trace symbol/state transitions")
426                .arg(
427                    Arg::new("input")
428                        .short('i')
429                        .long("input")
430                        .value_name("PATH")
431                        .help("Input .rygr file path")
432                        .default_value("-"),
433                )
434                .arg(
435                    Arg::new("block")
436                        .long("block")
437                        .value_name("INDEX")
438                        .help("Block index to trace")
439                        .default_value("0"),
440                )
441                .arg(
442                    Arg::new("max-symbols")
443                        .long("max-symbols")
444                        .value_name("N")
445                        .help("Maximum symbols to trace")
446                        .default_value("256"),
447                )
448                .arg(
449                    Arg::new("output-format")
450                        .long("output-format")
451                        .value_name("FMT")
452                        .help("Output format: text, jsonl")
453                        .default_value("text"),
454                ),
455        )
456        .subcommand(
457            Command::new("compare")
458                .about("Compare arithmetic paths, backends, files, or oracle")
459                .subcommand(
460                    Command::new("arithmetic").about("Compare division vs reciprocal encoding"),
461                )
462                .subcommand(Command::new("backends").about("Compare decode backends"))
463                .subcommand(Command::new("files").about("Compare two .rygr containers")),
464        )
465        .subcommand(
466            Command::new("bench")
467                .about("Benchmark production Rust codec backends")
468                .arg(
469                    Arg::new("codec")
470                        .long("codec")
471                        .value_name("CODEC")
472                        .help("Codec to benchmark")
473                        .default_value("byte-interleaved2"),
474                )
475                .arg(
476                    Arg::new("size")
477                        .long("size")
478                        .value_name("SIZE")
479                        .help("Block size")
480                        .default_value("1MiB"),
481                )
482                .arg(
483                    Arg::new("samples")
484                        .long("samples")
485                        .value_name("N")
486                        .help("Number of samples")
487                        .default_value("50"),
488                )
489                .arg(
490                    Arg::new("output-format")
491                        .long("output-format")
492                        .value_name("FMT")
493                        .help("Output format: human, json")
494                        .default_value("human"),
495                ),
496        )
497        .subcommand(
498            Command::new("capabilities")
499                .about("Show compiled and runtime-supported codecs and backends"),
500        )
501        .subcommand(
502            Command::new("completions")
503                .about("Generate shell completion scripts")
504                .arg(
505                    Arg::new("shell")
506                        .value_name("SHELL")
507                        .help("Shell type: bash, fish, zsh, powershell, elvish")
508                        .default_value("bash"),
509                ),
510        )
511}
512
513/// Generate shell completions.
514fn completions(shell: &str, app: &mut Command, buf: &mut dyn Write) -> Result<(), error::AppError> {
515    use clap_complete::{Generator, generate};
516    match shell {
517        "bash" => generate(clap_complete::shells::Bash, app, "ryg-rans", buf),
518        "fish" => generate(clap_complete::shells::Fish, app, "ryg-rans", buf),
519        "zsh" => generate(clap_complete::shells::Zsh, app, "ryg-rans", buf),
520        "powershell" => generate(clap_complete::shells::PowerShell, app, "ryg-rans", buf),
521        "elvish" => generate(clap_complete::shells::Elvish, app, "ryg-rans", buf),
522        _ => {
523            return Err(error::AppError::Io(error::IoError {
524                path: None,
525                detail: format!("unsupported shell: {}", shell),
526            }));
527        }
528    }
529    Ok(())
530}