499 lines
17 KiB
Odin
499 lines
17 KiB
Odin
package pdf
|
|
|
|
import "core:mem"
|
|
import "core:strings"
|
|
|
|
// The COS tokeniser (ISO 32000-2, 7.2).
|
|
//
|
|
// It is deliberately total: every call to `lexer_next` either advances `pos`
|
|
// or returns an EOF token, so no input -- valid, truncated or random -- can
|
|
// make a caller spin. Nothing here rejects input either; the malformations
|
|
// this layer meets (a string that never closes, a hex digit missing its pair,
|
|
// a number that is three signs and a dot) are all common enough in real files
|
|
// that failing on them would mean refusing documents every viewer opens. The
|
|
// damage is reported through `Token.truncated` and through the values
|
|
// themselves, and the parser turns that into a `Repair`.
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Character classes (ISO 32000-2, 7.2.3, tables 1 and 2)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
is_pdf_whitespace :: proc "contextless" (c: byte) -> bool {
|
|
switch c {
|
|
case 0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20: return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
is_pdf_delimiter :: proc "contextless" (c: byte) -> bool {
|
|
switch c {
|
|
case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%': return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// A regular character is anything that is neither whitespace nor a delimiter;
|
|
// names, numbers and keywords are runs of them.
|
|
is_pdf_regular :: proc "contextless" (c: byte) -> bool {
|
|
return !is_pdf_whitespace(c) && !is_pdf_delimiter(c)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tokens
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Token_Kind :: enum u8 {
|
|
EOF,
|
|
Integer,
|
|
Real,
|
|
String,
|
|
Name,
|
|
Array_Open,
|
|
Array_Close,
|
|
Dict_Open,
|
|
Dict_Close,
|
|
Brace_Open, // only meaningful inside a type 4 (PostScript) function
|
|
Brace_Close,
|
|
Keyword, // obj, endobj, stream, R, true, false, null, operators
|
|
Error, // a stray delimiter; one byte was consumed
|
|
}
|
|
|
|
Token :: struct {
|
|
kind: Token_Kind,
|
|
// Byte range of the token in the lexer's input: [start, end).
|
|
start: int,
|
|
end: int,
|
|
// Both numeric fields are filled for both numeric kinds, so a caller that
|
|
// wants "a number" never has to switch on the kind. A value that does not
|
|
// fit, or that parsed to an infinity, reads as 0 -- see `parse_number`.
|
|
int_value: i64,
|
|
real_value: f64,
|
|
// Names (with #xx resolved) and keywords. Borrows from the input unless
|
|
// the name contained an escape, in which case it was allocated with the
|
|
// allocator passed to `lexer_next`.
|
|
text: string,
|
|
// String objects, unescaped and hex-unpacked. Always allocated with the
|
|
// allocator passed to `lexer_next` (never a view into the input): a
|
|
// decryption pass may have to rewrite these bytes in place, and the input
|
|
// is the file itself.
|
|
bytes: []byte,
|
|
// The token ran into the end of the input before its closing delimiter.
|
|
// The value is still usable -- this is a repair, not an error.
|
|
truncated: bool,
|
|
}
|
|
|
|
Lexer :: struct {
|
|
data: []byte,
|
|
pos: int,
|
|
}
|
|
|
|
lexer_make :: proc(data: []byte) -> Lexer {
|
|
return Lexer{data = data, pos = 0}
|
|
}
|
|
|
|
// lexer_skip_whitespace steps over whitespace and comments. A '%' outside of a
|
|
// string runs to the end of the line and is whitespace for every purpose
|
|
// (ISO 32000-2, 7.2.4).
|
|
lexer_skip_whitespace :: proc(lex: ^Lexer) {
|
|
for lex.pos < len(lex.data) {
|
|
c := lex.data[lex.pos]
|
|
if is_pdf_whitespace(c) {
|
|
lex.pos += 1
|
|
continue
|
|
}
|
|
if c == '%' {
|
|
lex.pos += 1
|
|
for lex.pos < len(lex.data) {
|
|
d := lex.data[lex.pos]
|
|
if d == '\r' || d == '\n' { break }
|
|
lex.pos += 1
|
|
}
|
|
continue
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// lexer_next returns the next token, advancing past it.
|
|
//
|
|
// `allocator` owns any bytes the token had to materialise: the payload of a
|
|
// string, and the text of a name that contained #xx escapes. Callers holding a
|
|
// Document pass `doc.allocator` so the token's memory dies with the Document.
|
|
lexer_next :: proc(lex: ^Lexer, allocator := context.allocator) -> Token {
|
|
lexer_skip_whitespace(lex)
|
|
if lex.pos >= len(lex.data) {
|
|
return Token{kind = .EOF, start = lex.pos, end = lex.pos}
|
|
}
|
|
|
|
start := lex.pos
|
|
c := lex.data[start]
|
|
|
|
switch c {
|
|
case '[':
|
|
lex.pos += 1
|
|
return Token{kind = .Array_Open, start = start, end = lex.pos}
|
|
case ']':
|
|
lex.pos += 1
|
|
return Token{kind = .Array_Close, start = start, end = lex.pos}
|
|
case '{':
|
|
lex.pos += 1
|
|
return Token{kind = .Brace_Open, start = start, end = lex.pos}
|
|
case '}':
|
|
lex.pos += 1
|
|
return Token{kind = .Brace_Close, start = start, end = lex.pos}
|
|
case '(':
|
|
return lex_literal_string(lex, allocator)
|
|
case '/':
|
|
return lex_name(lex, allocator)
|
|
case '<':
|
|
if start + 1 < len(lex.data) && lex.data[start + 1] == '<' {
|
|
lex.pos += 2
|
|
return Token{kind = .Dict_Open, start = start, end = lex.pos}
|
|
}
|
|
return lex_hex_string(lex, allocator)
|
|
case '>':
|
|
if start + 1 < len(lex.data) && lex.data[start + 1] == '>' {
|
|
lex.pos += 2
|
|
return Token{kind = .Dict_Close, start = start, end = lex.pos}
|
|
}
|
|
// A lone '>' is junk. Consume it so the caller makes progress.
|
|
lex.pos += 1
|
|
return Token{kind = .Error, start = start, end = lex.pos}
|
|
case ')':
|
|
lex.pos += 1
|
|
return Token{kind = .Error, start = start, end = lex.pos}
|
|
case '+', '-', '.', '0' ..= '9':
|
|
return lex_number(lex)
|
|
}
|
|
|
|
if is_pdf_regular(c) {
|
|
return lex_keyword(lex)
|
|
}
|
|
|
|
// Unreachable for the delimiters above, but a delimiter added to the class
|
|
// table later must still consume a byte rather than stall.
|
|
lex.pos += 1
|
|
return Token{kind = .Error, start = start, end = lex.pos}
|
|
}
|
|
|
|
// lexer_next_structural returns the next token without ever allocating: the
|
|
// kinds whose value has to be materialised -- strings, and names carrying #xx
|
|
// escapes -- come back with an empty payload. Their `kind`, `start` and `end`
|
|
// are still right, which is all a structural probe looks at.
|
|
//
|
|
// The probes that hunt for "num gen obj" or an xref subsection line accept
|
|
// integers and keywords only, and in a damaged file they run at every
|
|
// plausible offset and reject nearly all of them. Lexing those rejects with a
|
|
// real allocator copies the string bodies they step over -- a file reading
|
|
// "1 (" at a hundred probed offsets would copy its own tail a hundred times,
|
|
// and whichever of those copies is not attached to a Document leaks outright.
|
|
lexer_next_structural :: proc(lex: ^Lexer) -> Token {
|
|
return lexer_next(lex, mem.nil_allocator())
|
|
}
|
|
|
|
// lexer_peek returns the next token without consuming it. It may allocate
|
|
// exactly as `lexer_next` does, so peeking at a string costs a copy; the
|
|
// parser keeps its own token queue instead of peeking for that reason.
|
|
lexer_peek :: proc(lex: ^Lexer, allocator := context.allocator) -> Token {
|
|
save := lex.pos
|
|
tok := lexer_next(lex, allocator)
|
|
lex.pos = save
|
|
return tok
|
|
}
|
|
|
|
// lexer_at_keyword reports whether the next token is exactly `kw`, without
|
|
// consuming anything. Used for the keywords that are found by position rather
|
|
// than parsed -- `stream`, `endstream`, `endobj`.
|
|
lexer_at_keyword :: proc(lex: ^Lexer, kw: string) -> bool {
|
|
save := lex.pos
|
|
defer lex.pos = save
|
|
lexer_skip_whitespace(lex)
|
|
return lexer_match_here(lex, kw)
|
|
}
|
|
|
|
// lexer_accept_keyword consumes `kw` if it is next, and reports whether it did.
|
|
lexer_accept_keyword :: proc(lex: ^Lexer, kw: string) -> bool {
|
|
save := lex.pos
|
|
lexer_skip_whitespace(lex)
|
|
if lexer_match_here(lex, kw) {
|
|
lex.pos += len(kw)
|
|
return true
|
|
}
|
|
lex.pos = save
|
|
return false
|
|
}
|
|
|
|
@(private = "file")
|
|
lexer_match_here :: proc(lex: ^Lexer, kw: string) -> bool {
|
|
end := lex.pos + len(kw)
|
|
if end > len(lex.data) { return false }
|
|
if string(lex.data[lex.pos:end]) != kw { return false }
|
|
// "stream" must not match the first six bytes of "streamable".
|
|
if end < len(lex.data) && is_pdf_regular(lex.data[end]) { return false }
|
|
return true
|
|
}
|
|
|
|
// lexer_skip_eol steps over one end-of-line marker: CRLF, LF or a bare CR.
|
|
// The bare CR is not legal after `stream` but is written by enough producers
|
|
// that rejecting it would fail real documents (ISO 32000-2, 7.3.8.1).
|
|
lexer_skip_eol :: proc(lex: ^Lexer) {
|
|
if lex.pos < len(lex.data) && lex.data[lex.pos] == '\r' {
|
|
lex.pos += 1
|
|
}
|
|
if lex.pos < len(lex.data) && lex.data[lex.pos] == '\n' {
|
|
lex.pos += 1
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Numbers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
lex_number :: proc(lex: ^Lexer) -> Token {
|
|
start := lex.pos
|
|
for lex.pos < len(lex.data) && is_pdf_regular(lex.data[lex.pos]) {
|
|
lex.pos += 1
|
|
}
|
|
text := string(lex.data[start:lex.pos])
|
|
|
|
tok := Token{start = start, end = lex.pos}
|
|
// parse_number never reports failure as an exception: a number a viewer
|
|
// would read as 0 must not abort the object around it.
|
|
value, _ := parse_number(text)
|
|
#partial switch v in value {
|
|
case i64:
|
|
tok.kind = .Integer
|
|
tok.int_value = v
|
|
tok.real_value = f64(v)
|
|
case f64:
|
|
tok.kind = .Real
|
|
tok.real_value = clamp_finite(v, 0)
|
|
tok.int_value = f64_to_i64_saturating(tok.real_value)
|
|
case:
|
|
tok.kind = .Integer
|
|
}
|
|
return tok
|
|
}
|
|
|
|
// f64_to_i64_saturating clamps before converting: an out-of-range float to
|
|
// integer conversion is undefined behaviour, and /Length values of 1e300 exist.
|
|
@(private)
|
|
f64_to_i64_saturating :: proc "contextless" (v: f64) -> i64 {
|
|
if !is_finite(v) { return 0 }
|
|
if v >= 9.2e18 { return max(i64) }
|
|
if v <= -9.2e18 { return min(i64) }
|
|
return i64(v)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Names (ISO 32000-2, 7.3.5)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
lex_name :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Token {
|
|
start := lex.pos
|
|
lex.pos += 1 // the '/'
|
|
body := lex.pos
|
|
for lex.pos < len(lex.data) && is_pdf_regular(lex.data[lex.pos]) {
|
|
lex.pos += 1
|
|
}
|
|
raw := string(lex.data[body:lex.pos])
|
|
// A bare '/' is the empty name, which is a legal object.
|
|
return Token{
|
|
kind = .Name,
|
|
start = start,
|
|
end = lex.pos,
|
|
text = decode_name_escapes(raw, allocator),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Keywords
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
lex_keyword :: proc(lex: ^Lexer) -> Token {
|
|
start := lex.pos
|
|
for lex.pos < len(lex.data) && is_pdf_regular(lex.data[lex.pos]) {
|
|
lex.pos += 1
|
|
}
|
|
return Token{
|
|
kind = .Keyword,
|
|
start = start,
|
|
end = lex.pos,
|
|
text = string(lex.data[start:lex.pos]),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Literal strings (ISO 32000-2, 7.3.4.2)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
lex_literal_string :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Token {
|
|
start := lex.pos
|
|
lex.pos += 1 // the '('
|
|
|
|
// The unescaped form is never longer than the escaped one, so one
|
|
// allocation of the remaining length is always enough and never wrong.
|
|
// Size the buffer from the raw body once, up front: the unescaped value is
|
|
// never longer than the bytes it came from, so this both fits and cannot
|
|
// over-allocate on a file that claims a huge string it does not contain.
|
|
b := strings.builder_make(0, literal_body_len(lex.data, lex.pos), allocator)
|
|
depth := 1
|
|
truncated := true
|
|
|
|
scan: for lex.pos < len(lex.data) {
|
|
c := lex.data[lex.pos]
|
|
lex.pos += 1
|
|
switch c {
|
|
case '\\':
|
|
if lex.pos >= len(lex.data) { break scan }
|
|
e := lex.data[lex.pos]
|
|
lex.pos += 1
|
|
switch e {
|
|
case 'n': strings.write_byte(&b, '\n')
|
|
case 'r': strings.write_byte(&b, '\r')
|
|
case 't': strings.write_byte(&b, '\t')
|
|
case 'b': strings.write_byte(&b, '\b')
|
|
case 'f': strings.write_byte(&b, '\f')
|
|
case '(': strings.write_byte(&b, '(')
|
|
case ')': strings.write_byte(&b, ')')
|
|
case '\\': strings.write_byte(&b, '\\')
|
|
case '\r':
|
|
// Backslash before an EOL is a line continuation: both the
|
|
// backslash and the EOL disappear.
|
|
if lex.pos < len(lex.data) && lex.data[lex.pos] == '\n' {
|
|
lex.pos += 1
|
|
}
|
|
case '\n':
|
|
// continuation
|
|
case '0' ..= '7':
|
|
value := int(e - '0')
|
|
for _ in 0 ..< 2 {
|
|
if lex.pos >= len(lex.data) { break }
|
|
d := lex.data[lex.pos]
|
|
if d < '0' || d > '7' { break }
|
|
value = value * 8 + int(d - '0')
|
|
lex.pos += 1
|
|
}
|
|
strings.write_byte(&b, byte(value & 0xFF))
|
|
case:
|
|
// "\q" is just "q": the backslash is dropped, the byte kept.
|
|
strings.write_byte(&b, e)
|
|
}
|
|
case '(':
|
|
depth += 1
|
|
strings.write_byte(&b, '(')
|
|
case ')':
|
|
depth -= 1
|
|
if depth == 0 {
|
|
truncated = false
|
|
break scan
|
|
}
|
|
strings.write_byte(&b, ')')
|
|
case '\r':
|
|
// A raw CR, LF or CRLF in the source is one LF in the value.
|
|
if lex.pos < len(lex.data) && lex.data[lex.pos] == '\n' {
|
|
lex.pos += 1
|
|
}
|
|
strings.write_byte(&b, '\n')
|
|
case:
|
|
strings.write_byte(&b, c)
|
|
}
|
|
}
|
|
|
|
return Token{
|
|
kind = .String,
|
|
start = start,
|
|
end = lex.pos,
|
|
bytes = transmute([]byte)strings.to_string(b),
|
|
truncated = truncated,
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Hex strings (ISO 32000-2, 7.3.4.3)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
lex_hex_string :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Token {
|
|
start := lex.pos
|
|
lex.pos += 1 // the '<'
|
|
|
|
b := strings.builder_make(0, hex_body_len(lex.data, lex.pos) / 2 + 1, allocator)
|
|
nibble: byte
|
|
have_nibble := false
|
|
truncated := true
|
|
|
|
for lex.pos < len(lex.data) {
|
|
c := lex.data[lex.pos]
|
|
lex.pos += 1
|
|
if c == '>' {
|
|
truncated = false
|
|
break
|
|
}
|
|
v, ok := hex_value(c)
|
|
// Whitespace is defined to be skipped here, and other junk is skipped
|
|
// too: files with a stray '<' or a UTF-8 quote inside a hex string are
|
|
// repaired by every viewer rather than rejected.
|
|
if !ok { continue }
|
|
if have_nibble {
|
|
strings.write_byte(&b, nibble << 4 | v)
|
|
have_nibble = false
|
|
} else {
|
|
nibble = v
|
|
have_nibble = true
|
|
}
|
|
}
|
|
// "An odd number of digits: the final digit is assumed to be followed by
|
|
// a zero." (7.3.4.3)
|
|
if have_nibble {
|
|
strings.write_byte(&b, nibble << 4)
|
|
}
|
|
|
|
return Token{
|
|
kind = .String,
|
|
start = start,
|
|
end = lex.pos,
|
|
bytes = transmute([]byte)strings.to_string(b),
|
|
truncated = truncated,
|
|
}
|
|
}
|
|
|
|
// literal_body_len counts the raw bytes of a literal string body starting at
|
|
// `pos` (just past the '('), stopping at the balancing ')' or at EOF. It is an
|
|
// upper bound on the decoded length, which is what the value buffer is sized
|
|
// from.
|
|
@(private = "file")
|
|
literal_body_len :: proc(data: []byte, pos: int) -> int {
|
|
depth := 1
|
|
i := pos
|
|
for i < len(data) {
|
|
switch data[i] {
|
|
case '\\':
|
|
i += 1 // whatever follows is data, never a delimiter
|
|
case '(':
|
|
depth += 1
|
|
case ')':
|
|
depth -= 1
|
|
if depth == 0 {
|
|
return i - pos
|
|
}
|
|
}
|
|
i += 1
|
|
}
|
|
return len(data) - pos
|
|
}
|
|
|
|
@(private = "file")
|
|
hex_body_len :: proc(data: []byte, pos: int) -> int {
|
|
for i := pos; i < len(data); i += 1 {
|
|
if data[i] == '>' {
|
|
return i - pos
|
|
}
|
|
}
|
|
return len(data) - pos
|
|
}
|