Something went wrong. Try again.
A sans-I/O reader and writer for Matroska and WebM in Zig.
Something went wrong. Try again.
6.5 kB · 153 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Undoing a track's `ContentEncodings` (RFC 9559 section 5.1.4.1.31).//!//! Two of them come up in practice. **Header stripping** removes octets//! every frame starts with — an MPEG audio sync word, say — and stores them//! once in the track; undoing it is putting them back, and a `Demuxer`//! reports them as `Frame.prefix` rather than copying every frame to do so.//! **zlib** compresses each frame, and undoing it takes memory, which is why//! it is here as a function the caller chooses to call rather than//! something the demuxer does behind its back.//!//! bzlib and LZO are in the specification and essentially absent from real//! files; they are reported as `error.UnsupportedEncoding`. So is//! encryption: what a file says about its keys is passed through in//! `tracks.ContentEncryption`, and decrypting is the caller's business.
const std = @import("std");const Allocator = std.mem.Allocator;const flate = std.compress.flate;const tracks = @import("tracks.zig");const ContentEncoding = tracks.ContentEncoding;const ContentCompression = tracks.ContentCompression;
pub const Error = Allocator.Error || error{ /// bzlib, LZO, encryption, or an algorithm the specification does not /// define. UnsupportedEncoding, /// zlib data that does not decompress, or that decompresses to more than /// the caller's limit. BadCompressedData,};
/// What it takes to turn a stored frame back into the frame the codec/// expects.pub const Plan = struct { /// Octets to put in front of the stored frame. prefix: []const u8 = "", /// Some other encoding applies: call `decode`. needs_decode: bool = false,};
/// Works out what reading a frame (`scope` = `ContentEncoding.scope_block`)/// or the codec private data (`scope_private`) of a track takes.pub fn plan(encodings: []const ContentEncoding, scope: u64) Plan { var p: Plan = .{}; var count: usize = 0; for (encodings) |e| { if (e.scope & scope == 0) continue; count += 1; if (e.compressionAlgo() == ContentCompression.header_stripping) { p.prefix = if (e.compression) |c| c.settings orelse "" else ""; } else { p.needs_decode = true; } } // A prefix is only enough on its own; with anything else in the chain // the order matters and `decode` has to do it. if (p.needs_decode and count > 1) p.prefix = ""; return p;}
/// Undoes every encoding of `scope` on `data`, in the order the/// specification says to — highest `ContentEncodingOrder` first, which is/// how `tracks.parseTrack` sorts them. The result is allocated with `gpa`/// and is never larger than `max_len`.pub fn decode( gpa: Allocator, encodings: []const ContentEncoding, scope: u64, data: []const u8, max_len: usize,) Error![]u8 { var current = try gpa.dupe(u8, data); errdefer gpa.free(current); for (encodings) |e| { if (e.scope & scope == 0) continue; const algo = e.compressionAlgo() orelse return error.UnsupportedEncoding; const next = switch (algo) { ContentCompression.header_stripping => blk: { const prefix = if (e.compression) |c| c.settings orelse "" else ""; if (prefix.len + current.len > max_len) return error.BadCompressedData; const out = try gpa.alloc(u8, prefix.len + current.len); @memcpy(out[0..prefix.len], prefix); @memcpy(out[prefix.len..], current); break :blk out; }, ContentCompression.zlib => try inflate(gpa, current, max_len), else => return error.UnsupportedEncoding, }; gpa.free(current); current = next; } return current;}
/// Decompresses one zlib stream.pub fn inflate(gpa: Allocator, data: []const u8, max_len: usize) Error![]u8 { const window = try gpa.alloc(u8, flate.max_window_len); defer gpa.free(window); var in: std.Io.Reader = .fixed(data); var d: flate.Decompress = .init(&in, .zlib, window); return d.reader.allocRemaining(gpa, .limited(max_len)) catch |err| switch (err) { error.OutOfMemory => error.OutOfMemory, else => error.BadCompressedData, };}
/// Compresses `data` as one zlib stream, for a muxer writing a track with/// zlib content encoding.pub fn deflate(gpa: Allocator, data: []const u8) Allocator.Error![]u8 { // The compressor wants room for a few octets in its output's buffer. var out: std.Io.Writer.Allocating = try .initCapacity(gpa, 64); errdefer out.deinit(); const window = try gpa.alloc(u8, flate.max_window_len); defer gpa.free(window); var c = flate.Compress.init(&out.writer, window, .zlib, .level_6) catch return error.OutOfMemory; c.writer.writeAll(data) catch return error.OutOfMemory; c.finish() catch return error.OutOfMemory; return out.toOwnedSlice();}
const testing = std.testing;
test "header stripping is a prefix" { const enc = [_]ContentEncoding{.{ .compression = .{ .algo = 3, .settings = "\xFF\xFB" } }}; const p = plan(&enc, ContentEncoding.scope_block); try testing.expectEqualStrings("\xFF\xFB", p.prefix); try testing.expect(!p.needs_decode); try testing.expect(plan(&enc, ContentEncoding.scope_private).prefix.len == 0); const out = try decode(testing.allocator, &enc, 1, "abc", 100); defer testing.allocator.free(out); try testing.expectEqualStrings("\xFF\xFBabc", out);}
test "zlib round trips" { const enc = [_]ContentEncoding{.{ .compression = .{ .algo = 0 } }}; const text = "hello hello hello hello hello, matroska"; const packed_ = try deflate(testing.allocator, text); defer testing.allocator.free(packed_); try testing.expect(plan(&enc, 1).needs_decode); const out = try decode(testing.allocator, &enc, 1, packed_, 1000); defer testing.allocator.free(out); try testing.expectEqualStrings(text, out); try testing.expectError(error.BadCompressedData, decode(testing.allocator, &enc, 1, packed_, 5)); try testing.expectError(error.BadCompressedData, decode(testing.allocator, &enc, 1, "garbage!", 1000));}
test "encryption is not undone here" { const enc = [_]ContentEncoding{.{ .type = 1, .encryption = .{ .algo = 5 } }}; try testing.expectError(error.UnsupportedEncoding, decode(testing.allocator, &enc, 1, "x", 10));}