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