lib/pdf/src/object.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 pub const ParseError = error{
  4     UnexpectedEnd,
  5     UnexpectedToken,
  6     BadNumber,
  7     BadName,
  8     BadString,
  9     BadDictionary,
 10     OutOfMemory,
 11 };
 12 
 13 pub const Reference = struct {
 14     number: u32,
 15     generation: u16,
 16 };
 17 
 18 pub const Entry = struct {
 19     key: []const u8,
 20     value: Value,
 21 };
 22 
 23 pub const Dict = struct {
 24     entries: []Entry = &.{},
 25 
 26     pub fn get(self: Dict, key: []const u8) ?Value {
 27         for (self.entries) |entry| {
 28             if (std.mem.eql(u8, entry.key, key)) return entry.value;
 29         }
 30         return null;
 31     }
 32 };
 33 
 34 pub const StringKind = enum {
 35     literal,
 36     hex,
 37 };
 38 
 39 pub const String = struct {
 40     kind: StringKind,
 41     raw: []const u8,
 42 };
 43 
 44 pub const Value = union(enum) {
 45     null,
 46     boolean: bool,
 47     integer: i64,
 48     real: f64,
 49     name: []const u8,
 50     string: String,
 51     array: []Value,
 52     dict: Dict,
 53     reference: Reference,
 54 };
 55 
 56 const NumberToken = union(enum) {
 57     integer: i64,
 58     real: f64,
 59 };
 60 
 61 pub const Parser = struct {
 62     bytes: []const u8,
 63     pos: usize,
 64 
 65     pub fn init(bytes: []const u8, pos: usize) Parser {
 66         return .{ .bytes = bytes, .pos = pos };
 67     }
 68 
 69     pub fn skipWhitespace(self: *Parser) void {
 70         if (self.pos >= self.bytes.len) return;
 71         for (self.bytes[self.pos..]) |_| {
 72             if (self.pos >= self.bytes.len) return;
 73             const byte = self.bytes[self.pos];
 74             if (whitespace(byte)) {
 75                 self.pos += 1;
 76                 continue;
 77             }
 78             if (byte == '%') {
 79                 while (self.pos < self.bytes.len and self.bytes[self.pos] != '\n' and self.bytes[self.pos] != '\r') self.pos += 1;
 80                 continue;
 81             }
 82             return;
 83         }
 84     }
 85 
 86     pub fn parseValue(self: *Parser, arena: std.mem.Allocator) ParseError!Value {
 87         self.skipWhitespace();
 88         if (self.pos >= self.bytes.len) return error.UnexpectedEnd;
 89         const byte = self.bytes[self.pos];
 90         switch (byte) {
 91             '/' => return .{ .name = try self.parseName() },
 92             '(' => return .{ .string = .{ .kind = .literal, .raw = try self.parseLiteralString() } },
 93             '[' => return .{ .array = try self.parseArray(arena) },
 94             '<' => {
 95                 if (self.pos + 1 < self.bytes.len and self.bytes[self.pos + 1] == '<') {
 96                     return .{ .dict = try self.parseDict(arena) };
 97                 }
 98                 return .{ .string = .{ .kind = .hex, .raw = try self.parseHexString() } };
 99             },
100             't', 'f', 'n' => return try self.parseKeywordValue(),
101             else => return try self.parseNumberOrReference(),
102         }
103     }
104 
105     pub fn expectKeyword(self: *Parser, keyword: []const u8) ParseError!void {
106         self.skipWhitespace();
107         if (self.pos + keyword.len > self.bytes.len) return error.UnexpectedEnd;
108         if (!std.mem.eql(u8, self.bytes[self.pos .. self.pos + keyword.len], keyword)) return error.UnexpectedToken;
109         self.pos += keyword.len;
110     }
111 
112     pub fn atKeyword(self: *Parser, keyword: []const u8) bool {
113         self.skipWhitespace();
114         if (self.pos + keyword.len > self.bytes.len) return false;
115         if (!std.mem.eql(u8, self.bytes[self.pos .. self.pos + keyword.len], keyword)) return false;
116         const end = self.pos + keyword.len;
117         if (end < self.bytes.len and !delimiterOrWhitespace(self.bytes[end])) return false;
118         return true;
119     }
120 
121     pub fn streamStart(self: *Parser) ParseError!usize {
122         if (!self.atKeyword("stream")) return error.UnexpectedToken;
123         self.pos += "stream".len;
124         if (self.pos < self.bytes.len and self.bytes[self.pos] == '\r') self.pos += 1;
125         if (self.pos < self.bytes.len and self.bytes[self.pos] == '\n') self.pos += 1;
126         return self.pos;
127     }
128 
129     pub fn parseUnsigned(self: *Parser, comptime T: type) ParseError!T {
130         self.skipWhitespace();
131         const start = self.pos;
132         while (self.pos < self.bytes.len and std.ascii.isDigit(self.bytes[self.pos])) self.pos += 1;
133         if (self.pos == start) return error.BadNumber;
134         return std.fmt.parseUnsigned(T, self.bytes[start..self.pos], 10) catch error.BadNumber;
135     }
136 
137     fn parseName(self: *Parser) ParseError![]const u8 {
138         self.pos += 1;
139         const start = self.pos;
140         while (self.pos < self.bytes.len and !delimiterOrWhitespace(self.bytes[self.pos])) self.pos += 1;
141         if (self.pos == start) return error.BadName;
142         return self.bytes[start..self.pos];
143     }
144 
145     fn parseLiteralString(self: *Parser) ParseError![]const u8 {
146         self.pos += 1;
147         const start = self.pos;
148         var depth: usize = 1;
149         for (self.bytes[self.pos..]) |_| {
150             if (self.pos >= self.bytes.len) break;
151             const byte = self.bytes[self.pos];
152             if (byte == '\\') {
153                 self.pos += @min(2, self.bytes.len - self.pos);
154                 continue;
155             }
156             if (byte == '(') depth += 1;
157             if (byte == ')') {
158                 depth -= 1;
159                 if (depth == 0) {
160                     const raw = self.bytes[start..self.pos];
161                     self.pos += 1;
162                     return raw;
163                 }
164             }
165             self.pos += 1;
166         }
167         return error.BadString;
168     }
169 
170     fn parseHexString(self: *Parser) ParseError![]const u8 {
171         self.pos += 1;
172         const start = self.pos;
173         while (self.pos < self.bytes.len and self.bytes[self.pos] != '>') self.pos += 1;
174         if (self.pos >= self.bytes.len) return error.BadString;
175         const raw = self.bytes[start..self.pos];
176         self.pos += 1;
177         return raw;
178     }
179 
180     fn parseArray(self: *Parser, arena: std.mem.Allocator) ParseError![]Value {
181         self.pos += 1;
182         var items: std.ArrayList(Value) = .empty;
183         for (self.bytes[self.pos..]) |_| {
184             self.skipWhitespace();
185             if (self.pos >= self.bytes.len) return error.UnexpectedEnd;
186             if (self.bytes[self.pos] == ']') {
187                 self.pos += 1;
188                 return try items.toOwnedSlice(arena);
189             }
190             try items.append(arena, try self.parseValue(arena));
191         }
192         return error.UnexpectedEnd;
193     }
194 
195     fn parseDict(self: *Parser, arena: std.mem.Allocator) ParseError!Dict {
196         self.pos += 2;
197         var entries: std.ArrayList(Entry) = .empty;
198         for (self.bytes[self.pos..]) |_| {
199             self.skipWhitespace();
200             if (self.pos + 1 < self.bytes.len and self.bytes[self.pos] == '>' and self.bytes[self.pos + 1] == '>') {
201                 self.pos += 2;
202                 return .{ .entries = try entries.toOwnedSlice(arena) };
203             }
204             if (self.pos >= self.bytes.len) return error.UnexpectedEnd;
205             if (self.bytes[self.pos] != '/') return error.BadDictionary;
206             const key = try self.parseName();
207             const value = try self.parseValue(arena);
208             try entries.append(arena, .{ .key = key, .value = value });
209         }
210         return error.UnexpectedEnd;
211     }
212 
213     fn parseKeywordValue(self: *Parser) ParseError!Value {
214         if (self.atKeyword("true")) {
215             self.pos += 4;
216             return .{ .boolean = true };
217         }
218         if (self.atKeyword("false")) {
219             self.pos += 5;
220             return .{ .boolean = false };
221         }
222         if (self.atKeyword("null")) {
223             self.pos += 4;
224             return .null;
225         }
226         return error.UnexpectedToken;
227     }
228 
229     fn parseNumberOrReference(self: *Parser) ParseError!Value {
230         const first = try self.parseNumberToken();
231         switch (first) {
232             .real => |value| return .{ .real = value },
233             .integer => |value| {
234                 const saved = self.pos;
235                 if (self.tryReferenceSuffix()) |generation| {
236                     if (value >= 0 and value <= std.math.maxInt(u32)) {
237                         return .{ .reference = .{ .number = @intCast(value), .generation = generation } };
238                     }
239                 }
240                 self.pos = saved;
241                 return .{ .integer = value };
242             },
243         }
244     }
245 
246     fn parseNumberToken(self: *Parser) ParseError!NumberToken {
247         self.skipWhitespace();
248         const start = self.pos;
249         var saw_dot = false;
250         if (self.pos < self.bytes.len and (self.bytes[self.pos] == '+' or self.bytes[self.pos] == '-')) self.pos += 1;
251         for (self.bytes[self.pos..]) |byte| {
252             if (std.ascii.isDigit(byte)) {
253                 self.pos += 1;
254                 continue;
255             }
256             if (byte == '.' and !saw_dot) {
257                 saw_dot = true;
258                 self.pos += 1;
259                 continue;
260             }
261             break;
262         }
263         const text = self.bytes[start..self.pos];
264         if (text.len == 0 or std.mem.eql(u8, text, "+") or std.mem.eql(u8, text, "-")) return error.BadNumber;
265         if (saw_dot) {
266             return .{ .real = std.fmt.parseFloat(f64, text) catch return error.BadNumber };
267         }
268         return .{ .integer = std.fmt.parseInt(i64, text, 10) catch return error.BadNumber };
269     }
270 
271     fn tryReferenceSuffix(self: *Parser) ?u16 {
272         const saved = self.pos;
273         self.skipWhitespace();
274         const generation_start = self.pos;
275         while (self.pos < self.bytes.len and std.ascii.isDigit(self.bytes[self.pos])) self.pos += 1;
276         if (self.pos == generation_start) {
277             self.pos = saved;
278             return null;
279         }
280         const generation = std.fmt.parseUnsigned(u16, self.bytes[generation_start..self.pos], 10) catch {
281             self.pos = saved;
282             return null;
283         };
284         self.skipWhitespace();
285         if (self.pos < self.bytes.len and self.bytes[self.pos] == 'R') {
286             const after = self.pos + 1;
287             if (after >= self.bytes.len or delimiterOrWhitespace(self.bytes[after])) {
288                 self.pos = after;
289                 return generation;
290             }
291         }
292         self.pos = saved;
293         return null;
294     }
295 };
296 
297 pub fn whitespace(byte: u8) bool {
298     return byte == ' ' or byte == '\t' or byte == '\r' or byte == '\n' or byte == '\x0C' or byte == 0;
299 }
300 
301 pub fn delimiterOrWhitespace(byte: u8) bool {
302     return whitespace(byte) or switch (byte) {
303         '(', ')', '<', '>', '[', ']', '{', '}', '/', '%' => true,
304         else => false,
305     };
306 }
307 
308 pub fn decodeLiteralAppend(allocator: std.mem.Allocator, out: *std.ArrayList(u8), raw: []const u8) !void {
309     var index: usize = 0;
310     for (raw) |_| {
311         if (index >= raw.len) break;
312         const byte = raw[index];
313         if (byte != '\\') {
314             try out.append(allocator, byte);
315             index += 1;
316             continue;
317         }
318         index += 1;
319         if (index >= raw.len) break;
320         const escaped = raw[index];
321         switch (escaped) {
322             'n' => try out.append(allocator, '\n'),
323             'r' => try out.append(allocator, '\r'),
324             't' => try out.append(allocator, '\t'),
325             'b' => try out.append(allocator, 0x08),
326             'f' => try out.append(allocator, 0x0C),
327             '(', ')', '\\' => try out.append(allocator, escaped),
328             '\n' => {},
329             '\r' => {
330                 if (index + 1 < raw.len and raw[index + 1] == '\n') index += 1;
331             },
332             '0'...'7' => {
333                 var code: u16 = 0;
334                 var digits: usize = 0;
335                 while (digits < 3 and index < raw.len and raw[index] >= '0' and raw[index] <= '7') {
336                     code = code * 8 + (raw[index] - '0');
337                     index += 1;
338                     digits += 1;
339                 }
340                 try out.append(allocator, @truncate(code));
341                 continue;
342             },
343             else => try out.append(allocator, escaped),
344         }
345         index += 1;
346     }
347 }
348 
349 pub fn decodeHexAppend(allocator: std.mem.Allocator, out: *std.ArrayList(u8), raw: []const u8) !void {
350     var high: ?u8 = null;
351     for (raw) |byte| {
352         const nibble: u8 = switch (byte) {
353             '0'...'9' => byte - '0',
354             'a'...'f' => byte - 'a' + 10,
355             'A'...'F' => byte - 'A' + 10,
356             else => continue,
357         };
358         if (high) |value| {
359             try out.append(allocator, (value << 4) | nibble);
360             high = null;
361         } else {
362             high = nibble;
363         }
364     }
365     if (high) |value| try out.append(allocator, value << 4);
366 }
367 
368 test "parser reads scalar values names strings and references" {
369     var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
370     defer arena_state.deinit();
371     const arena = arena_state.allocator();
372 
373     const source =
374         "% lead comment\n" ++
375         "<< /Type /Page /Count 3 /Rot -90 /Scale 1.5 /Parent 2 0 R " ++
376         "/Label (hi \\(there\\)) /Id <48690A> /Flags [true false null] >>";
377     var parser = Parser.init(source, 0);
378     const value = try parser.parseValue(arena);
379     const dict = value.dict;
380     try std.testing.expectEqualStrings("Page", dict.get("Type").?.name);
381     try std.testing.expectEqual(@as(i64, 3), dict.get("Count").?.integer);
382     try std.testing.expectEqual(@as(i64, -90), dict.get("Rot").?.integer);
383     try std.testing.expectEqual(@as(f64, 1.5), dict.get("Scale").?.real);
384     try std.testing.expectEqual(@as(u32, 2), dict.get("Parent").?.reference.number);
385     try std.testing.expectEqualStrings("hi \\(there\\)", dict.get("Label").?.string.raw);
386     try std.testing.expectEqual(StringKind.hex, dict.get("Id").?.string.kind);
387     try std.testing.expectEqual(@as(usize, 3), dict.get("Flags").?.array.len);
388     var literal = Parser.init("(", 0);
389     try std.testing.expectError(error.BadString, literal.parseValue(arena));
390     var array = Parser.init("[1", 0);
391     try std.testing.expectError(error.UnexpectedEnd, array.parseValue(arena));
392     var truncated_dict = Parser.init("<< /A 1", 0);
393     try std.testing.expectError(error.UnexpectedEnd, truncated_dict.parseValue(arena));
394     var comment = Parser.init("% trailing comment", 0);
395     comment.skipWhitespace();
396     try std.testing.expectEqual(comment.bytes.len, comment.pos);
397 }
398 
399 test "literal and hex decoding handle escapes and odd digits" {
400     var out = std.ArrayList(u8).empty;
401     defer out.deinit(std.testing.allocator);
402     try decodeLiteralAppend(std.testing.allocator, &out, "A\\nB\\051C\\\\D");
403     try std.testing.expectEqualStrings("A\nB)C\\D", out.items);
404     out.clearRetainingCapacity();
405     try decodeLiteralAppend(std.testing.allocator, &out, "A\\");
406     try std.testing.expectEqualStrings("A", out.items);
407     out.clearRetainingCapacity();
408     try decodeHexAppend(std.testing.allocator, &out, "48 65 6C 6C 6F 2");
409     try std.testing.expectEqualStrings("Hello ", out.items);
410 }