use crate::config::WriteOptions;
use crate::error::{FileEncodingError, FileParseError, TagEncodingError, TooMuchDataError};
use crate::file::FileType;
use crate::io::{Truncate, VerifiedFile};
use crate::macros::try_vec;
use crate::ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD};
use crate::ogg::tag::error::VorbisCommentsEncodingError;
use crate::ogg::tag::{VorbisCommentsRef, create_vorbis_comments_ref};
use crate::ogg::verify_signature;
use crate::picture::{Picture, PictureInformation};
use crate::tag::Tag;
use crate::util::io::FileLike;
use std::borrow::Cow;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use ogg_pager::{CONTAINS_FIRST_PAGE_OF_BITSTREAM, Packets, Page, PageHeader};
pub(crate) fn write_to<F>(
file: VerifiedFile<'_, F>,
tag: &Tag,
write_options: WriteOptions,
) -> Result<(), FileEncodingError>
where
F: FileLike,
{
let (vendor, items, pictures) = create_vorbis_comments_ref(tag);
let mut comments_ref = VorbisCommentsRef {
vendor: Cow::from(vendor),
items,
pictures,
};
write(file, &mut comments_ref, write_options)
}
pub(in crate::ogg) fn write<'a, F, II, IP>(
file: VerifiedFile<'_, F>,
tag: &mut VorbisCommentsRef<'a, II, IP>,
_write_options: WriteOptions,
) -> Result<(), FileEncodingError>
where
F: FileLike,
II: Iterator<Item = (&'a str, &'a str)>,
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
{
let format = file.format();
let (header_packet_count, comment_signature) = match format {
FileType::Opus => (2, Some(OPUSTAGS)),
FileType::Vorbis => (3, Some(VORBIS_COMMENT_HEAD)),
FileType::Speex => (2, None),
_ => unreachable!("file type verified beforehand"),
};
let mut file = file.into_inner();
let start = file.stream_position()?;
let first_page_header = PageHeader::read(&mut file).map_err(FileParseError::from)?;
let stream_serial = first_page_header.stream_serial;
file.seek(SeekFrom::Start(start))?;
let mut packets =
Packets::read_count(&mut file, header_packet_count).map_err(FileParseError::from)?;
let mut remaining_file_content = Vec::new();
file.read_to_end(&mut remaining_file_content)?;
let comment_packet = packets
.get(1)
.ok_or_else(|| FileParseError::message(None, "missing comment packet"))?;
if let Some(comment_signature) = comment_signature {
verify_signature(comment_packet, comment_signature)?;
}
let comment_signature = comment_signature.unwrap_or_default();
let md_reader = &mut &comment_packet[comment_signature.len()..];
let vendor_len = md_reader.read_u32::<LittleEndian>()?;
let mut vendor = try_vec![0; vendor_len as usize]?;
md_reader.read_exact(&mut vendor)?;
let vendor_str;
match String::from_utf8(vendor) {
Ok(s) => vendor_str = Cow::Owned(s),
Err(_) => {
log::warn!("OGG vendor string is not valid UTF-8, not re-using");
vendor_str = Cow::Borrowed("");
},
}
tag.vendor = vendor_str;
let add_framing_bit = format == FileType::Vorbis;
let new_metadata_packet = create_metadata_packet(tag, comment_signature, add_framing_bit)
.map_err(TagEncodingError::from)?;
packets.set(1, new_metadata_packet);
file.rewind()?;
file.truncate(0)?;
let pages_written = packets
.write_to(
&mut file,
stream_serial,
0,
CONTAINS_FIRST_PAGE_OF_BITSTREAM,
)
.map_err(|e| FileEncodingError::new(format, e.into()))? as u32;
let mut pages_reader = Cursor::new(&remaining_file_content[..]);
let mut idx = 0;
while let Ok(mut page) = Page::read(&mut pages_reader) {
let header = page.header_mut();
header.sequence_number = pages_written + idx;
page.gen_crc();
file.write_all(&page.as_bytes())?;
idx += 1;
}
Ok(())
}
pub(in crate::ogg) fn create_metadata_packet<'a, II, IP>(
tag: &mut VorbisCommentsRef<'a, II, IP>,
comment_signature: &[u8],
add_framing_bit: bool,
) -> Result<Vec<u8>, VorbisCommentsEncodingError>
where
II: Iterator<Item = (&'a str, &'a str)>,
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
{
let mut new_comment_packet = Cursor::new(Vec::new());
let vendor_bytes = tag.vendor.as_bytes();
new_comment_packet.write_all(comment_signature)?;
new_comment_packet.write_u32::<LittleEndian>(vendor_bytes.len() as u32)?;
new_comment_packet.write_all(vendor_bytes)?;
let item_count_pos = new_comment_packet.stream_position()?;
new_comment_packet.write_u32::<LittleEndian>(0)?;
let mut count = 0;
create_comments(&mut new_comment_packet, &mut count, &mut tag.items)?;
create_pictures(&mut new_comment_packet, &mut count, &mut tag.pictures)?;
new_comment_packet.seek(SeekFrom::Start(item_count_pos))?;
new_comment_packet.write_u32::<LittleEndian>(count)?;
if add_framing_bit {
new_comment_packet.get_mut().push(1);
}
Ok(new_comment_packet.into_inner())
}
pub(crate) fn create_comments(
packet: &mut impl Write,
count: &mut u32,
items: &mut dyn Iterator<Item = (&str, &str)>,
) -> Result<(), VorbisCommentsEncodingError> {
for (k, v) in items {
if v.is_empty() {
continue;
}
let comment = format!("{k}={v}");
let comment_bytes = comment.as_bytes();
let Ok(bytes_len) = u32::try_from(comment_bytes.len()) else {
return Err(TooMuchDataError.into());
};
*count += 1;
packet.write_u32::<LittleEndian>(bytes_len)?;
packet.write_all(comment_bytes)?;
}
Ok(())
}
fn create_pictures(
packet: &mut impl Write,
count: &mut u32,
pictures: &mut dyn Iterator<Item = (&Picture, PictureInformation)>,
) -> Result<(), VorbisCommentsEncodingError> {
const PICTURE_KEY: &str = "METADATA_BLOCK_PICTURE=";
for (pic, info) in pictures {
let picture = pic.as_flac_bytes(info, true);
let Ok(bytes_len) = u32::try_from(picture.len() + PICTURE_KEY.len()) else {
return Err(TooMuchDataError.into());
};
*count += 1;
packet.write_u32::<LittleEndian>(bytes_len)?;
packet.write_all(PICTURE_KEY.as_bytes())?;
packet.write_all(&picture)?;
}
Ok(())
}