Skip to main content

shadowforge_lib/domain/
ports.rs

1//! Port trait definitions for all bounded contexts.
2//!
3//! Each trait defines a capability boundary between the domain and its
4//! adapters. All traits are **object-safe** — verified by compile-time
5//! assertions below. No I/O, no `async`, no concrete types from external
6//! crates appear in return positions.
7
8use std::io::{Read, Write};
9use std::path::Path;
10
11use bytes::Bytes;
12use chrono::{DateTime, Utc};
13
14use crate::domain::errors::{
15    AdaptiveError, AnalysisError, ArchiveError, CanaryError, CorrectionError, CryptoError,
16    DeadDropError, DeniableError, DistributionError, MediaError, OpsecError, PdfError,
17    ReconstructionError, ScrubberError, StegoError, TimeLockError,
18};
19use crate::domain::types::{
20    AnalysisReport, ArchiveFormat, CanaryShard, Capacity, CoverMedia, DeniableKeySet,
21    DeniablePayloadPair, EmbeddingProfile, GeographicManifest, KeyPair, Payload, PlatformProfile,
22    Shard, Signature, StegoTechnique, StyloProfile, TimeLockPuzzle, WatermarkReceipt,
23    WatermarkTripwireTag,
24};
25
26// ─── Crypto ───────────────────────────────────────────────────────────────────
27
28/// Key encapsulation mechanism (KEM) port — ML-KEM-1024.
29///
30/// All operations are constant-time with respect to secret key material.
31pub trait Encryptor {
32    /// Generate a fresh key pair.
33    ///
34    /// # Errors
35    /// Returns [`CryptoError::KeyGenFailed`] if the RNG or parameter
36    /// validation fails.
37    fn generate_keypair(&self) -> Result<KeyPair, CryptoError>;
38
39    /// Encapsulate a shared secret using the recipient's public key.
40    ///
41    /// Returns `(ciphertext, shared_secret)`.
42    ///
43    /// # Errors
44    /// Returns [`CryptoError::EncapsulationFailed`] on invalid key material.
45    fn encapsulate(&self, public_key: &[u8]) -> Result<(Bytes, Bytes), CryptoError>;
46
47    /// Decapsulate a shared secret using the holder's secret key.
48    ///
49    /// # Errors
50    /// Returns [`CryptoError::DecapsulationFailed`] on invalid key or
51    /// ciphertext.
52    fn decapsulate(&self, secret_key: &[u8], ciphertext: &[u8]) -> Result<Bytes, CryptoError>;
53}
54
55/// Digital signature port — ML-DSA-87.
56///
57/// All comparisons over signature bytes use constant-time equality.
58pub trait Signer {
59    /// Generate a fresh signing key pair.
60    ///
61    /// # Errors
62    /// Returns [`CryptoError::KeyGenFailed`] if key generation fails.
63    fn generate_keypair(&self) -> Result<KeyPair, CryptoError>;
64
65    /// Sign a message with the secret signing key.
66    ///
67    /// # Errors
68    /// Returns [`CryptoError::SigningFailed`] on invalid key material.
69    fn sign(&self, secret_key: &[u8], message: &[u8]) -> Result<Signature, CryptoError>;
70
71    /// Verify a signature against the public key and message.
72    ///
73    /// Returns `true` when the signature is valid.
74    ///
75    /// # Errors
76    /// Returns [`CryptoError::VerificationFailed`] only on implementation
77    /// errors; an invalid signature returns `Ok(false)`.
78    fn verify(
79        &self,
80        public_key: &[u8],
81        message: &[u8],
82        signature: &Signature,
83    ) -> Result<bool, CryptoError>;
84}
85
86/// Symmetric cipher port — AES-256-GCM.
87pub trait SymmetricCipher {
88    /// Encrypt `plaintext` with `key` and `nonce`.
89    ///
90    /// # Errors
91    /// Returns [`CryptoError::InvalidKeyLength`] or
92    /// [`CryptoError::EncryptionFailed`].
93    fn encrypt(&self, key: &[u8], nonce: &[u8], plaintext: &[u8]) -> Result<Bytes, CryptoError>;
94
95    /// Decrypt and authenticate `ciphertext` with `key` and `nonce`.
96    ///
97    /// # Errors
98    /// Returns [`CryptoError::DecryptionFailed`] if authentication fails.
99    fn decrypt(&self, key: &[u8], nonce: &[u8], ciphertext: &[u8]) -> Result<Bytes, CryptoError>;
100}
101
102// ─── Error Correction ─────────────────────────────────────────────────────────
103
104/// Reed-Solomon K-of-N erasure coding port.
105pub trait ErrorCorrector {
106    /// Encode `data` into `data_shards + parity_shards` [`Shard`]s.
107    ///
108    /// Each shard carries an HMAC-SHA-256 tag over `index || total || data`.
109    ///
110    /// # Errors
111    /// Returns [`CorrectionError::InvalidParameters`] or
112    /// [`CorrectionError::ReedSolomonError`].
113    fn encode(
114        &self,
115        data: &[u8],
116        data_shards: u8,
117        parity_shards: u8,
118    ) -> Result<Vec<Shard>, CorrectionError>;
119
120    /// Decode `shards` back to the original bytes.
121    ///
122    /// Accepts partial shard sets (some may be `None`); requires at least
123    /// `data_shards` valid shards with passing HMAC tags.
124    ///
125    /// # Errors
126    /// Returns [`CorrectionError::InsufficientShards`],
127    /// [`CorrectionError::HmacMismatch`], or
128    /// [`CorrectionError::ReedSolomonError`].
129    fn decode(
130        &self,
131        shards: &[Option<Shard>],
132        data_shards: u8,
133        parity_shards: u8,
134    ) -> Result<Bytes, CorrectionError>;
135}
136
137// ─── Steganography ────────────────────────────────────────────────────────────
138
139/// Embedding half of a steganographic technique.
140pub trait EmbedTechnique {
141    /// The technique identifier for this implementation.
142    fn technique(&self) -> StegoTechnique;
143
144    /// Estimate how many payload bytes `cover` can hold.
145    ///
146    /// # Errors
147    /// Returns [`StegoError::UnsupportedCoverType`] if the cover kind is
148    /// incompatible with this technique.
149    fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError>;
150
151    /// Embed `payload` into `cover`, returning the stego cover.
152    ///
153    /// # Errors
154    /// Returns [`StegoError::PayloadTooLarge`] or
155    /// [`StegoError::MalformedCoverData`].
156    fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError>;
157}
158
159/// Extraction half of a steganographic technique.
160pub trait ExtractTechnique {
161    /// The technique identifier for this implementation.
162    fn technique(&self) -> StegoTechnique;
163
164    /// Extract a hidden payload from `stego`.
165    ///
166    /// # Errors
167    /// Returns [`StegoError::NoPayloadFound`] or
168    /// [`StegoError::IntegrityCheckFailed`].
169    fn extract(&self, stego: &CoverMedia) -> Result<Payload, StegoError>;
170}
171
172// ─── Media ────────────────────────────────────────────────────────────────────
173
174/// Codec adapter port for loading and saving cover media files.
175///
176/// I/O is performed by the adapter; the domain receives decoded pixels/samples.
177pub trait MediaLoader {
178    /// Load a media file from `path` and return decoded [`CoverMedia`].
179    ///
180    /// # Errors
181    /// Returns [`MediaError::UnsupportedFormat`], [`MediaError::DecodeFailed`],
182    /// or [`MediaError::IoError`].
183    fn load(&self, path: &Path) -> Result<CoverMedia, MediaError>;
184
185    /// Encode `media` and write it to `path`.
186    ///
187    /// # Errors
188    /// Returns [`MediaError::EncodeFailed`] or [`MediaError::IoError`].
189    fn save(&self, media: &CoverMedia, path: &Path) -> Result<(), MediaError>;
190}
191
192// ─── PDF ──────────────────────────────────────────────────────────────────────
193
194/// First-class PDF bounded context port.
195///
196/// Covers parsing, page rendering, content-stream LSB, and metadata embedding.
197pub trait PdfProcessor {
198    /// Parse a PDF file from `path` into a [`CoverMedia`].
199    ///
200    /// # Errors
201    /// Returns [`PdfError::ParseFailed`] or [`PdfError::IoError`].
202    fn load_pdf(&self, path: &Path) -> Result<CoverMedia, PdfError>;
203
204    /// Serialise `media` back to a PDF file at `path`.
205    ///
206    /// # Errors
207    /// Returns [`PdfError::RebuildFailed`] or [`PdfError::IoError`].
208    fn save_pdf(&self, media: &CoverMedia, path: &Path) -> Result<(), PdfError>;
209
210    /// Rasterise every page of `pdf` to a PNG [`CoverMedia`].
211    ///
212    /// # Errors
213    /// Returns [`PdfError::RenderFailed`] on any page failure.
214    fn render_pages_to_images(&self, pdf: &CoverMedia) -> Result<Vec<CoverMedia>, PdfError>;
215
216    /// Reconstruct a PDF from rasterised `images`, retaining `original`
217    /// metadata where possible.
218    ///
219    /// # Errors
220    /// Returns [`PdfError::RebuildFailed`].
221    fn rebuild_pdf_from_images(
222        &self,
223        images: Vec<CoverMedia>,
224        original: &CoverMedia,
225    ) -> Result<CoverMedia, PdfError>;
226
227    /// Embed `payload` via content-stream LSB coefficient modification.
228    ///
229    /// # Errors
230    /// Returns [`PdfError::EmbedFailed`].
231    fn embed_in_content_stream(
232        &self,
233        pdf: CoverMedia,
234        payload: &Payload,
235    ) -> Result<CoverMedia, PdfError>;
236
237    /// Extract a payload previously embedded in the content stream.
238    ///
239    /// # Errors
240    /// Returns [`PdfError::ExtractFailed`].
241    fn extract_from_content_stream(&self, pdf: &CoverMedia) -> Result<Payload, PdfError>;
242
243    /// Embed `payload` into XMP / document-level metadata fields.
244    ///
245    /// # Errors
246    /// Returns [`PdfError::EmbedFailed`].
247    fn embed_in_metadata(&self, pdf: CoverMedia, payload: &Payload)
248    -> Result<CoverMedia, PdfError>;
249
250    /// Extract a payload previously embedded in XMP / document-level metadata.
251    ///
252    /// # Errors
253    /// Returns [`PdfError::ExtractFailed`].
254    fn extract_from_metadata(&self, pdf: &CoverMedia) -> Result<Payload, PdfError>;
255}
256
257// ─── Distribution ─────────────────────────────────────────────────────────────
258
259/// Payload distribution port.
260///
261/// Accepts an embedder trait-object so the distribution pattern is decoupled
262/// from any specific steganographic technique.
263pub trait Distributor {
264    /// Distribute `payload` across `covers` according to `profile`.
265    ///
266    /// # Errors
267    /// Returns [`DistributionError::InsufficientCovers`] or
268    /// [`DistributionError::EmbedFailed`].
269    fn distribute(
270        &self,
271        payload: &Payload,
272        profile: &EmbeddingProfile,
273        covers: Vec<CoverMedia>,
274        embedder: &dyn EmbedTechnique,
275    ) -> Result<Vec<CoverMedia>, DistributionError>;
276}
277
278// ─── Reconstruction ───────────────────────────────────────────────────────────
279
280/// K-of-N shard reconstruction port.
281pub trait Reconstructor {
282    /// Reconstruct the original payload from stego `covers`.
283    ///
284    /// `progress_cb` is called with `(completed, total)` after each
285    /// extraction step so callers can display progress.
286    ///
287    /// # Errors
288    /// Returns [`ReconstructionError::InsufficientCovers`],
289    /// [`ReconstructionError::ExtractionFailed`], or
290    /// [`ReconstructionError::CorrectionFailed`].
291    fn reconstruct(
292        &self,
293        covers: Vec<CoverMedia>,
294        extractor: &dyn ExtractTechnique,
295        progress_cb: &dyn Fn(usize, usize),
296    ) -> Result<Payload, ReconstructionError>;
297}
298
299// ─── Analysis ─────────────────────────────────────────────────────────────────
300
301/// Steganalysis and capacity estimation port.
302pub trait CapacityAnalyser {
303    /// Analyse `cover` with `technique` and return an [`AnalysisReport`].
304    ///
305    /// # Errors
306    /// Returns [`AnalysisError::UnsupportedCoverType`] or
307    /// [`AnalysisError::ComputationFailed`].
308    fn analyse(
309        &self,
310        cover: &CoverMedia,
311        technique: StegoTechnique,
312    ) -> Result<AnalysisReport, AnalysisError>;
313}
314
315// ─── Archive ──────────────────────────────────────────────────────────────────
316
317/// Multi-carrier archive port (ZIP / TAR / TAR.GZ).
318pub trait ArchiveHandler {
319    /// Pack `files` (name, bytes) into an archive of `format`.
320    ///
321    /// # Errors
322    /// Returns [`ArchiveError::PackFailed`] or
323    /// [`ArchiveError::UnsupportedFormat`].
324    fn pack(&self, files: &[(&str, &[u8])], format: ArchiveFormat) -> Result<Bytes, ArchiveError>;
325
326    /// Unpack `archive` of `format` into `(name, bytes)` pairs.
327    ///
328    /// # Errors
329    /// Returns [`ArchiveError::UnpackFailed`] or
330    /// [`ArchiveError::UnsupportedFormat`].
331    fn unpack(
332        &self,
333        archive: &[u8],
334        format: ArchiveFormat,
335    ) -> Result<Vec<(String, Bytes)>, ArchiveError>;
336}
337
338// ─── Adaptive Embedding ───────────────────────────────────────────────────────
339
340/// Camera-model statistical fingerprint — used to defeat model-based
341/// steganalysis by matching the cover's noise floor.
342#[derive(Debug, Clone)]
343pub struct CameraProfile {
344    /// JPEG quantisation table (64 coefficients in zig-zag order).
345    pub quantisation_table: [u16; 64],
346    /// Estimated noise floor of the camera sensor (in decibels).
347    pub noise_floor_db: f64,
348    /// Human-readable camera model identifier.
349    pub model_id: String,
350}
351
352/// Adversarial embedding optimiser port (STC-inspired).
353///
354/// Reorders bit assignments after embedding to minimise chi-square distance
355/// from the unmodified cover's statistical distribution.
356pub trait AdaptiveOptimiser {
357    /// Optimise `stego` to stay within `target_db` of the original's
358    /// statistical distribution.
359    ///
360    /// # Errors
361    /// Returns [`AdaptiveError::BudgetNotMet`] if no permutation achieves
362    /// the target.
363    fn optimise(
364        &self,
365        stego: CoverMedia,
366        original: &CoverMedia,
367        target_db: f64,
368    ) -> Result<CoverMedia, AdaptiveError>;
369}
370
371/// Camera model fingerprint matching port.
372pub trait CoverProfileMatcher {
373    /// Return the best-matching [`CameraProfile`] for `cover`, or `None`
374    /// if no profile is close enough.
375    fn profile_for(&self, cover: &CoverMedia) -> Option<CameraProfile>;
376
377    /// Apply `profile` to `cover` to make it statistically match that
378    /// camera model.
379    ///
380    /// # Errors
381    /// Returns [`AdaptiveError::ProfileMatchFailed`].
382    fn apply_profile(
383        &self,
384        cover: CoverMedia,
385        profile: &CameraProfile,
386    ) -> Result<CoverMedia, AdaptiveError>;
387}
388
389/// Compression survivability port — social media platform recompression.
390pub trait CompressionSimulator {
391    /// Simulate a target platform's recompression pipeline on `cover`.
392    ///
393    /// # Errors
394    /// Returns [`AdaptiveError::CompressionSimFailed`].
395    fn simulate(
396        &self,
397        cover: CoverMedia,
398        platform: &PlatformProfile,
399    ) -> Result<CoverMedia, AdaptiveError>;
400
401    /// Estimate the embedding capacity that survives `platform`'s pipeline.
402    ///
403    /// # Errors
404    /// Returns [`AdaptiveError::CompressionSimFailed`].
405    fn survivable_capacity(
406        &self,
407        cover: &CoverMedia,
408        platform: &PlatformProfile,
409    ) -> Result<Capacity, AdaptiveError>;
410}
411
412// ─── Deniable Steganography ───────────────────────────────────────────────────
413
414/// Dual-payload deniable steganography port.
415///
416/// The stego cover is indistinguishable regardless of which key is presented.
417pub trait DeniableEmbedder {
418    /// Embed both the real and decoy payload in `cover`.
419    ///
420    /// The resulting cover decrypts to `pair.real_payload` under
421    /// `keys.primary_key` and to `pair.decoy_payload` under `keys.decoy_key`.
422    ///
423    /// # Errors
424    /// Returns [`DeniableError::InsufficientCapacity`] or
425    /// [`DeniableError::EmbedFailed`].
426    fn embed_dual(
427        &self,
428        cover: CoverMedia,
429        pair: &DeniablePayloadPair,
430        keys: &DeniableKeySet,
431        embedder: &dyn EmbedTechnique,
432    ) -> Result<CoverMedia, DeniableError>;
433
434    /// Extract a payload from `stego` using the provided `key`.
435    ///
436    /// Returns the decoy payload when given the decoy key, and the real
437    /// payload when given the primary key — neither party can prove which.
438    ///
439    /// # Errors
440    /// Returns [`DeniableError::ExtractionFailed`].
441    fn extract_with_key(
442        &self,
443        stego: &CoverMedia,
444        key: &[u8],
445        extractor: &dyn ExtractTechnique,
446    ) -> Result<Payload, DeniableError>;
447}
448
449// ─── Operational Security ─────────────────────────────────────────────────────
450
451/// Emergency panic-wipe port.
452///
453/// Synchronous and best-effort: logs failures internally but completes all
454/// wipe steps regardless. Must never propagate an error to the caller under
455/// duress.
456pub trait PanicWiper {
457    /// Securely erase all paths described in `config`.
458    ///
459    /// # Errors
460    /// Returns [`OpsecError::WipeStepFailed`] only when *all* steps have
461    /// been attempted and logging is safe. Under duress this should be
462    /// treated as non-fatal by the caller.
463    fn wipe(&self, config: &crate::domain::types::PanicWipeConfig) -> Result<(), OpsecError>;
464}
465
466/// Forensic watermark tripwire port.
467///
468/// Unique per-recipient watermarks allow identifying which copy of a
469/// distributed set was leaked.
470pub trait ForensicWatermarker {
471    /// Embed a per-recipient tripwire watermark into `cover`.
472    ///
473    /// # Errors
474    /// Returns [`OpsecError::WatermarkError`].
475    fn embed_tripwire(
476        &self,
477        cover: CoverMedia,
478        tag: &WatermarkTripwireTag,
479    ) -> Result<CoverMedia, OpsecError>;
480
481    /// Identify which recipient's watermark is present in `stego`.
482    ///
483    /// Returns the matching [`WatermarkTripwireTag`] from `tags`, or `None`
484    /// if no match is found.
485    ///
486    /// # Errors
487    /// Returns [`OpsecError::WatermarkError`] on implementation error.
488    fn identify_recipient(
489        &self,
490        stego: &CoverMedia,
491        tags: &[WatermarkTripwireTag],
492    ) -> Result<Option<WatermarkReceipt>, OpsecError>;
493}
494
495/// Amnesiac in-memory pipeline port.
496///
497/// The entire embed/extract cycle runs without touching the filesystem.
498/// Uses [`std::io::pipe`] (stable 1.87) internally.
499pub trait AmnesiaPipeline {
500    /// Embed a payload read from `cover_input` and `payload_input` using
501    /// `technique`, writing the stego output to `output`.
502    ///
503    /// No temporary files, logs of sensitive data, or crash dumps are created.
504    ///
505    /// # Errors
506    /// Returns [`OpsecError::PipelineError`].
507    fn embed_in_memory(
508        &self,
509        payload_input: &mut dyn Read,
510        cover_input: &mut dyn Read,
511        output: &mut dyn Write,
512        technique: &dyn EmbedTechnique,
513    ) -> Result<(), OpsecError>;
514}
515
516/// Geographic threshold distribution port.
517///
518/// Annotates shards with jurisdictional metadata, producing a
519/// [`GeographicManifest`] that makes legal compulsion across jurisdictions
520/// impractical.
521pub trait GeographicDistributor {
522    /// Distribute `payload` across `covers` and annotate each shard with
523    /// jurisdictional metadata from `manifest`.
524    ///
525    /// # Errors
526    /// Returns [`OpsecError::ManifestError`].
527    fn distribute_with_manifest(
528        &self,
529        payload: &Payload,
530        covers: Vec<CoverMedia>,
531        manifest: &GeographicManifest,
532        embedder: &dyn EmbedTechnique,
533    ) -> Result<Vec<CoverMedia>, OpsecError>;
534}
535
536// ─── Canary Shards ────────────────────────────────────────────────────────────
537
538/// Canary shard tripwire port.
539pub trait CanaryService {
540    /// Embed an additional canary shard in `covers` alongside the regular
541    /// distribution.
542    ///
543    /// Returns the modified covers and the [`CanaryShard`] to be planted in
544    /// a honeypot location.
545    ///
546    /// # Errors
547    /// Returns [`CanaryError::NoCovers`] or [`CanaryError::EmbedFailed`].
548    fn embed_canary(
549        &self,
550        covers: Vec<CoverMedia>,
551        embedder: &dyn EmbedTechnique,
552    ) -> Result<(Vec<CoverMedia>, CanaryShard), CanaryError>;
553
554    /// Return `true` if the `shard`'s notify URL is reachable, indicating
555    /// the canary has been accessed.
556    ///
557    /// Non-blocking check; returns `false` on network error.
558    fn check_canary(&self, shard: &CanaryShard) -> bool;
559}
560
561// ─── Dead Drop ────────────────────────────────────────────────────────────────
562
563/// Platform-aware dead drop encoder port.
564///
565/// Produces a stego cover optimised for posting publicly on a target
566/// platform. No direct file transfer between parties.
567pub trait DeadDropEncoder {
568    /// Encode `payload` into `cover` for posting on `platform`.
569    ///
570    /// The resulting cover survives the platform's recompression pipeline
571    /// and can be retrieved by the recipient via public URL.
572    ///
573    /// # Errors
574    /// Returns [`DeadDropError::UnsupportedPlatform`] or
575    /// [`DeadDropError::EncodeFailed`].
576    fn encode_for_platform(
577        &self,
578        cover: CoverMedia,
579        payload: &Payload,
580        platform: &PlatformProfile,
581        embedder: &dyn EmbedTechnique,
582    ) -> Result<CoverMedia, DeadDropError>;
583}
584
585// ─── Time-Lock ────────────────────────────────────────────────────────────────
586
587/// Rivest sequential-squaring time-lock puzzle port.
588///
589/// A payload cannot be decrypted before a specified time, even under
590/// compulsion.
591pub trait TimeLockService {
592    /// Wrap `payload` in a time-lock puzzle that cannot be solved before
593    /// `unlock_at`.
594    ///
595    /// # Errors
596    /// Returns [`TimeLockError::ComputationFailed`].
597    fn lock(
598        &self,
599        payload: &Payload,
600        unlock_at: DateTime<Utc>,
601    ) -> Result<TimeLockPuzzle, TimeLockError>;
602
603    /// Solve the `puzzle` by sequential squaring and decrypt the payload.
604    ///
605    /// Blocks until the puzzle is solved; may take significant time.
606    ///
607    /// # Errors
608    /// Returns [`TimeLockError::ComputationFailed`] or
609    /// [`TimeLockError::DecryptFailed`].
610    fn unlock(&self, puzzle: &TimeLockPuzzle) -> Result<Payload, TimeLockError>;
611
612    /// Non-blocking puzzle check. Returns `Ok(Some(payload))` if the puzzle
613    /// is already solved, `Ok(None)` if it cannot yet be solved, or an error
614    /// if the computation itself fails.
615    ///
616    /// # Errors
617    /// Returns [`TimeLockError::ComputationFailed`] or
618    /// [`TimeLockError::DecryptFailed`].
619    fn try_unlock(&self, puzzle: &TimeLockPuzzle) -> Result<Option<Payload>, TimeLockError>;
620}
621
622// ─── Stylometric Scrubber ─────────────────────────────────────────────────────
623
624/// Linguistic stylometric fingerprint scrubbing port.
625///
626/// Normalises text to destroy authorship attribution fingerprints without
627/// changing the semantic content.
628pub trait StyloScrubber {
629    /// Scrub `text` to match `profile`.
630    ///
631    /// # Errors
632    /// Returns [`ScrubberError::InvalidUtf8`] or
633    /// [`ScrubberError::ProfileNotSatisfied`].
634    fn scrub(&self, text: &str, profile: &StyloProfile) -> Result<String, ScrubberError>;
635}
636
637// ─── Corpus Steganography ─────────────────────────────────────────────────────
638
639/// Corpus index and zero-modification cover selection port.
640pub trait CorpusIndex {
641    /// Search the index for covers whose natural bit pattern already encodes
642    /// (or closely encodes) `payload` using `technique`.
643    ///
644    /// Returns up to `max_results` entries sorted by match quality.
645    ///
646    /// # Errors
647    /// Returns [`crate::domain::errors::CorpusError::NoSuitableCover`] or
648    /// [`crate::domain::errors::CorpusError::IndexError`].
649    fn search(
650        &self,
651        payload: &Payload,
652        technique: StegoTechnique,
653        max_results: usize,
654    ) -> Result<Vec<crate::domain::types::CorpusEntry>, crate::domain::errors::CorpusError>;
655
656    /// Add the file at `path` to the index, computing its bit-pattern
657    /// fingerprint.
658    ///
659    /// # Errors
660    /// Returns [`crate::domain::errors::CorpusError::AddFailed`].
661    fn add_to_index(
662        &self,
663        path: &Path,
664    ) -> Result<crate::domain::types::CorpusEntry, crate::domain::errors::CorpusError>;
665
666    /// Scan `corpus_dir` recursively and build the full index.
667    ///
668    /// Returns the number of entries indexed.
669    ///
670    /// # Errors
671    /// Returns [`crate::domain::errors::CorpusError::IndexError`].
672    fn build_index(&self, corpus_dir: &Path) -> Result<usize, crate::domain::errors::CorpusError>;
673}
674
675// ─── Object-Safety Assertions ─────────────────────────────────────────────────
676//
677// These compile-time checks verify that every port trait is object-safe.
678// If any trait is accidentally made non-object-safe (e.g. by adding a
679// method with a generic parameter), this module will fail to compile with
680// a clear error pointing at the offending trait.
681
682#[cfg(test)]
683mod object_safety_tests {
684    use super::*;
685
686    /// Verifies object safety of all port traits.
687    #[test]
688    fn all_port_traits_are_object_safe() {
689        fn assert_object_safe<T: ?Sized>() {}
690
691        assert_object_safe::<dyn Encryptor>();
692        assert_object_safe::<dyn Signer>();
693        assert_object_safe::<dyn SymmetricCipher>();
694        assert_object_safe::<dyn ErrorCorrector>();
695        assert_object_safe::<dyn EmbedTechnique>();
696        assert_object_safe::<dyn ExtractTechnique>();
697        assert_object_safe::<dyn MediaLoader>();
698        assert_object_safe::<dyn PdfProcessor>();
699        assert_object_safe::<dyn Distributor>();
700        assert_object_safe::<dyn Reconstructor>();
701        assert_object_safe::<dyn CapacityAnalyser>();
702        assert_object_safe::<dyn ArchiveHandler>();
703        assert_object_safe::<dyn AdaptiveOptimiser>();
704        assert_object_safe::<dyn CoverProfileMatcher>();
705        assert_object_safe::<dyn CompressionSimulator>();
706        assert_object_safe::<dyn DeniableEmbedder>();
707        assert_object_safe::<dyn PanicWiper>();
708        assert_object_safe::<dyn ForensicWatermarker>();
709        assert_object_safe::<dyn AmnesiaPipeline>();
710        assert_object_safe::<dyn GeographicDistributor>();
711        assert_object_safe::<dyn CanaryService>();
712        assert_object_safe::<dyn DeadDropEncoder>();
713        assert_object_safe::<dyn TimeLockService>();
714        assert_object_safe::<dyn StyloScrubber>();
715        assert_object_safe::<dyn CorpusIndex>();
716    }
717}