use crate::config::{ParseOptions, WriteOptions};
use crate::error::{
FileEncodingError, FileParseError, SizeMismatchError, TagEncodingError, TooMuchDataError,
UnknownFormatError,
};
use crate::file::FileType;
use crate::iff::chunk::{Chunks, IFF_CHUNK_HEADER_SIZE};
use crate::iff::error::ChunkParseError;
use crate::io::{Length, Truncate, VerifiedFile};
use crate::tag::TagType;
use crate::util::io::FileLike;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::ops::Range;
use byteorder::{ByteOrder, WriteBytesExt};
const CHUNK_NAME_UPPER: [u8; 4] = *b"ID3 ";
const CHUNK_NAME_LOWER: [u8; 4] = *b"id3 ";
const JUNK_CHUNK_NAME: [u8; 4] = *b"JUNK";
fn handle_chunk_error(format: FileType, error: ChunkParseError) -> FileParseError {
FileParseError::new(format, error.into())
}
fn write_id3v2_chunk<W, B>(
tag: &[u8],
write_options: WriteOptions,
writer: &mut W,
) -> Result<(), FileEncodingError>
where
W: Write,
B: ByteOrder,
{
if write_options.uppercase_id3v2_chunk {
writer.write_all(&CHUNK_NAME_UPPER)?;
} else {
writer.write_all(&CHUNK_NAME_LOWER)?;
}
writer.write_u32::<B>(tag.len() as u32)?;
writer.write_all(tag)?;
if !tag.len().is_multiple_of(2) {
writer.write_u8(0)?;
}
Ok(())
}
pub(in crate::id3::v2) fn write_to_chunk_file<F, B>(
file: VerifiedFile<'_, F>,
tag: &[u8],
write_options: WriteOptions,
) -> Result<(), FileEncodingError>
where
F: FileLike,
B: ByteOrder,
{
let mut tag_chunk_size;
if tag.is_empty() {
tag_chunk_size = 0u64;
} else {
tag_chunk_size = (tag.len() + IFF_CHUNK_HEADER_SIZE as usize)
.try_into()
.map_err(|_| TagEncodingError::new(TagType::Id3v2, TooMuchDataError.into()))?;
}
if !tag.len().is_multiple_of(2) {
tag_chunk_size += 1;
}
let format = file.format();
let mut file = file.into_inner();
let file_len = file.len()?;
let mut file_context =
find_existing_id3v2_tag::<_, B>(&mut file, file_len, format, write_options.parse_options)?;
if let Some(existing_id3_tag) = file_context.existing_id3_tag.clone() {
let existing_tag_len = existing_id3_tag.end - existing_id3_tag.start;
if existing_id3_tag.end == file_context.stream_length
&& file_len == file_context.stream_length
{
let mut updated_stream_len = file_context.stream_length;
if tag.is_empty() {
updated_stream_len -= existing_tag_len;
file.truncate(existing_id3_tag.start)?;
} else {
file.seek(SeekFrom::Start(existing_id3_tag.start))?;
write_id3v2_chunk::<_, B>(tag, write_options, &mut file)?;
if existing_tag_len > tag_chunk_size {
let remainder = existing_tag_len - tag_chunk_size;
updated_stream_len -= remainder;
file.truncate(existing_id3_tag.end - remainder)?;
} else {
updated_stream_len -= tag_chunk_size - existing_tag_len;
}
}
file.seek(SeekFrom::Start(4))?;
file.write_u32::<B>((updated_stream_len as u32) - IFF_CHUNK_HEADER_SIZE)?;
return Ok(());
}
if format == FileType::Wav && write_options.preferred_padding.is_some() {
if tag.is_empty() {
log::debug!("No items to write, removing ID3v2 tag");
file.seek(SeekFrom::Start(existing_id3_tag.start))?;
file.write_all(&JUNK_CHUNK_NAME)?;
return Ok(());
}
if (existing_tag_len - u64::from(IFF_CHUNK_HEADER_SIZE)) >= tag_chunk_size {
let remainder = existing_tag_len - tag_chunk_size;
log::trace!("Existing tag large enough to overwrite (padding size: {remainder})");
file.seek(SeekFrom::Start(existing_id3_tag.start))?;
write_id3v2_chunk::<_, B>(tag, write_options, &mut file)?;
file.write_all(&JUNK_CHUNK_NAME)?;
file.write_u32::<B>((remainder - u64::from(IFF_CHUNK_HEADER_SIZE)) as u32)?;
return Ok(());
}
}
log::debug!("Existing ID3v2 tag too small to overwrite, rewriting whole file");
} else if tag.is_empty() {
log::debug!("No tag to overwrite, nothing to do");
return Ok(());
}
let mut file_bytes = Cursor::new(Vec::with_capacity(file_context.stream_length as usize));
file.rewind()?;
file.read_to_end(file_bytes.get_mut())?;
let mut tag_bytes = Vec::new();
if !tag.is_empty() {
write_id3v2_chunk::<_, B>(tag, write_options, &mut tag_bytes)?;
}
file_bytes.get_mut().splice(
file_context.stream_length as usize..file_context.stream_length as usize,
tag_bytes.iter().copied(),
);
if let Some(existing_id3_tag) = file_context.existing_id3_tag {
file_context.stream_length -= existing_id3_tag.end - existing_id3_tag.start;
file_bytes
.get_mut()
.drain(existing_id3_tag.start as usize..existing_id3_tag.end as usize);
}
file_context.stream_length += tag_chunk_size;
file_bytes.seek(SeekFrom::Start(4))?;
file_bytes.write_u32::<B>((file_context.stream_length as u32) - IFF_CHUNK_HEADER_SIZE)?;
file.rewind()?;
file.truncate(0)?;
file.write_all(file_bytes.get_ref())?;
Ok(())
}
struct IffFileContext {
stream_length: u64,
existing_id3_tag: Option<Range<u64>>,
}
fn find_existing_id3v2_tag<F, B: ByteOrder>(
file: &mut F,
file_len: u64,
format: FileType,
parse_options: ParseOptions,
) -> Result<IffFileContext, FileParseError>
where
F: FileLike,
{
const FIRST_CHUNK_LEN: u64 = (IFF_CHUNK_HEADER_SIZE as u64) + 4;
let mut chunks = Chunks::<_, B>::new(file, file_len);
let Some(first_chunk) = chunks
.next(parse_options.parsing_mode)
.map_err(|e| handle_chunk_error(format, e))?
else {
return Err(UnknownFormatError.into());
};
let actual_stream_size = u64::from(first_chunk.size()) + u64::from(IFF_CHUNK_HEADER_SIZE);
if file_len < actual_stream_size {
return Err(SizeMismatchError.into());
}
if file_len > actual_stream_size {
log::warn!(
"File size is larger than expected stream size ({file_len} bytes vs \
{actual_stream_size} bytes), there may be trailing junk data!"
);
}
let mut context = IffFileContext {
stream_length: actual_stream_size,
existing_id3_tag: None,
};
let file = chunks.into_inner();
file.seek(SeekFrom::Start(FIRST_CHUNK_LEN))?;
let mut chunks = Chunks::<_, B>::new(
file,
context.stream_length - u64::from(IFF_CHUNK_HEADER_SIZE),
);
while let Some(chunk) = chunks
.next(parse_options.parsing_mode)
.map_err(|e| handle_chunk_error(format, e))?
{
if chunk.fourcc == CHUNK_NAME_UPPER || chunk.fourcc == CHUNK_NAME_LOWER {
let chunk_start = chunk.start() + FIRST_CHUNK_LEN;
chunks.skip().map_err(|e| handle_chunk_error(format, e))?;
let chunk_end = chunks.stream_position() + FIRST_CHUNK_LEN;
log::debug!(
"Found existing ID3v2 chunk, size: {} bytes",
chunk_end - chunk_start
);
context.existing_id3_tag = Some(chunk_start..chunk_end);
break;
}
}
Ok(context)
}