Skip to content

Commit

Permalink
Merge pull request #10 from tomascco:fix-annots-being-an-indirect-ref…
Browse files Browse the repository at this point in the history
…erence

Handle the case that the Annots Array is indirect
  • Loading branch information
tomascco authored Nov 29, 2023
2 parents 71752ea + 9f17338 commit 76d8121
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 13 deletions.
13 changes: 7 additions & 6 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
"dictionaryDefinitions": [],
"dictionaries": [],
"words": [
"Rubrik",
"PKCS",
"Tempfile",
"rubygems",
"ETSI",
"Annots",
"codebases",
"devcontainer",
"simplecov"
"ETSI",
"PKCS",
"Rubrik",
"rubygems",
"simplecov",
"Tempfile"
],
"ignoreWords": [],
"import": []
Expand Down
16 changes: 13 additions & 3 deletions lib/rubrik/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,20 @@ def add_signature_field
}
}

modified_page = objects.fetch(first_page_reference).dup
(modified_page[:Annots] ||= []) << signature_field_id
first_page = objects.fetch(first_page_reference)
annots = first_page[:Annots]

modified_objects << {id: first_page_reference, value: modified_page}
if annots.is_a?(PDF::Reader::Reference)
new_annots = objects.fetch(annots).dup
new_annots << signature_field_id

modified_objects << {id: annots, value: new_annots}
else
new_first_page = first_page.dup
(new_first_page[:Annots] ||= []) << signature_field_id

modified_objects << {id: first_page_reference, value: new_first_page}
end

(interactive_form[:Fields] ||= []) << signature_field_id

Expand Down
62 changes: 58 additions & 4 deletions test/rubrik/document_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_initialize_document_without_interactive_form
assert_equal(6, document.last_object_id)
assert_kind_of(PDF::Reader::ObjectHash, document.objects)

acro_form = document.modified_objects.find { |obj| obj.dig(:value, :Type) == :Catalog }
acro_form = document.modified_objects.find { |obj| obj.is_a?(Hash) && obj.dig(:value, :Type) == :Catalog }
acro_form_reference = T.must(acro_form).dig(:value, :AcroForm)
assert_pattern do
document.modified_objects => [*, {id: ^acro_form_reference, value: {Fields: [], SigFlags: 3}}, *]
Expand Down Expand Up @@ -62,7 +62,7 @@ def test_add_signature_field
assert_equal(3, number_of_added_objects)

assert_pattern do
signature_value = document.modified_objects.find { _1[:id] == result }
signature_value = document.modified_objects.find { _1.is_a?(Hash) && _1[:id] == result }
signature_value => {id: ^result,
value: {
Type: :Sig,
Expand All @@ -74,7 +74,7 @@ def test_add_signature_field
}
end

signature_field = document.modified_objects.find { _1.dig(:value, :FT) == :Sig }
signature_field = document.modified_objects.find { _1.is_a?(Hash) && _1.dig(:value, :FT) == :Sig }
assert_pattern do
first_page_reference = document.objects.page_references[0]
signature_field => {
Expand All @@ -93,12 +93,66 @@ def test_add_signature_field

signature_field_id = T.must(signature_field)[:id]

first_page = document.modified_objects.find { _1.dig(:value, :Type) == :Page }
first_page = document.modified_objects.find { _1.is_a?(Hash) && _1.dig(:value, :Type) == :Page }
assert_pattern { first_page => {value: {Annots: [*, ^signature_field_id, *]}}}

assert_pattern { document.send(:interactive_form) => {Fields: [*, ^signature_field_id, *]} }
ensure
input&.close
end

def test_add_signature_field_with_indirect_annots
# Arrange
input = File.open(SupportPDF["indirect_annots"], "rb")
document = Document.new(input)
initial_number_of_objects = document.modified_objects.size

# Act
result = document.add_signature_field

# Assert
number_of_added_objects = document.modified_objects.size - initial_number_of_objects
assert_equal(3, number_of_added_objects)

assert_pattern do
signature_value = document.modified_objects.find { _1.is_a?(Hash) && _1[:id] == result }
signature_value => {id: ^result,
value: {
Type: :Sig,
Filter: :"Adobe.PPKLite",
SubFilter: :"adbe.pkcs7.detached",
Contents: Document::CONTENTS_PLACEHOLDER,
ByteRange: Document::BYTE_RANGE_PLACEHOLDER
}
}
end

signature_field = document.modified_objects.find { _1.is_a?(Hash) && _1.dig(:value, :FT) == :Sig }
assert_pattern do
first_page_reference = document.objects.page_references[0]
signature_field => {
id: PDF::Reader::Reference,
value: {
T: /Signature-\w{4}/,
V: ^result,
Type: :Annot,
Subtype: :Widget,
Rect: [0, 0, 0, 0],
F: 4,
P: ^first_page_reference
}
}
end

signature_field_id = T.must(signature_field)[:id]

assert_pattern do
document.modified_objects => [*, {id: PDF::Reader::Reference, value: [signature_field_id]} , *]
end

assert_pattern { document.send(:interactive_form) => {Fields: [*, ^signature_field_id, *]} }
ensure
input&.close
end
end
end
File renamed without changes.
39 changes: 39 additions & 0 deletions test/rubrik/sign_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,44 @@ def test_document_without_interactive_form
input_pdf&.close
expected_output&.close
end

def test_document_with_indirect_annots
# Arrange
input_pdf = File.open(SupportPDF["indirect_annots"], "rb")
# FACT: the output file is opened with only write permission and should be later reopened to rw.
output_pdf = File.open(SupportPDF["indirect_annots_temp"], "wb")
certificate_file = File.open("test/support/demo_cert.pem", "rb")

private_key = OpenSSL::PKey::RSA.new(certificate_file, "")
certificate_file.rewind
certificate = OpenSSL::X509::Certificate.new(certificate_file)

# Act
Sign.call(input_pdf, output_pdf, private_key:, certificate:)

# Assert
expected_output = File.open(SupportPDF["indirect_annots.expected"], "rb")

expected_output.readlines.zip(output_pdf.readlines).each do |(expected_line, actual_line)|
# We can't verify the signature because it changes on every run
if actual_line&.match?("/Type /Sig")
actual_line.sub!(/<[a-f0-9]+>/, "")
expected_line.sub!(/<[a-f0-9]+>/, "")
# The signature field name is also random
elsif actual_line&.match?(/Signature-[a-f0-9]{4}/)
actual_line.sub!(/Signature-[a-f0-9]{4}/, "")
expected_line.sub!(/Signature-[a-f0-9]{4}/, "")
end

assert_equal(expected_line, actual_line)
end
ensure
certificate_file&.close
input_pdf&.close
expected_output&.close

output_pdf&.close
File.delete(output_pdf.path) if output_pdf
end
end
end
Binary file added test/support/indirect_annots.expected.pdf
Binary file not shown.
Binary file added test/support/indirect_annots.pdf
Binary file not shown.

0 comments on commit 76d8121

Please sign in to comment.