1#![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
24pub 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 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 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
513fn 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}