shadowforge_lib/adapters/
deadrop.rs1use crate::domain::errors::DeadDropError;
4use crate::domain::ports::{DeadDropEncoder, EmbedTechnique};
5use crate::domain::types::{CoverMedia, Payload, PlatformProfile};
6
7pub struct DeadDropEncoderImpl;
13
14impl DeadDropEncoderImpl {
15 #[must_use]
17 pub const fn new() -> Self {
18 Self
19 }
20}
21
22impl Default for DeadDropEncoderImpl {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl DeadDropEncoder for DeadDropEncoderImpl {
29 fn encode_for_platform(
30 &self,
31 cover: CoverMedia,
32 payload: &Payload,
33 platform: &PlatformProfile,
34 technique: &dyn EmbedTechnique,
35 ) -> Result<CoverMedia, DeadDropError> {
36 let capacity = technique
38 .capacity(&cover)
39 .map_err(|e| DeadDropError::EncodeFailed {
40 reason: format!("capacity check failed: {e}"),
41 })?;
42
43 if capacity.bytes < payload.as_bytes().len() as u64 {
44 return Err(DeadDropError::EncodeFailed {
45 reason: format!(
46 "payload ({} bytes) exceeds cover capacity ({} bytes) for platform {platform:?}",
47 payload.as_bytes().len(),
48 capacity.bytes,
49 ),
50 });
51 }
52
53 let stego_cover =
59 technique
60 .embed(cover, payload)
61 .map_err(|e| DeadDropError::EncodeFailed {
62 reason: format!("embedding failed for platform {platform:?}: {e}"),
63 })?;
64
65 Ok(stego_cover)
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::domain::errors::StegoError;
73 use crate::domain::types::{Capacity, CoverMediaKind, StegoTechnique};
74 use bytes::Bytes;
75 use std::collections::HashMap;
76
77 type TestResult = Result<(), Box<dyn std::error::Error>>;
78
79 struct MockEmbedder {
80 cap: u64,
81 fail_embed: bool,
82 }
83
84 impl EmbedTechnique for MockEmbedder {
85 fn technique(&self) -> StegoTechnique {
86 StegoTechnique::LsbImage
87 }
88
89 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
90 Ok(Capacity {
91 bytes: self.cap,
92 technique: StegoTechnique::LsbImage,
93 })
94 }
95
96 fn embed(
97 &self,
98 mut cover: CoverMedia,
99 payload: &Payload,
100 ) -> Result<CoverMedia, StegoError> {
101 if self.fail_embed {
102 return Err(StegoError::MalformedCoverData {
103 reason: "mock failure".into(),
104 });
105 }
106 let mut data = cover.data.to_vec();
107 data.extend_from_slice(payload.as_bytes());
108 cover.data = Bytes::from(data);
109 Ok(cover)
110 }
111 }
112
113 fn make_cover() -> CoverMedia {
114 CoverMedia {
115 kind: CoverMediaKind::PngImage,
116 data: Bytes::from(vec![0u8; 512]),
117 metadata: HashMap::new(),
118 }
119 }
120
121 #[test]
122 fn encode_for_telegram_succeeds() -> TestResult {
123 let encoder = DeadDropEncoderImpl::new();
124 let cover = make_cover();
125 let payload = Payload::from_bytes(vec![1, 2, 3, 4]);
126 let mock = MockEmbedder {
127 cap: 1024,
128 fail_embed: false,
129 };
130
131 let result =
132 encoder.encode_for_platform(cover, &payload, &PlatformProfile::Telegram, &mock);
133
134 assert!(result.is_ok());
135 let stego = result?;
136 assert!(stego.data.len() > 512);
138 Ok(())
139 }
140
141 #[test]
142 fn encode_for_instagram_succeeds() {
143 let encoder = DeadDropEncoderImpl::new();
144 let cover = make_cover();
145 let payload = Payload::from_bytes(vec![5, 6, 7]);
146 let mock = MockEmbedder {
147 cap: 1024,
148 fail_embed: false,
149 };
150
151 let result =
152 encoder.encode_for_platform(cover, &payload, &PlatformProfile::Instagram, &mock);
153
154 assert!(result.is_ok());
155 }
156
157 #[test]
158 fn encode_fails_when_payload_exceeds_capacity() {
159 let encoder = DeadDropEncoderImpl::new();
160 let cover = make_cover();
161 let payload = Payload::from_bytes(vec![0u8; 100]);
162 let mock = MockEmbedder {
163 cap: 10, fail_embed: false,
165 };
166
167 let result = encoder.encode_for_platform(cover, &payload, &PlatformProfile::Twitter, &mock);
168
169 assert!(matches!(result, Err(DeadDropError::EncodeFailed { .. })));
170 }
171
172 #[test]
173 fn encode_fails_when_embedder_fails() {
174 let encoder = DeadDropEncoderImpl::new();
175 let cover = make_cover();
176 let payload = Payload::from_bytes(vec![1, 2, 3]);
177 let mock = MockEmbedder {
178 cap: 1024,
179 fail_embed: true,
180 };
181
182 let result = encoder.encode_for_platform(cover, &payload, &PlatformProfile::Imgur, &mock);
183
184 assert!(matches!(result, Err(DeadDropError::EncodeFailed { .. })));
185 }
186
187 #[test]
188 fn encode_works_with_custom_platform() {
189 let encoder = DeadDropEncoderImpl::new();
190 let cover = make_cover();
191 let payload = Payload::from_bytes(vec![10, 20, 30]);
192 let mock = MockEmbedder {
193 cap: 1024,
194 fail_embed: false,
195 };
196 let platform = PlatformProfile::Custom {
197 quality: 75,
198 subsampling: crate::domain::types::ChromaSubsampling::Yuv422,
199 };
200
201 let result = encoder.encode_for_platform(cover, &payload, &platform, &mock);
202 assert!(result.is_ok());
203 }
204}