Skip to content

ID3v2: Restrict frame skipping to the bounds of the frame content #459

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **ItemKey**: `ItemKey::TrackArtists`, available for ID3v2, Vorbis Comments, APE, and MP4 Ilst ([PR](https://github.com/Serial-ATA/lofty-rs/issues/454))
- **ItemKey**: `ItemKey::TrackArtists`, available for ID3v2, Vorbis Comments, APE, and MP4 Ilst ([PR](https://github.com/Serial-ATA/lofty-rs/pull/454))
- This is a multi-value item that stores each artist for a track. It should be retrieved with `Tag::get_strings` or `Tag::take_strings`.
- For example, a track has `ItemKey::TrackArtist` = "Foo & Bar", then `ItemKey::TrackArtists` = ["Foo", "Bar"].
- See <https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists>
- **UnsynchronizedStream**: `UnsynchronizedStream::get_ref()` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/459))

### Fixed
- **MusePack**: Fix potential panic when the beginning silence makes up the entire sample count ([PR](https://github.com/Serial-ATA/lofty-rs/pull/449))
- **Timestamp**: Support timestamps without separators (ex. "20240906" vs "2024-09-06") ([issue](https://github.com/Serial-ATA/lofty-rs/issues/452)) ([PR](https://github.com/Serial-ATA/lofty-rs/issues/453))
- **ID3v2**: `ItemKey::Director` will now be written correctly as a TXXX frame ([PR](https://github.com/Serial-ATA/lofty-rs/issues/454))
- **ID3v2**:
- `ItemKey::Director` will now be written correctly as a TXXX frame ([PR](https://github.com/Serial-ATA/lofty-rs/issues/454))
- When skipping invalid frames in `ParsingMode::{BestAttempt, Relaxed}`, the parser will no longer be able to go out of the bounds
of the frame content ([issue](https://github.com/Serial-ATA/lofty-rs/issues/458)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/459))

## [0.21.1] - 2024-08-28

Expand Down
2 changes: 2 additions & 0 deletions lofty/src/id3/v2/frame/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub(super) fn parse_content<R: Read>(
version: Id3v2Version,
parse_mode: ParsingMode,
) -> Result<Option<Frame<'static>>> {
log::trace!("Parsing frame content for ID: {}", id);

Ok(match id.as_str() {
// The ID was previously upgraded, but the content remains unchanged, so version is necessary
"APIC" => {
Expand Down
39 changes: 32 additions & 7 deletions lofty/src/id3/v2/frame/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ use crate::config::{ParseOptions, ParsingMode};
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
use crate::id3::v2::frame::content::parse_content;
use crate::id3::v2::header::Id3v2Version;
use crate::id3::v2::tag::ATTACHED_PICTURE_ID;
use crate::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream};
use crate::id3::v2::{BinaryFrame, FrameFlags, FrameHeader, FrameId};
use crate::macros::try_vec;

use std::io::Read;

use crate::id3::v2::tag::ATTACHED_PICTURE_ID;
use byteorder::{BigEndian, ReadBytesExt};

pub(crate) enum ParsedFrame<'a> {
Next(Frame<'a>),
Skip { size: u32 },
Skip,
Eof,
}

Expand Down Expand Up @@ -46,16 +46,19 @@ impl<'a> ParsedFrame<'a> {
match parse_options.parsing_mode {
ParsingMode::Strict => return Err(err),
ParsingMode::BestAttempt | ParsingMode::Relaxed => {
log::warn!("Failed to read frame header, skipping: {}", err);

// Skip this frame and continue reading
// TODO: Log error?
return Ok(Self::Skip { size });
skip_frame(reader, size)?;
return Ok(Self::Skip);
},
}
},
};

if !parse_options.read_cover_art && id == ATTACHED_PICTURE_ID {
return Ok(Self::Skip { size });
skip_frame(reader, size)?;
return Ok(Self::Skip);
}

if size == 0 {
Expand All @@ -64,7 +67,9 @@ impl<'a> ParsedFrame<'a> {
}

log::debug!("Encountered a zero length frame, skipping");
return Ok(Self::Skip { size });

skip_frame(reader, size)?;
return Ok(Self::Skip);
}

// Get the encryption method symbol
Expand Down Expand Up @@ -252,6 +257,26 @@ fn parse_frame<R: Read>(
) -> Result<ParsedFrame<'static>> {
match parse_content(reader, id, flags, version, parse_mode)? {
Some(frame) => Ok(ParsedFrame::Next(frame)),
None => Ok(ParsedFrame::Skip { size }),
None => {
skip_frame(reader, size)?;
Ok(ParsedFrame::Skip)
},
}
}

// Note that this is only ever given the full frame size.
//
// In the context of `ParsedFrame::read`, the reader is restricted to the frame content, so this
// is a safe operation, regardless of where we are in parsing the frame.
//
// This assumption *CANNOT* be made in other contexts.
fn skip_frame(reader: &mut impl Read, size: u32) -> Result<()> {
log::trace!("Skipping frame of size {}", size);

let size = u64::from(size);
let mut reader = reader.take(size);
let skipped = std::io::copy(&mut reader, &mut std::io::sink())?;
debug_assert!(skipped <= size);

Ok(())
}
19 changes: 2 additions & 17 deletions lofty/src/id3/v2/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::frame::read::ParsedFrame;
use super::header::Id3v2Header;
use super::tag::Id3v2Tag;
use crate::config::ParseOptions;
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
use crate::error::Result;
use crate::id3::v2::util::synchsafe::UnsynchronizedStream;
use crate::id3::v2::{Frame, FrameId, Id3v2Version, TimestampFrame};
use crate::tag::items::Timestamp;
Expand Down Expand Up @@ -130,19 +130,6 @@ fn construct_tdrc_from_v3(tag: &mut Id3v2Tag) {
}
}

fn skip_frame(reader: &mut impl Read, size: u32) -> Result<()> {
log::trace!("Skipping frame of size {}", size);

let size = u64::from(size);
let mut reader = reader.take(size);
let skipped = std::io::copy(&mut reader, &mut std::io::sink())?;
debug_assert!(skipped <= size);
if skipped != size {
return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into());
}
Ok(())
}

fn read_all_frames_into_tag<R>(
reader: &mut R,
header: Id3v2Header,
Expand Down Expand Up @@ -181,9 +168,7 @@ where
}
},
// No frame content found or ignored due to errors, but we can expect more frames
ParsedFrame::Skip { size } => {
skip_frame(reader, size)?;
},
ParsedFrame::Skip => {},
// No frame content found, and we can expect there are no more frames
ParsedFrame::Eof => break,
}
Expand Down
2 changes: 1 addition & 1 deletion lofty/src/id3/v2/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ pub(crate) struct GenresIter<'a> {
}

impl<'a> GenresIter<'a> {
pub fn new(value: &'a str, preserve_indexes: bool) -> GenresIter<'_> {
pub fn new(value: &'a str, preserve_indexes: bool) -> GenresIter<'a> {
GenresIter {
value,
pos: 0,
Expand Down
29 changes: 29 additions & 0 deletions lofty/src/id3/v2/tag/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,3 +1554,32 @@ fn artists_tag_conversion() {

assert_eq!(id3v2_artists, ARTISTS);
}

#[test_log::test]
fn ensure_frame_skipping_within_bounds() {
// This tag has an invalid `TDEN` frame, but it is skippable in BestAttempt/Relaxed parsing mode.
// We should be able to continue reading the tag as normal, reaching the other `TDTG` frame.

let path = "tests/tags/assets/id3v2/skippable_frame_otherwise_valid.id3v24";
let tag = read_tag_with_options(
&read_path(path),
ParseOptions::new().parsing_mode(ParsingMode::BestAttempt),
);

assert_eq!(tag.len(), 1);
assert_eq!(
tag.get(&FrameId::Valid(Cow::Borrowed("TDTG"))),
Some(&Frame::Timestamp(TimestampFrame::new(
FrameId::Valid(Cow::Borrowed("TDTG")),
TextEncoding::Latin1,
Timestamp {
year: 2014,
month: Some(6),
day: Some(10),
hour: Some(2),
minute: Some(16),
second: Some(10),
},
)))
);
}
19 changes: 19 additions & 0 deletions lofty/src/id3/v2/util/synchsafe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ impl<R> UnsynchronizedStream<R> {
pub fn into_inner(self) -> R {
self.reader
}

/// Get a reference to the inner reader
///
/// # Examples
///
/// ```rust
/// use lofty::id3::v2::util::synchsafe::UnsynchronizedStream;
/// use std::io::Cursor;
///
/// # fn main() -> lofty::error::Result<()> {
/// let reader = Cursor::new([0xFF, 0x00, 0x1A]);
/// let unsynchronized_reader = UnsynchronizedStream::new(reader);
///
/// let reader = unsynchronized_reader.get_ref();
/// assert_eq!(reader.position(), 0);
/// # Ok(()) }
pub fn get_ref(&self) -> &R {
&self.reader
}
}

impl<R: Read> Read for UnsynchronizedStream<R> {
Expand Down
2 changes: 1 addition & 1 deletion lofty/src/ogg/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ impl VorbisComments {
/// let all_artists = vorbis_comments.get_all("ARTIST").collect::<Vec<&str>>();
/// assert_eq!(all_artists, vec!["Foo artist", "Bar artist", "Baz artist"]);
/// ```
pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + Clone + '_ {
pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + Clone + 'a {
self.items
.iter()
.filter_map(move |(k, v)| (k.eq_ignore_ascii_case(key)).then_some(v.as_str()))
Expand Down
2 changes: 1 addition & 1 deletion lofty/src/ogg/vorbis/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ where
let last_page_abgp = last_page.header().abgp;

if properties.sample_rate > 0 {
let total_samples = last_page_abgp.saturating_sub(first_page_abgp) as u128;
let total_samples = u128::from(last_page_abgp.saturating_sub(first_page_abgp));

// Best case scenario
if total_samples > 0 {
Expand Down
Binary file not shown.