diff --git a/lofty/src/ebml/element_reader.rs b/lofty/src/ebml/element_reader.rs index 9b0143d3..94bb1e2d 100644 --- a/lofty/src/ebml/element_reader.rs +++ b/lofty/src/ebml/element_reader.rs @@ -1,4 +1,4 @@ -use crate::ebml::vint::VInt; +use crate::ebml::vint::{ElementId, VInt}; use crate::error::Result; use crate::macros::{decode_err, try_vec}; @@ -10,8 +10,8 @@ use lofty_attr::ebml_master_elements; #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct ElementHeader { - pub(crate) id: VInt, - pub(crate) size: VInt, + pub(crate) id: ElementId, + pub(crate) size: VInt, } impl ElementHeader { @@ -20,8 +20,8 @@ impl ElementHeader { R: Read, { Ok(Self { - id: VInt::parse_from_element_id(reader, max_id_length)?, - size: VInt::parse(reader, max_vint_length)?, + id: ElementId::parse(reader, max_id_length)?, + size: VInt::::parse(reader, max_vint_length)?, }) } } @@ -41,7 +41,7 @@ pub enum ElementDataType { #[derive(Copy, Clone, Debug)] struct MasterElement { id: ElementIdent, - children: &'static [(VInt, ChildElementDescriptor)], + children: &'static [(ElementId, ChildElementDescriptor)], } #[derive(Copy, Clone, Debug)] @@ -247,7 +247,7 @@ const ROOT_DEPTH: u8 = 1; #[derive(Copy, Clone, Debug)] struct Depth { level: u8, - length: VInt, + length: VInt, } #[derive(Copy, Clone, Debug)] @@ -302,7 +302,7 @@ impl ElementReaderContext { self.masters.get((self.depth - 1) as usize).copied() } - fn current_master_length(&self) -> VInt { + fn current_master_length(&self) -> VInt { assert!(self.depth > 0); self.current_master() .expect("should have current master element") @@ -316,7 +316,7 @@ impl ElementReaderContext { } } - fn remaining_lock_length(&self) -> VInt { + fn remaining_lock_length(&self) -> VInt { assert!(self.locked && !self.lock_depths.is_empty()); let lock_depth = *self.lock_depths.last().unwrap(); @@ -326,8 +326,8 @@ impl ElementReaderContext { #[derive(Debug)] pub(crate) enum ElementReaderYield { - Master((ElementIdent, VInt)), - Child((ChildElementDescriptor, VInt)), + Master((ElementIdent, VInt)), + Child((ChildElementDescriptor, VInt)), Unknown(ElementHeader), Eof, } @@ -412,7 +412,7 @@ where self.ctx.max_size_length = len } - fn push_new_master(&mut self, master: MasterElement, size: VInt) -> Result<()> { + fn push_new_master(&mut self, master: MasterElement, size: VInt) -> Result<()> { log::debug!("New master element: {:?}", master.id); if self.ctx.depth == MAX_DEPTH { @@ -662,7 +662,7 @@ where // https://www.rfc-editor.org/rfc/rfc8794.html#section-7.8 // A Binary Element MUST declare a length in octets from zero to VINTMAX. - if element_length > VInt::MAX { + if element_length > VInt::::MAX { decode_err!(@BAIL Ebml, "Binary element length is too large") } diff --git a/lofty/src/ebml/mod.rs b/lofty/src/ebml/mod.rs index b8ebd32e..b10b5320 100644 --- a/lofty/src/ebml/mod.rs +++ b/lofty/src/ebml/mod.rs @@ -11,7 +11,7 @@ use lofty_attr::LoftyFile; pub use properties::EbmlProperties; pub use tag::*; -pub use vint::VInt; +pub use vint::*; /// An EBML file #[derive(LoftyFile, Default)] diff --git a/lofty/src/ebml/read.rs b/lofty/src/ebml/read.rs index e79068f2..0b032505 100644 --- a/lofty/src/ebml/read.rs +++ b/lofty/src/ebml/read.rs @@ -9,7 +9,7 @@ mod segment_tracks; use super::EbmlFile; use crate::config::ParseOptions; use crate::ebml::element_reader::{ElementHeader, ElementIdent, ElementReader, ElementReaderYield}; -use crate::ebml::vint::VInt; +use crate::ebml::vint::ElementId; use crate::ebml::EbmlProperties; use crate::error::Result; use crate::macros::decode_err; @@ -18,6 +18,9 @@ use std::io::{Read, Seek}; const SUPPORTED_DOC_TYPES: &[&str] = &["matroska", "webm"]; +const CRC32_ID: ElementId = ElementId(0xBF); +const VOID_ID: ElementId = ElementId(0xEC); + pub(super) fn read_from(reader: &mut R, parse_options: ParseOptions) -> Result where R: Read + Seek, @@ -45,7 +48,7 @@ where // CRC-32 (0xBF) and Void (0xEC) elements can occur at the top level. // This is valid, and we can just skip them. ElementReaderYield::Unknown(ElementHeader { - id: VInt(id @ (0xBF | 0xEC)), + id: id @ (CRC32_ID | VOID_ID), size, }) => { log::debug!("Skipping global element: {:X}", id); diff --git a/lofty/src/ebml/read/segment.rs b/lofty/src/ebml/read/segment.rs index 524e1db1..606b8cd6 100644 --- a/lofty/src/ebml/read/segment.rs +++ b/lofty/src/ebml/read/segment.rs @@ -3,7 +3,7 @@ use crate::config::ParseOptions; use crate::ebml::element_reader::{ElementHeader, ElementIdent, ElementReader, ElementReaderYield}; use crate::ebml::properties::EbmlProperties; use crate::ebml::tag::EbmlTag; -use crate::ebml::VInt; +use crate::ebml::ElementId; use crate::error::Result; use std::io::{Read, Seek}; @@ -72,7 +72,7 @@ where // elements, so we can just skip any useless ones. children_reader.skip_element(ElementHeader { - id: VInt(id as u64), + id: ElementId(id as u64), size, })?; }, diff --git a/lofty/src/ebml/read/segment_attachments.rs b/lofty/src/ebml/read/segment_attachments.rs index 309d0edb..6bb10d1f 100644 --- a/lofty/src/ebml/read/segment_attachments.rs +++ b/lofty/src/ebml/read/segment_attachments.rs @@ -74,7 +74,7 @@ where uid = Some(children_reader.read_unsigned_int(size)?); }, ElementIdent::FileReferral => { - referral = Some(children_reader.read_string(size)?); + referral = Some(children_reader.read_binary(size)?); }, ElementIdent::FileUsedStartTime => { used_start_time = Some(children_reader.read_unsigned_int(size)?); diff --git a/lofty/src/ebml/read/segment_tracks.rs b/lofty/src/ebml/read/segment_tracks.rs index 25da6ba9..9210e77d 100644 --- a/lofty/src/ebml/read/segment_tracks.rs +++ b/lofty/src/ebml/read/segment_tracks.rs @@ -10,7 +10,7 @@ use std::io::{Read, Seek}; pub(super) fn read_from( children_reader: &mut ElementChildIterator<'_, R>, parse_options: ParseOptions, - properties: &mut EbmlProperties, + _properties: &mut EbmlProperties, ) -> Result<()> where R: Read + Seek, @@ -44,7 +44,7 @@ const AUDIO_TRACK_TYPE: u64 = 2; fn read_track_entry( children_reader: &mut ElementChildIterator<'_, R>, - parse_options: ParseOptions, + _parse_options: ParseOptions, audio_tracks: &mut Vec, ) -> Result<()> where diff --git a/lofty/src/ebml/tag/attached_file.rs b/lofty/src/ebml/tag/attached_file.rs index 22158c52..704cf7ee 100644 --- a/lofty/src/ebml/tag/attached_file.rs +++ b/lofty/src/ebml/tag/attached_file.rs @@ -25,7 +25,7 @@ pub struct AttachedFile { /// Unique ID representing the file, as random as possible. pub uid: u64, /// A binary value that a track/codec can refer to when the attachment is needed. - pub referral: Option, + pub referral: Option>, /// The timestamp at which this optimized font attachment comes into context. /// /// This is expressed in Segment Ticks which is based on `TimestampScale`. This element is diff --git a/lofty/src/ebml/tag/target.rs b/lofty/src/ebml/tag/target.rs index 760177f3..8228f96f 100644 --- a/lofty/src/ebml/tag/target.rs +++ b/lofty/src/ebml/tag/target.rs @@ -102,3 +102,16 @@ impl From for Target { } } } + +impl Target { + // TargetType::Album is the default value. If nothing else is set, it is valid to write + // a zero-sized Targets element. + pub(super) fn is_empty_candidate(&self) -> bool { + self.target_type == TargetType::Album + && self.name.is_none() + && self.track_uids.is_none() + && self.edition_uids.is_none() + && self.chapter_uids.is_none() + && self.attachment_uids.is_none() + } +} diff --git a/lofty/src/ebml/tag/write/elements/attached_file.rs b/lofty/src/ebml/tag/write/elements/attached_file.rs new file mode 100644 index 00000000..81ee803c --- /dev/null +++ b/lofty/src/ebml/tag/write/elements/attached_file.rs @@ -0,0 +1,81 @@ +use crate::ebml::tag::write::{write_element, ElementWriterCtx, WriteableElement}; +use crate::ebml::{AttachedFile, ElementId, VInt}; +use crate::io::FileLike; + +const FileDescription_ID: ElementId = ElementId(0x467E); +const FileName_ID: ElementId = ElementId(0x466E); +const FileMediaType_ID: ElementId = ElementId(0x4660); +const FileData_ID: ElementId = ElementId(0x465C); +const FileUID_ID: ElementId = ElementId(0x46AE); +const FileReferral_ID: ElementId = ElementId(0x4675); +const FileUsedStartTime_ID: ElementId = ElementId(0x4661); +const FileUsedEndTime_ID: ElementId = ElementId(0x4662); + +impl WriteableElement for AttachedFile { + const ID: ElementId = ElementId(0x61A7); + + fn write_element( + &self, + ctx: ElementWriterCtx, + writer: &mut F, + ) -> crate::error::Result<()> { + self.validate()?; + + let mut element_children = Vec::new(); + if let Some(description) = &self.description { + write_element( + ctx, + FileDescription_ID, + &description.as_str(), + &mut element_children, + )?; + } + + write_element( + ctx, + FileName_ID, + &self.file_name.as_str(), + &mut element_children, + )?; + + write_element( + ctx, + FileMediaType_ID, + &self.mime_type.as_str(), + &mut element_children, + )?; + + write_element( + ctx, + FileData_ID, + &self.file_data.as_slice(), + &mut element_children, + )?; + + let uid = VInt::::try_from(self.uid)?; + write_element(ctx, FileUID_ID, &uid, &mut element_children)?; + + if let Some(referral) = &self.referral { + write_element( + ctx, + FileReferral_ID, + &referral.as_slice(), + &mut element_children, + )?; + } + + if let Some(start_time) = &self.used_start_time { + let vint = VInt::::try_from(*start_time)?; + write_element(ctx, FileUsedStartTime_ID, &vint, &mut element_children)?; + } + + if let Some(end_time) = &self.used_end_time { + let vint = VInt::::try_from(*end_time)?; + write_element(ctx, FileUsedEndTime_ID, &vint, &mut element_children)?; + } + + write_element(ctx, Self::ID, &element_children.as_slice(), writer)?; + + Ok(()) + } +} diff --git a/lofty/src/ebml/tag/write/elements/mod.rs b/lofty/src/ebml/tag/write/elements/mod.rs new file mode 100644 index 00000000..0baa7429 --- /dev/null +++ b/lofty/src/ebml/tag/write/elements/mod.rs @@ -0,0 +1,2 @@ +pub(super) mod attached_file; +pub(super) mod target; diff --git a/lofty/src/ebml/tag/write/elements/target.rs b/lofty/src/ebml/tag/write/elements/target.rs new file mode 100644 index 00000000..a2636255 --- /dev/null +++ b/lofty/src/ebml/tag/write/elements/target.rs @@ -0,0 +1,102 @@ +use crate::ebml::tag::write::{write_element, EbmlWriteExt, ElementWriterCtx, WriteableElement}; +use crate::ebml::{ElementId, Target, TargetType, VInt}; +use crate::io::FileLike; + +const TargetTypeValue_ID: ElementId = ElementId(0x68CA); +const TargetType_ID: ElementId = ElementId(0x63CA); +const TagTrackUID_ID: ElementId = ElementId(0x63C5); +const TagEditionUID_ID: ElementId = ElementId(0x63C9); +const TagChapterUID_ID: ElementId = ElementId(0x63C4); +const TagAttachmentUID_ID: ElementId = ElementId(0x63C6); + +impl WriteableElement for Target { + const ID: ElementId = ElementId(0x63C0); + + fn write_element( + &self, + ctx: ElementWriterCtx, + writer: &mut F, + ) -> crate::error::Result<()> { + if self.is_empty_candidate() { + writer.write_id(ctx, Self::ID)?; + writer.write_size(ctx, VInt::::ZERO)?; + return Ok(()); + } + + let mut element_children = Vec::new(); + if self.target_type == TargetType::Album { + write_element( + ctx, + TargetTypeValue_ID, + &[].as_slice(), + &mut element_children, + )?; + } else { + let vint = VInt::::try_from(self.target_type as u64)?; + write_element(ctx, TargetTypeValue_ID, &vint, &mut element_children)?; + } + + if let Some(name) = &self.name { + write_element(ctx, TargetType_ID, &name.as_str(), &mut element_children)?; + } + + if let Some(track_uids) = &self.track_uids { + for &uid in track_uids { + let vint = VInt::::try_from(uid)?; + write_element(ctx, TagTrackUID_ID, &vint, &mut element_children)?; + } + } + + if let Some(edition_uids) = &self.edition_uids { + for &uid in edition_uids { + let vint = VInt::::try_from(uid)?; + write_element(ctx, TagEditionUID_ID, &vint, &mut element_children)?; + } + } + + if let Some(chapter_uids) = &self.chapter_uids { + for &uid in chapter_uids { + let vint = VInt::::try_from(uid)?; + write_element(ctx, TagChapterUID_ID, &vint, &mut element_children)?; + } + } + + if let Some(attachment_uids) = &self.attachment_uids { + for &uid in attachment_uids { + let vint = VInt::::try_from(uid)?; + write_element(ctx, TagAttachmentUID_ID, &vint, &mut element_children)?; + } + } + + write_element(ctx, Self::ID, &element_children.as_slice(), writer)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::io::Cursor; + + #[test_log::test] + fn write_empty_default() { + let target = Target::default(); + + let mut buf = Cursor::new(Vec::new()); + target + .write_element( + ElementWriterCtx { + max_id_len: 4, + max_size_len: 8, + }, + &mut buf, + ) + .unwrap(); + + let expected = vec![0x63, 0xC0, 0x80]; + + assert_eq!(buf.into_inner(), expected); + } +} diff --git a/lofty/src/ebml/tag/write/mod.rs b/lofty/src/ebml/tag/write/mod.rs index e69de29b..27ace950 100644 --- a/lofty/src/ebml/tag/write/mod.rs +++ b/lofty/src/ebml/tag/write/mod.rs @@ -0,0 +1,48 @@ +mod elements; +mod type_encodings; + +use crate::ebml::{ElementId, VInt}; +use crate::error::Result; +use crate::io::FileLike; + +use std::io::Write; + +use type_encodings::ElementEncodable; + +#[derive(Copy, Clone)] +pub(crate) struct ElementWriterCtx { + pub(crate) max_id_len: u8, + pub(crate) max_size_len: u8, +} + +pub(crate) trait EbmlWriteExt: Write + Sized { + fn write_id(&mut self, ctx: ElementWriterCtx, id: ElementId) -> Result<()> { + id.write_to(Some(ctx.max_id_len), self)?; + Ok(()) + } + + fn write_size(&mut self, ctx: ElementWriterCtx, size: VInt) -> Result<()> { + VInt::::write_to(size.value(), None, Some(ctx.max_size_len), self)?; + Ok(()) + } +} + +impl EbmlWriteExt for T where T: Write {} + +pub(crate) trait WriteableElement { + const ID: ElementId; + + fn write_element(&self, ctx: ElementWriterCtx, writer: &mut F) -> Result<()>; +} + +pub(crate) fn write_element( + ctx: ElementWriterCtx, + id: ElementId, + element: &E, + writer: &mut W, +) -> Result<()> { + writer.write_id(ctx, id)?; + element.write_to(ctx, writer)?; + + Ok(()) +} diff --git a/lofty/src/ebml/tag/write/type_encodings.rs b/lofty/src/ebml/tag/write/type_encodings.rs new file mode 100644 index 00000000..13d643a0 --- /dev/null +++ b/lofty/src/ebml/tag/write/type_encodings.rs @@ -0,0 +1,110 @@ +use super::{EbmlWriteExt, ElementWriterCtx}; +use crate::ebml::{TagValue, VInt}; +use crate::error::Result; +use std::io::Write; + +use byteorder::WriteBytesExt; + +pub(crate) trait ElementEncodable { + fn len(&self) -> Result>; + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()>; +} + +impl ElementEncodable for VInt { + fn len(&self) -> Result> { + Ok(VInt(u64::from(self.octet_length()))) + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + writer.write_size(ctx, self.len()?)?; + VInt::::write_to(self.value(), None, None, writer)?; + Ok(()) + } +} + +impl ElementEncodable for VInt { + fn len(&self) -> Result> { + Ok(VInt(u64::from(self.octet_length()))) + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + writer.write_size(ctx, self.len()?)?; + VInt::::write_to(self.value() as u64, None, None, writer)?; + Ok(()) + } +} + +impl ElementEncodable for f32 { + fn len(&self) -> Result> { + Ok(VInt(size_of::() as u64)) + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + if *self == 0.0 { + VInt::::write_to(VInt::::ZERO.value(), None, None, writer)?; + return Ok(()); + } + + writer.write_size(ctx, self.len()?)?; + writer.write_f32::(*self)?; + Ok(()) + } +} + +impl ElementEncodable for f64 { + fn len(&self) -> Result> { + Ok(VInt(size_of::() as u64)) + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + if *self == 0.0 { + VInt::::write_to(VInt::::ZERO.value(), None, None, writer)?; + return Ok(()); + } + + writer.write_size(ctx, self.len()?)?; + writer.write_f64::(*self)?; + Ok(()) + } +} + +impl ElementEncodable for &[u8] { + fn len(&self) -> Result> { + VInt::try_from(<[u8]>::len(self) as u64) + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + writer.write_size(ctx, <&[u8] as ElementEncodable>::len(self)?)?; + writer.write_all(self)?; + Ok(()) + } +} + +impl ElementEncodable for &str { + fn len(&self) -> Result> { + VInt::try_from(str::len(self) as u64) + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + writer.write_size(ctx, <&str as ElementEncodable>::len(self)?)?; + writer.write_all(self.as_bytes())?; + Ok(()) + } +} + +impl ElementEncodable for TagValue<'_> { + fn len(&self) -> Result> { + match self { + TagValue::String(s) => <&str as ElementEncodable>::len(&&**s), + TagValue::Binary(b) => <&[u8] as ElementEncodable>::len(&&**b), + } + } + + fn write_to(&self, ctx: ElementWriterCtx, writer: &mut W) -> Result<()> { + match self { + TagValue::String(s) => <&str as ElementEncodable>::write_to(&&**s, ctx, writer), + TagValue::Binary(b) => <&[u8] as ElementEncodable>::write_to(&&**b, ctx, writer), + } + } +} diff --git a/lofty/src/ebml/vint.rs b/lofty/src/ebml/vint.rs index e555f6a1..ac19b1b3 100644 --- a/lofty/src/ebml/vint.rs +++ b/lofty/src/ebml/vint.rs @@ -1,289 +1,382 @@ use crate::error::Result; use crate::macros::err; +use std::fmt::UpperHex; -use std::io::Read; +use std::io::{Read, Write}; use std::ops::{Add, Sub}; use byteorder::{ReadBytesExt, WriteBytesExt}; +macro_rules! impl_vint { + ($($t:ty),*) => { + $( + paste::paste! { + #[allow(trivial_numeric_casts)] + impl VInt<$t> { + /// The maximum value that can be represented by a `VInt` + pub const MAX: $t = <$t>::MAX >> (<$t>::BITS as u64 - Self::USABLE_BITS); + /// The minimum value that can be represented by a `VInt` + pub const MIN: $t = <$t>::MIN; + /// A `VInt` with a value of 0 + pub const ZERO: Self = Self(0); + + /// Gets the inner value of the `VInt` + /// + /// # Examples + /// + /// ```rust + /// use lofty::ebml::VInt; + /// + /// # fn main() -> lofty::error::Result<()> { + #[doc = " let vint = VInt::<" $t ">::try_from(2)?;"] + /// assert_eq!(vint.value(), 2); + /// # Ok(()) } + /// ``` + pub fn value(&self) -> $t { + self.0 + } + + /// Parse a `VInt` from a reader + /// + /// `max_length` can be used to specify the maximum number of octets the number should + /// occupy, otherwise it should be `8`. + /// + /// # Errors + /// + /// * The int cannot fit within the maximum width of 54 bits + /// + /// # Examples + /// + /// ```rust + /// use lofty::ebml::VInt; + /// + /// # fn main() -> lofty::error::Result<()> { + /// // This octet count (9) is too large to represent + /// let mut invalid_vint_reader = &[0b0000_0000_1]; + #[doc = " let invalid_vint = VInt::<" $t ">::parse(&mut &invalid_vint_reader[..], 8);"] + /// assert!(invalid_vint.is_err()); + /// + /// // This octet count (4) is too large to represent given our `max_length` + /// let mut invalid_vint_reader2 = &[0b0001_1111]; + #[doc = " let invalid_vint2 = VInt::<" $t ">::parse(&mut &invalid_vint_reader2[..], 3);"] + /// assert!(invalid_vint2.is_err()); + /// + /// // This value is small enough to represent + /// let mut valid_vint_reader = &[0b1000_0010]; + #[doc = " let valid_vint = VInt::<" $t ">::parse(&mut &valid_vint_reader[..], 8)?;"] + /// assert_eq!(valid_vint.value(), 2); + /// # Ok(()) } + /// ``` + pub fn parse(reader: &mut R, max_length: u8) -> Result + where + R: Read, + { + Ok(Self(parse_vint(reader, max_length, false)? as $t)) + } + + /// Represents the length of the `VInt` in octets + /// + /// NOTE: The value returned will always be <= 8 + /// + /// # Examples + /// + /// ```rust + /// use lofty::ebml::VInt; + /// + /// # fn main() -> lofty::error::Result<()> { + /// // Anything <= 254 will fit into a single octet + /// let vint = VInt::try_from(100u64)?; + /// assert_eq!(vint.octet_length(), 1); + /// + /// // A larger number will need to + /// let vint = VInt::try_from(500_000u64)?; + /// assert_eq!(vint.octet_length(), 3); + /// # Ok(()) } + /// ``` + pub fn octet_length(&self) -> u8 { + octet_length(self.0 as u64) + } + + /// Converts the `VInt` into a byte Vec + /// + /// * `min_length` can be used to specify the minimum number of octets the number should + /// occupy. + /// * `max_length` can be used to specify the maximum number of octets the number should + /// occupy. + /// + /// # Errors + /// + /// * The octet length is greater than `max_length` (if provided) + /// * `min_length` is greater than `max_length` OR `8` + /// * Unable to write to the buffer + /// + /// # Examples + /// + /// ```rust + /// use lofty::ebml::VInt; + /// + /// # fn main() -> lofty::error::Result<()> { + /// let vint = VInt::try_from(10u64)?; + /// let bytes = vint.as_bytes(None, None)?; + /// + /// assert_eq!(bytes, &[0b1000_1010]); + /// # Ok(()) } + /// ``` + pub fn as_bytes(&self, min_length: Option, max_length: Option) -> Result> { + let mut ret = Vec::with_capacity(8); + VInt::<$t>::write_to(self.0 as u64, min_length, max_length, &mut ret)?; + Ok(ret) + } + + #[inline] + pub(crate) fn saturating_sub(self, other: $t) -> Self { + let v = self.0.saturating_sub(other); + if v < Self::MIN { + return Self(Self::MIN); + } + + Self(v) + } + } + + impl Add for VInt<$t> { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + let val = self.0 + other.0; + assert!(val <= Self::MAX, "VInt overflow"); + + Self(val) + } + } + + impl Sub for VInt<$t> { + type Output = Self; + + fn sub(self, other: Self) -> Self::Output { + Self(self.0 - other.0) + } + } + + impl PartialEq<$t> for VInt<$t> { + fn eq(&self, other: &$t) -> bool { + self.0 == *other + } + } + + impl TryFrom<$t> for VInt<$t> { + type Error = crate::error::LoftyError; + + fn try_from(value: $t) -> Result { + if value > Self::MAX { + err!(BadVintSize); + } + + Ok(Self(value)) + } + } + } + )* + }; +} + /// An EMBL variable-size integer /// /// A `VInt` is an unsigned integer composed of up to 8 octets, with 7 usable bits per octet. /// -/// To ensure safe construction of `VInt`s, users must create them through [`VInt::parse`] or [`VInt::from_u64`]. +/// To ensure safe construction of `VInt`s, users must create them through the `TryFrom` implementations or [`VInt::parse`]. #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] -pub struct VInt(pub(crate) u64); +pub struct VInt(pub(crate) T); -impl VInt { +impl VInt { // Each octet will shave a single bit off each byte const USABLE_BITS_PER_BYTE: u64 = 7; const MAX_OCTET_LENGTH: u64 = 8; const USABLE_BITS: u64 = Self::MAX_OCTET_LENGTH * Self::USABLE_BITS_PER_BYTE; - /// The maximum value that can be represented by a `VInt` - pub const MAX: u64 = u64::MAX >> (u64::BITS as u64 - Self::USABLE_BITS); - /// The minimum value that can be represented by a `VInt` - pub const MIN: u64 = 0; - /// A `VInt` with a value of 0 - pub const ZERO: Self = Self(0); - - /// Create a signed `VInt` from a `u64` - /// - /// # Errors - /// - /// * `uint` cannot fit within the maximum width of 56 bits - /// - /// # Examples - /// - /// ```rust - /// use lofty::ebml::VInt; - /// - /// # fn main() -> lofty::error::Result<()> { - /// // This value is too large to represent - /// let invalid_vint = VInt::from_u64(u64::MAX); - /// assert!(invalid_vint.is_err()); - /// - /// // This value is small enough to represent - /// let valid_vint = VInt::from_u64(500)?; - /// # Ok(()) } - /// ``` - pub fn from_u64(uint: u64) -> Result { - if uint > Self::MAX { + pub(crate) fn write_to( + mut value: u64, + min_length: Option, + max_length: Option, + writer: &mut W, + ) -> Result<()> + where + W: Write, + { + let octets = std::cmp::max(octet_length(value), min_length.unwrap_or(0)); + if octets > max_length.unwrap_or(Self::MAX_OCTET_LENGTH as u8) { err!(BadVintSize); } - Ok(Self(uint)) + // Add the octet length + value |= 1 << (octets * (Self::USABLE_BITS_PER_BYTE as u8)); + + let mut byte_shift = (octets - 1) as i8; + while byte_shift >= 0 { + writer.write_u8((value >> (byte_shift * 8)) as u8)?; + byte_shift -= 1; + } + + Ok(()) } +} - /// Gets the inner value of the `VInt` - /// - /// # Examples - /// - /// ```rust - /// use lofty::ebml::VInt; - /// - /// # fn main() -> lofty::error::Result<()> { - /// let vint = VInt::from_u64(2)?; - /// assert_eq!(vint.value(), 2); - /// # Ok(()) } - /// ``` - pub fn value(&self) -> u64 { - self.0 +impl_vint!(u64, i64); + +fn parse_vint(reader: &mut R, max_length: u8, retain_marker: bool) -> Result +where + R: Read, +{ + let start = reader.read_u8()?; + let octet_length = verify_length(start, max_length)?; + + let mut bytes_read = 1; + + let mut val = u64::from(start); + if !retain_marker { + val ^= 1 << start.ilog2(); } - /// Parse a `VInt` from a reader - /// - /// `max_length` can be used to specify the maximum number of octets the number should - /// occupy, otherwise it should be `8`. - /// - /// # Errors - /// - /// * `uint` cannot fit within the maximum width of 54 bits - /// - /// # Examples - /// - /// ```rust - /// use lofty::ebml::VInt; - /// - /// # fn main() -> lofty::error::Result<()> { - /// // This octet count (9) is too large to represent - /// let mut invalid_vint_reader = &[0b0000_0000_1]; - /// let invalid_vint = VInt::parse(&mut &invalid_vint_reader[..], 8); - /// assert!(invalid_vint.is_err()); - /// - /// // This octet count (4) is too large to represent given our `max_length` - /// let mut invalid_vint_reader2 = &[0b0001_1111]; - /// let invalid_vint2 = VInt::parse(&mut &invalid_vint_reader2[..], 3); - /// assert!(invalid_vint2.is_err()); - /// - /// // This value is small enough to represent - /// let mut valid_vint_reader = &[0b1000_0010]; - /// let valid_vint = VInt::parse(&mut &valid_vint_reader[..], 8)?; - /// assert_eq!(valid_vint.value(), 2); - /// # Ok(()) } - /// ``` - pub fn parse(reader: &mut R, max_length: u8) -> Result - where - R: Read, - { - let start = reader.read_u8()?; - let octet_length = Self::verify_length(start, max_length)?; - - let mut bytes_read = 1; - let mut val = u64::from(start) ^ (1 << start.ilog2()) as u64; - while bytes_read < octet_length { - bytes_read += 1; - val = (val << 8) | u64::from(reader.read_u8()?); - } + while bytes_read < octet_length { + bytes_read += 1; + val = (val << 8) | u64::from(reader.read_u8()?); + } - Ok(Self(val)) + Ok(val) +} + +// Verify that the octet length is nonzero and <= 8 +fn verify_length(first_byte: u8, max_length: u8) -> Result { + // A value of 0b0000_0000 indicates either an invalid VInt, or one with an octet length > 8 + if first_byte == 0b0000_0000 { + err!(BadVintSize); + } + + let octet_length = (VInt::<()>::MAX_OCTET_LENGTH as u32) - first_byte.ilog2(); + if octet_length > 8 || octet_length as u8 > max_length { + err!(BadVintSize); } - /// Parse a `VInt` from a reader, given the element ID + Ok(octet_length) +} + +fn octet_length(mut value: u64) -> u8 { + let mut octets = 0; + loop { + octets += 1; + + value >>= VInt::<()>::USABLE_BITS_PER_BYTE; + if value == 0 { + break; + } + } + + octets +} + +/// An EBML element ID +/// +/// An `ElementId` is a [`VInt`], but with the following conditions: +/// +/// * The `VINT_MARKER` is retained after parsing +/// * When encoding, the minimum number of octets must be used +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct ElementId(pub(crate) u64); + +impl ElementId { + /// Parse an `ElementId` from a reader /// - /// An element ID is parsed similarly to a normal `VInt`, but the `VINT_MARKER` is not - /// removed. + /// An element ID is parsed similarly to a normal [`VInt`], but the `VINT_MARKER` is retained. /// /// # Errors /// - /// * `uint` cannot fit within the maximum width + /// * The ID cannot fit within the maximum width /// /// # Examples /// /// ```rust - /// use lofty::ebml::VInt; + /// use lofty::ebml::ElementId; /// /// # fn main() -> lofty::error::Result<()> { /// // Parse the EBML header element ID /// let mut reader = &[0x1A, 0x45, 0xDF, 0xA3][..]; - /// let vint = VInt::parse_from_element_id(&mut reader, 8)?; - /// assert_eq!(vint.value(), 0x1A45DFA3); + /// let id = ElementId::parse(&mut reader, 8)?; + /// assert_eq!(id, 0x1A45DFA3); /// # Ok(()) } - pub fn parse_from_element_id(reader: &mut R, max_id_length: u8) -> Result + pub fn parse(reader: &mut R, max_id_length: u8) -> Result where R: Read, { - let start = reader.read_u8()?; - let octet_length = Self::verify_length(start, max_id_length)?; - - let mut bytes_read = 1; - let mut val = u64::from(start); - while bytes_read < octet_length { - bytes_read += 1; - val = (val << 8) | u64::from(reader.read_u8()?); - } - + let val = parse_vint(reader, max_id_length, true)?; Ok(Self(val)) } - // Verify that the octet length is nonzero and <= 8 - fn verify_length(first_byte: u8, max_length: u8) -> Result { - // A value of 0b0000_0000 indicates either an invalid VInt, or one with an octet length > 8 - if first_byte == 0b0000_0000 { - err!(BadVintSize); - } - - let octet_length = (Self::MAX_OCTET_LENGTH as u32) - first_byte.ilog2(); - if octet_length > 8 || octet_length as u8 > max_length { - err!(BadVintSize); - } - - Ok(octet_length) - } - - /// Represents the length of the `VInt` in octets - /// - /// NOTE: The value returned will always be <= 8 + /// Get the inner value of the `ElementId` /// /// # Examples /// /// ```rust - /// use lofty::ebml::VInt; + /// use lofty::ebml::ElementId; /// /// # fn main() -> lofty::error::Result<()> { - /// // Anything <= 254 will fit into a single octet - /// let vint = VInt::from_u64(100)?; - /// assert_eq!(vint.octet_length(), 1); - /// - /// // A larger number will need to - /// let vint = VInt::from_u64(500_000)?; - /// assert_eq!(vint.octet_length(), 3); + /// let id = ElementId::parse(&mut &[0x1A, 0x45, 0xDF, 0xA3][..], 8)?; + /// assert_eq!(id.value(), 0x1A45DFA3); /// # Ok(()) } - /// ``` - pub fn octet_length(&self) -> u8 { - let mut octets = 0; - let mut v = self.0; - loop { - octets += 1; - - v >>= Self::USABLE_BITS_PER_BYTE; - if v == 0 { - break; - } - } - - octets + pub fn value(&self) -> u64 { + self.0 } - /// Converts the `VInt` into a byte Vec + /// Converts the `ElementId` into a byte Vec /// - /// `length` can be used to specify the number of bytes to use to write the integer. If unspecified, - /// the integer will be represented in the minimum number of bytes. + /// Unlike a [`VInt`], an `ElementId` **MUST** be encoded with the shortest possible octet length. + /// + /// * `max_length` can be used to specify the maximum number of octets the number should + /// occupy. /// /// # Errors /// - /// * `length` > 8 or `length` == 0 + /// * The octet length is greater than `max_length` (if provided) /// * Unable to write to the buffer /// /// # Examples /// /// ```rust - /// use lofty::ebml::VInt; + /// use lofty::ebml::ElementId; + /// + /// const EBML_ID: [u8; 4] = [0x1A, 0x45, 0xDF, 0xA3]; /// /// # fn main() -> lofty::error::Result<()> { - /// let vint = VInt::from_u64(10)?; - /// let bytes = vint.as_bytes(None)?; + /// let id = ElementId::parse(&mut &EBML_ID[..], 8)?; + /// let bytes = id.as_bytes(None)?; /// - /// assert_eq!(bytes, &[0b1000_1010]); + /// assert_eq!(bytes, &EBML_ID); /// # Ok(()) } /// ``` - pub fn as_bytes(&self, length: Option) -> Result> { - let octets: u8; - if let Some(length) = length { - if length > (Self::MAX_OCTET_LENGTH as u8) || length == 0 { - err!(BadVintSize); - } - - octets = length; - } else { - octets = self.octet_length() - } - - let mut ret = Vec::with_capacity(octets as usize); - - let mut val = self.value(); - - // Add the octet length - val |= 1 << (octets * (Self::USABLE_BITS_PER_BYTE as u8)); - - let mut byte_shift = (octets - 1) as i8; - while byte_shift >= 0 { - ret.write_u8((val >> (byte_shift * 8)) as u8)?; - byte_shift -= 1; - } - - Ok(ret) + pub fn as_bytes(self, max_length: Option) -> Result> { + let mut buf = Vec::with_capacity(8); + self.write_to(max_length, &mut buf)?; + Ok(buf) } - #[inline] - pub(crate) fn saturating_sub(self, other: u64) -> Self { - Self(self.0.saturating_sub(other)) - } -} - -impl Sub for VInt { - type Output = Self; - - fn sub(self, other: Self) -> Self::Output { - Self(self.0 - other.0) + // Same as writing a VInt, but we need to remove the VINT_MARKER from the value first + pub(crate) fn write_to(self, max_length: Option, writer: &mut W) -> Result<()> { + let mut val = self.0; + val ^= 1 << val.ilog2(); + VInt::<()>::write_to(val, None, max_length, writer)?; + Ok(()) } } -impl Add for VInt { - type Output = Self; - - fn add(self, other: Self) -> Self::Output { - let val = self.0 + other.0; - assert!(val <= Self::MAX, "VInt overflow"); - - Self(val) +impl PartialEq for ElementId { + fn eq(&self, other: &u64) -> bool { + self.0 == *other } } -impl PartialEq for VInt { - fn eq(&self, other: &u64) -> bool { - self.0 == *other +impl UpperHex for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::UpperHex::fmt(&self.0, f) } } @@ -331,7 +424,7 @@ mod tests { fn bytes_to_vint() { for representation in VALID_REPRESENTATIONS_OF_2 { assert_eq!( - VInt::parse(&mut Cursor::new(representation), 8) + VInt::::parse(&mut Cursor::new(representation), 8) .unwrap() .value(), 2 @@ -342,9 +435,10 @@ mod tests { #[test_log::test] fn vint_to_bytes() { for representation in VALID_REPRESENTATIONS_OF_2 { - let vint = VInt::parse(&mut Cursor::new(representation), 8).unwrap(); + let vint = VInt::::parse(&mut Cursor::new(representation), 8).unwrap(); assert_eq!( - vint.as_bytes(Some(representation.len() as u8)).unwrap(), + vint.as_bytes(Some(representation.len() as u8), None) + .unwrap(), representation ); } @@ -352,25 +446,26 @@ mod tests { #[test_log::test] fn large_integers_should_fail() { - assert!(VInt::from_u64(u64::MAX).is_err()); + assert!(VInt::try_from(u64::MAX).is_err()); + assert!(VInt::try_from(i64::MAX).is_err()); let mut acc = 1000; for _ in 0..16 { - assert!(VInt::from_u64(u64::MAX - acc).is_err()); + assert!(VInt::try_from(u64::MAX - acc).is_err()); acc *= 10; } } #[test_log::test] fn maximum_possible_representable_vint() { - assert!(VInt::from_u64(u64::MAX >> 8).is_ok()); + assert!(VInt::try_from(u64::MAX >> 8).is_ok()); } #[test_log::test] fn octet_lengths() { let n = u64::MAX >> 8; for i in 1u8..=7 { - assert_eq!(VInt::from_u64(n >> (i * 7)).unwrap().octet_length(), 8 - i); + assert_eq!(VInt::try_from(n >> (i * 7)).unwrap().octet_length(), 8 - i); } } } diff --git a/lofty/src/lib.rs b/lofty/src/lib.rs index 64796fbf..986b128a 100644 --- a/lofty/src/lib.rs +++ b/lofty/src/lib.rs @@ -104,6 +104,7 @@ // proc macro hacks extern crate self as lofty; + pub(crate) mod _this_is_internal {} pub mod config; diff --git a/lofty_attr/src/lib.rs b/lofty_attr/src/lib.rs index 47daafe2..ad96a45f 100644 --- a/lofty_attr/src/lib.rs +++ b/lofty_attr/src/lib.rs @@ -87,7 +87,7 @@ pub fn ebml_master_elements(input: TokenStream) -> TokenStream { let id = child.info.id; let data_type = &child.info.data_type; quote! { - (VInt(#id), ChildElementDescriptor { + (ElementId(#id), ChildElementDescriptor { ident: ElementIdent::#readable_ident, data_type: ElementDataType::#data_type, }) @@ -96,7 +96,7 @@ pub fn ebml_master_elements(input: TokenStream) -> TokenStream { quote! { m.insert( - VInt(#id), + ElementId(#id), MasterElement { id: ElementIdent::#readable_ident, children: &[#( #children ),*][..] @@ -119,8 +119,8 @@ pub fn ebml_master_elements(input: TokenStream) -> TokenStream { #( #ident_variants )* } - fn master_elements() -> &'static ::std::collections::HashMap { - static INSTANCE: ::std::sync::OnceLock<::std::collections::HashMap> = ::std::sync::OnceLock::new(); + fn master_elements() -> &'static ::std::collections::HashMap { + static INSTANCE: ::std::sync::OnceLock<::std::collections::HashMap> = ::std::sync::OnceLock::new(); INSTANCE.get_or_init(|| { let mut m = ::std::collections::HashMap::new(); #( #elements_map_inserts )*