use crate::error::DecayError;
use crate::format::{FileType, Header, AUDIO_BITS_PER_SAMPLE};
use std::fs;
use std::io::Write;
use std::path::Path;
pub fn decode_image(source_bytes: &[u8], input: &Path) -> Result<(u32, u32, Vec<u8>), DecayError> {
let decoded =
image::load_from_memory(source_bytes).map_err(|error| DecayError::ImageDecode {
context: format!("encode: decode image '{}': {}", input.display(), error),
})?;
let rgba = decoded.to_rgba8();
let (width, height) = rgba.dimensions();
Ok((width, height, rgba.into_raw()))
}
pub fn decode_audio(source_bytes: &[u8], input: &Path) -> Result<(u32, u8, Vec<u8>), DecayError> {
use symphonia::core::codecs::audio::AudioDecoderOptions;
use symphonia::core::formats::probe::Hint;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
let decode_error = |message: String| DecayError::AudioDecode {
context: format!("encode: decode audio '{}': {}", input.display(), message),
};
let stream = MediaSourceStream::new(
Box::new(std::io::Cursor::new(source_bytes.to_vec())),
Default::default(),
);
let mut hint = Hint::new();
if let Some(extension) = input.extension().and_then(|raw| raw.to_str()) {
hint.with_extension(extension);
}
let mut format = symphonia::default::get_probe()
.probe(
&hint,
stream,
FormatOptions::default(),
MetadataOptions::default(),
)
.map_err(|error| decode_error(error.to_string()))?;
let track = format
.default_track(symphonia::core::formats::TrackType::Audio)
.ok_or_else(|| decode_error("no audio track found".to_string()))?;
let track_id = track.id;
let codec_params = track
.codec_params
.as_ref()
.and_then(|params| params.audio())
.ok_or_else(|| decode_error("track has no audio parameters".to_string()))?;
let mut decoder = symphonia::default::get_codecs()
.make_audio_decoder(codec_params, &AudioDecoderOptions::default())
.map_err(|error| decode_error(error.to_string()))?;
let mut sample_rate = codec_params.sample_rate.unwrap_or(0);
let mut channels = codec_params
.channels
.as_ref()
.map(|channels| channels.count())
.unwrap_or(0);
let mut payload: Vec<u8> = Vec::new();
let mut packet_bytes: Vec<u8> = Vec::new();
while let Some(packet) = format
.next_packet()
.map_err(|error| decode_error(error.to_string()))?
{
if packet.track_id != track_id {
continue;
}
let decoded = decoder
.decode(&packet)
.map_err(|error| decode_error(error.to_string()))?;
let spec = decoded.spec();
if sample_rate == 0 {
sample_rate = spec.rate();
}
if channels == 0 {
channels = spec.channels().count();
}
decoded.copy_bytes_to_vec_interleaved_as::<i16>(&mut packet_bytes);
payload.extend_from_slice(&packet_bytes);
}
if sample_rate == 0 || channels == 0 {
return Err(decode_error(
"missing sample rate or channel count".to_string(),
));
}
if payload.is_empty() {
return Err(decode_error("no audio samples decoded".to_string()));
}
let channels = u8::try_from(channels).map_err(|_| {
decode_error(format!(
"{channels} channels exceeds the 255 the header holds"
))
})?;
Ok((sample_rate, channels, payload))
}
pub fn text_payload(source_bytes: Vec<u8>) -> Result<Vec<u8>, DecayError> {
match std::str::from_utf8(&source_bytes) {
Ok(_) => Ok(source_bytes),
Err(_) => Err(DecayError::InvalidUtf8),
}
}
pub fn write_decayfmt(output: &Path, header: Header, payload: &[u8]) -> Result<(), DecayError> {
let write_error = |error| DecayError::Io {
context: format!("encode: write output '{}'", output.display()),
source: error,
};
let mut file = fs::File::create(output).map_err(write_error)?;
file.write_all(&header.write()).map_err(write_error)?;
file.write_all(payload).map_err(write_error)
}
pub fn encode_file(input: &Path, output: &Path) -> Result<(), DecayError> {
let (file_type, _x) = crate::format::parse_filename(output)?;
let source_bytes = fs::read(input).map_err(|error| DecayError::Io {
context: format!("encode: read input '{}'", input.display()),
source: error,
})?;
build_and_write(file_type, source_bytes, input, output)
}
pub fn encode_bytes(source: &[u8], output: &Path) -> Result<(), DecayError> {
let (file_type, _x) = crate::format::parse_filename(output)?;
build_and_write(file_type, source.to_vec(), output, output)
}
fn build_and_write(
file_type: FileType,
source_bytes: Vec<u8>,
source_path: &Path,
output: &Path,
) -> Result<(), DecayError> {
let (header, payload) = match file_type {
FileType::Image => {
let (width, height, payload) = decode_image(&source_bytes, source_path)?;
(Header::for_image(width, height), payload)
}
FileType::Text => (Header::for_text(), text_payload(source_bytes)?),
FileType::Audio => {
let (sample_rate, channels, payload) = decode_audio(&source_bytes, source_path)?;
(
Header::for_audio(sample_rate, channels, AUDIO_BITS_PER_SAMPLE),
payload,
)
}
};
write_decayfmt(output, header, &payload)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::{ImageDimensions, FILE_TYPE_TEXT, HEADER_SIZE, MAGIC, VERSION};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn unique_temp_path(suffix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("decayfmt_test_{}_{}", nanos, suffix))
}
#[test]
fn encode_text_writes_header_and_exact_payload() {
let source = b"the quick brown fox jumps over the lazy dog";
let input = unique_temp_path("source.txt");
let output = unique_temp_path("note.tdcy3");
fs::write(&input, source).expect("write test source");
encode_file(&input, &output).expect("encode text should succeed");
let written = fs::read(&output).expect("read encoded file");
assert_eq!(&written[0..4], &MAGIC, "magic bytes must be DCYF");
assert_eq!(written[4], VERSION, "version byte must match");
assert_eq!(written[5], FILE_TYPE_TEXT, "file_type byte must be text");
assert_eq!(
&written[HEADER_SIZE..],
source,
"text payload must match source bytes exactly"
);
let _ = fs::remove_file(&input);
let _ = fs::remove_file(&output);
}
#[test]
fn encode_image_payload_length_matches_dimensions() {
let width = 2u32;
let height = 2u32;
let source_image = image::RgbaImage::from_fn(width, height, |x, y| {
image::Rgba([x as u8 * 10, y as u8 * 10, 20, 255])
});
let input = unique_temp_path("source.png");
let output = unique_temp_path("photo.idcy3");
source_image.save(&input).expect("save test png");
encode_file(&input, &output).expect("encode image should succeed");
let written = fs::read(&output).expect("read encoded file");
assert_eq!(&written[0..4], &MAGIC, "magic bytes must be DCYF");
assert_eq!(written[4], VERSION, "version byte must match");
let payload_len = written.len() - HEADER_SIZE;
assert_eq!(
payload_len,
(width * height * 4) as usize,
"image payload must be width * height * 4 bytes"
);
let header = Header::read(&written).expect("encoded header must parse");
assert_eq!(
header.image_dimensions(),
Some(ImageDimensions { width, height }),
"header must record the source image dimensions"
);
let _ = fs::remove_file(&input);
let _ = fs::remove_file(&output);
}
}