Skip to content

Commit d9356ba

Browse files
committed
ID3v2: Support CHAP and CTOC frames
1 parent f3fcfd0 commit d9356ba

41 files changed

Lines changed: 1692 additions & 669 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- **ID3v2**:
12+
- `ChapterFrame` and `ChapterTableOfContentsFrame`
13+
- `FrameList`, a shared `Frame` storage used in `Id3v2Tag`, `ChapterFrame` and `ChapterTableOfContentsFrame`
14+
- `Id3v2ErrorKind::UnsupportedVersion`
15+
16+
### Changed
17+
18+
- **ID3v2**: Moved `error::{Id3v2Error, Id3v2ErrorKind}` to `id3::v2::error::{Id3v2Error, Id3v2ErrorKind}`
19+
920
## [0.23.3] - 2026-03-14
1021

1122
### Added

lofty/src/error.rs

Lines changed: 1 addition & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
//! which can be extended at any time.
55
66
use crate::file::FileType;
7-
use crate::id3::v2::FrameId;
8-
use crate::tag::ItemKey;
7+
use crate::id3::v2::error::Id3v2Error;
98
pub use crate::util::text::TextEncodingError;
109

1110
use std::collections::TryReserveError;
@@ -80,167 +79,6 @@ pub enum ErrorKind {
8079
Infallible(std::convert::Infallible),
8180
}
8281

83-
/// The types of errors that can occur while interacting with ID3v2 tags
84-
#[derive(Debug)]
85-
#[non_exhaustive]
86-
pub enum Id3v2ErrorKind {
87-
// Header
88-
/// Arises when an invalid ID3v2 version is found
89-
BadId3v2Version(u8, u8),
90-
/// Arises when a compressed ID3v2.2 tag is encountered
91-
///
92-
/// At the time the ID3v2.2 specification was written, a compression scheme wasn't decided.
93-
/// As such, it is recommended to ignore the tag entirely.
94-
V2Compression,
95-
/// Arises when an extended header has an invalid size (must be >= 6 bytes and less than the total tag size)
96-
BadExtendedHeaderSize,
97-
98-
// Frame
99-
/// Arises when a frame ID contains invalid characters (must be within `'A'..'Z'` or `'0'..'9'`)
100-
/// or if the ID is too short/long.
101-
BadFrameId(Vec<u8>),
102-
/// Arises when no frame ID is available in the ID3v2 specification for an item key
103-
/// and the associated value type.
104-
UnsupportedFrameId(ItemKey),
105-
/// Arises when a frame doesn't have enough data
106-
BadFrameLength,
107-
/// Arises when a frame with no content is parsed with [ParsingMode::Strict](crate::config::ParsingMode::Strict)
108-
EmptyFrame(FrameId<'static>),
109-
/// Arises when reading/writing a compressed or encrypted frame with no data length indicator
110-
MissingDataLengthIndicator,
111-
/// Arises when a frame or tag has its unsynchronisation flag set, but the content is not actually synchsafe
112-
///
113-
/// See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation) for an explanation.
114-
InvalidUnsynchronisation,
115-
/// Arises when a text encoding other than Latin-1 or UTF-16 appear in an ID3v2.2 tag
116-
V2InvalidTextEncoding,
117-
/// Arises when an invalid picture format is parsed. Only applicable to [`ID3v2Version::V2`](crate::id3::v2::Id3v2Version::V2)
118-
BadPictureFormat(String),
119-
/// Arises when invalid data is encountered while reading an ID3v2 synchronized text frame
120-
BadSyncText,
121-
/// Arises when decoding a [`UniqueFileIdentifierFrame`](crate::id3::v2::UniqueFileIdentifierFrame) with no owner
122-
MissingUfidOwner,
123-
/// Arises when decoding a [`RelativeVolumeAdjustmentFrame`](crate::id3::v2::RelativeVolumeAdjustmentFrame) with an invalid channel type
124-
BadRva2ChannelType,
125-
/// Arises when decoding a [`TimestampFormat`](crate::id3::v2::TimestampFormat) with an invalid type
126-
BadTimestampFormat,
127-
128-
// Compression
129-
#[cfg(feature = "id3v2_compression_support")]
130-
/// Arises when a compressed frame is unable to be decompressed
131-
Decompression(flate2::DecompressError),
132-
#[cfg(not(feature = "id3v2_compression_support"))]
133-
/// Arises when a compressed frame is encountered, but support is disabled
134-
CompressedFrameEncountered,
135-
136-
// Writing
137-
/// Arises when attempting to write an encrypted frame with an invalid encryption method symbol (must be <= 0x80)
138-
InvalidEncryptionMethodSymbol(u8),
139-
/// Arises when attempting to write an invalid Frame (Bad `FrameId`/`FrameValue` pairing)
140-
BadFrame(String, &'static str),
141-
/// Arises when attempting to write a [`CommentFrame`](crate::id3::v2::CommentFrame) or [`UnsynchronizedTextFrame`](crate::id3::v2::UnsynchronizedTextFrame) with an invalid language
142-
InvalidLanguage([u8; 3]),
143-
}
144-
145-
impl Display for Id3v2ErrorKind {
146-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147-
match self {
148-
// Header
149-
Self::BadId3v2Version(major, minor) => write!(
150-
f,
151-
"Found an invalid version (v{major}.{minor}), expected any major revision in: (2, \
152-
3, 4)"
153-
),
154-
Self::V2Compression => write!(f, "Encountered a compressed ID3v2.2 tag"),
155-
Self::BadExtendedHeaderSize => {
156-
write!(f, "Found an extended header with an invalid size")
157-
},
158-
159-
// Frame
160-
Self::BadFrameId(frame_id) => write!(f, "Failed to parse a frame ID: 0x{frame_id:x?}"),
161-
Self::UnsupportedFrameId(item_key) => {
162-
write!(f, "Unsupported frame ID for item key {item_key:?}")
163-
},
164-
Self::BadFrameLength => write!(
165-
f,
166-
"Frame isn't long enough to extract the necessary information"
167-
),
168-
Self::EmptyFrame(id) => write!(f, "Frame `{id}` is empty"),
169-
Self::MissingDataLengthIndicator => write!(
170-
f,
171-
"Encountered an encrypted frame without a data length indicator"
172-
),
173-
Self::InvalidUnsynchronisation => write!(f, "Encountered an invalid unsynchronisation"),
174-
Self::V2InvalidTextEncoding => {
175-
write!(f, "ID3v2.2 only supports Latin-1 and UTF-16 encodings")
176-
},
177-
Self::BadPictureFormat(format) => {
178-
write!(f, "Picture: Found unexpected format \"{format}\"")
179-
},
180-
Self::BadSyncText => write!(f, "Encountered invalid data in SYLT frame"),
181-
Self::MissingUfidOwner => write!(f, "Missing owner in UFID frame"),
182-
Self::BadRva2ChannelType => write!(f, "Encountered invalid channel type in RVA2 frame"),
183-
Self::BadTimestampFormat => write!(
184-
f,
185-
"Encountered an invalid timestamp format in a synchronized frame"
186-
),
187-
188-
// Compression
189-
#[cfg(feature = "id3v2_compression_support")]
190-
Self::Decompression(err) => write!(f, "Failed to decompress frame: {err}"),
191-
#[cfg(not(feature = "id3v2_compression_support"))]
192-
Self::CompressedFrameEncountered => write!(
193-
f,
194-
"Encountered a compressed ID3v2 frame, support is disabled"
195-
),
196-
197-
// Writing
198-
Self::InvalidEncryptionMethodSymbol(symbol) => write!(
199-
f,
200-
"Attempted to write an encrypted frame with an invalid method symbol ({symbol})"
201-
),
202-
Self::BadFrame(frame_id, frame_value) => write!(
203-
f,
204-
"Attempted to write an invalid frame. ID: \"{frame_id}\", Value: \"{frame_value}\"",
205-
),
206-
Self::InvalidLanguage(lang) => write!(
207-
f,
208-
"Invalid frame language found: {lang:?} (expected 3 ascii characters)"
209-
),
210-
}
211-
}
212-
}
213-
214-
/// An error that arises while interacting with an ID3v2 tag
215-
pub struct Id3v2Error {
216-
kind: Id3v2ErrorKind,
217-
}
218-
219-
impl Id3v2Error {
220-
/// Create a new `ID3v2Error` from an [`Id3v2ErrorKind`]
221-
#[must_use]
222-
pub const fn new(kind: Id3v2ErrorKind) -> Self {
223-
Self { kind }
224-
}
225-
226-
/// Returns the [`Id3v2ErrorKind`]
227-
pub fn kind(&self) -> &Id3v2ErrorKind {
228-
&self.kind
229-
}
230-
}
231-
232-
impl Debug for Id3v2Error {
233-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
234-
write!(f, "ID3v2: {:?}", self.kind)
235-
}
236-
}
237-
238-
impl Display for Id3v2Error {
239-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
240-
write!(f, "ID3v2: {}", self.kind)
241-
}
242-
}
243-
24482
/// An error that arises while decoding a file
24583
pub struct FileDecodingError {
24684
format: Option<FileType>,

lofty/src/id3/v2/error.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//! ID3v2 error types
2+
3+
use crate::id3::v2::{FrameId, Id3v2Version};
4+
use crate::prelude::ItemKey;
5+
6+
use std::fmt::{Debug, Display, Formatter};
7+
8+
/// The types of errors that can occur while interacting with ID3v2 tags
9+
#[derive(Debug)]
10+
#[non_exhaustive]
11+
pub enum Id3v2ErrorKind {
12+
// Header
13+
/// Arises when an invalid ID3v2 version is found
14+
BadId3v2Version(u8, u8),
15+
/// Arises when a compressed ID3v2.2 tag is encountered
16+
///
17+
/// At the time the ID3v2.2 specification was written, a compression scheme wasn't decided.
18+
/// As such, it is recommended to ignore the tag entirely.
19+
V2Compression,
20+
/// Arises when an extended header has an invalid size (must be >= 6 bytes and less than the total tag size)
21+
BadExtendedHeaderSize,
22+
23+
// Frame
24+
/// Arises when a frame ID contains invalid characters (must be within `'A'..'Z'` or `'0'..'9'`)
25+
/// or if the ID is too short/long.
26+
BadFrameId(Vec<u8>),
27+
/// Arises when no frame ID is available in the ID3v2 specification for an item key
28+
/// and the associated value type.
29+
UnsupportedFrameId(ItemKey),
30+
/// Arises when a frame doesn't have enough data
31+
BadFrameLength,
32+
/// Arises when a frame with no content is parsed with [ParsingMode::Strict](crate::config::ParsingMode::Strict)
33+
EmptyFrame(FrameId<'static>),
34+
/// Arises when reading/writing a compressed or encrypted frame with no data length indicator
35+
MissingDataLengthIndicator,
36+
/// Arises when a frame or tag has its unsynchronisation flag set, but the content is not actually syncsafe
37+
///
38+
/// See [`FrameFlags::unsynchronisation`](crate::id3::v2::FrameFlags::unsynchronisation) for an explanation.
39+
InvalidUnsynchronisation,
40+
/// Arises when a text encoding other than Latin-1 or UTF-16 appear in an ID3v2.2 tag
41+
V2InvalidTextEncoding,
42+
/// Arises when an invalid picture format is parsed. Only applicable to [`ID3v2Version::V2`](crate::id3::v2::Id3v2Version::V2)
43+
BadPictureFormat(String),
44+
/// Arises when invalid data is encountered while reading an ID3v2 synchronized text frame
45+
BadSyncText,
46+
/// Arises when decoding a [`UniqueFileIdentifierFrame`](crate::id3::v2::UniqueFileIdentifierFrame) with no owner
47+
MissingUfidOwner,
48+
/// Arises when decoding a [`RelativeVolumeAdjustmentFrame`](crate::id3::v2::RelativeVolumeAdjustmentFrame) with an invalid channel type
49+
BadRva2ChannelType,
50+
/// Arises when decoding a [`TimestampFormat`](crate::id3::v2::TimestampFormat) with an invalid type
51+
BadTimestampFormat,
52+
/// Arises when attempting to read/write a frame in a version that doesn't support it
53+
UnsupportedVersion {
54+
/// The ID of the frame being read/written
55+
id: FrameId<'static>,
56+
/// The version provided
57+
version: Id3v2Version,
58+
},
59+
60+
// Compression
61+
#[cfg(feature = "id3v2_compression_support")]
62+
/// Arises when a compressed frame is unable to be decompressed
63+
Decompression(flate2::DecompressError),
64+
#[cfg(not(feature = "id3v2_compression_support"))]
65+
/// Arises when a compressed frame is encountered, but support is disabled
66+
CompressedFrameEncountered,
67+
68+
// Writing
69+
/// Arises when attempting to write an encrypted frame with an invalid encryption method symbol (must be <= 0x80)
70+
InvalidEncryptionMethodSymbol(u8),
71+
/// Arises when attempting to write an invalid Frame (Bad `FrameId`/`FrameValue` pairing)
72+
BadFrame(String, &'static str),
73+
/// Arises when attempting to write a [`CommentFrame`](crate::id3::v2::CommentFrame) or [`UnsynchronizedTextFrame`](crate::id3::v2::UnsynchronizedTextFrame) with an invalid language
74+
InvalidLanguage([u8; 3]),
75+
}
76+
77+
impl Display for Id3v2ErrorKind {
78+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
79+
match self {
80+
// Header
81+
Self::BadId3v2Version(major, minor) => write!(
82+
f,
83+
"Found an invalid version (v{major}.{minor}), expected any major revision in: (2, \
84+
3, 4)"
85+
),
86+
Self::V2Compression => write!(f, "Encountered a compressed ID3v2.2 tag"),
87+
Self::BadExtendedHeaderSize => {
88+
write!(f, "Found an extended header with an invalid size")
89+
},
90+
91+
// Frame
92+
Self::BadFrameId(frame_id) => {
93+
write!(f, "Failed to parse a frame ID: {}", frame_id.escape_ascii())
94+
},
95+
Self::UnsupportedFrameId(item_key) => {
96+
write!(f, "Unsupported frame ID for item key {item_key:?}")
97+
},
98+
Self::BadFrameLength => write!(
99+
f,
100+
"Frame isn't long enough to extract the necessary information"
101+
),
102+
Self::EmptyFrame(id) => write!(f, "Frame `{id}` is empty"),
103+
Self::MissingDataLengthIndicator => write!(
104+
f,
105+
"Encountered an encrypted frame without a data length indicator"
106+
),
107+
Self::InvalidUnsynchronisation => write!(f, "Encountered an invalid unsynchronisation"),
108+
Self::V2InvalidTextEncoding => {
109+
write!(f, "ID3v2.2 only supports Latin-1 and UTF-16 encodings")
110+
},
111+
Self::BadPictureFormat(format) => {
112+
write!(f, "Picture: Found unexpected format \"{format}\"")
113+
},
114+
Self::BadSyncText => write!(f, "Encountered invalid data in SYLT frame"),
115+
Self::MissingUfidOwner => write!(f, "Missing owner in UFID frame"),
116+
Self::BadRva2ChannelType => write!(f, "Encountered invalid channel type in RVA2 frame"),
117+
Self::BadTimestampFormat => write!(
118+
f,
119+
"Encountered an invalid timestamp format in a synchronized frame"
120+
),
121+
Self::UnsupportedVersion { id, version } => write!(
122+
f,
123+
"attempted to read/write '{}' frame in version {version}",
124+
id.as_str()
125+
),
126+
127+
// Compression
128+
#[cfg(feature = "id3v2_compression_support")]
129+
Self::Decompression(err) => write!(f, "Failed to decompress frame: {err}"),
130+
#[cfg(not(feature = "id3v2_compression_support"))]
131+
Self::CompressedFrameEncountered => write!(
132+
f,
133+
"Encountered a compressed ID3v2 frame, support is disabled"
134+
),
135+
136+
// Writing
137+
Self::InvalidEncryptionMethodSymbol(symbol) => write!(
138+
f,
139+
"Attempted to write an encrypted frame with an invalid method symbol ({symbol})"
140+
),
141+
Self::BadFrame(frame_id, frame_value) => write!(
142+
f,
143+
"Attempted to write an invalid frame. ID: \"{frame_id}\", Value: \"{frame_value}\"",
144+
),
145+
Self::InvalidLanguage(lang) => write!(
146+
f,
147+
"Invalid frame language found: {lang:?} (expected 3 ascii characters)"
148+
),
149+
}
150+
}
151+
}
152+
153+
/// An error that arises while interacting with an ID3v2 tag
154+
pub struct Id3v2Error {
155+
kind: Id3v2ErrorKind,
156+
}
157+
158+
impl Id3v2Error {
159+
/// Create a new `ID3v2Error` from an [`Id3v2ErrorKind`]
160+
#[must_use]
161+
pub const fn new(kind: Id3v2ErrorKind) -> Self {
162+
Self { kind }
163+
}
164+
165+
/// Returns the [`Id3v2ErrorKind`]
166+
pub fn kind(&self) -> &Id3v2ErrorKind {
167+
&self.kind
168+
}
169+
}
170+
171+
impl Debug for Id3v2Error {
172+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
173+
write!(f, "ID3v2: {:?}", self.kind)
174+
}
175+
}
176+
177+
impl Display for Id3v2Error {
178+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
179+
write!(f, "ID3v2: {}", self.kind)
180+
}
181+
}

0 commit comments

Comments
 (0)