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