1use std::path::PathBuf;
4
5use clap::{Parser, Subcommand, ValueEnum};
6use clap_complete::Shell;
7
8#[derive(Parser, Debug)]
10#[command(
11 name = "shadowforge",
12 version,
13 about = "Quantum-resistant steganography toolkit",
14 long_about = None,
15 propagate_version = true
16)]
17pub struct Cli {
18 #[command(subcommand)]
20 pub command: Commands,
21}
22
23#[derive(Subcommand, Debug)]
25pub enum Commands {
26 Version,
28
29 Keygen(KeygenArgs),
31
32 Embed(EmbedArgs),
34
35 Extract(ExtractArgs),
37
38 #[command(name = "embed-distributed")]
40 EmbedDistributed(EmbedDistributedArgs),
41
42 #[command(name = "extract-distributed")]
44 ExtractDistributed(ExtractDistributedArgs),
45
46 #[command(name = "analyse")]
48 Analyse(AnalyseArgs),
49
50 Archive(ArchiveArgs),
52
53 Scrub(ScrubArgs),
55
56 #[command(name = "dead-drop")]
58 DeadDrop(DeadDropArgs),
59
60 #[command(name = "time-lock")]
62 TimeLock(TimeLockArgs),
63
64 Watermark(WatermarkArgs),
66
67 Corpus(CorpusArgs),
69
70 #[command(hide = true)]
72 Panic(PanicArgs),
73
74 Completions(CompletionsArgs),
76}
77
78#[derive(Debug, Clone, Copy, ValueEnum)]
82pub enum Algorithm {
83 Kyber1024,
85 Dilithium3,
87}
88
89#[derive(Debug, Clone, Copy, ValueEnum)]
91pub enum Technique {
92 Lsb,
94 Dct,
96 Palette,
98 #[value(name = "lsb-audio")]
100 LsbAudio,
101 Phase,
103 Echo,
105 #[value(name = "zero-width")]
107 ZeroWidth,
108 #[value(name = "pdf-stream")]
110 PdfStream,
111 #[value(name = "pdf-meta")]
113 PdfMeta,
114 Corpus,
116}
117
118#[derive(Debug, Clone, Copy, ValueEnum)]
120pub enum Profile {
121 Standard,
123 Adaptive,
125 Survivable,
127}
128
129#[derive(Debug, Clone, Copy, ValueEnum)]
131pub enum Platform {
132 Instagram,
134 Twitter,
136 Whatsapp,
138 Telegram,
140 Imgur,
142}
143
144#[derive(Debug, Clone, Copy, ValueEnum)]
146pub enum ArchiveFormat {
147 Zip,
149 Tar,
151 #[value(name = "tar-gz")]
153 TarGz,
154}
155
156#[derive(Parser, Debug)]
160pub struct KeygenArgs {
161 #[arg(long, value_enum)]
163 pub algorithm: Algorithm,
164 #[arg(long)]
166 pub output: PathBuf,
167}
168
169#[derive(Parser, Debug)]
171pub struct EmbedArgs {
172 #[arg(long)]
174 pub input: PathBuf,
175 #[arg(long)]
177 pub cover: Option<PathBuf>,
178 #[arg(long)]
180 pub output: PathBuf,
181 #[arg(long, value_enum)]
183 pub technique: Technique,
184 #[arg(long, value_enum, default_value = "standard")]
186 pub profile: Profile,
187 #[arg(long, value_enum)]
189 pub platform: Option<Platform>,
190 #[arg(long)]
192 pub amnesia: bool,
193 #[arg(long)]
195 pub scrub_style: bool,
196 #[arg(long)]
198 pub deniable: bool,
199 #[arg(long)]
201 pub decoy_payload: Option<PathBuf>,
202 #[arg(long)]
204 pub decoy_key: Option<PathBuf>,
205 #[arg(long)]
207 pub key: Option<PathBuf>,
208}
209
210#[derive(Parser, Debug)]
212pub struct ExtractArgs {
213 #[arg(long)]
215 pub input: PathBuf,
216 #[arg(long)]
218 pub output: PathBuf,
219 #[arg(long, value_enum)]
221 pub technique: Technique,
222 #[arg(long)]
224 pub key: Option<PathBuf>,
225 #[arg(long)]
227 pub amnesia: bool,
228}
229
230#[derive(Parser, Debug)]
232pub struct EmbedDistributedArgs {
233 #[arg(long)]
235 pub input: PathBuf,
236 #[arg(long)]
238 pub covers: String,
239 #[arg(long, default_value = "3")]
241 pub data_shards: u8,
242 #[arg(long, default_value = "2")]
244 pub parity_shards: u8,
245 #[arg(long)]
247 pub output_archive: PathBuf,
248 #[arg(long, value_enum)]
250 pub technique: Technique,
251 #[arg(long, value_enum, default_value = "standard")]
253 pub profile: Profile,
254 #[arg(long, value_enum)]
256 pub platform: Option<Platform>,
257 #[arg(long)]
259 pub canary: bool,
260 #[arg(long)]
262 pub geo_manifest: Option<PathBuf>,
263 #[arg(long)]
266 pub hmac_key: Option<PathBuf>,
267}
268
269#[derive(Parser, Debug)]
271pub struct ExtractDistributedArgs {
272 #[arg(long)]
274 pub input_archive: PathBuf,
275 #[arg(long)]
277 pub output: PathBuf,
278 #[arg(long, value_enum)]
280 pub technique: Technique,
281 #[arg(long, default_value = "3")]
283 pub data_shards: u8,
284 #[arg(long, default_value = "2")]
286 pub parity_shards: u8,
287 #[arg(long)]
289 pub hmac_key: Option<PathBuf>,
290}
291
292#[derive(Parser, Debug)]
294pub struct AnalyseArgs {
295 #[arg(long)]
297 pub cover: PathBuf,
298 #[arg(long, value_enum)]
300 pub technique: Technique,
301 #[arg(long)]
303 pub json: bool,
304}
305
306#[derive(Parser, Debug)]
308pub struct ArchiveArgs {
309 #[command(subcommand)]
311 pub subcmd: ArchiveSubcommand,
312}
313
314#[derive(Subcommand, Debug)]
316pub enum ArchiveSubcommand {
317 Pack {
319 #[arg(long, num_args = 1..)]
321 files: Vec<PathBuf>,
322 #[arg(long, value_enum)]
324 format: ArchiveFormat,
325 #[arg(long)]
327 output: PathBuf,
328 },
329 Unpack {
331 #[arg(long)]
333 input: PathBuf,
334 #[arg(long, value_enum)]
336 format: ArchiveFormat,
337 #[arg(long)]
339 output_dir: PathBuf,
340 },
341}
342
343#[derive(Parser, Debug)]
345pub struct ScrubArgs {
346 #[arg(long)]
348 pub input: PathBuf,
349 #[arg(long)]
351 pub output: PathBuf,
352 #[arg(long, default_value = "15")]
354 pub avg_sentence_len: u32,
355 #[arg(long, default_value = "1000")]
357 pub vocab_size: usize,
358}
359
360#[derive(Parser, Debug)]
362pub struct DeadDropArgs {
363 #[arg(long)]
365 pub cover: PathBuf,
366 #[arg(long)]
368 pub input: PathBuf,
369 #[arg(long, value_enum)]
371 pub platform: Platform,
372 #[arg(long)]
374 pub output: PathBuf,
375 #[arg(long, value_enum, default_value = "lsb")]
377 pub technique: Technique,
378}
379
380#[derive(Parser, Debug)]
382pub struct TimeLockArgs {
383 #[command(subcommand)]
385 pub subcmd: TimeLockSubcommand,
386}
387
388#[derive(Subcommand, Debug)]
390pub enum TimeLockSubcommand {
391 Lock {
393 #[arg(long)]
395 input: PathBuf,
396 #[arg(long)]
398 unlock_at: String,
399 #[arg(long)]
401 output_puzzle: PathBuf,
402 },
403 Unlock {
405 #[arg(long)]
407 puzzle: PathBuf,
408 #[arg(long)]
410 output: PathBuf,
411 },
412 TryUnlock {
414 #[arg(long)]
416 puzzle: PathBuf,
417 },
418}
419
420#[derive(Parser, Debug)]
422pub struct WatermarkArgs {
423 #[command(subcommand)]
425 pub subcmd: WatermarkSubcommand,
426}
427
428#[derive(Subcommand, Debug)]
430pub enum WatermarkSubcommand {
431 #[command(name = "embed-tripwire")]
433 EmbedTripwire {
434 #[arg(long)]
436 cover: PathBuf,
437 #[arg(long)]
439 output: PathBuf,
440 #[arg(long)]
442 recipient_id: String,
443 },
444 Identify {
446 #[arg(long)]
448 cover: PathBuf,
449 #[arg(long)]
451 tags: PathBuf,
452 },
453}
454
455#[derive(Parser, Debug)]
457pub struct CorpusArgs {
458 #[command(subcommand)]
460 pub subcmd: CorpusSubcommand,
461}
462
463#[derive(Subcommand, Debug)]
465pub enum CorpusSubcommand {
466 Build {
468 #[arg(long)]
470 dir: PathBuf,
471 },
472 Search {
474 #[arg(long)]
476 input: PathBuf,
477 #[arg(long, value_enum)]
479 technique: Technique,
480 #[arg(long, default_value = "5")]
482 top: usize,
483 },
484}
485
486#[derive(Parser, Debug)]
488pub struct PanicArgs {
489 #[arg(long, num_args = 0..)]
491 pub key_paths: Vec<String>,
492}
493
494#[derive(Parser, Debug)]
496pub struct CompletionsArgs {
497 #[arg(value_enum)]
499 pub shell: Shell,
500
501 #[arg(short, long)]
503 pub output: Option<PathBuf>,
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn cli_parse_version() {
512 let cli = Cli::try_parse_from(["shadowforge", "version"]);
513 assert!(cli.is_ok());
514 }
515
516 #[test]
517 fn cli_parse_keygen() {
518 let cli = Cli::try_parse_from([
519 "shadowforge",
520 "keygen",
521 "--algorithm",
522 "kyber1024",
523 "--output",
524 "/tmp/keys",
525 ]);
526 assert!(cli.is_ok());
527 }
528
529 #[test]
530 fn cli_parse_embed() {
531 let cli = Cli::try_parse_from([
532 "shadowforge",
533 "embed",
534 "--input",
535 "payload.bin",
536 "--cover",
537 "cover.png",
538 "--output",
539 "stego.png",
540 "--technique",
541 "lsb",
542 ]);
543 assert!(cli.is_ok());
544 }
545
546 #[test]
547 fn cli_parse_extract() {
548 let cli = Cli::try_parse_from([
549 "shadowforge",
550 "extract",
551 "--input",
552 "stego.png",
553 "--output",
554 "payload.bin",
555 "--technique",
556 "lsb",
557 ]);
558 assert!(cli.is_ok());
559 }
560
561 #[test]
562 fn cli_parse_analyse_json() {
563 let cli = Cli::try_parse_from([
564 "shadowforge",
565 "analyse",
566 "--cover",
567 "cover.png",
568 "--technique",
569 "lsb",
570 "--json",
571 ]);
572 assert!(cli.is_ok());
573 }
574
575 #[test]
576 fn cli_parse_scrub() {
577 let cli = Cli::try_parse_from([
578 "shadowforge",
579 "scrub",
580 "--input",
581 "text.txt",
582 "--output",
583 "clean.txt",
584 ]);
585 assert!(cli.is_ok());
586 }
587
588 #[test]
589 fn cli_parse_time_lock_lock() {
590 let cli = Cli::try_parse_from([
591 "shadowforge",
592 "time-lock",
593 "lock",
594 "--input",
595 "secret.bin",
596 "--unlock-at",
597 "2025-12-31T00:00:00Z",
598 "--output-puzzle",
599 "puzzle.json",
600 ]);
601 assert!(cli.is_ok());
602 }
603
604 #[test]
605 fn cli_parse_completions() {
606 let cli = Cli::try_parse_from(["shadowforge", "completions", "bash"]);
607 assert!(cli.is_ok());
608 }
609
610 #[test]
611 fn cli_parse_embed_distributed() {
612 let cli = Cli::try_parse_from([
613 "shadowforge",
614 "embed-distributed",
615 "--input",
616 "payload.bin",
617 "--covers",
618 "covers/*.png",
619 "--output-archive",
620 "dist.zip",
621 "--technique",
622 "lsb",
623 ]);
624 assert!(cli.is_ok());
625 }
626
627 #[test]
628 fn cli_panic_hidden() {
629 let cli = Cli::try_parse_from(["shadowforge", "panic"]);
631 assert!(cli.is_ok());
632 }
633
634 #[test]
635 fn version_output_contains_semver() {
636 let version = env!("CARGO_PKG_VERSION");
637 assert!(version.contains('.'), "version should be semver");
638 }
639}