mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-12 21:52:33 +00:00
ID3v2: Stop eagerly allocating frame content
This commit is contained in:
parent
d4943b4108
commit
a6b7823d73
13 changed files with 276 additions and 154 deletions
|
@ -8,31 +8,38 @@ use crate::id3::v2::ID3v2Version;
|
||||||
use crate::macros::err;
|
use crate::macros::err;
|
||||||
use crate::util::text::TextEncoding;
|
use crate::util::text::TextEncoding;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub(super) fn parse_content(
|
pub(super) fn parse_content<R: Read>(
|
||||||
content: &mut &[u8],
|
reader: &mut R,
|
||||||
id: &str,
|
id: &str,
|
||||||
version: ID3v2Version,
|
version: ID3v2Version,
|
||||||
) -> Result<Option<FrameValue>> {
|
) -> Result<Option<FrameValue>> {
|
||||||
Ok(match id {
|
Ok(match id {
|
||||||
// The ID was previously upgraded, but the content remains unchanged, so version is necessary
|
// The ID was previously upgraded, but the content remains unchanged, so version is necessary
|
||||||
"APIC" => {
|
"APIC" => {
|
||||||
let attached_picture = AttachedPictureFrame::parse(content, version)?;
|
let attached_picture = AttachedPictureFrame::parse(reader, version)?;
|
||||||
Some(FrameValue::Picture(attached_picture))
|
Some(FrameValue::Picture(attached_picture))
|
||||||
},
|
},
|
||||||
"TXXX" => ExtendedTextFrame::parse(content, version)?.map(FrameValue::UserText),
|
"TXXX" => ExtendedTextFrame::parse(reader, version)?.map(FrameValue::UserText),
|
||||||
"WXXX" => ExtendedUrlFrame::parse(content, version)?.map(FrameValue::UserUrl),
|
"WXXX" => ExtendedUrlFrame::parse(reader, version)?.map(FrameValue::UserUrl),
|
||||||
"COMM" => CommentFrame::parse(content, version)?.map(FrameValue::Comment),
|
"COMM" => CommentFrame::parse(reader, version)?.map(FrameValue::Comment),
|
||||||
"USLT" => UnsynchronizedTextFrame::parse(content, version)?.map(FrameValue::UnsynchronizedText),
|
"USLT" => UnsynchronizedTextFrame::parse(reader, version)?.map(FrameValue::UnsynchronizedText),
|
||||||
"UFID" => UniqueFileIdentifierFrame::parse(content)?.map(FrameValue::UniqueFileIdentifier),
|
"UFID" => UniqueFileIdentifierFrame::parse(reader)?.map(FrameValue::UniqueFileIdentifier),
|
||||||
_ if id.starts_with('T') => TextInformationFrame::parse(content, version)?.map(FrameValue::Text),
|
_ if id.starts_with('T') => TextInformationFrame::parse(reader, version)?.map(FrameValue::Text),
|
||||||
// Apple proprietary frames
|
// Apple proprietary frames
|
||||||
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
|
// WFED (Podcast URL), GRP1 (Grouping), MVNM (Movement Name), MVIN (Movement Number)
|
||||||
"WFED" | "GRP1" | "MVNM" | "MVIN" => TextInformationFrame::parse(content, version)?.map(FrameValue::Text),
|
"WFED" | "GRP1" | "MVNM" | "MVIN" => TextInformationFrame::parse(reader, version)?.map(FrameValue::Text),
|
||||||
_ if id.starts_with('W') => UrlLinkFrame::parse(content)?.map(FrameValue::Url),
|
_ if id.starts_with('W') => UrlLinkFrame::parse(reader)?.map(FrameValue::Url),
|
||||||
"POPM" => Some(FrameValue::Popularimeter(Popularimeter::parse(content)?)),
|
"POPM" => Some(FrameValue::Popularimeter(Popularimeter::parse(reader)?)),
|
||||||
// SYLT, GEOB, and any unknown frames
|
// SYLT, GEOB, and any unknown frames
|
||||||
_ => Some(FrameValue::Binary(content.to_vec())),
|
_ => {
|
||||||
|
let mut content = Vec::new();
|
||||||
|
reader.read_to_end(&mut content)?;
|
||||||
|
|
||||||
|
Some(FrameValue::Binary(content))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ impl From<TagItem> for Option<Frame<'static>> {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(FrameId::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
|
(FrameId::Valid(ref s), ItemValue::Binary(text)) if s == "POPM" => {
|
||||||
FrameValue::Popularimeter(Popularimeter::parse(&text).ok()?)
|
FrameValue::Popularimeter(Popularimeter::parse(&mut &text[..]).ok()?)
|
||||||
},
|
},
|
||||||
(_, item_value) => {
|
(_, item_value) => {
|
||||||
let Ok(value) = item_value.try_into() else {
|
let Ok(value) = item_value.try_into() else {
|
||||||
|
@ -496,7 +496,7 @@ impl<'a> TryFrom<&'a TagItem> for FrameRef<'a> {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
("POPM", ItemValue::Binary(contents)) => {
|
("POPM", ItemValue::Binary(contents)) => {
|
||||||
FrameValue::Popularimeter(Popularimeter::parse(contents)?)
|
FrameValue::Popularimeter(Popularimeter::parse(&mut &contents[..])?)
|
||||||
},
|
},
|
||||||
(_, value) => value.try_into()?,
|
(_, value) => value.try_into()?,
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,8 +2,8 @@ use super::header::{parse_header, parse_v2_header};
|
||||||
use super::Frame;
|
use super::Frame;
|
||||||
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
|
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
|
||||||
use crate::id3::v2::frame::content::parse_content;
|
use crate::id3::v2::frame::content::parse_content;
|
||||||
use crate::id3::v2::util::synchsafe::SynchsafeInteger;
|
use crate::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream};
|
||||||
use crate::id3::v2::{FrameValue, ID3v2Version};
|
use crate::id3::v2::{FrameFlags, FrameId, FrameValue, ID3v2Version};
|
||||||
use crate::macros::try_vec;
|
use crate::macros::try_vec;
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -58,50 +58,122 @@ impl<'a> Frame<'a> {
|
||||||
size -= 4;
|
size -= 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut content = try_vec![0; size as usize];
|
|
||||||
reader.read_exact(&mut content)?;
|
|
||||||
|
|
||||||
if flags.unsynchronisation {
|
|
||||||
content = crate::id3::v2::util::synchsafe::unsynch_content(content.as_slice())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "id3v2_compression_support")]
|
|
||||||
if flags.compression {
|
|
||||||
// This is guaranteed to be set above
|
|
||||||
let data_length_indicator = flags.data_length_indicator.unwrap() as usize;
|
|
||||||
|
|
||||||
let mut decompressed = Vec::with_capacity(data_length_indicator);
|
|
||||||
flate2::read::ZlibDecoder::new(&content[..]).read_to_end(&mut decompressed)?;
|
|
||||||
if data_length_indicator != decompressed.len() {
|
|
||||||
log::debug!("Frame data length indicator does not match true decompressed length");
|
|
||||||
}
|
|
||||||
|
|
||||||
content = decompressed
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "id3v2_compression_support"))]
|
|
||||||
if flags.compression {
|
|
||||||
return Err(Id3v2Error::new(Id3v2ErrorKind::CompressedFrameEncountered).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frames must have at least 1 byte, *after* all of the additional data flags can provide
|
// Frames must have at least 1 byte, *after* all of the additional data flags can provide
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into());
|
return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = if flags.encryption.is_some() {
|
// Restrict the reader to the frame content
|
||||||
if flags.data_length_indicator.is_none() {
|
let mut reader = reader.take(u64::from(size));
|
||||||
return Err(Id3v2Error::new(Id3v2ErrorKind::MissingDataLengthIndicator).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(FrameValue::Binary(content))
|
// It seems like the flags are applied in the order:
|
||||||
} else {
|
//
|
||||||
parse_content(&mut &content[..], id.as_str(), version)?
|
// unsynchronization -> compression -> encryption
|
||||||
};
|
//
|
||||||
|
// Which all have their own needs, so this gets a little messy...
|
||||||
|
match flags {
|
||||||
|
// Possible combinations:
|
||||||
|
//
|
||||||
|
// * unsynchronized + compressed + encrypted
|
||||||
|
// * unsynchronized + compressed
|
||||||
|
// * unsynchronized + encrypted
|
||||||
|
// * unsynchronized
|
||||||
|
FrameFlags {
|
||||||
|
unsynchronisation: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mut unsynchronized_reader = UnsynchronizedStream::new(reader);
|
||||||
|
|
||||||
match value {
|
if flags.compression {
|
||||||
Some(value) => Ok((Some(Self { id, value, flags }), false)),
|
let mut compression_reader = handle_compression(unsynchronized_reader)?;
|
||||||
None => Ok((None, false)),
|
|
||||||
|
if flags.encryption.is_some() {
|
||||||
|
return handle_encryption(&mut compression_reader, size, id, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse_frame(&mut compression_reader, id, flags, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.encryption.is_some() {
|
||||||
|
return handle_encryption(&mut unsynchronized_reader, size, id, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse_frame(&mut unsynchronized_reader, id, flags, version);
|
||||||
|
},
|
||||||
|
// Possible combinations:
|
||||||
|
//
|
||||||
|
// * compressed + encrypted
|
||||||
|
// * compressed
|
||||||
|
FrameFlags {
|
||||||
|
compression: true, ..
|
||||||
|
} => {
|
||||||
|
let mut compression_reader = handle_compression(reader)?;
|
||||||
|
|
||||||
|
if flags.encryption.is_some() {
|
||||||
|
return handle_encryption(&mut compression_reader, size, id, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse_frame(&mut compression_reader, id, flags, version);
|
||||||
|
},
|
||||||
|
// Possible combinations:
|
||||||
|
//
|
||||||
|
// * encrypted
|
||||||
|
FrameFlags {
|
||||||
|
encryption: Some(_),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
return handle_encryption(&mut reader, size, id, flags);
|
||||||
|
},
|
||||||
|
// Everything else that doesn't have special flags
|
||||||
|
_ => {
|
||||||
|
return parse_frame(&mut reader, id, flags, version);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "id3v2_compression_support")]
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
|
fn handle_compression<R: Read>(reader: R) -> Result<flate2::read::ZlibDecoder<R>> {
|
||||||
|
Ok(flate2::read::ZlibDecoder::new(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "id3v2_compression_support"))]
|
||||||
|
fn handle_compression<R>(reader: &mut R) -> flate2::read::ZlibDecoder<R> {
|
||||||
|
return Err(Id3v2Error::new(Id3v2ErrorKind::CompressedFrameEncountered).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_encryption<R: Read>(
|
||||||
|
reader: &mut R,
|
||||||
|
size: u32,
|
||||||
|
id: FrameId<'static>,
|
||||||
|
flags: FrameFlags,
|
||||||
|
) -> Result<(Option<Frame<'static>>, bool)> {
|
||||||
|
if flags.data_length_indicator.is_none() {
|
||||||
|
return Err(Id3v2Error::new(Id3v2ErrorKind::MissingDataLengthIndicator).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut content = try_vec![0; size as usize];
|
||||||
|
reader.read_exact(&mut content)?;
|
||||||
|
|
||||||
|
let encrypted_frame = Frame {
|
||||||
|
id,
|
||||||
|
value: FrameValue::Binary(content),
|
||||||
|
flags,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Nothing further we can do with encrypted frames
|
||||||
|
Ok((Some(encrypted_frame), false))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_frame<R: Read>(
|
||||||
|
reader: &mut R,
|
||||||
|
id: FrameId<'static>,
|
||||||
|
flags: FrameFlags,
|
||||||
|
version: ID3v2Version,
|
||||||
|
) -> Result<(Option<Frame<'static>>, bool)> {
|
||||||
|
match parse_content(reader, id.as_str(), version)? {
|
||||||
|
Some(value) => Ok((Some(Frame { id, value, flags }), false)),
|
||||||
|
None => Ok((None, false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::picture::{MimeType, Picture, PictureType};
|
||||||
use crate::util::text::{encode_text, TextEncoding};
|
use crate::util::text::{encode_text, TextEncoding};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::io::{Cursor, Read, Write as _};
|
use std::io::{Read, Write as _};
|
||||||
|
|
||||||
use byteorder::{ReadBytesExt as _, WriteBytesExt as _};
|
use byteorder::{ReadBytesExt as _, WriteBytesExt as _};
|
||||||
|
|
||||||
|
@ -33,17 +33,18 @@ impl AttachedPictureFrame {
|
||||||
/// ID3v2.2:
|
/// ID3v2.2:
|
||||||
///
|
///
|
||||||
/// * The format is not "PNG" or "JPG"
|
/// * The format is not "PNG" or "JPG"
|
||||||
pub fn parse(bytes: &[u8], version: ID3v2Version) -> Result<Self> {
|
pub fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Self>
|
||||||
let mut cursor = Cursor::new(bytes);
|
where
|
||||||
|
R: Read,
|
||||||
let encoding = match TextEncoding::from_u8(cursor.read_u8()?) {
|
{
|
||||||
|
let encoding = match TextEncoding::from_u8(reader.read_u8()?) {
|
||||||
Some(encoding) => encoding,
|
Some(encoding) => encoding,
|
||||||
None => err!(NotAPicture),
|
None => err!(NotAPicture),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mime_type = if version == ID3v2Version::V2 {
|
let mime_type = if version == ID3v2Version::V2 {
|
||||||
let mut format = [0; 3];
|
let mut format = [0; 3];
|
||||||
cursor.read_exact(&mut format)?;
|
reader.read_exact(&mut format)?;
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
[b'P', b'N', b'G'] => MimeType::Png,
|
[b'P', b'N', b'G'] => MimeType::Png,
|
||||||
|
@ -56,18 +57,18 @@ impl AttachedPictureFrame {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(crate::util::text::decode_text(&mut cursor, TextEncoding::UTF8, true)?.text_or_none())
|
(crate::util::text::decode_text(reader, TextEncoding::UTF8, true)?.text_or_none())
|
||||||
.map_or(MimeType::None, |mime_type| MimeType::from_str(&mime_type))
|
.map_or(MimeType::None, |mime_type| MimeType::from_str(&mime_type))
|
||||||
};
|
};
|
||||||
|
|
||||||
let pic_type = PictureType::from_u8(cursor.read_u8()?);
|
let pic_type = PictureType::from_u8(reader.read_u8()?);
|
||||||
|
|
||||||
let description = crate::util::text::decode_text(&mut cursor, encoding, true)?
|
let description = crate::util::text::decode_text(reader, encoding, true)?
|
||||||
.text_or_none()
|
.text_or_none()
|
||||||
.map(Cow::from);
|
.map(Cow::from);
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
cursor.read_to_end(&mut data)?;
|
reader.read_to_end(&mut data)?;
|
||||||
|
|
||||||
let picture = Picture {
|
let picture = Picture {
|
||||||
pic_type,
|
pic_type,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::id3::v2::ID3v2Version;
|
||||||
use crate::util::text::{decode_text, encode_text, read_to_terminator, utf16_decode, TextEncoding};
|
use crate::util::text::{decode_text, encode_text, read_to_terminator, utf16_decode, TextEncoding};
|
||||||
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::io::{Cursor, Read};
|
use std::io::Read;
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
|
@ -48,53 +48,58 @@ impl ExtendedTextFrame {
|
||||||
/// ID3v2.2:
|
/// ID3v2.2:
|
||||||
///
|
///
|
||||||
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
||||||
pub fn parse(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Option<Self>>
|
||||||
if content.len() < 2 {
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let Ok(encoding_byte) = reader.read_u8() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
};
|
||||||
|
|
||||||
let mut content = &mut &content[..];
|
let encoding = verify_encoding(encoding_byte, version)?;
|
||||||
let encoding = verify_encoding(content.read_u8()?, version)?;
|
let description = decode_text(reader, encoding, true)?;
|
||||||
|
|
||||||
let mut endianness: fn([u8; 2]) -> u16 = u16::from_le_bytes;
|
|
||||||
if encoding == TextEncoding::UTF16 {
|
|
||||||
let mut cursor = Cursor::new(content);
|
|
||||||
let mut bom = [0; 2];
|
|
||||||
cursor.read_exact(&mut bom)?;
|
|
||||||
|
|
||||||
match [bom[0], bom[1]] {
|
|
||||||
[0xFF, 0xFE] => endianness = u16::from_le_bytes,
|
|
||||||
[0xFE, 0xFF] => endianness = u16::from_be_bytes,
|
|
||||||
// We'll catch an invalid BOM below
|
|
||||||
_ => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
content = cursor.into_inner();
|
|
||||||
}
|
|
||||||
|
|
||||||
let description = decode_text(content, encoding, true)?.content;
|
|
||||||
|
|
||||||
let frame_content;
|
let frame_content;
|
||||||
|
if encoding != TextEncoding::UTF16 {
|
||||||
|
frame_content = decode_text(reader, encoding, false)?.content;
|
||||||
|
|
||||||
|
return Ok(Some(ExtendedTextFrame {
|
||||||
|
encoding,
|
||||||
|
description: description.content,
|
||||||
|
content: frame_content,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// It's possible for the description to be the only string with a BOM
|
// It's possible for the description to be the only string with a BOM
|
||||||
if encoding == TextEncoding::UTF16 {
|
'utf16: {
|
||||||
if content.len() >= 2 && (content[..2] == [0xFF, 0xFE] || content[..2] == [0xFE, 0xFF])
|
let bom = description.bom;
|
||||||
{
|
let Some(raw_text) = read_to_terminator(reader, TextEncoding::UTF16) else {
|
||||||
frame_content = decode_text(content, encoding, false)?.content;
|
// Nothing left to do
|
||||||
} else {
|
frame_content = String::new();
|
||||||
frame_content = match read_to_terminator(content, TextEncoding::UTF16) {
|
break 'utf16;
|
||||||
Some(raw_text) => utf16_decode(&raw_text, endianness).map_err(|_| {
|
};
|
||||||
Into::<LoftyError>::into(Id3v2Error::new(Id3v2ErrorKind::BadSyncText))
|
|
||||||
})?,
|
if raw_text.starts_with(&[0xFF, 0xFE]) || raw_text.starts_with(&[0xFE, 0xFF]) {
|
||||||
None => String::new(),
|
frame_content =
|
||||||
}
|
decode_text(&mut &raw_text[..], TextEncoding::UTF16, false)?.content;
|
||||||
|
break 'utf16;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
frame_content = decode_text(content, encoding, false)?.content;
|
let endianness = match bom {
|
||||||
|
[0xFF, 0xFE] => u16::from_le_bytes,
|
||||||
|
[0xFE, 0xFF] => u16::from_be_bytes,
|
||||||
|
// Handled in description decoding
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
frame_content = utf16_decode(&raw_text, endianness).map_err(|_| {
|
||||||
|
Into::<LoftyError>::into(Id3v2Error::new(Id3v2ErrorKind::BadSyncText))
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(ExtendedTextFrame {
|
Ok(Some(ExtendedTextFrame {
|
||||||
encoding,
|
encoding,
|
||||||
description,
|
description: description.content,
|
||||||
content: frame_content,
|
content: frame_content,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::id3::v2::ID3v2Version;
|
||||||
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
||||||
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
|
@ -47,16 +48,17 @@ impl ExtendedUrlFrame {
|
||||||
/// ID3v2.2:
|
/// ID3v2.2:
|
||||||
///
|
///
|
||||||
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
||||||
pub fn parse(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Option<Self>>
|
||||||
if content.len() < 2 {
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let Ok(encoding_byte) = reader.read_u8() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
};
|
||||||
|
|
||||||
let content = &mut &content[..];
|
let encoding = verify_encoding(encoding_byte, version)?;
|
||||||
|
let description = decode_text(reader, encoding, true)?.content;
|
||||||
let encoding = verify_encoding(content.read_u8()?, version)?;
|
let content = decode_text(reader, TextEncoding::Latin1, false)?.content;
|
||||||
let description = decode_text(content, encoding, true)?.content;
|
|
||||||
let content = decode_text(content, TextEncoding::Latin1, false)?.content;
|
|
||||||
|
|
||||||
Ok(Some(ExtendedUrlFrame {
|
Ok(Some(ExtendedUrlFrame {
|
||||||
encoding,
|
encoding,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
|
|
||||||
use crate::error::{Id3v2Error, Id3v2ErrorKind};
|
use crate::error::{Id3v2Error, Id3v2ErrorKind};
|
||||||
use crate::util::text::{decode_text, encode_text};
|
use crate::util::text::{decode_text, encode_text};
|
||||||
use crate::{Result, TextEncoding};
|
use crate::{Result, TextEncoding};
|
||||||
|
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
/// An `ID3v2` unique file identifier frame (UFID).
|
/// An `ID3v2` unique file identifier frame (UFID).
|
||||||
#[derive(Clone, Debug, Eq)]
|
#[derive(Clone, Debug, Eq)]
|
||||||
pub struct UniqueFileIdentifierFrame {
|
pub struct UniqueFileIdentifierFrame {
|
||||||
|
@ -19,15 +20,16 @@ impl UniqueFileIdentifierFrame {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Owner is missing or improperly encoded
|
/// Owner is missing or improperly encoded
|
||||||
pub fn parse(input: &mut &[u8]) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R) -> Result<Option<Self>>
|
||||||
if input.is_empty() {
|
where
|
||||||
return Ok(None);
|
R: Read,
|
||||||
}
|
{
|
||||||
|
let Some(owner) = decode_text(reader, TextEncoding::Latin1, true)?.text_or_none() else {
|
||||||
let Some(owner) = decode_text(input, TextEncoding::Latin1, true)?.text_or_none() else {
|
|
||||||
return Err(Id3v2Error::new(Id3v2ErrorKind::MissingUfidOwner).into());
|
return Err(Id3v2Error::new(Id3v2ErrorKind::MissingUfidOwner).into());
|
||||||
};
|
};
|
||||||
let identifier = input.to_vec();
|
|
||||||
|
let mut identifier = Vec::new();
|
||||||
|
reader.read_to_end(&mut identifier)?;
|
||||||
|
|
||||||
Ok(Some(Self { owner, identifier }))
|
Ok(Some(Self { owner, identifier }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,19 +19,21 @@ struct LanguageFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageFrame {
|
impl LanguageFrame {
|
||||||
fn parse(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
|
fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Option<Self>>
|
||||||
if content.len() < 5 {
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let Ok(encoding_byte) = reader.read_u8() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
};
|
||||||
|
|
||||||
let content = &mut &content[..];
|
let encoding = verify_encoding(encoding_byte, version)?;
|
||||||
let encoding = verify_encoding(content.read_u8()?, version)?;
|
|
||||||
|
|
||||||
let mut language = [0; 3];
|
let mut language = [0; 3];
|
||||||
content.read_exact(&mut language)?;
|
reader.read_exact(&mut language)?;
|
||||||
|
|
||||||
let description = decode_text(content, encoding, true)?.content;
|
let description = decode_text(reader, encoding, true)?.content;
|
||||||
let content = decode_text(content, encoding, false)?.content;
|
let content = decode_text(reader, encoding, false)?.content;
|
||||||
|
|
||||||
Ok(Some(Self {
|
Ok(Some(Self {
|
||||||
encoding,
|
encoding,
|
||||||
|
@ -111,8 +113,11 @@ impl CommentFrame {
|
||||||
/// ID3v2.2:
|
/// ID3v2.2:
|
||||||
///
|
///
|
||||||
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
||||||
pub fn parse(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Option<Self>>
|
||||||
Ok(LanguageFrame::parse(content, version)?.map(Into::into))
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
Ok(LanguageFrame::parse(reader, version)?.map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a [`CommentFrame`] to a byte vec
|
/// Convert a [`CommentFrame`] to a byte vec
|
||||||
|
@ -183,8 +188,11 @@ impl UnsynchronizedTextFrame {
|
||||||
/// ID3v2.2:
|
/// ID3v2.2:
|
||||||
///
|
///
|
||||||
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
||||||
pub fn parse(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Option<Self>>
|
||||||
Ok(LanguageFrame::parse(content, version)?.map(Into::into))
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
Ok(LanguageFrame::parse(reader, version)?.map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a [`UnsynchronizedTextFrame`] to a byte vec
|
/// Convert a [`UnsynchronizedTextFrame`] to a byte vec
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
/// The contents of a popularimeter ("POPM") frame
|
/// The contents of a popularimeter ("POPM") frame
|
||||||
///
|
///
|
||||||
|
@ -30,21 +32,25 @@ impl Popularimeter {
|
||||||
///
|
///
|
||||||
/// * Email is improperly encoded
|
/// * Email is improperly encoded
|
||||||
/// * `bytes` doesn't contain enough data
|
/// * `bytes` doesn't contain enough data
|
||||||
pub fn parse(mut bytes: &[u8]) -> Result<Self> {
|
pub fn parse<R>(reader: &mut R) -> Result<Self>
|
||||||
let content = &mut bytes;
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let email = decode_text(reader, TextEncoding::Latin1, true)?;
|
||||||
|
let rating = reader.read_u8()?;
|
||||||
|
|
||||||
let email = decode_text(content, TextEncoding::Latin1, true)?;
|
let mut counter_content = Vec::new();
|
||||||
let rating = content.read_u8()?;
|
reader.read_to_end(&mut counter_content)?;
|
||||||
|
|
||||||
let counter;
|
let counter;
|
||||||
let remaining_size = content.len();
|
let remaining_size = counter_content.len();
|
||||||
if remaining_size > 8 {
|
if remaining_size > 8 {
|
||||||
counter = u64::MAX;
|
counter = u64::MAX;
|
||||||
} else {
|
} else {
|
||||||
let mut counter_bytes = [0; 8];
|
let mut counter_bytes = [0; 8];
|
||||||
let counter_start_pos = 8 - remaining_size;
|
let counter_start_pos = 8 - remaining_size;
|
||||||
|
|
||||||
counter_bytes[counter_start_pos..].copy_from_slice(content);
|
counter_bytes[counter_start_pos..].copy_from_slice(&counter_content);
|
||||||
counter = u64::from_be_bytes(counter_bytes);
|
counter = u64::from_be_bytes(counter_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ use crate::util::text::{decode_text, encode_text, TextEncoding};
|
||||||
|
|
||||||
use byteorder::ReadBytesExt;
|
use byteorder::ReadBytesExt;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
/// An `ID3v2` text frame
|
/// An `ID3v2` text frame
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct TextInformationFrame {
|
pub struct TextInformationFrame {
|
||||||
|
@ -26,14 +28,16 @@ impl TextInformationFrame {
|
||||||
/// ID3v2.2:
|
/// ID3v2.2:
|
||||||
///
|
///
|
||||||
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
/// * The encoding is not [`TextEncoding::Latin1`] or [`TextEncoding::UTF16`]
|
||||||
pub fn parse(content: &[u8], version: ID3v2Version) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R, version: ID3v2Version) -> Result<Option<Self>>
|
||||||
if content.len() < 2 {
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let Ok(encoding_byte) = reader.read_u8() else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
};
|
||||||
|
|
||||||
let content = &mut &content[..];
|
let encoding = verify_encoding(encoding_byte, version)?;
|
||||||
let encoding = verify_encoding(content.read_u8()?, version)?;
|
let value = decode_text(reader, encoding, true)?.content;
|
||||||
let value = decode_text(content, encoding, true)?.content;
|
|
||||||
|
|
||||||
Ok(Some(TextInformationFrame { encoding, value }))
|
Ok(Some(TextInformationFrame { encoding, value }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
use crate::util::text::{decode_text, encode_text, TextEncoding};
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
/// An `ID3v2` URL frame
|
/// An `ID3v2` URL frame
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct UrlLinkFrame(pub(crate) String);
|
pub struct UrlLinkFrame(pub(crate) String);
|
||||||
|
@ -13,14 +15,16 @@ impl UrlLinkFrame {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// * Unable to decode the text as [`TextEncoding::Latin1`]
|
/// * Unable to decode the text as [`TextEncoding::Latin1`]
|
||||||
pub fn parse(content: &[u8]) -> Result<Option<Self>> {
|
pub fn parse<R>(reader: &mut R) -> Result<Option<Self>>
|
||||||
if content.is_empty() {
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let url = decode_text(reader, TextEncoding::Latin1, true)?;
|
||||||
|
if url.bytes_read == 0 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = decode_text(&mut &content[..], TextEncoding::Latin1, true)?.content;
|
Ok(Some(UrlLinkFrame(url.content)))
|
||||||
|
|
||||||
Ok(Some(UrlLinkFrame(url)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert an [`UrlLinkFrame`] to a byte vec
|
/// Convert an [`UrlLinkFrame`] to a byte vec
|
||||||
|
|
|
@ -41,6 +41,7 @@ impl TextEncoding {
|
||||||
pub(crate) struct DecodeTextResult {
|
pub(crate) struct DecodeTextResult {
|
||||||
pub(crate) content: String,
|
pub(crate) content: String,
|
||||||
pub(crate) bytes_read: usize,
|
pub(crate) bytes_read: usize,
|
||||||
|
pub(crate) bom: [u8; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecodeTextResult {
|
impl DecodeTextResult {
|
||||||
|
@ -56,6 +57,7 @@ impl DecodeTextResult {
|
||||||
const EMPTY_DECODED_TEXT: DecodeTextResult = DecodeTextResult {
|
const EMPTY_DECODED_TEXT: DecodeTextResult = DecodeTextResult {
|
||||||
content: String::new(),
|
content: String::new(),
|
||||||
bytes_read: 0,
|
bytes_read: 0,
|
||||||
|
bom: [0, 0],
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn decode_text<R>(
|
pub(crate) fn decode_text<R>(
|
||||||
|
@ -93,6 +95,7 @@ where
|
||||||
raw_bytes = bytes;
|
raw_bytes = bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut bom = [0, 0];
|
||||||
let read_string = match encoding {
|
let read_string = match encoding {
|
||||||
TextEncoding::Latin1 => raw_bytes.iter().map(|c| *c as char).collect::<String>(),
|
TextEncoding::Latin1 => raw_bytes.iter().map(|c| *c as char).collect::<String>(),
|
||||||
TextEncoding::UTF16 => {
|
TextEncoding::UTF16 => {
|
||||||
|
@ -105,8 +108,14 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
match (raw_bytes[0], raw_bytes[1]) {
|
match (raw_bytes[0], raw_bytes[1]) {
|
||||||
(0xFE, 0xFF) => utf16_decode(&raw_bytes[2..], u16::from_be_bytes)?,
|
(0xFE, 0xFF) => {
|
||||||
(0xFF, 0xFE) => utf16_decode(&raw_bytes[2..], u16::from_le_bytes)?,
|
bom = [0xFE, 0xFF];
|
||||||
|
utf16_decode(&raw_bytes[2..], u16::from_be_bytes)?
|
||||||
|
},
|
||||||
|
(0xFF, 0xFE) => {
|
||||||
|
bom = [0xFF, 0xFE];
|
||||||
|
utf16_decode(&raw_bytes[2..], u16::from_le_bytes)?
|
||||||
|
},
|
||||||
_ => err!(TextDecode("UTF-16 string has an invalid byte order mark")),
|
_ => err!(TextDecode("UTF-16 string has an invalid byte order mark")),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -122,6 +131,7 @@ where
|
||||||
Ok(DecodeTextResult {
|
Ok(DecodeTextResult {
|
||||||
content: read_string,
|
content: read_string,
|
||||||
bytes_read,
|
bytes_read,
|
||||||
|
bom,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +271,8 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(be_utf16_decode, le_utf16_decode);
|
assert_eq!(be_utf16_decode.content, le_utf16_decode.content);
|
||||||
|
assert_eq!(be_utf16_decode.bytes_read, le_utf16_decode.bytes_read);
|
||||||
assert_eq!(be_utf16_decode.content, TEST_STRING.to_string());
|
assert_eq!(be_utf16_decode.content, TEST_STRING.to_string());
|
||||||
|
|
||||||
let utf8_decode =
|
let utf8_decode =
|
||||||
|
|
|
@ -28,7 +28,7 @@ fn create_original_picture() -> Picture {
|
||||||
fn id3v24_apic() {
|
fn id3v24_apic() {
|
||||||
let buf = get_buf("tests/picture/assets/png_640x628.apic");
|
let buf = get_buf("tests/picture/assets/png_640x628.apic");
|
||||||
|
|
||||||
let apic = AttachedPictureFrame::parse(&buf, ID3v2Version::V4).unwrap();
|
let apic = AttachedPictureFrame::parse(&mut &buf[..], ID3v2Version::V4).unwrap();
|
||||||
|
|
||||||
assert_eq!(create_original_picture(), apic.picture);
|
assert_eq!(create_original_picture(), apic.picture);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ fn as_apic_bytes() {
|
||||||
fn id3v22_pic() {
|
fn id3v22_pic() {
|
||||||
let buf = get_buf("tests/picture/assets/png_640x628.pic");
|
let buf = get_buf("tests/picture/assets/png_640x628.pic");
|
||||||
|
|
||||||
let pic = AttachedPictureFrame::parse(&buf, ID3v2Version::V2).unwrap();
|
let pic = AttachedPictureFrame::parse(&mut &buf[..], ID3v2Version::V2).unwrap();
|
||||||
|
|
||||||
assert_eq!(create_original_picture(), pic.picture);
|
assert_eq!(create_original_picture(), pic.picture);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue