// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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); }