lib/pdf/src/filter/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const flate = std.compress.flate;
3
4 const filter = @import("root.zig");
5
6 const Storage = filter.Storage;
7 const bytesFromStream = filter.bytesFromStream;
8
9 fn deflateAlloc(allocator: std.mem.Allocator, raw: []const u8) ![]u8 {
10 var output = try std.Io.Writer.Allocating.initCapacity(allocator, 64);
11 errdefer output.deinit();
12 const window = try allocator.alloc(u8, flate.max_window_len);
13 defer allocator.free(window);
14 var compressor = try flate.Compress.init(
15 &output.writer,
16 window,
17 .zlib,
18 flate.Compress.Options.level_1,
19 );
20 try compressor.writer.writeAll(raw);
21 try compressor.finish();
22 return try output.toOwnedSlice();
23 }
24
25 test "PDF filter decodes Flate and PNG predictor streams" {
26 const raw = [_]u8{ 2, 1, 2, 3, 2, 1, 1, 1 };
27 const compressed = try deflateAlloc(std.testing.allocator, &raw);
28 defer std.testing.allocator.free(compressed);
29 var storage = try Storage.init(std.testing.allocator, .{ .bounds = .{
30 .input_bytes = compressed.len,
31 .decoded_bytes = raw.len,
32 } });
33 defer storage.deinit(std.testing.allocator);
34 storage.activate();
35 const decoded = try bytesFromStream(
36 &storage,
37 "FlateDecode",
38 .{ .predictor = 12, .columns = 3 },
39 compressed,
40 );
41 defer storage.reset();
42 try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 2, 3, 4 }, decoded);
43 }
44
45 test "PDF filter storage accepts exact bounds and rejects max plus one" {
46 comptime {
47 @stardustClaim(
48 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "pdf_filter_boundaries"),
49 null,
50 null,
51 null,
52 null,
53 null,
54 null,
55 );
56 }
57
58 var storage = try Storage.init(std.testing.allocator, .{ .bounds = .{
59 .input_bytes = 5,
60 .decoded_bytes = 4,
61 } });
62 defer storage.deinit(std.testing.allocator);
63 storage.activate();
64 @memset(storage.output, 0xa5);
65 const hash = std.hash.Wyhash.hash(0, storage.output);
66 try std.testing.expectError(
67 error.InputByteCapacityExceeded,
68 bytesFromStream(&storage, null, .{}, "123456"),
69 );
70 try std.testing.expectEqual(hash, std.hash.Wyhash.hash(0, storage.output));
71 const exact = try bytesFromStream(&storage, null, .{}, "1234");
72 try std.testing.expectEqualStrings("1234", exact);
73 storage.reset();
74 try std.testing.expectError(
75 error.DecodedByteCapacityExceeded,
76 bytesFromStream(&storage, null, .{}, "12345"),
77 );
78 }
79
80 test "PDF filter storage rejects concurrent use and supports reuse" {
81 comptime {
82 @stardustClaim(
83 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "pdf_filter_reuse"),
84 null,
85 null,
86 null,
87 null,
88 null,
89 null,
90 );
91 }
92
93 var storage = try Storage.init(std.testing.allocator, .{ .bounds = .{
94 .input_bytes = 4,
95 .decoded_bytes = 4,
96 } });
97 defer storage.deinit(std.testing.allocator);
98 storage.activate();
99 _ = try bytesFromStream(&storage, null, .{}, "one");
100 try std.testing.expectError(
101 error.DecodeStorageInUse,
102 bytesFromStream(&storage, null, .{}, "two"),
103 );
104 storage.reset();
105 const second = try bytesFromStream(&storage, null, .{}, "two");
106 defer storage.reset();
107 try std.testing.expectEqualStrings("two", second);
108 }
109
110 test "Activated PDF filter storage performs no backing allocation" {
111 comptime {
112 @stardustClaim(
113 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "pdf_filter_sealed"),
114 null,
115 null,
116 null,
117 null,
118 null,
119 null,
120 );
121 }
122
123 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
124 var storage = try Storage.init(counting.allocator(), .{ .bounds = .{
125 .input_bytes = 4,
126 .decoded_bytes = 4,
127 } });
128 defer storage.deinit(counting.allocator());
129 storage.activate();
130 const decoded = try bytesFromStream(&storage, null, .{}, "data");
131 defer storage.reset();
132 try std.testing.expectEqualStrings("data", decoded);
133 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
134 try std.testing.expectEqual(@as(usize, 0), counting.resize_index);
135 }
136
137 test "PDF filter decode errors release storage before retry" {
138 var storage = try Storage.init(std.testing.allocator, .{ .bounds = .{
139 .input_bytes = 4,
140 .decoded_bytes = 4,
141 } });
142 defer storage.deinit(std.testing.allocator);
143 storage.activate();
144 try std.testing.expectError(
145 error.DecodeFailed,
146 bytesFromStream(&storage, "FlateDecode", .{}, "bad"),
147 );
148 try std.testing.expect(!storage.status().in_use);
149 const decoded = try bytesFromStream(&storage, null, .{}, "ok");
150 defer storage.reset();
151 try std.testing.expectEqualStrings("ok", decoded);
152 }
153
154 test "PDF filter DEFLATE overflow stays private and releases storage" {
155 comptime {
156 @stardustClaim(
157 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "pdf_filter_decode_overflow"),
158 null,
159 null,
160 null,
161 null,
162 null,
163 null,
164 );
165 }
166
167 const compressed = try deflateAlloc(std.testing.allocator, "12345");
168 defer std.testing.allocator.free(compressed);
169 var storage = try Storage.init(std.testing.allocator, .{ .bounds = .{
170 .input_bytes = compressed.len,
171 .decoded_bytes = 4,
172 } });
173 defer storage.deinit(std.testing.allocator);
174 storage.activate();
175 try std.testing.expectError(
176 error.DecodedByteCapacityExceeded,
177 bytesFromStream(&storage, "FlateDecode", .{}, compressed),
178 );
179 try std.testing.expect(!storage.status().in_use);
180 const decoded = try bytesFromStream(&storage, null, .{}, "next");
181 defer storage.reset();
182 try std.testing.expectEqualStrings("next", decoded);
183 }
184
185 test "PDF filter rejects unsupported filters" {
186 var storage = try Storage.init(std.testing.allocator, .{ .bounds = .{
187 .input_bytes = 1,
188 .decoded_bytes = 1,
189 } });
190 defer storage.deinit(std.testing.allocator);
191 storage.activate();
192 try std.testing.expectError(
193 error.UnsupportedFilter,
194 bytesFromStream(&storage, "LZWDecode", .{}, ""),
195 );
196 }