// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The metadata blocks that stand between the `fLaC` signature and the first //! audio frame (RFC 9639, Section 8). The first of them is always a streaminfo //! block, and the last of them says so in its header. //! //! Everything here reads and writes whole bytes, so it works on a plain //! `std.Io.Reader` rather than the bit reader the frames need. Blocks that //! carry strings or lists own their memory and are freed with `deinit`. const std = @import("std"); const Allocator = std.mem.Allocator; const format = @import("format.zig"); const limits = format.limits; /// The block types this format defines (RFC 9639, Section 8.1, Table 2). /// Anything from 7 to 126 is reserved and is kept as `unknown`; 127 is /// forbidden, so that a metadata block header can never be mistaken for the /// start of a frame. pub const BlockType = enum(u7) { streaminfo = 0, padding = 1, application = 2, seek_table = 3, vorbis_comment = 4, cuesheet = 5, picture = 6, _, pub const forbidden: u7 = 127; }; pub const ReadError = std.Io.Reader.Error || Allocator.Error || error{ NotFlac, ForbiddenBlockType, MalformedMetadata, InvalidStreamInfo, }; pub const WriteError = std.Io.Writer.Error || error{ MetadataTooLong, InvalidStreamInfo, }; /// What the whole stream looks like, in the block that must come first /// (RFC 9639, Section 8.2). pub const StreamInfo = struct { /// The smallest and largest block sizes in the stream, in interchannel /// samples. The smallest excludes the last block, so a stream whose two /// values are equal has a fixed block size. min_block_size: u16, max_block_size: u16, /// The smallest and largest frame sizes in bytes, or zero for unknown. min_frame_size: u32 = 0, max_frame_size: u32 = 0, sample_rate: u32, channels: u8, bits_per_sample: u8, /// Interchannel samples in the stream, or zero for unknown. total_samples: u64 = 0, /// MD5 of the unencoded audio, or all zeroes for unknown. md5: [16]u8 = @splat(0), /// The length of the block body, which is fixed. pub const encoded_length = 34; /// How many bytes one sample takes in the message the MD5 is computed /// over, where samples are sign-extended to a whole number of bytes /// (RFC 9639, Section 8.2). pub fn bytesPerSample(info: StreamInfo) u8 { return (info.bits_per_sample + 7) / 8; } pub fn validate(info: StreamInfo) error{InvalidStreamInfo}!void { if (info.min_block_size < limits.min_block_size) return error.InvalidStreamInfo; if (info.max_block_size < info.min_block_size) return error.InvalidStreamInfo; if (info.channels < limits.min_channels or info.channels > limits.max_channels) return error.InvalidStreamInfo; if (info.bits_per_sample < limits.min_bits_per_sample or info.bits_per_sample > limits.max_bits_per_sample) return error.InvalidStreamInfo; if (info.sample_rate > limits.max_sample_rate) return error.InvalidStreamInfo; if (info.total_samples > limits.max_total_samples) return error.InvalidStreamInfo; if (info.min_frame_size > limits.max_metadata_length) return error.InvalidStreamInfo; if (info.max_frame_size > limits.max_metadata_length) return error.InvalidStreamInfo; return; } pub fn read(r: *std.Io.Reader) (std.Io.Reader.Error || error{InvalidStreamInfo})!StreamInfo { const min_block_size = try r.takeInt(u16, .big); const max_block_size = try r.takeInt(u16, .big); const min_frame_size = try r.takeInt(u24, .big); const max_frame_size = try r.takeInt(u24, .big); // Twenty bits of sample rate, three of channels and five of bit depth // pack into the next five bytes along with the first four bits of the // sample count. const packed_bits = try r.takeInt(u64, .big); const info: StreamInfo = .{ .min_block_size = min_block_size, .max_block_size = max_block_size, .min_frame_size = min_frame_size, .max_frame_size = max_frame_size, .sample_rate = @intCast(packed_bits >> 44), .channels = @as(u8, @intCast((packed_bits >> 41) & 0x7)) + 1, .bits_per_sample = @as(u8, @intCast((packed_bits >> 36) & 0x1f)) + 1, .total_samples = packed_bits & limits.max_total_samples, .md5 = (try r.takeArray(16)).*, }; try info.validate(); return info; } pub fn write(info: StreamInfo, w: *std.Io.Writer) WriteError!void { try info.validate(); try w.writeInt(u16, info.min_block_size, .big); try w.writeInt(u16, info.max_block_size, .big); try w.writeInt(u24, @intCast(info.min_frame_size), .big); try w.writeInt(u24, @intCast(info.max_frame_size), .big); const packed_bits: u64 = (@as(u64, info.sample_rate) << 44) | (@as(u64, info.channels - 1) << 41) | (@as(u64, info.bits_per_sample - 1) << 36) | info.total_samples; try w.writeInt(u64, packed_bits, .big); try w.writeAll(&info.md5); } }; /// A block reserved by an application, identified by a registered four-byte ID /// (RFC 9639, Section 8.4). pub const Application = struct { id: [4]u8, data: []const u8, }; /// One entry of a seek table (RFC 9639, Section 8.5.1). pub const SeekPoint = struct { /// The first sample of the frame this point names, or `placeholder`. sample: u64, /// Where that frame's header starts, counted from the first byte of the /// first frame header in an unmuxed stream. offset: u64, frame_samples: u16, /// A point that reserves space in the table without naming a frame. These /// come last, and a decoder ignores them. pub const placeholder: u64 = std.math.maxInt(u64); pub const encoded_length = 18; pub fn isPlaceholder(p: SeekPoint) bool { return p.sample == placeholder; } }; /// Human-readable tags, in the shape the Vorbis codec stores them, which means /// the only little-endian lengths in the whole format (RFC 9639, Section 8.6). pub const VorbisComment = struct { /// What wrote the file. vendor: []const u8, /// Each entry is a whole `NAME=value` field, unparsed; the name is /// printable ASCII other than `=` and is matched case-insensitively. fields: []const []const u8, /// The value of the first field with this name, or null. The comparison /// ignores case, as the format requires. pub fn get(c: VorbisComment, name: []const u8) ?[]const u8 { for (c.fields) |field| { const eq = std.mem.indexOfScalar(u8, field, '=') orelse continue; if (std.ascii.eqlIgnoreCase(field[0..eq], name)) return field[eq + 1 ..]; } return null; } }; /// Track and index structure, in the shape a Compact Disc's table of contents /// has it (RFC 9639, Section 8.7). pub const CueSheet = struct { /// Right-padded with zero bytes if it is shorter than the field. media_catalog_number: [128]u8, lead_in: u64, is_cd: bool, tracks: []const Track, pub const Track = struct { offset: u64, number: u8, isrc: [12]u8, /// The Q-channel control bit: false for audio, true for anything else. non_audio: bool, pre_emphasis: bool, index_points: []const Index, }; pub const Index = struct { offset: u64, number: u8, }; }; /// An image belonging to the audio, in the shape ID3v2's attached picture /// frame has it, but with counted rather than terminated strings (RFC 9639, /// Section 8.8). pub const Picture = struct { kind: Kind, /// An IANA media type, or the three bytes `-->` to say that `data` is a /// URI naming the picture rather than the picture itself. media_type: []const u8, description: []const u8, /// Width, height, colour depth and palette size are informational only, /// and are zero where they have no meaning. width: u32, height: u32, depth: u32, colors: u32, data: []const u8, pub const uri_media_type = "-->"; /// RFC 9639, Section 8.8, Table 13. pub const Kind = enum(u32) { other = 0, file_icon_32x32 = 1, file_icon = 2, front_cover = 3, back_cover = 4, liner_notes = 5, media_label = 6, lead_artist = 7, artist = 8, conductor = 9, band = 10, composer = 11, lyricist = 12, recording_location = 13, during_recording = 14, during_performance = 15, screen_capture = 16, /// The specification's own words. Its origin is unclear; it was /// carried over from ID3v2 for compatibility, and applications are /// discouraged from offering it. bright_colored_fish = 17, illustration = 18, band_logotype = 19, publisher_logotype = 20, _, }; pub fn isUri(p: Picture) bool { return std.mem.eql(u8, p.media_type, uri_media_type); } }; /// A block whose type this implementation does not know, kept verbatim so that /// rewriting a file does not lose it. pub const Unknown = struct { block_type: u7, data: []const u8, }; /// One metadata block, header and body. pub const Block = struct { /// Set on the last block before the first frame. last: bool = false, body: Body, /// The body exactly as it was read, kept only when writing `body` back /// would not reproduce it. /// /// The format states a block's length in its header, and for every block /// type but padding and application that length is also implied by the /// block's own structure. A file in which the two disagree -- a Vorbis /// comment block with bytes after its last field, a padding block whose /// padding is not zeroes, a cuesheet whose reserved bits are not clear -- /// is malformed, and both of the obvious things to do about it are wrong: /// refusing the file stops audio playing over a fault in a tag, and /// dropping the surplus loses bytes the next person to write the file back /// out would never get again. So the block is parsed as far as it goes, /// which is what `vorbisComment` and the rest need, and the bytes are kept /// beside it, which is what `writeBlock` uses. A block a caller builds /// itself leaves this null and is written from `body`. raw: ?[]const u8 = null, pub const Body = union(enum) { streaminfo: StreamInfo, /// Held as a length, since the contents are zero by definition. padding: u32, application: Application, seek_table: []const SeekPoint, vorbis_comment: VorbisComment, cuesheet: CueSheet, picture: Picture, unknown: Unknown, }; pub fn blockType(b: Block) u7 { return switch (b.body) { .streaminfo => 0, .padding => 1, .application => 2, .seek_table => 3, .vorbis_comment => 4, .cuesheet => 5, .picture => 6, .unknown => |u| u.block_type, }; } /// The length of the body, which the header states and which therefore /// has to be known before a byte of it is written. pub fn bodyLength(b: Block) error{MetadataTooLong}!u32 { if (b.raw) |raw| { if (raw.len > limits.max_metadata_length) return error.MetadataTooLong; return @intCast(raw.len); } const length: u64 = switch (b.body) { .streaminfo => StreamInfo.encoded_length, .padding => |n| n, .application => |a| 4 + a.data.len, .seek_table => |points| points.len * SeekPoint.encoded_length, .vorbis_comment => |c| blk: { var n: u64 = 4 + c.vendor.len + 4; for (c.fields) |field| n += 4 + field.len; break :blk n; }, .cuesheet => |c| blk: { var n: u64 = 128 + 8 + 1 + 258 + 1; for (c.tracks) |track| n += 8 + 1 + 12 + 1 + 13 + 1 + track.index_points.len * 12; break :blk n; }, .picture => |p| 32 + p.media_type.len + p.description.len + p.data.len, .unknown => |u| u.data.len, }; if (length > limits.max_metadata_length) return error.MetadataTooLong; return @intCast(length); } pub fn deinit(b: Block, gpa: Allocator) void { if (b.raw) |raw| gpa.free(raw); switch (b.body) { .streaminfo, .padding => {}, .application => |a| gpa.free(a.data), .seek_table => |points| gpa.free(points), .vorbis_comment => |c| { gpa.free(c.vendor); for (c.fields) |field| gpa.free(field); gpa.free(c.fields); }, .cuesheet => |c| { for (c.tracks) |track| gpa.free(track.index_points); gpa.free(c.tracks); }, .picture => |p| { gpa.free(p.media_type); gpa.free(p.description); gpa.free(p.data); }, .unknown => |u| gpa.free(u.data), } } }; /// Read one metadata block, header and all. The caller frees it. pub fn readBlock(gpa: Allocator, r: *std.Io.Reader) ReadError!Block { const first = try r.takeByte(); const last = first & 0x80 != 0; const block_type: u7 = @intCast(first & 0x7f); if (block_type == BlockType.forbidden) return error.ForbiddenBlockType; const length = try r.takeInt(u24, .big); const raw = try gpa.alloc(u8, length); defer gpa.free(raw); r.readSliceAll(raw) catch |err| return switch (err) { error.EndOfStream => error.EndOfStream, error.ReadFailed => error.ReadFailed, }; var body: std.Io.Reader = .fixed(raw); // Only a cuesheet has reserved bits deep enough inside it that running out // of body is not the only way for a parse to be inexact. var reserved_clear = true; const parsed: Block.Body = switch (@as(BlockType, @fromBackingInt(@intCast(block_type)))) { .streaminfo => .{ .streaminfo = StreamInfo.read(&body) catch |err| switch (err) { error.EndOfStream => return error.MalformedMetadata, else => |other| return other, } }, .padding => .{ .padding = length }, .application => try readApplication(gpa, &body, raw), .seek_table => try readSeekTable(gpa, &body, length), .vorbis_comment => try readVorbisComment(gpa, &body), .cuesheet => try readCueSheet(gpa, &body, &reserved_clear), .picture => try readPicture(gpa, &body), _ => .{ .unknown = .{ .block_type = block_type, .data = try gpa.dupe(u8, raw) } }, }; var block: Block = .{ .last = last, .body = parsed }; errdefer block.deinit(gpa); // A padding block's parse consumes nothing, since a length is all there is // to it, so what makes it exact is that its bytes really are zeroes. const exact = switch (parsed) { .padding => std.mem.allEqual(u8, raw, 0), else => body.bufferedLen() == 0 and reserved_clear, }; if (!exact) block.raw = try gpa.dupe(u8, raw); return block; } fn readApplication(gpa: Allocator, body: *std.Io.Reader, raw: []const u8) ReadError!Block.Body { const id = (body.takeArray(4) catch return error.MalformedMetadata).*; return .{ .application = .{ .id = id, .data = try gpa.dupe(u8, raw[4..]) } }; } fn readSeekTable(gpa: Allocator, body: *std.Io.Reader, length: u32) ReadError!Block.Body { if (length % SeekPoint.encoded_length != 0) return error.MalformedMetadata; const points = try gpa.alloc(SeekPoint, length / SeekPoint.encoded_length); errdefer gpa.free(points); for (points) |*point| point.* = .{ .sample = body.takeInt(u64, .big) catch return error.MalformedMetadata, .offset = body.takeInt(u64, .big) catch return error.MalformedMetadata, .frame_samples = body.takeInt(u16, .big) catch return error.MalformedMetadata, }; return .{ .seek_table = points }; } fn readVorbisComment(gpa: Allocator, body: *std.Io.Reader) ReadError!Block.Body { const vendor = try takeCountedString(gpa, body); errdefer gpa.free(vendor); const count = body.takeInt(u32, .little) catch return error.MalformedMetadata; // A field takes at least its own four length bytes, so a count larger // than the block can hold is a malformed block rather than a big one. if (count > body.bufferedLen() / 4) return error.MalformedMetadata; var fields: std.ArrayList([]const u8) = .empty; errdefer { for (fields.items) |field| gpa.free(field); fields.deinit(gpa); } try fields.ensureTotalCapacityPrecise(gpa, count); for (0..count) |_| fields.appendAssumeCapacity(try takeCountedString(gpa, body)); return .{ .vorbis_comment = .{ .vendor = vendor, .fields = try fields.toOwnedSlice(gpa), } }; } fn takeCountedString(gpa: Allocator, body: *std.Io.Reader) ReadError![]const u8 { const length = body.takeInt(u32, .little) catch return error.MalformedMetadata; if (length > body.bufferedLen()) return error.MalformedMetadata; const bytes = body.take(length) catch return error.MalformedMetadata; return gpa.dupe(u8, bytes); } fn readCueSheet(gpa: Allocator, body: *std.Io.Reader, reserved_clear: *bool) ReadError!Block.Body { var sheet: CueSheet = .{ .media_catalog_number = (body.takeArray(128) catch return error.MalformedMetadata).*, .lead_in = body.takeInt(u64, .big) catch return error.MalformedMetadata, .is_cd = false, .tracks = &.{}, }; const flags = body.takeByte() catch return error.MalformedMetadata; sheet.is_cd = flags & 0x80 != 0; if (flags & 0x7f != 0) reserved_clear.* = false; if (!try reservedIsClear(body, 258)) reserved_clear.* = false; const track_count = body.takeByte() catch return error.MalformedMetadata; const tracks = try gpa.alloc(CueSheet.Track, track_count); var filled: usize = 0; errdefer { for (tracks[0..filled]) |track| gpa.free(track.index_points); gpa.free(tracks); } while (filled < track_count) : (filled += 1) { const offset = body.takeInt(u64, .big) catch return error.MalformedMetadata; const number = body.takeByte() catch return error.MalformedMetadata; const isrc = (body.takeArray(12) catch return error.MalformedMetadata).*; const track_flags = body.takeByte() catch return error.MalformedMetadata; if (track_flags & 0x3f != 0) reserved_clear.* = false; if (!try reservedIsClear(body, 13)) reserved_clear.* = false; const index_count = body.takeByte() catch return error.MalformedMetadata; const points = try gpa.alloc(CueSheet.Index, index_count); errdefer gpa.free(points); for (points) |*point| { point.* = .{ .offset = body.takeInt(u64, .big) catch return error.MalformedMetadata, .number = body.takeByte() catch return error.MalformedMetadata, }; if (!try reservedIsClear(body, 3)) reserved_clear.* = false; } tracks[filled] = .{ .offset = offset, .number = number, .isrc = isrc, .non_audio = track_flags & 0x80 != 0, .pre_emphasis = track_flags & 0x40 != 0, .index_points = points, }; } sheet.tracks = tracks; return .{ .cuesheet = sheet }; } /// Consume `n` reserved bytes, saying whether every one of them was zero as /// the format requires. fn reservedIsClear(body: *std.Io.Reader, n: usize) ReadError!bool { const bytes = body.take(n) catch return error.MalformedMetadata; return std.mem.allEqual(u8, bytes, 0); } fn readPicture(gpa: Allocator, body: *std.Io.Reader) ReadError!Block.Body { const kind: Picture.Kind = @fromBackingInt(@intCast(body.takeInt(u32, .big) catch return error.MalformedMetadata)); const media_type = try takeBigCountedString(gpa, body); errdefer gpa.free(media_type); const description = try takeBigCountedString(gpa, body); errdefer gpa.free(description); const width = body.takeInt(u32, .big) catch return error.MalformedMetadata; const height = body.takeInt(u32, .big) catch return error.MalformedMetadata; const depth = body.takeInt(u32, .big) catch return error.MalformedMetadata; const colors = body.takeInt(u32, .big) catch return error.MalformedMetadata; const data = try takeBigCountedString(gpa, body); return .{ .picture = .{ .kind = kind, .media_type = media_type, .description = description, .width = width, .height = height, .depth = depth, .colors = colors, .data = data, } }; } fn takeBigCountedString(gpa: Allocator, body: *std.Io.Reader) ReadError![]const u8 { const length = body.takeInt(u32, .big) catch return error.MalformedMetadata; if (length > body.bufferedLen()) return error.MalformedMetadata; const bytes = body.take(length) catch return error.MalformedMetadata; return gpa.dupe(u8, bytes); } /// Write one metadata block, header and all. pub fn writeBlock(w: *std.Io.Writer, b: Block) WriteError!void { const length = try b.bodyLength(); try w.writeByte((if (b.last) @as(u8, 0x80) else 0) | b.blockType()); try w.writeInt(u24, @intCast(length), .big); if (b.raw) |raw| return w.writeAll(raw); switch (b.body) { .streaminfo => |info| try info.write(w), .padding => |n| try w.splatByteAll(0, n), .application => |a| { try w.writeAll(&a.id); try w.writeAll(a.data); }, .seek_table => |points| for (points) |point| { try w.writeInt(u64, point.sample, .big); try w.writeInt(u64, point.offset, .big); try w.writeInt(u16, point.frame_samples, .big); }, .vorbis_comment => |c| { try w.writeInt(u32, @intCast(c.vendor.len), .little); try w.writeAll(c.vendor); try w.writeInt(u32, @intCast(c.fields.len), .little); for (c.fields) |field| { try w.writeInt(u32, @intCast(field.len), .little); try w.writeAll(field); } }, .cuesheet => |c| { try w.writeAll(&c.media_catalog_number); try w.writeInt(u64, c.lead_in, .big); try w.writeByte(if (c.is_cd) 0x80 else 0); try w.splatByteAll(0, 258); try w.writeByte(@intCast(c.tracks.len)); for (c.tracks) |track| { try w.writeInt(u64, track.offset, .big); try w.writeByte(track.number); try w.writeAll(&track.isrc); try w.writeByte((if (track.non_audio) @as(u8, 0x80) else 0) | (if (track.pre_emphasis) @as(u8, 0x40) else 0)); try w.splatByteAll(0, 13); try w.writeByte(@intCast(track.index_points.len)); for (track.index_points) |point| { try w.writeInt(u64, point.offset, .big); try w.writeByte(point.number); try w.splatByteAll(0, 3); } } }, .picture => |p| { try w.writeInt(u32, @backingInt(p.kind), .big); try w.writeInt(u32, @intCast(p.media_type.len), .big); try w.writeAll(p.media_type); try w.writeInt(u32, @intCast(p.description.len), .big); try w.writeAll(p.description); try w.writeInt(u32, p.width, .big); try w.writeInt(u32, p.height, .big); try w.writeInt(u32, p.depth, .big); try w.writeInt(u32, p.colors, .big); try w.writeInt(u32, @intCast(p.data.len), .big); try w.writeAll(p.data); }, .unknown => |u| try w.writeAll(u.data), } } /// Skip an ID3v2 tag if one is at the front of the stream, returning how many /// bytes it took. /// /// FLAC has no business carrying an ID3v2 tag -- it has metadata blocks of its /// own, and the format says the signature comes first -- but taggers written /// for other formats put one there anyway, and a file with one plays /// everywhere else. The tag's length is stored in seven-bit bytes, so that no /// byte of the length can be mistaken for the frame sync an MPEG decoder is /// looking for. pub fn skipId3v2(r: *std.Io.Reader) std.Io.Reader.Error!u32 { const header = r.peek(10) catch |err| switch (err) { // Too short to be a tag, which is not an error here: whatever it is, // it is not one, and the caller will make its own mind up about it. error.EndOfStream => return 0, error.ReadFailed => return error.ReadFailed, }; if (!std.mem.eql(u8, header[0..3], "ID3")) return 0; // The size is four bytes of seven bits each, most significant first, and // covers everything after the ten-byte header. var length: u32 = 0; for (header[6..10]) |byte| { if (byte & 0x80 != 0) return 0; length = (length << 7) | byte; } // Bit 4 of the flags says a ten-byte footer follows the tag as well. const total = 10 + length + @as(u32, if (header[5] & 0x10 != 0) 10 else 0); try r.discardAll(total); return total; } /// Read the signature and every metadata block up to and including the one /// flagged last, leaving the reader on the first frame. The caller frees the /// result with `freeBlocks`. pub fn readAll(gpa: Allocator, r: *std.Io.Reader) ReadError![]Block { const magic = r.takeArray(4) catch |err| switch (err) { error.EndOfStream => return error.NotFlac, error.ReadFailed => return error.ReadFailed, }; if (!std.mem.eql(u8, magic, format.signature)) return error.NotFlac; var blocks: std.ArrayList(Block) = .empty; errdefer { for (blocks.items) |b| b.deinit(gpa); blocks.deinit(gpa); } while (true) { const b = try readBlock(gpa, r); if (blocks.items.len == 0 and b.body != .streaminfo) { b.deinit(gpa); return error.MalformedMetadata; } try blocks.append(gpa, b); if (b.last) break; } return blocks.toOwnedSlice(gpa); } pub fn freeBlocks(gpa: Allocator, blocks: []const Block) void { for (blocks) |b| b.deinit(gpa); gpa.free(blocks); } /// Write the signature and every block, flagging the last one as such. pub fn writeAll(w: *std.Io.Writer, blocks: []const Block) WriteError!void { try w.writeAll(format.signature); for (blocks, 0..) |b, i| { var copy = b; copy.last = i + 1 == blocks.len; try writeBlock(w, copy); } } const testing = std.testing; test "an ID3v2 tag at the front of a stream is stepped over" { // Ten bytes of header saying eight bytes follow, then the signature. const tagged = "ID3\x04\x00\x00\x00\x00\x00\x08" ++ @as([8]u8, @splat(0x00)) ++ "fLaC"; var r: std.Io.Reader = .fixed(tagged); try testing.expectEqual(@as(u32, 18), try skipId3v2(&r)); try testing.expectEqualStrings("fLaC", (try r.takeArray(4))); // The same, with the footer flag set, which adds ten more. const footered = "ID3\x04\x00\x10\x00\x00\x00\x08" ++ @as([18]u8, @splat(0x00)) ++ "fLaC"; var with_footer: std.Io.Reader = .fixed(footered); try testing.expectEqual(@as(u32, 28), try skipId3v2(&with_footer)); // A stream that is already a FLAC stream is left alone, and so is one too // short to be a tag. var plain: std.Io.Reader = .fixed("fLaC\x80\x00\x00\x22"); try testing.expectEqual(@as(u32, 0), try skipId3v2(&plain)); var stub: std.Io.Reader = .fixed("ID3"); try testing.expectEqual(@as(u32, 0), try skipId3v2(&stub)); } test "the streaminfo of RFC 9639 example 2" { const body = [_]u8{ 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x17, 0x00, 0x00, 0x44, 0x0a, 0xc4, 0x42, 0xf0, 0x00, 0x00, 0x00, 0x13, 0xd5, 0xb0, 0x56, 0x49, 0x75, 0xe9, 0x8b, 0x8d, 0x8b, 0x93, 0x04, 0x22, 0x75, 0x7b, 0x81, 0x03, }; var r: std.Io.Reader = .fixed(&body); const info = try StreamInfo.read(&r); try testing.expectEqual(@as(u16, 16), info.min_block_size); try testing.expectEqual(@as(u16, 16), info.max_block_size); try testing.expectEqual(@as(u32, 23), info.min_frame_size); try testing.expectEqual(@as(u32, 68), info.max_frame_size); try testing.expectEqual(@as(u32, 44100), info.sample_rate); try testing.expectEqual(@as(u8, 2), info.channels); try testing.expectEqual(@as(u8, 16), info.bits_per_sample); try testing.expectEqual(@as(u64, 19), info.total_samples); try testing.expectEqual(@as(u8, 2), info.bytesPerSample()); var buffer: [StreamInfo.encoded_length]u8 = undefined; var w: std.Io.Writer = .fixed(&buffer); try info.write(&w); try testing.expectEqualSlices(u8, &body, &buffer); } test "reading every block of RFC 9639 example 2" { const gpa = testing.allocator; const file = @embedFile("tests/rfc9639/example2.flac"); var r: std.Io.Reader = .fixed(file); const blocks = try readAll(gpa, &r); defer freeBlocks(gpa, blocks); try testing.expectEqual(@as(usize, 4), blocks.len); try testing.expectEqual(@as(u64, 19), blocks[0].body.streaminfo.total_samples); // One seek point, naming the first sample of the first frame. const points = blocks[1].body.seek_table; try testing.expectEqual(@as(usize, 1), points.len); try testing.expectEqual(@as(u64, 0), points[0].sample); try testing.expectEqual(@as(u64, 0), points[0].offset); try testing.expectEqual(@as(u16, 16), points[0].frame_samples); const comment = blocks[2].body.vorbis_comment; try testing.expectEqualStrings("reference libFLAC 1.3.3 20190804", comment.vendor); try testing.expectEqual(@as(usize, 1), comment.fields.len); try testing.expectEqualStrings("שלום", comment.get("title").?); try testing.expectEqual(@as(?[]const u8, null), comment.get("artist")); try testing.expectEqual(@as(u32, 6), blocks[3].body.padding); try testing.expect(blocks[3].last); // Writing them back reproduces the file's metadata byte for byte. var out: std.Io.Writer.Allocating = .init(gpa); defer out.deinit(); try writeAll(&out.writer, blocks); try testing.expectEqualSlices(u8, file[0..out.written().len], out.written()); } test "metadata blocks that are not blocks" { const gpa = testing.allocator; var truncated: std.Io.Reader = .fixed("fLa"); try testing.expectError(error.NotFlac, readAll(gpa, &truncated)); var wrong_magic: std.Io.Reader = .fixed("OggS" ++ "\x80\x00\x00\x22"); try testing.expectError(error.NotFlac, readAll(gpa, &wrong_magic)); // Block type 127 exists only so that a metadata block header can never be // mistaken for a frame sync code. var forbidden: std.Io.Reader = .fixed("fLaC" ++ "\xff\x00\x00\x00"); try testing.expectError(error.ForbiddenBlockType, readAll(gpa, &forbidden)); // The first block has to be a streaminfo block. var padding_first: std.Io.Reader = .fixed("fLaC" ++ "\x81\x00\x00\x00"); try testing.expectError(error.MalformedMetadata, readAll(gpa, &padding_first)); // A seek table has to be a whole number of seek points. var ragged: std.Io.Reader = .fixed("\x83\x00\x00\x11" ++ @as([17]u8, @splat(0x00))); try testing.expectError(error.MalformedMetadata, readBlock(gpa, &ragged)); } test "blocks this implementation does not know survive a rewrite" { const gpa = testing.allocator; const block = "\x0a\x00\x00\x05" ++ "hello"; var r: std.Io.Reader = .fixed(block); const b = try readBlock(gpa, &r); defer b.deinit(gpa); try testing.expectEqual(@as(u7, 10), b.body.unknown.block_type); try testing.expectEqualStrings("hello", b.body.unknown.data); var buffer: [16]u8 = undefined; var w: std.Io.Writer = .fixed(&buffer); try writeBlock(&w, b); try testing.expectEqualStrings(block, buffer[0..w.end]); }