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 }