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