Skip to documentation
SLOP

tiny.pdf.xref

Reference tiny.pdf xref

Defined in tiny.pdf.

API (6)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.pdfxref
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsDocumentdeinitxref.Tabledeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsDocumentinitprivate sourcelib.pdf.src.xrefparseTestprivate sourcelib.pdf.src.xrefparseSectionprivate sourcelib.pdf.src.xrefstartxrefxrefparse
Static calls · unresolved targets: 0 · external targets: 1.

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

Definitions7
Public names7
Members6
Version26.7.0
Revisiondaab053ee433