tiny.pdf.xref
Defined in tiny.pdf.
API (6)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/pdf/src/root.zig:16
zig
pub const xref = @import("xref.zig");Source: lib/pdf/src/xref.zig
zig
const std = @import("std");const filter = @import("filter/root.zig");const object = @import("object.zig");const XrefFormatError = error{ MissingStartxref, MissingXrefTable, MissingTrailerRoot, BadXrefEntry, BadXrefStream,};pub const XrefError = XrefFormatError || object.ParseError || filter.Error;pub const Compressed = struct { container: u32, index: u32,};pub const Location = union(enum) { offset: usize, compressed: Compressed,};pub const Table = struct { locations: std.AutoHashMapUnmanaged(u32, Location) = .empty, root: object.Reference, pub fn deinit(self: *Table, allocator: std.mem.Allocator) void { self.locations.deinit(allocator); }};pub fn parse( allocator: std.mem.Allocator, arena: std.mem.Allocator, filter_storage: *filter.Storage, bytes: []const u8,) XrefError!Table { var table = Table{ .root = .{ .number = 0, .generation = 0 } }; errdefer table.deinit(allocator); var root: ?object.Reference = null; var offset: ?usize = try startxref(bytes); var guard: usize = 0; while (offset) |section_offset| { guard += 1; if (guard > 64) break; offset = try parseSection( allocator, arena, filter_storage, bytes, section_offset, &table, &root, ); } table.root = root orelse return error.MissingTrailerRoot; return table;}fn startxref(bytes: []const u8) XrefError!usize { const window_start = bytes.len -| 256; const window = bytes[window_start..]; const found = std.mem.lastIndexOf(u8, window, "startxref") orelse return error.MissingStartxref; var parser = object.Parser.init(bytes, window_start + found + "startxref".len); return parser.parseUnsigned(usize) catch error.MissingStartxref;}fn parseSection( allocator: std.mem.Allocator, arena: std.mem.Allocator, filter_storage: *filter.Storage, bytes: []const u8, section_offset: usize, table: *Table, root: *?object.Reference,) XrefError!?usize { if (section_offset >= bytes.len) return error.MissingXrefTable; var parser = object.Parser.init(bytes, section_offset); if (parser.atKeyword("xref")) { return parseTableSection( allocator, arena, filter_storage, bytes, &parser, table, root, ); } return parseStreamSection( allocator, arena, filter_storage, bytes, section_offset, table, root, );}fn register(allocator: std.mem.Allocator, table: *Table, number: u32, location: Location) XrefError!void { const slot = table.locations.getOrPut(allocator, number) catch return error.OutOfMemory; if (!slot.found_existing) slot.value_ptr.* = location;}const TableEntry = struct { number: u32, offset: usize,};fn parseTableSection( allocator: std.mem.Allocator, arena: std.mem.Allocator, filter_storage: *filter.Storage, bytes: []const u8, parser: *object.Parser, table: *Table, root: *?object.Reference,) XrefError!?usize { try parser.expectKeyword("xref"); var scratch: std.ArrayList(TableEntry) = .empty; defer scratch.deinit(allocator); for (bytes[parser.pos..]) |_| { if (parser.atKeyword("trailer")) break; const first = try parser.parseUnsigned(u32); const count = try parser.parseUnsigned(u32); parser.skipWhitespace(); var index: u32 = 0; while (index < count) : (index += 1) { if (parser.pos + 18 > bytes.len) return error.BadXrefEntry; const entry = bytes[parser.pos .. parser.pos + 18]; const entry_offset = std.fmt.parseUnsigned(usize, entry[0..10], 10) catch return error.BadXrefEntry; const kind = entry[17]; const number = std.math.add(u32, first, index) catch return error.BadXrefEntry; if (kind == 'n') scratch.append(allocator, .{ .number = number, .offset = entry_offset }) catch return error.OutOfMemory; parser.pos += 18; while (parser.pos < bytes.len and object.whitespace(bytes[parser.pos])) parser.pos += 1; } } if (!parser.atKeyword("trailer")) return error.BadNumber; try parser.expectKeyword("trailer"); const trailer = switch (try parser.parseValue(arena)) { .dict => |dict| dict, else => return error.MissingXrefTable, }; if (trailer.get("XRefStm")) |value| { switch (value) { .integer => |stream_offset| if (stream_offset >= 0) { _ = try parseStreamSection( allocator, arena, filter_storage, bytes, @intCast(stream_offset), table, root, ); }, else => {}, } } for (scratch.items) |entry| try register(allocator, table, entry.number, .{ .offset = entry.offset }); applyRoot(trailer, root); return previousOffset(trailer);}fn parseStreamSection( allocator: std.mem.Allocator, arena: std.mem.Allocator, filter_storage: *filter.Storage, bytes: []const u8, section_offset: usize, table: *Table, root: *?object.Reference,) XrefError!?usize { if (section_offset >= bytes.len) return error.MissingXrefTable; var parser = object.Parser.init(bytes, section_offset); _ = parser.parseUnsigned(u32) catch return error.BadXrefStream; _ = parser.parseUnsigned(u16) catch return error.BadXrefStream; parser.expectKeyword("obj") catch return error.BadXrefStream; const dict = switch (try parser.parseValue(arena)) { .dict => |dict| dict, else => return error.BadXrefStream, }; const type_value = dict.get("Type") orelse return error.BadXrefStream; switch (type_value) { .name => |name| if (!std.mem.eql(u8, name, "XRef")) return error.BadXrefStream, else => return error.BadXrefStream, } const length = directUnsigned(dict, "Length") orelse return error.BadXrefStream; const data_start = parser.streamStart() catch return error.BadXrefStream; if (data_start + length > bytes.len) return error.BadXrefStream; const filter_name = try filter.nameOf(try filter.single(dict.get("Filter") orelse .null)); const params = try filter.paramsOf(try filter.single(dict.get("DecodeParms") orelse .null)); const decoded = try filter.bytesFromStream( filter_storage, filter_name, params, bytes[data_start .. data_start + length], ); defer filter_storage.reset(); const widths = try fieldWidths(dict); const size = directUnsigned(dict, "Size") orelse return error.BadXrefStream; var cursor: usize = 0; if (dict.get("Index")) |index_value| { const items = switch (index_value) { .array => |array| array, else => return error.BadXrefStream, }; if (items.len % 2 != 0) return error.BadXrefStream; var pair: usize = 0; while (pair < items.len) : (pair += 2) { const first = valueUnsigned(items[pair]) orelse return error.BadXrefStream; const count = valueUnsigned(items[pair + 1]) orelse return error.BadXrefStream; try decodeEntries(allocator, table, decoded, &cursor, widths, first, count); } } else { try decodeEntries(allocator, table, decoded, &cursor, widths, 0, size); } applyRoot(dict, root); return previousOffset(dict);}fn fieldWidths(dict: object.Dict) XrefError![3]usize { const value = dict.get("W") orelse return error.BadXrefStream; const items = switch (value) { .array => |array| array, else => return error.BadXrefStream, }; if (items.len != 3) return error.BadXrefStream; var widths: [3]usize = undefined; for (items, 0..) |item, index| { widths[index] = switch (item) { .integer => |raw| if (raw >= 0 and raw <= 8) @intCast(raw) else return error.BadXrefStream, else => return error.BadXrefStream, }; } if (widths[0] + widths[1] + widths[2] == 0) return error.BadXrefStream; return widths;}fn decodeEntries(allocator: std.mem.Allocator, table: *Table, decoded: []const u8, cursor: *usize, widths: [3]usize, first: usize, count: usize) XrefError!void { const row_len = widths[0] + widths[1] + widths[2]; var index: usize = 0; while (index < count) : (index += 1) { if (cursor.* + row_len > decoded.len) return error.BadXrefStream; const row = decoded[cursor.* .. cursor.* + row_len]; cursor.* += row_len; const kind: u64 = if (widths[0] == 0) 1 else readBigEndian(row[0..widths[0]]); const second = readBigEndian(row[widths[0] .. widths[0] + widths[1]]); const third = readBigEndian(row[widths[0] + widths[1] ..]); const number = std.math.cast(u32, first + index) orelse return error.BadXrefStream; switch (kind) { 0 => {}, 1 => try register(allocator, table, number, .{ .offset = std.math.cast(usize, second) orelse return error.BadXrefStream, }), 2 => try register(allocator, table, number, .{ .compressed = .{ .container = std.math.cast(u32, second) orelse return error.BadXrefStream, .index = std.math.cast(u32, third) orelse return error.BadXrefStream, } }), else => {}, } }}fn readBigEndian(bytes: []const u8) u64 { var value: u64 = 0; for (bytes) |byte| value = (value << 8) | byte; return value;}fn directUnsigned(dict: object.Dict, key: []const u8) ?usize { return valueUnsigned(dict.get(key) orelse return null);}fn valueUnsigned(value: object.Value) ?usize { return switch (value) { .integer => |raw| if (raw >= 0) std.math.cast(usize, raw) else null, else => null, };}fn applyRoot(trailer: object.Dict, root: *?object.Reference) void { if (root.* != null) return; const value = trailer.get("Root") orelse return; switch (value) { .reference => |reference| root.* = reference, else => {}, }}fn previousOffset(trailer: object.Dict) ?usize { const value = trailer.get("Prev") orelse return null; return valueUnsigned(value);}fn parseTest( allocator: std.mem.Allocator, arena: std.mem.Allocator, bytes: []const u8,) !Table { var storage = try filter.Storage.init(allocator, .{ .bounds = .{ .input_bytes = bytes.len, .decoded_bytes = bytes.len, } }); defer storage.deinit(allocator); storage.activate(); return parse(allocator, arena, &storage, bytes);}test "xref parses table entries trailer root and prev chains" { const body = "%PDF-1.4\n" ++ "xref\n0 3\n" ++ "0000000000 65535 f \n" ++ "0000000009 00000 n \n" ++ "0000000100 00000 n \n" ++ "trailer\n<< /Size 3 /Root 1 0 R >>\n" ++ "startxref\n9\n%%EOF"; var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); var table = try parseTest(std.testing.allocator, arena_state.allocator(), body); defer table.deinit(std.testing.allocator); try std.testing.expectEqual(@as(u32, 1), table.root.number); try std.testing.expectEqual(@as(usize, 9), table.locations.get(1).?.offset); try std.testing.expectEqual(@as(usize, 100), table.locations.get(2).?.offset); try std.testing.expect(table.locations.get(0) == null); const missing_trailer = "%PDF-1.4\nxref\n0 0\nstartxref\n9\n%%EOF"; try std.testing.expectError( error.BadNumber, parseTest(std.testing.allocator, arena_state.allocator(), missing_trailer), );}fn xrefStreamBodyAlloc(allocator: std.mem.Allocator, dict_fragment: []const u8, rows: []const u8) ![]u8 { var out = std.ArrayList(u8).empty; errdefer out.deinit(allocator); const head = try std.fmt.allocPrint(allocator, "9 0 obj\n<< /Type /XRef /Length {d} {s} >>\nstream\n", .{ rows.len, dict_fragment }); defer allocator.free(head); try out.appendSlice(allocator, head); try out.appendSlice(allocator, rows); try out.appendSlice(allocator, "\nendstream\nendobj\n"); return out.toOwnedSlice(allocator);}test "xref stream decodes typed entries with explicit index" { const rows = [_]u8{ 1, 0, 64, 0, 2, 0, 7, 3, 0, 0, 0, 0, 1, 0, 200, 1, }; const section = try xrefStreamBodyAlloc( std.testing.allocator, "/Size 12 /W [1 2 1] /Index [4 2 10 2] /Root 4 0 R", &rows, ); defer std.testing.allocator.free(section); const body = try std.mem.concat(std.testing.allocator, u8, &.{ "%PDF-1.5\n", section, "startxref\n9\n%%EOF" }); defer std.testing.allocator.free(body); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); var table = try parseTest(std.testing.allocator, arena_state.allocator(), body); defer table.deinit(std.testing.allocator); try std.testing.expectEqual(@as(u32, 4), table.root.number); try std.testing.expectEqual(@as(usize, 64), table.locations.get(4).?.offset); try std.testing.expectEqual(@as(u32, 7), table.locations.get(5).?.compressed.container); try std.testing.expectEqual(@as(u32, 3), table.locations.get(5).?.compressed.index); try std.testing.expect(table.locations.get(10) == null); try std.testing.expectEqual(@as(usize, 200), table.locations.get(11).?.offset);}test "xref stream defaults index to whole size and honors zero width types" { const rows = [_]u8{ 0, 0, 30, 0, 60, 2, }; const section = try xrefStreamBodyAlloc( std.testing.allocator, "/Size 3 /W [0 1 1] /Root 1 0 R", &rows, ); defer std.testing.allocator.free(section); const body = try std.mem.concat(std.testing.allocator, u8, &.{ "%PDF-1.5\n", section, "startxref\n9\n%%EOF" }); defer std.testing.allocator.free(body); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); var table = try parseTest(std.testing.allocator, arena_state.allocator(), body); defer table.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), table.locations.get(0).?.offset); try std.testing.expectEqual(@as(usize, 30), table.locations.get(1).?.offset); try std.testing.expectEqual(@as(usize, 60), table.locations.get(2).?.offset);}test "hybrid tables read the xref stream before their own entries" { var body = std.ArrayList(u8).empty; defer body.deinit(std.testing.allocator); try body.appendSlice(std.testing.allocator, "%PDF-1.4\n"); const stream_offset = body.items.len; const rows = [_]u8{ 2, 0, 9, 0 }; const stream_body = try xrefStreamBodyAlloc( std.testing.allocator, "/Size 6 /W [1 2 1] /Index [5 1] /Root 2 0 R", &rows, ); defer std.testing.allocator.free(stream_body); try body.appendSlice(std.testing.allocator, stream_body); const table_offset = body.items.len; const classic = try std.fmt.allocPrint( std.testing.allocator, "xref\n5 1\n0000000777 00000 n \n" ++ "trailer\n<< /Size 6 /Root 2 0 R /XRefStm {d} >>\nstartxref\n{d}\n%%EOF", .{ stream_offset, table_offset }, ); defer std.testing.allocator.free(classic); try body.appendSlice(std.testing.allocator, classic); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); var table = try parseTest(std.testing.allocator, arena_state.allocator(), body.items); defer table.deinit(std.testing.allocator); try std.testing.expectEqual(@as(u32, 9), table.locations.get(5).?.compressed.container);}test "malformed xref streams fail typed" { const missing_w = "%PDF-1.5\n9 0 obj\n<< /Type /XRef /Size 2 /Length 0 /Root 1 0 R >>\nstream\n\nendstream\nendobj\nstartxref\n9\n%%EOF"; var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); try std.testing.expectError( error.BadXrefStream, parseTest(std.testing.allocator, arena_state.allocator(), missing_w), ); const not_xref = "%PDF-1.5\n9 0 obj\n<< /Type /Font >>\nendobj\nstartxref\n9\n%%EOF"; try std.testing.expectError( error.BadXrefStream, parseTest(std.testing.allocator, arena_state.allocator(), not_xref), );}Audit
| Definitions | 7 |
|---|---|
| Public names | 7 |
| Members | 6 |
| Version | 26.7.0 |
| Revision | daab053ee433 |