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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! A command-line front end to the library: say what is in an Ogg file, list//! its pages or its packets, or check that its framing is intact.//!//! It is the only thing here that opens a file. The library takes a//! `std.Io.Reader` and knows nothing about where the bytes came from, which is//! what lets it read a socket as readily as a file -- so all of that lives in//! this one place.
const std = @import("std");const Io = std.Io;
const build = @import("build.zig.zon");const ogg = @import("ogg");
const usage = \\zig-ogg -- read the Ogg container format \\ \\Usage: \\ zig-ogg info <file.ogg>... What logical bitstreams a file holds \\ zig-ogg pages <file.ogg> One line per page \\ zig-ogg packets <file.ogg> One line per packet \\ zig-ogg check <file.ogg>... Verify every page's checksum and order \\ \\Options: \\ -h, --help This. \\ --version Print the version and exit. \\;
pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io;
var stderr_buffer: [4096]u8 = undefined; var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer); const stderr = &stderr_writer.interface; defer stderr.flush() catch {};
var stdout_buffer: [4096]u8 = undefined; var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; defer stdout.flush() catch {};
var args = init.minimal.args.iterate(); defer args.deinit(); _ = args.skip();
const command = args.next() orelse { try stderr.writeAll(usage); try stderr.flush(); std.process.exit(2); };
if (std.mem.eql(u8, command, "-h") or std.mem.eql(u8, command, "--help")) { try stdout.writeAll(usage); return; } if (std.mem.eql(u8, command, "--version")) { try stdout.print("zig-ogg {s}\n", .{build.version}); return; }
const Command = enum { info, pages, packets, check }; const which: Command = if (std.mem.eql(u8, command, "info")) .info else if (std.mem.eql(u8, command, "pages")) .pages else if (std.mem.eql(u8, command, "packets")) .packets else if (std.mem.eql(u8, command, "check")) .check else return fatal(stderr, "'{s}' is not a command; try --help", .{command});
var any = false; var trouble = false; while (args.next()) |path| { any = true; if (which != .info and which != .check and trouble) break; const ok = switch (which) { .info => try info(gpa, io, stdout, stderr, path), .pages => try listPages(gpa, io, stdout, stderr, path), .packets => try listPackets(gpa, io, stdout, stderr, path), .check => try check(gpa, io, stdout, stderr, path), }; if (!ok) trouble = true; } if (!any) return fatal(stderr, "{s} wants a file to read", .{command}); try stdout.flush(); if (trouble) std.process.exit(1);}
fn fatal(stderr: *Io.Writer, comptime fmt: []const u8, args: anytype) noreturn { stderr.print("zig-ogg: " ++ fmt ++ "\n", args) catch {}; stderr.flush() catch {}; std.process.exit(1);}
/// A file open for reading, with the two buffers the library wants around it.const Input = struct { file: Io.File, io: std.Io, gpa: std.mem.Allocator, file_reader: *Io.File.Reader, read_buffer: []u8, page_buffer: *[ogg.Page.max_size]u8,
fn open(gpa: std.mem.Allocator, io: std.Io, path: []const u8) !Input { const file = try Io.Dir.cwd().openFile(io, path, .{}); errdefer file.close(io); const read_buffer = try gpa.alloc(u8, 64 * 1024); errdefer gpa.free(read_buffer); const page_buffer = try gpa.create([ogg.Page.max_size]u8); errdefer gpa.destroy(page_buffer); const file_reader = try gpa.create(Io.File.Reader); file_reader.* = .init(file, io, read_buffer); return .{ .file = file, .io = io, .gpa = gpa, .file_reader = file_reader, .read_buffer = read_buffer, .page_buffer = page_buffer, }; }
fn close(in: *Input) void { in.gpa.destroy(in.file_reader); in.gpa.destroy(in.page_buffer); in.gpa.free(in.read_buffer); in.file.close(in.io); }
fn stream(in: *Input) *Io.Reader { return &in.file_reader.interface; }};
/// What `info` accumulates about one logical bitstream.const StreamSummary = struct { serial_number: u32, codec: ?ogg.Codec = null, packets: u64 = 0, bytes: u64 = 0, last_granule: i64 = ogg.Page.no_granule, ended: bool = false, holes: u64 = 0,};
fn info(gpa: std.mem.Allocator, io: std.Io, out: *Io.Writer, err: *Io.Writer, path: []const u8) !bool { var in = Input.open(gpa, io, path) catch |e| { try err.print("zig-ogg: {s}: {t}\n", .{ path, e }); return false; }; defer in.close();
var summaries: std.ArrayList(StreamSummary) = .empty; defer summaries.deinit(gpa);
var d: ogg.Demuxer = .init(gpa, in.stream(), in.page_buffer, .{}); defer d.deinit();
var complaint: ?anyerror = null; while (d.next() catch |e| blk: { complaint = e; break :blk null; }) |packet| { const summary = for (summaries.items) |*s| { if (s.serial_number == packet.serial_number) break s; } else blk: { try summaries.append(gpa, .{ .serial_number = packet.serial_number }); break :blk &summaries.items[summaries.items.len - 1]; }; if (packet.first) summary.codec = ogg.codec.identify(packet.data); summary.packets += 1; summary.bytes += packet.data.len; if (packet.granule_position) |granule| summary.last_granule = granule; if (packet.last) summary.ended = true; if (packet.after_hole) summary.holes += 1; }
try out.print("{s}: {d} pages, {d} logical bitstream{s}\n", .{ path, d.pages.pages, summaries.items.len, if (summaries.items.len == 1) "" else "s", }); for (summaries.items) |summary| { try out.print(" serial 0x{x:0>8} {s: <18} {d} packets, {d} bytes", .{ summary.serial_number, if (summary.codec) |c| c.description() else "unrecognised", summary.packets, summary.bytes, }); if (summary.last_granule != ogg.Page.no_granule) { try out.print(", ends at granule {d}", .{summary.last_granule}); } if (!summary.ended) try out.writeAll(", unterminated"); if (summary.holes != 0) try out.print(", {d} holes", .{summary.holes}); try out.writeAll("\n"); } if (d.lost_pages != 0) try out.print(" {d} pages missing from the sequence\n", .{d.lost_pages}); if (complaint) |e| { try err.print("zig-ogg: {s}: {t}\n", .{ path, e }); return false; } return true;}
fn listPages(gpa: std.mem.Allocator, io: std.Io, out: *Io.Writer, err: *Io.Writer, path: []const u8) !bool { var in = Input.open(gpa, io, path) catch |e| { try err.print("zig-ogg: {s}: {t}\n", .{ path, e }); return false; }; defer in.close();
var pr: ogg.PageReader = .init(in.stream(), in.page_buffer); var offset: u64 = 0; while (pr.next() catch |e| { try err.print("zig-ogg: {s}: at byte {d}: {t}\n", .{ path, offset, e }); return false; }) |page| { try out.print("{d: >10} serial 0x{x:0>8} page {d: <6} {s}{s}{s} {d: >3} segments, {d: >5} bytes, granule ", .{ offset, page.serial_number, page.sequence_number, if (page.header_type.continued) "c" else "-", if (page.header_type.first) "b" else "-", if (page.header_type.last) "e" else "-", page.lacing.len, page.body.len, }); if (page.granule_position == ogg.Page.no_granule) { try out.writeAll("none\n"); } else { try out.print("{d}\n", .{page.granule_position}); } offset += page.size(); } return true;}
fn listPackets(gpa: std.mem.Allocator, io: std.Io, out: *Io.Writer, err: *Io.Writer, path: []const u8) !bool { var in = Input.open(gpa, io, path) catch |e| { try err.print("zig-ogg: {s}: {t}\n", .{ path, e }); return false; }; defer in.close();
var d: ogg.Demuxer = .init(gpa, in.stream(), in.page_buffer, .{}); defer d.deinit();
while (d.next() catch |e| { try err.print("zig-ogg: {s}: {t}\n", .{ path, e }); return false; }) |packet| { try out.print("serial 0x{x:0>8} packet {d: <6} {d: >7} bytes {s}{s}{s} granule ", .{ packet.serial_number, packet.index, packet.data.len, if (packet.first) "b" else "-", if (packet.last) "e" else "-", if (packet.after_hole) "!" else "-", }); if (packet.granule_position) |granule| { try out.print("{d}", .{granule}); } else { try out.writeAll("none"); } if (packet.first) { if (ogg.codec.identify(packet.data)) |c| try out.print(" {s}", .{c.description()}); } try out.writeAll("\n"); } return true;}
/// Read every page, checking its checksum and its place in its stream's/// sequence, and say what was wrong. This is the framing layer only: nothing/// here looks inside a packet.fn check(gpa: std.mem.Allocator, io: std.Io, out: *Io.Writer, err: *Io.Writer, path: []const u8) !bool { var in = Input.open(gpa, io, path) catch |e| { try err.print("zig-ogg: {s}: {t}\n", .{ path, e }); return false; }; defer in.close();
var d: ogg.Demuxer = .init(gpa, in.stream(), in.page_buffer, .{}); defer d.deinit();
var packets: u64 = 0; var holes: u64 = 0; while (d.next() catch |e| { try err.print("{s}: FAILED after {d} packets: {t}\n", .{ path, packets, e }); return false; }) |packet| { packets += 1; if (packet.after_hole) holes += 1; } if (holes != 0 or d.lost_pages != 0) { try err.print("{s}: FAILED: {d} pages missing, {d} packets after a hole\n", .{ path, d.lost_pages, holes, }); return false; } try out.print("{s}: OK, {d} pages, {d} packets\n", .{ path, d.pages.pages, packets }); return true;}
test "the golden streams survive being described" { // The command line tool is the only thing here that opens a file, so what // is worth testing about it is that the paths through it compile and run; // everything they report on is checked against the library's own tests. _ = &main; _ = &info; _ = &listPages; _ = &listPackets; _ = ✓}