pdfrb 0.8.11 → 0.8.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +154 -0
- data/docs/USAGE.md +25 -1
- data/lib/pdfrb/digital_signature/verification.rb +53 -26
- data/lib/pdfrb/version.rb +1 -1
- metadata +2 -3
- data/lib/pdfrb/source/tokenizer.rb.bak +0 -320
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20aa9dfc7f1144740e4138b85909b2434e1652fcc88ea6d83445d0e76c40f68b
|
|
4
|
+
data.tar.gz: 8384f5165e823cc2077b290db27d74c2534b5f53951c4afa6d61a4eec277c340
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e988fa5b3a1e691b49ebb716311daa0a30d67d1b4c20a1e18e4d32d827f791ffd3931abd7250fcd4732cb4ad53cd92f45b84ac030b380f6d8e32de4edd52f1aa
|
|
7
|
+
data.tar.gz: 9d280097604c785c8c39aee8defb1fb7979901cec4b965e4b76d8b553d5d620036a7efb2925cd7b473baf72538662aa17c0c1f0217f67d58efd7ba4b3d3185e6
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,160 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the pdfrb gem will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.8.11] — 2026-08-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
* **Outline reading** — `Document::Outline#each` / `#to_a` walk an
|
|
10
|
+
existing outline depth-first (preorder), yielding typed
|
|
11
|
+
`Model::Type::OutlineItem` objects (title, count, destination,
|
|
12
|
+
bold/italic flags); `#empty?` for outline-less documents.
|
|
13
|
+
`OutlineItem#title` decodes via the shared TextString seam.
|
|
14
|
+
|
|
15
|
+
## [0.8.10] — 2026-08-31
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* **Text-string decoding at every semantic accessor** — the shared
|
|
20
|
+
`Model::Cos::TextString.decode` seam now serves
|
|
21
|
+
`Annotation#contents`/`#name`, the `FormField` accessors, the
|
|
22
|
+
`Form` facade (`find`/`field_names`/`get_value`), and
|
|
23
|
+
`Document::Metadata#read_field`. UTF-16BE+BOM and
|
|
24
|
+
PDFDocEncoding bytes read back as UTF-8; non-strings pass through.
|
|
25
|
+
|
|
26
|
+
## [0.8.9] — 2026-08-31
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
* `Document::Metadata#read_field` relabelled string bytes with
|
|
31
|
+
`force_encoding("UTF-8")`, which never converts — UTF-16 titles
|
|
32
|
+
read back as garbage. Info values are text strings (s7.9.2.2) and
|
|
33
|
+
now decode properly.
|
|
34
|
+
|
|
35
|
+
## [0.8.8] — 2026-08-31
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
* **Read-path decryption** — `ObjectReader#load_oid` decrypts
|
|
40
|
+
strings and stream payloads with the per-object key (s7.6.1) as
|
|
41
|
+
objects enter the Model, via a memoized
|
|
42
|
+
`Document::Encryption#reader_handler` that verifies the configured
|
|
43
|
+
password and raises `EncryptionError` when it does not verify.
|
|
44
|
+
Round trips now close at 256/128/40-bit.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
* `pad_password` sliced the pad string at the password length
|
|
49
|
+
instead of its head (Algorithm 3.2), so handler-derived keys
|
|
50
|
+
diverged from `PasswordVerification`'s.
|
|
51
|
+
* Algorithm 3.5 existed twice with different RC4 round sets
|
|
52
|
+
(writer 0..18, verifier 19..1; the spec runs 1..19 ascending);
|
|
53
|
+
one implementation now serves both, and R2 verification compares
|
|
54
|
+
instead of returning true unconditionally.
|
|
55
|
+
* V4 /Encrypt dicts declared no `CF`/`StmF`/`StrF`, but absent
|
|
56
|
+
filters default to Identity (s7.6.3.2 Table 21) — conforming
|
|
57
|
+
readers saw AES ciphertext as cleartext. V4 now declares AESV2.
|
|
58
|
+
* The cipher is chosen from the resolved CFM (AESV2/V2/Identity),
|
|
59
|
+
direction-aware (`encrypt_string`/`encrypt_stream` + decrypt
|
|
60
|
+
twins), instead of `V >= 4` always meaning AES.
|
|
61
|
+
* AES helpers hard-coded aes-128-cbc and truncated 32-byte V5 object
|
|
62
|
+
keys to 16 bytes; the cipher now follows the key size.
|
|
63
|
+
* Cross-reference streams are never encrypted; the writer now skips
|
|
64
|
+
them alongside the /Encrypt dictionary.
|
|
65
|
+
|
|
66
|
+
**Compatibility:** 128-bit files written by 0.8.7 will not decrypt
|
|
67
|
+
under 0.8.8 (they were non-interoperable outside pdfrb); re-encrypt.
|
|
68
|
+
|
|
69
|
+
## [0.8.7] — 2026-08-30
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
* **Silent plaintext writes** — the Writer's encrypter construction
|
|
74
|
+
rescued `StandardError` to `{}`, so any handler failure (wrong
|
|
75
|
+
password, unsupported filter) wrote encrypted documents as
|
|
76
|
+
plaintext. Behind that rescue: calls to a nonexistent
|
|
77
|
+
`verify_user_password` and keyword-args construction where the
|
|
78
|
+
handler takes a positional trailer hash. A wrong password now
|
|
79
|
+
raises `EncryptionError`.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
* `Writer.serializer_for(document)` is the single Serializer factory
|
|
84
|
+
(compression + encrypter); `Linearization::Writer` shares it
|
|
85
|
+
instead of a bare serializer that emitted encrypted documents
|
|
86
|
+
unencrypted.
|
|
87
|
+
* `serialize_indirect` encrypts dictionary strings with the
|
|
88
|
+
per-object key (s7.6.1), not just stream payloads; the /Encrypt
|
|
89
|
+
dictionary stays cleartext.
|
|
90
|
+
* Linearized trailers carry /Encrypt and /ID; the hint stream no
|
|
91
|
+
longer claims a fictitious `/Type /XRef`.
|
|
92
|
+
* Dead autoloads for nonexistent `Revision`/`Revisions` removed;
|
|
93
|
+
`DigitalSignature.handler_for` resolves handler classes lazily.
|
|
94
|
+
|
|
95
|
+
## [0.8.6] — 2026-08-30
|
|
96
|
+
|
|
97
|
+
### Fixed
|
|
98
|
+
|
|
99
|
+
* **Canvas `concat` with a block emitted `SaveGraphicsState` in its
|
|
100
|
+
`ensure` instead of `Restore`** — every block-form
|
|
101
|
+
translate/scale/rotate left an unbalanced `q` on the graphics
|
|
102
|
+
state stack. The block form now scopes the matrix inside
|
|
103
|
+
`save_graphics_state`; `with_transparency` reuses the same seam;
|
|
104
|
+
the draw_image trio folds onto one `invoke_xobject`.
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
|
|
108
|
+
* `Pdfrb::PdfVersion.compare` / `.at_least?` (extracted from
|
|
109
|
+
byte-identical private copies in Writer and Conformance::PdfX).
|
|
110
|
+
* `Document#each_indirect_of(klass)` — type-filtered object
|
|
111
|
+
iteration, Enumerator when blockless; 15 mechanical call sites
|
|
112
|
+
migrated.
|
|
113
|
+
|
|
114
|
+
## [0.8.5] — 2026-08-29
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
* Document/Font seams deepened: `Document#resolve` (dereference),
|
|
119
|
+
`Model::Object#ref`, `Pages#attach_resource`,
|
|
120
|
+
`Document::Fonts::Subsetting`, shared `Pdfrb::Font::Sfnt` module
|
|
121
|
+
(sfnt directory parsing/rebuild with real checksums) used by both
|
|
122
|
+
the TrueType and CFF subsetters.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
* TrueType subsetting regression: `rubocop -A` had rewritten
|
|
127
|
+
`sorted.each do |_tag, data|` on an Array to `each_value`,
|
|
128
|
+
silently falling back to the full font (773KB instead of 205KB).
|
|
129
|
+
|
|
130
|
+
## [0.8.4] — 2026-08-29
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
|
|
134
|
+
* `Model::Type::NameMap` concern extracted from 17 hand-duplicated
|
|
135
|
+
resource-map classes (symbol-or-string `[]`, normalizing `add`,
|
|
136
|
+
`names`, `each_entry`).
|
|
137
|
+
|
|
138
|
+
## [0.8.3] — 2026-08-28
|
|
139
|
+
|
|
140
|
+
### Added
|
|
141
|
+
|
|
142
|
+
* PDF/A generation section in the executable USAGE cookbook
|
|
143
|
+
(`spec/pdfrb/usage_doc_spec.rb` runs every snippet).
|
|
144
|
+
|
|
145
|
+
## [0.8.2] — 2026-08-28
|
|
146
|
+
|
|
147
|
+
### Added
|
|
148
|
+
|
|
149
|
+
* veraPDF round-trip sweep over the fixture corpus.
|
|
150
|
+
|
|
151
|
+
## [0.8.1] — 2026-08-27
|
|
152
|
+
|
|
153
|
+
### Added
|
|
154
|
+
|
|
155
|
+
* veraPDF cross-check integration (local `rake verapdf` + macOS CI
|
|
156
|
+
job) with PDF/A conformance fixes: trailer /ID, explicit page
|
|
157
|
+
/Resources, parseable XMP.
|
|
158
|
+
|
|
5
159
|
## [0.8.0] — 2026-08-27
|
|
6
160
|
|
|
7
161
|
### Added
|
data/docs/USAGE.md
CHANGED
|
@@ -153,18 +153,42 @@ File.binwrite("linearized.pdf", io.string)
|
|
|
153
153
|
```ruby
|
|
154
154
|
doc = Pdfrb::Document.new
|
|
155
155
|
doc.pages.add
|
|
156
|
+
doc.metadata[:Title] = "Confidential"
|
|
156
157
|
doc.encrypt!(user_password: "secret", owner_password: "owner", bits: 128)
|
|
157
158
|
doc.write("encrypted.pdf")
|
|
158
159
|
|
|
159
|
-
# Opening an encrypted document
|
|
160
|
+
# Opening an encrypted document — strings and streams decrypt on
|
|
161
|
+
# read; a wrong password raises Pdfrb::EncryptionError:
|
|
160
162
|
doc = Pdfrb.open("encrypted.pdf",
|
|
161
163
|
config: { "encryption.password" => "secret" })
|
|
164
|
+
doc.metadata[:Title] # => "Confidential"
|
|
162
165
|
|
|
163
166
|
# Removing encryption from a parsed document:
|
|
164
167
|
doc.decrypt!
|
|
165
168
|
doc.write("decrypted.pdf")
|
|
166
169
|
```
|
|
167
170
|
|
|
171
|
+
## Bookmarks (Outlines)
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
doc = Pdfrb::Document.new
|
|
175
|
+
page = doc.pages.add
|
|
176
|
+
font = doc.fonts.add("Helvetica")
|
|
177
|
+
page.canvas.text("Intro", at: [72, 720], font: font, size: 12)
|
|
178
|
+
|
|
179
|
+
chapter = doc.outline.add("Chapter 1", dest: :xyz)
|
|
180
|
+
chapter.add_child(Pdfrb::Document::OutlineEntry.new(title: "1.1", dest: :xyz))
|
|
181
|
+
doc.outline.add("Chapter 2", dest: :xyz)
|
|
182
|
+
doc.outline.build!
|
|
183
|
+
doc.write("bookmarked.pdf")
|
|
184
|
+
|
|
185
|
+
# Reading an outline back (depth-first, parents before children):
|
|
186
|
+
doc = Pdfrb.open("bookmarked.pdf")
|
|
187
|
+
doc.outline.each do |item|
|
|
188
|
+
puts item.title # "Chapter 1", "1.1", "Chapter 2"
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
168
192
|
## Merging PDFs
|
|
169
193
|
|
|
170
194
|
```ruby
|
|
@@ -68,33 +68,60 @@ module Pdfrb
|
|
|
68
68
|
hex_start = sig_info[:contents_hex_start]
|
|
69
69
|
hex_end = sig_info[:contents_hex_end]
|
|
70
70
|
contents_hex = pdf_bytes[hex_start...hex_end]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
71
|
+
|
|
72
|
+
# The /Contents placeholder is zero-padded to its reserved
|
|
73
|
+
# size; DER is self-delimiting, so der_prefix reads the
|
|
74
|
+
# declared length instead of guessing where the signature
|
|
75
|
+
# ends (blindly trimming trailing zeros also eats legitimate
|
|
76
|
+
# 0x00 final bytes and can split a hex byte).
|
|
77
|
+
verify_pkcs7(der_prefix([contents_hex].pack("H*")),
|
|
78
|
+
signed_data, trusted_certs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def verify_pkcs7(der, signed_data, trusted_certs)
|
|
82
|
+
pkcs7 = OpenSSL::PKCS7.new(der)
|
|
83
|
+
|
|
84
|
+
store = OpenSSL::X509::Store.new
|
|
85
|
+
trusted_certs.each { |c| store.add_cert(c) }
|
|
86
|
+
|
|
87
|
+
valid = pkcs7.verify(nil, store, signed_data,
|
|
88
|
+
OpenSSL::PKCS7::DETACHED |
|
|
89
|
+
OpenSSL::PKCS7::BINARY)
|
|
90
|
+
|
|
91
|
+
VerificationResult.new(
|
|
92
|
+
signer: pkcs7.signers.first&.issuer&.to_s,
|
|
93
|
+
valid?: valid,
|
|
94
|
+
byte_range_ok?: true,
|
|
95
|
+
cert_chain: pkcs7.certificates || [],
|
|
96
|
+
trusted?: !trusted_certs.empty? && valid,
|
|
97
|
+
error: valid ? nil : "signature verification failed",
|
|
98
|
+
)
|
|
99
|
+
rescue OpenSSL::PKCS7::PKCS7Error, ArgumentError => e
|
|
100
|
+
VerificationResult.new(valid?: false,
|
|
101
|
+
byte_range_ok?: true,
|
|
102
|
+
cert_chain: [],
|
|
103
|
+
error: e.message)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The DER structure starting at +bytes+ (a SEQUENCE), exactly
|
|
107
|
+
# as long as its own declared length - any placeholder padding
|
|
108
|
+
# after it is dropped. Non-SEQUENCE input passes through.
|
|
109
|
+
def der_prefix(bytes)
|
|
110
|
+
return bytes if bytes.bytesize < 2 || bytes.getbyte(0) != 0x30
|
|
111
|
+
|
|
112
|
+
length = bytes.getbyte(1)
|
|
113
|
+
offset = 2
|
|
114
|
+
if length.anybits?(0x80)
|
|
115
|
+
count = length & 0x7F
|
|
116
|
+
return bytes if count.zero? || bytes.bytesize < 2 + count
|
|
117
|
+
|
|
118
|
+
length = 0
|
|
119
|
+
count.times do |i|
|
|
120
|
+
length = (length << 8) | bytes.getbyte(2 + i)
|
|
121
|
+
end
|
|
122
|
+
offset = 2 + count
|
|
97
123
|
end
|
|
124
|
+
bytes.byteslice(0, offset + length)
|
|
98
125
|
end
|
|
99
126
|
end
|
|
100
127
|
end
|
data/lib/pdfrb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pdfrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: benchmark
|
|
@@ -1276,7 +1276,6 @@ files:
|
|
|
1276
1276
|
- lib/pdfrb/source/recovery.rb
|
|
1277
1277
|
- lib/pdfrb/source/token.rb
|
|
1278
1278
|
- lib/pdfrb/source/tokenizer.rb
|
|
1279
|
-
- lib/pdfrb/source/tokenizer.rb.bak
|
|
1280
1279
|
- lib/pdfrb/source/trailer_reader.rb
|
|
1281
1280
|
- lib/pdfrb/source/xref_stream_reader.rb
|
|
1282
1281
|
- lib/pdfrb/source/xref_table_reader.rb
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "set"
|
|
4
|
-
|
|
5
|
-
module Pdfrb
|
|
6
|
-
module Source
|
|
7
|
-
# Byte-level PDF lexer (s7.2). State machine that emits Token
|
|
8
|
-
# values via +next_token+ / +peek+. Pull-based so the Parser can
|
|
9
|
-
# stream through arbitrarily large PDFs without materialising the
|
|
10
|
-
# whole token stream.
|
|
11
|
-
#
|
|
12
|
-
# States:
|
|
13
|
-
# :top — between tokens; dispatch by first byte.
|
|
14
|
-
# :string — inside (...) with depth tracking and \ escapes.
|
|
15
|
-
# :hexstring — inside <...> (after distinguishing << as dict open).
|
|
16
|
-
# :comment — inside %... until EOL.
|
|
17
|
-
#
|
|
18
|
-
# The keyword set per s7.2: obj/endobj/stream/endstream/xref/
|
|
19
|
-
# startxref/trailer/true/false/null. The Tokenizer emits them with
|
|
20
|
-
# type :keyword and value equal to the keyword string; the Parser
|
|
21
|
-
# dispatches on the value.
|
|
22
|
-
class Tokenizer
|
|
23
|
-
WHITESPACE_BYTES = PdfConstants::WHITESPACE.bytes.to_set.freeze
|
|
24
|
-
DELIMITER_BYTES = PdfConstants::DELIMITERS.bytes.to_set.freeze
|
|
25
|
-
|
|
26
|
-
# Public so the Parser can recognise the same set when scanning
|
|
27
|
-
# post-stream whitespace.
|
|
28
|
-
public_constant :WHITESPACE_BYTES
|
|
29
|
-
public_constant :DELIMITER_BYTES
|
|
30
|
-
|
|
31
|
-
attr_reader :pos, :io
|
|
32
|
-
|
|
33
|
-
def initialize(io)
|
|
34
|
-
@io = io
|
|
35
|
-
@pos = 0
|
|
36
|
-
@pushback = []
|
|
37
|
-
@lookahead = []
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def next_token
|
|
41
|
-
return @pushback.pop unless @pushback.empty?
|
|
42
|
-
|
|
43
|
-
fill_lookahead(1) if @lookahead.empty?
|
|
44
|
-
@lookahead.shift
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def peek(offset = 0)
|
|
48
|
-
fill_lookahead(offset + 1)
|
|
49
|
-
@lookahead[offset]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def pushback(token)
|
|
53
|
-
@pushback << token
|
|
54
|
-
self
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def fill_lookahead(n)
|
|
60
|
-
while @lookahead.length < n
|
|
61
|
-
tok = lex_one
|
|
62
|
-
break if tok.nil?
|
|
63
|
-
|
|
64
|
-
@lookahead << tok
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def lex_one
|
|
69
|
-
skip_whitespace_and_comments
|
|
70
|
-
return nil if eos?
|
|
71
|
-
|
|
72
|
-
start_pos = @pos
|
|
73
|
-
b = peek_byte
|
|
74
|
-
case b
|
|
75
|
-
when 40 then read_string(start_pos)
|
|
76
|
-
when 60
|
|
77
|
-
peek_byte(1) == 60 ? read_two_byte_token(:dict_open, "<<", start_pos) : read_hex_string(start_pos)
|
|
78
|
-
when 62
|
|
79
|
-
peek_byte(1) == 62 ? read_two_byte_token(:dict_close, ">>", start_pos) : raise(lex_error("lone '>' at #{start_pos}"))
|
|
80
|
-
when 91 then advance_byte && emit(:array_open, "[", start_pos)
|
|
81
|
-
when 93 then advance_byte && emit(:array_close, "]", start_pos)
|
|
82
|
-
when 47 then read_name(start_pos)
|
|
83
|
-
when 43, 45, 46, *digit_bytes then read_number_or_keyword(start_pos)
|
|
84
|
-
else read_keyword(start_pos)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def digit_bytes
|
|
89
|
-
(48..57).to_a
|
|
90
|
-
end
|
|
91
|
-
private :digit_bytes
|
|
92
|
-
|
|
93
|
-
def eos?
|
|
94
|
-
@io.eof?
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def peek_byte(offset = 0)
|
|
98
|
-
cur = @io.pos
|
|
99
|
-
if offset.zero?
|
|
100
|
-
b = @io.getbyte
|
|
101
|
-
@io.seek(cur, IO::SEEK_SET) if b
|
|
102
|
-
return b
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
@io.seek(cur + offset, IO::SEEK_SET)
|
|
106
|
-
b = @io.getbyte
|
|
107
|
-
@io.seek(cur, IO::SEEK_SET)
|
|
108
|
-
b
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def advance_byte
|
|
112
|
-
@pos += 1
|
|
113
|
-
@io.getbyte
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Append an Integer byte to +buf+ without re-encoding through
|
|
117
|
-
# UTF-8 (which is the default for `+""` in a UTF-8 source file).
|
|
118
|
-
def append_byte(buf, b)
|
|
119
|
-
buf << b.chr(Encoding::BINARY)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def skip_whitespace_and_comments
|
|
123
|
-
loop do
|
|
124
|
-
break if eos?
|
|
125
|
-
|
|
126
|
-
b = peek_byte
|
|
127
|
-
if WHITESPACE_BYTES.include?(b)
|
|
128
|
-
advance_byte
|
|
129
|
-
elsif b == 37 # %
|
|
130
|
-
skip_comment
|
|
131
|
-
else
|
|
132
|
-
break
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def skip_comment
|
|
138
|
-
until eos? || peek_byte == 10 # \n
|
|
139
|
-
advance_byte
|
|
140
|
-
end
|
|
141
|
-
advance_byte if !eos? && peek_byte == 10
|
|
142
|
-
nil
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def read_two_byte_token(type, literal, start_pos)
|
|
146
|
-
advance_byte
|
|
147
|
-
advance_byte
|
|
148
|
-
emit(type, literal, start_pos)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def read_string(start_pos)
|
|
152
|
-
advance_byte # consume (
|
|
153
|
-
depth = 1
|
|
154
|
-
bytes = +""
|
|
155
|
-
until eos?
|
|
156
|
-
b = advance_byte
|
|
157
|
-
case b
|
|
158
|
-
when 92 # backslash
|
|
159
|
-
esc = read_string_escape
|
|
160
|
-
bytes << esc if esc >= 0
|
|
161
|
-
when 40 # (
|
|
162
|
-
depth += 1
|
|
163
|
-
bytes << 40
|
|
164
|
-
when 41 # )
|
|
165
|
-
depth -= 1
|
|
166
|
-
break if depth.zero?
|
|
167
|
-
|
|
168
|
-
bytes << 41
|
|
169
|
-
else
|
|
170
|
-
bytes << b
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
raise lex_error("unterminated string at #{start_pos}") if depth.positive?
|
|
174
|
-
|
|
175
|
-
emit(:string, bytes.force_encoding(Encoding::BINARY), start_pos)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def read_string_escape
|
|
179
|
-
b = advance_byte
|
|
180
|
-
case b
|
|
181
|
-
when 110 then 10 # \n
|
|
182
|
-
when 114 then 13 # \r
|
|
183
|
-
when 116 then 9 # \t
|
|
184
|
-
when 98 then 8 # \b
|
|
185
|
-
when 102 then 12 # \f
|
|
186
|
-
when 40 then 40 # \(
|
|
187
|
-
when 41 then 41 # \)
|
|
188
|
-
when 92 then 92 # \\
|
|
189
|
-
when 48..55 then read_octal_escape(b)
|
|
190
|
-
when 10, 13
|
|
191
|
-
# Line continuation — eat CRLF/LF/CR; emit no byte.
|
|
192
|
-
advance_byte if b == 13 && peek_byte == 10
|
|
193
|
-
-1
|
|
194
|
-
else b
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def read_octal_escape(first)
|
|
199
|
-
digits = [first]
|
|
200
|
-
2.times do
|
|
201
|
-
break if eos?
|
|
202
|
-
|
|
203
|
-
b = peek_byte
|
|
204
|
-
break unless (48..55).cover?(b)
|
|
205
|
-
|
|
206
|
-
digits << advance_byte
|
|
207
|
-
end
|
|
208
|
-
digits.map(&:chr).join.to_i(8) & 0xFF
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def read_hex_string(start_pos)
|
|
212
|
-
advance_byte # consume <
|
|
213
|
-
bytes = +""
|
|
214
|
-
hi = nil
|
|
215
|
-
until eos?
|
|
216
|
-
b = advance_byte
|
|
217
|
-
break if b == 62 # >
|
|
218
|
-
|
|
219
|
-
next if WHITESPACE_BYTES.include?(b)
|
|
220
|
-
|
|
221
|
-
nibble = hex_value(b)
|
|
222
|
-
raise lex_error("bad hex digit #{b.chr.inspect} at #{@pos}") unless nibble
|
|
223
|
-
|
|
224
|
-
if hi.nil?
|
|
225
|
-
hi = nibble
|
|
226
|
-
else
|
|
227
|
-
bytes << (hi * 16 + nibble)
|
|
228
|
-
hi = nil
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
bytes << (hi * 16 + 0) if hi # odd nibble padded with 0
|
|
232
|
-
emit(:hex_string, bytes.force_encoding(Encoding::BINARY), start_pos)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def hex_value(b)
|
|
236
|
-
return (b - 48) if (48..57).cover?(b)
|
|
237
|
-
return (b - 55) if (65..70).cover?(b)
|
|
238
|
-
return (b - 87) if (97..102).cover?(b)
|
|
239
|
-
|
|
240
|
-
nil
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def read_name(start_pos)
|
|
244
|
-
advance_byte # consume /
|
|
245
|
-
bytes = +""
|
|
246
|
-
until eos?
|
|
247
|
-
b = peek_byte
|
|
248
|
-
break if WHITESPACE_BYTES.include?(b) || DELIMITER_BYTES.include?(b)
|
|
249
|
-
|
|
250
|
-
advance_byte
|
|
251
|
-
if b == 35 # # — expect two hex digits
|
|
252
|
-
hi = peek_byte
|
|
253
|
-
lo = peek_byte(1)
|
|
254
|
-
if hi && lo && (n1 = hex_value(hi)) && (n2 = hex_value(lo))
|
|
255
|
-
advance_byte; advance_byte
|
|
256
|
-
bytes << (n1 * 16 + n2)
|
|
257
|
-
else
|
|
258
|
-
bytes << 35
|
|
259
|
-
end
|
|
260
|
-
else
|
|
261
|
-
bytes << b
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
emit(:name, bytes.force_encoding(Encoding::BINARY), start_pos)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def read_number_or_keyword(start_pos)
|
|
268
|
-
bytes = +""
|
|
269
|
-
bytes << advance_byte # first char
|
|
270
|
-
until eos?
|
|
271
|
-
b = peek_byte
|
|
272
|
-
break if WHITESPACE_BYTES.include?(b) || DELIMITER_BYTES.include?(b)
|
|
273
|
-
|
|
274
|
-
advance_byte
|
|
275
|
-
bytes << b
|
|
276
|
-
end
|
|
277
|
-
emit_number_or_keyword(bytes, start_pos)
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def read_keyword(start_pos)
|
|
281
|
-
bytes = +""
|
|
282
|
-
until eos?
|
|
283
|
-
b = peek_byte
|
|
284
|
-
break if WHITESPACE_BYTES.include?(b) || DELIMITER_BYTES.include?(b)
|
|
285
|
-
|
|
286
|
-
advance_byte
|
|
287
|
-
bytes << b
|
|
288
|
-
end
|
|
289
|
-
emit_keyword(bytes, start_pos)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def emit_number_or_keyword(bytes, start_pos)
|
|
293
|
-
if bytes.match?(/\A[-+]?\d+\z/)
|
|
294
|
-
emit(:integer, bytes.to_i, start_pos)
|
|
295
|
-
elsif bytes.match?(/\A[-+]?(\d*\.\d+|\d+\.?\d*)(?:[eE][-+]?\d+)?\z/) && (bytes.include?(".") || bytes =~ /[eE]/)
|
|
296
|
-
emit(:real, bytes.to_f, start_pos)
|
|
297
|
-
else
|
|
298
|
-
emit_keyword(bytes, start_pos)
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def emit_keyword(bytes, start_pos)
|
|
303
|
-
case bytes
|
|
304
|
-
when "true" then emit(:true, true, start_pos)
|
|
305
|
-
when "false" then emit(:false, false, start_pos)
|
|
306
|
-
when "null" then emit(:null, nil, start_pos)
|
|
307
|
-
else emit(:keyword, bytes, start_pos)
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def emit(type, value, position)
|
|
312
|
-
Token.new(type: type, value: value, position: position)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def lex_error(message)
|
|
316
|
-
Pdfrb::LexError.new(message, source_position: @pos)
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
end
|