use super::{
chunk_total_size, copy_chunk, info, read_all_chunks, validate_riff_header, write_chunk,
write_riff_header, CHUNK_HEADER_SIZE, LIST_CHUNK_ID,
};
use crate::core::error::{XmpError, XmpResult};
use crate::core::metadata::XmpMeta;
use crate::files::handler::{FileHandler, XmpOptions};
use std::io::{Read, Seek, SeekFrom, Write};
const WAVE_SIGNATURE: &[u8; 4] = b"WAVE";
const XMP_CHUNK_ID: &[u8; 4] = b"_PMX";
#[derive(Debug, Clone, Copy, Default)]
pub struct WavHandler;
impl FileHandler for WavHandler {
fn can_handle<R: Read + Seek>(&self, reader: &mut R) -> XmpResult<bool> {
let pos = reader.stream_position()?;
let file_len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(pos))?;
if file_len < 20 {
return Ok(false);
}
match validate_riff_header(reader) {
Ok(form_type) => {
reader.seek(SeekFrom::Start(pos))?;
Ok(&form_type == WAVE_SIGNATURE)
}
Err(_) => {
reader.seek(SeekFrom::Start(pos))?;
Ok(false)
}
}
}
fn read_xmp<R: Read + Seek>(
&self,
reader: &mut R,
options: &XmpOptions,
) -> XmpResult<Option<XmpMeta>> {
let form_type = validate_riff_header(reader)?;
if &form_type != WAVE_SIGNATURE {
return Err(XmpError::BadValue("Not a valid WAV file".to_string()));
}
let chunks = read_all_chunks(reader)?;
let mut meta = None;
if let Some(xmp_chunk) = chunks.iter().find(|c| c.id == *XMP_CHUNK_ID) {
reader.seek(SeekFrom::Start(xmp_chunk.offset + CHUNK_HEADER_SIZE))?;
let mut xmp_data = vec![0u8; xmp_chunk.size as usize];
reader.read_exact(&mut xmp_data)?;
let xmp_str = String::from_utf8(xmp_data)
.map_err(|e| XmpError::ParseError(format!("Invalid UTF-8 in XMP: {}", e)))?;
meta = Some(XmpMeta::parse(&xmp_str)?);
}
if options.only_xmp {
return Ok(meta);
}
let had_xmp = meta.is_some();
let mut xmp_meta = meta.unwrap_or_else(XmpMeta::new);
let mut reconciled = false;
for chunk in &chunks {
if chunk.id == *LIST_CHUNK_ID {
let info_items = info::read_info_list(reader, chunk)?;
if !info_items.is_empty() {
info::reconcile_to_xmp(&mut xmp_meta, &info_items);
reconciled = true;
}
}
}
if !had_xmp && !reconciled {
Ok(None)
} else {
Ok(Some(xmp_meta))
}
}
fn write_xmp<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
meta: &XmpMeta,
) -> XmpResult<()> {
let form_type = validate_riff_header(reader)?;
if &form_type != WAVE_SIGNATURE {
return Err(XmpError::BadValue("Not a valid WAV file".to_string()));
}
let xmp_packet = meta.serialize_packet()?;
let xmp_bytes = xmp_packet.as_bytes();
let chunks = read_all_chunks(reader)?;
let xmp_chunk = chunks.iter().find(|c| c.id == *XMP_CHUNK_ID);
let old_xmp_size = xmp_chunk.map(|c| c.total_size()).unwrap_or(0);
let new_xmp_size = chunk_total_size(xmp_bytes.len() as u32);
reader.seek(SeekFrom::Start(4))?;
let mut old_file_size_bytes = [0u8; 4];
reader.read_exact(&mut old_file_size_bytes)?;
let old_file_size = u32::from_le_bytes(old_file_size_bytes);
let new_file_size = if xmp_chunk.is_some() {
old_file_size - old_xmp_size as u32 + new_xmp_size as u32
} else {
old_file_size + new_xmp_size as u32
};
write_riff_header(writer, new_file_size, WAVE_SIGNATURE)?;
let mut xmp_written = false;
for chunk in &chunks {
if chunk.id == *XMP_CHUNK_ID {
write_chunk(writer, XMP_CHUNK_ID, xmp_bytes)?;
xmp_written = true;
continue;
}
copy_chunk(reader, writer, chunk)?;
}
if !xmp_written {
write_chunk(writer, XMP_CHUNK_ID, xmp_bytes)?;
}
Ok(())
}
fn format_name(&self) -> &'static str {
"WAV"
}
fn extensions(&self) -> &'static [&'static str] {
&["wav"]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::namespace::ns;
use crate::types::value::XmpValue;
use std::io::Cursor;
const FMT_CHUNK_ID: &[u8; 4] = b"fmt ";
const DATA_CHUNK_ID: &[u8; 4] = b"data";
fn create_minimal_wav() -> Vec<u8> {
let mut wav = Vec::new();
wav.extend_from_slice(b"RIFF");
let fmt_data: Vec<u8> = vec![
0x01, 0x00, 0x01, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x02, 0x00, 0x10, 0x00, ];
let data_chunk: Vec<u8> = vec![];
let file_size = 4 + 8 + fmt_data.len() + 8 + data_chunk.len();
wav.extend_from_slice(&(file_size as u32).to_le_bytes());
wav.extend_from_slice(WAVE_SIGNATURE);
wav.extend_from_slice(FMT_CHUNK_ID);
wav.extend_from_slice(&(fmt_data.len() as u32).to_le_bytes());
wav.extend_from_slice(&fmt_data);
wav.extend_from_slice(DATA_CHUNK_ID);
wav.extend_from_slice(&(data_chunk.len() as u32).to_le_bytes());
wav
}
fn create_wav_with_info() -> Vec<u8> {
let mut wav = Vec::new();
wav.extend_from_slice(b"RIFF");
let fmt_data: Vec<u8> = vec![
0x01, 0x00, 0x01, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x02, 0x00,
0x10, 0x00,
];
let mut info_data = Vec::new();
info_data.extend_from_slice(b"INFO");
info_data.extend_from_slice(b"INAM");
let title = b"Test Title\0";
info_data.extend_from_slice(&(title.len() as u32).to_le_bytes());
info_data.extend_from_slice(title);
if title.len() % 2 == 1 {
info_data.push(0);
}
info_data.extend_from_slice(b"IART");
let artist = b"Test Artist\0";
info_data.extend_from_slice(&(artist.len() as u32).to_le_bytes());
info_data.extend_from_slice(artist);
let data_chunk: Vec<u8> = vec![];
let file_size = 4 + 8 + fmt_data.len() + 8 + info_data.len() + 8 + data_chunk.len();
wav.extend_from_slice(&(file_size as u32).to_le_bytes());
wav.extend_from_slice(WAVE_SIGNATURE);
wav.extend_from_slice(FMT_CHUNK_ID);
wav.extend_from_slice(&(fmt_data.len() as u32).to_le_bytes());
wav.extend_from_slice(&fmt_data);
wav.extend_from_slice(LIST_CHUNK_ID);
wav.extend_from_slice(&(info_data.len() as u32).to_le_bytes());
wav.extend_from_slice(&info_data);
wav.extend_from_slice(DATA_CHUNK_ID);
wav.extend_from_slice(&(data_chunk.len() as u32).to_le_bytes());
wav
}
#[test]
fn test_can_handle_wav() {
let handler = WavHandler;
let wav_data = create_minimal_wav();
let mut reader = Cursor::new(wav_data);
assert!(handler.can_handle(&mut reader).unwrap());
}
#[test]
fn test_can_handle_non_wav() {
let handler = WavHandler;
let non_wav_data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
let mut reader = Cursor::new(non_wav_data);
assert!(!handler.can_handle(&mut reader).unwrap());
}
#[test]
fn test_read_xmp_no_xmp() {
let handler = WavHandler;
let wav_data = create_minimal_wav();
let mut reader = Cursor::new(wav_data);
let result = handler
.read_xmp(&mut reader, &XmpOptions::default())
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_read_info_reconcile() {
let handler = WavHandler;
let wav_data = create_wav_with_info();
let mut reader = Cursor::new(wav_data);
let result = handler
.read_xmp(&mut reader, &XmpOptions::default())
.unwrap();
assert!(result.is_some(), "Should have XMP from INFO reconciliation");
}
#[test]
fn test_write_and_read_xmp() {
let handler = WavHandler;
let wav_data = create_minimal_wav();
let mut reader = Cursor::new(wav_data);
let mut writer = Cursor::new(Vec::new());
let mut meta = XmpMeta::new();
meta.set_property(ns::DC, "title", XmpValue::String("Test WAV".to_string()))
.unwrap();
handler.write_xmp(&mut reader, &mut writer, &meta).unwrap();
writer.set_position(0);
let result = handler
.read_xmp(&mut writer, &XmpOptions::default().only_xmp())
.unwrap();
assert!(result.is_some());
}
#[test]
fn test_format_info() {
let handler = WavHandler;
assert_eq!(handler.format_name(), "WAV");
assert_eq!(handler.extensions(), &["wav"]);
}
}