paperback 0.0.4 → 0.0.5

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.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/tests.yml +46 -0
  3. data/.rubocop-disables.yml +26 -9
  4. data/CHANGELOG.md +7 -0
  5. data/README.md +4 -1
  6. data/lib/paperback/cli.rb +17 -0
  7. data/lib/paperback/document.rb +74 -11
  8. data/lib/paperback/preparer.rb +76 -21
  9. data/lib/paperback/version.rb +2 -1
  10. data/lib/paperback.rb +12 -0
  11. data/paperback.gemspec +10 -7
  12. data/sorbet/config +3 -0
  13. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  14. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  15. data/sorbet/rbi/gems/chunky_png@1.4.0.rbi +4498 -0
  16. data/sorbet/rbi/gems/coderay@1.1.3.rbi +3426 -0
  17. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  18. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  19. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  20. data/sorbet/rbi/gems/parallel@1.22.1.rbi +277 -0
  21. data/sorbet/rbi/gems/parser@3.2.0.0.rbi +6963 -0
  22. data/sorbet/rbi/gems/pdf-core@0.4.0.rbi +1682 -0
  23. data/sorbet/rbi/gems/prawn@1.3.0.rbi +5567 -0
  24. data/sorbet/rbi/gems/pry@0.14.1.rbi +9990 -0
  25. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +408 -0
  26. data/sorbet/rbi/gems/rake@13.0.6.rbi +3023 -0
  27. data/sorbet/rbi/gems/rbi@0.0.16.rbi +3008 -0
  28. data/sorbet/rbi/gems/regexp_parser@2.6.1.rbi +3481 -0
  29. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  30. data/sorbet/rbi/gems/rqrcode@0.10.1.rbi +617 -0
  31. data/sorbet/rbi/gems/rspec-core@3.12.0.rbi +10791 -0
  32. data/sorbet/rbi/gems/rspec-expectations@3.12.1.rbi +8106 -0
  33. data/sorbet/rbi/gems/rspec-mocks@3.12.1.rbi +5305 -0
  34. data/sorbet/rbi/gems/rspec-support@3.12.0.rbi +1617 -0
  35. data/sorbet/rbi/gems/rspec@3.12.0.rbi +88 -0
  36. data/sorbet/rbi/gems/rubocop-ast@1.24.1.rbi +6617 -0
  37. data/sorbet/rbi/gems/rubocop@0.93.1.rbi +40848 -0
  38. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +1234 -0
  39. data/sorbet/rbi/gems/sixword@0.4.0.rbi +536 -0
  40. data/sorbet/rbi/gems/spoom@1.1.15.rbi +2383 -0
  41. data/sorbet/rbi/gems/subprocess@1.5.6.rbi +391 -0
  42. data/sorbet/rbi/gems/tapioca@0.10.5.rbi +3207 -0
  43. data/sorbet/rbi/gems/thor@1.2.1.rbi +3956 -0
  44. data/sorbet/rbi/gems/ttfunk@1.4.0.rbi +1951 -0
  45. data/sorbet/rbi/gems/unicode-display_width@1.8.0.rbi +40 -0
  46. data/sorbet/rbi/gems/unparser@0.6.7.rbi +4524 -0
  47. data/sorbet/rbi/gems/webrick@1.7.0.rbi +2555 -0
  48. data/sorbet/rbi/gems/yard-sorbet@0.8.0.rbi +441 -0
  49. data/sorbet/rbi/gems/yard@0.9.28.rbi +17816 -0
  50. data/sorbet/tapioca/config.yml +13 -0
  51. data/sorbet/tapioca/require.rb +4 -0
  52. data/spec/functional/paperback/cli_spec.rb +54 -25
  53. data/spec/spec_helper.rb +1 -0
  54. data/spec/unit/paperback_spec.rb +1 -0
  55. metadata +89 -13
  56. data/sample/aes.key +0 -1
  57. data/sample/aes.pdf +0 -14413
  58. data/sample/aes.pdf.passphrase.txt +0 -1
  59. data/sample/rsa2048.pdf +0 -106803
  60. data/sample/rsa2048.pdf.passphrase.txt +0 -1
  61. data/sample/rsa2048.pem +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 80af4f42712942cb193a1d3875f0de751a7b3803
4
- data.tar.gz: 5f23830de9c39209eeb6e7a72c8c39e40db2a921
2
+ SHA256:
3
+ metadata.gz: fe9b3003bc8749cbd4c5bd312b2b1d0b7546f9268e0f1c41e94c71f12a62df2c
4
+ data.tar.gz: d9630db98c97621804f79a951a140d2d9faa5080bc853c7e2492920e58dec07f
5
5
  SHA512:
6
- metadata.gz: 61dd66d5b7c0e2db779b5124b1b7d20553303841fc1c7dbcfe04443f7b1e8b96af4dcad4af5d47e701dbd5e708106e699c90a08ca3c600ff4d6d27fd307bde41
7
- data.tar.gz: fec463faf717472399de4884229a3223b9f64b61136f5f72dfdf327331544d20c3dde37307d5bf33703640850d7267015f17cc00ff1ada99bfafb280b03935c6
6
+ metadata.gz: 15f5f07874c345f8ed9c0565a7704bf46be836c9d398ba8ec0f39fac6501cd4152762ed257cba92ea3e2f0e1abe223a9aec2a3318e13bc620507ec809033ba30
7
+ data.tar.gz: e6bbb5beb3ea669e663031ffe8d25088d2657f264fdd7569048f7f3d90264c0954f838025ae4c020c9b28c5971a8ca53669377edf5a731cfff8bcacfcb8fd4eb
@@ -0,0 +1,46 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ - push
12
+ - pull_request
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ matrix:
23
+ ruby-version: ['2.7', '3.0', '3.1', '3.2']
24
+
25
+ steps:
26
+ - uses: actions/checkout@v3
27
+
28
+ - name: Install apt dependencies for tests
29
+ run: |
30
+ sudo apt-get update -y
31
+ sudo apt-get install poppler-utils
32
+
33
+ - name: Set up Ruby
34
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
35
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
36
+ # uses: ruby/setup-ruby@v1
37
+ uses: ruby/setup-ruby@319066216501fbd5e2d568f14b7d68c19fb67a5d # v1.33.1 / 2023-01-06
38
+ with:
39
+ ruby-version: ${{ matrix.ruby-version }}
40
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
41
+
42
+ - name: Run tests
43
+ run: bundle exec rake test
44
+
45
+ - name: Sorbet Typecheck
46
+ run: bundle exec srb tc
@@ -21,6 +21,9 @@ Metrics/MethodLength:
21
21
  Metrics/ModuleLength:
22
22
  Enabled: false
23
23
 
24
+ Metrics/ParameterLists:
25
+ Enabled: false
26
+
24
27
  # Configuration parameters: Exclude.
25
28
  #
26
29
  # We could re-enable this if it understood that the top level module should
@@ -36,6 +39,11 @@ Style/Documentation:
36
39
  # Disagree with these style points
37
40
  # ********************************
38
41
 
42
+ Layout/EmptyLineBetweenDefs:
43
+ Enabled: false
44
+ Layout/EmptyLineAfterGuardClause:
45
+ Enabled: false
46
+
39
47
  Style/DotPosition:
40
48
  Enabled: false
41
49
  Style/DoubleNegation:
@@ -57,12 +65,16 @@ Style/IfUnlessModifier:
57
65
  Style/WhileUntilModifier:
58
66
  Enabled: false
59
67
 
68
+ # Don't favor wacky unless methods
69
+ Style/NegatedIf:
70
+ Enabled: false
71
+ Style/InverseMethods:
72
+ Enabled: false
73
+
60
74
  Style/InfiniteLoop:
61
75
  Enabled: false
62
76
  Style/PercentLiteralDelimiters:
63
- PreferredDelimiters:
64
- '%w': '{}'
65
- '%W': '{}'
77
+ Enabled: false
66
78
  Style/RaiseArgs:
67
79
  EnforcedStyle: compact
68
80
  Style/RedundantReturn:
@@ -84,14 +96,13 @@ Style/SpaceInsideBlockBraces:
84
96
  Style/SpaceInsideHashLiteralBraces:
85
97
  Enabled: false
86
98
 
87
- # Offense count: 27
88
- # Cop supports --auto-correct.
89
99
  # Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
90
- Style/TrailingCommaInLiteral:
91
- # SupportedStyles:
92
- # - comma
93
- # - no_comma
100
+ Style/TrailingCommaInArrayLiteral:
101
+ EnforcedStyleForMultiline: comma
102
+ Style/TrailingCommaInHashLiteral:
94
103
  EnforcedStyleForMultiline: comma
104
+ Style/TrailingCommaInArguments:
105
+ Enabled: false
95
106
 
96
107
  # Don't favor %w for arrays of words.
97
108
  Style/WordArray:
@@ -101,6 +112,9 @@ Style/WordArray:
101
112
  Style/SymbolArray:
102
113
  Enabled: false
103
114
 
115
+ Style/NumericLiteralPrefix:
116
+ Enabled: false
117
+
104
118
  # Definitely do NOT assign variables within a conditional. Christ.
105
119
  Style/ConditionalAssignment:
106
120
  Enabled: false
@@ -128,3 +142,6 @@ Style/GlobalVars:
128
142
  Style/SpecialGlobalVars:
129
143
  Exclude:
130
144
  - 'paperback.gemspec'
145
+
146
+ Naming/HeredocDelimiterNaming:
147
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.0.5] -- 2023-01-17
8
+
9
+ - Upgrade to modern versions of ruby
10
+ - Upgrade a few dependencies, fix type checking
11
+ - Fix tests to work on newer versions of pdftotext
12
+ - Run tests on Github Actions
13
+
7
14
  ## [0.0.4] -- 2019-02-26
8
15
 
9
16
  - Add some basic end-to-end tests.
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
- # Paperback
1
+ # Paperback
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/paperback.svg)](https://rubygems.org/gems/paperback)
4
4
  [![Inline Docs](http://inch-ci.org/github/ab/paperback.svg?branch=master)](http://www.rubydoc.info/github/ab/paperback/master)
5
+ [![Test status](https://github.com/ab/paperback/actions/workflows/tests.yml/badge.svg)](https://github.com/ab/paperback/actions/workflows/tests.yml)
5
6
 
6
7
  *Paperback* is a library that facilitates the creation of paper offline backups
7
8
  of small amounts of important data, such as encryption keys.
@@ -42,6 +43,8 @@ paperback data.key out.pdf
42
43
 
43
44
  See [sample directory](./sample)
44
45
 
46
+ ![sample page one](./sample/sample.pg1.png)
47
+ ![sample page two](./sample/sample.pg2.png)
45
48
 
46
49
  ### More complex patterns
47
50
 
data/lib/paperback/cli.rb CHANGED
@@ -1,7 +1,10 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module Paperback
4
5
  module CLI
6
+ extend T::Sig
7
+
5
8
  # Top level CLI interface for Paperback. This is the one stop shop for
6
9
  # calling paperback.
7
10
  #
@@ -18,6 +21,20 @@ module Paperback
18
21
  # [Paperback::Preparer#render]
19
22
  # @param [Boolean] include_base64 Whether to include a Base64 copy of the
20
23
  # input
24
+ sig do
25
+ params(
26
+ input: String,
27
+ output: String,
28
+ encrypt: T::Boolean,
29
+ qr_base64: T::Boolean,
30
+ qr_level: Symbol,
31
+ comment: T.nilable(String),
32
+ passphrase_file: T.nilable(String),
33
+ extra_draw_opts: T::Hash[T.untyped, T.untyped],
34
+ include_base64: T::Boolean,
35
+ )
36
+ .void
37
+ end
21
38
  def self.create_backup(input:, output:, encrypt: true, qr_base64: true,
22
39
  qr_level: :l, comment: nil, passphrase_file: nil,
23
40
  extra_draw_opts: {}, include_base64: true)
@@ -1,26 +1,43 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'prawn'
4
5
 
5
6
  # Main class for creating and rendering PDFs
6
7
  module Paperback; class Document
7
- attr_reader :pdf, :debug
8
+ extend T::Sig
8
9
 
10
+ sig {returns(Prawn::Document)}
11
+ attr_reader :pdf
12
+
13
+ sig {returns(T::Boolean)}
14
+ attr_reader :debug
15
+
16
+ sig {params(debug: T::Boolean).void}
9
17
  def initialize(debug: false)
10
18
  log.debug('Document#initialize')
11
- @debug = debug
12
- @pdf = Prawn::Document.new
19
+ @debug = T.let(debug, T::Boolean)
20
+ @pdf = T.let(Prawn::Document.new, Prawn::Document)
21
+ @log = T.let(nil, T.nilable(Logger))
13
22
  end
14
23
 
24
+ sig {returns(Logger)}
15
25
  def log
16
26
  @log ||= Paperback.class_log(self.class)
17
27
  end
18
28
 
29
+ sig do
30
+ params(
31
+ output_file: String,
32
+ draw_opts: T::Hash[Symbol, T.untyped],
33
+ )
34
+ .void
35
+ end
19
36
  def render(output_file:, draw_opts:)
20
37
  log.info('Rendering PDF')
21
38
 
22
39
  # Create all the PDF content
23
- draw_paperback(**draw_opts)
40
+ draw_paperback(**T.unsafe(draw_opts))
24
41
 
25
42
  # Render to output file
26
43
  log.info("Writing PDF to #{output_file.inspect}")
@@ -42,13 +59,25 @@ module Paperback; class Document
42
59
  # content (possibly encrypted) encoded using Base64.
43
60
  # @param [Integer, nil] base64_bytes The length of the original content
44
61
  # before encoding to base64. This is used for the informational header.
62
+ sig do
63
+ params(
64
+ qr_code: RQRCode::QRCode,
65
+ sixword_lines: T::Array[String],
66
+ sixword_bytes: Integer,
67
+ labels: T::Hash[String, T.untyped],
68
+ passphrase_sha: T.nilable(String),
69
+ passphrase_len: T.nilable(Integer),
70
+ sixword_font_size: T.nilable(Float),
71
+ base64_content: T.nilable(String),
72
+ base64_bytes: T.nilable(Integer),
73
+ )
74
+ .void
75
+ end
45
76
  def draw_paperback(qr_code:, sixword_lines:, sixword_bytes:, labels:,
46
77
  passphrase_sha: nil, passphrase_len: nil,
47
78
  sixword_font_size: nil, base64_content: nil,
48
79
  base64_bytes: nil)
49
- unless qr_code.is_a?(RQRCode::QRCode)
50
- raise ArgumentError.new('qr_code must be RQRCode::QRCode')
51
- end
80
+ T.assert_type!(qr_code, RQRCode::QRCode)
52
81
 
53
82
  # Header & QR code page
54
83
  pdf.font('Times-Roman')
@@ -71,11 +100,11 @@ module Paperback; class Document
71
100
 
72
101
  draw_sixword(lines: sixword_lines, sixword_bytes: sixword_bytes,
73
102
  font_size: sixword_font_size,
74
- is_encrypted: passphrase_len)
103
+ is_encrypted: !!passphrase_len)
75
104
 
76
105
  if base64_content
77
- draw_base64(b64_content: base64_content, b64_bytes: base64_bytes,
78
- is_encrypted: passphrase_len)
106
+ draw_base64(b64_content: base64_content, b64_bytes: T.must(base64_bytes),
107
+ is_encrypted: !!passphrase_len)
79
108
  end
80
109
 
81
110
  pdf.number_pages('<page> of <total>', align: :right,
@@ -83,16 +112,27 @@ module Paperback; class Document
83
112
  end
84
113
 
85
114
  # If in debug mode, draw axes on the page to assist with layout
115
+ sig {void}
86
116
  def debug_draw_axes
87
117
  return unless debug
88
118
  pdf.float { pdf.stroke_axis }
89
119
  end
90
120
 
91
121
  # Move cursor down by one line
122
+ sig {void}
92
123
  def add_newline
93
124
  pdf.move_down(pdf.font_size)
94
125
  end
95
126
 
127
+ sig do
128
+ params(
129
+ labels: T::Hash[String, T.untyped],
130
+ passphrase_sha: T.nilable(String),
131
+ passphrase_len: T.nilable(Integer),
132
+ repo_url: String,
133
+ )
134
+ .void
135
+ end
96
136
  def draw_header(labels:, passphrase_sha:, passphrase_len:,
97
137
  repo_url: 'https://github.com/ab/paperback')
98
138
 
@@ -103,7 +143,7 @@ module Paperback; class Document
103
143
  pdf.text(intro, inline_format: true)
104
144
  add_newline
105
145
 
106
- label_pad = labels.keys.map(&:length).max + 1
146
+ label_pad = T.must(labels.keys.map(&:length).max) + 1
107
147
 
108
148
  unless passphrase_sha && passphrase_len
109
149
  labels['Encrypted'] = 'no'
@@ -140,6 +180,16 @@ module Paperback; class Document
140
180
  # @param [Integer] columns The number of text columns on the page
141
181
  # @param [Integer] hunks_per_row The number of 6-word sentences per line
142
182
  # @param [Integer] sixword_bytes Bytesize of the sixword encoded data
183
+ sig do
184
+ params(
185
+ lines: T::Array[String],
186
+ sixword_bytes: Integer,
187
+ columns: Integer,
188
+ hunks_per_row: Integer,
189
+ font_size: T.nilable(Float),
190
+ is_encrypted: T::Boolean,
191
+ ).void
192
+ end
143
193
  def draw_sixword(lines:, sixword_bytes:, columns: 3, hunks_per_row: 1,
144
194
  font_size: nil, is_encrypted: true)
145
195
  font_size ||= 11
@@ -171,6 +221,11 @@ module Paperback; class Document
171
221
  end
172
222
  end
173
223
 
224
+ sig do
225
+ params(
226
+ qr_modules: T::Array[T::Array[T::Boolean]],
227
+ ).void
228
+ end
174
229
  def draw_qr_code(qr_modules:)
175
230
  qr_height = pdf.cursor # entire rest of page
176
231
  qr_width = pdf.bounds.width # entire page width
@@ -203,6 +258,14 @@ module Paperback; class Document
203
258
  end
204
259
 
205
260
  # @param [String] b64_content
261
+ sig do
262
+ params(
263
+ b64_content: String,
264
+ b64_bytes: Integer,
265
+ font_size: T.nilable(Float),
266
+ is_encrypted: T::Boolean,
267
+ ).void
268
+ end
206
269
  def draw_base64(b64_content:, b64_bytes:, font_size: nil, is_encrypted: true)
207
270
  font_size ||= 11
208
271
 
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'base64'
@@ -12,39 +13,67 @@ module Paperback
12
13
  # Class wrapping functions to prepare data for paperback storage, including
13
14
  # QR code and sixword encoding.
14
15
  class Preparer
16
+ extend T::Sig
17
+
18
+ sig {returns(String)}
15
19
  attr_reader :data
20
+
21
+ sig {returns(T::Hash[String, T.untyped])}
16
22
  attr_reader :labels
23
+
24
+ sig {returns(T::Boolean)}
17
25
  attr_reader :qr_base64
26
+
27
+ sig {returns(T::Boolean)}
18
28
  attr_reader :encrypt
29
+
30
+ sig {returns(T.nilable(String))}
19
31
  attr_reader :passphrase_file
20
32
 
33
+ sig do
34
+ params(
35
+ filename: String,
36
+ encrypt: T::Boolean,
37
+ qr_base64: T::Boolean,
38
+ qr_level: T.nilable(Symbol),
39
+ comment: T.nilable(String),
40
+ passphrase_file: T.nilable(String),
41
+ include_base64: T::Boolean,
42
+ ).void
43
+ end
21
44
  def initialize(filename:, encrypt: true, qr_base64: false, qr_level: nil,
22
45
  comment: nil, passphrase_file: nil, include_base64: true)
23
46
 
24
47
  log.debug('Preparer#initialize')
25
48
 
49
+ # lazy initializers, all explicitly set to nil
50
+ @log = T.let(nil, T.nilable(Logger))
51
+ @qr_code = T.let(nil, T.nilable(RQRCode::QRCode))
52
+ @sixword_lines = T.let(nil, T.nilable(T::Array[String]))
53
+ @passphrase = T.let(nil, T.nilable(String))
54
+
26
55
  log.info("Reading #{filename.inspect}")
27
56
  plain_data = File.read(filename)
28
57
 
29
58
  log.debug("Read #{plain_data.bytesize} bytes")
30
59
 
31
- @encrypt = encrypt
60
+ @encrypt = T.let(encrypt, T::Boolean)
32
61
 
33
62
  if encrypt
34
63
  @data = self.class.gpg_encrypt(filename: filename, password: passphrase)
35
64
  else
36
- @data = plain_data
65
+ @data = T.let(plain_data, String)
37
66
  end
38
- @sha256 = Digest::SHA256.hexdigest(plain_data)
67
+ @sha256 = T.let(Digest::SHA256.hexdigest(plain_data), String)
39
68
 
40
- @qr_base64 = qr_base64
41
- @qr_level = qr_level
69
+ @qr_base64 = T.let(qr_base64, T::Boolean)
70
+ @qr_level = T.let(qr_level, T.nilable(Symbol))
42
71
 
43
- @passphrase_file = passphrase_file
72
+ @passphrase_file = T.let(passphrase_file, T.nilable(String))
44
73
 
45
- @include_base64 = !!include_base64
74
+ @include_base64 = T.let(!!include_base64, T::Boolean)
46
75
 
47
- @labels = {}
76
+ @labels = T.let({}, T::Hash[String, T.untyped])
48
77
  @labels['Filename'] = filename
49
78
  @labels['Backed up'] = Time.now.to_s
50
79
 
@@ -55,16 +84,21 @@ module Paperback
55
84
 
56
85
  @labels['SHA256'] = Digest::SHA256.hexdigest(plain_data)
57
86
 
58
- @document = Paperback::Document.new
87
+ @document = T.let(Paperback::Document.new, Paperback::Document)
59
88
  end
60
89
 
90
+ @log = T.let(nil, T.nilable(Logger))
91
+
92
+ sig {returns(Logger)}
61
93
  def log
62
94
  @log ||= Paperback.class_log(self.class)
63
95
  end
96
+ sig {returns(Logger)}
64
97
  def self.log
65
98
  @log ||= Paperback.class_log(self)
66
99
  end
67
100
 
101
+ sig {params(output_filename: String, extra_draw_opts: T::Hash[T.untyped, T.untyped]).void}
68
102
  def render(output_filename:, extra_draw_opts: {})
69
103
  log.debug('Preparer#render')
70
104
 
@@ -84,8 +118,11 @@ module Paperback
84
118
  opts[:passphrase_sha] = self.class.truncated_sha256(passphrase)
85
119
  opts[:passphrase_len] = passphrase.length
86
120
  if passphrase_file
87
- File.open(passphrase_file, File::CREAT|File::EXCL|File::WRONLY,
88
- 0400) do |f|
121
+ File.open(
122
+ T.must(passphrase_file),
123
+ File::CREAT | File::EXCL | File::WRONLY,
124
+ 0o400
125
+ ) do |f|
89
126
  f.write(passphrase)
90
127
  end
91
128
  log.info("Wrote passphrase to #{passphrase_file.inspect}")
@@ -99,7 +136,7 @@ module Paperback
99
136
  log.info('Render complete')
100
137
 
101
138
  if encrypt
102
- puts "SHA256(passphrase)[0...16]: " + opts.fetch(:passphrase_sha)
139
+ puts 'SHA256(passphrase)[0...16]: ' + opts.fetch(:passphrase_sha)
103
140
  if !passphrase_file
104
141
  puts "Passphrase: #{passphrase}"
105
142
  end
@@ -108,13 +145,20 @@ module Paperback
108
145
  end
109
146
  end
110
147
 
148
+ sig {returns(String)}
111
149
  def passphrase
112
150
  raise "Can't have passphrase without encrypt" unless encrypt
113
151
  @passphrase ||= self.class.random_passphrase
114
152
  end
115
153
 
116
- PassChars = [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze
154
+ PassChars = T.let(
155
+ [*'a'..'z', *'A'..'Z', *'0'..'9'].freeze, T::Array[String]
156
+ )
117
157
 
158
+ sig do
159
+ params(entropy_bits: Integer, char_set: T::Array[String])
160
+ .returns(String)
161
+ end
118
162
  def self.random_passphrase(entropy_bits: 256, char_set: PassChars)
119
163
  chars_needed = (entropy_bits / Math.log2(char_set.length)).ceil
120
164
  (0...chars_needed).map {
@@ -123,35 +167,40 @@ module Paperback
123
167
  end
124
168
 
125
169
  # Compute a truncated SHA256 digest
170
+ sig {params(content: String).returns(String)}
126
171
  def self.truncated_sha256(content)
127
172
  Digest::SHA256.hexdigest(content)[0...16]
128
173
  end
129
174
 
175
+ sig {params(filename: String, password: String).returns(String)}
130
176
  def self.gpg_encrypt(filename:, password:)
131
177
  cmd = %w[
132
178
  gpg -c -o - --batch --cipher-algo aes256 --passphrase-fd 0 --
133
179
  ] + [filename]
134
- out = nil
180
+ out = T.let(nil, T.nilable(String))
135
181
 
136
182
  log.debug('+ ' + cmd.join(' '))
137
183
  Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
138
- stdout: Subprocess::PIPE) do |p|
184
+ stdout: Subprocess::PIPE) do |p|
139
185
  out, _err = p.communicate(password)
140
186
  end
141
187
 
142
- out
188
+ T.must(out)
143
189
  end
144
190
 
191
+ sig {params(data: String, strip_comments: T::Boolean).returns(String)}
145
192
  def self.gpg_ascii_enarmor(data, strip_comments: true)
146
193
  cmd = %w[gpg --batch --enarmor]
147
- out = nil
194
+ out = T.let(nil, T.nilable(String))
148
195
 
149
196
  log.debug('+ ' + cmd.join(' '))
150
197
  Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
151
- stdout: Subprocess::PIPE) do |p|
198
+ stdout: Subprocess::PIPE) do |p|
152
199
  out, _err = p.communicate(data)
153
200
  end
154
201
 
202
+ out = T.must(out)
203
+
155
204
  if strip_comments
156
205
  out = out.each_line.select { |l| !l.start_with?('Comment: ') }.join
157
206
  end
@@ -159,32 +208,36 @@ module Paperback
159
208
  out
160
209
  end
161
210
 
211
+ sig {params(data: String).returns(String)}
162
212
  def self.gpg_ascii_dearmor(data)
163
213
  cmd = %w[gpg --batch --dearmor]
164
- out = nil
214
+ out = T.let(nil, T.nilable(String))
165
215
 
166
216
  log.debug('+ ' + cmd.join(' '))
167
217
  Subprocess.check_call(cmd, stdin: Subprocess::PIPE,
168
- stdout: Subprocess::PIPE) do |p|
218
+ stdout: Subprocess::PIPE) do |p|
169
219
  out, _err = p.communicate(data)
170
220
  end
171
221
 
172
- out
222
+ T.must(out)
173
223
  end
174
224
 
175
225
  # Whether to add the Base64 encoding to the generated document.
176
226
  #
177
227
  # @return [Boolean]
228
+ sig {returns(T::Boolean)}
178
229
  def include_base64?
179
230
  !!@include_base64
180
231
  end
181
232
 
182
233
  private
183
234
 
235
+ sig {returns(RQRCode::QRCode)}
184
236
  def qr_code
185
237
  @qr_code ||= qr_code!
186
238
  end
187
239
 
240
+ sig {returns(RQRCode::QRCode)}
188
241
  def qr_code!
189
242
  log.info('Generating QR code')
190
243
 
@@ -211,12 +264,14 @@ module Paperback
211
264
  RQRCode::QRCode.new(input, level: @qr_level)
212
265
  end
213
266
 
267
+ sig {returns(T::Array[String])}
214
268
  def sixword_lines
215
269
  log.info('Encoding with Sixword')
216
270
  @sixword_lines ||=
217
271
  Sixword.pad_encode_to_sentences(data).map(&:downcase)
218
272
  end
219
273
 
274
+ sig {returns(String)}
220
275
  def base64_content
221
276
  log.debug('Encoding with Base64')
222
277
  if encrypt
@@ -1,5 +1,6 @@
1
+ # typed: strong
1
2
  # frozen_string_literal: true
2
3
  module Paperback
3
4
  # version string
4
- VERSION = '0.0.4'
5
+ VERSION = '0.0.5'
5
6
  end
data/lib/paperback.rb CHANGED
@@ -1,7 +1,14 @@
1
+ # typed: strict
1
2
  require 'logger'
3
+ require 'sorbet-runtime'
2
4
 
3
5
  # Paperback is a library for creating paper backups of sensitive data.
4
6
  module Paperback
7
+ extend T::Sig
8
+
9
+ @log = T.let(nil, T.nilable(Logger))
10
+
11
+ sig {returns(Logger)}
5
12
  def self.log
6
13
  return @log if @log
7
14
  @log = Logger.new(STDERR)
@@ -10,6 +17,7 @@ module Paperback
10
17
  @log
11
18
  end
12
19
 
20
+ sig {params(klass: Class, stream: IO).returns(Logger)}
13
21
  def self.class_log(klass, stream=STDERR)
14
22
  log = Logger.new(stream)
15
23
  log.progname = klass.name
@@ -17,10 +25,14 @@ module Paperback
17
25
  log
18
26
  end
19
27
 
28
+ @log_level = T.let(nil, T.nilable(Integer))
29
+
30
+ sig {returns(Integer)}
20
31
  def self.log_level
21
32
  @log_level ||= Logger::INFO
22
33
  end
23
34
 
35
+ sig {params(val: Integer).returns(Integer)}
24
36
  def self.log_level=(val)
25
37
  @log_level = val
26
38
  end