ParseOptions: Add ParseOptions::read_tags

This makes it possible to use Lofty exclusively for its property reading, which many projects do at this point.

closes #251
This commit is contained in:
Serial 2024-06-11 13:51:00 -04:00 committed by Alex
parent b2c310d709
commit 89dd85c3dc
34 changed files with 388 additions and 122 deletions

View file

@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- **ParseOptions**: `ParseOptions::read_tags` to skip the parsing of tags ([issue](https://github.com/Serial-ATA/lofty-rs/issues/251)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/406))
## [0.20.1] - 2024-07-02 ## [0.20.1] - 2024-07-02
### Fixed ### Fixed

View file

@ -48,6 +48,7 @@ where
stream_len -= u64::from(header.size); stream_len -= u64::from(header.size);
if parse_options.read_tags {
let id3v2 = parse_id3v2(reader, header, parse_mode)?; let id3v2 = parse_id3v2(reader, header, parse_mode)?;
if let Some(existing_tag) = &mut file.id3v2_tag { if let Some(existing_tag) = &mut file.id3v2_tag {
log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag"); log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag");
@ -60,6 +61,7 @@ where
continue; continue;
} }
file.id3v2_tag = Some(id3v2); file.id3v2_tag = Some(id3v2);
}
// Skip over the footer // Skip over the footer
if skip_footer { if skip_footer {
@ -94,7 +96,7 @@ where
} }
#[allow(unused_variables)] #[allow(unused_variables)]
let ID3FindResults(header, id3v1) = find_id3v1(reader, true)?; let ID3FindResults(header, id3v1) = find_id3v1(reader, parse_options.read_tags)?;
if header.is_some() { if header.is_some() {
stream_len -= 128; stream_len -= 128;

View file

@ -8,7 +8,7 @@ use crate::id3::v1::tag::Id3v1Tag;
use crate::id3::v2::read::parse_id3v2; use crate::id3::v2::read::parse_id3v2;
use crate::id3::v2::tag::Id3v2Tag; use crate::id3::v2::tag::Id3v2Tag;
use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2, FindId3v2Config, ID3FindResults}; use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2, FindId3v2Config, ID3FindResults};
use crate::macros::decode_err; use crate::macros::{decode_err, err};
use std::io::{Read, Seek, SeekFrom}; use std::io::{Read, Seek, SeekFrom};
@ -27,10 +27,14 @@ where
let mut id3v1_tag: Option<Id3v1Tag> = None; let mut id3v1_tag: Option<Id3v1Tag> = None;
let mut ape_tag: Option<ApeTag> = None; let mut ape_tag: Option<ApeTag> = None;
let find_id3v2_config = if parse_options.read_tags {
FindId3v2Config::READ_TAG
} else {
FindId3v2Config::NO_READ_TAG
};
// ID3v2 tags are unsupported in APE files, but still possible // ID3v2 tags are unsupported in APE files, but still possible
if let ID3FindResults(Some(header), Some(content)) = if let ID3FindResults(Some(header), content) = find_id3v2(data, find_id3v2_config)? {
find_id3v2(data, FindId3v2Config::READ_TAG)?
{
log::warn!("Encountered an ID3v2 tag. This tag cannot be rewritten to the APE file!"); log::warn!("Encountered an ID3v2 tag. This tag cannot be rewritten to the APE file!");
stream_len -= u64::from(header.size); stream_len -= u64::from(header.size);
@ -40,11 +44,13 @@ where
stream_len -= 10; stream_len -= 10;
} }
if let Some(content) = content {
let reader = &mut &*content; let reader = &mut &*content;
let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?; let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?;
id3v2_tag = Some(id3v2); id3v2_tag = Some(id3v2);
} }
}
let mut found_mac = false; let mut found_mac = false;
let mut mac_start = 0; let mut mac_start = 0;
@ -76,14 +82,16 @@ where
})?; })?;
if &remaining[..4] != b"AGEX" { if &remaining[..4] != b"AGEX" {
decode_err!(@BAIL Ape, "Found incomplete APE tag"); err!(FakeTag)
} }
let ape_header = read_ape_header(data, false)?; let ape_header = read_ape_header(data, false)?;
stream_len -= u64::from(ape_header.size); stream_len -= u64::from(ape_header.size);
if parse_options.read_tags {
let ape = read_ape_tag_with_header(data, ape_header)?; let ape = read_ape_tag_with_header(data, ape_header)?;
ape_tag = Some(ape); ape_tag = Some(ape);
}
}, },
_ => { _ => {
decode_err!(@BAIL Ape, "Invalid data found while reading header, expected any of [\"MAC \", \"APETAGEX\", \"ID3\"]") decode_err!(@BAIL Ape, "Invalid data found while reading header, expected any of [\"MAC \", \"APETAGEX\", \"ID3\"]")
@ -96,7 +104,7 @@ where
// Starts with ['T', 'A', 'G'] // Starts with ['T', 'A', 'G']
// Exactly 128 bytes long (including the identifier) // Exactly 128 bytes long (including the identifier)
#[allow(unused_variables)] #[allow(unused_variables)]
let ID3FindResults(id3v1_header, id3v1) = find_id3v1(data, true)?; let ID3FindResults(id3v1_header, id3v1) = find_id3v1(data, parse_options.read_tags)?;
if id3v1_header.is_some() { if id3v1_header.is_some() {
stream_len -= 128; stream_len -= 128;
@ -117,9 +125,9 @@ where
// Strongly recommended to be at the end of the file // Strongly recommended to be at the end of the file
data.seek(SeekFrom::Current(-32))?; data.seek(SeekFrom::Current(-32))?;
if let Some((tag, header)) = read_ape_tag(data, true)? { if let (tag, Some(header)) = read_ape_tag(data, true, parse_options.read_tags)? {
stream_len -= u64::from(header.size); stream_len -= u64::from(header.size);
ape_tag = Some(tag); ape_tag = tag;
} }
let file_length = data.stream_position()?; let file_length = data.stream_position()?;

View file

@ -622,9 +622,11 @@ mod tests {
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2"); let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
let mut reader = Cursor::new(tag); let mut reader = Cursor::new(tag);
let (parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false) let (Some(parsed_tag), _) =
.unwrap() crate::ape::tag::read::read_ape_tag(&mut reader, false, true).unwrap()
.unwrap(); else {
unreachable!();
};
assert_eq!(expected_tag.len(), parsed_tag.len()); assert_eq!(expected_tag.len(), parsed_tag.len());
@ -638,9 +640,11 @@ mod tests {
let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2"); let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
let mut reader = Cursor::new(tag_bytes); let mut reader = Cursor::new(tag_bytes);
let (parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false) let (Some(parsed_tag), _) =
.unwrap() crate::ape::tag::read::read_ape_tag(&mut reader, false, true).unwrap()
.unwrap(); else {
unreachable!();
};
let mut writer = Vec::new(); let mut writer = Vec::new();
parsed_tag parsed_tag
@ -649,9 +653,11 @@ mod tests {
let mut temp_reader = Cursor::new(writer); let mut temp_reader = Cursor::new(writer);
let (temp_parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut temp_reader, false) let (Some(temp_parsed_tag), _) =
.unwrap() crate::ape::tag::read::read_ape_tag(&mut temp_reader, false, true).unwrap()
.unwrap(); else {
unreachable!()
};
assert_eq!(parsed_tag, temp_parsed_tag); assert_eq!(parsed_tag, temp_parsed_tag);
} }
@ -661,9 +667,10 @@ mod tests {
let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2"); let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
let mut reader = Cursor::new(tag_bytes); let mut reader = Cursor::new(tag_bytes);
let (ape, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false) let (Some(ape), _) = crate::ape::tag::read::read_ape_tag(&mut reader, false, true).unwrap()
.unwrap() else {
.unwrap(); unreachable!()
};
let tag: Tag = ape.into(); let tag: Tag = ape.into();

View file

@ -86,16 +86,20 @@ where
pub(crate) fn read_ape_tag<R: Read + Seek>( pub(crate) fn read_ape_tag<R: Read + Seek>(
reader: &mut R, reader: &mut R,
footer: bool, footer: bool,
) -> Result<Option<(ApeTag, ApeHeader)>> { read_tag: bool,
) -> Result<(Option<ApeTag>, Option<ApeHeader>)> {
let mut ape_preamble = [0; 8]; let mut ape_preamble = [0; 8];
reader.read_exact(&mut ape_preamble)?; reader.read_exact(&mut ape_preamble)?;
let mut ape_tag = None;
if &ape_preamble == APE_PREAMBLE { if &ape_preamble == APE_PREAMBLE {
let ape_header = header::read_ape_header(reader, footer)?; let ape_header = header::read_ape_header(reader, footer)?;
if read_tag {
let ape = read_ape_tag_with_header(reader, ape_header)?; ape_tag = Some(read_ape_tag_with_header(reader, ape_header)?);
return Ok(Some((ape, ape_header)));
} }
Ok(None) return Ok((ape_tag, Some(ape_header)));
}
Ok((None, None))
} }

View file

@ -48,8 +48,8 @@ where
let mut header_ape_tag = (false, (0, 0)); let mut header_ape_tag = (false, (0, 0));
let start = file.stream_position()?; let start = file.stream_position()?;
match read::read_ape_tag(file, false)? { match read::read_ape_tag(file, false, true)? {
Some((mut existing_tag, header)) => { (Some(mut existing_tag), Some(header)) => {
if write_options.respect_read_only { if write_options.respect_read_only {
// Only keep metadata around that's marked read only // Only keep metadata around that's marked read only
existing_tag.items.retain(|i| i.read_only); existing_tag.items.retain(|i| i.read_only);
@ -61,7 +61,7 @@ where
header_ape_tag = (true, (start, start + u64::from(header.size))) header_ape_tag = (true, (start, start + u64::from(header.size)))
}, },
None => { _ => {
file.seek(SeekFrom::Current(-8))?; file.seek(SeekFrom::Current(-8))?;
}, },
} }
@ -80,7 +80,7 @@ where
// Also check this tag for any read only items // Also check this tag for any read only items
let start = file.stream_position()? as usize + 32; let start = file.stream_position()? as usize + 32;
if let Some((mut existing_tag, header)) = read::read_ape_tag(file, true)? { if let (Some(mut existing_tag), Some(header)) = read::read_ape_tag(file, true, true)? {
if write_options.respect_read_only { if write_options.respect_read_only {
existing_tag.items.retain(|i| i.read_only); existing_tag.items.retain(|i| i.read_only);

View file

@ -3,6 +3,7 @@
#[non_exhaustive] #[non_exhaustive]
pub struct ParseOptions { pub struct ParseOptions {
pub(crate) read_properties: bool, pub(crate) read_properties: bool,
pub(crate) read_tags: bool,
pub(crate) parsing_mode: ParsingMode, pub(crate) parsing_mode: ParsingMode,
pub(crate) max_junk_bytes: usize, pub(crate) max_junk_bytes: usize,
} }
@ -15,6 +16,7 @@ impl Default for ParseOptions {
/// ```rust,ignore /// ```rust,ignore
/// ParseOptions { /// ParseOptions {
/// read_properties: true, /// read_properties: true,
/// read_tags: true,
/// parsing_mode: ParsingMode::BestAttempt, /// parsing_mode: ParsingMode::BestAttempt,
/// max_junk_bytes: 1024 /// max_junk_bytes: 1024
/// } /// }
@ -46,6 +48,7 @@ impl ParseOptions {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
read_properties: true, read_properties: true,
read_tags: true,
parsing_mode: Self::DEFAULT_PARSING_MODE, parsing_mode: Self::DEFAULT_PARSING_MODE,
max_junk_bytes: Self::DEFAULT_MAX_JUNK_BYTES, max_junk_bytes: Self::DEFAULT_MAX_JUNK_BYTES,
} }
@ -66,6 +69,21 @@ impl ParseOptions {
*self *self
} }
/// Whether or not to read the tags
///
/// # Examples
///
/// ```rust
/// use lofty::config::ParseOptions;
///
/// // By default, `read_tags` is enabled. Here, we don't want to read them.
/// let parsing_options = ParseOptions::new().read_tags(false);
/// ```
pub fn read_tags(&mut self, read_tags: bool) -> Self {
self.read_tags = read_tags;
*self
}
/// The parsing mode to use, see [`ParsingMode`] for details /// The parsing mode to use, see [`ParsingMode`] for details
/// ///
/// # Examples /// # Examples

View file

@ -47,10 +47,14 @@ where
properties: FlacProperties::default(), properties: FlacProperties::default(),
}; };
let find_id3v2_config = if parse_options.read_tags {
FindId3v2Config::READ_TAG
} else {
FindId3v2Config::NO_READ_TAG
};
// It is possible for a FLAC file to contain an ID3v2 tag // It is possible for a FLAC file to contain an ID3v2 tag
if let ID3FindResults(Some(header), Some(content)) = if let ID3FindResults(Some(header), Some(content)) = find_id3v2(data, find_id3v2_config)? {
find_id3v2(data, FindId3v2Config::READ_TAG)?
{
log::warn!("Encountered an ID3v2 tag. This tag cannot be rewritten to the FLAC file!"); log::warn!("Encountered an ID3v2 tag. This tag cannot be rewritten to the FLAC file!");
let reader = &mut &*content; let reader = &mut &*content;
@ -78,7 +82,7 @@ where
decode_err!(@BAIL Flac, "Encountered a zero-sized metadata block"); decode_err!(@BAIL Flac, "Encountered a zero-sized metadata block");
} }
if block.ty == BLOCK_ID_VORBIS_COMMENTS { if block.ty == BLOCK_ID_VORBIS_COMMENTS && parse_options.read_tags {
log::debug!("Encountered a Vorbis Comments block, parsing"); log::debug!("Encountered a Vorbis Comments block, parsing");
// NOTE: According to the spec // NOTE: According to the spec
@ -106,7 +110,7 @@ where
continue; continue;
} }
if block.ty == BLOCK_ID_PICTURE { if block.ty == BLOCK_ID_PICTURE && parse_options.read_tags {
log::debug!("Encountered a FLAC picture block, parsing"); log::debug!("Encountered a FLAC picture block, parsing");
match Picture::from_flac_bytes(&block.content, false, parse_options.parsing_mode) { match Picture::from_flac_bytes(&block.content, false, parse_options.parsing_mode) {

View file

@ -69,7 +69,7 @@ where
while chunks.next(data).is_ok() { while chunks.next(data).is_ok() {
match &chunks.fourcc { match &chunks.fourcc {
b"ID3 " | b"id3 " => { b"ID3 " | b"id3 " if parse_options.read_tags => {
let tag = chunks.id3_chunk(data, parse_options.parsing_mode)?; let tag = chunks.id3_chunk(data, parse_options.parsing_mode)?;
if let Some(existing_tag) = id3v2_tag.as_mut() { if let Some(existing_tag) = id3v2_tag.as_mut() {
log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag"); log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag");
@ -95,12 +95,12 @@ where
stream_len = chunks.size; stream_len = chunks.size;
chunks.skip(data)?; chunks.skip(data)?;
}, },
b"ANNO" => { b"ANNO" if parse_options.read_tags => {
annotations.push(chunks.read_pstring(data, None)?); annotations.push(chunks.read_pstring(data, None)?);
}, },
// These four chunks are expected to appear at most once per file, // These four chunks are expected to appear at most once per file,
// so there's no need to replace anything we already read // so there's no need to replace anything we already read
b"COMT" if comments.is_empty() => { b"COMT" if comments.is_empty() && parse_options.read_tags => {
if chunks.size < 2 { if chunks.size < 2 {
continue; continue;
} }
@ -123,13 +123,13 @@ where
chunks.correct_position(data)?; chunks.correct_position(data)?;
}, },
b"NAME" if text_chunks.name.is_none() => { b"NAME" if text_chunks.name.is_none() && parse_options.read_tags => {
text_chunks.name = Some(chunks.read_pstring(data, None)?); text_chunks.name = Some(chunks.read_pstring(data, None)?);
}, },
b"AUTH" if text_chunks.author.is_none() => { b"AUTH" if text_chunks.author.is_none() && parse_options.read_tags => {
text_chunks.author = Some(chunks.read_pstring(data, None)?); text_chunks.author = Some(chunks.read_pstring(data, None)?);
}, },
b"(c) " if text_chunks.copyright.is_none() => { b"(c) " if text_chunks.copyright.is_none() && parse_options.read_tags => {
text_chunks.copyright = Some(chunks.read_pstring(data, None)?); text_chunks.copyright = Some(chunks.read_pstring(data, None)?);
}, },
_ => chunks.skip(data)?, _ => chunks.skip(data)?,

View file

@ -78,7 +78,7 @@ where
data.read_exact(&mut list_type)?; data.read_exact(&mut list_type)?;
match &list_type { match &list_type {
b"INFO" => { b"INFO" if parse_options.read_tags => {
let end = data.stream_position()? + u64::from(chunks.size - 4); let end = data.stream_position()? + u64::from(chunks.size - 4);
super::tag::read::parse_riff_info(data, &mut chunks, end, &mut riff_info)?; super::tag::read::parse_riff_info(data, &mut chunks, end, &mut riff_info)?;
}, },
@ -88,7 +88,7 @@ where
}, },
} }
}, },
b"ID3 " | b"id3 " => { b"ID3 " | b"id3 " if parse_options.read_tags => {
let tag = chunks.id3_chunk(data, parse_options.parsing_mode)?; let tag = chunks.id3_chunk(data, parse_options.parsing_mode)?;
if let Some(existing_tag) = id3v2_tag.as_mut() { if let Some(existing_tag) = id3v2_tag.as_mut() {
log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag"); log::warn!("Duplicate ID3v2 tag found, appending frames to previous tag");

View file

@ -2,7 +2,7 @@ use super::atom_info::{AtomIdent, AtomInfo};
use super::ilst::read::parse_ilst; use super::ilst::read::parse_ilst;
use super::ilst::Ilst; use super::ilst::Ilst;
use super::read::{meta_is_full, nested_atom, skip_unneeded, AtomReader}; use super::read::{meta_is_full, nested_atom, skip_unneeded, AtomReader};
use crate::config::ParsingMode; use crate::config::{ParseOptions, ParsingMode};
use crate::error::Result; use crate::error::Result;
use crate::macros::decode_err; use crate::macros::decode_err;
@ -34,11 +34,7 @@ impl Moov {
moov.ok_or_else(|| decode_err!(Mp4, "No \"moov\" atom found")) moov.ok_or_else(|| decode_err!(Mp4, "No \"moov\" atom found"))
} }
pub(super) fn parse<R>( pub(super) fn parse<R>(reader: &mut AtomReader<R>, parse_options: ParseOptions) -> Result<Self>
reader: &mut AtomReader<R>,
parse_mode: ParsingMode,
read_properties: bool,
) -> Result<Self>
where where
R: Read + Seek, R: Read + Seek,
{ {
@ -48,15 +44,18 @@ impl Moov {
while let Ok(Some(atom)) = reader.next() { while let Ok(Some(atom)) = reader.next() {
if let AtomIdent::Fourcc(fourcc) = atom.ident { if let AtomIdent::Fourcc(fourcc) = atom.ident {
match &fourcc { match &fourcc {
b"trak" if read_properties => { b"trak" if parse_options.read_properties => {
// All we need from here is trak.mdia // All we need from here is trak.mdia
if let Some(mdia) = nested_atom(reader, atom.len, b"mdia", parse_mode)? { if let Some(mdia) =
nested_atom(reader, atom.len, b"mdia", parse_options.parsing_mode)?
{
skip_unneeded(reader, mdia.extended, mdia.len)?; skip_unneeded(reader, mdia.extended, mdia.len)?;
traks.push(mdia); traks.push(mdia);
} }
}, },
b"udta" => { b"udta" if parse_options.read_tags => {
let ilst_parsed = ilst_from_udta(reader, parse_mode, atom.len - 8)?; let ilst_parsed =
ilst_from_udta(reader, parse_options.parsing_mode, atom.len - 8)?;
if let Some(ilst_parsed) = ilst_parsed { if let Some(ilst_parsed) = ilst_parsed {
let Some(mut existing_ilst) = ilst else { let Some(mut existing_ilst) = ilst else {
ilst = Some(ilst_parsed); ilst = Some(ilst_parsed);

View file

@ -192,11 +192,7 @@ where
let moov_info = Moov::find(&mut reader)?; let moov_info = Moov::find(&mut reader)?;
reader.reset_bounds(moov_info.start + 8, moov_info.len - 8); reader.reset_bounds(moov_info.start + 8, moov_info.len - 8);
let moov = Moov::parse( let moov = Moov::parse(&mut reader, parse_options)?;
&mut reader,
parse_options.parsing_mode,
parse_options.read_properties,
)?;
Ok(Mp4File { Ok(Mp4File {
ftyp, ftyp,

View file

@ -43,6 +43,7 @@ where
let header = Id3v2Header::parse(reader)?; let header = Id3v2Header::parse(reader)?;
let skip_footer = header.flags.footer; let skip_footer = header.flags.footer;
if parse_options.read_tags {
let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?; let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?;
if let Some(existing_tag) = &mut file.id3v2_tag { if let Some(existing_tag) = &mut file.id3v2_tag {
// https://github.com/Serial-ATA/lofty-rs/issues/87 // https://github.com/Serial-ATA/lofty-rs/issues/87
@ -53,6 +54,9 @@ where
continue; continue;
} }
file.id3v2_tag = Some(id3v2); file.id3v2_tag = Some(id3v2);
} else {
reader.seek(SeekFrom::Current(i64::from(header.size)))?;
}
// Skip over the footer // Skip over the footer
if skip_footer { if skip_footer {
@ -74,9 +78,13 @@ where
if &header_remaining == b"AGEX" { if &header_remaining == b"AGEX" {
let ape_header = read_ape_header(reader, false)?; let ape_header = read_ape_header(reader, false)?;
if parse_options.read_tags {
file.ape_tag = Some(crate::ape::tag::read::read_ape_tag_with_header( file.ape_tag = Some(crate::ape::tag::read::read_ape_tag_with_header(
reader, ape_header, reader, ape_header,
)?); )?);
} else {
reader.seek(SeekFrom::Current(i64::from(ape_header.size)))?;
}
continue; continue;
} }
@ -105,7 +113,7 @@ where
std::cmp::min(_first_frame_offset, parse_options.max_junk_bytes as u64); std::cmp::min(_first_frame_offset, parse_options.max_junk_bytes as u64);
let config = FindId3v2Config { let config = FindId3v2Config {
read: true, read: parse_options.read_tags,
allowed_junk_window: Some(search_window_size), allowed_junk_window: Some(search_window_size),
}; };
@ -137,7 +145,7 @@ where
} }
#[allow(unused_variables)] #[allow(unused_variables)]
let ID3FindResults(header, id3v1) = find_id3v1(reader, true)?; let ID3FindResults(header, id3v1) = find_id3v1(reader, parse_options.read_tags)?;
if header.is_some() { if header.is_some() {
file.id3v1_tag = id3v1; file.id3v1_tag = id3v1;
@ -147,15 +155,15 @@ where
reader.seek(SeekFrom::Current(-32))?; reader.seek(SeekFrom::Current(-32))?;
match crate::ape::tag::read::read_ape_tag(reader, true)? { match crate::ape::tag::read::read_ape_tag(reader, true, parse_options.read_tags)? {
Some((tag, header)) => { (tag, Some(header)) => {
file.ape_tag = Some(tag); file.ape_tag = tag;
// Seek back to the start of the tag // Seek back to the start of the tag
let pos = reader.stream_position()?; let pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(pos - u64::from(header.size)))?; reader.seek(SeekFrom::Start(pos - u64::from(header.size)))?;
}, },
None => { _ => {
// Correct the position (APE header - Preamble) // Correct the position (APE header - Preamble)
reader.seek(SeekFrom::Current(24))?; reader.seek(SeekFrom::Current(24))?;
}, },

View file

@ -22,11 +22,15 @@ where
let mut stream_length = reader.stream_len_hack()?; let mut stream_length = reader.stream_len_hack()?;
let find_id3v2_config = if parse_options.read_tags {
FindId3v2Config::READ_TAG
} else {
FindId3v2Config::NO_READ_TAG
};
// ID3v2 tags are unsupported in MPC files, but still possible // ID3v2 tags are unsupported in MPC files, but still possible
#[allow(unused_variables)] #[allow(unused_variables)]
if let ID3FindResults(Some(header), Some(content)) = if let ID3FindResults(Some(header), Some(content)) = find_id3v2(reader, find_id3v2_config)? {
find_id3v2(reader, FindId3v2Config::READ_TAG)?
{
let reader = &mut &*content; let reader = &mut &*content;
let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?; let id3v2 = parse_id3v2(reader, header, parse_options.parsing_mode)?;
@ -39,7 +43,7 @@ where
let pos_past_id3v2 = reader.stream_position()?; let pos_past_id3v2 = reader.stream_position()?;
#[allow(unused_variables)] #[allow(unused_variables)]
let ID3FindResults(header, id3v1) = find_id3v1(reader, true)?; let ID3FindResults(header, id3v1) = find_id3v1(reader, parse_options.read_tags)?;
if header.is_some() { if header.is_some() {
file.id3v1_tag = id3v1; file.id3v1_tag = id3v1;
@ -51,8 +55,10 @@ where
reader.seek(SeekFrom::Current(-32))?; reader.seek(SeekFrom::Current(-32))?;
if let Some((tag, header)) = crate::ape::tag::read::read_ape_tag(reader, true)? { if let (tag, Some(header)) =
file.ape_tag = Some(tag); crate::ape::tag::read::read_ape_tag(reader, true, parse_options.read_tags)?
{
file.ape_tag = tag;
// Seek back to the start of the tag // Seek back to the start of the tag
let pos = reader.stream_position()?; let pos = reader.stream_position()?;

View file

@ -30,7 +30,7 @@ impl OpusFile {
R: Read + Seek, R: Read + Seek,
{ {
let file_information = let file_information =
super::read::read_from(reader, OPUSHEAD, OPUSTAGS, 2, parse_options.parsing_mode)?; super::read::read_from(reader, OPUSHEAD, OPUSTAGS, 2, parse_options)?;
Ok(Self { Ok(Self {
properties: if parse_options.read_properties { properties: if parse_options.read_properties {
@ -38,8 +38,8 @@ impl OpusFile {
} else { } else {
OpusProperties::default() OpusProperties::default()
}, },
// Safe to unwrap, a metadata packet is mandatory in Opus // A metadata packet is mandatory in Opus
vorbis_comments_tag: file_information.0.unwrap(), vorbis_comments_tag: file_information.0.unwrap_or_default(),
}) })
} }
} }

View file

@ -32,7 +32,11 @@ impl From<OpusProperties> for FileProperties {
sample_rate: Some(input.input_sample_rate), sample_rate: Some(input.input_sample_rate),
bit_depth: None, bit_depth: None,
channels: Some(input.channels), channels: Some(input.channels),
channel_mask: Some(input.channel_mask), channel_mask: if input.channel_mask == ChannelMask(0) {
None
} else {
Some(input.channel_mask)
},
} }
} }
} }

View file

@ -1,6 +1,6 @@
use super::tag::VorbisComments; use super::tag::VorbisComments;
use super::verify_signature; use super::verify_signature;
use crate::config::ParsingMode; use crate::config::{ParseOptions, ParsingMode};
use crate::error::{ErrorKind, LoftyError, Result}; use crate::error::{ErrorKind, LoftyError, Result};
use crate::macros::{decode_err, err, parse_mode_choice}; use crate::macros::{decode_err, err, parse_mode_choice};
use crate::picture::{MimeType, Picture, PictureInformation, PictureType}; use crate::picture::{MimeType, Picture, PictureInformation, PictureType};
@ -189,7 +189,7 @@ pub(crate) fn read_from<T>(
header_sig: &[u8], header_sig: &[u8],
comment_sig: &[u8], comment_sig: &[u8],
packets_to_read: isize, packets_to_read: isize,
parse_mode: ParsingMode, parse_options: ParseOptions,
) -> Result<OGGTags> ) -> Result<OGGTags>
where where
T: Read + Seek, T: Read + Seek,
@ -210,6 +210,10 @@ where
.ok_or_else(|| decode_err!("OGG: Expected identification packet"))?; .ok_or_else(|| decode_err!("OGG: Expected identification packet"))?;
verify_signature(identification_packet, header_sig)?; verify_signature(identification_packet, header_sig)?;
if !parse_options.read_tags {
return Ok((None, first_page_header, packets));
}
let mut metadata_packet = packets let mut metadata_packet = packets
.get(1) .get(1)
.ok_or_else(|| decode_err!("OGG: Expected comment packet"))?; .ok_or_else(|| decode_err!("OGG: Expected comment packet"))?;
@ -219,7 +223,7 @@ where
metadata_packet = &metadata_packet[comment_sig.len()..]; metadata_packet = &metadata_packet[comment_sig.len()..];
let reader = &mut metadata_packet; let reader = &mut metadata_packet;
let tag = read_comments(reader, reader.len() as u64, parse_mode)?; let tag = read_comments(reader, reader.len() as u64, parse_options.parsing_mode)?;
Ok((Some(tag), first_page_header, packets)) Ok((Some(tag), first_page_header, packets))
} }

View file

@ -28,8 +28,7 @@ impl SpeexFile {
where where
R: Read + Seek, R: Read + Seek,
{ {
let file_information = let file_information = super::read::read_from(reader, SPEEXHEADER, &[], 2, parse_options)?;
super::read::read_from(reader, SPEEXHEADER, &[], 2, parse_options.parsing_mode)?;
Ok(Self { Ok(Self {
properties: if parse_options.read_properties { properties: if parse_options.read_properties {
@ -37,8 +36,8 @@ impl SpeexFile {
} else { } else {
SpeexProperties::default() SpeexProperties::default()
}, },
// Safe to unwrap, a metadata packet is mandatory in Speex // A metadata packet is mandatory in Speex
vorbis_comments_tag: file_information.0.unwrap(), vorbis_comments_tag: file_information.0.unwrap_or_default(),
}) })
} }
} }

View file

@ -34,7 +34,7 @@ impl VorbisFile {
VORBIS_IDENT_HEAD, VORBIS_IDENT_HEAD,
VORBIS_COMMENT_HEAD, VORBIS_COMMENT_HEAD,
3, 3,
parse_options.parsing_mode, parse_options,
)?; )?;
Ok(Self { Ok(Self {
@ -43,8 +43,8 @@ impl VorbisFile {
} else { } else {
VorbisProperties::default() VorbisProperties::default()
}, },
// Safe to unwrap, a metadata packet is mandatory in OGG Vorbis // A metadata packet is mandatory in OGG Vorbis
vorbis_comments_tag: file_information.0.unwrap(), vorbis_comments_tag: file_information.0.unwrap_or_default(),
}) })
} }
} }

View file

@ -459,6 +459,10 @@ impl<R: Read + Seek> Probe<R> {
let reader = &mut self.inner; let reader = &mut self.inner;
let options = self.options.unwrap_or_default(); let options = self.options.unwrap_or_default();
if !options.read_tags && !options.read_properties {
log::warn!("Skipping both tag and property reading, file will be empty");
}
match self.f_ty { match self.f_ty {
Some(f_type) => Ok(match f_type { Some(f_type) => Ok(match f_type {
FileType::Aac => AacFile::read_from(reader, options)?.into(), FileType::Aac => AacFile::read_from(reader, options)?.into(),

View file

@ -85,4 +85,21 @@ impl FileProperties {
pub fn channel_mask(&self) -> Option<ChannelMask> { pub fn channel_mask(&self) -> Option<ChannelMask> {
self.channel_mask self.channel_mask
} }
/// Used for tests
#[doc(hidden)]
pub fn is_empty(&self) -> bool {
matches!(
self,
Self {
duration: Duration::ZERO,
overall_bitrate: None | Some(0),
audio_bitrate: None | Some(0),
sample_rate: None | Some(0),
bit_depth: None | Some(0),
channels: None | Some(0),
channel_mask: None,
}
)
}
} }

View file

@ -32,7 +32,11 @@ impl From<WavPackProperties> for FileProperties {
sample_rate: Some(input.sample_rate), sample_rate: Some(input.sample_rate),
bit_depth: Some(input.bit_depth), bit_depth: Some(input.bit_depth),
channels: Some(input.channels as u8), channels: Some(input.channels as u8),
channel_mask: Some(input.channel_mask), channel_mask: if input.channel_mask == ChannelMask(0) {
None
} else {
Some(input.channel_mask)
},
} }
} }
} }

View file

@ -17,7 +17,7 @@ where
let mut id3v1_tag = None; let mut id3v1_tag = None;
let mut ape_tag = None; let mut ape_tag = None;
let ID3FindResults(id3v1_header, id3v1) = find_id3v1(reader, true)?; let ID3FindResults(id3v1_header, id3v1) = find_id3v1(reader, parse_options.read_tags)?;
if id3v1_header.is_some() { if id3v1_header.is_some() {
stream_length -= 128; stream_length -= 128;
@ -38,9 +38,11 @@ where
// Strongly recommended to be at the end of the file // Strongly recommended to be at the end of the file
reader.seek(SeekFrom::Current(-32))?; reader.seek(SeekFrom::Current(-32))?;
if let Some((tag, header)) = crate::ape::tag::read::read_ape_tag(reader, true)? { if let (tag, Some(header)) =
crate::ape::tag::read::read_ape_tag(reader, true, parse_options.read_tags)?
{
stream_length -= u64::from(header.size); stream_length -= u64::from(header.size);
ape_tag = Some(tag); ape_tag = tag;
} }
Ok(WavPackFile { Ok(WavPackFile {

View file

@ -5,7 +5,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -96,3 +96,13 @@ fn remove_id3v2() {
fn remove_id3v1() { fn remove_id3v1() {
crate::remove_tag!("tests/files/assets/minimal/full_test.aac", TagType::Id3v1); crate::remove_tag!("tests/files/assets/minimal/full_test.aac", TagType::Id3v1);
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.aac");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/full_test.aac");
}

View file

@ -5,7 +5,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -70,3 +70,13 @@ fn remove_text_chunks() {
fn remove_id3v2() { fn remove_id3v2() {
crate::remove_tag!("tests/files/assets/minimal/full_test.aiff", TagType::Id3v2); crate::remove_tag!("tests/files/assets/minimal/full_test.aiff", TagType::Id3v2);
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.aiff");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/full_test.aiff");
}

View file

@ -5,7 +5,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -76,3 +76,13 @@ fn remove_id3v1() {
fn remove_id3v2() { fn remove_id3v2() {
crate::remove_tag!("tests/files/assets/minimal/full_test.ape", TagType::Id3v2); crate::remove_tag!("tests/files/assets/minimal/full_test.ape", TagType::Id3v2);
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.ape");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/full_test.ape");
}

View file

@ -30,3 +30,13 @@ fn multiple_vorbis_comments() {
Some("Artist 2") Some("Artist 2")
); );
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.flac");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/full_test.flac");
}

View file

@ -5,7 +5,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -58,3 +58,13 @@ fn remove() {
TagType::Mp4Ilst TagType::Mp4Ilst
); );
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/m4a_codec_aac.m4a");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/m4a_codec_aac.m4a");
}

View file

@ -6,7 +6,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
// Marker test so IntelliJ Rust recognizes this as a test module // Marker test so IntelliJ Rust recognizes this as a test module
#[test] #[test]
@ -79,6 +79,16 @@ macro_rules! generate_tests {
fn [<remove_ape_ $stream_version>]() { fn [<remove_ape_ $stream_version>]() {
crate::remove_tag!($path, TagType::Ape); crate::remove_tag!($path, TagType::Ape);
} }
#[test]
fn [<read_no_properties_ $stream_version>]() {
crate::no_properties_test!($path);
}
#[test]
fn [<read_no_tags_ $stream_version>]() {
crate::no_tag_test!($path);
}
} }
}; };
} }

View file

@ -8,7 +8,7 @@ use lofty::probe::Probe;
use lofty::tag::{Tag, TagType}; use lofty::tag::{Tag, TagType};
use std::borrow::Cow; use std::borrow::Cow;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -369,3 +369,26 @@ fn read_and_write_tpil_frame() {
assert_eq!(key_value_pairs, content.key_value_pairs); assert_eq!(key_value_pairs, content.key_value_pairs);
} }
#[test]
fn read_no_properties() {
let mut file = crate::temp_file!("tests/files/assets/minimal/full_test.mp3");
let tagged_file = Probe::new(&mut file)
.options(ParseOptions::new().read_properties(false))
.guess_file_type()
.unwrap()
.read()
.unwrap();
let properties = tagged_file.properties();
assert!(properties.duration().is_zero());
assert_eq!(properties.overall_bitrate(), Some(0));
assert_eq!(properties.audio_bitrate(), Some(0));
assert_eq!(properties.sample_rate(), Some(0));
assert!(properties.bit_depth().is_none());
assert_eq!(properties.channels(), Some(0));
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/full_test.mp3");
}

View file

@ -5,9 +5,9 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
// The tests for OGG Opus/Vorbis are nearly identical // The tests for OGG Opus/Vorbis/Speex are nearly identical
// We have the vendor string and a title stored in the tag // We have the vendor string and a title stored in the tag
#[test] #[test]
@ -186,3 +186,33 @@ fn flac_try_write_non_empty_id3v2() {
) )
.is_err()); .is_err());
} }
#[test]
fn read_no_properties_opus() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.opus");
}
#[test]
fn read_no_tags_opus() {
crate::no_tag_test!(@MANDATORY_TAG "tests/files/assets/minimal/full_test.opus", expected_len: 1);
}
#[test]
fn read_no_properties_vorbis() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.ogg");
}
#[test]
fn read_no_tags_vorbis() {
crate::no_tag_test!(@MANDATORY_TAG "tests/files/assets/minimal/full_test.ogg", expected_len: 1);
}
#[test]
fn read_no_properties_speex() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.spx");
}
#[test]
fn read_no_tags_speex() {
crate::no_tag_test!(@MANDATORY_TAG "tests/files/assets/minimal/full_test.spx", expected_len: 1);
}

View file

@ -1,6 +1,8 @@
#[macro_export] #[macro_export]
macro_rules! temp_file { macro_rules! temp_file {
($path:tt) => {{ ($path:tt) => {{
use std::io::Write as _;
let mut file = tempfile::tempfile().unwrap(); let mut file = tempfile::tempfile().unwrap();
file.write_all(&std::fs::read($path).unwrap()).unwrap(); file.write_all(&std::fs::read($path).unwrap()).unwrap();
@ -10,6 +12,48 @@ macro_rules! temp_file {
}}; }};
} }
#[macro_export]
macro_rules! no_tag_test {
($path:literal) => {{
let mut file = crate::temp_file!($path);
let tagged_file = lofty::probe::Probe::new(&mut file)
.options(lofty::config::ParseOptions::new().read_tags(false))
.guess_file_type()
.unwrap()
.read()
.unwrap();
assert!(!tagged_file.contains_tag());
}};
(@MANDATORY_TAG $path:literal, expected_len: $expected_len:literal) => {{
use lofty::tag::TagExt as _;
let mut file = crate::temp_file!($path);
let tagged_file = lofty::probe::Probe::new(&mut file)
.options(lofty::config::ParseOptions::new().read_tags(false))
.guess_file_type()
.unwrap()
.read()
.unwrap();
for tag in tagged_file.tags() {
assert_eq!(tag.len(), $expected_len);
}
}};
}
#[macro_export]
macro_rules! no_properties_test {
($path:literal) => {{
let mut file = crate::temp_file!($path);
let tagged_file = lofty::probe::Probe::new(&mut file)
.options(lofty::config::ParseOptions::new().read_properties(false))
.guess_file_type()
.unwrap()
.read()
.unwrap();
assert!(tagged_file.properties().is_empty());
}};
}
#[macro_export] #[macro_export]
macro_rules! verify_artist { macro_rules! verify_artist {
($file:ident, $method:ident, $expected_value:literal, $item_count:expr) => {{ ($file:ident, $method:ident, $expected_value:literal, $item_count:expr) => {{

View file

@ -5,7 +5,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -85,3 +85,13 @@ fn issue_174_divide_by_zero() {
assert_eq!(file.file_type(), FileType::Wav); assert_eq!(file.file_type(), FileType::Wav);
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/wav_format_pcm.wav");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/wav_format_pcm.wav");
}

View file

@ -5,7 +5,7 @@ use lofty::prelude::*;
use lofty::probe::Probe; use lofty::probe::Probe;
use lofty::tag::TagType; use lofty::tag::TagType;
use std::io::{Seek, Write}; use std::io::Seek;
#[test] #[test]
fn read() { fn read() {
@ -67,3 +67,13 @@ fn remove_id3v1() {
fn remove_ape() { fn remove_ape() {
crate::remove_tag!("tests/files/assets/minimal/full_test.wv", TagType::Ape); crate::remove_tag!("tests/files/assets/minimal/full_test.wv", TagType::Ape);
} }
#[test]
fn read_no_properties() {
crate::no_properties_test!("tests/files/assets/minimal/full_test.wv");
}
#[test]
fn read_no_tags() {
crate::no_tag_test!("tests/files/assets/minimal/full_test.wv");
}