initial commit
This commit is contained in:
+552
@@ -0,0 +1,552 @@
|
||||
package pdf
|
||||
|
||||
import "core:mem"
|
||||
import "core:mem/virtual"
|
||||
import "core:os"
|
||||
import "core:slice"
|
||||
import "core:strings"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Limits
|
||||
//
|
||||
// Every one of these bounds something an attacker (or a fuzzer, or a merely
|
||||
// broken producer) controls. They are deliberately generous -- real documents
|
||||
// stay far below them -- and exist only so that a malformed file fails instead
|
||||
// of allocating the machine to death or spinning forever.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Nested arrays and dictionaries, and form-XObject recursion.
|
||||
MAX_DEPTH :: 32
|
||||
// Object numbers above this are rejected outright (ISO 32000-2, 7.5.4 caps
|
||||
// them at 8388607 for a reason: the xref table is indexed by them).
|
||||
MAX_OBJECT_NUMBER :: 8388607
|
||||
// /Prev hops through the cross-reference chain.
|
||||
MAX_XREF_CHAIN :: 64
|
||||
// Operands stacked before a content operator; the format never needs more
|
||||
// than 8 apart from `d0`/`d1` and the shading operators.
|
||||
MAX_CONTENT_OPERANDS :: 32
|
||||
// 268M pixels, about 16k x 16k. Above this `render_page` refuses.
|
||||
MAX_BITMAP_PIXELS :: 1 << 28
|
||||
// Ref chains: "1 0 R" pointing at "2 0 R" pointing at ... Legal, but only
|
||||
// ever seen in files trying to make a reader loop.
|
||||
MAX_REF_CHAIN :: 32
|
||||
// How deeply one object's definition may require another's before the library
|
||||
// stops following. This is *nesting*, not chaining: it bounds only the cases
|
||||
// where parsing object A cannot finish until object B is parsed -- an indirect
|
||||
// /Length, an object stream's container, an indirect /Filter. Legitimate files
|
||||
// nest two or three deep. Without this bound a file can name an acyclic chain
|
||||
// of 100000 such objects and overflow the native stack, which the cycle
|
||||
// guards cannot catch because no object number ever repeats.
|
||||
MAX_LOAD_DEPTH :: 64
|
||||
// Bytes an object stream is allowed to expand to.
|
||||
MAX_OBJSTM_SIZE :: 1 << 30
|
||||
// Repairs recorded before the library stops bothering; a file this broken is
|
||||
// not going to be diagnosed by reading more of the list.
|
||||
MAX_REPAIRS :: 4096
|
||||
// Pages one document may contain. The largest real documents run to a few
|
||||
// tens of thousands; a /Kids graph that yields more than this is a file
|
||||
// trying to make the page-tree walk allocate without end.
|
||||
MAX_PAGES :: 1 << 20
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cross-reference table
|
||||
//
|
||||
// Declared here rather than in xref.odin because the table is the Document's
|
||||
// own state; xref.odin holds the algorithms that fill it in.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Xref_Kind :: enum u8 {
|
||||
// Never defined, or on the free list. Reads as null.
|
||||
Free = 0,
|
||||
// Lives at `offset` in the file as "num gen obj ... endobj".
|
||||
Normal,
|
||||
// Lives inside object stream `container` at index `index`.
|
||||
Compressed,
|
||||
}
|
||||
|
||||
Xref_Entry :: struct {
|
||||
offset: i64, // Normal: byte offset in the file
|
||||
container: u32, // Compressed: object number of the /ObjStm
|
||||
index: u32, // Compressed: position within that stream
|
||||
gen: u16,
|
||||
kind: Xref_Kind,
|
||||
// Set once the entry has been read and its object cached, so a second
|
||||
// lookup skips straight to the cache.
|
||||
cached: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Open_Options :: struct {
|
||||
// User or owner password for an encrypted document. Empty is correct for
|
||||
// the common case of a file encrypted with an empty user password -- which
|
||||
// is what "restricted permissions, opens without prompting" means.
|
||||
password: string,
|
||||
// When true the Document takes ownership of the byte slice handed to
|
||||
// `open_bytes` and frees it in `close`. `open_file` sets this.
|
||||
take_ownership: bool,
|
||||
// Skip the page-tree walk. Useful for tools that only want to inspect
|
||||
// objects, and for opening documents whose page tree is broken.
|
||||
skip_page_tree: bool,
|
||||
// Allocator for the Document handle itself, and for the file bytes when
|
||||
// `open_file` reads them.
|
||||
//
|
||||
// It is deliberately NOT where parsed objects come from: those live in a
|
||||
// growing virtual-memory arena that takes pages from the OS directly, so
|
||||
// that a large document costs one address-space reservation rather than
|
||||
// thousands of small allocations. A caller that needs to account for a
|
||||
// document's real footprint should read `document_memory_used`, not
|
||||
// instrument this allocator.
|
||||
allocator: mem.Allocator,
|
||||
}
|
||||
|
||||
Document :: struct {
|
||||
// The whole file. Object offsets index into this directly, and most
|
||||
// strings and stream payloads are sub-slices of it rather than copies.
|
||||
data: []byte,
|
||||
owns_data: bool,
|
||||
backing: mem.Allocator,
|
||||
|
||||
// Everything parsed out of the file lives here and dies with `close`.
|
||||
arena: virtual.Arena,
|
||||
allocator: mem.Allocator,
|
||||
|
||||
version_major: int,
|
||||
version_minor: int,
|
||||
// Byte position of "%PDF-". Files with junk prepended (a mail header, an
|
||||
// HTTP response, a self-extracting wrapper) are common, and every offset
|
||||
// inside such a file is relative to this, not to zero.
|
||||
header_offset: i64,
|
||||
|
||||
xref: [dynamic]Xref_Entry,
|
||||
trailer: ^Dict,
|
||||
catalog: ^Dict,
|
||||
|
||||
// Resolved objects, keyed by object number. Generation is not part of the
|
||||
// key: a file never has two live generations of the same number, and the
|
||||
// ones that try are broken in ways the generation check makes worse.
|
||||
cache: map[u32]Object,
|
||||
// Objects currently being parsed, to break reference cycles that would
|
||||
// otherwise recurse until the stack ends.
|
||||
loading: map[u32]bool,
|
||||
// Decoded object streams, keyed by their object number.
|
||||
objstm_cache: map[u32]^Objstm,
|
||||
|
||||
// Colour spaces and functions built from this file, keyed by the address
|
||||
// of the object they were built from. A page that names the same space in
|
||||
// four hundred XObjects, or a shading that names one tint transform from
|
||||
// every pattern that uses it, must build each of them once. Both are
|
||||
// filled in lazily and, like everything else the Document owns, die with
|
||||
// its arena.
|
||||
cs_cache: map[uintptr]^Colorspace,
|
||||
fn_cache: map[uintptr]^Function,
|
||||
// Fonts, keyed the same way, by the address of the font dictionary. A
|
||||
// page that selects the same font two hundred times must build it once,
|
||||
// and building one means parsing an embedded font program.
|
||||
font_cache: map[uintptr]^Font,
|
||||
|
||||
// Images read from this file, and the decoded pixels of the ones most
|
||||
// recently painted. Unlike the two caches above this one has a byte
|
||||
// budget and evicts, because a decoded photograph is megabytes and a
|
||||
// catalogue has hundreds of them; see image.odin.
|
||||
img_cache: Image_Cache,
|
||||
|
||||
encrypt: ^Encryption,
|
||||
|
||||
// Page dictionaries in document order, flattened from the page tree.
|
||||
pages: [dynamic]^Dict,
|
||||
page_refs: [dynamic]Ref,
|
||||
|
||||
repairs: [dynamic]Repair,
|
||||
// True once anything was worked around. Cheaper to test than len(repairs).
|
||||
is_damaged: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Opening and closing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// open_file reads a PDF from disk. The Document owns the bytes.
|
||||
open_file :: proc(path: string, options := Open_Options{}) -> (doc: ^Document, err: Error) {
|
||||
opts := options
|
||||
if opts.allocator.procedure == nil {
|
||||
opts.allocator = context.allocator
|
||||
}
|
||||
data, read_err := os.read_entire_file(path, opts.allocator)
|
||||
if read_err != nil {
|
||||
return nil, .Cant_Open_File
|
||||
}
|
||||
opts.take_ownership = true
|
||||
return open_bytes(data, opts)
|
||||
}
|
||||
|
||||
// open_bytes reads a PDF already in memory.
|
||||
//
|
||||
// Unless `options.take_ownership` is set the slice is borrowed, not copied, and
|
||||
// must outlive the Document -- object offsets, string values and stream
|
||||
// payloads all point into it.
|
||||
open_bytes :: proc(data: []byte, options := Open_Options{}) -> (doc: ^Document, err: Error) {
|
||||
opts := options
|
||||
if opts.allocator.procedure == nil {
|
||||
opts.allocator = context.allocator
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, .Not_A_PDF
|
||||
}
|
||||
|
||||
doc = new(Document, opts.allocator)
|
||||
doc.data = data
|
||||
doc.owns_data = opts.take_ownership
|
||||
doc.backing = opts.allocator
|
||||
|
||||
if arena_err := virtual.arena_init_growing(&doc.arena); arena_err != nil {
|
||||
// `close` cannot run yet -- there is no arena for it to destroy -- so
|
||||
// the bytes we took ownership of have to be released here.
|
||||
if doc.owns_data {
|
||||
delete(doc.data, opts.allocator)
|
||||
}
|
||||
free(doc, opts.allocator)
|
||||
return nil, .Out_Of_Memory
|
||||
}
|
||||
doc.allocator = virtual.arena_allocator(&doc.arena)
|
||||
|
||||
doc.xref = make([dynamic]Xref_Entry, doc.allocator)
|
||||
doc.cache = make(map[u32]Object, 64, doc.allocator)
|
||||
doc.loading = make(map[u32]bool, 16, doc.allocator)
|
||||
doc.objstm_cache = make(map[u32]^Objstm, 8, doc.allocator)
|
||||
doc.pages = make([dynamic]^Dict, doc.allocator)
|
||||
doc.page_refs = make([dynamic]Ref, doc.allocator)
|
||||
doc.repairs = make([dynamic]Repair, doc.allocator)
|
||||
|
||||
// A missing header is not fatal on its own: enough files in the wild have
|
||||
// one that a reader which insists on it is less useful than one that goes
|
||||
// looking for objects.
|
||||
read_header(doc)
|
||||
|
||||
if xerr := xref_load(doc); xerr != .None {
|
||||
close(doc)
|
||||
return nil, xerr
|
||||
}
|
||||
|
||||
if cerr := crypt_init(doc, transmute([]byte)opts.password); cerr != .None {
|
||||
close(doc)
|
||||
return nil, cerr
|
||||
}
|
||||
|
||||
if derr := find_catalog(doc); derr != .None {
|
||||
close(doc)
|
||||
return nil, derr
|
||||
}
|
||||
|
||||
if !opts.skip_page_tree {
|
||||
if perr := pagetree_build(doc); perr != .None {
|
||||
close(doc)
|
||||
return nil, perr
|
||||
}
|
||||
}
|
||||
|
||||
return doc, .None
|
||||
}
|
||||
|
||||
// close releases everything the Document owns. The Document pointer, every
|
||||
// object reachable from it, and (if it was taken) the file's byte slice are all
|
||||
// invalid afterwards.
|
||||
close :: proc(doc: ^Document) {
|
||||
if doc == nil { return }
|
||||
crypt_destroy(doc)
|
||||
// Decoded image pixels are the one thing the arena does not own: they have
|
||||
// to be individually freeable for the cache's budget to mean anything.
|
||||
image_cache_release(doc)
|
||||
virtual.arena_destroy(&doc.arena)
|
||||
if doc.owns_data && doc.data != nil {
|
||||
delete(doc.data, doc.backing)
|
||||
}
|
||||
backing := doc.backing
|
||||
free(doc, backing)
|
||||
}
|
||||
|
||||
// read_header locates "%PDF-M.N" and records the version and the offset
|
||||
// everything else in the file is measured from.
|
||||
@(private)
|
||||
read_header :: proc(doc: ^Document) {
|
||||
// The header is required to be at byte 0, but junk in front of it is
|
||||
// ordinary enough that viewers all scan a window for it. 1 KiB matches
|
||||
// what the format's own conformance guidance allows for.
|
||||
limit := min(len(doc.data), 4096)
|
||||
idx := strings.index(string(doc.data[:limit]), "%PDF-")
|
||||
if idx < 0 {
|
||||
// Last resort: some generators emit "%!PS-Adobe" wrappers, and some
|
||||
// emit nothing at all. Leave the offset at zero and let the xref
|
||||
// rebuild find objects by scanning.
|
||||
doc.version_major = 1
|
||||
doc.version_minor = 7
|
||||
return
|
||||
}
|
||||
|
||||
doc.header_offset = i64(idx)
|
||||
v := doc.data[idx + 5:]
|
||||
doc.version_major = 1
|
||||
doc.version_minor = 7
|
||||
if len(v) >= 3 && v[0] >= '0' && v[0] <= '9' && v[1] == '.' && v[2] >= '0' && v[2] <= '9' {
|
||||
doc.version_major = int(v[0] - '0')
|
||||
doc.version_minor = int(v[2] - '0')
|
||||
}
|
||||
}
|
||||
|
||||
// document_version returns the version from the header, upgraded by the
|
||||
// catalog's /Version entry when that is newer (ISO 32000-2, 7.5.5: a file may
|
||||
// be upgraded by an incremental update without rewriting the header).
|
||||
document_version :: proc(doc: ^Document) -> (major, minor: int) {
|
||||
major, minor = doc.version_major, doc.version_minor
|
||||
if v := dict_name(doc, doc.catalog, "Version"); len(v) >= 3 && v[1] == '.' &&
|
||||
v[0] >= '0' && v[0] <= '9' && v[2] >= '0' && v[2] <= '9' {
|
||||
cm := int(v[0] - '0')
|
||||
cn := int(v[2] - '0')
|
||||
if cm > major || (cm == major && cn > minor) {
|
||||
major, minor = cm, cn
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Object access
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// resolve follows indirect references until it reaches a direct object.
|
||||
//
|
||||
// This is the single most-called procedure in the library: nearly every value
|
||||
// read out of a dictionary is allowed to be a reference, and callers that skip
|
||||
// it work on most files and mysteriously fail on the rest. A nil Document
|
||||
// makes it a no-op, which is what the standalone object parser wants.
|
||||
resolve :: proc(doc: ^Document, o: Object) -> Object {
|
||||
if doc == nil { return o }
|
||||
obj := o
|
||||
for i in 0 ..< MAX_REF_CHAIN {
|
||||
ref, is_ref := obj.(Ref)
|
||||
if !is_ref { return obj }
|
||||
obj = get_object(doc, ref)
|
||||
_ = i
|
||||
}
|
||||
// A chain this long is a loop. Null is what a viewer shows.
|
||||
return nil
|
||||
}
|
||||
|
||||
// get_object loads one indirect object, from the cache if it is there.
|
||||
//
|
||||
// A reference to an object that does not exist is null, not an error: the
|
||||
// format says so (ISO 32000-2, 7.3.10) and files rely on it.
|
||||
get_object :: proc(doc: ^Document, ref: Ref) -> Object {
|
||||
if doc == nil || ref.num == 0 || ref.num > MAX_OBJECT_NUMBER {
|
||||
return nil
|
||||
}
|
||||
if cached, ok := doc.cache[ref.num]; ok {
|
||||
return cached
|
||||
}
|
||||
// Already being parsed further up the stack: an object whose own
|
||||
// definition refers back to it. Break the cycle with null.
|
||||
if doc.loading[ref.num] or_else false {
|
||||
add_repair(doc, .Missing_Object, 0, ref)
|
||||
return nil
|
||||
}
|
||||
// `loading` holds exactly the objects whose parses are in flight, so its
|
||||
// size is the current nesting depth. Bounding it stops an acyclic chain --
|
||||
// object 2's /Length pointing at object 3's, and so on for thousands of
|
||||
// distinct objects -- from walking the native stack off its end. The
|
||||
// cycle guard above cannot see such a chain, because no object number
|
||||
// ever repeats.
|
||||
if len(doc.loading) >= MAX_LOAD_DEPTH {
|
||||
add_repair(doc, .Recursion_Limit_Hit, 0, ref)
|
||||
return nil
|
||||
}
|
||||
if int(ref.num) >= len(doc.xref) {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry := doc.xref[ref.num]
|
||||
doc.loading[ref.num] = true
|
||||
defer delete_key(&doc.loading, ref.num)
|
||||
|
||||
obj: Object
|
||||
switch entry.kind {
|
||||
case .Free:
|
||||
obj = nil
|
||||
case .Normal:
|
||||
parsed, got, err := parse_indirect_object_at(doc, entry.offset, Ref{ref.num, entry.gen})
|
||||
if err == .None {
|
||||
// An xref pointing at the wrong object is a classic symptom of a
|
||||
// file that was edited without its table being updated. Take what
|
||||
// is actually there, but only if the number matches -- taking a
|
||||
// different object's body would be worse than nothing.
|
||||
if got.num == ref.num {
|
||||
obj = parsed
|
||||
} else {
|
||||
add_repair(doc, .Xref_Entry_Mismatch, entry.offset, ref)
|
||||
obj = nil
|
||||
}
|
||||
} else {
|
||||
add_repair(doc, .Missing_Object, entry.offset, ref)
|
||||
}
|
||||
case .Compressed:
|
||||
parsed, err := objstm_get_object(doc, entry.container, entry.index, ref.num)
|
||||
if err == .None {
|
||||
obj = parsed
|
||||
} else {
|
||||
add_repair(doc, .Missing_Object, 0, ref)
|
||||
}
|
||||
}
|
||||
|
||||
doc.cache[ref.num] = obj
|
||||
return obj
|
||||
}
|
||||
|
||||
// get_dict, get_array, get_stream are the typed forms of get_object, for the
|
||||
// many call sites that know what they are looking for.
|
||||
get_dict :: proc(doc: ^Document, ref: Ref) -> ^Dict {
|
||||
d, _ := to_dict(get_object(doc, ref))
|
||||
return d
|
||||
}
|
||||
|
||||
get_array :: proc(doc: ^Document, ref: Ref) -> ^Array {
|
||||
a, _ := to_array(get_object(doc, ref))
|
||||
return a
|
||||
}
|
||||
|
||||
get_stream :: proc(doc: ^Document, ref: Ref) -> ^Stream {
|
||||
s, _ := to_stream(get_object(doc, ref))
|
||||
return s
|
||||
}
|
||||
|
||||
// document_memory_used is how many bytes the Document's arena has handed out:
|
||||
// every object, dictionary, stream payload and decoded buffer parsed so far.
|
||||
// It grows as pages are visited, since streams decode lazily, and drops to
|
||||
// zero only at `close`.
|
||||
document_memory_used :: proc(doc: ^Document) -> int {
|
||||
if doc == nil { return 0 }
|
||||
return int(doc.arena.total_used)
|
||||
}
|
||||
|
||||
// document_memory_reserved is the address space the arena holds. On a 64-bit
|
||||
// system this is reserved, not committed, so it is much larger than the
|
||||
// resident cost and is not the number to budget against.
|
||||
document_memory_reserved :: proc(doc: ^Document) -> int {
|
||||
if doc == nil { return 0 }
|
||||
return int(doc.arena.total_reserved)
|
||||
}
|
||||
|
||||
// object_count is the highest object number the file defines, plus one.
|
||||
object_count :: proc(doc: ^Document) -> int {
|
||||
return len(doc.xref)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catalog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@(private)
|
||||
find_catalog :: proc(doc: ^Document) -> Error {
|
||||
if root := dict_dict(doc, doc.trailer, "Root"); root != nil {
|
||||
// Trust it only if it looks like a catalog. Some files have a /Root
|
||||
// pointing at a page or at a free object after a bad incremental
|
||||
// update, and scanning finds the real one.
|
||||
if dict_type(doc, root) == "Catalog" || dict_has(root, "Pages") {
|
||||
doc.catalog = root
|
||||
return .None
|
||||
}
|
||||
}
|
||||
|
||||
// Scan every object for /Type /Catalog. Costly, but only reached on files
|
||||
// whose trailer is unusable, and the alternative is refusing to open them.
|
||||
for num in 1 ..< len(doc.xref) {
|
||||
obj := get_object(doc, Ref{u32(num), doc.xref[num].gen})
|
||||
d, ok := to_dict(obj)
|
||||
if !ok { continue }
|
||||
if dict_type(doc, d) == "Catalog" {
|
||||
doc.catalog = d
|
||||
add_repair(doc, .Missing_Root, 0, Ref{u32(num), 0})
|
||||
return .None
|
||||
}
|
||||
}
|
||||
|
||||
// A file with no catalog can still have pages; pagetree_build falls back to
|
||||
// scanning for /Type /Page. Report the damage and let it try.
|
||||
add_repair(doc, .Missing_Root, 0, {})
|
||||
return .None
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repairs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
add_repair :: proc(doc: ^Document, kind: Repair_Kind, offset: i64, ref := Ref{}) {
|
||||
if doc == nil { return }
|
||||
doc.is_damaged = true
|
||||
if len(doc.repairs) >= MAX_REPAIRS { return }
|
||||
append(&doc.repairs, Repair{kind = kind, offset = offset, ref = ref})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Byte-slice helpers used across the reader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// slice_at returns doc.data[offset:] with the offset clamped, so a garbage
|
||||
// offset out of a broken xref yields an empty slice instead of a panic.
|
||||
@(private)
|
||||
slice_at :: proc(doc: ^Document, offset: i64) -> []byte {
|
||||
if offset < 0 || offset >= i64(len(doc.data)) {
|
||||
return nil
|
||||
}
|
||||
return doc.data[offset:]
|
||||
}
|
||||
|
||||
// arena_bytes copies `src` into the Document's arena. Used when a value cannot
|
||||
// borrow from `doc.data` -- decrypted strings, decoded streams, anything
|
||||
// synthesised.
|
||||
@(private)
|
||||
arena_bytes :: proc(doc: ^Document, src: []byte) -> []byte {
|
||||
if len(src) == 0 { return nil }
|
||||
dst := make([]byte, len(src), doc.allocator)
|
||||
copy(dst, src)
|
||||
return dst
|
||||
}
|
||||
|
||||
@(private)
|
||||
arena_string :: proc(doc: ^Document, src: string) -> string {
|
||||
return string(arena_bytes(doc, transmute([]byte)src))
|
||||
}
|
||||
|
||||
// index_bytes finds `needle` in `haystack`, or -1.
|
||||
@(private)
|
||||
index_bytes :: proc(haystack, needle: []byte) -> int {
|
||||
return slice_indexof_bytes(haystack, needle)
|
||||
}
|
||||
|
||||
@(private)
|
||||
slice_indexof_bytes :: proc(haystack, needle: []byte) -> int {
|
||||
if len(needle) == 0 { return 0 }
|
||||
if len(needle) > len(haystack) { return -1 }
|
||||
first := needle[0]
|
||||
for i := 0; i + len(needle) <= len(haystack); i += 1 {
|
||||
if haystack[i] != first { continue }
|
||||
if slice.equal(haystack[i:i + len(needle)], needle) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// last_index_bytes finds the last occurrence of `needle`, or -1. Used to walk
|
||||
// backwards to `startxref`.
|
||||
@(private)
|
||||
last_index_bytes :: proc(haystack, needle: []byte) -> int {
|
||||
if len(needle) == 0 { return len(haystack) }
|
||||
if len(needle) > len(haystack) { return -1 }
|
||||
for i := len(haystack) - len(needle); i >= 0; i -= 1 {
|
||||
if slice.equal(haystack[i:i + len(needle)], needle) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user