Skip to content

Commit 06618cf

Browse files
committed
ID3v2: Restrict frame skipping to the bounds of the frame content
1 parent 79336cd commit 06618cf

File tree

10 files changed

+93
-29
lines changed

10 files changed

+93
-29
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88

99
### Added
10-
- **ItemKey**: `ItemKey::TrackArtists`, available for ID3v2, Vorbis Comments, APE, and MP4 Ilst ([PR](https://github.com/Serial-ATA/lofty-rs/issues/454))
10+
- **ItemKey**: `ItemKey::TrackArtists`, available for ID3v2, Vorbis Comments, APE, and MP4 Ilst ([PR](https://github.com/Serial-ATA/lofty-rs/pull/454))
1111
- 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`.
1212
- For example, a track has `ItemKey::TrackArtist` = "Foo & Bar", then `ItemKey::TrackArtists` = ["Foo", "Bar"].
1313
- See <https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists>
14+
- **UnsynchronizedStream**: `UnsynchronizedStream::get_ref()` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/459))
1415

1516
### Fixed
1617
- **MusePack**: Fix potential panic when the beginning silence makes up the entire sample count ([PR](https://github.com/Serial-ATA/lofty-rs/pull/449))
1718
- **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))
18-
- **ID3v2**: `ItemKey::Director` will now be written correctly as a TXXX frame ([PR](https://github.com/Serial-ATA/lofty-rs/issues/454))
19+
- **ID3v2**:
20+
- `ItemKey::Director` will now be written correctly as a TXXX frame ([PR](https://github.com/Serial-ATA/lofty-rs/issues/454))
21+
- When skipping invalid frames in `ParsingMode::{BestAttempt, Relaxed}`, the parser will no longer be able to go out of the bounds
22+
of the frame content ([issue](https://github.com/Serial-ATA/lofty-rs/issues/458)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/459))
1923

2024
## [0.21.1] - 2024-08-28
2125

lofty/src/id3/v2/frame/content.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub(super) fn parse_content<R: Read>(
2121
version: Id3v2Version,
2222
parse_mode: ParsingMode,
2323
) -> Result<Option<Frame<'static>>> {
24+
log::trace!("Parsing frame content for ID: {}", id);
25+
2426
Ok(match id.as_str() {
2527
// The ID was previously upgraded, but the content remains unchanged, so version is necessary
2628
"APIC" => {

lofty/src/id3/v2/frame/read.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ use crate::config::{ParseOptions, ParsingMode};
44
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
55
use crate::id3::v2::frame::content::parse_content;
66
use crate::id3::v2::header::Id3v2Version;
7+
use crate::id3::v2::tag::ATTACHED_PICTURE_ID;
78
use crate::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream};
89
use crate::id3::v2::{BinaryFrame, FrameFlags, FrameHeader, FrameId};
910
use crate::macros::try_vec;
1011

1112
use std::io::Read;
1213

13-
use crate::id3::v2::tag::ATTACHED_PICTURE_ID;
1414
use byteorder::{BigEndian, ReadBytesExt};
1515

1616
pub(crate) enum ParsedFrame<'a> {
1717
Next(Frame<'a>),
18-
Skip { size: u32 },
18+
Skip,
1919
Eof,
2020
}
2121

@@ -46,16 +46,19 @@ impl<'a> ParsedFrame<'a> {
4646
match parse_options.parsing_mode {
4747
ParsingMode::Strict => return Err(err),
4848
ParsingMode::BestAttempt | ParsingMode::Relaxed => {
49+
log::warn!("Failed to read frame header, skipping: {}", err);
50+
4951
// Skip this frame and continue reading
50-
// TODO: Log error?
51-
return Ok(Self::Skip { size });
52+
skip_frame(reader, size)?;
53+
return Ok(Self::Skip);
5254
},
5355
}
5456
},
5557
};
5658

5759
if !parse_options.read_cover_art && id == ATTACHED_PICTURE_ID {
58-
return Ok(Self::Skip { size });
60+
skip_frame(reader, size)?;
61+
return Ok(Self::Skip);
5962
}
6063

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

6669
log::debug!("Encountered a zero length frame, skipping");
67-
return Ok(Self::Skip { size });
70+
71+
skip_frame(reader, size)?;
72+
return Ok(Self::Skip);
6873
}
6974

7075
// Get the encryption method symbol
@@ -252,6 +257,26 @@ fn parse_frame<R: Read>(
252257
) -> Result<ParsedFrame<'static>> {
253258
match parse_content(reader, id, flags, version, parse_mode)? {
254259
Some(frame) => Ok(ParsedFrame::Next(frame)),
255-
None => Ok(ParsedFrame::Skip { size }),
260+
None => {
261+
skip_frame(reader, size)?;
262+
Ok(ParsedFrame::Skip)
263+
},
256264
}
257265
}
266+
267+
// Note that this is only ever given the full frame size.
268+
//
269+
// In the context of `ParsedFrame::read`, the reader is restricted to the frame content, so this
270+
// is a safe operation, regardless of where we are in parsing the frame.
271+
//
272+
// This assumption *CANNOT* be made in other contexts.
273+
fn skip_frame(reader: &mut impl Read, size: u32) -> Result<()> {
274+
log::trace!("Skipping frame of size {}", size);
275+
276+
let size = u64::from(size);
277+
let mut reader = reader.take(size);
278+
let skipped = std::io::copy(&mut reader, &mut std::io::sink())?;
279+
debug_assert!(skipped <= size);
280+
281+
Ok(())
282+
}

lofty/src/id3/v2/read.rs

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::frame::read::ParsedFrame;
22
use super::header::Id3v2Header;
33
use super::tag::Id3v2Tag;
44
use crate::config::ParseOptions;
5-
use crate::error::{Id3v2Error, Id3v2ErrorKind, Result};
5+
use crate::error::Result;
66
use crate::id3::v2::util::synchsafe::UnsynchronizedStream;
77
use crate::id3::v2::{Frame, FrameId, Id3v2Version, TimestampFrame};
88
use crate::tag::items::Timestamp;
@@ -130,19 +130,6 @@ fn construct_tdrc_from_v3(tag: &mut Id3v2Tag) {
130130
}
131131
}
132132

133-
fn skip_frame(reader: &mut impl Read, size: u32) -> Result<()> {
134-
log::trace!("Skipping frame of size {}", size);
135-
136-
let size = u64::from(size);
137-
let mut reader = reader.take(size);
138-
let skipped = std::io::copy(&mut reader, &mut std::io::sink())?;
139-
debug_assert!(skipped <= size);
140-
if skipped != size {
141-
return Err(Id3v2Error::new(Id3v2ErrorKind::BadFrameLength).into());
142-
}
143-
Ok(())
144-
}
145-
146133
fn read_all_frames_into_tag<R>(
147134
reader: &mut R,
148135
header: Id3v2Header,
@@ -181,9 +168,7 @@ where
181168
}
182169
},
183170
// No frame content found or ignored due to errors, but we can expect more frames
184-
ParsedFrame::Skip { size } => {
185-
skip_frame(reader, size)?;
186-
},
171+
ParsedFrame::Skip => {},
187172
// No frame content found, and we can expect there are no more frames
188173
ParsedFrame::Eof => break,
189174
}

lofty/src/id3/v2/tag.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ pub(crate) struct GenresIter<'a> {
574574
}
575575

576576
impl<'a> GenresIter<'a> {
577-
pub fn new(value: &'a str, preserve_indexes: bool) -> GenresIter<'_> {
577+
pub fn new(value: &'a str, preserve_indexes: bool) -> GenresIter<'a> {
578578
GenresIter {
579579
value,
580580
pos: 0,

lofty/src/id3/v2/tag/tests.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,3 +1554,32 @@ fn artists_tag_conversion() {
15541554

15551555
assert_eq!(id3v2_artists, ARTISTS);
15561556
}
1557+
1558+
#[test_log::test]
1559+
fn ensure_frame_skipping_within_bounds() {
1560+
// This tag has an invalid `TDEN` frame, but it is skippable in BestAttempt/Relaxed parsing mode.
1561+
// We should be able to continue reading the tag as normal, reaching the other `TDTG` frame.
1562+
1563+
let path = "tests/tags/assets/id3v2/skippable_frame_otherwise_valid.id3v24";
1564+
let tag = read_tag_with_options(
1565+
&read_path(path),
1566+
ParseOptions::new().parsing_mode(ParsingMode::BestAttempt),
1567+
);
1568+
1569+
assert_eq!(tag.len(), 1);
1570+
assert_eq!(
1571+
tag.get(&FrameId::Valid(Cow::Borrowed("TDTG"))),
1572+
Some(&Frame::Timestamp(TimestampFrame::new(
1573+
FrameId::Valid(Cow::Borrowed("TDTG")),
1574+
TextEncoding::Latin1,
1575+
Timestamp {
1576+
year: 2014,
1577+
month: Some(6),
1578+
day: Some(10),
1579+
hour: Some(2),
1580+
minute: Some(16),
1581+
second: Some(10),
1582+
},
1583+
)))
1584+
);
1585+
}

lofty/src/id3/v2/util/synchsafe.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ impl<R> UnsynchronizedStream<R> {
7878
pub fn into_inner(self) -> R {
7979
self.reader
8080
}
81+
82+
/// Get a reference to the inner reader
83+
///
84+
/// # Examples
85+
///
86+
/// ```rust
87+
/// use lofty::id3::v2::util::synchsafe::UnsynchronizedStream;
88+
/// use std::io::Cursor;
89+
///
90+
/// # fn main() -> lofty::error::Result<()> {
91+
/// let reader = Cursor::new([0xFF, 0x00, 0x1A]);
92+
/// let unsynchronized_reader = UnsynchronizedStream::new(reader);
93+
///
94+
/// let reader = unsynchronized_reader.get_ref();
95+
/// assert_eq!(reader.position(), 0);
96+
/// # Ok(()) }
97+
pub fn get_ref(&self) -> &R {
98+
&self.reader
99+
}
81100
}
82101

83102
impl<R: Read> Read for UnsynchronizedStream<R> {

lofty/src/ogg/tag.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ impl VorbisComments {
210210
/// let all_artists = vorbis_comments.get_all("ARTIST").collect::<Vec<&str>>();
211211
/// assert_eq!(all_artists, vec!["Foo artist", "Bar artist", "Baz artist"]);
212212
/// ```
213-
pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + Clone + '_ {
213+
pub fn get_all<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'a str> + Clone + 'a {
214214
self.items
215215
.iter()
216216
.filter_map(move |(k, v)| (k.eq_ignore_ascii_case(key)).then_some(v.as_str()))

lofty/src/ogg/vorbis/properties.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ where
121121
let last_page_abgp = last_page.header().abgp;
122122

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

126126
// Best case scenario
127127
if total_samples > 0 {
Binary file not shown.

0 commit comments

Comments
 (0)