use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
const MAX_STDIN_PAYLOAD: u64 = 256 * 1024 * 1024;
use clap::Parser;
use crate::application::services::{
AnalyseService, AppError, ArchiveService, EmbedService, ExtractService, KeyGenService,
ScrubService,
};
use crate::domain::types::{CoverMedia, CoverMediaKind, Payload};
use super::cli::{self, Cli, Commands};
pub fn run() -> Result<(), AppError> {
let cli = Cli::parse();
dispatch(cli)
}
pub fn dispatch(cli: Cli) -> Result<(), AppError> {
match cli.command {
Commands::Version => {
print_version();
Ok(())
}
Commands::Keygen(args) => cmd_keygen(&args),
Commands::Embed(args) => cmd_embed(&args),
Commands::Extract(args) => cmd_extract(&args),
Commands::EmbedDistributed(args) => cmd_embed_distributed(&args),
Commands::ExtractDistributed(args) => cmd_extract_distributed(&args),
Commands::Analyse(args) => cmd_analyse(&args),
Commands::Archive(args) => cmd_archive(&args),
Commands::Scrub(args) => cmd_scrub(&args),
Commands::DeadDrop(args) => cmd_dead_drop(&args),
Commands::TimeLock(args) => cmd_time_lock(&args),
Commands::Watermark(args) => cmd_watermark(&args),
Commands::Corpus(args) => cmd_corpus(&args),
Commands::Panic(args) => cmd_panic(&args),
Commands::Completions(args) => cmd_completions(&args),
}
}
fn print_version() {
let version = env!("CARGO_PKG_VERSION");
let sha = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown");
println!("shadowforge {version} ({sha})");
}
fn cmd_keygen(args: &cli::KeygenArgs) -> Result<(), AppError> {
let dir = &args.output;
fs_create_dir_all(dir)?;
match args.algorithm {
cli::Algorithm::Kyber1024 => {
let enc = crate::adapters::crypto::MlKemEncryptor;
let kp = KeyGenService::generate_keypair(&enc)?;
fs_write(&dir.join("public.key"), kp.public_key.as_ref())?;
fs_write(&dir.join("secret.key"), kp.secret_key.as_ref())?;
}
cli::Algorithm::Dilithium3 => {
let signer = crate::adapters::crypto::MlDsaSigner;
let kp = KeyGenService::generate_signing_keypair(&signer)?;
fs_write(&dir.join("public.key"), kp.public_key.as_ref())?;
fs_write(&dir.join("secret.key"), kp.secret_key.as_ref())?;
}
}
eprintln!("Keys written to {}", dir.display());
Ok(())
}
fn cmd_embed(args: &cli::EmbedArgs) -> Result<(), AppError> {
let technique = resolve_technique(args.technique);
let embedder = build_embedder(technique);
let payload_bytes = fs_read(&args.input)?;
let mut payload = Payload::from_bytes(payload_bytes);
if args.scrub_style {
let scrubber = crate::adapters::scrubber::StyloScrubberImpl::new();
let profile = crate::domain::types::StyloProfile {
normalize_punctuation: true,
target_avg_sentence_len: 15.0,
target_vocab_size: 1000,
};
let text = String::from_utf8_lossy(payload.as_bytes());
let result = ScrubService::scrub(&text, &profile, &scrubber)?;
payload = Payload::from_bytes(result.into_bytes());
}
if args.amnesia {
let pipeline = crate::adapters::opsec::AmnesiaPipelineImpl::new();
let mut payload_cursor = io::Cursor::new(payload.as_bytes().to_vec());
let mut cover_input = io::stdin().lock();
let mut output = io::stdout().lock();
crate::application::services::AmnesiaPipelineService::embed_in_memory(
&mut payload_cursor,
&mut cover_input,
&mut output,
embedder.as_ref(),
&pipeline,
)?;
} else if args.deniable {
let cover_path = args.cover.as_ref().ok_or_else(|| {
crate::domain::errors::DeniableError::EmbedFailed {
reason: "--cover is required for deniable embedding".into(),
}
})?;
let cover_bytes = fs_read(cover_path)?;
let cover = load_cover(technique, &cover_bytes);
let decoy_path = args.decoy_payload.as_ref().ok_or_else(|| {
crate::domain::errors::DeniableError::EmbedFailed {
reason: "--decoy-payload is required for deniable embedding".into(),
}
})?;
let decoy_bytes = fs_read(decoy_path)?;
let primary_key = match &args.key {
Some(p) => fs_read(p)?,
None => vec![0u8; 32],
};
let decoy_key = match &args.decoy_key {
Some(p) => fs_read(p)?,
None => vec![1u8; 32],
};
let pair = crate::domain::types::DeniablePayloadPair {
real_payload: payload.as_bytes().to_vec(),
decoy_payload: decoy_bytes,
};
let keys = crate::domain::types::DeniableKeySet {
primary_key,
decoy_key,
};
let deniable = crate::adapters::stego::DualPayloadEmbedder;
let stego = crate::application::services::DeniableEmbedService::embed_dual(
cover,
&pair,
&keys,
embedder.as_ref(),
&deniable,
)?;
fs_write(&args.output, stego.data.as_ref())?;
eprintln!("Deniable embedding complete");
} else {
let cover_path = args.cover.as_ref().ok_or_else(|| {
crate::domain::errors::StegoError::MalformedCoverData {
reason: "--cover is required when not using --amnesia".into(),
}
})?;
let cover_bytes = fs_read(cover_path)?;
let cover = load_cover(technique, &cover_bytes);
let stego = EmbedService::embed(cover, &payload, embedder.as_ref())?;
fs_write(&args.output, stego.data.as_ref())?;
eprintln!("Embedded {} bytes", payload.len());
}
Ok(())
}
fn cmd_extract(args: &cli::ExtractArgs) -> Result<(), AppError> {
let technique = resolve_technique(args.technique);
let extractor = build_extractor(technique);
if args.amnesia {
let mut buf = Vec::new();
io::stdin()
.lock()
.take(MAX_STDIN_PAYLOAD)
.read_to_end(&mut buf)
.map_err(|e| crate::domain::errors::StegoError::MalformedCoverData {
reason: format!("stdin read: {e}"),
})?;
let cover = load_cover(technique, &buf);
let payload = ExtractService::extract(&cover, extractor.as_ref())?;
io::stdout().write_all(payload.as_bytes()).map_err(|e| {
crate::domain::errors::StegoError::MalformedCoverData {
reason: format!("stdout write: {e}"),
}
})?;
} else {
let stego_bytes = fs_read(&args.input)?;
let stego = load_cover(technique, &stego_bytes);
let payload = ExtractService::extract(&stego, extractor.as_ref())?;
fs_write(&args.output, payload.as_bytes())?;
eprintln!("Extracted {} bytes", payload.len());
}
Ok(())
}
fn cmd_embed_distributed(args: &cli::EmbedDistributedArgs) -> Result<(), AppError> {
let technique = resolve_technique(args.technique);
let embedder = build_embedder(technique);
let payload_bytes = fs_read(&args.input)?;
let payload = Payload::from_bytes(payload_bytes);
let cover_paths = collect_cover_paths(&args.covers)?;
let covers: Result<Vec<CoverMedia>, AppError> = cover_paths
.iter()
.map(|p| {
let data = fs_read(p)?;
Ok(load_cover(technique, &data))
})
.collect();
let covers = covers?;
let profile = resolve_profile(args.profile, args.platform);
let hmac_key = match &args.hmac_key {
Some(p) => fs_read(p)?,
None => crate::adapters::distribution::DistributorImpl::generate_hmac_key(),
};
let distributor = crate::adapters::distribution::DistributorImpl::new(hmac_key.clone());
let stego_covers = crate::application::services::DistributeService::distribute(
&payload,
covers,
&profile,
&distributor,
embedder.as_ref(),
)?;
let files: Vec<(String, Vec<u8>)> = stego_covers
.iter()
.enumerate()
.map(|(i, c)| (format!("shard_{i:04}.png"), c.data.to_vec()))
.collect();
let file_refs: Vec<(&str, &[u8])> = files
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let handler = crate::adapters::archive::ArchiveHandlerImpl::new();
let archive = ArchiveService::pack(
&file_refs,
crate::domain::types::ArchiveFormat::Zip,
&handler,
)?;
fs_write(&args.output_archive, &archive)?;
if args.hmac_key.is_none() {
let key_path = args.output_archive.with_extension("hmac");
fs_write(&key_path, &hmac_key)?;
eprintln!("HMAC key written to {}", key_path.display());
}
eprintln!(
"Distributed into {} shards → {}",
stego_covers.len(),
args.output_archive.display()
);
Ok(())
}
fn cmd_extract_distributed(args: &cli::ExtractDistributedArgs) -> Result<(), AppError> {
let technique = resolve_technique(args.technique);
let extractor = build_extractor(technique);
let archive_bytes = fs_read(&args.input_archive)?;
let handler = crate::adapters::archive::ArchiveHandlerImpl::new();
let entries = ArchiveService::unpack(
&archive_bytes,
crate::domain::types::ArchiveFormat::Zip,
&handler,
)?;
let covers: Vec<CoverMedia> = entries
.iter()
.map(|(_, data)| load_cover(technique, data))
.collect();
let hmac_key = if let Some(p) = &args.hmac_key {
fs_read(p)?
} else {
let default_path = args.input_archive.with_extension("hmac");
fs_read(&default_path)?
};
let reconstructor = crate::adapters::reconstruction::ReconstructorImpl::new(
args.data_shards,
args.parity_shards,
hmac_key,
0,
);
let payload = crate::application::services::ReconstructService::reconstruct(
covers,
extractor.as_ref(),
&reconstructor,
&|done, total| eprintln!("Reconstructing: {done}/{total}"),
)?;
fs_write(&args.output, payload.as_bytes())?;
eprintln!("Reconstructed {} bytes", payload.len());
Ok(())
}
fn cmd_analyse(args: &cli::AnalyseArgs) -> Result<(), AppError> {
let technique = resolve_technique(args.technique);
let cover_bytes = fs_read(&args.cover)?;
let cover = load_cover(technique, &cover_bytes);
let analyser = crate::adapters::analysis::CapacityAnalyserImpl::new();
let report = AnalyseService::analyse(&cover, technique, &analyser)?;
if args.json {
let json = serde_json::to_string_pretty(&report).map_err(|e| {
crate::domain::errors::AnalysisError::ComputationFailed {
reason: format!("json serialisation: {e}"),
}
})?;
println!("{json}");
} else {
println!("Technique: {:?}", report.technique);
println!("Capacity: {} bytes", report.cover_capacity.bytes);
println!("Chi-square: {:.2} dB", report.chi_square_score);
println!("Risk: {:?}", report.detectability_risk);
println!(
"Recommended: {} bytes",
report.recommended_max_payload_bytes
);
}
Ok(())
}
fn cmd_archive(args: &cli::ArchiveArgs) -> Result<(), AppError> {
match &args.subcmd {
cli::ArchiveSubcommand::Pack {
files,
format,
output,
} => {
let file_data: Result<Vec<(String, Vec<u8>)>, AppError> = files
.iter()
.map(|p| {
let name = p.file_name().map_or_else(
|| p.display().to_string(),
|n| n.to_string_lossy().into_owned(),
);
let data = fs_read(p)?;
Ok((name, data))
})
.collect();
let file_data = file_data?;
let refs: Vec<(&str, &[u8])> = file_data
.iter()
.map(|(n, d)| (n.as_str(), d.as_slice()))
.collect();
let handler = crate::adapters::archive::ArchiveHandlerImpl::new();
let fmt = resolve_archive_format(*format);
let packed = ArchiveService::pack(&refs, fmt, &handler)?;
fs_write(output, &packed)?;
eprintln!("Packed {} files → {}", files.len(), output.display());
}
cli::ArchiveSubcommand::Unpack {
input,
format,
output_dir,
} => {
let data = fs_read(input)?;
let handler = crate::adapters::archive::ArchiveHandlerImpl::new();
let fmt = resolve_archive_format(*format);
let entries = ArchiveService::unpack(&data, fmt, &handler)?;
fs_create_dir_all(output_dir)?;
for (name, content) in &entries {
let path = output_dir.join(name);
if let Some(parent) = path.parent() {
fs_create_dir_all(parent)?;
}
fs_write(&path, content.as_ref())?;
}
eprintln!(
"Unpacked {} entries → {}",
entries.len(),
output_dir.display()
);
}
}
Ok(())
}
fn cmd_scrub(args: &cli::ScrubArgs) -> Result<(), AppError> {
let text = std::fs::read_to_string(&args.input).map_err(|e| {
crate::domain::errors::ScrubberError::InvalidUtf8 {
reason: format!("read: {e}"),
}
})?;
let scrubber = crate::adapters::scrubber::StyloScrubberImpl::new();
let profile = crate::domain::types::StyloProfile {
normalize_punctuation: true,
target_avg_sentence_len: f64::from(args.avg_sentence_len),
target_vocab_size: args.vocab_size,
};
let result = ScrubService::scrub(&text, &profile, &scrubber)?;
fs_write(&args.output, result.as_bytes())?;
eprintln!("Scrubbed text → {}", args.output.display());
Ok(())
}
fn cmd_dead_drop(args: &cli::DeadDropArgs) -> Result<(), AppError> {
let technique = resolve_technique(args.technique);
let embedder = build_embedder(technique);
let cover_bytes = fs_read(&args.cover)?;
let cover = load_cover(technique, &cover_bytes);
let payload_bytes = fs_read(&args.input)?;
let payload = Payload::from_bytes(payload_bytes);
let platform = resolve_platform(args.platform);
let encoder = crate::adapters::deadrop::DeadDropEncoderImpl::new();
let stego = crate::application::services::DeadDropService::encode(
cover,
&payload,
&platform,
embedder.as_ref(),
&encoder,
)?;
fs_write(&args.output, stego.data.as_ref())?;
eprintln!("Dead drop encoded for {platform:?}");
Ok(())
}
fn cmd_time_lock(args: &cli::TimeLockArgs) -> Result<(), AppError> {
let service = crate::adapters::timelock::TimeLockServiceImpl::default();
match &args.subcmd {
cli::TimeLockSubcommand::Lock {
input,
unlock_at,
output_puzzle,
} => {
let data = fs_read(input)?;
let payload = Payload::from_bytes(data);
let ts = chrono::DateTime::parse_from_rfc3339(unlock_at)
.map_err(
|e| crate::domain::errors::TimeLockError::ComputationFailed {
reason: format!("invalid RFC 3339 timestamp: {e}"),
},
)?
.with_timezone(&chrono::Utc);
let puzzle =
crate::application::services::TimeLockServiceApp::lock(&payload, ts, &service)?;
let encoded = serde_json::to_vec_pretty(&puzzle).map_err(|e| {
crate::domain::errors::TimeLockError::ComputationFailed {
reason: format!("serialize puzzle: {e}"),
}
})?;
fs_write(output_puzzle, &encoded)?;
eprintln!("Puzzle → {}", output_puzzle.display());
}
cli::TimeLockSubcommand::Unlock {
puzzle: puzzle_path,
output,
} => {
let data = fs_read(puzzle_path)?;
let puzzle: crate::domain::types::TimeLockPuzzle = serde_json::from_slice(&data)
.map_err(
|e| crate::domain::errors::TimeLockError::ComputationFailed {
reason: format!("deserialize puzzle: {e}"),
},
)?;
let payload =
crate::application::services::TimeLockServiceApp::unlock(&puzzle, &service)?;
fs_write(output, payload.as_bytes())?;
eprintln!("Unlocked {} bytes", payload.len());
}
cli::TimeLockSubcommand::TryUnlock {
puzzle: puzzle_path,
} => {
let data = fs_read(puzzle_path)?;
let puzzle: crate::domain::types::TimeLockPuzzle = serde_json::from_slice(&data)
.map_err(
|e| crate::domain::errors::TimeLockError::ComputationFailed {
reason: format!("deserialize puzzle: {e}"),
},
)?;
match crate::application::services::TimeLockServiceApp::try_unlock(&puzzle, &service)? {
Some(p) => eprintln!("Puzzle solved: {} bytes", p.len()),
None => eprintln!("Puzzle not yet solvable"),
}
}
}
Ok(())
}
fn cmd_watermark(args: &cli::WatermarkArgs) -> Result<(), AppError> {
match &args.subcmd {
cli::WatermarkSubcommand::EmbedTripwire {
cover,
output,
recipient_id,
} => {
let cover_bytes = fs_read(cover)?;
let cover_media =
load_cover(crate::domain::types::StegoTechnique::LsbImage, &cover_bytes);
let watermarker = crate::adapters::opsec::ForensicWatermarkerImpl::new();
let rid = uuid::Uuid::parse_str(recipient_id).map_err(|e| {
crate::domain::errors::OpsecError::WatermarkError {
reason: format!("invalid UUID: {e}"),
}
})?;
let tag = crate::domain::types::WatermarkTripwireTag {
recipient_id: rid,
embedding_seed: recipient_id.as_bytes().to_vec(),
};
let stego = crate::application::services::ForensicService::embed_tripwire(
cover_media,
&tag,
&watermarker,
)?;
fs_write(output, stego.data.as_ref())?;
eprintln!("Tripwire embedded for {recipient_id}");
}
cli::WatermarkSubcommand::Identify { cover, tags } => {
let cover_bytes = fs_read(cover)?;
let cover_media =
load_cover(crate::domain::types::StegoTechnique::LsbImage, &cover_bytes);
let watermarker = crate::adapters::opsec::ForensicWatermarkerImpl::new();
let tag_list = load_watermark_tags(tags)?;
let receipt = crate::application::services::ForensicService::identify_recipient(
&cover_media,
&tag_list,
&watermarker,
)?;
match receipt {
Some(r) => println!("Identified recipient: {r}"),
None => println!("No matching watermark found"),
}
}
}
Ok(())
}
fn cmd_corpus(args: &cli::CorpusArgs) -> Result<(), AppError> {
use crate::domain::ports::CorpusIndex;
let index = crate::adapters::corpus::CorpusIndexImpl::new();
match &args.subcmd {
cli::CorpusSubcommand::Build { dir } => {
let count = index.build_index(dir)?;
eprintln!("Indexed {count} images from {}", dir.display());
}
cli::CorpusSubcommand::Search {
input,
technique,
top,
} => {
let data = fs_read(input)?;
let payload = Payload::from_bytes(data);
let tech = resolve_technique(*technique);
let results = index.search(&payload, tech, *top)?;
for entry in &results {
println!("{}", entry.path);
}
}
}
Ok(())
}
fn cmd_panic(args: &cli::PanicArgs) -> Result<(), AppError> {
let config = crate::domain::types::PanicWipeConfig {
key_paths: args.key_paths.iter().map(PathBuf::from).collect(),
config_paths: Vec::new(),
temp_dirs: Vec::new(),
};
let wiper = crate::adapters::opsec::SecurePanicWiper::new();
crate::application::services::PanicWipeService::wipe(&config, &wiper)?;
Ok(())
}
fn cmd_completions(args: &cli::CompletionsArgs) -> Result<(), AppError> {
use clap::CommandFactory;
use clap_complete::generate;
let mut cmd = Cli::command();
match &args.output {
Some(path) => {
let mut file = std::fs::File::create(path).map_err(|e| {
AppError::Stego(crate::domain::errors::StegoError::MalformedCoverData {
reason: format!("write {}: {e}", path.display()),
})
})?;
generate(args.shell, &mut cmd, "shadowforge", &mut file);
}
None => {
generate(args.shell, &mut cmd, "shadowforge", &mut io::stdout());
}
}
Ok(())
}
fn fs_read(path: &Path) -> Result<Vec<u8>, AppError> {
std::fs::read(path).map_err(|e| {
AppError::Stego(crate::domain::errors::StegoError::MalformedCoverData {
reason: format!("read {}: {e}", path.display()),
})
})
}
fn fs_write(path: &Path, data: &[u8]) -> Result<(), AppError> {
if let Some(parent) = path.parent() {
fs_create_dir_all(parent)?;
}
std::fs::write(path, data).map_err(|e| {
AppError::Stego(crate::domain::errors::StegoError::MalformedCoverData {
reason: format!("write {}: {e}", path.display()),
})
})
}
fn fs_create_dir_all(path: &Path) -> Result<(), AppError> {
std::fs::create_dir_all(path).map_err(|e| {
AppError::Stego(crate::domain::errors::StegoError::MalformedCoverData {
reason: format!("mkdir {}: {e}", path.display()),
})
})
}
fn load_cover(technique: crate::domain::types::StegoTechnique, data: &[u8]) -> CoverMedia {
let kind = match technique {
crate::domain::types::StegoTechnique::LsbAudio
| crate::domain::types::StegoTechnique::PhaseEncoding
| crate::domain::types::StegoTechnique::EchoHiding => CoverMediaKind::WavAudio,
crate::domain::types::StegoTechnique::ZeroWidthText => CoverMediaKind::PlainText,
crate::domain::types::StegoTechnique::PdfContentStream
| crate::domain::types::StegoTechnique::PdfMetadata => CoverMediaKind::PdfDocument,
crate::domain::types::StegoTechnique::LsbImage
| crate::domain::types::StegoTechnique::DctJpeg
| crate::domain::types::StegoTechnique::Palette
| crate::domain::types::StegoTechnique::CorpusSelection
| crate::domain::types::StegoTechnique::DualPayload => CoverMediaKind::PngImage,
};
CoverMedia {
kind,
data: bytes::Bytes::from(data.to_vec()),
metadata: std::collections::HashMap::new(),
}
}
fn build_embedder(
technique: crate::domain::types::StegoTechnique,
) -> Box<dyn crate::domain::ports::EmbedTechnique> {
use crate::domain::types::StegoTechnique;
match technique {
StegoTechnique::DctJpeg => Box::new(crate::adapters::stego::DctJpeg),
StegoTechnique::Palette => Box::new(crate::adapters::stego::PaletteStego),
StegoTechnique::LsbAudio => Box::new(crate::adapters::stego::LsbAudio),
StegoTechnique::PhaseEncoding => Box::new(crate::adapters::stego::PhaseEncoding),
StegoTechnique::EchoHiding => Box::new(crate::adapters::stego::EchoHiding),
StegoTechnique::ZeroWidthText => Box::new(crate::adapters::stego::ZeroWidthText),
StegoTechnique::LsbImage
| StegoTechnique::PdfContentStream
| StegoTechnique::PdfMetadata
| StegoTechnique::CorpusSelection
| StegoTechnique::DualPayload => Box::new(crate::adapters::stego::LsbImage::new()),
}
}
fn build_extractor(
technique: crate::domain::types::StegoTechnique,
) -> Box<dyn crate::domain::ports::ExtractTechnique> {
use crate::domain::types::StegoTechnique;
match technique {
StegoTechnique::DctJpeg => Box::new(crate::adapters::stego::DctJpeg),
StegoTechnique::Palette => Box::new(crate::adapters::stego::PaletteStego),
StegoTechnique::LsbAudio => Box::new(crate::adapters::stego::LsbAudio),
StegoTechnique::PhaseEncoding => Box::new(crate::adapters::stego::PhaseEncoding),
StegoTechnique::EchoHiding => Box::new(crate::adapters::stego::EchoHiding),
StegoTechnique::ZeroWidthText => Box::new(crate::adapters::stego::ZeroWidthText),
StegoTechnique::LsbImage
| StegoTechnique::PdfContentStream
| StegoTechnique::PdfMetadata
| StegoTechnique::CorpusSelection
| StegoTechnique::DualPayload => Box::new(crate::adapters::stego::LsbImage::new()),
}
}
const fn resolve_technique(t: cli::Technique) -> crate::domain::types::StegoTechnique {
match t {
cli::Technique::Lsb => crate::domain::types::StegoTechnique::LsbImage,
cli::Technique::Dct => crate::domain::types::StegoTechnique::DctJpeg,
cli::Technique::Palette => crate::domain::types::StegoTechnique::Palette,
cli::Technique::LsbAudio => crate::domain::types::StegoTechnique::LsbAudio,
cli::Technique::Phase => crate::domain::types::StegoTechnique::PhaseEncoding,
cli::Technique::Echo => crate::domain::types::StegoTechnique::EchoHiding,
cli::Technique::ZeroWidth => crate::domain::types::StegoTechnique::ZeroWidthText,
cli::Technique::PdfStream => crate::domain::types::StegoTechnique::PdfContentStream,
cli::Technique::PdfMeta => crate::domain::types::StegoTechnique::PdfMetadata,
cli::Technique::Corpus => crate::domain::types::StegoTechnique::CorpusSelection,
}
}
fn resolve_profile(
profile: cli::Profile,
platform: Option<cli::Platform>,
) -> crate::domain::types::EmbeddingProfile {
match profile {
cli::Profile::Standard => crate::domain::types::EmbeddingProfile::Standard,
cli::Profile::Adaptive => crate::domain::types::EmbeddingProfile::Adaptive {
max_detectability_db: -12.0,
},
cli::Profile::Survivable => {
let p = platform.map_or(crate::domain::types::PlatformProfile::Instagram, |pl| {
resolve_platform(pl)
});
crate::domain::types::EmbeddingProfile::CompressionSurvivable { platform: p }
}
}
}
const fn resolve_platform(p: cli::Platform) -> crate::domain::types::PlatformProfile {
match p {
cli::Platform::Instagram => crate::domain::types::PlatformProfile::Instagram,
cli::Platform::Twitter => crate::domain::types::PlatformProfile::Twitter,
cli::Platform::Whatsapp => crate::domain::types::PlatformProfile::WhatsApp,
cli::Platform::Telegram => crate::domain::types::PlatformProfile::Telegram,
cli::Platform::Imgur => crate::domain::types::PlatformProfile::Imgur,
}
}
const fn resolve_archive_format(f: cli::ArchiveFormat) -> crate::domain::types::ArchiveFormat {
match f {
cli::ArchiveFormat::Zip => crate::domain::types::ArchiveFormat::Zip,
cli::ArchiveFormat::Tar => crate::domain::types::ArchiveFormat::Tar,
cli::ArchiveFormat::TarGz => crate::domain::types::ArchiveFormat::TarGz,
}
}
fn collect_cover_paths(pattern: &str) -> Result<Vec<PathBuf>, AppError> {
let path = PathBuf::from(pattern);
let paths: Vec<PathBuf> = if path.is_dir() {
std::fs::read_dir(&path)
.map_err(
|_| crate::domain::errors::DistributionError::InsufficientCovers {
needed: 1,
got: 0,
},
)?
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file())
.collect()
} else {
vec![path]
};
if paths.is_empty() {
return Err(
crate::domain::errors::DistributionError::InsufficientCovers { needed: 1, got: 0 }
.into(),
);
}
Ok(paths)
}
fn load_watermark_tags(
dir: &Path,
) -> Result<Vec<crate::domain::types::WatermarkTripwireTag>, AppError> {
let mut tags = Vec::new();
if dir.is_dir() {
let entries = std::fs::read_dir(dir).map_err(|e| {
crate::domain::errors::OpsecError::WatermarkError {
reason: format!("read dir: {e}"),
}
})?;
for entry in entries {
let entry = entry.map_err(|e: std::io::Error| {
crate::domain::errors::OpsecError::WatermarkError {
reason: format!("dir entry: {e}"),
}
})?;
let path = entry.path();
if path.is_file() {
if let Ok(content) = std::fs::read_to_string(&path) {
let id_str = content.trim();
let rid = uuid::Uuid::parse_str(id_str).map_err(|e| {
crate::domain::errors::OpsecError::WatermarkError {
reason: format!("invalid UUID in {}: {e}", path.display()),
}
})?;
tags.push(crate::domain::types::WatermarkTripwireTag {
recipient_id: rid,
embedding_seed: id_str.as_bytes().to_vec(),
});
}
}
}
}
Ok(tags)
}