Skip to content

Commit 5dd1017

Browse files
committed
WAV: Constrain writer to stream length not file length
Previously, tags were simply written to the end of the file, but this would break files that have junk data appended. This allows for files with appended junk data that falls outside of the stream length. This can be caused by buggy software misusing padding.
1 parent fa878b6 commit 5dd1017

File tree

10 files changed

+128
-64
lines changed

10 files changed

+128
-64
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
* If a `free` atom claims to be larger than the remainder of the stream, parsing will simply stop. This will now only
1616
be a `SizeMismatch` error in `Strict` mode. Invalid padding is a common issue in all tag formats due to buggy software,
1717
so it's better to work around it by default rather than discard the entire stream as invalid.
18+
* **WAV**:
19+
* When writing tags, the writer will be constrained to the stream size reported by the file, not by the file's actual length ([PR](https://github.com/Serial-ATA/lofty-rs/pull/517))
20+
* Previously, tags were simply written to the end of the file, but this would break files that have junk data appended.
21+
* This allows for files with appended junk data that falls outside of the stream length. This can be caused by buggy software
22+
misusing padding.
1823

1924
## [0.22.3] - 2025-04-04
2025

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ into_iter_without_iter = "allow" # This is only going to fire on some i
7171
struct_excessive_bools = "allow" # I have yet to find one case of this being useful
7272
needless_continue = "allow" # All occurences of this lint are just for clarity in large loops
7373
unbuffered_bytes = "allow" # It is up to the caller to wrap their data in `BufReader`s
74+
struct_field_names = "allow"
7475

7576
[workspace.lints.rustdoc]
7677
broken_intra_doc_links = "deny"

lofty/src/id3/v2/write/chunk_file.rs

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use crate::config::WriteOptions;
22
use crate::error::{LoftyError, Result};
33
use crate::iff::chunk::Chunks;
4+
use crate::macros::err;
45
use crate::util::io::{FileLike, Length, Truncate};
56

6-
use std::io::SeekFrom;
7+
use std::io::{Cursor, Seek, SeekFrom, Write};
78

89
use byteorder::{ByteOrder, WriteBytesExt};
910

1011
const CHUNK_NAME_UPPER: [u8; 4] = [b'I', b'D', b'3', b' '];
1112
const CHUNK_NAME_LOWER: [u8; 4] = [b'i', b'd', b'3', b' '];
13+
const RIFF_CHUNK_HEADER_SIZE: usize = 8;
1214

1315
pub(in crate::id3::v2) fn write_to_chunk_file<F, B>(
1416
file: &mut F,
@@ -21,71 +23,93 @@ where
2123
LoftyError: From<<F as Length>::Error>,
2224
B: ByteOrder,
2325
{
24-
// RIFF....WAVE
25-
file.seek(SeekFrom::Current(12))?;
26+
// Only rely on the actual file for the first chunk read
27+
let file_len = file.len()?;
2628

27-
let file_len = file.len()?.saturating_sub(12);
29+
let mut chunks = Chunks::<B>::new(file_len);
30+
chunks.next(file)?;
2831

29-
let mut id3v2_chunk = (None, None);
32+
let mut actual_stream_size = chunks.size;
3033

31-
let mut chunks = Chunks::<B>::new(file_len);
34+
file.rewind()?;
35+
36+
let mut file_bytes = Cursor::new(Vec::with_capacity(actual_stream_size as usize));
37+
file.read_to_end(file_bytes.get_mut())?;
38+
39+
if file_bytes.get_ref().len() < (actual_stream_size as usize + RIFF_CHUNK_HEADER_SIZE) {
40+
err!(SizeMismatch);
41+
}
42+
43+
// The first chunk format is RIFF....WAVE
44+
file_bytes.seek(SeekFrom::Start(12))?;
3245

33-
while chunks.next(file).is_ok() {
46+
let (mut exising_id3_start, mut existing_id3_size) = (None, None);
47+
48+
let mut chunks = Chunks::<B>::new(u64::from(actual_stream_size));
49+
while let Ok(true) = chunks.next(&mut file_bytes) {
3450
if chunks.fourcc == CHUNK_NAME_UPPER || chunks.fourcc == CHUNK_NAME_LOWER {
35-
id3v2_chunk = (Some(file.stream_position()? - 8), Some(chunks.size));
51+
exising_id3_start = Some(file_bytes.stream_position()? - 8);
52+
existing_id3_size = Some(chunks.size);
3653
break;
3754
}
3855

39-
file.seek(SeekFrom::Current(i64::from(chunks.size)))?;
40-
41-
chunks.correct_position(file)?;
56+
chunks.skip(&mut file_bytes)?;
4257
}
4358

44-
if let (Some(chunk_start), Some(mut chunk_size)) = id3v2_chunk {
45-
file.rewind()?;
46-
59+
if let (Some(exising_id3_start), Some(mut existing_id3_size)) =
60+
(exising_id3_start, existing_id3_size)
61+
{
4762
// We need to remove the padding byte if it exists
48-
if chunk_size % 2 != 0 {
49-
chunk_size += 1;
63+
if existing_id3_size % 2 != 0 {
64+
existing_id3_size += 1;
5065
}
5166

52-
let mut file_bytes = Vec::new();
53-
file.read_to_end(&mut file_bytes)?;
54-
55-
file_bytes.splice(
56-
chunk_start as usize..(chunk_start + u64::from(chunk_size) + 8) as usize,
57-
[],
58-
);
67+
let existing_tag_end =
68+
exising_id3_start as usize + RIFF_CHUNK_HEADER_SIZE + existing_id3_size as usize;
69+
let _ = file_bytes
70+
.get_mut()
71+
.drain(exising_id3_start as usize..existing_tag_end);
5972

60-
file.rewind()?;
61-
file.truncate(0)?;
62-
file.write_all(&file_bytes)?;
73+
actual_stream_size -= existing_id3_size + RIFF_CHUNK_HEADER_SIZE as u32;
6374
}
6475

6576
if !tag.is_empty() {
66-
file.seek(SeekFrom::End(0))?;
67-
77+
let mut tag_bytes = Cursor::new(Vec::new());
6878
if write_options.uppercase_id3v2_chunk {
69-
file.write_all(&CHUNK_NAME_UPPER)?;
79+
tag_bytes.write_all(&CHUNK_NAME_UPPER)?;
7080
} else {
71-
file.write_all(&CHUNK_NAME_LOWER)?;
81+
tag_bytes.write_all(&CHUNK_NAME_LOWER)?;
7282
}
7383

74-
file.write_u32::<B>(tag.len() as u32)?;
75-
file.write_all(tag)?;
84+
tag_bytes.write_u32::<B>(tag.len() as u32)?;
85+
tag_bytes.write_all(tag)?;
7686

7787
// It is required an odd length chunk be padded with a 0
7888
// The 0 isn't included in the chunk size, however
7989
if tag.len() % 2 != 0 {
80-
file.write_u8(0)?;
90+
tag_bytes.write_u8(0)?;
8191
}
8292

83-
let total_size = file.stream_position()? - 8;
93+
let Ok(tag_size): std::result::Result<u32, _> = tag_bytes.get_ref().len().try_into() else {
94+
err!(TooMuchData)
95+
};
8496

85-
file.seek(SeekFrom::Start(4))?;
97+
let tag_position = actual_stream_size as usize + RIFF_CHUNK_HEADER_SIZE;
8698

87-
file.write_u32::<B>(total_size as u32)?;
99+
file_bytes.get_mut().splice(
100+
tag_position..tag_position,
101+
tag_bytes.get_ref().iter().copied(),
102+
);
103+
104+
actual_stream_size += tag_size;
88105
}
89106

107+
file_bytes.seek(SeekFrom::Start(4))?;
108+
file_bytes.write_u32::<B>(actual_stream_size)?;
109+
110+
file.rewind()?;
111+
file.truncate(0)?;
112+
file.write_all(file_bytes.get_ref())?;
113+
90114
Ok(())
91115
}

lofty/src/iff/aiff/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ where
6767

6868
let mut chunks = Chunks::<BigEndian>::new(file_len);
6969

70-
while chunks.next(data).is_ok() {
70+
while let Ok(true) = chunks.next(data) {
7171
match &chunks.fourcc {
7272
b"ID3 " | b"id3 " if parse_options.read_tags => {
7373
let tag = chunks.id3_chunk(data, parse_options)?;

lofty/src/iff/aiff/tag.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ where
436436

437437
let mut chunks = Chunks::<BigEndian>::new(file_len);
438438

439-
while chunks.next(file).is_ok() {
439+
while let Ok(true) = chunks.next(file) {
440440
match &chunks.fourcc {
441441
b"NAME" | b"AUTH" | b"(c) " | b"ANNO" | b"COMT" => {
442442
let start = (file.stream_position()? - 8) as usize;

lofty/src/iff/chunk.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::marker::PhantomData;
99

1010
use byteorder::{ByteOrder, ReadBytesExt};
1111

12+
const RIFF_CHUNK_HEADER_SIZE: u64 = 8;
13+
1214
pub(crate) struct Chunks<B>
1315
where
1416
B: ByteOrder,
@@ -30,16 +32,20 @@ impl<B: ByteOrder> Chunks<B> {
3032
}
3133
}
3234

33-
pub fn next<R>(&mut self, data: &mut R) -> Result<()>
35+
pub fn next<R>(&mut self, data: &mut R) -> Result<bool>
3436
where
3537
R: Read,
3638
{
39+
if self.remaining_size < RIFF_CHUNK_HEADER_SIZE {
40+
return Ok(false);
41+
}
42+
3743
data.read_exact(&mut self.fourcc)?;
3844
self.size = data.read_u32::<B>()?;
3945

4046
self.remaining_size = self.remaining_size.saturating_sub(8);
4147

42-
Ok(())
48+
Ok(true)
4349
}
4450

4551
pub fn read_cstring<R>(&mut self, data: &mut R) -> Result<String>

lofty/src/iff/wav/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! WAV specific items
22
33
mod properties;
4-
mod read;
4+
pub(crate) mod read;
55
pub(crate) mod tag;
66

77
use crate::id3::v2::tag::Id3v2Tag;

lofty/src/iff/wav/read.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use std::io::{Read, Seek, SeekFrom};
1111

1212
use byteorder::{LittleEndian, ReadBytesExt};
1313

14-
pub(super) fn verify_wav<T>(data: &mut T) -> Result<()>
14+
// Verifies that the stream is a WAV file and returns the stream length
15+
pub(crate) fn verify_wav<T>(data: &mut T) -> Result<u32>
1516
where
1617
T: Read + Seek,
1718
{
@@ -27,7 +28,7 @@ where
2728
}
2829

2930
log::debug!("File verified to be WAV");
30-
Ok(())
31+
Ok(u32::from_le_bytes(id[4..8].try_into().unwrap()))
3132
}
3233

3334
pub(super) fn read_from<R>(data: &mut R, parse_options: ParseOptions) -> Result<WavFile>
@@ -50,7 +51,7 @@ where
5051

5152
let mut chunks = Chunks::<LittleEndian>::new(file_len);
5253

53-
while chunks.next(data).is_ok() {
54+
while let Ok(true) = chunks.next(data) {
5455
match &chunks.fourcc {
5556
b"fmt " if parse_options.read_properties => {
5657
if fmt.is_empty() {

lofty/src/iff/wav/tag/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub(in crate::iff::wav) fn parse_riff_info<R>(
1919
where
2020
R: Read + Seek,
2121
{
22-
while data.stream_position()? != end && chunks.next(data).is_ok() {
22+
while data.stream_position()? != end && matches!(chunks.next(data), Ok(true)) {
2323
let key_str = utf8_decode_str(&chunks.fourcc)
2424
.map_err(|_| decode_err!(Wav, "Invalid item key found in RIFF INFO"))?;
2525

lofty/src/iff/wav/tag/write.rs

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ use crate::iff::wav::read::verify_wav;
66
use crate::macros::err;
77
use crate::util::io::{FileLike, Length, Truncate};
88

9-
use std::io::{Read, Seek, SeekFrom};
9+
use std::io::{Cursor, Read, Seek, SeekFrom};
1010

1111
use byteorder::{LittleEndian, WriteBytesExt};
1212

13+
const RIFF_CHUNK_HEADER_SIZE: usize = 8;
14+
1315
pub(in crate::iff::wav) fn write_riff_info<'a, F, I>(
1416
file: &mut F,
1517
tag: &mut RIFFInfoListRef<'a, I>,
@@ -21,44 +23,69 @@ where
2123
LoftyError: From<<F as Length>::Error>,
2224
I: Iterator<Item = (&'a str, &'a str)>,
2325
{
24-
verify_wav(file)?;
25-
let file_len = file.len()?.saturating_sub(12);
26+
let mut stream_length = verify_wav(file)?;
2627

2728
let mut riff_info_bytes = Vec::new();
2829
create_riff_info(&mut tag.items, &mut riff_info_bytes)?;
2930

30-
let Some(info_list_size) = find_info_list(file, file_len)? else {
31+
file.rewind()?;
32+
33+
let mut file_bytes = Cursor::new(Vec::new());
34+
file.read_to_end(file_bytes.get_mut())?;
35+
36+
if file_bytes.get_ref().len() < (stream_length as usize + RIFF_CHUNK_HEADER_SIZE) {
37+
err!(SizeMismatch);
38+
}
39+
40+
// The first chunk format is RIFF....WAVE
41+
file_bytes.seek(SeekFrom::Start(12))?;
42+
43+
let Some(info_list_size) = find_info_list(&mut file_bytes, u64::from(stream_length - 4))?
44+
else {
3145
// Simply append the info list to the end of the file and update the file size
32-
file.seek(SeekFrom::End(0))?;
46+
file_bytes.rewind()?;
47+
48+
let tag_position = stream_length as usize + RIFF_CHUNK_HEADER_SIZE;
49+
50+
file_bytes.seek(SeekFrom::Start(tag_position as u64))?;
3351

34-
file.write_all(&riff_info_bytes)?;
52+
file_bytes
53+
.get_mut()
54+
.splice(tag_position..tag_position, riff_info_bytes.iter().copied());
3555

36-
let len = (file.stream_position()? - 8) as u32;
56+
let len = (riff_info_bytes.len() + tag_position - 8) as u32;
3757

38-
file.seek(SeekFrom::Start(4))?;
39-
file.write_u32::<LittleEndian>(len)?;
58+
file_bytes.seek(SeekFrom::Start(4))?;
59+
file_bytes.write_u32::<LittleEndian>(len)?;
60+
61+
file.rewind()?;
62+
file.truncate(0)?;
63+
file.write_all(file_bytes.get_ref())?;
4064

4165
return Ok(());
4266
};
4367

4468
// Replace the existing tag
4569

46-
let info_list_start = file.seek(SeekFrom::Current(-12))? as usize;
47-
let info_list_end = info_list_start + 8 + info_list_size as usize;
70+
let info_list_start = file_bytes.seek(SeekFrom::Current(-12))? as usize;
71+
let info_list_end = info_list_start + RIFF_CHUNK_HEADER_SIZE + info_list_size as usize;
4872

49-
file.rewind()?;
73+
stream_length -= info_list_end as u32 - info_list_start as u32;
5074

51-
let mut file_bytes = Vec::new();
52-
file.read_to_end(&mut file_bytes)?;
75+
let new_tag_len = riff_info_bytes.len() as u32;
76+
let _ = file_bytes
77+
.get_mut()
78+
.splice(info_list_start..info_list_end, riff_info_bytes);
5379

54-
let _ = file_bytes.splice(info_list_start..info_list_end, riff_info_bytes);
80+
stream_length += new_tag_len;
5581

56-
let total_size = (file_bytes.len() - 8) as u32;
57-
let _ = file_bytes.splice(4..8, total_size.to_le_bytes());
82+
let _ = file_bytes
83+
.get_mut()
84+
.splice(4..8, stream_length.to_le_bytes());
5885

5986
file.rewind()?;
6087
file.truncate(0)?;
61-
file.write_all(&file_bytes)?;
88+
file.write_all(file_bytes.get_ref())?;
6289

6390
Ok(())
6491
}
@@ -71,7 +98,7 @@ where
7198

7299
let mut chunks = Chunks::<LittleEndian>::new(file_size);
73100

74-
while chunks.next(data).is_ok() {
101+
while let Ok(true) = chunks.next(data) {
75102
if &chunks.fourcc == b"LIST" {
76103
let mut list_type = [0; 4];
77104
data.read_exact(&mut list_type)?;

0 commit comments

Comments
 (0)