Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- **ParsingMode**: A new variant, `BestAttempt` will attempt to fill holes in otherwise valid tag items ([PR](https://github.com/Serial-ATA/lofty-rs/pull/205))
- **🎉 Support for Musepack files** ([issue](https://github.com/Serial-ATA/lofty-rs/issues/199)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/200))

### Changed
- **Probe**: The default `ParsingMode` is now `ParsingMode::BestAttempt` (It was previously `ParsingMode::Strict`)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ byteorder = "1.4.3"
# ID3 compressed frames
flate2 = { version = "1.0.26", optional = true }
# Proc macros
lofty_attr = "0.7.0"
lofty_attr = { path = "lofty_attr" }
# Debug logging
log = "0.4.17"
# OGG Vorbis/Opus
Expand Down
27 changes: 14 additions & 13 deletions SUPPORTED_FORMATS.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
| File Format | Metadata Format(s) |
|-------------|--------------------------------------|
| AAC (ADTS) | `ID3v2`, `ID3v1` |
| Ape | `APEv2`, `APEv1`, `ID3v2`\*, `ID3v1` |
| AIFF | `ID3v2`, `Text Chunks` |
| FLAC | `Vorbis Comments`, `ID3v2`\* |
| MP3 | `ID3v2`, `ID3v1`, `APEv2`, `APEv1` |
| MP4 | `iTunes-style ilst` |
| Opus | `Vorbis Comments` |
| Ogg Vorbis | `Vorbis Comments` |
| Speex | `Vorbis Comments` |
| WAV | `ID3v2`, `RIFF INFO` |
| WavPack | `APEv2`, `APEv1`, `ID3v1` |
| File Format | Metadata Format(s) |
|-------------|------------------------------|
| AAC (ADTS) | `ID3v2`, `ID3v1` |
| Ape | `APE`, `ID3v2`\*, `ID3v1` |
| AIFF | `ID3v2`, `Text Chunks` |
| FLAC | `Vorbis Comments`, `ID3v2`\* |
| MP3 | `ID3v2`, `ID3v1`, `APE` |
| MP4 | `iTunes-style ilst` |
| MPC | `APE`, `ID3v2`\*, `ID3v1`\* |
| Opus | `Vorbis Comments` |
| Ogg Vorbis | `Vorbis Comments` |
| Speex | `Vorbis Comments` |
| WAV | `ID3v2`, `RIFF INFO` |
| WavPack | `APE`, `ID3v1` |

\* The tag will be **read only**, due to lack of official support
1 change: 1 addition & 0 deletions benches/read_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fn content_infer_read(c: &mut Criterion) {
(FLAC, "../tests/files/assets/minimal/full_test.flac"),
(MP4, "../tests/files/assets/minimal/m4a_codec_aac.m4a"),
(MP3, "../tests/files/assets/minimal/full_test.mp3"),
(MPC, "../tests/files/assets/minimal/mpc_sv8.mpc"),
(OPUS, "../tests/files/assets/minimal/full_test.opus"),
(RIFF, "../tests/files/assets/minimal/wav_format_pcm.wav"),
(SPEEX, "../tests/files/assets/minimal/full_test.spx"),
Expand Down
4 changes: 4 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ members = ["."]
name = "filetype_from_buffer"
path = "fuzz_targets/filetype_from_buffer.rs"

[[bin]]
name = "mpcfile_read_from"
path = "fuzz_targets/mpcfile_read_from.rs"

[[bin]]
name = "mpegfile_read_from"
path = "fuzz_targets/mpegfile_read_from.rs"
Expand Down
10 changes: 10 additions & 0 deletions fuzz/fuzz_targets/mpcfile_read_from.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#![no_main]

use std::io::Cursor;

use libfuzzer_sys::fuzz_target;
use lofty::{AudioFile, ParseOptions};

fuzz_target!(|data: Vec<u8>| {
let _ = lofty::musepack::MpcFile::read_from(&mut Cursor::new(data), ParseOptions::new());
});
5 changes: 3 additions & 2 deletions lofty_attr/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ use quote::quote;
pub(crate) fn opt_internal_file_type(
struct_name: String,
) -> Option<(proc_macro2::TokenStream, bool)> {
const LOFTY_FILE_TYPES: [&str; 11] = [
"Aac", "Aiff", "Ape", "Flac", "Mpeg", "Mp4", "Opus", "Vorbis", "Speex", "Wav", "WavPack",
const LOFTY_FILE_TYPES: [&str; 12] = [
"Aac", "Aiff", "Ape", "Flac", "Mpeg", "Mp4", "Mpc", "Opus", "Vorbis", "Speex", "Wav",
"WavPack",
];

const ID3V2_STRIPPABLE: [&str; 2] = ["Flac", "Ape"];
Expand Down
17 changes: 5 additions & 12 deletions src/ape/read.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use super::constants::APE_PREAMBLE;
use super::header::read_ape_header;
use super::tag::read::read_ape_tag;
use super::tag::ApeTag;
use super::{ApeFile, ApeProperties};
use crate::ape::tag::read::{read_ape_tag, read_ape_tag_with_header};
use crate::error::Result;
use crate::id3::v1::tag::Id3v1Tag;
use crate::id3::v2::read::parse_id3v2;
Expand Down Expand Up @@ -76,7 +75,7 @@ where
let ape_header = read_ape_header(data, false)?;
stream_len -= u64::from(ape_header.size);

let ape = read_ape_tag(data, ape_header)?;
let ape = read_ape_tag_with_header(data, ape_header)?;
ape_tag = Some(ape);
},
_ => {
Expand Down Expand Up @@ -111,15 +110,9 @@ where
// Strongly recommended to be at the end of the file
data.seek(SeekFrom::Current(-32))?;

let mut ape_preamble = [0; 8];
data.read_exact(&mut ape_preamble)?;

if &ape_preamble == APE_PREAMBLE {
let ape_header = read_ape_header(data, true)?;
stream_len -= u64::from(ape_header.size);

let ape = read_ape_tag(data, ape_header)?;
ape_tag = Some(ape);
if let Some((tag, header)) = read_ape_tag(data, true)? {
stream_len -= u64::from(header.size);
ape_tag = Some(tag);
}

let file_length = data.stream_position()?;
Expand Down
41 changes: 17 additions & 24 deletions src/ape/tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ macro_rules! impl_accessor {
/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded.
/// For items, see [`ApeItem::new`].
#[derive(Default, Debug, PartialEq, Eq, Clone)]
#[tag(description = "An `APE` tag", supported_formats(Ape, Mpeg, WavPack))]
#[tag(
description = "An `APE` tag",
supported_formats(Ape, Mpeg, Mpc, WavPack)
)]
pub struct ApeTag {
/// Whether or not to mark the tag as read only
pub read_only: bool,
Expand Down Expand Up @@ -485,11 +488,10 @@ pub(crate) fn tagitems_into_ape<'a>(

#[cfg(test)]
mod tests {
use crate::ape::header::read_ape_header;
use crate::ape::{ApeItem, ApeTag};
use crate::{ItemValue, Tag, TagExt, TagType};

use std::io::{Cursor, Seek, SeekFrom};
use std::io::Cursor;

#[test]
fn parse_ape() {
Expand Down Expand Up @@ -542,11 +544,9 @@ mod tests {
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
let mut reader = Cursor::new(tag);

// Remove the APE preamble
reader.seek(SeekFrom::Current(8)).unwrap();

let header = read_ape_header(&mut reader, false).unwrap();
let parsed_tag = crate::ape::tag::read::read_ape_tag(&mut reader, header).unwrap();
let (parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false)
.unwrap()
.unwrap();

assert_eq!(expected_tag.len(), parsed_tag.len());

Expand All @@ -560,23 +560,18 @@ mod tests {
let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
let mut reader = Cursor::new(tag_bytes);

// Remove the APE preamble
reader.seek(SeekFrom::Current(8)).unwrap();

let header = read_ape_header(&mut reader, false).unwrap();
let parsed_tag = crate::ape::tag::read::read_ape_tag(&mut reader, header).unwrap();
let (parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false)
.unwrap()
.unwrap();

let mut writer = Vec::new();
parsed_tag.dump_to(&mut writer).unwrap();

let mut temp_reader = Cursor::new(writer);

// Remove the APE preamble
temp_reader.seek(SeekFrom::Current(8)).unwrap();

let temp_header = read_ape_header(&mut temp_reader, false).unwrap();
let temp_parsed_tag =
crate::ape::tag::read::read_ape_tag(&mut temp_reader, temp_header).unwrap();
let (temp_parsed_tag, _) = crate::ape::tag::read::read_ape_tag(&mut temp_reader, false)
.unwrap()
.unwrap();

assert_eq!(parsed_tag, temp_parsed_tag);
}
Expand All @@ -586,11 +581,9 @@ mod tests {
let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
let mut reader = Cursor::new(tag_bytes);

// Remove the APE preamble
reader.seek(SeekFrom::Current(8)).unwrap();

let header = read_ape_header(&mut reader, false).unwrap();
let ape = crate::ape::tag::read::read_ape_tag(&mut reader, header).unwrap();
let (ape, _) = crate::ape::tag::read::read_ape_tag(&mut reader, false)
.unwrap()
.unwrap();

let tag: Tag = ape.into();

Expand Down
23 changes: 20 additions & 3 deletions src/ape/tag/read.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::item::ApeItem;
use super::ApeTag;
use crate::ape::constants::INVALID_KEYS;
use crate::ape::header::ApeHeader;
use crate::ape::constants::{APE_PREAMBLE, INVALID_KEYS};
use crate::ape::header::{self, ApeHeader};
use crate::error::Result;
use crate::macros::{decode_err, err, try_vec};
use crate::tag::item::ItemValue;
Expand All @@ -10,7 +10,7 @@ use std::io::{Read, Seek, SeekFrom};

use byteorder::{LittleEndian, ReadBytesExt};

pub(crate) fn read_ape_tag<R>(data: &mut R, header: ApeHeader) -> Result<ApeTag>
pub(crate) fn read_ape_tag_with_header<R>(data: &mut R, header: ApeHeader) -> Result<ApeTag>
where
R: Read + Seek,
{
Expand Down Expand Up @@ -81,3 +81,20 @@ where

Ok(tag)
}

pub(crate) fn read_ape_tag<R: Read + Seek>(
reader: &mut R,
footer: bool,
) -> Result<Option<(ApeTag, ApeHeader)>> {
let mut ape_preamble = [0; 8];
reader.read_exact(&mut ape_preamble)?;

if &ape_preamble == APE_PREAMBLE {
let ape_header = header::read_ape_header(reader, footer)?;

let ape = read_ape_tag_with_header(reader, ape_header)?;
return Ok(Some((ape, ape_header)));
}

Ok(None)
}
54 changes: 21 additions & 33 deletions src/ape/tag/write.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use super::item::ApeItemRef;
use super::read::read_ape_tag;
use super::ApeTagRef;
use crate::ape::constants::APE_PREAMBLE;
use crate::ape::header::read_ape_header;
use crate::ape::tag::read;
use crate::error::Result;
use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2};
use crate::macros::{decode_err, err};
Expand Down Expand Up @@ -41,26 +40,21 @@ where
// If one is found, it'll be removed and rewritten at the bottom, where it should be
let mut header_ape_tag = (false, (0, 0));

if &ape_preamble == APE_PREAMBLE {
let start = data.seek(SeekFrom::Current(-8))?;

data.seek(SeekFrom::Current(8))?;

let header = read_ape_header(data, false)?;
let size = header.size;

let mut existing = read_ape_tag(data, header)?;

// Only keep metadata around that's marked read only
existing.items.retain(|i| i.read_only);

if !existing.items.is_empty() {
read_only = Some(existing)
}

header_ape_tag = (true, (start, start + u64::from(size)))
} else {
data.seek(SeekFrom::Current(-8))?;
let start = data.stream_position()?;
match read::read_ape_tag(data, false)? {
Some((mut existing_tag, header)) => {
// Only keep metadata around that's marked read only
existing_tag.items.retain(|i| i.read_only);

if !existing_tag.items.is_empty() {
read_only = Some(existing_tag)
}

header_ape_tag = (true, (start, start + u64::from(header.size)))
},
None => {
data.seek(SeekFrom::Current(-8))?;
},
}

// Skip over ID3v1 and Lyrics3v2 tags
Expand All @@ -73,23 +67,17 @@ where
// Now search for an APE tag at the end
data.seek(SeekFrom::Current(-32))?;

data.read_exact(&mut ape_preamble)?;

let mut ape_tag_location = None;

// Also check this tag for any read only items
if &ape_preamble == APE_PREAMBLE {
let start = data.stream_position()? as usize + 24;

let header = read_ape_header(data, true)?;
let start = data.stream_position()? as usize + 32;
if let Some((mut existing_tag, header)) = read::read_ape_tag(data, true)? {
let size = header.size;

let mut existing = read_ape_tag(data, header)?;

existing.items.retain(|i| i.read_only);
existing_tag.items.retain(|i| i.read_only);

if !existing.items.is_empty() {
read_only = Some(existing)
if !existing_tag.items.is_empty() {
read_only = Some(existing_tag)
}

// Since the "start" was really at the end of the tag, this sanity check seems necessary
Expand Down
Loading