diff --git a/src/components/logic/mod.rs b/src/components/logic/mod.rs index a910e25d..9a083c71 100644 --- a/src/components/logic/mod.rs +++ b/src/components/logic/mod.rs @@ -1,11 +1,15 @@ -#[cfg(any(feature = "format-opus", feature = "format-vorbis", feature = "format-riff"))] +#[cfg(any( + feature = "format-opus", + feature = "format-vorbis", + feature = "format-riff" +))] pub(crate) mod constants; -#[cfg(any(feature = "format-opus", feature = "format-vorbis"))] -pub(crate) mod ogg_generic; - #[cfg(feature = "format-flac")] pub(crate) mod flac; #[cfg(feature = "format-riff")] pub(crate) mod riff; + +#[cfg(any(feature = "format-opus", feature = "format-vorbis"))] +pub(crate) mod ogg; diff --git a/src/components/logic/ogg/mod.rs b/src/components/logic/ogg/mod.rs new file mode 100644 index 00000000..e0386e46 --- /dev/null +++ b/src/components/logic/ogg/mod.rs @@ -0,0 +1,86 @@ +pub(crate) mod read; +pub(crate) mod write; + +use crate::{LoftyError, Result}; +use ogg_pager::Page; +use std::io::{Read, Seek}; + +pub fn page_from_packet(packet: &mut [u8]) -> Result> { + let mut pages: Vec = Vec::new(); + + let reader = &mut &packet[..]; + + let mut start = 0_usize; + let mut i = 0; + + while !reader.is_empty() { + let header_type = if i == 0 { 0 } else { 1_u8 }; + + let size = std::cmp::min(65025, reader.len()); + + if i != 0 { + if let Some(s) = start.checked_add(size) { + start = s + } else { + return Err(LoftyError::TooMuchData); + } + } + + let mut content = vec![0; size]; + reader.read_exact(&mut content)?; + + let end = start + size; + + pages.push(Page { + content, + header_type, + abgp: 0, + serial: 0, // Retrieved later + seq_num: (i + 1) as u32, + checksum: 0, // Calculated later + start, + end, + }); + + i += 1; + } + + Ok(pages) +} + +pub(self) fn reach_metadata(mut data: T, sig: &[u8]) -> Result<()> +where + T: Read + Seek, +{ + let first_page = Page::read(&mut data)?; + + let head = first_page.content; + let (ident, head) = head.split_at(sig.len()); + + if ident != sig { + return Err(LoftyError::InvalidData("OGG file missing magic signature")); + } + + 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)?; + } + + Ok(()) +} + +// Verify the 2nd page contains the comment header +pub(self) fn is_metadata(page: &Page, sig: &[u8]) -> Result<()> { + let sig_len = sig.len(); + + if page.content.len() < sig_len || &page.content[0..sig_len] != sig { + return Err(LoftyError::InvalidData( + "OGG file missing the mandatory comment header", + )); + } + + Ok(()) +} diff --git a/src/components/logic/ogg/read.rs b/src/components/logic/ogg/read.rs new file mode 100644 index 00000000..29b08c4a --- /dev/null +++ b/src/components/logic/ogg/read.rs @@ -0,0 +1,82 @@ +use crate::components::logic::constants::OPUSHEAD; +use crate::components::logic::ogg::{is_metadata, reach_metadata}; +use crate::{LoftyError, OggFormat, Result}; + +use std::collections::HashMap; +use std::io::{Read, Seek}; + +use byteorder::{LittleEndian, ReadBytesExt}; +use ogg_pager::Page; + +pub type OGGTags = (String, Vec, HashMap, OggFormat); + +pub(crate) fn read_from(mut data: T, header_sig: &[u8], comment_sig: &[u8]) -> Result +where + T: Read + Seek, +{ + reach_metadata(&mut data, header_sig)?; + + let md_page = Page::read(&mut data)?; + is_metadata(&md_page, comment_sig)?; + + let mut md_pages: Vec = Vec::new(); + + md_pages.extend(md_page.content[comment_sig.len()..].iter()); + + while let Ok(page) = Page::read(&mut data) { + if md_pages.len() > 125_829_120 { + return Err(LoftyError::TooMuchData); + } + + if page.header_type == 1 { + md_pages.extend(page.content.iter()); + } else { + break; + } + } + + let mut md: HashMap = HashMap::new(); + let mut pictures = Vec::new(); + + let reader = &mut &md_pages[..]; + + let vendor_len = reader.read_u32::()?; + let mut vendor = vec![0; vendor_len as usize]; + reader.read_exact(&mut vendor)?; + + let vendor_str = match String::from_utf8(vendor) { + Ok(v) => v, + Err(_) => { + return Err(LoftyError::InvalidData( + "OGG file has an invalid vendor string", + )) + }, + }; + + let comments_total_len = reader.read_u32::()?; + + for _ in 0..comments_total_len { + let comment_len = reader.read_u32::()?; + + let mut comment_bytes = vec![0; comment_len as usize]; + reader.read_exact(&mut comment_bytes)?; + + let comment = String::from_utf8(comment_bytes)?; + + let split: Vec<&str> = comment.splitn(2, '=').collect(); + + if split[0] == "METADATA_BLOCK_PICTURE" { + pictures.push(split[1].to_string()) + } else { + md.insert(split[0].to_string(), split[1].to_string()); + } + } + + let vorbis_format = if header_sig == OPUSHEAD { + OggFormat::Opus + } else { + OggFormat::Vorbis + }; + + Ok((vendor_str, pictures, md, vorbis_format)) +} diff --git a/src/components/tags/vorbis_tag.rs b/src/components/tags/vorbis_tag.rs index 57222d25..b77ade5a 100644 --- a/src/components/tags/vorbis_tag.rs +++ b/src/components/tags/vorbis_tag.rs @@ -7,14 +7,14 @@ use crate::components::logic::constants::{ OPUSHEAD, OPUSTAGS, VORBIS_COMMENT_HEAD, VORBIS_IDENT_HEAD, }; -use crate::components::logic::{flac, ogg_generic}; +use crate::components::logic::{flac, ogg}; use crate::{ Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, LoftyError, OggFormat, Picture, PictureType, Result, TagType, ToAny, ToAnyTag, }; #[cfg(feature = "format-opus")] -use crate::components::logic::ogg_generic::OGGTags; +use crate::components::logic::ogg::read::OGGTags; use lofty_attr::impl_tag; @@ -75,13 +75,13 @@ impl VorbisInnerTag { { match format { OggFormat::Vorbis => { - let tag = ogg_generic::read_from(reader, &VORBIS_IDENT_HEAD, &VORBIS_COMMENT_HEAD)?; + let tag = ogg::read::read_from(reader, &VORBIS_IDENT_HEAD, &VORBIS_COMMENT_HEAD)?; let vorbis_tag: VorbisTag = tag.try_into()?; Ok(vorbis_tag.inner) }, OggFormat::Opus => { - let tag = ogg_generic::read_from(reader, &OPUSHEAD, &OPUSTAGS)?; + let tag = ogg::read::read_from(reader, &OPUSHEAD, &OPUSTAGS)?; let vorbis_tag: VorbisTag = tag.try_into()?; Ok(vorbis_tag.inner) @@ -465,7 +465,7 @@ impl AudioTagWrite for VorbisTag { if let Some(format) = self.inner.format.clone() { match format { OggFormat::Vorbis => { - ogg_generic::create_pages( + ogg::write::create_pages( file, &VORBIS_COMMENT_HEAD, &self.inner.vendor, @@ -474,7 +474,7 @@ impl AudioTagWrite for VorbisTag { )?; }, OggFormat::Opus => { - ogg_generic::create_pages( + ogg::write::create_pages( file, &OPUSTAGS, &self.inner.vendor,