lofty 0.2.0

Unified IO for different types of audio metadata
Documentation
use crate::vorbis_tag::VORBIS;
use crate::{Error, Picture, Result};

#[cfg(feature = "format-flac")]
use metaflac::BlockType;
#[cfg(feature = "format-vorbis")]
use ogg::PacketWriteEndInfo;
use std::borrow::{BorrowMut, Cow};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};

#[cfg(any(feature = "format-vorbis", feature = "format-opus"))]
pub(crate) fn vorbis_generic(
	file: &mut File,
	sig: &[u8],
	vendor: &str,
	comments: &HashMap<String, String>,
) -> Result<()> {
	let mut packet = Vec::new();
	packet.extend(sig.iter());

	let comments: Vec<(String, String)> = comments
		.iter()
		.map(|(a, b)| (a.to_string(), b.to_string()))
		.collect();

	let vendor_len = vendor.len() as u32;
	packet.extend(vendor_len.to_le_bytes().iter());
	packet.extend(vendor.as_bytes().iter());

	let comments_len = comments.len() as u32;
	packet.extend(comments_len.to_le_bytes().iter());

	let mut comment_str = Vec::new();

	for (a, b) in comments {
		comment_str.push(format!("{}={}", a, b));
		let last = comment_str.last().unwrap();
		let len = last.as_bytes().len() as u32;
		packet.extend(len.to_le_bytes().iter());
		packet.extend(last.as_bytes().iter());
	}

	if sig == VORBIS {
		packet.push(1);
	}

	let mut file_bytes = Vec::new();
	file.read_to_end(&mut file_bytes)?;

	let data = if sig == VORBIS {
		ogg(Cursor::new(file_bytes), &*packet)?
	} else {
		opus(Cursor::new(file_bytes), &*packet)?
	};

	file.seek(SeekFrom::Start(0))?;
	file.set_len(0)?;
	file.write_all(&data)?;

	Ok(())
}

#[cfg(feature = "format-flac")]
pub(crate) fn flac<T>(
	mut data: T,
	vendor: &str,
	comments: &HashMap<String, String>,
	pictures: &Option<Cow<'static, [Picture]>>,
) -> Result<()>
where
	T: Read + Write + Seek,
{
	let mut tag = metaflac::Tag::read_from(&mut data)?;

	tag.remove_blocks(BlockType::VorbisComment);
	tag.remove_blocks(BlockType::Picture);

	let mut padding = None;

	if let Some(pad) = tag.get_blocks(BlockType::Padding).last() {
		padding = Some(pad.clone());
		tag.remove_blocks(BlockType::Padding)
	}

	let mut pics_final: Vec<metaflac::Block> = Vec::new();
	let mut comment_collection: HashMap<String, Vec<String>> = HashMap::new();

	if let Some(pics) = pictures.clone() {
		for pic in pics.iter() {
			pics_final.push(metaflac::Block::Picture(
				metaflac::block::Picture::from_bytes(&*pic.as_apic_bytes())?,
			))
		}
	}

	for (k, v) in comments.clone() {
		comment_collection.insert(k, vec![v]);
	}

	let comments = metaflac::Block::VorbisComment(metaflac::block::VorbisComment {
		vendor_string: vendor.to_string(),
		comments: comment_collection,
	});

	tag.push_block(comments);

	for pic in pics_final {
		tag.push_block(pic)
	}

	if let Some(padding) = padding {
		tag.push_block(padding)
	}

	data.seek(SeekFrom::Start(0))?;

	tag.write_to(&mut data)?;

	Ok(())
}

#[cfg(feature = "format-vorbis")]
pub(crate) fn ogg<T>(data: T, packet: &[u8]) -> Result<Vec<u8>>
where
	T: Read + Seek,
{
	let mut c = Cursor::new(Vec::new());

	let mut reader = ogg::PacketReader::new(data);
	let mut writer = ogg::PacketWriter::new(&mut c);

	let mut replaced = false;

	loop {
		match reader.read_packet()? {
			None => break,
			Some(mut p) => {
				let inf = if p.last_in_stream() {
					PacketWriteEndInfo::EndStream
				} else if p.last_in_page() {
					PacketWriteEndInfo::EndPage
				} else {
					PacketWriteEndInfo::NormalPacket
				};

				if !replaced {
					let comment_header = lewton::header::read_header_comment(&p.data);

					if comment_header.is_ok() {
						p.data = packet.to_vec();
						replaced = true;
					}
				}

				writer.write_packet(
					p.data.clone().into_boxed_slice(),
					p.stream_serial(),
					inf,
					p.absgp_page(),
				)?;

				if p.last_in_stream() && p.last_in_page() {
					break;
				}
			},
		}
	}

	c.seek(SeekFrom::Start(0))?;
	Ok(c.into_inner())
}

#[cfg(feature = "format-opus")]
struct Page {
	pub size_idx: usize,
	pub content: Vec<u8>,
	pub header_type: u8,
	pub start: usize,
	pub end: usize,
}

#[cfg(feature = "format-opus")]
pub(crate) fn opus<T>(mut data: T, packet: &[u8]) -> Result<Vec<u8>>
where
	T: Read + Seek,
{
	fn read_page<V>(mut data: V) -> Result<Page>
	where
		V: Read + Seek,
	{
		let mut sig = [0; 4];
		data.read_exact(&mut sig)?;

		if &sig != b"OggS" {
			return Err(Error::UnknownFormat);
		}

		let mut info = [0; 2];
		data.read_exact(&mut info)?;

		let header_type = info[1];

		let mut page = [0; 21];
		data.read_exact(&mut page)?;

		let size_idx = data.seek(SeekFrom::Current(0))? as usize;

		let mut segment_table = vec![0; page[20] as usize];
		data.read_exact(&mut segment_table)?;

		let start = data.seek(SeekFrom::Current(0))? as usize;

		let mut content = vec![0; segment_table.iter().map(|&b| b as usize).sum()];
		data.read_exact(&mut content)?;

		let end = data.seek(SeekFrom::Current(0))? as usize;

		Ok(Page {
			size_idx,
			content,
			header_type,
			start,
			end,
		})
	}

	let first_page = read_page(&mut data)?;

	let head = first_page.content;
	let (ident, head) = head.split_at(8);

	if ident != b"OpusHead" {
		return Err(Error::UnknownFormat);
	}

	if head[10] != 0 {
		let mut channel_mapping_info = [0; 1];
		data.read_exact(&mut channel_mapping_info)?;

		let mut channel_mapping = vec![0; channel_mapping_info[0] as usize];
		data.read_exact(&mut channel_mapping)?;
	}

	let second_page = read_page(&mut data)?;
	let tags = second_page.content;
	let size_pos = second_page.size_idx;
	let start = second_page.start;
	let end = second_page.end;

	if &tags[0..8] != b"OpusTags" {
		return Err(Error::UnknownFormat);
	}

	let last_len = (packet.len() % 255) as u8;
	let needed = (packet.len() / 255) + 1;

	// TODO: multi page support
	let mut segments = Vec::with_capacity(needed);

	for i in 0..needed {
		if i + 1 < needed {
			segments.push(255)
		} else {
			segments.push(last_len)
		}
	}

	data.seek(SeekFrom::Start(0))?;

	let mut content = Vec::new();
	data.read_to_end(&mut content)?;

	content.splice(start..end, packet.to_vec());
	content.insert(size_pos - 1, needed as u8);
	content.remove(size_pos);
	content.splice(size_pos..start, segments);

	Ok(content)
}

#[cfg(feature = "format-riff")]
pub(crate) fn riff(data: &mut File, metadata: HashMap<String, String>) -> Result<()> {
	let mut packet = Vec::new();

	packet.extend(riff::LIST_ID.value.iter());

	let fourcc = "INFO";
	packet.extend(fourcc.as_bytes().iter());

	for (k, v) in metadata {
		if let Some(fcc) = super::read::key_to_fourcc(&*k) {
			let mut val = v.as_bytes().to_vec();

			if val.len() % 2 != 0 {
				val.push(0)
			}

			let size = val.len() as u32;

			packet.extend(fcc.iter());
			packet.extend(size.to_le_bytes().iter());
			packet.extend(val.iter());
		}
	}

	let mut file_bytes = Vec::new();
	std::io::copy(data.borrow_mut(), &mut file_bytes)?;

	let len = (packet.len() - 4) as u32;
	let size = len.to_le_bytes();

	#[allow(clippy::needless_range_loop)]
	for i in 0..4 {
		packet.insert(i + 4, size[i]);
	}

	let mut file = Cursor::new(file_bytes);

	let chunk = riff::Chunk::read(&mut file, 0)?;

	let (mut list_pos, mut list_len): (Option<u32>, Option<u32>) = (None, None);

	if chunk.id() != riff::RIFF_ID {
		return Err(Error::Wav(
			"This file does not contain a RIFF chunk".to_string(),
		));
	}

	for child in chunk.iter(&mut file) {
		if child.id() == riff::LIST_ID {
			list_pos = Some(child.offset() as u32);
			list_len = Some(child.len());
		}
	}

	file.seek(SeekFrom::Start(0))?;

	let mut content = Vec::new();
	std::io::copy(&mut file, &mut content)?;

	if let (Some(list_pos), Some(list_len)) = (list_pos, list_len) {
		let list_end = (list_pos + list_len) as usize;

		let _ = content.splice(list_pos as usize..list_end, packet);

		let total_size = (content.len() - 8) as u32;
		let _ = content.splice(4..8, total_size.to_le_bytes().to_vec());

		data.seek(SeekFrom::Start(0))?;
		data.set_len(0)?;
		data.write_all(&*content)?;

		Ok(())
	} else {
		Err(Error::Wav(
			"This file does not contain an INFO chunk".to_string(),
		))
	}
}