hexapdf 1.9.1 → 1.10.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e272dafd41bb732e5110aadc794399a3e5d9823571de2ee67f119f05422af9ac
4
- data.tar.gz: 1269ed888e0b5c3fac4b4a25afbcdab75835252aadaf46fe6a92cfbde379f330
3
+ metadata.gz: 0c85f671cd9d2e469b7f0e8c1ea2f9beec6a09247881c2404b8c9789ba65cd03
4
+ data.tar.gz: e6d53dd3da7a366f5b724448d31bc7edacae9acb65f6f4fe95e6e3c5caf27564
5
5
  SHA512:
6
- metadata.gz: 69f36a31e49cb7b434bc53e63c2fde4c271a506046224a0dbb76b473176f435d618d0e9de90a64f3b95c4fa64cec8a8addbcb4c5c997ac9790fd79bd46093306
7
- data.tar.gz: 786835f800b29263364d97c872465b7fcaf431bf846a793a3fe502810090bc6ce1b94089a18f2787ed5cc3e9b982cf0a2d33db68b7812861f938e4f209af17d4
6
+ metadata.gz: 2572d491909fa5f4574705de076a2889d7a0869f6cb3358787394b02ed00ec7e0763617515f9f2ecb50718f6b584189f88e0dad0ab612f0472b8551402331628
7
+ data.tar.gz: cfe5c5efeaa319b781c566a3d1b0034bf1876ddc8100fecdb96257f68064779476f579e266e796b5a5740177b899319c1fde9e4f21b355d14054e5f157cb3e30
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## 1.10.0 - 2026-08-21
2
+
3
+ ### Added
4
+
5
+ * Support for PDF 2.0 UTF-8 strings to
6
+ [HexaPDF::DictionaryFields::StringConverter]
7
+ * [HexaPDF::Type::Catalog#dss] for returning the document security store
8
+ * [HexaPDF::DigitalSignature::PKCS1Handler#embedded_tsa_signature]
9
+ * [HexaPDF::Type::DocumentSecurityStore] convenience methods for adding entries
10
+
11
+ ### Fixed
12
+
13
+ * Removed invalid dictionary fields from [HexaPDF::Type::DocumentSecurityStore]
14
+ * [HexaPDF::DigitalSignature::CMSHandler] to handle padding of /Contents
15
+ correctly
16
+ * Regression in [HexaPDF::Layout::TextFragment::create_with_fallback_glyphs]
17
+ with respect to handling `\r\n` as a single newline
18
+ * Double decryption when reconstructing invalid files
19
+ * Parsing encrypted indirect objects that should be streams but aren't
20
+ * Serializing of large floats that used scientific instead of fixed-point
21
+ notation
22
+ * [HexaPDF::Content::SmartTextExtractor::layout_text_runs] to work in case of
23
+ degenerate input where the majority of text runs have zero height
24
+
25
+
1
26
  ## 1.9.1 - 2026-06-09
2
27
 
3
28
  ### Fixed
@@ -254,8 +254,8 @@ module HexaPDF
254
254
  # In nearly all cases this option should not be changed from its default setting!
255
255
  #
256
256
  # document.on_invalid_string::
257
- # A callable object that takes the invalid UTF-16BE encoded string and returns a valid UTF-8
258
- # encoded string.
257
+ # A callable object that takes the invalid UTF-16BE or UTF-8 encoded string and returns a valid
258
+ # UTF-8 encoded string.
259
259
  #
260
260
  # The default is to remove all invalid characters.
261
261
  #
@@ -172,8 +172,9 @@ module HexaPDF
172
172
  return '' if text_runs.empty?
173
173
 
174
174
  # Use the median height of all text runs as an approximation of the main font size used on
175
- # the page. The line tolerance uses a hard floor for small fonts.
176
- median_height = median(text_runs.map(&:height).sort)
175
+ # the page. In case the majority of text runs have a height of 0, use a non-zero height
176
+ # value. The line tolerance uses a hard floor for small fonts.
177
+ median_height = [median(text_runs.map(&:height).sort), 1].max
177
178
  line_tolerance = [median_height * line_tolerance_factor, 2].max
178
179
 
179
180
  # Group the text runs into lines which are sorted top to bottom. Text runs are pre-sorted by
@@ -262,6 +262,13 @@ module HexaPDF
262
262
  else
263
263
  document.config['document.on_invalid_string'].call(str)
264
264
  end
265
+ elsif str.getbyte(0) == 239 && str.getbyte(1) == 187 && str.getbyte(2) == 191
266
+ str = str[3..-1].force_encoding(Encoding::UTF_8)
267
+ if str.valid_encoding?
268
+ str
269
+ else
270
+ document.config['document.on_invalid_string'].call(str)
271
+ end
265
272
  else
266
273
  Utils::PDFDocEncoding.convert_to_utf8(str)
267
274
  end
@@ -92,7 +92,7 @@ module HexaPDF
92
92
  return @embedded_tsa_signature if defined?(@embedded_tsa_signature)
93
93
 
94
94
  @embedded_tsa_signature = nil
95
- p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
95
+ p7 = decode_asn1(signature_dict.contents)
96
96
  signed_data = p7.value[1].value[0]
97
97
  signer_info = signed_data.value[-1].value[0] # first (and only) signer info
98
98
  return unless signer_info.value[-1].tag == 1 # check for unsigned attributes
@@ -140,7 +140,7 @@ module HexaPDF
140
140
 
141
141
  if signature_dict.signature_type == 'ETSI.RFC3161'
142
142
  # Getting the needed values is not directly supported by Ruby OpenSSL
143
- p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
143
+ p7 = decode_asn1(signature_dict.contents)
144
144
  signed_data = p7.value[1].value[0]
145
145
  content_info = signed_data.value[2]
146
146
  content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
@@ -175,6 +175,31 @@ module HexaPDF
175
175
  result
176
176
  end
177
177
 
178
+ private
179
+
180
+ # Decode the first data structure in the given +data+ binary string.
181
+ #
182
+ # Since ASN1.decode raises an error if there are trailing bytes in +data+, we need to try
183
+ # several things to get the first data structure out of +data+ that is possibly zero-padded
184
+ # (due to definite and indefinite encodings; \x00\x00 is the EOD marker for indefinite
185
+ # encodings complicating things).
186
+ def decode_asn1(data)
187
+ length = OpenSSL::ASN1.traverse(data) do |_depth, _offset, header_length, length, *|
188
+ break length > 0 ? header_length + length : 0
189
+ end
190
+ if length > 0
191
+ OpenSSL::ASN1.decode(data[0, length])
192
+ else
193
+ begin
194
+ OpenSSL::ASN1.decode(data)
195
+ rescue OpenSSL::ASN1::ASN1Error => e
196
+ length = e.message.scan(/Total bytes read: (\d+)/)&.first&.first.to_i
197
+ data = data[0, length]
198
+ retry
199
+ end
200
+ end
201
+ end
202
+
178
203
  end
179
204
 
180
205
  end
@@ -59,6 +59,11 @@ module HexaPDF
59
59
  certificate_chain.first
60
60
  end
61
61
 
62
+ # Returns +nil+.
63
+ def embedded_tsa_signature
64
+ nil
65
+ end
66
+
62
67
  # Verifies the signature using the provided OpenSSL::X509::Store object.
63
68
  def verify(store, allow_self_signed: false)
64
69
  result = super
@@ -276,7 +276,9 @@ module HexaPDF
276
276
  str.replace(string_algorithm.decrypt(key, str, &error_proc))
277
277
  end
278
278
 
279
- if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream.filter[0] != :Crypt
279
+ # The (obj.raw_stream == '') case may occur for PDFs where a typed object that should be a
280
+ # stream isn't one. For example, if a /Type /Form object doesn't have stream ... endstream.
281
+ if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream != '' && obj.raw_stream.filter[0] != :Crypt
280
282
  unless string_algorithm == stream_algorithm
281
283
  key = object_key(obj.oid, obj.gen, stream_algorithm)
282
284
  end
@@ -119,7 +119,11 @@ module HexaPDF
119
119
  items = []
120
120
  end
121
121
  if glyph.control_char?
122
- result.append(new([glyph], style))
122
+ if result.last&.style == style
123
+ result.last.items << glyph
124
+ else
125
+ result.append(new([glyph], style))
126
+ end
123
127
  else
124
128
  fallback = yield(codepoint, glyph)
125
129
  unless fallback.empty?
@@ -531,8 +531,7 @@ module HexaPDF
531
531
 
532
532
  loader = lambda do |xref_entry|
533
533
  obj, oid, gen, stream = parse_indirect_object(xref_entry.pos)
534
- obj = @document.wrap(obj, oid: oid, gen: gen, stream: stream)
535
- @document.security_handler ? @document.security_handler.decrypt(obj) : obj
534
+ @document.wrap(obj, oid: oid, gen: gen, stream: stream)
536
535
  end
537
536
 
538
537
  @in_reconstruct_revision = false
@@ -206,7 +206,7 @@ module HexaPDF
206
206
  if -0.0001 < obj && obj < 0.0001 && obj != 0
207
207
  sprintf("%.6f", obj)
208
208
  elsif obj.finite?
209
- obj.round(6).to_s
209
+ obj.clamp(-999999999999999, 999999999999999).round(6).to_s
210
210
  else
211
211
  raise HexaPDF::Error, "Can't serialize special floating point number #{obj}"
212
212
  end
@@ -84,7 +84,7 @@ module HexaPDF
84
84
  define_field :Requirements, type: PDFArray, version: '1.7'
85
85
  define_field :Collection, type: Dictionary, version: '1.7'
86
86
  define_field :NeedsRendering, type: Boolean, version: '1.7'
87
- define_field :DSS, type: Dictionary, version: '2.0'
87
+ define_field :DSS, type: :DSS, version: '2.0'
88
88
  define_field :AF, type: PDFArray, version: '2.0'
89
89
  define_field :DPartRoot, type: Dictionary, version: '2.0'
90
90
 
@@ -124,6 +124,13 @@ module HexaPDF
124
124
  self[:OCProperties] ||= document.add({OCGs: [], D: {Creator: 'HexaPDF'}}, type: :XXOCProperties)
125
125
  end
126
126
 
127
+ # Returns the document security store, creating it if needed.
128
+ #
129
+ # See: DocumentSecurityStore
130
+ def dss
131
+ self[:DSS] ||= document.add({}, type: :DSS)
132
+ end
133
+
127
134
  # Returns the main AcroForm object.
128
135
  #
129
136
  # * If an AcroForm object exists, the +create+ argument is not used.
@@ -34,6 +34,7 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
+ require 'openssl'
37
38
  require 'hexapdf/dictionary'
38
39
 
39
40
  module HexaPDF
@@ -54,25 +55,78 @@ module HexaPDF
54
55
  define_type :VRI
55
56
 
56
57
  define_field :Type, type: Symbol, default: type
57
- define_field :Cert, type: PDFArray
58
- define_field :CRL, type: PDFArray
59
- define_field :OCSP, type: PDFArray
60
- define_field :TU, type: PDFDate
61
- define_field :TS, type: Stream
58
+ define_field :Cert, type: PDFArray, version: '2.0'
59
+ define_field :CRL, type: PDFArray, version: '2.0'
60
+ define_field :OCSP, type: PDFArray, version: '2.0'
61
+ define_field :TU, type: PDFDate, version: '2.0'
62
+ define_field :TS, type: Stream, version: '2.0'
62
63
 
63
64
  end
64
65
 
65
66
  define_type :DSS
66
67
 
67
68
  define_field :Type, type: Symbol, default: type
68
- define_field :VRI, type: Dictionary
69
- define_field :Certs, type: PDFArray
70
- define_field :OCSPs, type: PDFArray
71
- define_field :CRLs, type: PDFArray
72
- define_field :SW, type: Symbol, default: :A, allowed_values: [:A, :B, :S, :N]
73
- define_field :S, type: Symbol, default: :P, allowed_values: [:A, :P]
74
- define_field :A, type: PDFArray, default: [0.5, 0.5]
75
- define_field :FB, type: Boolean, default: false, version: '1.5'
69
+ define_field :VRI, type: Dictionary, version: '2.0'
70
+ define_field :Certs, type: PDFArray, version: '2.0'
71
+ define_field :OCSPs, type: PDFArray, version: '2.0'
72
+ define_field :CRLs, type: PDFArray, version: '2.0'
73
+
74
+ # Adds validation data for a single signature to the /VRI dictionary and to the various
75
+ # arrays, and returns the new VRI entry.
76
+ #
77
+ # +signature+::
78
+ # The signature for which the validation data should be added.
79
+ #
80
+ # +certs+::
81
+ # Array of DER-encoded certificates.
82
+ #
83
+ # +ocsps+::
84
+ # Array of DER-encoded OCSP responses.
85
+ #
86
+ # +crls+::
87
+ # Array of DER-encoded CRLs.
88
+ def add_vri(signature, certs: [], ocsps: [], crls: [])
89
+ key = OpenSSL::Digest::SHA1.hexdigest(signature.contents).upcase.to_sym
90
+ vri = {Type: :VRI}
91
+ vri[:Cert] = certs.map {|der| add_cert(der) } unless certs.empty?
92
+ vri[:OCSP] = ocsps.map {|der| add_ocsp(der) } unless ocsps.empty?
93
+ vri[:CRL] = crls.map {|der| add_crl(der) } unless crls.empty?
94
+ (self[:VRI] ||= {})[key] = document.add(vri)
95
+ end
96
+
97
+ # Adds the DER-encoded certificate to the /Certs array if not already present and returns
98
+ # the stream object containing it.
99
+ def add_cert(cert_der)
100
+ add_data_as_stream_to_field(cert_der, :Certs)
101
+ end
102
+
103
+ # Adds the DER-encoded OCSP response to the /OCSPs array if not already present and returns
104
+ # the stream object containing it.
105
+ def add_ocsp(ocsp_der)
106
+ add_data_as_stream_to_field(ocsp_der, :OCSPs)
107
+ end
108
+
109
+ # Adds the DER-encoded CRL to the /CRLs array if not already present and returns the stream
110
+ # object containing it.
111
+ def add_crl(crl_der)
112
+ add_data_as_stream_to_field(crl_der, :CRLs)
113
+ end
114
+
115
+ private
116
+
117
+ # Adds the given string +data+ to the +field+ array as stream object and returns the resulting
118
+ # stream.
119
+ #
120
+ # If the field already contains a stream with the same data, this existing stream is returned.
121
+ def add_data_as_stream_to_field(data, field)
122
+ self[field] ||= []
123
+ existing_stream = self[field].find {|stream| stream.stream == data }
124
+ return existing_stream if existing_stream
125
+
126
+ stream = document.add({Filter: :FlateDecode}, stream: data)
127
+ self[field] << stream
128
+ stream
129
+ end
76
130
 
77
131
  end
78
132
 
@@ -146,7 +146,7 @@ module HexaPDF
146
146
  registry = system_info[:Registry]
147
147
  ordering = system_info[:Ordering]
148
148
  if (encoding.kind_of?(Symbol) && HexaPDF::Font::CMap.predefined?(encoding.to_s) &&
149
- encoding != :'Identity-H' && encoding != :'Identity-V') ||
149
+ encoding != :'Identity-H' && encoding != :'Identity-V') ||
150
150
  (registry == "Adobe" && ['GB1', 'CNS1', 'Japan1', 'Korea1'].include?(ordering))
151
151
  HexaPDF::Font::CMap.for_name("#{registry}-#{ordering}-UCS2")
152
152
  end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '1.9.1'
40
+ VERSION = '1.10.0'
41
41
 
42
42
  end
@@ -101,6 +101,13 @@ describe HexaPDF::Content::SmartTextExtractor do
101
101
  ['Foot', 50, 10, 66, 20]]))
102
102
  end
103
103
 
104
+
105
+ it "works in case the majority of text runs have zero height" do
106
+ assert_equal(["Hello", "World News"].join("\n"*17), layout_runs([['World', 50, 80, 70, 80],
107
+ ['News', 75, 80, 100, 80],
108
+ ['Hello', 50, 100, 70, 100]]))
109
+ end
110
+
104
111
  it "ignores outliers when calculating the normal line spacing" do
105
112
  assert_equal("Hello\nWorld\n\n\n\nHere",
106
113
  layout_runs([['Hello', 50, 100, 70, 110],
@@ -125,7 +125,9 @@ describe HexaPDF::DigitalSignature::CMSHandler do
125
125
  fac.serial_number = 1
126
126
  fac.allowed_digests = ["sha256", "sha512"]
127
127
  res = fac.create_timestamp(CERTIFICATES.signer_key, CERTIFICATES.timestamp_certificate, req)
128
- @dict.contents = res.token.to_der
128
+ der_form = res.token.to_der
129
+ # Convert DER to BER with indefinite length encoding as variation
130
+ @dict.contents = der_form[0] << "\x80".b << der_form[4..-1] << "\x00\x00".b << "\x00\x00\x00\x00".b
129
131
  @dict.signature_type = 'ETSI.RFC3161'
130
132
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
131
133
 
@@ -151,7 +153,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
151
153
  key: CERTIFICATES.signer_key, timestamp_handler: tsh,
152
154
  certificates: [CERTIFICATES.ca_certificate]
153
155
  )
154
- @dict.contents = cms.to_der
156
+ @dict.contents = cms.to_der << "\x00\x00\x00\x00"
155
157
  @dict.signed_data = @data
156
158
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
157
159
  end
@@ -28,6 +28,10 @@ describe HexaPDF::DigitalSignature::PKCS1Handler do
28
28
  assert_equal(CERTIFICATES.signer_certificate, @handler.signer_certificate)
29
29
  end
30
30
 
31
+ it "returns nil for the embedded timestamp signature" do
32
+ assert_nil(@handler.embedded_tsa_signature)
33
+ end
34
+
31
35
  describe "verify" do
32
36
  before do
33
37
  @store = OpenSSL::X509::Store.new
@@ -302,6 +302,18 @@ describe HexaPDF::Encryption::SecurityHandler do
302
302
  assert_equal('string', obj.stream)
303
303
  end
304
304
 
305
+ it "handles decryption of should-be stream objects without actual streams" do
306
+ doc = HexaPDF::Document.new
307
+ obj = doc.add({}) # Create XObject as Dictionary and not as Stream
308
+ obj[:Type] = :XObject
309
+ obj[:Subtype] = :Form
310
+ doc.catalog[:Test] = obj
311
+ doc.encrypt
312
+ doc = HexaPDF::Document.new(io: StringIO.new(doc.write_to_string))
313
+ assert_kind_of(HexaPDF::Stream, doc.catalog[:Test])
314
+ assert_equal('', doc.catalog[:Test].raw_stream)
315
+ end
316
+
305
317
  it "doesn't decrypt a document's Encrypt dictionaries" do
306
318
  @document = HexaPDF::Document.new
307
319
  @document.trailer[:Encrypt] = @document.add({Key: "Something"})
@@ -43,10 +43,18 @@ describe HexaPDF::Layout::TextFragment do
43
43
 
44
44
  it "allows using a style object instead of directly specifying style properties" do
45
45
  style = HexaPDF::Layout::Style.new(font: @font, font_size: 20)
46
- frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("Tom", style)
46
+ frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("Tom", style) {}
47
47
  assert_equal(37.78, frags[0].width)
48
48
  end
49
49
 
50
+ it "handles control characters separately by not running them through the text shaper" do
51
+ frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("\tA\nB\rC\r\nD", font: @font) {}
52
+ assert_equal(5, frags.size)
53
+ assert_equal("\t", frags[0].text)
54
+ assert_equal("A\n", frags[1].text)
55
+ assert_equal("C\r\n", frags[3].text)
56
+ end
57
+
50
58
  it "replaces invalid glyphs with the result of the block" do
51
59
  zapf_dingbats = @doc.fonts.add('ZapfDingbats')
52
60
  i = 0
@@ -62,7 +70,7 @@ describe HexaPDF::Layout::TextFragment do
62
70
 
63
71
  frags = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs("✂Tom✂Tom✂Tom✂Tom\u{ad}",
64
72
  style, &fallback)
65
- assert_equal(8, frags.size)
73
+ assert_equal(7, frags.size)
66
74
  assert_equal(zapf_dingbats, frags[0].style.font)
67
75
  assert_equal(:a2, frags[0].items[0].name)
68
76
  assert_equal("Tom", frags[1].text)
@@ -71,8 +79,7 @@ describe HexaPDF::Layout::TextFragment do
71
79
  assert_equal("Tom", frags[3].text)
72
80
  assert_equal(:'.notdef', frags[4].items[0].name)
73
81
  assert_equal("Tom", frags[5].text)
74
- assert_equal("Tom", frags[6].text)
75
- assert_equal("\u{ad}", frags[7].text)
82
+ assert_equal("Tom\u{ad}", frags[6].text)
76
83
  end
77
84
  end
78
85
 
@@ -129,6 +129,11 @@ describe HexaPDF::DictionaryFields do
129
129
  str = @field.convert("\xfe\xff\x00t\x00e\x00s\x00t".b, self)
130
130
  assert_equal('test', str)
131
131
  assert_equal(Encoding::UTF_8, str.encoding)
132
+
133
+ str = @field.convert("\xef\xbb\xbfHall\xC3\xB6".b, self)
134
+ assert_equal('Hallö', str)
135
+ assert_equal(Encoding::UTF_8, str.encoding)
136
+
132
137
  str = @field.convert("Testing\x9c\x92".b, self)
133
138
  assert_equal("Testing\u0153\u2122", str)
134
139
  assert_equal(Encoding::UTF_8, str.encoding)
@@ -141,6 +146,9 @@ describe HexaPDF::DictionaryFields do
141
146
  it "calls document.on_invalid_string if the provided string is invalid" do
142
147
  str = "\xfe\xff\xD8\x00\x00s\x00t".b
143
148
  assert_equal("st", @field.convert(str, self))
149
+
150
+ str = "\xef\xbb\xbfHall\xFF\xC3".b
151
+ assert_equal("Hall", @field.convert(str, self))
144
152
  end
145
153
  end
146
154
 
@@ -702,15 +702,6 @@ describe HexaPDF::Parser do
702
702
  assert_equal(6, @parser.load_object(@xref).value)
703
703
  end
704
704
 
705
- it "uses a security handler for decrypting indirect objects if necessary" do
706
- handler = Minitest::Mock.new
707
- handler.expect(:decrypt, HexaPDF::Object.new(:result, oid: 1), [HexaPDF::Object])
708
- @document.instance_variable_set(:@security_handler, handler)
709
- create_parser("1 0 obj\n6\nendobj\ntrailer\n<</Size 1>>")
710
- assert_equal(:result, @parser.load_object(@xref).value)
711
- assert(handler.verify)
712
- end
713
-
714
705
  it "ignores parts where the starting line is split across lines" do
715
706
  create_parser("1 0 obj\n5\nendobj\n1 0\nobj\n6\nendobj\ntrailer\n<</Size 1>>")
716
707
  assert_equal(5, @parser.load_object(@xref).value)
@@ -774,6 +765,31 @@ describe HexaPDF::Parser do
774
765
  assert_equal({Root: HexaPDF::Reference.new(1, 0)}, @parser.reconstructed_revision.trailer.value)
775
766
  end
776
767
 
768
+ it "decrypts strings and streams correctly in case of whole/part document recovery" do
769
+ io = StringIO.new
770
+ doc = HexaPDF::Document.new
771
+ doc.catalog[:XXTest] = doc.add({Data: "string"}, stream: "stream")
772
+ doc.encrypt
773
+ xref_section = doc.write(io)[1]
774
+ test_oid = doc.catalog[:XXTest].oid
775
+
776
+ # whole document corruption
777
+ invalid_doc = io.string.sub(/xref\n0 #{xref_section.max_oid + 1}/, "xref\n0 1")
778
+ doc = HexaPDF::Document.new(io: StringIO.new(invalid_doc))
779
+ assert(doc.revisions.parser.reconstructed?)
780
+ assert_equal('string', doc.catalog[:XXTest][:Data])
781
+ assert_equal('stream', doc.catalog[:XXTest].stream)
782
+
783
+ # part document corruption
784
+ invalid_doc = io.string.sub(xref_section[test_oid].pos.to_s.rjust(10, '0'),
785
+ (xref_section[test_oid].pos + 5).to_s.rjust(10, '0'))
786
+ doc = HexaPDF::Document.new(io: StringIO.new(invalid_doc))
787
+ refute(doc.revisions.parser.reconstructed?)
788
+ assert_equal('string', doc.catalog[:XXTest][:Data])
789
+ assert_equal('stream', doc.catalog[:XXTest].stream)
790
+ assert(doc.revisions.parser.reconstructed?)
791
+ end
792
+
777
793
  it "fails if no valid trailer is found and couldn't be constructed" do
778
794
  create_parser("1 0 obj\n5\nendobj\nquack trailer <</Size 1>>\nstartxref\n22\n%%EOF")
779
795
  assert_raises(HexaPDF::MalformedPDFError) { @parser.reconstructed_revision.trailer }
@@ -58,7 +58,7 @@ describe HexaPDF::Serializer do
58
58
  assert_serialized("1208925819614629174706176", 1_208_925_819_614_629_174_706_176)
59
59
  end
60
60
 
61
- it "serializes floats with a precision of 4" do
61
+ it "serializes floats with a precision of 6" do
62
62
  assert_serialized("1.5", 1.5)
63
63
  assert_serialized("-1.5", -1.5)
64
64
  assert_serialized("9.123456", 9.123456)
@@ -66,6 +66,12 @@ describe HexaPDF::Serializer do
66
66
  assert_serialized("0.000005", 0.000005)
67
67
  assert_serialized("-0.000005", -0.000005)
68
68
  assert_serialized("0.0", 0.0)
69
+ assert_serialized("123456789012345.0", 1.23456789012345e14)
70
+ assert_serialized("123456789012345.34", 1.2345678901234534e14)
71
+ assert_serialized("-123456789012345.0", -1.23456789012345e14)
72
+ assert_serialized("-123456789012345.34", -1.2345678901234534e14)
73
+ assert_serialized("999999999999999", 1.23456789012345e15)
74
+ assert_serialized("-999999999999999", -1.23456789012345e15)
69
75
  assert_raises(HexaPDF::Error) { @serializer.serialize(0.0 / 0) }
70
76
  assert_raises(HexaPDF::Error) { @serializer.serialize(1.0 / 0) }
71
77
  assert_raises(HexaPDF::Error) { @serializer.serialize(-1.0 / 0) }
@@ -50,6 +50,16 @@ describe HexaPDF::Type::Catalog do
50
50
  assert_equal(:XXOCConfiguration, oc[:D].type)
51
51
  end
52
52
 
53
+ it "uses or creates the document security store on access" do
54
+ @catalog[:DSS] = {}
55
+ assert_equal(:DSS, @catalog.dss.type)
56
+
57
+ @catalog.delete(:DSS)
58
+ dss = @catalog.dss
59
+ assert_equal(:DSS, dss.type)
60
+ assert_same(dss, @catalog.dss)
61
+ end
62
+
53
63
  describe "acro_form" do
54
64
  it "returns an existing form object" do
55
65
  @catalog[:AcroForm] = :test
@@ -0,0 +1,90 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/document_security_store'
6
+
7
+ describe HexaPDF::Type::DocumentSecurityStore do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @dss = @doc.add({Type: :DSS})
11
+ end
12
+
13
+ [[:add_cert, :Certs], [:add_ocsp, :OCSPs], [:add_crl, :CRLs]].each do |method, field|
14
+ describe method do
15
+ it "adds a #{field[0..-2]} as an indirect stream object" do
16
+ result = @dss.send(method, "der_data")
17
+ assert_kind_of(HexaPDF::Stream, result)
18
+ assert_equal(:FlateDecode, result[:Filter])
19
+ assert_equal("der_data", result.stream)
20
+ assert_equal(1, @dss[field].size)
21
+ end
22
+
23
+ it "returns the same stream object for duplicate #{field[0..-2]} data" do
24
+ result1 = @dss.send(method, "der_data")
25
+ result2 = @dss.send(method, "der_data")
26
+ assert_same(result1, result2)
27
+ assert_equal(1, @dss[field].size)
28
+ end
29
+
30
+ it "adds distinct stream objects for different #{field[0..-2]} data" do
31
+ @dss.send(method, "data_one")
32
+ @dss.send(method, "data_two")
33
+ assert_equal(2, @dss[field].size)
34
+ end
35
+ end
36
+ end
37
+
38
+ describe "add_vri" do
39
+ before do
40
+ @signature_contents = 'signature bytes'
41
+ @signature = @doc.add({Type: :Sig, Contents: @signature_contents})
42
+ @vri_key = OpenSSL::Digest::SHA1.hexdigest(@signature_contents).upcase.to_sym
43
+ end
44
+
45
+ it "returns the VRI entry" do
46
+ vri = @dss.add_vri(@signature)
47
+ assert_equal(:VRI, vri.type)
48
+ end
49
+
50
+ it "creates the /VRI dictionary with an entry for the signature" do
51
+ vri = @dss.add_vri(@signature)
52
+ assert(@dss.key?(:VRI))
53
+ assert_same(vri, @dss[:VRI][@vri_key])
54
+ end
55
+
56
+ it "populates /Cert in the VRI entry and the DSS /Certs array" do
57
+ @dss.add_vri(@signature, certs: ["cert1", "cert2"])
58
+ assert_equal(2, @dss[:VRI][@vri_key][:Cert].size)
59
+ assert_equal(2, @dss[:Certs].size)
60
+ end
61
+
62
+ it "populates /OCSP in the VRI entry and the DSS /OCSPs array" do
63
+ @dss.add_vri(@signature, ocsps: ["ocsp1", "ocsp2"])
64
+ assert_equal(2, @dss[:VRI][@vri_key][:OCSP].size)
65
+ assert_equal(2, @dss[:OCSPs].size)
66
+ end
67
+
68
+ it "populates /CRL in the VRI entry and the DSS /CRLs array" do
69
+ @dss.add_vri(@signature, crls: ["crl1"])
70
+ assert_equal(1, @dss[:VRI][@vri_key][:CRL].size)
71
+ assert_equal(1, @dss[:CRLs].size)
72
+ end
73
+
74
+ it "de-duplicates DER streams shared across multiple VRI entries" do
75
+ @dss.add_vri(@signature, certs: ["shared_cert", "cert_a"])
76
+ @dss.add_vri(@doc.add({Type: :Sig, Contents: 'new'}), certs: ["shared_cert", "cert_b"])
77
+ # shared_cert added once despite appearing in both VRI entries
78
+ assert_equal(2, @dss[:VRI].value.size)
79
+ assert_equal(3, @dss[:Certs].size)
80
+ end
81
+
82
+ it "creates a VRI entry with no optional arrays when all inputs are empty" do
83
+ @dss.add_vri(@signature)
84
+ vri = @dss[:VRI][@vri_key]
85
+ refute(vri.key?(:Cert))
86
+ refute(vri.key?(:OCSP))
87
+ refute(vri.key?(:CRL))
88
+ end
89
+ end
90
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
@@ -874,6 +874,7 @@ files:
874
874
  - test/hexapdf/type/test_annotation.rb
875
875
  - test/hexapdf/type/test_catalog.rb
876
876
  - test/hexapdf/type/test_cid_font.rb
877
+ - test/hexapdf/type/test_document_security_store.rb
877
878
  - test/hexapdf/type/test_file_specification.rb
878
879
  - test/hexapdf/type/test_font.rb
879
880
  - test/hexapdf/type/test_font_descriptor.rb