use uuid::Uuid;
use crate::domain::canary::{generate_canary_shard, is_canary_shard};
use crate::domain::errors::CanaryError;
use crate::domain::ports::{CanaryService, EmbedTechnique};
use crate::domain::types::{CanaryShard, CoverMedia, Payload};
pub struct CanaryServiceImpl {
shard_size: usize,
total_shards: u8,
}
impl CanaryServiceImpl {
#[must_use]
pub const fn new(shard_size: usize, total_shards: u8) -> Self {
Self {
shard_size,
total_shards,
}
}
#[must_use]
pub fn verify_canary(shard: &crate::domain::types::Shard, canary_id: Uuid) -> bool {
is_canary_shard(shard, canary_id)
}
}
impl CanaryService for CanaryServiceImpl {
fn embed_canary(
&self,
covers: Vec<CoverMedia>,
embedder: &dyn EmbedTechnique,
) -> Result<(Vec<CoverMedia>, CanaryShard), CanaryError> {
if covers.is_empty() {
return Err(CanaryError::NoCovers);
}
let canary_id = Uuid::new_v4();
let canary = generate_canary_shard(canary_id, self.shard_size, self.total_shards, None);
let canary_payload = Payload::from_bytes(canary.shard.data.clone());
let mut result_covers = Vec::with_capacity(covers.len());
let mut placed = false;
for cover in covers {
if !placed
&& let Ok(cap) = embedder.capacity(&cover)
&& cap.bytes >= canary_payload.as_bytes().len() as u64
{
if let Ok(stego_cover) = embedder.embed(cover, &canary_payload) {
result_covers.push(stego_cover);
placed = true;
continue;
}
continue;
}
result_covers.push(cover);
}
if !placed {
return Err(CanaryError::EmbedFailed {
source: crate::domain::errors::StegoError::PayloadTooLarge {
available: 0,
needed: canary_payload.as_bytes().len() as u64,
},
});
}
Ok((result_covers, canary))
}
fn check_canary(&self, shard: &CanaryShard) -> bool {
let Some(url) = &shard.notify_url else {
return false;
};
if !url.starts_with("https://") {
return false;
}
let _ = url;
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::errors::StegoError;
use crate::domain::types::{Capacity, CoverMedia, CoverMediaKind, StegoTechnique};
use bytes::Bytes;
use std::collections::HashMap;
type TestResult = Result<(), Box<dyn std::error::Error>>;
struct MockEmbedder {
capacity_bytes: u64,
should_fail: bool,
}
impl EmbedTechnique for MockEmbedder {
fn technique(&self) -> StegoTechnique {
StegoTechnique::LsbImage
}
fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
Ok(Capacity {
bytes: self.capacity_bytes,
technique: StegoTechnique::LsbImage,
})
}
fn embed(
&self,
mut cover: CoverMedia,
payload: &Payload,
) -> Result<CoverMedia, StegoError> {
if self.should_fail {
return Err(StegoError::PayloadTooLarge {
available: 0,
needed: payload.as_bytes().len() as u64,
});
}
let mut new_data = cover.data.to_vec();
new_data.extend_from_slice(payload.as_bytes());
cover.data = Bytes::from(new_data);
Ok(cover)
}
}
fn make_cover() -> CoverMedia {
CoverMedia {
kind: CoverMediaKind::PngImage,
data: Bytes::from(vec![0u8; 1024]),
metadata: HashMap::new(),
}
}
#[test]
fn embed_canary_returns_canary_shard() -> TestResult {
let service = CanaryServiceImpl::new(64, 5);
let embedder = MockEmbedder {
capacity_bytes: 1024,
should_fail: false,
};
let covers = vec![make_cover()];
let (result_covers, canary) = service.embed_canary(covers, &embedder)?;
assert_eq!(result_covers.len(), 1);
assert_eq!(canary.shard.data.len(), 64);
assert_eq!(canary.shard.index, 5); Ok(())
}
#[test]
fn embed_canary_fails_on_empty_covers() {
let service = CanaryServiceImpl::new(64, 5);
let embedder = MockEmbedder {
capacity_bytes: 1024,
should_fail: false,
};
let result = service.embed_canary(vec![], &embedder);
assert!(matches!(result, Err(CanaryError::NoCovers)));
}
#[test]
fn embed_canary_fails_when_no_cover_has_capacity() {
let service = CanaryServiceImpl::new(64, 5);
let embedder = MockEmbedder {
capacity_bytes: 0,
should_fail: false,
};
let covers = vec![make_cover()];
let result = service.embed_canary(covers, &embedder);
assert!(matches!(result, Err(CanaryError::EmbedFailed { .. })));
}
#[test]
fn embed_canary_skips_failing_cover_tries_next() -> TestResult {
let service = CanaryServiceImpl::new(64, 5);
let embedder = MockEmbedder {
capacity_bytes: 1024,
should_fail: false,
};
let covers = vec![make_cover(), make_cover()];
let (result_covers, canary) = service.embed_canary(covers, &embedder)?;
assert_eq!(result_covers.len(), 2);
assert!(!canary.shard.data.is_empty());
Ok(())
}
#[test]
fn canary_shard_not_in_original_covers() -> TestResult {
let service = CanaryServiceImpl::new(64, 5);
let embedder = MockEmbedder {
capacity_bytes: 1024,
should_fail: false,
};
let original_cover = make_cover();
let original_data = original_cover.data.clone();
let covers = vec![original_cover];
let (result_covers, _canary) = service.embed_canary(covers, &embedder)?;
assert_ne!(
result_covers.first().ok_or("index out of bounds")?.data,
original_data
);
Ok(())
}
#[test]
fn check_canary_returns_false_without_notify_url() {
let service = CanaryServiceImpl::new(64, 5);
let canary = generate_canary_shard(Uuid::new_v4(), 64, 5, None);
assert!(!service.check_canary(&canary));
}
#[test]
fn check_canary_returns_false_for_non_https_url() {
let service = CanaryServiceImpl::new(64, 5);
let canary = generate_canary_shard(
Uuid::new_v4(),
64,
5,
Some("http://insecure.example.com/canary".to_owned()),
);
assert!(!service.check_canary(&canary));
}
#[test]
fn check_canary_returns_false_for_unreachable_url() {
let service = CanaryServiceImpl::new(64, 5);
let canary = generate_canary_shard(
Uuid::new_v4(),
64,
5,
Some("https://nonexistent.example.invalid/canary".to_owned()),
);
assert!(!service.check_canary(&canary));
}
#[test]
fn verify_canary_detects_canary_shard() {
let canary_id = Uuid::new_v4();
let canary = generate_canary_shard(canary_id, 64, 5, None);
assert!(CanaryServiceImpl::verify_canary(&canary.shard, canary_id));
}
#[test]
fn verify_canary_rejects_normal_shard() {
let canary_id = Uuid::new_v4();
let normal = crate::domain::types::Shard {
index: 0,
total: 5,
data: vec![42u8; 64],
hmac_tag: [0u8; 32],
};
assert!(!CanaryServiceImpl::verify_canary(&normal, canary_id));
}
}