Files
pdf-odin-llm/filter_ascii.odin
T
2026-09-25 23:32:29 +02:00

195 lines
6.0 KiB
Odin

package pdf
// ASCIIHexDecode and ASCII85Decode: the two filters that exist so a PDF can be
// mailed through a 7-bit channel. Both are self-delimiting -- the data ends at
// a marker, not at the end of the buffer -- so both are routinely handed more
// bytes than they will consume, and both must ignore whitespace anywhere.
//
// Neither ever fails. A character that belongs to neither alphabet is dropped:
// producers emit stray line-noise (a '\0' padding a stream, a lone '<' left
// over from a hex string) often enough that refusing the stream would lose a
// page over one byte. ISO 32000-2, 7.4.2 and 7.4.3 both make an unexpected
// character an error; treating it as a repair instead is the difference
// between a reader and a validator.
// ascii_hex_decode decodes ASCIIHexDecode data.
//
// Whitespace is ignored, '>' ends the data, and an odd final digit is padded
// with '0' as the spec requires. Any other character is skipped.
//
// The returned slice is owned by the caller and allocated with `allocator`.
ascii_hex_decode :: proc(data: []byte, allocator := context.allocator) -> (out: []byte, err: Error) {
n := ascii_hex_scan(data, nil)
if n == 0 {
return nil, .None
}
buf, alloc_err := make([]byte, n, allocator)
if alloc_err != nil {
return nil, .Out_Of_Memory
}
ascii_hex_scan(data, buf)
return buf, .None
}
// ascii85_decode decodes ASCII85Decode data.
//
// "~>" ends the data, 'z' at the start of a group stands for four zero bytes,
// whitespace is ignored anywhere, and a truncated final group of n characters
// yields n-1 bytes. A leading "<~" -- which the filter does not define but
// which producers copying Adobe's own encoder emit -- is skipped.
//
// The returned slice is owned by the caller and allocated with `allocator`.
ascii85_decode :: proc(data: []byte, allocator := context.allocator) -> (out: []byte, err: Error) {
n := ascii85_scan(data, nil)
if n == 0 {
return nil, .None
}
buf, alloc_err := make([]byte, n, allocator)
if alloc_err != nil {
return nil, .Out_Of_Memory
}
ascii85_scan(data, buf)
return buf, .None
}
// ---------------------------------------------------------------------------
// Scanners
//
// Each decoder is one procedure run twice: once with `out == nil` to measure
// the result, once to fill it. Measuring first means the allocation is exactly
// the size of the output rather than an upper bound derived from an
// attacker-supplied length, and means the two passes cannot disagree about
// what the data says.
// ---------------------------------------------------------------------------
// ISO 32000-2, Table 1: the six white-space characters. NUL counts, which
// matters here because it is what stream padding is usually made of.
@(private)
ascii_is_whitespace :: proc "contextless" (c: byte) -> bool {
switch c {
case 0x00, 0x09, 0x0A, 0x0C, 0x0D, 0x20:
return true
}
return false
}
@(private)
ascii_hex_scan :: proc(data: []byte, out: []byte) -> int {
n := 0
hi: byte
have_hi := false
for c in data {
if c == '>' {
break
}
// hex_value rejects whitespace and junk alike, and both are skipped,
// so there is nothing to tell apart here.
v, ok := hex_value(c)
if !ok {
continue
}
if !have_hi {
hi, have_hi = v, true
continue
}
if n < len(out) {
out[n] = hi << 4 | v
}
n += 1
have_hi = false
}
// A final odd digit is padded with '0', i.e. shifted into the high nibble.
if have_hi {
if n < len(out) {
out[n] = hi << 4
}
n += 1
}
return n
}
@(private)
ascii85_scan :: proc(data: []byte, out: []byte) -> int {
i := 0
for i < len(data) && ascii_is_whitespace(data[i]) {
i += 1
}
if i + 1 < len(data) && data[i] == '<' && data[i + 1] == '~' {
i += 2
}
n := 0
group: [5]byte
ngroup := 0
for ; i < len(data); i += 1 {
c := data[i]
if ascii_is_whitespace(c) {
continue
}
// The terminator is "~>", but a '~' that is not followed by '>' is
// still the end of anything decodable, so do not look at the next byte.
if c == '~' {
break
}
if c == 'z' && ngroup == 0 {
n = ascii85_emit(out, n, 0, 4)
continue
}
// Everything outside the alphabet is dropped -- including a 'z' in the
// middle of a group, which has no defined meaning ('z' is above 'u').
if c < '!' || c > 'u' {
continue
}
group[ngroup] = c - '!'
ngroup += 1
if ngroup == 5 {
n = ascii85_emit(out, n, ascii85_value(group[:], 5), 4)
ngroup = 0
}
}
// A partial group of n characters encodes n-1 bytes. A single leftover
// character encodes nothing at all and is dropped.
if ngroup >= 2 {
n = ascii85_emit(out, n, ascii85_value(group[:], ngroup), ngroup - 1)
}
return n
}
// ascii85_value folds a group of 1..5 digits, padding a short group with 'u'
// as ISO 32000-2, 7.4.3 requires. Five digits can encode up to 85^5-1, which
// is larger than a u32 can hold; such a group is out of spec, and clamping it
// keeps the bytes that were probably meant rather than dropping the group.
@(private)
ascii85_value :: proc(digits: []byte, count: int) -> u32 {
v: u64 = 0
for k in 0 ..< 5 {
d: u64 = 84 // 'u' - '!'
if k < count && k < len(digits) {
d = u64(digits[k])
}
v = v * 85 + d
}
if v > 0xFFFF_FFFF {
return 0xFFFF_FFFF
}
return u32(v)
}
@(private)
ascii85_emit :: proc(out: []byte, n: int, v: u32, count: int) -> int {
b := [4]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
m := n
for k in 0 ..< count {
if k >= len(b) {
break
}
if m < len(out) {
out[m] = b[k]
}
m += 1
}
return m
}