Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle the case that the Annots Array is indirect #10

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.