AIFF: Fix parsing of invalid f80 sample rates

This commit is contained in:
Serial 2024-07-13 16:15:18 -04:00 committed by Alex
parent 37fdc657a3
commit e9ec05babf
6 changed files with 245 additions and 87 deletions

View file

@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **WAV**: Fix panic when reading properties with large written bytes per second ([issue](https://github.com/Serial-ATA/lofty-rs/issues/420))
- **Vorbis**: Fix panic when reading properties of a file with large absolute granule positions ([issue](https://github.com/Serial-ATA/lofty-rs/issues/421))
- **FLAC**: Fix panic when reading properties of a file with incorrect block sizes ([issue](https://github.com/Serial-ATA/lofty-rs/issues/422))
- **AIFF**: Fix panic when reading properties of a file with invalid f80 sample rate ([issue](https://github.com/Serial-ATA/lofty-rs/issues/424))
## [0.20.1] - 2024-07-02

View file

@ -8,6 +8,7 @@ use std::borrow::Cow;
use std::io::Read;
use std::time::Duration;
use crate::io::ReadExt;
use byteorder::{BigEndian, ReadBytesExt};
/// The AIFC compression type
@ -166,34 +167,13 @@ pub(super) fn read_properties(
let sample_frames = comm.read_u32::<BigEndian>()?;
let sample_size = comm.read_u16::<BigEndian>()?;
let mut sample_rate_bytes = [0; 10];
comm.read_exact(&mut sample_rate_bytes)?;
let sample_rate_extended = comm.read_f80()?;
let sample_rate_64 = sample_rate_extended.as_f64();
if !sample_rate_64.is_finite() || !sample_rate_64.is_sign_positive() {
decode_err!(@BAIL Aiff, "Invalid sample rate");
}
let sign = u64::from(sample_rate_bytes[0] & 0x80);
sample_rate_bytes[0] &= 0x7F;
let mut exponent = u16::from(sample_rate_bytes[0]) << 8 | u16::from(sample_rate_bytes[1]);
exponent = exponent - 16383 + 1023;
let fraction = &mut sample_rate_bytes[2..];
fraction[0] &= 0x7F;
let fraction: Vec<u64> = fraction.iter_mut().map(|v| u64::from(*v)).collect();
let fraction = fraction[0] << 56
| fraction[1] << 48
| fraction[2] << 40
| fraction[3] << 32
| fraction[4] << 24
| fraction[5] << 16
| fraction[6] << 8
| fraction[7];
let f64_bytes = sign << 56 | u64::from(exponent) << 52 | fraction >> 11;
let float = f64::from_be_bytes(f64_bytes.to_be_bytes());
let sample_rate = float.round() as u32;
let sample_rate = sample_rate_64.round() as u32;
let (duration, overall_bitrate, audio_bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (f64::from(sample_frames) * 1000.0) / f64::from(sample_rate);
@ -207,50 +187,60 @@ pub(super) fn read_properties(
(Duration::ZERO, 0, 0)
};
let mut compression = None;
if comm.len() >= 5 && compression_present == CompressionPresent::Yes {
let mut compression_type = [0u8; 4];
comm.read_exact(&mut compression_type)?;
compression = Some(match &compression_type {
b"NONE" => AiffCompressionType::None,
b"ACE2" => AiffCompressionType::ACE2,
b"ACE8" => AiffCompressionType::ACE8,
b"MAC3" => AiffCompressionType::MAC3,
b"MAC6" => AiffCompressionType::MAC6,
b"sowt" => AiffCompressionType::sowt,
b"fl32" => AiffCompressionType::fl32,
b"fl64" => AiffCompressionType::fl64,
b"alaw" => AiffCompressionType::alaw,
b"ulaw" => AiffCompressionType::ulaw,
b"ULAW" => AiffCompressionType::ULAW,
b"ALAW" => AiffCompressionType::ALAW,
b"FL32" => AiffCompressionType::FL32,
_ => {
log::debug!(
"Encountered unknown compression type: {:?}",
compression_type
);
// We have to read the compression name string
let mut compression_name = String::new();
let compression_name_size = comm.read_u8()?;
if compression_name_size > 0 {
let mut compression_name_bytes = try_vec![0u8; compression_name_size as usize];
comm.read_exact(&mut compression_name_bytes)?;
compression_name = utf8_decode(compression_name_bytes)?;
}
AiffCompressionType::Other {
compression_type,
compression_name,
}
},
let is_compressed = comm.len() >= 5 && compression_present == CompressionPresent::Yes;
if !is_compressed {
return Ok(AiffProperties {
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
sample_size,
channels,
compression_type: None,
});
}
let mut compression_type = [0u8; 4];
comm.read_exact(&mut compression_type)?;
let compression = Some(match &compression_type {
b"NONE" => AiffCompressionType::None,
b"ACE2" => AiffCompressionType::ACE2,
b"ACE8" => AiffCompressionType::ACE8,
b"MAC3" => AiffCompressionType::MAC3,
b"MAC6" => AiffCompressionType::MAC6,
b"sowt" => AiffCompressionType::sowt,
b"fl32" => AiffCompressionType::fl32,
b"fl64" => AiffCompressionType::fl64,
b"alaw" => AiffCompressionType::alaw,
b"ulaw" => AiffCompressionType::ulaw,
b"ULAW" => AiffCompressionType::ULAW,
b"ALAW" => AiffCompressionType::ALAW,
b"FL32" => AiffCompressionType::FL32,
_ => {
log::debug!(
"Encountered unknown compression type: {:?}",
compression_type
);
// We have to read the compression name string
let mut compression_name = String::new();
let compression_name_size = comm.read_u8()?;
if compression_name_size > 0 {
let mut compression_name_bytes = try_vec![0u8; compression_name_size as usize];
comm.read_exact(&mut compression_name_bytes)?;
compression_name = utf8_decode(compression_name_bytes)?;
}
AiffCompressionType::Other {
compression_type,
compression_name,
}
},
});
Ok(AiffProperties {
duration,
overall_bitrate,

View file

@ -1,6 +1,7 @@
//! Various traits for reading and writing to file-like objects
use crate::error::LoftyError;
use crate::error::{LoftyError, Result};
use crate::util::math::F80;
use std::collections::VecDeque;
use std::fs::File;
@ -49,13 +50,13 @@ pub trait Truncate {
/// # Errors
///
/// Errors depend on the object being truncated, which may not always be fallible.
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error>;
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error>;
}
impl Truncate for File {
type Error = std::io::Error;
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> {
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.set_len(new_len)
}
}
@ -63,7 +64,7 @@ impl Truncate for File {
impl Truncate for Vec<u8> {
type Error = std::convert::Infallible;
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> {
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.truncate(new_len as usize);
Ok(())
}
@ -72,7 +73,7 @@ impl Truncate for Vec<u8> {
impl Truncate for VecDeque<u8> {
type Error = std::convert::Infallible;
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> {
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.truncate(new_len as usize);
Ok(())
}
@ -84,7 +85,7 @@ where
{
type Error = <T as Truncate>::Error;
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> {
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.get_mut().truncate(new_len)
}
}
@ -95,7 +96,7 @@ where
{
type Error = <T as Truncate>::Error;
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> {
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.as_mut().truncate(new_len)
}
}
@ -106,7 +107,7 @@ where
{
type Error = <T as Truncate>::Error;
fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> {
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
(**self).truncate(new_len)
}
}
@ -136,13 +137,13 @@ pub trait Length {
/// # Errors
///
/// Errors depend on the object being read, which may not always be fallible.
fn len(&self) -> Result<u64, Self::Error>;
fn len(&self) -> std::result::Result<u64, Self::Error>;
}
impl Length for File {
type Error = std::io::Error;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
self.metadata().map(|m| m.len())
}
}
@ -150,7 +151,7 @@ impl Length for File {
impl Length for Vec<u8> {
type Error = std::convert::Infallible;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
Ok(self.len() as u64)
}
}
@ -158,7 +159,7 @@ impl Length for Vec<u8> {
impl Length for VecDeque<u8> {
type Error = std::convert::Infallible;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
Ok(self.len() as u64)
}
}
@ -169,7 +170,7 @@ where
{
type Error = <T as Length>::Error;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
Length::len(self.get_ref())
}
}
@ -180,7 +181,7 @@ where
{
type Error = <T as Length>::Error;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
Length::len(self.as_ref())
}
}
@ -191,7 +192,7 @@ where
{
type Error = <T as Length>::Error;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
Length::len(*self)
}
}
@ -202,7 +203,7 @@ where
{
type Error = <T as Length>::Error;
fn len(&self) -> Result<u64, Self::Error> {
fn len(&self) -> std::result::Result<u64, Self::Error> {
Length::len(*self)
}
}
@ -229,6 +230,22 @@ where
{
}
pub(crate) trait ReadExt: Read {
fn read_f80(&mut self) -> Result<F80>;
}
impl<R> ReadExt for R
where
R: Read,
{
fn read_f80(&mut self) -> Result<F80> {
let mut bytes = [0; 10];
self.read_exact(&mut bytes)?;
Ok(F80::from_be_bytes(bytes))
}
}
#[cfg(test)]
mod tests {
use crate::config::{ParseOptions, WriteOptions};

View file

@ -25,6 +25,73 @@ macro_rules! unsigned_rounded_division {
unsigned_rounded_division!(u8, u16, u32, u64, u128, usize);
/// An 80-bit extended precision floating-point number.
///
/// This is used in AIFF.
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub(crate) struct F80 {
signed: bool,
// 15-bit exponent with a bias of 16383
exponent: u16,
fraction: u64,
}
impl F80 {
/// Create a new `F80` from big-endian bytes.
///
/// See [here](https://en.wikipedia.org/wiki/Extended_precision#/media/File:X86_Extended_Floating_Point_Format.svg) for a diagram of the format.
pub fn from_be_bytes(bytes: [u8; 10]) -> Self {
let signed = bytes[0] & 0x80 != 0;
let exponent = (u16::from(bytes[0] & 0x7F) << 8) | u16::from(bytes[1]);
let mut fraction_bytes = [0; 8];
fraction_bytes.copy_from_slice(&bytes[2..]);
let fraction = u64::from_be_bytes(fraction_bytes);
Self {
signed,
exponent,
fraction,
}
}
/// Convert the `F80` to an `f64`.
pub fn as_f64(&self) -> f64 {
// Apple® Apple Numerics Manual, Second Edition, Table 2-7:
//
// Biased exponent e Integer i Fraction f Value v Class of v
// 0 <= e <= 32766 1 (any) v = (-1)^s * 2^(e-16383) * (1.f) Normalized
// 0 <= e <= 32766 0 f != 0 v = (-1)^s * 2^(e-16383) * (0.f) Denormalized
// 0 <= e <= 32766 0 f = 0 v = (-1)^s * 0 Zero
// e = 32767 (any) f = 0 v = (-1)^s * Infinity Infinity
// e = 32767 (any) f != 0 v is a NaN NaN
let sign = if self.signed { 1 } else { 0 };
// e = 32767
if self.exponent == 32767 {
if self.fraction == 0 {
return f64::from_bits((sign << 63) | f64::INFINITY.to_bits());
}
return f64::from_bits((sign << 63) | f64::NAN.to_bits());
}
// 0 <= e <= 32766, i = 0, f = 0
if self.fraction == 0 {
return f64::from_bits(sign << 63);
}
// 0 <= e <= 32766, 0 <= i <= 1, f >= 0
let fraction = self.fraction & 0x7FFF_FFFF_FFFF_FFFF;
let exponent = self.exponent as i16 - 16383 + 1023;
let bits = (sign << 63) | ((exponent as u64) << 52) | (fraction >> 11);
f64::from_bits(bits)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -46,11 +113,11 @@ mod tests {
TestEntry { lhs: 3, rhs: 2, result: 2 },
TestEntry { lhs: 4, rhs: 2, result: 2 },
TestEntry { lhs: 5, rhs: 2, result: 3 },
// Should be rounded up to 1
TestEntry { lhs: 800, rhs: 1500, result: 1 },
TestEntry { lhs: 1500, rhs: 3000, result: 1 },
// Shouldn't be rounded
TestEntry { lhs: 0, rhs: 4000, result: 0 },
TestEntry { lhs: 1500, rhs: 4000, result: 0 },
@ -61,4 +128,78 @@ mod tests {
assert_eq!(result, test.result, "{}.div_round({})", test.lhs, test.rhs);
}
}
#[test]
fn test_f80() {
fn cmp_float_nearly_equal(a: f64, b: f64) -> bool {
if a.is_infinite() && b.is_infinite() {
return true;
}
if a.is_nan() && b.is_nan() {
return true;
}
(a - b).abs() < f64::EPSILON
}
#[derive(Debug)]
struct TestEntry {
input: [u8; 10],
output_f64: f64,
}
let tests = [
TestEntry {
input: [0; 10],
output_f64: 0.0,
},
TestEntry {
input: [0x7F, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0],
output_f64: f64::INFINITY,
},
TestEntry {
input: [0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0],
output_f64: f64::NEG_INFINITY,
},
TestEntry {
input: [0x7F, 0xFF, 0x80, 0, 0, 0, 0, 0, 0, 0],
output_f64: f64::NAN,
},
TestEntry {
input: [0xFF, 0xFF, 0x80, 0, 0, 0, 0, 0, 0, 0],
output_f64: -f64::NAN,
},
TestEntry {
input: [0x3F, 0xFC, 0x80, 0, 0, 0, 0, 0, 0, 0],
output_f64: 0.125,
},
TestEntry {
input: [0x3F, 0xFF, 0x80, 0, 0, 0, 0, 0, 0, 0],
output_f64: 1.0,
},
TestEntry {
input: [0x40, 0x00, 0x80, 0, 0, 0, 0, 0, 0, 0],
output_f64: 2.0,
},
TestEntry {
input: [0x40, 0x00, 0xC0, 0, 0, 0, 0, 0, 0, 0],
output_f64: 3.0,
},
TestEntry {
input: [0x40, 0x0E, 0xBB, 0x80, 0, 0, 0, 0, 0, 0],
output_f64: 48000.0,
},
];
for test in &tests {
let f80 = F80::from_be_bytes(test.input);
let f64 = f80.as_f64();
assert!(
cmp_float_nearly_equal(f64, test.output_f64),
"F80::as_f64({f80:?}) == {f64} (expected {})",
test.output_f64
);
}
}
}

View file

@ -1,7 +1,16 @@
use crate::oom_test;
use crate::{get_reader, oom_test};
use lofty::config::ParseOptions;
use lofty::file::AudioFile;
use lofty::iff::aiff::AiffFile;
#[test]
fn oom1() {
oom_test::<AiffFile>("aifffile_read_from/oom-88065007d35ee271d5812fd723a3b458488813ea");
}
#[test]
fn panic1() {
let mut reader =
get_reader("aifffile_read_from/full_test_IDX_5_RAND_89430166450532348786207.aiff");
let _ = AiffFile::read_from(&mut reader, ParseOptions::default());
}