lib/png/src/decode/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const encode = @import("../encode/root.zig");
3 const model = @import("model.zig");
4
5 pub const Bounds = struct {
6 input_bytes: usize,
7 image_pixels: usize,
8 source_row_bytes: usize,
9 idat_bytes: usize,
10
11 pub fn exact(bytes: []const u8) Error!Bounds {
12 const plan = try Plan.inspect(bytes, .{
13 .input_bytes = bytes.len,
14 .image_pixels = std.math.maxInt(usize),
15 .source_row_bytes = std.math.maxInt(usize),
16 .idat_bytes = bytes.len,
17 });
18 return plan.exactBounds();
19 }
20 };
21
22 pub const Error = model.Error;
23
24 pub const Plan = struct {
25 input_bytes: usize,
26 input_hash: u64,
27 width: u32,
28 height: u32,
29 bit_depth: u8,
30 color_type: u8,
31 image_pixels: usize,
32 source_row_bytes: usize,
33 filter_bytes_per_pixel: usize,
34 raw_bytes: usize,
35 idat_bytes: usize,
36 rgba8_bytes: usize,
37 palette_offset: usize,
38 palette_bytes: usize,
39 transparency_offset: usize,
40 transparency_bytes: usize,
41 bounds: Bounds,
42
43 pub fn inspect(bytes: []const u8, bounds: Bounds) Error!Plan {
44 if (bytes.len > bounds.input_bytes) return error.InputByteCapacityExceeded;
45 if (bytes.len < encode.signature.len) return error.InvalidSignature;
46 if (!std.mem.eql(u8, bytes[0..encode.signature.len], &encode.signature)) {
47 return error.InvalidSignature;
48 }
49
50 var scan = Scan{};
51 var offset: usize = encode.signature.len;
52 while (bytes.len - offset >= 8) {
53 const chunk = try readChunk(bytes, offset);
54 const finished = try scan.observe(chunk, bounds);
55 offset = chunk.next_offset;
56 if (finished) break;
57 }
58 return scan.finish(bytes, bounds);
59 }
60
61 pub fn exactBounds(self: Plan) Bounds {
62 return .{
63 .input_bytes = self.input_bytes,
64 .image_pixels = self.image_pixels,
65 .source_row_bytes = self.source_row_bytes,
66 .idat_bytes = self.idat_bytes,
67 };
68 }
69
70 pub fn header(self: Plan) model.Header {
71 return .{
72 .width = self.width,
73 .height = self.height,
74 .bit_depth = self.bit_depth,
75 .color_type = self.color_type,
76 };
77 }
78
79 pub fn palette(self: Plan, bytes: []const u8) []const u8 {
80 return bytes[self.palette_offset..][0..self.palette_bytes];
81 }
82
83 pub fn transparency(self: Plan, bytes: []const u8) []const u8 {
84 return bytes[self.transparency_offset..][0..self.transparency_bytes];
85 }
86 };
87
88 const Chunk = struct {
89 kind: []const u8,
90 data: []const u8,
91 data_offset: usize,
92 next_offset: usize,
93 };
94
95 const Scan = struct {
96 header_value: ?model.Header = null,
97 palette_offset: usize = 0,
98 palette_bytes: usize = 0,
99 transparency_offset: usize = 0,
100 transparency_bytes: usize = 0,
101 idat_bytes: usize = 0,
102
103 fn observe(self: *Scan, chunk: Chunk, bounds: Bounds) Error!bool {
104 if (std.mem.eql(u8, chunk.kind, "IHDR")) {
105 if (chunk.data.len != 13) return error.InvalidChunk;
106 if (chunk.data[10] != 0 or chunk.data[11] != 0 or chunk.data[12] != 0) {
107 return error.UnsupportedFormat;
108 }
109 const header = model.Header{
110 .width = std.mem.readInt(u32, chunk.data[0..4], .big),
111 .height = std.mem.readInt(u32, chunk.data[4..8], .big),
112 .bit_depth = chunk.data[8],
113 .color_type = chunk.data[9],
114 };
115 if (!model.validDepth(header.color_type, header.bit_depth)) {
116 return error.UnsupportedFormat;
117 }
118 self.header_value = header;
119 } else if (std.mem.eql(u8, chunk.kind, "PLTE")) {
120 self.palette_offset = chunk.data_offset;
121 self.palette_bytes = chunk.data.len;
122 } else if (std.mem.eql(u8, chunk.kind, "tRNS")) {
123 self.transparency_offset = chunk.data_offset;
124 self.transparency_bytes = chunk.data.len;
125 } else if (std.mem.eql(u8, chunk.kind, "IDAT")) {
126 self.idat_bytes = try model.added(self.idat_bytes, chunk.data.len);
127 if (self.idat_bytes > bounds.idat_bytes) return error.IdatByteCapacityExceeded;
128 }
129 return std.mem.eql(u8, chunk.kind, "IEND");
130 }
131
132 fn finish(self: Scan, bytes: []const u8, bounds: Bounds) Error!Plan {
133 const header = self.header_value orelse return error.UnsupportedFormat;
134 if (header.width == 0 or header.height == 0) return error.UnsupportedFormat;
135 if (header.color_type == 3 and self.palette_bytes < 3) return error.MissingPalette;
136
137 const width = try model.toUsize(header.width);
138 const height = try model.toUsize(header.height);
139 const image_pixels = try model.multiplied(width, height);
140 if (image_pixels > bounds.image_pixels) return error.ImagePixelCapacityExceeded;
141 const sample_bits = try model.multiplied(
142 try model.multiplied(width, model.samplesPerPixel(header.color_type)),
143 header.bit_depth,
144 );
145 const source_row_bytes = try model.added(sample_bits, 7) / 8;
146 if (source_row_bytes > bounds.source_row_bytes) {
147 return error.SourceRowByteCapacityExceeded;
148 }
149 const stride = try model.added(source_row_bytes, 1);
150 return .{
151 .input_bytes = bytes.len,
152 .input_hash = std.hash.Wyhash.hash(0, bytes),
153 .width = header.width,
154 .height = header.height,
155 .bit_depth = header.bit_depth,
156 .color_type = header.color_type,
157 .image_pixels = image_pixels,
158 .source_row_bytes = source_row_bytes,
159 .filter_bytes_per_pixel = @max(
160 1,
161 model.samplesPerPixel(header.color_type) * header.bit_depth / 8,
162 ),
163 .raw_bytes = try model.multiplied(stride, height),
164 .idat_bytes = self.idat_bytes,
165 .rgba8_bytes = try model.multiplied(image_pixels, 4),
166 .palette_offset = self.palette_offset,
167 .palette_bytes = self.palette_bytes,
168 .transparency_offset = self.transparency_offset,
169 .transparency_bytes = self.transparency_bytes,
170 .bounds = bounds,
171 };
172 }
173 };
174
175 fn readChunk(bytes: []const u8, offset: usize) Error!Chunk {
176 const data_offset = try model.added(offset, 8);
177 if (data_offset > bytes.len) return error.InvalidChunk;
178 const length = try model.toUsize(std.mem.readInt(u32, bytes[offset..][0..4], .big));
179 const data_end = try model.added(data_offset, length);
180 const next_offset = try model.added(data_end, 4);
181 if (next_offset > bytes.len) return error.InvalidChunk;
182 return .{
183 .kind = bytes[offset + 4 ..][0..4],
184 .data = bytes[data_offset..data_end],
185 .data_offset = data_offset,
186 .next_offset = next_offset,
187 };
188 }
189
190 test "PNG decode planning accepts exact bounds and rejects each max plus one" {
191 comptime {
192 @stardustClaim(
193 @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "png_decode_boundaries"),
194 null,
195 null,
196 null,
197 null,
198 null,
199 null,
200 );
201 }
202
203 const witness = @import("fixture.zig").indexed;
204 const exact = try Bounds.exact(&witness);
205 _ = try Plan.inspect(&witness, exact);
206 var short = exact;
207 short.input_bytes -= 1;
208 try std.testing.expectError(error.InputByteCapacityExceeded, Plan.inspect(&witness, short));
209 short = exact;
210 short.image_pixels -= 1;
211 try std.testing.expectError(error.ImagePixelCapacityExceeded, Plan.inspect(&witness, short));
212 short = exact;
213 short.source_row_bytes -= 1;
214 try std.testing.expectError(
215 error.SourceRowByteCapacityExceeded,
216 Plan.inspect(&witness, short),
217 );
218 short = exact;
219 short.idat_bytes -= 1;
220 try std.testing.expectError(error.IdatByteCapacityExceeded, Plan.inspect(&witness, short));
221 }