lib/deflate/src/frame.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const decode = @import("decode.zig");
3 const model = @import("model.zig");
4
5 fn readLittle16(input: []const u8) u16 {
6 return @as(u16, input[0]) | (@as(u16, input[1]) << 8);
7 }
8
9 fn readLittle32(input: []const u8) u32 {
10 return @as(u32, input[0]) |
11 (@as(u32, input[1]) << 8) |
12 (@as(u32, input[2]) << 16) |
13 (@as(u32, input[3]) << 24);
14 }
15
16 fn readBig32(input: []const u8) u32 {
17 return (@as(u32, input[0]) << 24) |
18 (@as(u32, input[1]) << 16) |
19 (@as(u32, input[2]) << 8) |
20 @as(u32, input[3]);
21 }
22
23 fn zlib(input: []const u8, output: []u8) model.Error!model.Result {
24 if (input.len < 6) return error.TruncatedInput;
25 const header = (@as(u16, input[0]) << 8) | input[1];
26 if ((input[0] & 0x0f) != 8 or (input[0] >> 4) > 7 or header % 31 != 0) {
27 return error.BadHeader;
28 }
29 if ((input[1] & 0x20) != 0) return error.PresetDictionaryUnsupported;
30 const payload = try decode.raw(input[2..], output);
31 const footer_start = 2 + payload.consumed;
32 if (footer_start > input.len or input.len - footer_start < 4) {
33 return error.TruncatedInput;
34 }
35 const expected = readBig32(input[footer_start..][0..4]);
36 if (std.hash.Adler32.hash(output[0..payload.written]) != expected) {
37 return error.BadChecksum;
38 }
39 return .{
40 .consumed = footer_start + 4,
41 .written = payload.written,
42 };
43 }
44
45 fn zeroTerminated(input: []const u8, start: usize) model.Error!usize {
46 const relative = std.mem.indexOfScalar(u8, input[start..], 0) orelse {
47 return error.TruncatedInput;
48 };
49 return start + relative + 1;
50 }
51
52 fn gzip(input: []const u8, output: []u8) model.Error!model.Result {
53 if (input.len < 18) return error.TruncatedInput;
54 if (input[0] != 0x1f or input[1] != 0x8b or input[2] != 8) {
55 return error.BadHeader;
56 }
57 const flags = input[3];
58 if ((flags & 0xe0) != 0) return error.BadHeader;
59 var index: usize = 10;
60 if ((flags & 0x04) != 0) {
61 if (input.len - index < 2) return error.TruncatedInput;
62 const extra_length = readLittle16(input[index..][0..2]);
63 index += 2;
64 if (extra_length > input.len - index) return error.TruncatedInput;
65 index += extra_length;
66 }
67 if ((flags & 0x08) != 0) index = try zeroTerminated(input, index);
68 if ((flags & 0x10) != 0) index = try zeroTerminated(input, index);
69 if ((flags & 0x02) != 0) {
70 if (input.len - index < 2) return error.TruncatedInput;
71 const expected = readLittle16(input[index..][0..2]);
72 const actual: u16 = @truncate(std.hash.Crc32.hash(input[0..index]));
73 if (actual != expected) return error.BadChecksum;
74 index += 2;
75 }
76 const payload = try decode.raw(input[index..], output);
77 const footer_start = index + payload.consumed;
78 if (footer_start > input.len or input.len - footer_start < 8) {
79 return error.TruncatedInput;
80 }
81 const expected_crc = readLittle32(input[footer_start..][0..4]);
82 const expected_size = readLittle32(input[footer_start + 4 ..][0..4]);
83 if (std.hash.Crc32.hash(output[0..payload.written]) != expected_crc) {
84 return error.BadChecksum;
85 }
86 if (@as(u32, @truncate(payload.written)) != expected_size) {
87 return error.BadSize;
88 }
89 return .{
90 .consumed = footer_start + 8,
91 .written = payload.written,
92 };
93 }
94
95 /// Expands one compressed stream with whatever header and trailer its format adds (a frame of
96 /// `input`), in the container that `format` names, into the front of `output`, so callers can
97 /// expand a compressed buffer, such as a PDF content stream, into memory they already own. The call
98 /// allocates nothing. The call returns the number of input bytes the frame used and the number of
99 /// bytes written to `output`. After a success, only the first `result.written` bytes of `output`
100 /// hold expanded data. After a failure, `output` may hold part of the expanded data. The zlib and
101 /// gzip checksums, and the gzip length, are checked before the call returns. The exact-input and
102 /// exact-output checks run after the frame and its checksum pass. With exact input on, which is the
103 /// default, bytes left after the frame fail with `error.TrailingInput`. With exact output on,
104 /// expanded data shorter than `output` fails with `error.BadSize`. A stream that needs more room
105 /// than `output` has fails with `error.OutputTooSmall`. Every other failure is one tag of `Error`
106 /// that describes the input. The PDF reader calls `decompress` with the zlib container and the
107 /// default options, and treats `error.OutputTooSmall` as its own capacity error.
108 pub fn decompress(
109 input: []const u8,
110 output: []u8,
111 format: model.Format,
112 options: model.Options,
113 ) model.Error!model.Result {
114 const result = switch (format) {
115 .raw => try decode.raw(input, output),
116 .zlib => try zlib(input, output),
117 .gzip => try gzip(input, output),
118 };
119 if (options.exact_input and result.consumed != input.len) {
120 return error.TrailingInput;
121 }
122 if (options.exact_output and result.written != output.len) {
123 return error.BadSize;
124 }
125 return result;
126 }