1use crate::domain::distribution::{assign_many_to_many, assign_one_to_many, validate_cover_count};
5use crate::domain::errors::DistributionError;
6use crate::domain::ports::{Distributor, EmbedTechnique, ErrorCorrector};
7use crate::domain::types::{CoverMedia, DistributionPattern, EmbeddingProfile, Payload};
8
9pub struct DistributorImpl {
11 hmac_key: Vec<u8>,
13 shard_config: Option<(u8, u8)>,
15 corrector: Box<dyn ErrorCorrector>,
17}
18
19impl Default for DistributorImpl {
20 fn default() -> Self {
21 let hmac_key = Self::generate_hmac_key();
22 Self::new(
23 hmac_key.clone(),
24 Box::new(crate::adapters::correction::RsErrorCorrector::new(hmac_key)),
25 )
26 }
27}
28
29impl DistributorImpl {
30 #[must_use]
32 pub fn new(hmac_key: Vec<u8>, corrector: Box<dyn ErrorCorrector>) -> Self {
33 Self {
34 hmac_key,
35 shard_config: None,
36 corrector,
37 }
38 }
39
40 #[must_use]
42 pub fn new_with_shard_config(
43 hmac_key: Vec<u8>,
44 data_shards: u8,
45 parity_shards: u8,
46 corrector: Box<dyn ErrorCorrector>,
47 ) -> Self {
48 Self {
49 hmac_key,
50 shard_config: Some((data_shards, parity_shards)),
51 corrector,
52 }
53 }
54
55 #[must_use]
57 pub fn generate_hmac_key() -> Vec<u8> {
58 use rand::Rng;
59 let mut key = vec![0u8; 32];
60 rand::rng().fill_bytes(&mut key);
61 key
62 }
63
64 #[must_use]
66 pub fn hmac_key(&self) -> &[u8] {
67 &self.hmac_key
68 }
69}
70
71impl Distributor for DistributorImpl {
72 fn distribute(
73 &self,
74 payload: &Payload,
75 profile: &EmbeddingProfile,
76 covers: Vec<CoverMedia>,
77 embedder: &dyn EmbedTechnique,
78 ) -> Result<Vec<CoverMedia>, DistributionError> {
79 let pattern = pattern_from_profile(profile, covers.len(), self.shard_config);
80 validate_cover_count(&pattern, covers.len())?;
81
82 match pattern {
83 DistributionPattern::OneToOne => distribute_one_to_one(payload, covers, embedder),
84 DistributionPattern::OneToMany {
85 data_shards,
86 parity_shards,
87 } => distribute_one_to_many(
88 payload,
89 covers,
90 embedder,
91 data_shards,
92 parity_shards,
93 self.corrector.as_ref(),
94 ),
95 DistributionPattern::ManyToOne => {
96 distribute_one_to_one(payload, covers, embedder)
99 }
100 DistributionPattern::ManyToMany { mode } => {
101 distribute_many_to_many(payload, covers, embedder, mode)
102 }
103 }
104 }
105}
106
107fn pattern_from_profile(
112 profile: &EmbeddingProfile,
113 cover_count: usize,
114 shard_config: Option<(u8, u8)>,
115) -> DistributionPattern {
116 let default_one_to_many = || {
119 #[expect(
120 clippy::cast_possible_truncation,
121 reason = "cover_count bounded by caller"
122 )]
123 let data = (cover_count.saturating_sub(1)) as u8;
124 DistributionPattern::OneToMany {
125 data_shards: data.max(1),
126 parity_shards: 1,
127 }
128 };
129
130 let explicit_one_to_many =
131 |data_shards: u8, parity_shards: u8| DistributionPattern::OneToMany {
132 data_shards: data_shards.max(1),
133 parity_shards: parity_shards.max(1),
134 };
135
136 let one_to_many = |shard_config: Option<(u8, u8)>| {
137 shard_config.map_or_else(default_one_to_many, |(data_shards, parity_shards)| {
138 explicit_one_to_many(data_shards, parity_shards)
139 })
140 };
141
142 match profile {
143 EmbeddingProfile::Standard
144 | EmbeddingProfile::Adaptive { .. }
145 | EmbeddingProfile::CompressionSurvivable { .. } => {
146 if cover_count <= 1 {
147 DistributionPattern::OneToOne
148 } else {
149 one_to_many(shard_config)
150 }
151 }
152 EmbeddingProfile::CorpusBased => DistributionPattern::OneToOne,
153 }
154}
155
156fn distribute_one_to_one(
158 payload: &Payload,
159 mut covers: Vec<CoverMedia>,
160 embedder: &dyn EmbedTechnique,
161) -> Result<Vec<CoverMedia>, DistributionError> {
162 if covers.is_empty() {
163 return Err(DistributionError::InsufficientCovers { needed: 1, got: 0 });
164 }
165 let cover = covers.remove(0);
166 let stego = embedder
167 .embed(cover, payload)
168 .map_err(|source| DistributionError::EmbedFailed { index: 0, source })?;
169 let mut result = vec![stego];
170 result.extend(covers);
171 Ok(result)
172}
173
174fn distribute_one_to_many(
176 payload: &Payload,
177 covers: Vec<CoverMedia>,
178 embedder: &dyn EmbedTechnique,
179 data_shards: u8,
180 parity_shards: u8,
181 corrector: &dyn ErrorCorrector,
182) -> Result<Vec<CoverMedia>, DistributionError> {
183 let shards = corrector
184 .encode(payload.as_bytes(), data_shards, parity_shards)
185 .map_err(|source| DistributionError::CorrectionFailed { source })?;
186
187 let assignments = assign_one_to_many(shards.len(), covers.len());
188 let mut result = covers;
189
190 for (shard_idx, cover_idx) in assignments {
191 let shard = shards
192 .get(shard_idx)
193 .ok_or_else(|| DistributionError::InsufficientCovers {
194 needed: shard_idx.strict_add(1),
195 got: shards.len(),
196 })?;
197 let shard_payload = Payload::from_bytes(shard.data.clone());
198 let cover = result.remove(cover_idx);
199 let stego = embedder.embed(cover, &shard_payload).map_err(|source| {
200 DistributionError::EmbedFailed {
201 index: cover_idx,
202 source,
203 }
204 })?;
205 result.insert(cover_idx, stego);
206 }
207
208 Ok(result)
209}
210
211fn distribute_many_to_many(
213 payload: &Payload,
214 covers: Vec<CoverMedia>,
215 embedder: &dyn EmbedTechnique,
216 mode: crate::domain::types::ManyToManyMode,
217) -> Result<Vec<CoverMedia>, DistributionError> {
218 let cover_count = covers.len();
220 let chunk_size = (payload.len().strict_add(cover_count).strict_sub(1)) / cover_count;
222 let chunks: Vec<Payload> = payload
223 .as_bytes()
224 .chunks(chunk_size)
225 .map(|c| Payload::from_bytes(c.to_vec()))
226 .collect();
227
228 let assignments = assign_many_to_many(mode, chunks.len(), cover_count, 42);
229 let mut result = covers;
230
231 for (shard_idx, cover_indices) in assignments.iter().enumerate() {
232 let Some(chunk) = chunks.get(shard_idx) else {
233 break;
234 };
235 for &cover_idx in cover_indices {
236 let cover = result.remove(cover_idx);
237 let stego =
238 embedder
239 .embed(cover, chunk)
240 .map_err(|source| DistributionError::EmbedFailed {
241 index: cover_idx,
242 source,
243 })?;
244 result.insert(cover_idx, stego);
245 }
246 }
247
248 Ok(result)
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::domain::distribution::pack_many_payloads;
255 use crate::domain::errors::StegoError;
256 use crate::domain::types::{Capacity, CoverMedia, CoverMediaKind, StegoTechnique};
257 use bytes::Bytes;
258
259 type TestResult = Result<(), Box<dyn std::error::Error>>;
260
261 struct MockEmbedder;
263
264 impl EmbedTechnique for MockEmbedder {
265 fn technique(&self) -> StegoTechnique {
266 StegoTechnique::LsbImage
267 }
268
269 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
270 Ok(Capacity {
271 bytes: cover.data.len() as u64,
272 technique: StegoTechnique::LsbImage,
273 })
274 }
275
276 fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
277 let mut data = cover.data.to_vec();
278 data.extend_from_slice(payload.as_bytes());
279 Ok(CoverMedia {
280 kind: cover.kind,
281 data: Bytes::from(data),
282 metadata: cover.metadata,
283 })
284 }
285 }
286
287 fn make_cover(size: usize) -> CoverMedia {
288 CoverMedia {
289 kind: CoverMediaKind::PngImage,
290 data: Bytes::from(vec![0u8; size]),
291 metadata: std::collections::HashMap::new(),
292 }
293 }
294
295 #[test]
296 fn one_to_one_round_trip() -> TestResult {
297 let corrector: Box<dyn ErrorCorrector> = Box::new(
298 crate::adapters::correction::RsErrorCorrector::new(b"test-hmac-key".to_vec()),
299 );
300 let distributor = DistributorImpl::new(b"test-hmac-key".to_vec(), corrector);
301 let payload = Payload::from_bytes(b"secret message".to_vec());
302 let covers = vec![make_cover(128)];
303 let result =
304 distributor.distribute(&payload, &EmbeddingProfile::Standard, covers, &MockEmbedder)?;
305 assert_eq!(result.len(), 1);
306 assert_eq!(
308 result.first().ok_or("index out of bounds")?.data.len(),
309 128 + 14
310 );
311 Ok(())
312 }
313
314 #[test]
315 fn one_to_many_produces_correct_shard_count() -> TestResult {
316 let covers: Vec<CoverMedia> = (0..8).map(|_| make_cover(256)).collect();
317 let payload = Payload::from_bytes(vec![0xAB; 64]);
318
319 let pattern = DistributionPattern::OneToMany {
321 data_shards: 5,
322 parity_shards: 3,
323 };
324 validate_cover_count(&pattern, covers.len())?;
325
326 let corrector: Box<dyn ErrorCorrector> = Box::new(
327 crate::adapters::correction::RsErrorCorrector::new(b"test-hmac-key".to_vec()),
328 );
329 let result =
330 distribute_one_to_many(&payload, covers, &MockEmbedder, 5, 3, corrector.as_ref())?;
331 assert_eq!(result.len(), 8);
332 for cover in &result {
334 assert!(cover.data.len() > 256);
335 }
336 Ok(())
337 }
338
339 #[test]
340 fn many_to_one_embed_single_cover() -> TestResult {
341 let corrector: Box<dyn ErrorCorrector> = Box::new(
342 crate::adapters::correction::RsErrorCorrector::new(b"test-hmac-key".to_vec()),
343 );
344 let distributor = DistributorImpl::new(b"test-hmac-key".to_vec(), corrector);
345 let payload = Payload::from_bytes(b"combined payload".to_vec());
346 let covers = vec![make_cover(512)];
347 let result =
348 distributor.distribute(&payload, &EmbeddingProfile::Standard, covers, &MockEmbedder)?;
349 assert_eq!(result.len(), 1);
350 assert!(result.first().ok_or("empty result")?.data.len() > 512);
351 Ok(())
352 }
353
354 #[test]
355 fn many_to_many_replicate_mode() -> TestResult {
356 let covers = vec![make_cover(256), make_cover(256), make_cover(256)];
357 let payload = Payload::from_bytes(vec![0xCC; 30]);
358
359 let result = distribute_many_to_many(
360 &payload,
361 covers,
362 &MockEmbedder,
363 crate::domain::types::ManyToManyMode::Replicate,
364 )?;
365 assert_eq!(result.len(), 3);
366 for cover in &result {
368 assert!(cover.data.len() > 256);
369 }
370 Ok(())
371 }
372
373 #[test]
374 fn insufficient_covers_returns_error() {
375 let corrector: Box<dyn ErrorCorrector> = Box::new(
376 crate::adapters::correction::RsErrorCorrector::new(b"test-hmac-key".to_vec()),
377 );
378 let distributor = DistributorImpl::new(b"test-hmac-key".to_vec(), corrector);
379 let payload = Payload::from_bytes(b"test".to_vec());
380 let covers: Vec<CoverMedia> = vec![];
381 let result =
382 distributor.distribute(&payload, &EmbeddingProfile::Standard, covers, &MockEmbedder);
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn pattern_from_profile_non_standard_preserves_distribution_topology() {
388 let adaptive = EmbeddingProfile::Adaptive {
389 max_detectability_db: 0.5,
390 };
391 let pattern = pattern_from_profile(&adaptive, 5, None);
392 assert_eq!(
393 pattern,
394 DistributionPattern::OneToMany {
395 data_shards: 4,
396 parity_shards: 1,
397 }
398 );
399
400 let corpus = EmbeddingProfile::CorpusBased;
401 let pattern = pattern_from_profile(&corpus, 10, None);
402 assert_eq!(pattern, DistributionPattern::OneToOne);
403 }
404
405 #[test]
406 fn distribute_via_trait_many_to_many_replicate() -> TestResult {
407 let covers = vec![make_cover(256), make_cover(256)];
408 let payload = Payload::from_bytes(vec![0xAA; 20]);
409 let result = distribute_many_to_many(
410 &payload,
411 covers,
412 &MockEmbedder,
413 crate::domain::types::ManyToManyMode::Stripe,
414 )?;
415 assert_eq!(result.len(), 2);
417 for cover in &result {
418 assert!(cover.data.len() > 256);
419 }
420 Ok(())
421 }
422
423 struct FailEmbedder;
425
426 impl EmbedTechnique for FailEmbedder {
427 fn technique(&self) -> StegoTechnique {
428 StegoTechnique::LsbImage
429 }
430
431 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
432 Ok(Capacity {
433 bytes: 0,
434 technique: StegoTechnique::LsbImage,
435 })
436 }
437
438 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
439 Err(StegoError::PayloadTooLarge {
440 available: 0,
441 needed: 1,
442 })
443 }
444 }
445
446 #[test]
447 fn distribute_one_to_one_embed_failure() {
448 let covers = vec![make_cover(64)];
449 let payload = Payload::from_bytes(b"data".to_vec());
450 let result = distribute_one_to_one(&payload, covers, &FailEmbedder);
451 assert!(result.is_err());
452 }
453
454 #[test]
455 fn distribute_one_to_many_embed_failure() {
456 let covers: Vec<CoverMedia> = (0..4).map(|_| make_cover(256)).collect();
457 let payload = Payload::from_bytes(vec![0xBB; 32]);
458 let corrector: Box<dyn ErrorCorrector> = Box::new(
459 crate::adapters::correction::RsErrorCorrector::new(b"test-hmac-key".to_vec()),
460 );
461 let result =
462 distribute_one_to_many(&payload, covers, &FailEmbedder, 3, 1, corrector.as_ref());
463 assert!(result.is_err());
464 }
465
466 #[test]
467 fn distribute_many_to_many_embed_failure() {
468 let covers = vec![make_cover(128), make_cover(128)];
469 let payload = Payload::from_bytes(vec![0xCC; 20]);
470 let result = distribute_many_to_many(
471 &payload,
472 covers,
473 &FailEmbedder,
474 crate::domain::types::ManyToManyMode::Replicate,
475 );
476 assert!(result.is_err());
477 }
478
479 #[test]
480 fn distribute_default_impl() -> TestResult {
481 let distributor = DistributorImpl::default();
482 let payload = Payload::from_bytes(b"hello".to_vec());
483 let covers = vec![make_cover(128)];
484 let result =
485 distributor.distribute(&payload, &EmbeddingProfile::Standard, covers, &MockEmbedder)?;
486 assert_eq!(result.len(), 1);
487 Ok(())
488 }
489
490 #[test]
491 fn pack_unpack_multiple_payloads_for_many_to_one() -> TestResult {
492 let payloads = vec![
493 Payload::from_bytes(b"payload_a".to_vec()),
494 Payload::from_bytes(b"payload_b".to_vec()),
495 Payload::from_bytes(b"payload_c".to_vec()),
496 ];
497 let packed = pack_many_payloads(&payloads);
498 let combined = Payload::from_bytes(packed);
499
500 let covers = vec![make_cover(1024)];
502 let result = distribute_one_to_one(&combined, covers, &MockEmbedder)?;
503 assert_eq!(result.len(), 1);
504
505 let stego_data = &result.first().ok_or("empty result")?.data;
507 let embedded_portion = stego_data.get(1024..).ok_or("slice out of bounds")?;
508 let unpacked = crate::domain::distribution::unpack_many_payloads(embedded_portion)?;
509 assert_eq!(unpacked.len(), 3);
510 assert_eq!(
511 unpacked.first().ok_or("index out of bounds")?.as_bytes(),
512 b"payload_a"
513 );
514 assert_eq!(
515 unpacked.get(1).ok_or("index out of bounds")?.as_bytes(),
516 b"payload_b"
517 );
518 assert_eq!(
519 unpacked.get(2).ok_or("index out of bounds")?.as_bytes(),
520 b"payload_c"
521 );
522 Ok(())
523 }
524}