Skip to content

Commit 553d16f

Browse files
committed
APE: Fix property reading on old stream versions
This also makes property reading take `ParsingMode` into account.
1 parent 6cab8aa commit 553d16f

File tree

6 files changed

+83
-99
lines changed

6 files changed

+83
-99
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- **WavPack**: Custom sample rates will no longer be overwritten
3333
- When a custom sample rate (or multiplier) was encountered, it would accidentally be overwritten with 0, causing
3434
incorrect duration and bitrate values.
35+
- **APE**: Reading properties on older files will no longer error
36+
- Older APE stream versions were not properly handled, leading to incorrect properties and errors.
3537

3638
## [0.15.0] - 2023-07-11
3739

src/ape/properties.rs

Lines changed: 66 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::error::Result;
22
use crate::macros::decode_err;
3+
use crate::probe::ParsingMode;
34
use crate::properties::FileProperties;
45

56
use std::convert::TryInto;
@@ -76,6 +77,7 @@ pub(super) fn read_properties<R>(
7677
data: &mut R,
7778
stream_len: u64,
7879
file_length: u64,
80+
parse_mode: ParsingMode,
7981
) -> Result<ApeProperties>
8082
where
8183
R: Read + Seek,
@@ -86,9 +88,9 @@ where
8688

8789
// Property reading differs between versions
8890
if version >= 3980 {
89-
properties_gt_3980(data, version, stream_len, file_length)
91+
properties_gt_3980(data, version, stream_len, file_length, parse_mode)
9092
} else {
91-
properties_lt_3980(data, version, stream_len, file_length)
93+
properties_lt_3980(data, version, stream_len, file_length, parse_mode)
9294
}
9395
}
9496

@@ -97,6 +99,7 @@ fn properties_gt_3980<R>(
9799
version: u16,
98100
stream_len: u64,
99101
file_length: u64,
102+
parse_mode: ParsingMode,
100103
) -> Result<ApeProperties>
101104
where
102105
R: Read + Seek,
@@ -126,6 +129,9 @@ where
126129
data.read_exact(&mut header)
127130
.map_err(|_| decode_err!(Ape, "Not enough data left in reader to finish MAC header"))?;
128131

132+
let mut properties = ApeProperties::default();
133+
properties.version = version;
134+
129135
// Skip the first 4 bytes of the header
130136
// Compression type (2)
131137
// Format flags (2)
@@ -135,72 +141,57 @@ where
135141
let final_frame_blocks = header_read.read_u32::<LittleEndian>()?;
136142
let total_frames = header_read.read_u32::<LittleEndian>()?;
137143

138-
if total_frames == 0 {
139-
decode_err!(@BAIL Ape, "File contains no frames");
140-
}
141-
142-
let bits_per_sample = header_read.read_u16::<LittleEndian>()?;
144+
properties.bit_depth = header_read.read_u16::<LittleEndian>()? as u8;
145+
properties.channels = header_read.read_u16::<LittleEndian>()? as u8;
146+
properties.sample_rate = header_read.read_u32::<LittleEndian>()?;
143147

144-
let channels = header_read.read_u16::<LittleEndian>()?;
145-
146-
if !(1..=32).contains(&channels) {
147-
decode_err!(@BAIL Ape, "File has an invalid channel count (must be between 1 and 32 inclusive)");
148+
match verify(total_frames, properties.channels) {
149+
Err(e) if parse_mode == ParsingMode::Strict => return Err(e),
150+
Err(_) => return Ok(properties),
151+
_ => {},
148152
}
149153

150-
let sample_rate = header_read.read_u32::<LittleEndian>()?;
151-
152-
let (duration, overall_bitrate, audio_bitrate) = get_duration_bitrate(
154+
get_duration_bitrate(
155+
&mut properties,
153156
file_length,
154157
total_frames,
155158
final_frame_blocks,
156159
blocks_per_frame,
157-
sample_rate,
158160
stream_len,
159161
);
160162

161-
Ok(ApeProperties {
162-
version,
163-
duration,
164-
overall_bitrate,
165-
audio_bitrate,
166-
sample_rate,
167-
bit_depth: bits_per_sample as u8,
168-
channels: channels as u8,
169-
})
163+
Ok(properties)
170164
}
171165

172166
fn properties_lt_3980<R>(
173167
data: &mut R,
174168
version: u16,
175169
stream_len: u64,
176170
file_length: u64,
171+
parse_mode: ParsingMode,
177172
) -> Result<ApeProperties>
178173
where
179-
R: Read + Seek,
174+
R: Read,
180175
{
181176
// Versions < 3980 don't have a descriptor
182177
let mut header = [0; 26];
183178
data.read_exact(&mut header)
184179
.map_err(|_| decode_err!(Ape, "Not enough data left in reader to finish MAC header"))?;
185180

186-
// We don't need all the header data, so just make 2 slices
187-
let header_first = &mut &header[..8];
181+
let mut properties = ApeProperties::default();
182+
properties.version = version;
188183

189-
// Skipping 8 bytes
190-
// WAV header length (4)
191-
// WAV tail length (4)
192-
let header_second = &mut &header[18..];
184+
let header_reader = &mut &header[..];
193185

194-
let compression_level = header_first.read_u16::<LittleEndian>()?;
195-
196-
let format_flags = header_first.read_u16::<LittleEndian>()?;
197186
// https://github.com/fernandotcl/monkeys-audio/blob/5fe956c7e67c13daa80518a4cc7001e9fa185297/src/MACLib/MACLib.h#L74
198-
let bit_depth = if (format_flags & 0b1) == 1 {
199-
8
200-
} else if (format_flags & 0b100) == 4 {
201-
24
187+
let compression_level = header_reader.read_u16::<LittleEndian>()?;
188+
let format_flags = header_reader.read_u16::<LittleEndian>()?;
189+
if (format_flags & 0b1) == 1 {
190+
properties.bit_depth = 8
191+
} else if (format_flags & 0b1000) == 8 {
192+
properties.bit_depth = 24
202193
} else {
203-
16
194+
properties.bit_depth = 16
204195
};
205196

206197
let blocks_per_frame = match version {
@@ -209,74 +200,68 @@ where
209200
_ => 9216,
210201
};
211202

212-
let channels = header_first.read_u16::<LittleEndian>()?;
213-
214-
if !(1..=32).contains(&channels) {
215-
decode_err!(@BAIL Ape, "File has an invalid channel count (must be between 1 and 32 inclusive)");
216-
}
203+
properties.channels = header_reader.read_u16::<LittleEndian>()? as u8;
204+
properties.sample_rate = header_reader.read_u32::<LittleEndian>()?;
217205

218-
let sample_rate = header_first.read_u32::<LittleEndian>()?;
206+
// Skipping 8 bytes
207+
// WAV header length (4)
208+
// WAV tail length (4)
209+
let mut _skip = [0; 8];
210+
header_reader.read_exact(&mut _skip)?;
219211

220-
// Move on the second part of header
221-
let total_frames = header_second.read_u32::<LittleEndian>()?;
212+
let total_frames = header_reader.read_u32::<LittleEndian>()?;
213+
let final_frame_blocks = header_reader.read_u32::<LittleEndian>()?;
222214

223-
if total_frames == 0 {
224-
decode_err!(@BAIL Ape, "File contains no frames");
215+
match verify(total_frames, properties.channels) {
216+
Err(e) if parse_mode == ParsingMode::Strict => return Err(e),
217+
Err(_) => return Ok(properties),
218+
_ => {},
225219
}
226220

227-
let final_frame_blocks = data.read_u32::<LittleEndian>()?;
228-
229-
let (duration, overall_bitrate, audio_bitrate) = get_duration_bitrate(
221+
get_duration_bitrate(
222+
&mut properties,
230223
file_length,
231224
total_frames,
232225
final_frame_blocks,
233226
blocks_per_frame,
234-
sample_rate,
235227
stream_len,
236228
);
237229

238-
Ok(ApeProperties {
239-
version,
240-
duration,
241-
overall_bitrate,
242-
audio_bitrate,
243-
sample_rate,
244-
bit_depth,
245-
channels: channels as u8,
246-
})
230+
Ok(properties)
231+
}
232+
233+
/// Verifies the channel count falls within the bounds of the spec, and we have some audio frames to work with.
234+
fn verify(total_frames: u32, channels: u8) -> Result<()> {
235+
if !(1..=32).contains(&channels) {
236+
decode_err!(@BAIL Ape, "File has an invalid channel count (must be between 1 and 32 inclusive)");
237+
}
238+
239+
if total_frames == 0 {
240+
decode_err!(@BAIL Ape, "File contains no frames");
241+
}
242+
243+
Ok(())
247244
}
248245

249246
fn get_duration_bitrate(
247+
properties: &mut ApeProperties,
250248
file_length: u64,
251249
total_frames: u32,
252250
final_frame_blocks: u32,
253251
blocks_per_frame: u32,
254-
sample_rate: u32,
255252
stream_len: u64,
256-
) -> (Duration, u32, u32) {
253+
) {
257254
let mut total_samples = u64::from(final_frame_blocks);
258255

259256
if total_samples > 1 {
260257
total_samples += u64::from(blocks_per_frame) * u64::from(total_frames - 1)
261258
}
262259

263-
let mut overall_bitrate = 0;
264-
let mut audio_bitrate = 0;
265-
266-
if sample_rate > 0 {
267-
let length = (total_samples * 1000) / u64::from(sample_rate);
260+
if properties.sample_rate > 0 {
261+
let length = (total_samples as f64 * 1000.0) / f64::from(properties.sample_rate);
268262

269-
if length > 0 {
270-
overall_bitrate = crate::div_ceil(file_length * 8, length) as u32;
271-
audio_bitrate = crate::div_ceil(stream_len * 8, length) as u32;
272-
}
273-
274-
(
275-
Duration::from_millis(length),
276-
overall_bitrate,
277-
audio_bitrate,
278-
)
279-
} else {
280-
(Duration::ZERO, overall_bitrate, audio_bitrate)
263+
properties.duration = Duration::from_millis((length + 0.5) as u64);
264+
properties.audio_bitrate = ((stream_len as f64) * 8.0 / length + 0.5) as u32;
265+
properties.overall_bitrate = ((file_length as f64) * 8.0 / length + 0.5) as u32;
281266
}
282267
}

src/ape/read.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,12 @@ where
125125
id3v2_tag,
126126
ape_tag,
127127
properties: if parse_options.read_properties {
128-
super::properties::read_properties(data, stream_len, file_length)?
128+
super::properties::read_properties(
129+
data,
130+
stream_len,
131+
file_length,
132+
parse_options.parsing_mode,
133+
)?
129134
} else {
130135
ApeProperties::default()
131136
},

src/lib.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@
138138
clippy::field_reassign_with_default,
139139
clippy::manual_range_patterns, /* This is not at all clearer as it suggests */
140140
clippy::explicit_iter_loop,
141-
clippy::from_iter_instead_of_collect
141+
clippy::from_iter_instead_of_collect,
142+
clippy::no_effect_underscore_binding,
143+
clippy::used_underscore_binding,
142144
)]
143145
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
144146

@@ -183,15 +185,3 @@ pub use crate::traits::{Accessor, MergeTag, SplitTag, TagExt};
183185
pub use picture::PictureInformation;
184186

185187
pub use lofty_attr::LoftyFile;
186-
187-
// TODO: https://github.com/rust-lang/rust/issues/88581
188-
#[inline]
189-
pub(crate) const fn div_ceil(dividend: u64, divisor: u64) -> u64 {
190-
let d = dividend / divisor;
191-
let r = dividend % divisor;
192-
if r > 0 && divisor > 0 {
193-
d + 1
194-
} else {
195-
d
196-
}
197-
}

src/properties.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ mod tests {
171171
version: 3990,
172172
duration: Duration::from_millis(1428),
173173
overall_bitrate: 361,
174-
audio_bitrate: 361,
174+
audio_bitrate: 360,
175175
sample_rate: 48000,
176176
bit_depth: 16,
177177
channels: 2,

tests/files/zero_sized.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ use lofty::iff::aiff::AiffFile;
44
use lofty::iff::wav::WavFile;
55
use lofty::mp4::Mp4File;
66
use lofty::mpeg::MpegFile;
7-
use lofty::{AudioFile, ParseOptions};
7+
use lofty::{AudioFile, ParseOptions, ParsingMode};
88

99
fn read_file_with_properties<A: AudioFile>(path: &str) -> bool {
10-
let res =
11-
<A as AudioFile>::read_from(&mut std::fs::File::open(path).unwrap(), ParseOptions::new());
10+
let res = <A as AudioFile>::read_from(
11+
&mut std::fs::File::open(path).unwrap(),
12+
ParseOptions::new().parsing_mode(ParsingMode::Strict),
13+
);
1214
res.is_ok()
1315
}
1416

0 commit comments

Comments
 (0)