Something went wrong. Try again.
Reads and writes the Ogg container format in Zig: pages, packets, grouping and chaining, with a page layer that allocates nothing.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The page: the only structure Ogg actually defines.//!//! A physical Ogg bitstream is a sequence of concatenated pages and nothing//! else (RFC 3533, Section 6). Each page belongs to exactly one logical//! bitstream, named by the serial number in its header, and carries a run of//! bytes out of that bitstream together with a table saying where the packet//! boundaries inside it fall. Everything else -- demultiplexing, reassembling//! packets, what a granule position means -- is built out of that one//! structure, and everything in this file is pure: it parses and writes bytes//! and does no I/O and no allocation.//!//! **A page has a bound, and it is the reason most of this library allocates//! nothing.** A segment table holds at most 255 entries of at most 255 bytes,//! so a page is at most `Page.max_size` = 65307 bytes however hostile the//! stream is. A *packet* has no such bound -- pages can go on continuing one//! forever -- which is why reassembling packets is the one job here that needs//! an allocator and a limit.
const std = @import("std");const assert = std.debug.assert;
const crc = @import("crc.zig");
/// The three flags in a page header's second byte (RFC 3533, Section 6,/// field 3). The bit numbering is the format's: `continued` is 0x01.pub const HeaderType = packed struct(u8) { /// This page opens with the tail of a packet that began on an earlier page /// of the same logical bitstream. Unset means the page opens a fresh /// packet. continued: bool = false, /// This is the first page of a logical bitstream -- its "bos" page. By /// convention the first bytes of the packet it carries identify the codec. first: bool = false, /// This is the last page of a logical bitstream -- its "eos" page. It is /// allowed to be a "nil" page carrying no content at all. last: bool = false, /// Bits no version of the format has ever defined. Kept rather than /// masked away so that a page carrying one can be written back unchanged. reserved: u5 = 0,};
/// One page, borrowing the bytes it was parsed out of.////// `lacing` and `body` point into the caller's buffer; nothing here owns/// memory. A `Page` built by hand -- to write one out -- must keep/// `bodySize(lacing) == body.len`, which every function that writes asserts.pub const Page = struct { /// The four bytes every page starts with, which is what lets a decoder /// find page boundaries again after reading corrupted bytes. pub const capture_pattern = "OggS".*;
/// The only version of the format there has ever been (RFC 3533, /// Section 6, field 2). pub const supported_version: u8 = 0;
/// A segment table holds at most this many lacing values, because its /// length is stated in one byte. pub const max_segments = 255;
/// A page header without its segment table. pub const min_header_size = 27;
/// `min_header_size` plus the longest segment table. pub const max_header_size = min_header_size + max_segments;
/// 255 segments of at most 255 bytes each. pub const max_body_size = max_segments * 255;
/// 65307 bytes. RFC 3533 gives this number in Section 6 and it is the /// bound the whole library leans on. pub const max_size = max_header_size + max_body_size;
/// The granule position a page carries when no packet finishes on it /// (RFC 3533, Section 6, field 4). Written as all ones; -1 in two's /// complement, which is why the field is signed here. pub const no_granule: i64 = -1;
/// Where the codec's position counter stood after the last packet that /// finished on this page, in whatever unit the media mapping chose, or /// `no_granule` when no packet finished here. granule_position: i64, /// Which logical bitstream this page belongs to. serial_number: u32, /// Counts pages within the logical bitstream, so that a gap in it is a /// hole in the stream. sequence_number: u32, /// The checksum the page was stored with. `computeChecksum` says what it /// ought to be; `parse` has already compared them. checksum: u32, header_type: HeaderType, /// Always `supported_version` in a page that parsed. version: u8 = supported_version, /// The segment table: one lacing value per segment, in order. lacing: []const u8, /// The segments themselves, concatenated. As long as the sum of `lacing`. body: []const u8,
pub const ParseError = error{ /// The first four bytes were not `capture_pattern`. NotAPage, /// The version byte was not zero. Nothing has ever written anything /// else, so this is corruption rather than a stream from the future. UnsupportedVersion, /// The bytes ran out before the segment table or the body did. Truncated, /// The page is well formed but its checksum is not the checksum of its /// contents, so some of those contents are not what was written. ChecksumMismatch, };
/// Parse one page from the front of `bytes`, checking its checksum. /// /// Bytes after the page are ignored -- in a physical bitstream they are the /// next page -- so `page.size()` is how far to advance to find it. pub fn parse(bytes: []const u8) ParseError!Page { const page = try parseUnchecked(bytes); if (page.computeChecksum() != page.checksum) return error.ChecksumMismatch; return page; }
/// `parse` without the checksum comparison, for a tool that wants to /// describe a page whose checksum is wrong rather than refuse to look at /// it. Anything that acts on the contents should use `parse`. pub fn parseUnchecked(bytes: []const u8) ParseError!Page { if (bytes.len < min_header_size) return error.Truncated; if (!std.mem.eql(u8, bytes[0..4], &capture_pattern)) return error.NotAPage; if (bytes[4] != supported_version) return error.UnsupportedVersion;
const segments = bytes[26]; const header_size = min_header_size + @as(usize, segments); if (bytes.len < header_size) return error.Truncated; const lacing = bytes[min_header_size..header_size];
const body_size = bodySize(lacing); if (bytes.len < header_size + body_size) return error.Truncated;
return .{ .version = bytes[4], .header_type = @bitCast(bytes[5]), .granule_position = std.mem.readInt(i64, bytes[6..14], .little), .serial_number = std.mem.readInt(u32, bytes[14..18], .little), .sequence_number = std.mem.readInt(u32, bytes[18..22], .little), .checksum = std.mem.readInt(u32, bytes[22..26], .little), .lacing = lacing, .body = bytes[header_size..][0..body_size], }; }
/// How many bytes of body a segment table accounts for. pub fn bodySize(lacing: []const u8) usize { var total: usize = 0; for (lacing) |value| total += value; return total; }
/// The page's length in bytes, header and body together. pub fn size(page: Page) usize { return min_header_size + page.lacing.len + page.body.len; }
/// The checksum the page's contents come to, which for a parsed page is /// what `checksum` already holds. pub fn computeChecksum(page: Page) u32 { var header: [max_header_size]u8 = undefined; var c: crc.Crc32 = .init(); c.update(page.writeHeader(&header, 0)); c.update(page.body); return c.final(); }
/// Write the page out, with a checksum computed over what is being /// written. A page that parsed reproduces its input exactly, since its /// stored checksum was already the checksum of its contents. pub fn write(page: Page, w: *std.Io.Writer) std.Io.Writer.Error!void { assert(page.lacing.len <= max_segments); assert(bodySize(page.lacing) == page.body.len); var header: [max_header_size]u8 = undefined; _ = page.writeHeader(&header, page.computeChecksum()); try w.writeAll(header[0 .. min_header_size + page.lacing.len]); try w.writeAll(page.body); }
/// Lay the header out into `buffer`, with `stored_checksum` in the /// checksum field, and return the part used. Passing zero there is how the /// checksum itself is computed. fn writeHeader(page: Page, buffer: *[max_header_size]u8, stored_checksum: u32) []u8 { assert(page.lacing.len <= max_segments); buffer[0..4].* = capture_pattern; buffer[4] = page.version; buffer[5] = @bitCast(page.header_type); std.mem.writeInt(i64, buffer[6..14], page.granule_position, .little); std.mem.writeInt(u32, buffer[14..18], page.serial_number, .little); std.mem.writeInt(u32, buffer[18..22], page.sequence_number, .little); std.mem.writeInt(u32, buffer[22..26], stored_checksum, .little); buffer[26] = @intCast(page.lacing.len); @memcpy(buffer[min_header_size..][0..page.lacing.len], page.lacing); return buffer[0 .. min_header_size + page.lacing.len]; }
/// True when the page's last segment is 255 bytes long, which is the /// format's way of saying the packet it belongs to has not finished: "a /// lacing value of 255 implies that a second lacing value follows" /// (RFC 3533, Section 5). pub fn endsMidPacket(page: Page) bool { return page.lacing.len > 0 and page.lacing[page.lacing.len - 1] == 255; }
/// How many packets finish inside this page -- one per lacing value below /// 255. The page's granule position belongs to the last of them. pub fn completedPackets(page: Page) usize { var count: usize = 0; for (page.lacing) |value| { if (value < 255) count += 1; } return count; }
/// The packet fragments this page carries, in order. The iterator borrows /// the page's slices rather than the page, so it can outlive it and be /// stored. pub fn packets(page: Page) PacketIterator { return .{ .lacing = page.lacing, .body = page.body, .continued = page.header_type.continued, }; }
/// As much of a packet as one page holds. A whole packet when `complete` /// is set and `continued` is not, which is the ordinary case. pub const Fragment = struct { bytes: []const u8, /// This fragment is the tail of a packet that began on an earlier /// page: only the first fragment of a page can have it set, and only /// when the page's `continued` flag is. continued: bool, /// The packet ends here. Unset on the last fragment of a page that /// ends mid-packet, and then only there. complete: bool, /// True of the last fragment of the page, whether or not it completes /// a packet. Worth knowing because the page's granule position and its /// end-of-stream flag both apply to what ends last on it. final: bool, };
pub const PacketIterator = struct { lacing: []const u8, body: []const u8, /// The page's `continued` flag, which applies to the first fragment. continued: bool, segment: usize = 0, offset: usize = 0,
pub fn next(it: *PacketIterator) ?Fragment { if (it.segment == it.lacing.len) return null; const first = it.segment == 0; const start = it.offset; var complete = false; while (it.segment < it.lacing.len) { const value = it.lacing[it.segment]; it.segment += 1; it.offset += value; if (value < 255) { complete = true; break; } } return .{ .bytes = it.body[start..it.offset], .continued = first and it.continued, .complete = complete, .final = it.segment == it.lacing.len, }; } };};
const testing = std.testing;
/// The first page of src/tests/data/tone.oga: the bos page of an Ogg FLAC/// stream, carrying one 51-byte packet.const flac_bos_page = @embedFile("tests/data/tone.oga")[0..79];
test "parsing a page ffmpeg wrote" { const page = try Page.parse(flac_bos_page); try testing.expectEqual(@as(u8, 0), page.version); try testing.expectEqual(HeaderType{ .first = true }, page.header_type); try testing.expectEqual(@as(i64, 0), page.granule_position); try testing.expectEqual(@as(u32, 0x2c45036d), page.serial_number); try testing.expectEqual(@as(u32, 0), page.sequence_number); try testing.expectEqual(@as(u32, 0x05d0cddd), page.checksum); try testing.expectEqualSlices(u8, &.{51}, page.lacing); try testing.expectEqual(@as(usize, 51), page.body.len); try testing.expectEqual(@as(usize, 79), page.size());
// The packet on a bos page identifies the codec: 0x7f then "FLAC", from // the Ogg FLAC mapping. try testing.expectEqualSlices(u8, "\x7fFLAC", page.body[0..5]); try testing.expect(!page.endsMidPacket()); try testing.expectEqual(@as(usize, 1), page.completedPackets());}
test "writing a parsed page reproduces it byte for byte" { const page = try Page.parse(flac_bos_page); var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try page.write(&out.writer); try testing.expectEqualSlices(u8, flac_bos_page, out.written());}
test "a page is rejected rather than half believed" { try testing.expectError(error.Truncated, Page.parse(flac_bos_page[0..26])); try testing.expectError(error.Truncated, Page.parse(flac_bos_page[0..78]));
var broken = flac_bos_page.*; broken[1] = 'b'; try testing.expectError(error.NotAPage, Page.parse(&broken));
broken = flac_bos_page.*; broken[4] = 1; try testing.expectError(error.UnsupportedVersion, Page.parse(&broken));
// One flipped bit anywhere in the body has to be caught. broken = flac_bos_page.*; broken[40] ^= 0x01; try testing.expectError(error.ChecksumMismatch, Page.parse(&broken));
// And so does a header field, which is what a checksum computed over the // header rather than just the body is for. broken = flac_bos_page.*; broken[18] = 7; try testing.expectError(error.ChecksumMismatch, Page.parse(&broken));
// `parseUnchecked` is how a tool reports that rather than refusing. const page = try Page.parseUnchecked(&broken); try testing.expectEqual(@as(u32, 7), page.sequence_number); try testing.expect(page.computeChecksum() != page.checksum);}
/// Build a page around a segment table, for the lacing tests below. The body/// is filled with the index of the byte so that a misplaced boundary shows up/// as wrong content rather than only as a wrong length.fn pageWith(lacing: []const u8, body: []u8, header_type: HeaderType) Page { for (body, 0..) |*byte, i| byte.* = @truncate(i); return .{ .granule_position = Page.no_granule, .serial_number = 1, .sequence_number = 0, .checksum = 0, .header_type = header_type, .lacing = lacing, .body = body[0..Page.bodySize(lacing)], };}
test "lacing: a packet per lacing value below 255" { var body: [60]u8 = undefined; const page = pageWith(&.{ 10, 20, 30 }, &body, .{}); var it = page.packets();
const first = it.next().?; try testing.expectEqual(@as(usize, 10), first.bytes.len); try testing.expect(first.complete and !first.continued and !first.final); try testing.expectEqual(@as(usize, 20), it.next().?.bytes.len); const third = it.next().?; try testing.expectEqual(@as(usize, 30), third.bytes.len); try testing.expect(third.final); try testing.expectEqual(@as(?Page.Fragment, null), it.next());}
test "lacing: a zero is a packet of no bytes, which is not an error" { var body: [1]u8 = undefined; const page = pageWith(&.{0}, &body, .{}); var it = page.packets(); const only = it.next().?; try testing.expectEqual(@as(usize, 0), only.bytes.len); try testing.expect(only.complete); try testing.expectEqual(@as(?Page.Fragment, null), it.next()); try testing.expectEqual(@as(usize, 1), page.completedPackets());}
test "lacing: a packet whose length is a multiple of 255 ends in a zero" { var body: [255]u8 = undefined; const page = pageWith(&.{ 255, 0 }, &body, .{}); var it = page.packets(); const only = it.next().?; try testing.expectEqual(@as(usize, 255), only.bytes.len); try testing.expect(only.complete); try testing.expectEqual(@as(?Page.Fragment, null), it.next()); try testing.expect(!page.endsMidPacket());}
test "lacing: a trailing 255 means the packet runs on to the next page" { var body: [265]u8 = undefined; const page = pageWith(&.{ 10, 255 }, &body, .{}); try testing.expect(page.endsMidPacket()); try testing.expectEqual(@as(usize, 1), page.completedPackets());
var it = page.packets(); try testing.expect(it.next().?.complete); const tail = it.next().?; try testing.expectEqual(@as(usize, 255), tail.bytes.len); try testing.expect(!tail.complete); try testing.expect(tail.final);}
test "lacing: only the first fragment of a continued page is a continuation" { var body: [30]u8 = undefined; const page = pageWith(&.{ 10, 20 }, &body, .{ .continued = true }); var it = page.packets(); try testing.expect(it.next().?.continued); try testing.expect(!it.next().?.continued);}
test "a nil page carries no packets at all" { // Which RFC 3533 Section 4 allows for an end-of-stream page: "pages // containing no content but simply a page header with position information // and the eos flag set". var body: [0]u8 = undefined; const page = pageWith(&.{}, &body, .{ .last = true }); var it = page.packets(); try testing.expectEqual(@as(?Page.Fragment, null), it.next()); try testing.expectEqual(@as(usize, Page.min_header_size), page.size()); try testing.expect(!page.endsMidPacket());}
test "the page bound is the one RFC 3533 states" { try testing.expectEqual(@as(usize, 65307), Page.max_size); try testing.expectEqual(@as(usize, 65025), Page.max_body_size);}