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 }