From 7b70f4b43a19ff54aacab7aaab854f05f2339843 Mon Sep 17 00:00:00 2001 From: "Benoit Brummer (Trougnouf)" Date: Wed, 23 Sep 2026 10:00:27 +0200 Subject: [PATCH 1/3] Ogg: Don't drop audio when the setup header ends mid-page ( https://github.com/Serial-ATA/lofty-rs/issues/729 ) --- lofty/src/ogg/tag/write.rs | 189 +++++++++++++++++++++++++++++++------ 1 file changed, 160 insertions(+), 29 deletions(-) diff --git a/lofty/src/ogg/tag/write.rs b/lofty/src/ogg/tag/write.rs index c9ef0fad1..c49326cb2 100644 --- a/lofty/src/ogg/tag/write.rs +++ b/lofty/src/ogg/tag/write.rs @@ -15,7 +15,7 @@ use std::borrow::Cow; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use ogg_pager::{CONTAINS_FIRST_PAGE_OF_BITSTREAM, Packets, Page, PageHeader}; +use ogg_pager::{CONTAINS_FIRST_PAGE_OF_BITSTREAM, crc32, paginate}; pub(crate) fn write_to( file: VerifiedFile<'_, F>, @@ -36,6 +36,55 @@ where write(file, &mut comments_ref, write_options) } +// A raw OGG page: the 27-byte header, the segment table, and the body +struct RawPage { + header: [u8; 27], + segments: Vec, + body: Vec, +} + +impl RawPage { + // Parses a single page from the front of `data`, advancing it past the page + fn parse(data: &mut &[u8]) -> Result { + if data.len() < 27 { + return Err(FileParseError::message(None, "truncated page header")); + } + if &data[..4] != b"OggS" { + return Err(FileParseError::message(None, "missing page magic")); + } + + let nsegs = data[26] as usize; + if nsegs == 0 || data.len() < 27 + nsegs { + return Err(FileParseError::message(None, "invalid segment table")); + } + + let segments = data[27..27 + nsegs].to_vec(); + let body_len: usize = segments.iter().map(|&b| b as usize).sum(); + let total = 27 + nsegs + body_len; + if total > data.len() { + return Err(FileParseError::message(None, "truncated page body")); + } + + let header = data[..27].try_into().unwrap(); + let body = data[27 + nsegs..total].to_vec(); + *data = &data[total..]; + + Ok(Self { + header, + segments, + body, + }) + } + + fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(27 + self.segments.len() + self.body.len()); + bytes.extend_from_slice(&self.header); + bytes.extend_from_slice(&self.segments); + bytes.extend_from_slice(&self.body); + bytes + } +} + pub(in crate::ogg) fn write<'a, F, II, IP>( file: VerifiedFile<'_, F>, tag: &mut VorbisCommentsRef<'a, II, IP>, @@ -57,23 +106,63 @@ where let mut file = file.into_inner(); - // Read the first page header to get the stream serial number + // Read the whole file and work at page granularity. + // + // NOTE: The header packets must not be read at packet granularity (e.g. via + // `Packets::read_count`): if the last header packet ends mid-page, the + // reader is left in the middle of a page, the remaining content no longer + // starts on a page boundary, and the audio pages can no longer be parsed. let start = file.stream_position()?; - let first_page_header = PageHeader::read(&mut file).map_err(FileParseError::from)?; + file.seek(SeekFrom::Start(start))?; + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content)?; - let stream_serial = first_page_header.stream_serial; + let mut pages = Vec::new(); + let mut rest = &file_content[..]; + while !rest.is_empty() { + pages.push(RawPage::parse(&mut rest)?); + } - file.seek(SeekFrom::Start(start))?; + if pages.is_empty() { + return Err(FileParseError::message(None, "no pages found").into()); + } - let mut packets = - Packets::read_count(&mut file, header_packet_count).map_err(FileParseError::from)?; + let stream_serial = u32::from_le_bytes(pages[0].header[14..18].try_into().unwrap()); + + // Reassemble the first `header_packet_count` packets from the pages. + // + // `last_header_page` is the page on which the last header packet ends and + // `audio_start_seg` is the index of the first segment on that page which + // belongs to audio data, i.e. the last header packet ended mid-page. + let mut header_packets: Vec> = Vec::with_capacity(header_packet_count as usize); + let mut current_packet: Vec = Vec::new(); + let mut last_header_page = 0usize; + let mut audio_start_seg = 0usize; + + 'pages: for (page_idx, page) in pages.iter().enumerate() { + let mut body_off = 0usize; + for (seg_idx, &lacing) in page.segments.iter().enumerate() { + let n = lacing as usize; + current_packet.extend_from_slice(&page.body[body_off..body_off + n]); + body_off += n; + + if lacing < 255 { + header_packets.push(std::mem::take(&mut current_packet)); + + if header_packets.len() == header_packet_count as usize { + last_header_page = page_idx; + audio_start_seg = seg_idx + 1; + break 'pages; + } + } + } + } - let mut remaining_file_content = Vec::new(); - file.read_to_end(&mut remaining_file_content)?; + if header_packets.len() != header_packet_count as usize { + return Err(FileParseError::message(None, "missing header packets").into()); + } - let comment_packet = packets - .get(1) - .ok_or_else(|| FileParseError::message(None, "missing comment packet"))?; + let comment_packet = &header_packets[1]; if let Some(comment_signature) = comment_signature { verify_signature(comment_packet, comment_signature)?; @@ -105,30 +194,72 @@ where .map_err(TagEncodingError::from)?; // Replace the old comment packet - packets.set(1, new_metadata_packet); + header_packets[1] = new_metadata_packet; + + // Re-paginate the header packets into fresh pages + let header_pages = paginate( + header_packets.iter().map(Vec::as_slice), + stream_serial, + 0, + CONTAINS_FIRST_PAGE_OF_BITSTREAM, + ) + .map_err(|e| FileEncodingError::new(format, e.into()))?; + + // The audio pages: the rest of the page on which the last header packet + // ended (if it ended mid-page), followed by all subsequent pages. + let mut audio_pages: Vec> = Vec::new(); + + { + let page = &pages[last_header_page]; + if audio_start_seg < page.segments.len() { + // The last header packet ended mid-page: re-emit the remainder of + // the page as the first audio page, with the header packets' + // segments removed. + let segments = &page.segments[audio_start_seg..]; + let body_len: usize = segments.iter().map(|&b| b as usize).sum(); + let body = &page.body[page.body.len() - body_len..]; + + let mut raw = Vec::with_capacity(27 + segments.len() + body_len); + raw.extend_from_slice(&page.header); + raw.extend_from_slice(segments); + raw.extend_from_slice(body); + + // The page now has fewer segments than the original + raw[26] = segments.len() as u8; + + // The first packet on this page is now a fresh audio packet, so + // the page neither continues a packet from the previous page nor + // contains the first page of the bitstream. + raw[5] &= !0x03; + + audio_pages.push(raw); + } + } + + for page in &pages[last_header_page + 1..] { + audio_pages.push(page.to_bytes()); + } + + // Renumber the audio pages and fix up their checksums + for (idx, raw) in audio_pages.iter_mut().enumerate() { + let seq = (header_pages.len() + idx) as u32; + raw[18..22].copy_from_slice(&seq.to_le_bytes()); + + raw[22..26].fill(0); + let checksum = crc32(raw); + raw[22..26].copy_from_slice(&checksum.to_le_bytes()); + } file.rewind()?; file.truncate(0)?; - let pages_written = packets - .write_to( - &mut file, - stream_serial, - 0, - CONTAINS_FIRST_PAGE_OF_BITSTREAM, - ) - .map_err(|e| FileEncodingError::new(format, e.into()))? as u32; - - // Correct all remaining page sequence numbers - let mut pages_reader = Cursor::new(&remaining_file_content[..]); - let mut idx = 0; - while let Ok(mut page) = Page::read(&mut pages_reader) { - let header = page.header_mut(); - header.sequence_number = pages_written + idx; + for mut page in header_pages { page.gen_crc(); file.write_all(&page.as_bytes())?; + } - idx += 1; + for raw in &audio_pages { + file.write_all(raw)?; } Ok(()) From 3da5434166f40b5eeb807eb3145214ef885d65cd Mon Sep 17 00:00:00 2001 From: "Benoit Brummer (Trougnouf)" Date: Thu, 24 Sep 2026 10:44:34 +0200 Subject: [PATCH 2/3] Ogg: Add regression test for mid-page setup header (#729) --- lofty/tests/files/assets/issue_729.ogg | Bin 0 -> 8403 bytes lofty/tests/files/ogg.rs | 59 +++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 lofty/tests/files/assets/issue_729.ogg diff --git a/lofty/tests/files/assets/issue_729.ogg b/lofty/tests/files/assets/issue_729.ogg new file mode 100644 index 0000000000000000000000000000000000000000..91d15beb6a87d1cbe0945524aa3c0f715cd8fa04 GIT binary patch literal 8403 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 J>mOJ${s(FT=F0#8 literal 0 HcmV?d00001 diff --git a/lofty/tests/files/ogg.rs b/lofty/tests/files/ogg.rs index b82415d69..38ce55111 100644 --- a/lofty/tests/files/ogg.rs +++ b/lofty/tests/files/ogg.rs @@ -205,6 +205,65 @@ fn opus_issue_499_vinyl_track_number() { assert_eq!(comments.get("TRACKNUMBER"), Some("a5")); } +// Writing a tag used to drop every audio page when the setup header ends +// mid-page () +#[test_log::test] +fn vorbis_issue_729() { + let data = std::fs::read("tests/files/assets/issue_729.ogg").unwrap(); + let pages_before = count_ogg_pages(&data); + assert!( + pages_before > 3, + "fixture should contain audio pages, found {pages_before}" + ); + + let mut file = std::io::Cursor::new(data); + let mut tagged_file = Probe::new(&mut file) + .options(ParseOptions::new().read_properties(false)) + .guess_file_type() + .unwrap() + .read_bound() + .unwrap(); + + let tag = tagged_file.tag_mut(TagType::VorbisComments).unwrap(); + tag.insert_unchecked(lofty::tag::TagItem::new( + ItemKey::TrackArtist, + lofty::tag::ItemValue::Text(String::from("Issue 729 artist")), + )); + tagged_file.save(WriteOptions::default()).unwrap(); + drop(tagged_file); + + let data = file.into_inner(); + let pages_after = count_ogg_pages(&data); + + assert!( + pages_after >= pages_before, + "audio pages were dropped: {pages_before} before, {pages_after} after" + ); +} + +// Counts the OGG pages in `data` by walking the segment tables +fn count_ogg_pages(data: &[u8]) -> usize { + let mut count = 0; + let mut rest = data; + while !rest.is_empty() { + if rest.len() < 27 || &rest[..4] != b"OggS" { + break; + } + let nsegs = rest[26] as usize; + if nsegs == 0 || rest.len() < 27 + nsegs { + break; + } + let body_len: usize = rest[27..27 + nsegs].iter().map(|&b| b as usize).sum(); + let total = 27 + nsegs + body_len; + if total > rest.len() { + break; + } + count += 1; + rest = &rest[total..]; + } + count +} + #[test_log::test] fn flac_remove_id3v2() { crate::util::remove_tag_test("tests/files/assets/flac_with_id3v2.flac", TagType::Id3v2); From 8068aac1e0a34daeccc0ae6078b7e1489ff3de49 Mon Sep 17 00:00:00 2001 From: "Benoit Brummer (Trougnouf)" Date: Thu, 24 Sep 2026 10:44:34 +0200 Subject: [PATCH 3/3] Ogg: Stream audio pages to reduce peak memory (#729) --- lofty/src/ogg/tag/write.rs | 57 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/lofty/src/ogg/tag/write.rs b/lofty/src/ogg/tag/write.rs index c49326cb2..82f93d9dd 100644 --- a/lofty/src/ogg/tag/write.rs +++ b/lofty/src/ogg/tag/write.rs @@ -85,6 +85,14 @@ impl RawPage { } } +// Renumber a page and recompute its checksum +fn fixup_page(raw: &mut [u8], seq: u32) { + raw[18..22].copy_from_slice(&seq.to_le_bytes()); + raw[22..26].fill(0); + let checksum = crc32(raw); + raw[22..26].copy_from_slice(&checksum.to_le_bytes()); +} + pub(in crate::ogg) fn write<'a, F, II, IP>( file: VerifiedFile<'_, F>, tag: &mut VorbisCommentsRef<'a, II, IP>, @@ -112,8 +120,6 @@ where // `Packets::read_count`): if the last header packet ends mid-page, the // reader is left in the middle of a page, the remaining content no longer // starts on a page boundary, and the audio pages can no longer be parsed. - let start = file.stream_position()?; - file.seek(SeekFrom::Start(start))?; let mut file_content = Vec::new(); file.read_to_end(&mut file_content)?; @@ -122,6 +128,7 @@ where while !rest.is_empty() { pages.push(RawPage::parse(&mut rest)?); } + drop(file_content); if pages.is_empty() { return Err(FileParseError::message(None, "no pages found").into()); @@ -205,9 +212,18 @@ where ) .map_err(|e| FileEncodingError::new(format, e.into()))?; - // The audio pages: the rest of the page on which the last header packet - // ended (if it ended mid-page), followed by all subsequent pages. - let mut audio_pages: Vec> = Vec::new(); + // The audio pages are written as they are renumbered and re-checksummed: + // first the rest of the page on which the last header packet ended (if it + // ended mid-page), then all subsequent pages. + let mut next_seq = header_pages.len() as u32; + + file.rewind()?; + file.truncate(0)?; + + for mut page in header_pages { + page.gen_crc(); + file.write_all(&page.as_bytes())?; + } { let page = &pages[last_header_page]; @@ -232,34 +248,17 @@ where // contains the first page of the bitstream. raw[5] &= !0x03; - audio_pages.push(raw); + fixup_page(&mut raw, next_seq); + file.write_all(&raw)?; + next_seq += 1; } } for page in &pages[last_header_page + 1..] { - audio_pages.push(page.to_bytes()); - } - - // Renumber the audio pages and fix up their checksums - for (idx, raw) in audio_pages.iter_mut().enumerate() { - let seq = (header_pages.len() + idx) as u32; - raw[18..22].copy_from_slice(&seq.to_le_bytes()); - - raw[22..26].fill(0); - let checksum = crc32(raw); - raw[22..26].copy_from_slice(&checksum.to_le_bytes()); - } - - file.rewind()?; - file.truncate(0)?; - - for mut page in header_pages { - page.gen_crc(); - file.write_all(&page.as_bytes())?; - } - - for raw in &audio_pages { - file.write_all(raw)?; + let mut raw = page.to_bytes(); + fixup_page(&mut raw, next_seq); + file.write_all(&raw)?; + next_seq += 1; } Ok(())