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}