diff --git a/README.md b/README.md index 682c678..0358f2c 100644 --- a/README.md +++ b/README.md @@ -289,11 +289,11 @@ The targets, and what each is really for: | arbitrary bytes | the sync scanner and the tag skipper | | the bit reader | the eight-byte load against a plain bit-at-a-time reader | | the bit cache | the 57 bits the Huffman loop reads through, driven as the decoder drives it, against the same plain reader | -| the filterbanks | the halved arithmetic against the standard's own, in `reference.zig` | +| the filterbanks | the halved arithmetic against the standard's own, in `reference.zig`, at both slot counts and writing into one channel of an interleaved pair as well as a buffer of its own | | resets | the bit reservoir and the filterbank history across discontinuities | | side information | that a malformed granule is rejected, not misread | | Huffman | that decoding always terminates, always advances, always returns a representable pair | -| Layer I and II frames | that any allocation, scalefactor and code an arbitrary body can name decodes to finite, bounded samples | +| Layer I and II frames | that any allocation, scalefactor and code an arbitrary body can name decodes to finite, bounded samples, including when the subbands between them ask for more bits than the frame holds | | ID3 tags | that everything comes out valid UTF-8, and nothing leaks when allocation fails | | Xing, Info and VBRI headers | that a duration is a finite number of seconds and a seek lands inside the file | | the one shot interface | that it holds together, and leaks nothing when allocation fails | diff --git a/src/fuzz_test.zig b/src/fuzz_test.zig index fb858c2..57fa854 100644 --- a/src/fuzz_test.zig +++ b/src/fuzz_test.zig @@ -477,20 +477,45 @@ fn checkFilterbanks(choices: Choices) !void { var fast = synthesis.State{}; var slow = reference.SynthesisState{}; + // Layer III hands the filterbank eighteen time slots per granule and + // Layers I and II hand it twelve, and either may be writing into one + // channel of an interleaved pair rather than a buffer of its own. Both + // the slot count and the stride are taken from the arguments now, so + // both are chosen here. + const slot_count: usize = if (choices.boolean("fb_slots")) 18 else 12; + const stride: usize = if (choices.boolean("fb_stride")) 2 else 1; + const lines = slot_count * synthesis.subbands; + const granules = choices.int(u8, "fb_granules", 1, 24); for (0..granules) |_| { var spectrum: [576]f32 = undefined; // Bounded, because the two sum in different orders and comparing // them means comparing rounding, not scale. - for (&spectrum) |*value| { + for (spectrum[0..lines]) |*value| { value.* = @as(f32, @floatFromInt(choices.int(i16, "fb_line", -1000, 1000))) / 1000.0; } - var got: [576]f32 = undefined; + var got: [576 * 2]f32 = undefined; var want: [576]f32 = undefined; - synthesis.synthesize(&fast, &spectrum, &got, 1); - reference.synthesize(&slow, &spectrum, &want); - for (got, want) |g, w| try std.testing.expectApproxEqAbs(w, g, 1e-3); + // Filled with rubbish first, so that writing to the wrong place -- + // or not writing at all -- shows up rather than reading back as the + // zero it should have been. + @memset(std.mem.sliceAsBytes(got[0..]), 0x5a); + synthesis.synthesize(&fast, spectrum[0..lines], got[0 .. lines * stride], stride); + reference.synthesize(&slow, spectrum[0..lines], want[0..lines]); + for (0..lines) |i| { + try std.testing.expectApproxEqAbs(want[i], got[i * stride], 1e-3); + } + // The gaps between the strided samples belong to the other channel + // and must come back exactly as they were left. + if (stride == 2) { + for (0..lines) |i| { + try std.testing.expectEqual( + @as(u32, 0x5a5a5a5a), + @as(u32, @bitCast(got[i * 2 + 1])), + ); + } + } } // The inverse MDCT, over whichever block shape was asked for. @@ -901,9 +926,11 @@ fn checkHeaders(choices: Choices) !void { /// What a run of `checkLayer12` reached. Layers I and II have far fewer /// paths than Layer III, but the ones they do have are easy to leave /// untouched: an encoder picks the grouped quantization classes only at low -/// bit rates, and joint stereo only when it decides the channels are alike. +/// bit rates, joint stereo only when it decides the channels are alike, and +/// never asks a frame for more bits than it holds. const Layer12Coverage = struct { - frames: usize = 0, + layer1: usize = 0, + layer2: usize = 0, /// Subbands quantized with one of the three classes that pack a triple of /// values into a single code word. grouped: usize = 0, @@ -912,14 +939,35 @@ const Layer12Coverage = struct { shared: usize = 0, /// Frames using the MPEG-2 allocation table rather than one of MPEG-1's. low_sampling: usize = 0, + /// Frames whose allocations ask for more bits than the frame carries, so + /// that the reader runs off the end and starts returning zeroes. A + /// corrupt or hostile stream reaches this and nothing else does. + overrun: usize = 0, + /// Frames handed a body shorter than the header says, which is not + /// something the decoder itself produces -- it steps over a truncated + /// final frame -- but which this function is entitled to survive. + short: usize = 0, + /// Which scalefactor selectors turned up, as a bit per value. Layer II + /// sends one of four, and two of them repeat a scalefactor across + /// granules in a way the other two do not. + selectors: u8 = 0, + /// Which subband counts turned up, as a bit per distinct value. There + /// are five: 32 for Layer I, and 8, 12, 27 and 30 for Layer II depending + /// on the bit rate, the sampling frequency and the version. + band_counts: u64 = 0, /// Frames that decoded to something other than silence. audible: usize = 0, fn add(self: *Layer12Coverage, other: Layer12Coverage) void { - self.frames += other.frames; + self.layer1 += other.layer1; + self.layer2 += other.layer2; self.grouped += other.grouped; self.shared += other.shared; self.low_sampling += other.low_sampling; + self.overrun += other.overrun; + self.short += other.short; + self.selectors |= other.selectors; + self.band_counts |= other.band_counts; self.audible += other.audible; } }; @@ -927,9 +975,10 @@ const Layer12Coverage = struct { /// Decode one arbitrary Layer I or Layer II frame body. /// /// Every field a frame can carry is chosen freely, so the allocations name -/// classes an encoder would never pick and the scalefactors point anywhere in -/// their table. Nothing about that is illegal, and all of it must decode to -/// finite samples. +/// classes an encoder would never pick, the scalefactors point anywhere in +/// their table, and the subbands between them routinely ask for more bits +/// than the frame has. None of that is illegal for a decoder to be handed, +/// and all of it must produce finite, bounded samples. fn checkLayer12(choices: Choices) !Layer12Coverage { const versions = [3]u2{ 0, 2, 3 }; // MPEG-2.5, MPEG-2, MPEG-1 const raw: [4]u8 = .{ @@ -948,10 +997,18 @@ fn checkLayer12(choices: Choices) !Layer12Coverage { if (size <= 4) return .{}; var body: [1728]u8 = undefined; // the largest a frame body can be - const length = @min(size - 4, body.len); + var length = @min(size - 4, body.len); + var coverage: Layer12Coverage = .{}; + // One frame in eight is cut short. The decoder steps over a truncated + // final frame rather than decoding it, so this is defence rather than a + // path in the current caller; it costs nothing to hold to it. + if (choices.chance("l12_short", 8)) { + length = choices.int(u11, "l12_length", 0, @intCast(length)); + coverage.short += 1; + } choices.fill("l12_body", body[0..length]); - var coverage: Layer12Coverage = .{ .frames = 1 }; + if (header.layer == .layer1) coverage.layer1 += 1 else coverage.layer2 += 1; // Read the allocations a second time, on their own, to see what the // generator actually reached. @@ -961,21 +1018,55 @@ fn checkLayer12(choices: Choices) !Layer12Coverage { if (header.version.isLowSamplingFrequency() and header.layer == .layer2) { coverage.low_sampling += 1; } + // Walk the granules on this reader too, so that the overrun counted is + // the whole frame's and not just its allocations: a frame runs out of + // bits in its samples far more often than in its scalefactors. + { + var scratch: [2][layer12.granule_len]f32 = undefined; + const granules: usize = if (header.layer == .layer1) 1 else 3; + for (0..granules) |_| layer12.readGranule(&reader, &info, header, &scratch); + } + if (reader.overrun > 0) coverage.overrun += 1; + if (info.total_bands < 64) { + coverage.band_counts |= @as(u64, 1) << @intCast(info.total_bands); + } for (0..info.total_bands) |band| { for (0..2) |ch| { - if (info.allocation[band * 2 + ch] >= 17) coverage.grouped += 1; + const allocation = info.allocation[band * 2 + ch]; + if (allocation >= 17) coverage.grouped += 1; + if (allocation != 0) { + coverage.selectors |= @as(u8, 1) << @intCast(info.selection[band * 2 + ch]); + } } if (band >= info.stereo_bands and header.channels() == 2) coverage.shared += 1; } + // Decode twice, from fresh state, into buffers left holding different + // rubbish. Anything the decoder leaves untouched, or reads before + // writing, shows up as a difference; anything it computes comes out the + // same both times. + var first: [layer12.max_samples * 2]f32 = undefined; + var second: [layer12.max_samples * 2]f32 = undefined; + @memset(std.mem.sliceAsBytes(first[0..]), 0x5a); + @memset(std.mem.sliceAsBytes(second[0..]), 0xa5); + var state: layer12.State = .{}; - var out: [layer12.max_samples * 2]f32 = undefined; - const produced = layer12.decodeFrame(&state, header, body[0..length], &out); + const produced = layer12.decodeFrame(&state, header, body[0..length], &first); + var again: layer12.State = .{}; + const produced_again = layer12.decodeFrame(&again, header, body[0..length], &second); + try std.testing.expectEqual(produced, produced_again); const channels: usize = header.channels(); try std.testing.expect(produced == layer12.granule_len or produced == layer12.granule_len * 3); - for (out[0 .. produced * channels]) |sample| { + const written = produced * channels; + try std.testing.expectEqualSlices( + u8, + std.mem.sliceAsBytes(first[0..written]), + std.mem.sliceAsBytes(second[0..written]), + ); + + for (first[0..written]) |sample| { try std.testing.expect(std.math.isFinite(sample)); // A ceiling nothing a frame can say may reach. Requantization // keeps a subband sample within the loudest scalefactor, which is 2; @@ -1023,11 +1114,20 @@ test "Layer I and II frames survive arbitrary bodies" { // As with the built streams, the floors are there so that a change which // stopped the generator reaching these paths fails loudly rather than // quietly testing nothing. Each is well under what it currently manages. - try std.testing.expect(coverage.frames > rounds / 2); + try std.testing.expect(coverage.layer1 > rounds / 4); + try std.testing.expect(coverage.layer2 > rounds / 4); try std.testing.expect(coverage.audible > rounds); try std.testing.expect(coverage.grouped > rounds / 8); try std.testing.expect(coverage.shared > rounds / 8); try std.testing.expect(coverage.low_sampling > rounds / 32); + try std.testing.expect(coverage.overrun > rounds / 8); + try std.testing.expect(coverage.short > rounds / 32); + // All four scalefactor selectors, and all five subband counts: 32 for + // Layer I, and 8, 12, 27 and 30 for Layer II. + try std.testing.expectEqual(@as(u8, 0b1111), coverage.selectors & 0b1111); + const wanted_counts = (@as(u64, 1) << 8) | (@as(u64, 1) << 12) | + (@as(u64, 1) << 27) | (@as(u64, 1) << 30) | (@as(u64, 1) << 32); + try std.testing.expectEqual(wanted_counts, coverage.band_counts & wanted_counts); } test "arbitrary bytes decode without falling over" { @@ -1154,6 +1254,10 @@ fn fuzzArbitraryBytes(_: void, smith: *Smith) anyerror!void { try checkArbitraryBytes(.{ .smith = smith }, &scratch); } +fn fuzzLayer12(_: void, smith: *Smith) anyerror!void { + _ = try checkLayer12(.{ .smith = smith }); +} + fn fuzzSideInfo(_: void, smith: *Smith) anyerror!void { try checkSideInfo(.{ .smith = smith }); } @@ -1202,6 +1306,10 @@ test "fuzz arbitrary bytes" { try std.testing.fuzz({}, fuzzArbitraryBytes, .{}); } +test "fuzz Layer I and II frames" { + try std.testing.fuzz({}, fuzzLayer12, .{}); +} + test "fuzz side information" { try std.testing.fuzz({}, fuzzSideInfo, .{}); }