From 78430fc13c0cfadef71db2dc1f3079e4da2f5865 Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Sun, 15 Sep 2024 08:04:19 -0400 Subject: [PATCH] EBML: Finish parsing \Segment\Tags --- lofty/src/ebml/read.rs | 2 +- lofty/src/ebml/read/segment_attachments.rs | 2 +- lofty/src/ebml/read/segment_tags.rs | 87 ++++++++++---------- lofty/src/ebml/tag/mod.rs | 23 +++--- lofty/src/ebml/tag/simple_tag.rs | 3 + lofty/src/ebml/tag/tag.rs | 16 ++++ lofty/src/ebml/tag/target.rs | 95 ++++++++++++++++++++++ lofty/src/ebml/tag/target_type.rs | 54 ------------ 8 files changed, 173 insertions(+), 109 deletions(-) create mode 100644 lofty/src/ebml/tag/tag.rs create mode 100644 lofty/src/ebml/tag/target.rs delete mode 100644 lofty/src/ebml/tag/target_type.rs diff --git a/lofty/src/ebml/read.rs b/lofty/src/ebml/read.rs index 0a544b724..e79068f23 100644 --- a/lofty/src/ebml/read.rs +++ b/lofty/src/ebml/read.rs @@ -26,7 +26,7 @@ where // new ones all scattered throughout the file let mut properties = EbmlProperties::default(); - let mut ebml_tag = None; + let ebml_tag; let mut element_reader = ElementReader::new(reader); diff --git a/lofty/src/ebml/read/segment_attachments.rs b/lofty/src/ebml/read/segment_attachments.rs index 81007ea62..309d0edb3 100644 --- a/lofty/src/ebml/read/segment_attachments.rs +++ b/lofty/src/ebml/read/segment_attachments.rs @@ -19,7 +19,7 @@ where { while let Some(child) = children_reader.next()? { match child { - ElementReaderYield::Master((ElementIdent::AttachedFile, size)) => { + ElementReaderYield::Master((ElementIdent::AttachedFile, _size)) => { let attached_file = read_attachment(children_reader)?; tag.attached_files.push(attached_file); }, diff --git a/lofty/src/ebml/read/segment_tags.rs b/lofty/src/ebml/read/segment_tags.rs index 01f26a7d2..d0c8b5d2e 100644 --- a/lofty/src/ebml/read/segment_tags.rs +++ b/lofty/src/ebml/read/segment_tags.rs @@ -1,6 +1,6 @@ use crate::config::ParseOptions; use crate::ebml::element_reader::{ElementChildIterator, ElementIdent, ElementReaderYield}; -use crate::ebml::{EbmlTag, Language, SimpleTag, TagValue, TargetType}; +use crate::ebml::{EbmlTag, Language, SimpleTag, Tag, TagValue, Target, TargetType}; use crate::error::Result; use crate::macros::decode_err; @@ -17,7 +17,8 @@ where while let Some(child) = children_reader.next()? { match child { ElementReaderYield::Master((ElementIdent::Tag, _size)) => { - read_tag(&mut children_reader.children(), tag)? + let tag_element = read_tag(&mut children_reader.children())?; + tag.tags.push(tag_element); }, ElementReaderYield::Eof => break, _ => unimplemented!("Unhandled child element in \\Segment\\Tags: {child:?}"), @@ -27,10 +28,13 @@ where Ok(()) } -fn read_tag(children_reader: &mut ElementChildIterator<'_, R>, _tag: &mut EbmlTag) -> Result<()> +fn read_tag(children_reader: &mut ElementChildIterator<'_, R>) -> Result where R: Read + Seek, { + let mut target = None; + let mut simple_tags = Vec::new(); + while let Some(child) = children_reader.next()? { let ElementReaderYield::Master((master, _size)) = child else { match child { @@ -43,10 +47,17 @@ where match master { ElementIdent::Targets => { - let _ = read_targets(&mut children_reader.children())?; + if target.is_some() { + decode_err!( + @BAIL Ebml, + "Duplicate Targets element found in \\Segment\\Tags\\Tag" + ); + } + + target = Some(read_targets(&mut children_reader.children())?); }, ElementIdent::SimpleTag => { - let _ = read_simple_tag(&mut children_reader.children())?; + simple_tags.push(read_simple_tag(&mut children_reader.children())?); }, _ => { unimplemented!("Unhandled child element in \\Segment\\Tags\\Tag: {master:?}"); @@ -54,28 +65,21 @@ where } } - Ok(()) -} + let Some(target) = target else { + decode_err!(@BAIL Ebml, "\\Segment\\Tags\\Tag is missing the required `Targets` element"); + }; -struct Target { - target_type_value: TargetType, - target_type: Option, - track_uid: Vec, - edition_uid: Vec, - chapter_uid: Vec, - attachment_uid: Vec, + Ok(Tag { + target, + simple_tags, + }) } fn read_targets(children_reader: &mut ElementChildIterator<'_, R>) -> Result where R: Read + Seek, { - let mut target_type_value = None; - let mut target_type = None; - let mut track_uid = Vec::new(); - let mut edition_uid = Vec::new(); - let mut chapter_uid = Vec::new(); - let mut attachment_uid = Vec::new(); + let mut target = Target::default(); while let Some(child) = children_reader.next()? { let ElementReaderYield::Child((child, size)) = child else { @@ -89,22 +93,35 @@ where match child.ident { ElementIdent::TargetTypeValue => { - target_type_value = Some(children_reader.read_unsigned_int(size.value())?); + let value = children_reader.read_unsigned_int(size.value())?; + + // Casting the `u64` to `u8` is safe because the value is checked to be within + // the range of `TargetType` anyway. + let target_type = TargetType::try_from(value as u8)?; + target.target_type = target_type; }, ElementIdent::TargetType => { - target_type = Some(children_reader.read_string(size.value())?); + target.name = Some(children_reader.read_string(size.value())?); }, ElementIdent::TagTrackUID => { - track_uid.push(children_reader.read_unsigned_int(size.value())?); + let mut track_uids = target.track_uids.unwrap_or_default(); + track_uids.push(children_reader.read_unsigned_int(size.value())?); + target.track_uids = Some(track_uids); }, ElementIdent::TagEditionUID => { - edition_uid.push(children_reader.read_unsigned_int(size.value())?); + let mut edition_uids = target.edition_uids.unwrap_or_default(); + edition_uids.push(children_reader.read_unsigned_int(size.value())?); + target.edition_uids = Some(edition_uids); }, ElementIdent::TagChapterUID => { - chapter_uid.push(children_reader.read_unsigned_int(size.value())?); + let mut chapter_uids = target.chapter_uids.unwrap_or_default(); + chapter_uids.push(children_reader.read_unsigned_int(size.value())?); + target.chapter_uids = Some(chapter_uids); }, ElementIdent::TagAttachmentUID => { - attachment_uid.push(children_reader.read_unsigned_int(size.value())?); + let mut attachment_uids = target.attachment_uids.unwrap_or_default(); + attachment_uids.push(children_reader.read_unsigned_int(size.value())?); + target.attachment_uids = Some(attachment_uids); }, _ => { unreachable!("Unhandled child element in \\Segment\\Tags\\Tag\\Targets: {child:?}") @@ -112,23 +129,7 @@ where } } - let target_type_value = match target_type_value { - // Casting the `u64` to `u8` is safe because the value is checked to be within - // the range of `TargetType` anyway. - Some(value) => TargetType::try_from(value as u8)?, - // The spec defines TargetType 50 (Album) as the default value, as it is the most - // common grouping level. - None => TargetType::Album, - }; - - Ok(Target { - target_type_value, - target_type, - track_uid, - edition_uid, - chapter_uid, - attachment_uid, - }) + Ok(target) } fn read_simple_tag(children_reader: &mut ElementChildIterator<'_, R>) -> Result diff --git a/lofty/src/ebml/tag/mod.rs b/lofty/src/ebml/tag/mod.rs index 89ead6bcb..5019beba5 100644 --- a/lofty/src/ebml/tag/mod.rs +++ b/lofty/src/ebml/tag/mod.rs @@ -1,15 +1,17 @@ -pub(crate) mod attached_file; -pub(crate) mod simple_tag; -pub(crate) mod target_type; +mod attached_file; +mod simple_tag; +mod tag; +mod target; pub use attached_file::*; pub use simple_tag::*; -pub use target_type::*; +pub use tag::*; +pub use target::*; use crate::config::WriteOptions; use crate::error::LoftyError; use crate::io::{FileLike, Length, Truncate}; -use crate::tag::{Accessor, MergeTag, SplitTag, Tag, TagExt, TagType}; +use crate::tag::{Accessor, MergeTag, SplitTag, TagExt, TagType}; use std::io::Write; use std::ops::Deref; @@ -21,6 +23,7 @@ use lofty_attr::tag; #[derive(Default, Debug, PartialEq, Eq, Clone)] #[tag(description = "An `EBML` tag", supported_formats(Ebml))] pub struct EbmlTag { + pub(crate) tags: Vec, pub(crate) attached_files: Vec, } @@ -107,7 +110,7 @@ impl Deref for SplitTagRemainder { impl SplitTag for EbmlTag { type Remainder = SplitTagRemainder; - fn split_tag(mut self) -> (Self::Remainder, Tag) { + fn split_tag(mut self) -> (Self::Remainder, crate::tag::Tag) { todo!() } } @@ -115,19 +118,19 @@ impl SplitTag for EbmlTag { impl MergeTag for SplitTagRemainder { type Merged = EbmlTag; - fn merge_tag(self, _tag: Tag) -> Self::Merged { + fn merge_tag(self, _tag: crate::tag::Tag) -> Self::Merged { todo!() } } -impl From for Tag { +impl From for crate::tag::Tag { fn from(input: EbmlTag) -> Self { input.split_tag().1 } } -impl From for EbmlTag { - fn from(input: Tag) -> Self { +impl From for EbmlTag { + fn from(input: crate::tag::Tag) -> Self { SplitTagRemainder::default().merge_tag(input) } } diff --git a/lofty/src/ebml/tag/simple_tag.rs b/lofty/src/ebml/tag/simple_tag.rs index 184d64cd6..427d408f5 100644 --- a/lofty/src/ebml/tag/simple_tag.rs +++ b/lofty/src/ebml/tag/simple_tag.rs @@ -9,6 +9,7 @@ use crate::tag::ItemValue; /// - The ISO-639-2 language code allows for an optional country code, so the [Lang] type cannot be used. /// /// [Lang]: crate::tag::items::Lang +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Language { /// An ISO-639-2 language code Iso639_2(String), @@ -33,6 +34,7 @@ pub enum Language { /// /// - [`ItemValue::Text`] | [`ItemValue::Locator`] -> [`TagValue::String`] /// - [`ItemValue::Binary`] -> [`TagValue::Binary`] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum TagValue { /// A UTF-8 string tag value String(String), @@ -66,6 +68,7 @@ impl From for TagValue { /// - They each describe a single [`Target`]. /// - This also means that multiple tags can describe the same target. /// - They **do not** need to have a value. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct SimpleTag { /// The name of the tag as it is stored /// diff --git a/lofty/src/ebml/tag/tag.rs b/lofty/src/ebml/tag/tag.rs new file mode 100644 index 000000000..ffc7386b4 --- /dev/null +++ b/lofty/src/ebml/tag/tag.rs @@ -0,0 +1,16 @@ +use super::{SimpleTag, Target}; + +/// A single metadata descriptor. +/// +/// This represents a `\Segment\Tags\Tag` element in the EBML tree. It contains a single [`Target`] and +/// its associated [`SimpleTag`]s. +/// +/// This structure is very different from other formats. See [`Target`] and [`SimpleTag`] for more +/// information on how these work. +#[derive(Default, Debug, PartialEq, Eq, Clone)] +pub struct Tag { + /// The target for which the tags are applied. + pub target: Target, + /// General information about the target + pub simple_tags: Vec, +} diff --git a/lofty/src/ebml/tag/target.rs b/lofty/src/ebml/tag/target.rs new file mode 100644 index 000000000..ece41bfda --- /dev/null +++ b/lofty/src/ebml/tag/target.rs @@ -0,0 +1,95 @@ +use crate::error::{LoftyError, Result}; +use crate::macros::decode_err; + +/// The type of the target. +/// +/// This is used to determine the type of the target that the tag is applied to. +#[repr(u8)] +#[non_exhaustive] +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub enum TargetType { + /// For video, this represents: SHOT + Shot = 10, + /// This is used to represent the following: + /// + /// - Audio: SUBTRACK / PART / MOVEMENT + /// - Video: SCENE + Scene = 20, + /// This is used to represent the following: + /// + /// - Audio: TRACK / SONG + /// - Video: CHAPTER + Track = 30, + /// For both audio and video, this represents: PART / SESSION + Part = 40, + /// This is used to represent the following: + /// + /// - Audio: ALBUM / OPERA / CONCERT + /// - Video: MOVIE / EPISODE / CONCERT + // The spec defines TargetType 50 (Album) as the default value, as it is the most + // common grouping level. + #[default] + Album = 50, + /// This is used to represent the following: + /// + /// - Audio: EDITION / ISSUE / VOLUME / OPUS + /// - Video: SEASON / SEQUEL / VOLUME + Edition = 60, + /// For both audio and video, this represents: COLLECTION + Collection = 70, +} + +impl TryFrom for TargetType { + type Error = LoftyError; + + fn try_from(value: u8) -> Result { + match value { + 10 => Ok(Self::Shot), + 20 => Ok(Self::Scene), + 30 => Ok(Self::Track), + 40 => Ok(Self::Part), + 50 => Ok(Self::Album), + 60 => Ok(Self::Edition), + 70 => Ok(Self::Collection), + _ => decode_err!(@BAIL Ebml, "TargetType value out of range"), + } + } +} + +/// The target for which a [`SimpleTag`] is applied. +/// +/// In Matroska, tags are specified on the level of targets. For example, there is no "TRACK TITLE" +/// tag, but rather a "TITLE" tag that is applied to a [`TargetType::Track`] target. +/// +/// See [`TargetType`] for more information on the types of targets. +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +pub struct Target { + /// The type of the target. + pub target_type: TargetType, + /// An informational string that can be used to display the logical level of the target. + pub name: Option, + /// A unique ID to identify the [Track](s) the tags belong to. + /// + /// If the value is 0 at this level, the tags apply to all tracks in the Segment. If set to any + /// other value, it **MUST** match the [`TrackUID`] value of a track found in this Segment. + pub track_uids: Option>, + /// A unique ID to identify the [EditionEntry](s) the tags belong to. + /// + /// If the value is 0 at this level, the tags apply to all editions in the Segment. If set to + /// any other value, it **MUST** match the [`EditionUID`] value of an edition found in this Segment. + pub edition_uids: Option>, + /// A unique ID to identify the [Chapter](s) the tags belong to. + /// + /// If the value is 0 at this level, the tags apply to all chapters in the Segment. If set to + /// any other value, it **MUST** match the [`ChapterUID`] value of a chapter found in this Segment. + pub chapter_uids: Option>, + /// A unique ID to identify the [`AttachedFile`]\(s) the tags belong to. + /// + /// If the value is 0 at this level, the tags apply to all the attachments in the Segment. If + /// set to any other value, it **MUST** match the [`AttachedFile::uid`]) value of an attachment + /// found in this Segment. + /// + /// [`AttachedFile`]: crate::ebml::AttachedFile + /// [`AttachedFile::uid`]: crate::ebml::AttachedFile::uid + pub attachment_uids: Option>, +} diff --git a/lofty/src/ebml/tag/target_type.rs b/lofty/src/ebml/tag/target_type.rs deleted file mode 100644 index 128e779dc..000000000 --- a/lofty/src/ebml/tag/target_type.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::error::{LoftyError, Result}; -use crate::macros::decode_err; - -/// The type of the target. -/// -/// This is used to determine the type of the target that the tag is applied to. -#[repr(u8)] -#[non_exhaustive] -#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] -pub enum TargetType { - /// For video, this represents: SHOT - Shot = 10, - /// This is used to represent the following: - /// - /// - Audio: SUBTRACK / PART / MOVEMENT - /// - Video: SCENE - Scene = 20, - /// This is used to represent the following: - /// - /// - Audio: TRACK / SONG - /// - Video: CHAPTER - Track = 30, - /// For both audio and video, this represents: PART / SESSION - Part = 40, - /// This is used to represent the following: - /// - /// - Audio: ALBUM / OPERA / CONCERT - /// - Video: MOVIE / EPISODE / CONCERT - Album = 50, - /// This is used to represent the following: - /// - /// - Audio: EDITION / ISSUE / VOLUME / OPUS - /// - Video: SEASON / SEQUEL / VOLUME - Edition = 60, - /// For both audio and video, this represents: COLLECTION - Collection = 70, -} - -impl TryFrom for TargetType { - type Error = LoftyError; - - fn try_from(value: u8) -> Result { - match value { - 10 => Ok(Self::Shot), - 20 => Ok(Self::Scene), - 30 => Ok(Self::Track), - 40 => Ok(Self::Part), - 50 => Ok(Self::Album), - 60 => Ok(Self::Edition), - 70 => Ok(Self::Collection), - _ => decode_err!(@BAIL Ebml, "TargetType value out of range"), - } - } -}