Something went wrong. Try again.
A FLAC encoder and decoder for Zig, written against RFC 9639, for native FLAC streams and FLAC in Ogg
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// 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]);}