mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
AIFF: Fix parsing of invalid f80 sample rates
This commit is contained in:
parent
37fdc657a3
commit
e9ec05babf
6 changed files with 245 additions and 87 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Binary file not shown.
Loading…
Reference in a new issue