// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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 ... What logical bitstreams a file holds \\ zig-ogg pages One line per page \\ zig-ogg packets One line per packet \\ zig-ogg check ... 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; _ = ✓ }