use crate::error::{LupinError, Result};
use crate::SteganographyEngine;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
pub struct PngEngine;
impl PngEngine {
pub fn new() -> Self {
Self
}
const LUPIN_CHUNK_TYPE: &'static [u8] = b"lpNg";
const CRC32_INIT: u32 = 0xFFFFFFFF;
const CRC32_POLYNOMIAL: u32 = 0xEDB88320;
const CRC32_FINAL_XOR: u32 = 0xFFFFFFFF;
fn calculate_crc(chunk_type: &[u8], data: &[u8]) -> u32 {
let mut crc = Self::CRC32_INIT;
for &byte in chunk_type {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ Self::CRC32_POLYNOMIAL;
} else {
crc >>= 1;
}
}
}
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ Self::CRC32_POLYNOMIAL;
} else {
crc >>= 1;
}
}
}
crc ^ Self::CRC32_FINAL_XOR
}
fn find_iend_position(data: &[u8]) -> Result<usize> {
let mut pos = 8;
while pos + 8 <= data.len() {
let chunk_length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
let chunk_type = &data[pos + 4..pos + 8];
if chunk_type == b"IEND" {
return Ok(pos); }
pos += 4 + 4 + chunk_length + 4;
}
Err(LupinError::PngNoIdatChunk) }
fn create_chunk(chunk_type: &[u8], data: &[u8]) -> Vec<u8> {
let mut chunk = Vec::new();
chunk.extend_from_slice(&(data.len() as u32).to_be_bytes());
chunk.extend_from_slice(chunk_type);
chunk.extend_from_slice(data);
let crc = Self::calculate_crc(chunk_type, data);
chunk.extend_from_slice(&crc.to_be_bytes());
chunk
}
fn extract_custom_chunk(data: &[u8], chunk_type: &[u8]) -> Result<Vec<u8>> {
let mut pos = 8;
while pos + 8 <= data.len() {
let chunk_length =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
as usize;
let current_chunk_type = &data[pos + 4..pos + 8];
if current_chunk_type == chunk_type {
let data_start = pos + 8;
let data_end = data_start + chunk_length;
if data_end + 4 > data.len() {
return Err(LupinError::PngNoHiddenData);
}
let chunk_data = &data[data_start..data_end];
let stored_crc = u32::from_be_bytes([
data[data_end],
data[data_end + 1],
data[data_end + 2],
data[data_end + 3],
]);
let calculated_crc = Self::calculate_crc(chunk_type, chunk_data);
if stored_crc != calculated_crc {
return Err(LupinError::PngCorruptedData);
}
return Ok(chunk_data.to_vec());
}
pos += 4 + 4 + chunk_length + 4;
if current_chunk_type == b"IEND" {
break;
}
}
Err(LupinError::PngNoHiddenData)
}
}
impl Default for PngEngine {
fn default() -> Self {
Self::new()
}
}
impl SteganographyEngine for PngEngine {
fn magic_bytes(&self) -> &[u8] {
b"\x89PNG\r\n\x1a\n"
}
fn format_name(&self) -> &str {
"PNG"
}
fn format_ext(&self) -> &str {
"png"
}
fn embed(&self, source_data: &[u8], payload: &[u8]) -> Result<Vec<u8>> {
let iend_pos = Self::find_iend_position(source_data)?;
let encoded_payload = BASE64.encode(payload);
let steg_chunk = Self::create_chunk(Self::LUPIN_CHUNK_TYPE, encoded_payload.as_bytes());
let mut output = Vec::with_capacity(source_data.len() + steg_chunk.len());
output.extend_from_slice(&source_data[..iend_pos]);
output.extend_from_slice(&steg_chunk);
output.extend_from_slice(&source_data[iend_pos..]);
Ok(output)
}
fn extract(&self, source_data: &[u8]) -> Result<Vec<u8>> {
let encoded_data = Self::extract_custom_chunk(source_data, Self::LUPIN_CHUNK_TYPE)?;
BASE64
.decode(&encoded_data)
.map_err(|_| LupinError::PngCorruptedData)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_minimal_png() -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(b"\x89PNG\r\n\x1a\n");
png.extend_from_slice(&13u32.to_be_bytes()); png.extend_from_slice(b"IHDR"); png.extend_from_slice(&10u32.to_be_bytes()); png.extend_from_slice(&10u32.to_be_bytes()); png.push(8); png.push(2); png.push(0); png.push(0); png.push(0); png.extend_from_slice(&[0x9a, 0x76, 0x82, 0x70]);
let pixel_data = vec![0u8; 500]; png.extend_from_slice(&(pixel_data.len() as u32).to_be_bytes()); png.extend_from_slice(b"IDAT"); png.extend_from_slice(&pixel_data);
png.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
png.extend_from_slice(&0u32.to_be_bytes()); png.extend_from_slice(b"IEND"); png.extend_from_slice(&[0xae, 0x42, 0x60, 0x82]);
png
}
#[test]
fn test_magic_bytes() {
let engine = PngEngine::new();
let magic = engine.magic_bytes();
assert_eq!(magic, b"\x89PNG\r\n\x1a\n");
}
#[test]
fn test_format_name() {
let engine = PngEngine::new();
let name = engine.format_name();
assert_eq!(name, "PNG");
}
#[test]
fn test_format_ext() {
let engine = PngEngine::new();
let ext = engine.format_ext();
assert_eq!(ext, "png");
}
#[test]
fn test_find_iend_position() {
let png_data = create_minimal_png();
let result = PngEngine::find_iend_position(&png_data);
assert!(result.is_ok());
let iend_pos = result.unwrap();
assert!(iend_pos > 8, "IEND position should be after PNG signature");
assert_eq!(&png_data[iend_pos + 4..iend_pos + 8], b"IEND");
}
#[test]
fn test_find_iend_no_iend() {
let mut png = Vec::new();
png.extend_from_slice(b"\x89PNG\r\n\x1a\n");
png.extend_from_slice(&13u32.to_be_bytes());
png.extend_from_slice(b"IHDR");
png.extend_from_slice(&[0u8; 13]); png.extend_from_slice(&[0u8; 4]);
let result = PngEngine::find_iend_position(&png);
assert!(result.is_err());
match result {
Err(LupinError::PngNoIdatChunk) => (), other => panic!("Expected error, got {:?}", other),
}
}
#[test]
fn test_embed_success() {
let engine = PngEngine::new();
let source = create_minimal_png();
let payload = b"Hello, PNG steganography!";
let expected_size = source.len() + BASE64.encode(payload).len() + 12;
let result = engine.embed(&source, payload);
assert!(result.is_ok());
let embedded = result.unwrap();
assert!(
expected_size == embedded.len(),
"Expect output to grow by payload size plus chunk overhead"
);
assert!(embedded.starts_with(b"\x89PNG\r\n\x1a\n")); }
#[test]
fn test_embed_and_extract_round_trip() {
let engine = PngEngine::new();
let source = create_minimal_png();
let payload = b"Secret message hidden in PNG!";
let embedded = engine
.embed(&source, payload)
.expect("Embed should succeed");
let extracted = engine.extract(&embedded).expect("Extract should succeed");
assert_eq!(extracted, payload);
}
#[test]
fn test_extract_no_hidden_data() {
let engine = PngEngine::new();
let source = create_minimal_png();
let result = engine.extract(&source);
assert!(result.is_err(), "Should fail to find hidden data");
match result {
Err(LupinError::PngNoHiddenData) => (),
other => panic!("Expected PngNoHiddenData error, got {:?}", other),
}
}
#[test]
fn test_round_trip_with_binary_data() {
let engine = PngEngine::new();
let source = create_minimal_png();
let payload: Vec<u8> = (0..=255).cycle().take(100).collect();
let embedded = engine
.embed(&source, &payload)
.expect("Embed should succeed");
let extracted = engine.extract(&embedded).expect("Extract should succeed");
assert_eq!(extracted, payload);
}
#[test]
fn test_round_trip_with_empty_payload() {
let engine = PngEngine::new();
let source = create_minimal_png();
let payload = b"";
let result = engine.embed(&source, payload);
assert!(result.is_ok(), "Empty payload should be embeddable");
if let Ok(embedded) = result {
let extracted = engine.extract(&embedded).expect("Extract should succeed");
assert_eq!(extracted, payload, "Extracted empty payload should match");
}
}
#[test]
fn test_crc_calculation() {
let crc = PngEngine::calculate_crc(b"IEND", &[]);
assert_eq!(crc, 0xae426082);
}
}