Skip to content

Commit

Permalink
Merge pull request #4 from tomascco:chore/improve-tests
Browse files Browse the repository at this point in the history
Improve tests and refactor
  • Loading branch information
tomascco authored Oct 20, 2023
2 parents 3713b22 + 769d4b5 commit 9190792
Show file tree
Hide file tree
Showing 15 changed files with 379 additions and 92 deletions.
2 changes: 1 addition & 1 deletion lib/rubrik.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@

require "sorbet-runtime"
require "pdf-reader"
require "irb"

module Rubrik
class Error < StandardError; end
end

require_relative "rubrik/document"
require_relative "rubrik/document/increment"
require_relative "rubrik/document/serialize_object"
require_relative "rubrik/fill_signature"
require_relative "rubrik/sign"
37 changes: 14 additions & 23 deletions lib/rubrik/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,18 @@ class Document
SIGNATURE_SIZE = 8_192

sig {returns(T.any(File, Tempfile, StringIO))}
attr_reader :io
attr_accessor :io

sig {returns(PDF::Reader::ObjectHash)}
attr_reader :objects
attr_accessor :objects

sig {returns(T::Array[{id: PDF::Reader::Reference, value: T.untyped}])}
attr_reader :modified_objects

sig {returns(PDF::Reader::Reference)}
attr_reader :interactive_form_id
attr_accessor :modified_objects

sig {returns(Integer)}
attr_reader :last_object_id
attr_accessor :last_object_id

private :io=, :objects=, :modified_objects=, :last_object_id=

sig {params(input: T.any(File, Tempfile, StringIO)).void}
def initialize(input)
Expand All @@ -36,9 +35,11 @@ def initialize(input)
fetch_or_create_interactive_form!
end

sig {void}
sig {returns(PDF::Reader::Reference)}
# Returns the reference of the Signature Value dictionary.
def add_signature_field
# create signature value dictionary
# To add an signature to the PDF, we need the following structure
# Interactive Form -> Signature Field -> Signature Value
signature_value_id = assign_new_object_id!
modified_objects << {
id: signature_value_id,
Expand All @@ -63,7 +64,7 @@ def add_signature_field
V: signature_value_id,
Type: :Annot,
Subtype: :Widget,
Rect: [20, 20, 120, 120],
Rect: [0, 0, 0, 0],
F: 4,
P: first_page_reference
}
Expand All @@ -75,6 +76,8 @@ def add_signature_field
modified_objects << {id: first_page_reference, value: modified_page}

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

signature_value_id
end

private
Expand All @@ -87,7 +90,7 @@ def interactive_form
sig {void}
def fetch_or_create_interactive_form!
root_ref = objects.trailer[:Root]
root = T.let(objects.fetch(root_ref), Hash)
root = T.let(objects.fetch(root_ref), T::Hash[Symbol, T.untyped])

if root.key?(:AcroForm)
form_id = root[:AcroForm]
Expand All @@ -111,17 +114,5 @@ def fetch_or_create_interactive_form!
def assign_new_object_id!
PDF::Reader::Reference.new(self.last_object_id += 1, 0)
end

sig {params(io: T.any(File, Tempfile, StringIO)).returns(T.any(File, Tempfile, StringIO))}
attr_writer :io

sig {params(objects: PDF::Reader::ObjectHash).returns(PDF::Reader::ObjectHash)}
attr_writer :objects

sig {params(modified_objects: T::Array[{id: PDF::Reader::Reference, value: T.untyped}]).returns(T::Array[{id: PDF::Reader::Reference, value: T.untyped}])}
attr_writer :modified_objects

sig {params(last_object_id: Integer).returns(Integer)}
attr_writer :last_object_id
end
end
69 changes: 17 additions & 52 deletions lib/rubrik/document/increment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ module Increment
extend T::Sig
extend self

sig {params(document: Rubrik::Document, io: T.any(File, Tempfile, StringIO)).returns(T.any(File, Tempfile, StringIO))}
sig {params(document: Rubrik::Document, io: T.any(File, Tempfile, StringIO)).void}
def call(document, io:)
document.io.rewind
IO.copy_stream(T.unsafe(document.io), T.unsafe(io))
IO.copy_stream(document.io, io)

io << "\n"
new_xref = Array.new

new_xref = T.let([], T::Array[T::Hash[Symbol, Integer]])
new_xref << {id: 0}

document.modified_objects.each do |object|
integer_id = T.let(object[:id].to_i, Integer)
new_xref << {id: integer_id, offset: io.pos}

io << "#{integer_id} 0 obj\n" "#{serialize(object[:value])}\n" "endobj\n\n"
value = object[:value]
io << "#{integer_id} 0 obj\n" "#{SerializeObject[value]}\n" "endobj\n\n"
end

updated_trailer = document.objects.trailer.dup
Expand All @@ -31,72 +34,34 @@ def call(document, io:)
new_xref_pos = io.pos

new_xref_subsections = new_xref
.sort_by { _1[:id] }
.chunk_while { _1[:id] + 1 == _2[:id] }
.sort_by { |entry| entry.fetch(:id) }
.chunk_while { _1.fetch(:id) + 1 == _2[:id] }

io << "xref\n"
io << "0 1\n"
io << "0000000000 65535 f\n"

new_xref_subsections.each do |subsection|
starting_id = subsection.first[:id]
starting_id = T.must(subsection.first).fetch(:id)
length = subsection.length

io << "#{starting_id} #{length}\n"

if starting_id.zero?
io << "0000000000 65535 f\n"
subsection.shift
end

subsection.each { |entry| io << "#{format("%010d", entry[:offset])} 00000 n\n" }
end

io << "trailer\n"
io << "#{serialize(updated_trailer)}\n"
io << "#{SerializeObject[updated_trailer]}\n"
io << "startxref\n"
io << "#{new_xref_pos.to_s}\n"
io << "%%EOF\n"

io.rewind
io
end

private

sig {params(obj: T.untyped).returns(String)}
def serialize(obj)
case obj
when Hash
serialized_objs = obj.flatten.map { |e| serialize(e) }
"<<#{serialized_objs.join(" ")}>>"
when Symbol
"/#{obj}"
when Array
serialized_objs = obj.map { |e| serialize(e) }
"[#{serialized_objs.join(" ")}]"
when PDF::Reader::Reference
"#{obj.id} #{obj.gen} R"
when String
"(#{obj})"
when TrueClass
"true"
when FalseClass
"false"
when Document::CONTENTS_PLACEHOLDER
"<#{"0" * Document::SIGNATURE_SIZE}>"
when Document::BYTE_RANGE_PLACEHOLDER
"[0 0000000000 0000000000 0000000000]"
when Numeric
obj.to_s
when NilClass
"null"
when PDF::Reader::Stream
<<~OBJECT.chomp
#{serialize(obj.hash)}
stream
#{obj.data}
endstream
OBJECT
else
raise NotImplementedError.new("Don't know how to serialize #{obj}")
end
end

sig {params(document: Rubrik::Document).returns(Integer)}
def last_xref_pos(document)
PDF::Reader::Buffer.new(document.io, seek: 0).find_first_xref_offset
Expand Down
53 changes: 53 additions & 0 deletions lib/rubrik/document/serialize_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# typed: true
# frozen_string_literal: true

module Rubrik
class Document
module SerializeObject
include Kernel
extend T::Sig
extend self

sig {params(obj: T.untyped).returns(String)}
def [](obj)
case obj
when Hash
serialized_objs = obj.flatten.map { |e| SerializeObject[e] }
"<<#{serialized_objs.join(" ")}>>"
when Symbol
"/#{obj}"
when Array
serialized_objs = obj.map { |e| SerializeObject[e] }
"[#{serialized_objs.join(" ")}]"
when PDF::Reader::Reference
"#{obj.id} #{obj.gen} R"
when String
"(#{obj})"
when TrueClass
"true"
when FalseClass
"false"
when Document::CONTENTS_PLACEHOLDER
"<#{"0" * Document::SIGNATURE_SIZE}>"
when Document::BYTE_RANGE_PLACEHOLDER
"[0 0000000000 0000000000 0000000000]"
when Float, Integer
obj.to_s
when NilClass
"null"
when PDF::Reader::Stream
<<~OBJECT.chomp
#{SerializeObject[obj.hash]}
stream
#{obj.data}
endstream
OBJECT
else
raise "Don't know how to serialize #{obj}"
end
end

alias call []
end
end
end
5 changes: 2 additions & 3 deletions lib/rubrik/fill_signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module FillSignature
private_key: OpenSSL::PKey::RSA,
public_key: OpenSSL::X509::Certificate,
certificate_chain: T::Array[OpenSSL::X509::Certificate])
.returns(T.any(File, StringIO, Tempfile))}
.void}

FIRST_OFFSET = 0

Expand All @@ -29,8 +29,7 @@ def call(io, signature_value_ref:, private_key:, public_key:, certificate_chain:
io.gets("<")

first_length = io.pos - 1
# we need to double the SIGNATURE_SIZE because the hex encoding double the data size
# we also need to sum +2 to account for "<" and ">" of the hex string
# We need to sum +2 to account for "<" and ">" of the hex string
second_offset = first_length + Document::SIGNATURE_SIZE + 2
second_length = io.size - second_offset

Expand Down
8 changes: 4 additions & 4 deletions lib/rubrik/sign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ module Sign
certificate_chain: T::Array[OpenSSL::X509::Certificate])
.void}
def self.call(input, output, private_key:, public_key:, certificate_chain: [])
input.binmode
output.reopen(T.unsafe(output), "wb+") if !output.is_a?(StringIO)

document = Rubrik::Document.new(input)

document.add_signature_field
signature_value_ref = document.add_signature_field

Document::Increment.call(document, io: output)

signature_value = T.must(document.modified_objects.find { _1.dig(:value, :Type) == :Sig })

signature_value_ref = T.let(signature_value[:id], PDF::Reader::Reference)
FillSignature.call(output, signature_value_ref:, private_key:, public_key:, certificate_chain:)
end
end
Expand Down
Loading

0 comments on commit 9190792

Please sign in to comment.