iodine 0.7.59 → 0.8.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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +15 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -3
  5. data/CHANGELOG.md +132 -12
  6. data/Gemfile +5 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +147 -671
  9. data/Rakefile +9 -16
  10. data/examples/chatroom.nru +9 -0
  11. data/examples/client +74 -0
  12. data/examples/config-hello.nru +9 -0
  13. data/examples/config-hello.ru +11 -0
  14. data/examples/config-hijack.ru +13 -0
  15. data/examples/config-shootout.ru +43 -0
  16. data/examples/config.nru +120 -0
  17. data/examples/config.rb +37 -0
  18. data/examples/config.ru +42 -53
  19. data/examples/fake +24 -0
  20. data/examples/hello_neorack +137 -0
  21. data/examples/routes.nru +29 -0
  22. data/examples/routes.ru +42 -0
  23. data/exe/iodine +218 -258
  24. data/ext/iodine/extconf.rb +27 -100
  25. data/ext/iodine/fio-stl.h +128336 -0
  26. data/ext/iodine/fio-stl.md +26396 -0
  27. data/ext/iodine/iodine.c +213 -1393
  28. data/ext/iodine/iodine.h +250 -55
  29. data/ext/iodine/iodine_arg_helper.h +329 -0
  30. data/ext/iodine/iodine_caller.h +234 -25
  31. data/ext/iodine/iodine_cli.h +434 -0
  32. data/ext/iodine/iodine_compression.h +312 -0
  33. data/ext/iodine/iodine_connection.h +3484 -34
  34. data/ext/iodine/iodine_core.h +363 -0
  35. data/ext/iodine/iodine_crypto.h +1441 -0
  36. data/ext/iodine/iodine_defer.h +225 -4
  37. data/ext/iodine/iodine_json.h +738 -4
  38. data/ext/iodine/iodine_listener.h +263 -0
  39. data/ext/iodine/iodine_log.h +297 -0
  40. data/ext/iodine/iodine_minimap.h +543 -0
  41. data/ext/iodine/iodine_mustache.h +461 -4
  42. data/ext/iodine/iodine_pubsub_eng.h +729 -0
  43. data/ext/iodine/iodine_pubsub_history.h +547 -0
  44. data/ext/iodine/iodine_pubsub_msg.h +228 -0
  45. data/ext/iodine/iodine_redis.h +322 -0
  46. data/ext/iodine/iodine_store.h +418 -16
  47. data/ext/iodine/iodine_threads.h +279 -0
  48. data/ext/iodine/iodine_tls.h +515 -6
  49. data/ext/iodine/iodine_utils.h +923 -0
  50. data/iodine.gemspec +37 -42
  51. data/lib/iodine/benchmark.rb +1690 -0
  52. data/lib/iodine/documentation.rb +3085 -0
  53. data/lib/iodine/version.rb +17 -1
  54. data/lib/iodine.rb +54 -255
  55. data/logo.svg +36 -0
  56. data/mustache.md +55 -0
  57. data/sig/iodine.rbs +4 -0
  58. metadata +58 -184
  59. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  60. data/.github/workflows/ruby.yml +0 -42
  61. data/.gitignore +0 -20
  62. data/LIMITS.md +0 -41
  63. data/SPEC-PubSub-Draft.md +0 -159
  64. data/SPEC-WebSocket-Draft.md +0 -239
  65. data/bin/console +0 -22
  66. data/bin/info.md +0 -353
  67. data/bin/mustache_bench.rb +0 -100
  68. data/bin/poc/Gemfile.lock +0 -23
  69. data/bin/poc/README.md +0 -37
  70. data/bin/poc/config.ru +0 -66
  71. data/bin/poc/gemfile +0 -1
  72. data/bin/poc/www/index.html +0 -57
  73. data/examples/async_task.ru +0 -92
  74. data/examples/bates/README.md +0 -3
  75. data/examples/bates/config.ru +0 -342
  76. data/examples/bates/david+bold.pdf +0 -0
  77. data/examples/bates/public/drop-pdf.png +0 -0
  78. data/examples/bates/public/index.html +0 -600
  79. data/examples/echo.ru +0 -59
  80. data/examples/etag.ru +0 -16
  81. data/examples/hello.ru +0 -29
  82. data/examples/pubsub_engine.ru +0 -81
  83. data/examples/rack3.ru +0 -12
  84. data/examples/redis.ru +0 -70
  85. data/examples/shootout.ru +0 -73
  86. data/examples/sub-protocols.ru +0 -90
  87. data/examples/tcp_client.rb +0 -66
  88. data/examples/x-sendfile.ru +0 -14
  89. data/ext/iodine/fio.c +0 -11995
  90. data/ext/iodine/fio.h +0 -6371
  91. data/ext/iodine/fio_cli.c +0 -431
  92. data/ext/iodine/fio_cli.h +0 -189
  93. data/ext/iodine/fio_json_parser.h +0 -688
  94. data/ext/iodine/fio_siphash.c +0 -157
  95. data/ext/iodine/fio_siphash.h +0 -37
  96. data/ext/iodine/fio_tls.h +0 -129
  97. data/ext/iodine/fio_tls_missing.c +0 -649
  98. data/ext/iodine/fio_tls_openssl.c +0 -1106
  99. data/ext/iodine/fio_tmpfile.h +0 -50
  100. data/ext/iodine/fiobj.h +0 -44
  101. data/ext/iodine/fiobj4fio.h +0 -21
  102. data/ext/iodine/fiobj_ary.c +0 -333
  103. data/ext/iodine/fiobj_ary.h +0 -139
  104. data/ext/iodine/fiobj_data.c +0 -1187
  105. data/ext/iodine/fiobj_data.h +0 -167
  106. data/ext/iodine/fiobj_hash.c +0 -409
  107. data/ext/iodine/fiobj_hash.h +0 -176
  108. data/ext/iodine/fiobj_json.c +0 -622
  109. data/ext/iodine/fiobj_json.h +0 -68
  110. data/ext/iodine/fiobj_mem.h +0 -71
  111. data/ext/iodine/fiobj_mustache.c +0 -317
  112. data/ext/iodine/fiobj_mustache.h +0 -62
  113. data/ext/iodine/fiobj_numbers.c +0 -344
  114. data/ext/iodine/fiobj_numbers.h +0 -127
  115. data/ext/iodine/fiobj_str.c +0 -433
  116. data/ext/iodine/fiobj_str.h +0 -172
  117. data/ext/iodine/fiobject.c +0 -620
  118. data/ext/iodine/fiobject.h +0 -655
  119. data/ext/iodine/hpack.h +0 -1923
  120. data/ext/iodine/http.c +0 -2743
  121. data/ext/iodine/http.h +0 -1002
  122. data/ext/iodine/http1.c +0 -910
  123. data/ext/iodine/http1.h +0 -29
  124. data/ext/iodine/http1_parser.h +0 -878
  125. data/ext/iodine/http_internal.c +0 -1279
  126. data/ext/iodine/http_internal.h +0 -237
  127. data/ext/iodine/http_mime_parser.h +0 -350
  128. data/ext/iodine/iodine_caller.c +0 -230
  129. data/ext/iodine/iodine_connection.c +0 -941
  130. data/ext/iodine/iodine_defer.c +0 -420
  131. data/ext/iodine/iodine_fiobj2rb.h +0 -120
  132. data/ext/iodine/iodine_helpers.c +0 -282
  133. data/ext/iodine/iodine_helpers.h +0 -12
  134. data/ext/iodine/iodine_http.c +0 -1181
  135. data/ext/iodine/iodine_http.h +0 -23
  136. data/ext/iodine/iodine_json.c +0 -302
  137. data/ext/iodine/iodine_mustache.c +0 -567
  138. data/ext/iodine/iodine_pubsub.c +0 -580
  139. data/ext/iodine/iodine_pubsub.h +0 -26
  140. data/ext/iodine/iodine_rack_io.c +0 -273
  141. data/ext/iodine/iodine_rack_io.h +0 -20
  142. data/ext/iodine/iodine_store.c +0 -146
  143. data/ext/iodine/iodine_tcp.c +0 -346
  144. data/ext/iodine/iodine_tcp.h +0 -13
  145. data/ext/iodine/iodine_tls.c +0 -261
  146. data/ext/iodine/mustache_parser.h +0 -1546
  147. data/ext/iodine/redis_engine.c +0 -957
  148. data/ext/iodine/redis_engine.h +0 -79
  149. data/ext/iodine/resp_parser.h +0 -317
  150. data/ext/iodine/websocket_parser.h +0 -506
  151. data/ext/iodine/websockets.c +0 -743
  152. data/ext/iodine/websockets.h +0 -185
  153. data/lib/iodine/connection.rb +0 -61
  154. data/lib/iodine/json.rb +0 -42
  155. data/lib/iodine/mustache.rb +0 -113
  156. data/lib/iodine/pubsub.rb +0 -55
  157. data/lib/iodine/rack_utils.rb +0 -43
  158. data/lib/iodine/tls.rb +0 -16
  159. data/lib/rack/handler/iodine.rb +0 -33
  160. data/logo.png +0 -0
@@ -0,0 +1,1690 @@
1
+ require 'iodine'
2
+
3
+ module Iodine
4
+ # This module includes benchmarking methods to test performance.
5
+ #
6
+ # Before running the benchmarks, require the `Iodine::Benchmark` module:
7
+ #
8
+ # require 'iodine/benchmark'
9
+ #
10
+ # This module does **not** include benchmarking for the Iodine server itself.
11
+ #
12
+ # These methods and the results shown should be used with caution, as they often tell a partial story.
13
+ #
14
+ # To run the benchmarks, you'll need to install the `benchmark-ips` gem.
15
+ #
16
+ # To install the benchmarking gem add `benchmark-ips` to your `Gemfile` or run:
17
+ #
18
+ # gem install benchmark-ips
19
+ #
20
+ module Benchmark
21
+ # Benchmarks Iodine::JSON vs native Ruby JSON and vs Oj (if installed).
22
+ def self.json(data_length = 1000)
23
+ begin
24
+ require 'oj'
25
+ rescue LoadError
26
+ nil
27
+ end
28
+ require 'benchmark/ips'
29
+ require 'json'
30
+ # make a big data store with nothings
31
+ data_1000 = []
32
+ data_length.times do
33
+ tmp = { f: rand }
34
+ tmp[:i] = (tmp[:f] * 1_000_000).to_i
35
+ tmp[:str] = tmp[:i].to_s
36
+ tmp[:escaped] = "Hello\nNew\nLines!"
37
+ tmp[:sym] = tmp[:str].to_sym
38
+ tmp[:ary] = []
39
+ tmp[:ary_empty] = []
40
+ tmp[:hash_empty] = {}
41
+ 100.times { |i| tmp[:ary] << i }
42
+ data_1000 << tmp
43
+ end
44
+ json_string = data_1000.to_json
45
+ puts '-----'
46
+ puts "Benchmark #{data_1000.length} item tree, and #{json_string.length} bytes of JSON"
47
+ # benchmark stringification
48
+ ::Benchmark.ips do |x|
49
+ x.report(' Ruby obj.to_json') do |_times|
50
+ data_1000.to_json
51
+ end
52
+ x.report('::JSON.pretty_generate') do |_times|
53
+ ::JSON.pretty_generate(data_1000)
54
+ end
55
+ x.report('Iodine::JSON.stringify') do |_times|
56
+ Iodine::JSON.stringify(data_1000)
57
+ end
58
+ x.report('Iodine::JSON.beautify') do |_times|
59
+ Iodine::JSON.beautify(data_1000)
60
+ end
61
+ if defined?(Oj)
62
+ x.report(' Oj.dump') do |_times|
63
+ Oj.dump(data_1000)
64
+ end
65
+ end
66
+ x.compare!
67
+ end
68
+
69
+ # benchmark parsing
70
+ ::Benchmark.ips do |x|
71
+ x.report(' Ruby JSON.parse') do |_times|
72
+ JSON.parse(json_string)
73
+ end
74
+ x.report('Iodine::JSON.parse') do |_times|
75
+ Iodine::JSON.parse(json_string)
76
+ end
77
+ if defined?(Oj)
78
+ x.report(' Oj.load') do |_times|
79
+ Oj.load(json_string)
80
+ end
81
+ end
82
+ x.compare!
83
+ end
84
+ nil
85
+ end
86
+
87
+ # Benchmarks Iodine::Mustache vs the original Mustache Ruby gem (if installed).
88
+ #
89
+ # Benchmark code was copied, in part, from:
90
+ # https://github.com/mustache/mustache/blob/master/benchmarks/render_collection_benchmark.rb
91
+ #
92
+ # The test is, sadly, biased and doesn't test for missing elements, proc/method resolution or template partials.
93
+ #
94
+ def self.mustache(items = 1000)
95
+ require 'benchmark/ips'
96
+ require 'mustache'
97
+ template = "
98
+ {{#products}}
99
+ <div class='product_brick'>
100
+ <div class='container'>
101
+ <div class='element'>
102
+ <img src='images/{{image}}' class='product_miniature' />
103
+ </div>
104
+ <div class='element description'>
105
+ <a href={{url}} class='product_name block bold'>
106
+ {{external_index}}
107
+ </a>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ {{/products}}
112
+ "
113
+
114
+ # fill Hash objects with values for template rendering
115
+ data = {
116
+ products: []
117
+ }
118
+ data_escaped = {
119
+ products: []
120
+ }
121
+
122
+ items.times do
123
+ data[:products] << {
124
+ external_index: 'product',
125
+ url: '/products/7',
126
+ image: 'products/product.jpg'
127
+ }
128
+ data_escaped[:products] << {
129
+ external_index: "This <product> should've been \"properly\" escaped.",
130
+ url: '/products/7',
131
+ image: 'products/product.jpg'
132
+ }
133
+ end
134
+
135
+ # prepare Iodine::Mustache reduced Mustache template engine
136
+ mus_view = Iodine::Mustache.new template: template
137
+
138
+ # prepare official Mustache template engine
139
+ view = ::Mustache.new
140
+ view.template = template
141
+ view.render # Call render once so the template will be compiled
142
+
143
+ # benchmark different use cases
144
+ ::Benchmark.ips do |x|
145
+ x.report("Ruby Mustache render list of #{items}") do |_times|
146
+ view.render(data)
147
+ end
148
+ x.report("Iodine::Mustache render list of #{items}") do |_times|
149
+ mus_view.render(data)
150
+ end
151
+
152
+ x.report("Ruby Mustache render list of #{items} with escaped data") do |_times|
153
+ view.render(data_escaped)
154
+ end
155
+ x.report("Iodine::Mustache render list of #{items} with escaped data") do |_times|
156
+ mus_view.render(data_escaped)
157
+ end
158
+
159
+ x.report("Ruby Mustache - no caching - render list of #{items}") do |_times|
160
+ ::Mustache.render(template, data)
161
+ end
162
+ x.report("Iodine::Mustache - no caching - render list of #{items}") do |_times|
163
+ Iodine::Mustache.render(template: template, ctx: data)
164
+ end
165
+ x.compare!
166
+ end
167
+ nil
168
+ end
169
+
170
+ # Benchmarks keyed MACs (including non-cryptographic Risky Hash MACs).
171
+ def self.hmac
172
+ require 'benchmark/ips'
173
+ begin
174
+ require 'openssl'
175
+ rescue LoadError
176
+ nil
177
+ end
178
+
179
+ # -----------------------------------------------------------------------
180
+ # HMAC benchmarks — all MACs (cryptographic and non-cryptographic) ranked
181
+ #
182
+ # NOTE on algorithm mapping:
183
+ # hmac128 => Poly1305 MAC (not HMAC-SHA1/SHA256; no standard OpenSSL equivalent)
184
+ # hmac160 => HMAC-SHA1 (compared against OpenSSL HMAC-SHA1)
185
+ # hmac256 => HMAC-SHA256 (compared against OpenSSL HMAC-SHA256)
186
+ # hmac512 => HMAC-SHA512 (compared against OpenSSL HMAC-SHA512)
187
+ # risky256_hmac => keyed MAC on non-crypto Risky Hash — NOT for security-critical use
188
+ # risky512_hmac => keyed MAC on non-crypto Risky Hash — NOT for security-critical use
189
+ #
190
+ # Payload: 512 bytes of the benchmark file itself (representative of a
191
+ # short-to-medium authenticated payload).
192
+ # -----------------------------------------------------------------------
193
+ benchmark_secret = Iodine.secret
194
+ benchmark_payload = (File.exist?(__FILE__) ? IO.binread(__FILE__)[0...512] : 'my message payload')
195
+
196
+ puts "HMAC benchmarks — all MACs ranked — #{benchmark_payload.bytesize}-byte message, #{benchmark_secret.bytesize}-byte secret"
197
+ puts " [WARNING: risky256_hmac / risky512_hmac are NOT suitable for security-critical authentication]"
198
+
199
+ # Pre-create OpenSSL digest objects outside the hot loop. Compare the
200
+ # same Base64 output format as Iodine's hmac* methods.
201
+ if defined?(OpenSSL)
202
+ digest_sha1 = OpenSSL::Digest.new('SHA1')
203
+ digest_sha256 = OpenSSL::Digest.new('SHA256')
204
+ digest_sha512 = OpenSSL::Digest.new('SHA512')
205
+ end
206
+
207
+ ::Benchmark.ips do |bm|
208
+ # Non-cryptographic MACs — no standard equivalent; included for throughput ranking
209
+ bm.report('Iodine::Utils.risky256_hmac (non-crypto, 32B)') do
210
+ Iodine::Utils.risky256_hmac(benchmark_secret, benchmark_payload)
211
+ end
212
+ bm.report('Iodine::Utils.risky512_hmac (non-crypto, 64B)') do
213
+ Iodine::Utils.risky512_hmac(benchmark_secret, benchmark_payload)
214
+ end
215
+ # Poly1305 MAC — no direct OpenSSL equivalent; shown for reference only
216
+ bm.report('Iodine::Utils.hmac128 (Poly1305) ') do
217
+ Iodine::Utils.hmac128(benchmark_secret, benchmark_payload)
218
+ end
219
+ # HMAC-SHA1
220
+ bm.report('Iodine::Utils.hmac160 (HMAC-SHA1) ') do
221
+ Iodine::Utils.hmac160(benchmark_secret, benchmark_payload)
222
+ end
223
+ # HMAC-SHA256
224
+ bm.report('Iodine::Utils.hmac256 (HMAC-SHA256) ') do
225
+ Iodine::Utils.hmac256(benchmark_secret, benchmark_payload)
226
+ end
227
+ # HMAC-SHA512
228
+ bm.report('Iodine::Utils.hmac512 (HMAC-SHA512) ') do
229
+ Iodine::Utils.hmac512(benchmark_secret, benchmark_payload)
230
+ end
231
+ if defined?(OpenSSL)
232
+ bm.report('OpenSSL::HMAC.base64digest SHA1 ') do
233
+ OpenSSL::HMAC.base64digest(digest_sha1, benchmark_secret, benchmark_payload)
234
+ end
235
+ bm.report('OpenSSL::HMAC.base64digest SHA256 ') do
236
+ OpenSSL::HMAC.base64digest(digest_sha256, benchmark_secret, benchmark_payload)
237
+ end
238
+ bm.report('OpenSSL::HMAC.base64digest SHA512 ') do
239
+ OpenSSL::HMAC.base64digest(digest_sha512, benchmark_secret, benchmark_payload)
240
+ end
241
+ end
242
+ bm.compare!
243
+ end
244
+
245
+ nil
246
+ end
247
+
248
+ # Benchmarks UUIDs, random bytes (including secure_random), and random floats.
249
+ def self.random
250
+ require 'benchmark/ips'
251
+ require 'securerandom'
252
+ begin
253
+ require 'openssl'
254
+ rescue LoadError
255
+ nil
256
+ end
257
+
258
+ # -----------------------------------------------------------------------
259
+ # UUID generation
260
+ # -----------------------------------------------------------------------
261
+ puts "\nUUID generation (random v4)"
262
+ ::Benchmark.ips do |bm|
263
+ bm.report('Iodine::Utils.uuid') { Iodine::Utils.uuid }
264
+ bm.report(' SecureRandom.uuid') { SecureRandom.uuid }
265
+ bm.report(' Random.uuid_v4') { Random.uuid_v4 }
266
+ bm.compare!
267
+ end
268
+
269
+ # -----------------------------------------------------------------------
270
+ # Random bytes generation — all generators ranked per size
271
+ #
272
+ # Iodine::Utils.random uses a fast PRNG (not cryptographically secure).
273
+ # Iodine::Utils.secure_random requests system CSPRNG bytes (with a
274
+ # PRNG fallback if the system source fails); unlike random, it is
275
+ # intended for keys and nonces under normal operation.
276
+ # Compared against SecureRandom.random_bytes and OpenSSL::Random.random_bytes.
277
+ # -----------------------------------------------------------------------
278
+ puts "\nRandom bytes — all generators ranked — 16 bytes"
279
+ ::Benchmark.ips do |bm|
280
+ bm.report('Iodine::Utils.random(16) ') { Iodine::Utils.random(16) }
281
+ bm.report('Random.bytes(16) ') { ::Random.bytes(16) }
282
+ bm.report('Iodine::Utils.secure_random(bytes: 16)') { Iodine::Utils.secure_random(bytes: 16) }
283
+ bm.report('SecureRandom.random_bytes(16) ') { SecureRandom.random_bytes(16) }
284
+ if defined?(OpenSSL)
285
+ bm.report('OpenSSL::Random.random_bytes(16) ') { OpenSSL::Random.random_bytes(16) }
286
+ end
287
+ bm.compare!
288
+ end
289
+
290
+ puts "\nRandom bytes — all generators ranked — 4096 bytes"
291
+ ::Benchmark.ips do |bm|
292
+ bm.report('Iodine::Utils.random(4096) ') { Iodine::Utils.random(4096) }
293
+ bm.report('Random.bytes(4096) ') { ::Random.bytes(4096) }
294
+ bm.report('Iodine::Utils.secure_random(bytes: 4096)') { Iodine::Utils.secure_random(bytes: 4096) }
295
+ bm.report('SecureRandom.random_bytes(4096) ') { SecureRandom.random_bytes(4096) }
296
+ if defined?(OpenSSL)
297
+ bm.report('OpenSSL::Random.random_bytes(4096) ') { OpenSSL::Random.random_bytes(4096) }
298
+ end
299
+ bm.compare!
300
+ end
301
+
302
+ puts "\nRandom float (0.0..1.0)"
303
+ ::Benchmark.ips do |bm|
304
+ bm.report('Iodine::Utils.random') { Iodine::Utils.random }
305
+ bm.report(' SecureRandom.rand') { SecureRandom.rand }
306
+ bm.report(' Random.rand') { ::Random.rand }
307
+ bm.compare!
308
+ end
309
+
310
+ nil
311
+ end
312
+
313
+ # Benchmark the {Iodine::Utils} module and see if you want to use {Iodine::Utils.monkey_patch} when using Rack.
314
+ def self.utils
315
+ require 'rack'
316
+ require 'cgi'
317
+ require 'benchmark/ips'
318
+ # a String in need of decoding
319
+ encoded = '%E3 + %83 + %AB + %E3 + %83 + %93 + %E3 + %82 + %A4 + %E3 + %82 + %B9 + %E3 + %81 + %A8'
320
+ decoded = ::Rack::Utils.unescape(encoded, 'binary')
321
+ html_xss = "<script>alert('avoid xss attacks')</script>"
322
+ html_xss_safe = Rack::Utils.escape_html html_xss
323
+ short_str1 = Array.new(64) { 'a' }.join
324
+ short_str2 = Array.new(64) { 'a' }.join
325
+ long_str1 = Array.new(4094) { 'a' }.join
326
+ long_str2 = Array.new(4094) { 'a' }.join
327
+ now_preclaculated = Time.now
328
+ ::Benchmark.ips do |bm|
329
+ bm.report(' Iodine rfc2822') { Iodine::Utils.rfc2822(now_preclaculated) }
330
+ bm.report(' Rack rfc2822') { ::Rack::Utils.rfc2822(now_preclaculated) }
331
+ bm.compare!
332
+ end; ::Benchmark.ips do |bm|
333
+ bm.report('Iodine unescape') { Iodine::Utils.unescape encoded }
334
+ bm.report(' Rack unescape') { ::Rack::Utils.unescape encoded }
335
+ bm.compare!
336
+ end; ::Benchmark.ips do |bm|
337
+ bm.report('Iodine escape') { Iodine::Utils.escape decoded }
338
+ bm.report(' Rack escape') { ::Rack::Utils.escape decoded }
339
+ bm.compare!
340
+ end; ::Benchmark.ips do |bm|
341
+ bm.report('Iodine escape HTML') { Iodine::Utils.escape_html html_xss }
342
+ bm.report(' Rack escape HTML') { ::Rack::Utils.escape_html html_xss }
343
+ bm.compare!
344
+ end; ::Benchmark.ips do |bm|
345
+ bm.report('Iodine unescape HTML') { Iodine::Utils.unescape_html html_xss_safe }
346
+ bm.report(' CGI unescape HTML') { CGI.unescapeHTML html_xss_safe }
347
+ bm.compare!
348
+ end; ::Benchmark.ips do |bm|
349
+ bm.report("Iodine secure compare (#{short_str2.bytesize} Bytes)") do
350
+ Iodine::Utils.secure_compare short_str1, short_str2
351
+ end
352
+ bm.report(" Rack secure compare (#{short_str2.bytesize} Bytes)") do
353
+ ::Rack::Utils.secure_compare short_str1, short_str2
354
+ end
355
+ bm.compare!
356
+ end; ::Benchmark.ips do |bm|
357
+ bm.report("Iodine secure compare (#{long_str1.bytesize} Bytes)") do
358
+ Iodine::Utils.secure_compare long_str1, long_str2
359
+ end
360
+ bm.report(" Rack secure compare (#{long_str1.bytesize} Bytes)") do
361
+ ::Rack::Utils.secure_compare long_str1, long_str2
362
+ end
363
+ bm.compare!
364
+ end && nil
365
+ end
366
+
367
+ # Benchmark the internal mini-Hash map.
368
+ def self.minimap(counter = 20)
369
+ require 'benchmark/ips'
370
+ keys = []
371
+ (counter + 1).times { |i| keys << "counter_string_#{i}" }
372
+ frozen_keys = []
373
+ (counter + 1).times { |i| frozen_keys << "counter_string_#{i}".freeze }
374
+
375
+ tests = [
376
+ ["New (#{counter * 2} Empty Maps)", proc { |m| counter.times { m.class.new } }, true],
377
+ ["New + Set (#{counter} Numbers)", proc { |m| counter.times { |i| m[i] = i } }, true],
378
+ ["Overwrite (#{counter} Numbers)", proc { |m| counter.times { |i| m[i] = i } }],
379
+ ["Get (#{counter} Numbers + 1 missing)", proc { |m| (counter + 1).times { |i| m[i] } }],
380
+ ["New + Set (#{counter} Strings)", proc { |m| counter.times { |i| m["counter_string_#{i}"] = i } }, true],
381
+ ["Overwrite (#{counter} Strings)", proc { |m| counter.times { |i| m["counter_string_#{i}"] = i } }],
382
+ ["Get (#{counter} Strings + 1 missing)", proc { |m| (counter + 1).times { |i| m["counter_string_#{i}"] } }],
383
+ ["New + Set (#{counter} Strings.freeze)", proc { |m|
384
+ counter.times do |i|
385
+ m[frozen_keys[i]] = keys[i]
386
+ end
387
+ }, true],
388
+ ["Overwrite (#{counter} Strings.freeze)", proc { |m| counter.times { |i| m[frozen_keys[i]] = keys[i] } }],
389
+ ["Get (#{counter} Strings.freeze + 1 missing)", proc { |m| (counter + 1).times { |i| m[frozen_keys[i]] } }],
390
+ ["each (#{counter} Strings)", proc { |m|
391
+ m.each do |k, v|
392
+ end
393
+ }]
394
+ ]
395
+
396
+ klasses = [Iodine::Base::MiniMap, ::Hash]
397
+
398
+ maps = []
399
+ klasses.each { |k| maps << k.new }
400
+
401
+ tests.each do |t|
402
+ ::Benchmark.ips do |bm|
403
+ klasses.each_index do |i|
404
+ if t[2]
405
+ bm.report("#{klasses[i].name.ljust(22)} #{t[0]}") do
406
+ maps[i] = klasses[i].new
407
+ t[1].call(maps[i])
408
+ end
409
+ else
410
+ bm.report("#{klasses[i].name.ljust(22)} #{t[0]}") { t[1].call(maps[i]) }
411
+ end
412
+ end
413
+ bm.compare!
414
+ end
415
+ end
416
+ nil
417
+ end
418
+
419
+ # Benchmarks Iodine's AEAD symmetric encryption ciphers vs OpenSSL equivalents.
420
+ #
421
+ # Tests ChaCha20-Poly1305, XChaCha20-Poly1305, AES-128-GCM, and AES-256-GCM
422
+ # with various payload sizes (64 bytes, 1KB, 64KB).
423
+ #
424
+ # Requires OpenSSL for comparison benchmarks.
425
+ #
426
+ # FAIRNESS NOTES:
427
+ # - All ciphertexts for decryption are pre-computed OUTSIDE the hot loop so
428
+ # that only the actual encrypt/decrypt operation is timed.
429
+ # - OpenSSL cipher setup (new/encrypt/key/iv/auth_data) is inside the hot
430
+ # loop because Iodine also re-initialises state per call — both sides pay
431
+ # the same per-call setup cost.
432
+ # - XChaCha20-Poly1305 has no OpenSSL equivalent; it is included in the
433
+ # unified block with a label suffix so its throughput is visible in context.
434
+ # - All implementations use the same key, nonce, and additional data.
435
+ def self.crypto_symmetric
436
+ require 'benchmark/ips'
437
+ begin
438
+ require 'openssl'
439
+ rescue LoadError
440
+ nil
441
+ end
442
+
443
+ # Test data sizes
444
+ sizes = {
445
+ '64B' => 64,
446
+ '1KB' => 1024,
447
+ '64KB' => 65_536
448
+ }
449
+
450
+ # Keys and nonces — generated once, reused across all benchmarks
451
+ key_32 = Iodine::Utils.random(32)
452
+ key_16 = Iodine::Utils.random(16)
453
+ nonce_12 = Iodine::Utils.random(12)
454
+ nonce_24 = Iodine::Utils.random(24)
455
+ ad = 'additional authenticated data'
456
+
457
+ # Check which OpenSSL ciphers are available
458
+ openssl_chacha_available = defined?(OpenSSL) &&
459
+ OpenSSL::Cipher.ciphers.include?('chacha20-poly1305')
460
+ openssl_aes_available = defined?(OpenSSL)
461
+
462
+ # Label width — wide enough for 'Iodine XChaCha20-Poly1305 encrypt (24B nonce)'
463
+ lw = 36
464
+
465
+ puts '=' * 70
466
+ puts 'AEAD Symmetric Encryption Benchmarks'
467
+ puts '=' * 70
468
+ puts
469
+ puts 'NOTE: All ciphertexts are pre-computed outside the hot loop.'
470
+ puts 'OpenSSL cipher setup is inside the loop (same cost as Iodine).'
471
+ puts 'XChaCha20-Poly1305 has no OpenSSL equivalent — included in the'
472
+ puts 'unified block with a label note so throughput is visible in context.'
473
+ puts
474
+
475
+ sizes.each do |size_name, size|
476
+ plaintext = Iodine::Utils.random(size)
477
+
478
+ puts "\n--- Payload Size: #{size_name} — all ciphers ranked together ---\n\n"
479
+
480
+ # ------------------------------------------------------------------
481
+ # Pre-compute all ciphertexts outside the hot loop so that only the
482
+ # decrypt operation itself is timed in the decrypt block below.
483
+ # ------------------------------------------------------------------
484
+ iodine_ct_chacha, iodine_mac_chacha =
485
+ Iodine::Base::Crypto::ChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
486
+
487
+ iodine_ct_xchacha, iodine_mac_xchacha =
488
+ Iodine::Base::Crypto::XChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_24, ad: ad)
489
+
490
+ iodine_ct_aes128, iodine_mac_aes128 =
491
+ Iodine::Base::Crypto::AES128GCM.encrypt(plaintext, key: key_16, nonce: nonce_12, ad: ad)
492
+
493
+ iodine_ct_aes256, iodine_mac_aes256 =
494
+ Iodine::Base::Crypto::AES256GCM.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
495
+
496
+ openssl_ct_chacha = nil
497
+ openssl_tag_chacha = nil
498
+ if openssl_chacha_available
499
+ c = OpenSSL::Cipher.new('chacha20-poly1305')
500
+ c.encrypt
501
+ c.key = key_32
502
+ c.iv = nonce_12
503
+ c.auth_data = ad
504
+ openssl_ct_chacha = c.update(plaintext) + c.final
505
+ openssl_tag_chacha = c.auth_tag
506
+ end
507
+
508
+ openssl_ct_aes128 = nil
509
+ openssl_tag_aes128 = nil
510
+ openssl_ct_aes256 = nil
511
+ openssl_tag_aes256 = nil
512
+ if openssl_aes_available
513
+ c = OpenSSL::Cipher.new('aes-128-gcm')
514
+ c.encrypt
515
+ c.key = key_16
516
+ c.iv = nonce_12
517
+ c.auth_data = ad
518
+ openssl_ct_aes128 = c.update(plaintext) + c.final
519
+ openssl_tag_aes128 = c.auth_tag
520
+
521
+ c = OpenSSL::Cipher.new('aes-256-gcm')
522
+ c.encrypt
523
+ c.key = key_32
524
+ c.iv = nonce_12
525
+ c.auth_data = ad
526
+ openssl_ct_aes256 = c.update(plaintext) + c.final
527
+ openssl_tag_aes256 = c.auth_tag
528
+ end
529
+
530
+ # ------------------------------------------------------------------
531
+ # Unified encrypt block — all ciphers compete side-by-side.
532
+ # XChaCha20 has no OpenSSL equivalent — labelled with (no OSSl equiv).
533
+ # x.compare! prints a ranked table so the fastest cipher is clear.
534
+ # ------------------------------------------------------------------
535
+ puts "Encrypt — all ciphers ranked:"
536
+ puts " (XChaCha20-Poly1305 has no OpenSSL equivalent — included for ranking context)"
537
+ ::Benchmark.ips do |x|
538
+ x.report('Iodine ChaCha20-Poly1305 encrypt'.ljust(lw)) do
539
+ Iodine::Base::Crypto::ChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
540
+ end
541
+ if openssl_chacha_available
542
+ # OpenSSL cipher setup is inside the loop — same per-call cost as Iodine
543
+ x.report('OpenSSL ChaCha20-Poly1305 encrypt'.ljust(lw)) do
544
+ c = OpenSSL::Cipher.new('chacha20-poly1305')
545
+ c.encrypt
546
+ c.key = key_32
547
+ c.iv = nonce_12
548
+ c.auth_data = ad
549
+ _ = c.update(plaintext) + c.final
550
+ c.auth_tag
551
+ end
552
+ end
553
+ # XChaCha20 has no OpenSSL equivalent — included for ranking context
554
+ x.report('Iodine XChaCha20-Poly1305 encrypt (24B nonce)'.ljust(lw)) do
555
+ Iodine::Base::Crypto::XChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_24, ad: ad)
556
+ end
557
+ x.report('Iodine AES-128-GCM encrypt'.ljust(lw)) do
558
+ Iodine::Base::Crypto::AES128GCM.encrypt(plaintext, key: key_16, nonce: nonce_12, ad: ad)
559
+ end
560
+ if openssl_aes_available
561
+ x.report('OpenSSL AES-128-GCM encrypt'.ljust(lw)) do
562
+ c = OpenSSL::Cipher.new('aes-128-gcm')
563
+ c.encrypt
564
+ c.key = key_16
565
+ c.iv = nonce_12
566
+ c.auth_data = ad
567
+ _ = c.update(plaintext) + c.final
568
+ c.auth_tag
569
+ end
570
+ end
571
+ x.report('Iodine AES-256-GCM encrypt'.ljust(lw)) do
572
+ Iodine::Base::Crypto::AES256GCM.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
573
+ end
574
+ if openssl_aes_available
575
+ x.report('OpenSSL AES-256-GCM encrypt'.ljust(lw)) do
576
+ c = OpenSSL::Cipher.new('aes-256-gcm')
577
+ c.encrypt
578
+ c.key = key_32
579
+ c.iv = nonce_12
580
+ c.auth_data = ad
581
+ _ = c.update(plaintext) + c.final
582
+ c.auth_tag
583
+ end
584
+ end
585
+ x.compare!
586
+ end
587
+
588
+ # ------------------------------------------------------------------
589
+ # Unified decrypt block — all ciphers compete side-by-side.
590
+ # Ciphertexts were pre-computed above (outside the hot loop).
591
+ # x.compare! prints a ranked table so the fastest cipher is clear.
592
+ # ------------------------------------------------------------------
593
+ puts "\nDecrypt — all ciphers ranked:"
594
+ puts " (XChaCha20-Poly1305 has no OpenSSL equivalent — included for ranking context)"
595
+ ::Benchmark.ips do |x|
596
+ x.report('Iodine ChaCha20-Poly1305 decrypt'.ljust(lw)) do
597
+ Iodine::Base::Crypto::ChaCha20Poly1305.decrypt(
598
+ iodine_ct_chacha, mac: iodine_mac_chacha, key: key_32, nonce: nonce_12, ad: ad
599
+ )
600
+ end
601
+ if openssl_chacha_available
602
+ x.report('OpenSSL ChaCha20-Poly1305 decrypt'.ljust(lw)) do
603
+ d = OpenSSL::Cipher.new('chacha20-poly1305')
604
+ d.decrypt
605
+ d.key = key_32
606
+ d.iv = nonce_12
607
+ d.auth_data = ad
608
+ d.auth_tag = openssl_tag_chacha
609
+ d.update(openssl_ct_chacha) + d.final
610
+ end
611
+ end
612
+ # XChaCha20 has no OpenSSL equivalent — included for ranking context
613
+ x.report('Iodine XChaCha20-Poly1305 decrypt (24B nonce)'.ljust(lw)) do
614
+ Iodine::Base::Crypto::XChaCha20Poly1305.decrypt(
615
+ iodine_ct_xchacha, mac: iodine_mac_xchacha, key: key_32, nonce: nonce_24, ad: ad
616
+ )
617
+ end
618
+ x.report('Iodine AES-128-GCM decrypt'.ljust(lw)) do
619
+ Iodine::Base::Crypto::AES128GCM.decrypt(
620
+ iodine_ct_aes128, mac: iodine_mac_aes128, key: key_16, nonce: nonce_12, ad: ad
621
+ )
622
+ end
623
+ if openssl_aes_available
624
+ x.report('OpenSSL AES-128-GCM decrypt'.ljust(lw)) do
625
+ d = OpenSSL::Cipher.new('aes-128-gcm')
626
+ d.decrypt
627
+ d.key = key_16
628
+ d.iv = nonce_12
629
+ d.auth_data = ad
630
+ d.auth_tag = openssl_tag_aes128
631
+ d.update(openssl_ct_aes128) + d.final
632
+ end
633
+ end
634
+ x.report('Iodine AES-256-GCM decrypt'.ljust(lw)) do
635
+ Iodine::Base::Crypto::AES256GCM.decrypt(
636
+ iodine_ct_aes256, mac: iodine_mac_aes256, key: key_32, nonce: nonce_12, ad: ad
637
+ )
638
+ end
639
+ if openssl_aes_available
640
+ x.report('OpenSSL AES-256-GCM decrypt'.ljust(lw)) do
641
+ d = OpenSSL::Cipher.new('aes-256-gcm')
642
+ d.decrypt
643
+ d.key = key_32
644
+ d.iv = nonce_12
645
+ d.auth_data = ad
646
+ d.auth_tag = openssl_tag_aes256
647
+ d.update(openssl_ct_aes256) + d.final
648
+ end
649
+ end
650
+ x.compare!
651
+ end
652
+ end
653
+
654
+ nil
655
+ end
656
+
657
+ # Benchmarks Iodine's asymmetric cryptography vs OpenSSL/RbNaCl equivalents.
658
+ #
659
+ # Tests Ed25519 signing/verification and X25519 key exchange.
660
+ #
661
+ # Optional dependencies: OpenSSL (>= 1.1.1 for Ed25519/X25519), RbNaCl gem.
662
+ def self.crypto_asymmetric
663
+ require 'benchmark/ips'
664
+ begin
665
+ require 'openssl'
666
+ rescue LoadError
667
+ nil
668
+ end
669
+ begin
670
+ require 'rbnacl'
671
+ rescue LoadError
672
+ nil
673
+ end
674
+
675
+ puts '=' * 70
676
+ puts 'Asymmetric Cryptography Benchmarks'
677
+ puts '=' * 70
678
+
679
+ # Ed25519 Signing
680
+ puts "\n--- Ed25519 Digital Signatures ---\n\n"
681
+
682
+ iodine_sk, iodine_pk = Iodine::Base::Crypto::Ed25519.keypair
683
+ message_short = 'Short message for signing'
684
+ message_long = Iodine::Utils.random(4096)
685
+
686
+ # Prepare OpenSSL Ed25519 keys if available
687
+ openssl_ed25519_available = false
688
+ if defined?(OpenSSL)
689
+ begin
690
+ openssl_ed_key = OpenSSL::PKey.generate_key('ED25519')
691
+ openssl_ed25519_available = true
692
+ rescue StandardError => e
693
+ puts "OpenSSL Ed25519 not available: #{e.message}"
694
+ end
695
+ end
696
+
697
+ # Prepare RbNaCl keys if available — probe via actual instantiation so
698
+ # that a partially-loaded gem (defined?(RbNaCl) true but sub-constants
699
+ # missing) is caught cleanly rather than raising NameError later.
700
+ rbnacl_available = false
701
+ rbnacl_signing_key = nil
702
+ rbnacl_verify_key = nil
703
+ begin
704
+ rbnacl_signing_key = RbNaCl::SigningKey.new(iodine_sk)
705
+ rbnacl_verify_key = rbnacl_signing_key.verify_key
706
+ rbnacl_available = true
707
+ rescue NameError, LoadError => e
708
+ puts "RbNaCl not available (#{e.message}); skipping RbNaCl comparisons."
709
+ end
710
+
711
+ # Label width — wide enough for 'Iodine Ed25519 sign (4KB) '
712
+ lw = 34
713
+
714
+ puts 'Ed25519 Key Generation:'
715
+ ::Benchmark.ips do |x|
716
+ x.report('Iodine Ed25519.keypair') do
717
+ Iodine::Base::Crypto::Ed25519.keypair
718
+ end
719
+
720
+ if openssl_ed25519_available
721
+ x.report('OpenSSL Ed25519 generate') do
722
+ OpenSSL::PKey.generate_key('ED25519')
723
+ end
724
+ end
725
+
726
+ if rbnacl_available
727
+ x.report('RbNaCl SigningKey.generate') do
728
+ RbNaCl::SigningKey.generate
729
+ end
730
+ end
731
+
732
+ x.compare!
733
+ end
734
+
735
+ # ------------------------------------------------------------------
736
+ # Unified signing block — both message sizes compete side-by-side.
737
+ # This lets readers see how message length affects signing throughput.
738
+ # x.compare! prints a ranked table across all entries.
739
+ # ------------------------------------------------------------------
740
+ puts "\nEd25519 Signing — all implementations and message sizes ranked:"
741
+ ::Benchmark.ips do |x|
742
+ x.report("Iodine Ed25519 sign (#{message_short.bytesize}B)".ljust(lw)) do
743
+ Iodine::Base::Crypto::Ed25519.sign(message_short, secret_key: iodine_sk, public_key: iodine_pk)
744
+ end
745
+ x.report("Iodine Ed25519 sign (#{message_long.bytesize / 1024}KB)".ljust(lw)) do
746
+ Iodine::Base::Crypto::Ed25519.sign(message_long, secret_key: iodine_sk, public_key: iodine_pk)
747
+ end
748
+ if openssl_ed25519_available
749
+ x.report("OpenSSL Ed25519 sign (#{message_short.bytesize}B)".ljust(lw)) do
750
+ openssl_ed_key.sign(nil, message_short)
751
+ end
752
+ x.report("OpenSSL Ed25519 sign (#{message_long.bytesize / 1024}KB)".ljust(lw)) do
753
+ openssl_ed_key.sign(nil, message_long)
754
+ end
755
+ end
756
+ if rbnacl_available
757
+ x.report("RbNaCl sign (#{message_short.bytesize}B)".ljust(lw)) do
758
+ rbnacl_signing_key.sign(message_short)
759
+ end
760
+ x.report("RbNaCl sign (#{message_long.bytesize / 1024}KB)".ljust(lw)) do
761
+ rbnacl_signing_key.sign(message_long)
762
+ end
763
+ end
764
+ x.compare!
765
+ end
766
+
767
+ # Verification benchmark
768
+ iodine_signature = Iodine::Base::Crypto::Ed25519.sign(message_short, secret_key: iodine_sk, public_key: iodine_pk)
769
+
770
+ puts "\nEd25519 Verification (short message):"
771
+ ::Benchmark.ips do |x|
772
+ x.report('Iodine Ed25519.verify') do
773
+ Iodine::Base::Crypto::Ed25519.verify(message_short, iodine_signature, public_key: iodine_pk)
774
+ end
775
+
776
+ if openssl_ed25519_available
777
+ openssl_sig = openssl_ed_key.sign(nil, message_short)
778
+ x.report('OpenSSL Ed25519 verify') do
779
+ openssl_ed_key.verify(nil, openssl_sig, message_short)
780
+ end
781
+ end
782
+
783
+ if rbnacl_available
784
+ rbnacl_sig = rbnacl_signing_key.sign(message_short)
785
+ x.report('RbNaCl verify') do
786
+ rbnacl_verify_key.verify(rbnacl_sig, message_short)
787
+ end
788
+ end
789
+
790
+ x.compare!
791
+ end
792
+
793
+ # X25519 Key Exchange
794
+ puts "\n--- X25519 Key Exchange (ECDH) ---\n\n"
795
+
796
+ alice_sk, = Iodine::Base::Crypto::X25519.keypair
797
+ bob_sk, bob_pk = Iodine::Base::Crypto::X25519.keypair
798
+
799
+ # Prepare OpenSSL X25519 keys if available
800
+ openssl_x25519_available = false
801
+ if defined?(OpenSSL)
802
+ begin
803
+ openssl_alice = OpenSSL::PKey.generate_key('X25519')
804
+ openssl_bob = OpenSSL::PKey.generate_key('X25519')
805
+ openssl_x25519_available = true
806
+ rescue StandardError => e
807
+ puts "OpenSSL X25519 not available: #{e.message}"
808
+ end
809
+ end
810
+
811
+ # Prepare RbNaCl keys if available
812
+ if rbnacl_available
813
+ rbnacl_alice = RbNaCl::PrivateKey.new(alice_sk)
814
+ rbnacl_bob = RbNaCl::PrivateKey.new(bob_sk)
815
+ end
816
+
817
+ puts 'X25519 Key Generation:'
818
+ ::Benchmark.ips do |x|
819
+ x.report('Iodine X25519.keypair') do
820
+ Iodine::Base::Crypto::X25519.keypair
821
+ end
822
+
823
+ if openssl_x25519_available
824
+ x.report('OpenSSL X25519 generate') do
825
+ OpenSSL::PKey.generate_key('X25519')
826
+ end
827
+ end
828
+
829
+ if rbnacl_available
830
+ x.report('RbNaCl PrivateKey.generate') do
831
+ RbNaCl::PrivateKey.generate
832
+ end
833
+ end
834
+
835
+ x.compare!
836
+ end
837
+
838
+ puts "\nX25519 Shared Secret Computation:"
839
+ ::Benchmark.ips do |x|
840
+ x.report('Iodine X25519.shared_secret') do
841
+ Iodine::Base::Crypto::X25519.shared_secret(secret_key: alice_sk, their_public: bob_pk)
842
+ end
843
+
844
+ if openssl_x25519_available
845
+ x.report('OpenSSL X25519 derive') do
846
+ openssl_alice.derive(openssl_bob)
847
+ end
848
+ end
849
+
850
+ if rbnacl_available
851
+ x.report('RbNaCl GroupElement') do
852
+ RbNaCl::GroupElement.new(rbnacl_bob.public_key.to_bytes).mult(rbnacl_alice.to_bytes)
853
+ end
854
+ end
855
+
856
+ x.compare!
857
+ end
858
+
859
+ nil
860
+ end
861
+
862
+ # Benchmarks Iodine's hashing functions vs OpenSSL/Digest equivalents.
863
+ #
864
+ # Tests SHA-256, SHA-512, SHA3-224, SHA3-256, SHA3-512, BLAKE2b, BLAKE2s,
865
+ # SHAKE128, SHAKE256, risky256, and risky512 across three payload sizes
866
+ # (64B, 1KB, 64KB). risky256/risky512 are non-cryptographic but are
867
+ # included as faster alternatives suitable for message authentication where
868
+ # cryptographic strength is not required.
869
+ #
870
+ # For each payload size, ALL algorithms compete in a single Benchmark.ips
871
+ # block so results can be used directly for algorithm selection — you can
872
+ # immediately see "at 1KB, which hash is fastest?"
873
+ #
874
+ # Requires OpenSSL for comparison benchmarks.
875
+ #
876
+ # FAIRNESS NOTES:
877
+ # - OpenSSL::Digest objects are pre-instantiated ONCE before the sizes loop
878
+ # and reused across all iterations. Calling instance.digest(data) resets
879
+ # the existing EVP_MD_CTX state (via EVP_DigestInit_ex) without allocating
880
+ # a new context — only the hash computation itself is timed.
881
+ # - By contrast, the class methods OpenSSL::Digest.digest('SHA3-256', data)
882
+ # and OpenSSL::Digest::SHA256.digest(data) allocate a new EVP_MD_CTX AND
883
+ # run EVP_get_digestbyname() on every call — unfairly penalising OpenSSL.
884
+ # - Digest::SHA256/SHA512 (Ruby stdlib) delegates to OpenSSL internally.
885
+ # The same pre-instantiation fix is applied: Digest::SHA256.new is created
886
+ # once and instance.digest(data) is called in the hot loop.
887
+ # - SHA3 and BLAKE2 availability depends on the OpenSSL build. If not
888
+ # available, only Iodine results are shown for those algorithms.
889
+ # - SHAKE128/SHAKE256 have no OpenSSL Ruby binding; they are included in
890
+ # the unified block so their throughput is visible in the ranking.
891
+ #
892
+ # NOTE on digest == hash: Iodine's sha256/sha3_256/blake2b etc. and
893
+ # OpenSSL::Digest both compute cryptographic message digests — they are the
894
+ # same operation (deterministic, fixed-output-length, one-way hash functions
895
+ # over arbitrary input). The comparison is apples-to-apples.
896
+ def self.hashing
897
+ require 'benchmark/ips'
898
+ begin
899
+ require 'openssl'
900
+ rescue LoadError
901
+ nil
902
+ end
903
+ begin
904
+ require 'digest'
905
+ rescue LoadError
906
+ nil
907
+ end
908
+
909
+ puts '=' * 70
910
+ puts 'Hashing Benchmarks'
911
+ puts '=' * 70
912
+ puts
913
+ puts 'NOTE: All algorithms are benchmarked together per payload size so'
914
+ puts 'results can be used for algorithm selection. OpenSSL::Digest and'
915
+ puts 'Digest:: objects are pre-instantiated once before the sizes loop;'
916
+ puts 'instance.digest(data) reuses the EVP_MD_CTX (only resets state) —'
917
+ puts 'no per-call allocation or string lookup overhead.'
918
+ puts
919
+
920
+ # Test data sizes
921
+ sizes = {
922
+ '64B' => 64,
923
+ '1KB' => 1024,
924
+ '64KB' => 65_536
925
+ }
926
+
927
+ # ------------------------------------------------------------------
928
+ # Pre-check which OpenSSL algorithms are available and pre-instantiate
929
+ # digest objects ONCE here — before the sizes loop — so they are
930
+ # allocated exactly once regardless of how many size iterations run.
931
+ # ------------------------------------------------------------------
932
+ openssl_sha3_256_available = false
933
+ openssl_sha3_512_available = false
934
+ openssl_blake2b_available = false
935
+ openssl_blake2s_available = false
936
+ openssl_sha3_224_available = false
937
+
938
+ # Pre-instantiated digest objects (nil if algorithm unavailable)
939
+ ossl_sha256 = nil
940
+ ossl_sha512 = nil
941
+ ossl_sha3_256 = nil
942
+ ossl_sha3_512 = nil
943
+ ossl_blake2b = nil
944
+ ossl_blake2s = nil
945
+ ossl_sha3_224 = nil
946
+ digest_sha256 = nil
947
+ digest_sha512 = nil
948
+
949
+ if defined?(OpenSSL)
950
+ # SHA-256 and SHA-512 are always available in any OpenSSL build
951
+ ossl_sha256 = OpenSSL::Digest.new('SHA256')
952
+ ossl_sha512 = OpenSSL::Digest.new('SHA512')
953
+
954
+ begin
955
+ ossl_sha3_256 = OpenSSL::Digest.new('SHA3-256')
956
+ ossl_sha3_256.digest('') # probe — raises if unavailable
957
+ openssl_sha3_256_available = true
958
+ rescue OpenSSL::Digest::DigestError, RuntimeError
959
+ ossl_sha3_256 = nil
960
+ # SHA3-256 not available in this OpenSSL build
961
+ end
962
+ begin
963
+ ossl_sha3_512 = OpenSSL::Digest.new('SHA3-512')
964
+ ossl_sha3_512.digest('')
965
+ openssl_sha3_512_available = true
966
+ rescue OpenSSL::Digest::DigestError, RuntimeError
967
+ ossl_sha3_512 = nil
968
+ # SHA3-512 not available
969
+ end
970
+ begin
971
+ ossl_blake2b = OpenSSL::Digest.new('BLAKE2b512')
972
+ ossl_blake2b.digest('')
973
+ openssl_blake2b_available = true
974
+ rescue OpenSSL::Digest::DigestError, RuntimeError
975
+ ossl_blake2b = nil
976
+ # BLAKE2b not available
977
+ end
978
+ begin
979
+ ossl_blake2s = OpenSSL::Digest.new('BLAKE2s256')
980
+ ossl_blake2s.digest('')
981
+ openssl_blake2s_available = true
982
+ rescue OpenSSL::Digest::DigestError, RuntimeError
983
+ ossl_blake2s = nil
984
+ # BLAKE2s not available
985
+ end
986
+ begin
987
+ ossl_sha3_224 = OpenSSL::Digest.new('SHA3-224')
988
+ ossl_sha3_224.digest('')
989
+ openssl_sha3_224_available = true
990
+ rescue OpenSSL::Digest::DigestError, RuntimeError
991
+ ossl_sha3_224 = nil
992
+ # SHA3-224 not available in this OpenSSL build
993
+ end
994
+ end
995
+
996
+ if defined?(Digest)
997
+ # Ruby stdlib Digest — pre-instantiate once; instance.digest(data)
998
+ # calls reset+update+finish on the existing object (no allocation).
999
+ digest_sha256 = Digest::SHA256.new
1000
+ digest_sha512 = Digest::SHA512.new
1001
+ end
1002
+
1003
+ # Label width — wide enough for the longest entry ("Iodine risky512 (non-crypto, 64B)")
1004
+ lw = 34
1005
+
1006
+ sizes.each do |size_name, size|
1007
+ data = Iodine::Utils.random(size)
1008
+
1009
+ puts "\n--- Data Size: #{size_name} — all algorithms ranked together ---\n\n"
1010
+ puts " (SHAKE128/SHAKE256 have no OpenSSL Ruby binding; risky256/risky512 are non-crypto"
1011
+ puts " alternatives suitable for fast message authentication; all included for ranking context)"
1012
+ puts
1013
+
1014
+ # ------------------------------------------------------------------
1015
+ # Unified block: all algorithms compete side-by-side for this size.
1016
+ # x.compare! prints a ranked table so the fastest algorithm is clear.
1017
+ # ------------------------------------------------------------------
1018
+ ::Benchmark.ips do |x|
1019
+ # ---- Iodine entries (always present) ----
1020
+ x.report('Iodine sha256'.ljust(lw)) { Iodine::Utils.sha256(data) }
1021
+ x.report('Iodine sha512'.ljust(lw)) { Iodine::Utils.sha512(data) }
1022
+ x.report('Iodine sha3_224'.ljust(lw)) { Iodine::Utils.sha3_224(data) }
1023
+ x.report('Iodine sha3_256'.ljust(lw)) { Iodine::Utils.sha3_256(data) }
1024
+ x.report('Iodine sha3_512'.ljust(lw)) { Iodine::Utils.sha3_512(data) }
1025
+ x.report('Iodine blake2b (64B)'.ljust(lw)) { Iodine::Utils.blake2b(data, len: 64) }
1026
+ x.report('Iodine blake2s (32B)'.ljust(lw)) { Iodine::Utils.blake2s(data, len: 32) }
1027
+ x.report('Iodine shake128 (XOF, 32B)'.ljust(lw)) { Iodine::Utils.shake128(data, length: 32) }
1028
+ x.report('Iodine shake256 (XOF, 64B)'.ljust(lw)) { Iodine::Utils.shake256(data, length: 64) }
1029
+ ## risky256/risky512: non-crypto alternatives for fast message authentication
1030
+ # x.report('Iodine risky256 (non-crypto, 32B)'.ljust(lw)) { Iodine::Utils.risky256(data) }
1031
+ # x.report('Iodine risky512 (non-crypto, 64B)'.ljust(lw)) { Iodine::Utils.risky512(data) }
1032
+
1033
+ # ---- OpenSSL entries (conditionally present) ----
1034
+ if ossl_sha256
1035
+ x.report('OpenSSL SHA256'.ljust(lw)) { ossl_sha256.digest(data) }
1036
+ end
1037
+ if ossl_sha512
1038
+ x.report('OpenSSL SHA512'.ljust(lw)) { ossl_sha512.digest(data) }
1039
+ end
1040
+ if openssl_sha3_224_available
1041
+ x.report('OpenSSL SHA3-224'.ljust(lw)) { ossl_sha3_224.digest(data) }
1042
+ end
1043
+ if openssl_sha3_256_available
1044
+ x.report('OpenSSL SHA3-256'.ljust(lw)) { ossl_sha3_256.digest(data) }
1045
+ end
1046
+ if openssl_sha3_512_available
1047
+ x.report('OpenSSL SHA3-512'.ljust(lw)) { ossl_sha3_512.digest(data) }
1048
+ end
1049
+ if openssl_blake2b_available
1050
+ x.report('OpenSSL BLAKE2b512'.ljust(lw)) { ossl_blake2b.digest(data) }
1051
+ end
1052
+ if openssl_blake2s_available
1053
+ x.report('OpenSSL BLAKE2s256'.ljust(lw)) { ossl_blake2s.digest(data) }
1054
+ end
1055
+
1056
+ # ---- Ruby stdlib Digest entries (conditionally present) ----
1057
+ if digest_sha256
1058
+ x.report('Digest::SHA256'.ljust(lw)) { digest_sha256.digest(data) }
1059
+ end
1060
+ if digest_sha512
1061
+ x.report('Digest::SHA512'.ljust(lw)) { digest_sha512.digest(data) }
1062
+ end
1063
+
1064
+ puts "\n--- Data Size: #{size_name} ---\n\n"
1065
+ x.compare!
1066
+ end
1067
+ end
1068
+
1069
+ nil
1070
+ end
1071
+
1072
+ # Benchmarks Iodine's X25519 ECIES public-key encryption vs RbNaCl sealed boxes.
1073
+ #
1074
+ # Tests encryption/decryption with various payload sizes and all cipher variants.
1075
+ #
1076
+ # Optional dependencies: RbNaCl gem for comparison.
1077
+ #
1078
+ # FAIRNESS NOTES:
1079
+ # - All ciphertexts for decryption are pre-computed OUTSIDE the hot loop so
1080
+ # that only the actual decrypt operation is timed.
1081
+ # - All three Iodine cipher variants (ChaCha20, AES-128, AES-256) compete in
1082
+ # the unified encrypt block so readers can choose the right cipher.
1083
+ # - RbNaCl SimpleBox uses XSalsa20-Poly1305 (different cipher) — included for
1084
+ # throughput reference; label makes the difference clear.
1085
+ def self.crypto_ecies
1086
+ require 'benchmark/ips'
1087
+ begin
1088
+ require 'rbnacl'
1089
+ rescue LoadError
1090
+ nil
1091
+ end
1092
+
1093
+ puts '=' * 70
1094
+ puts 'X25519 ECIES (Public-Key Encryption) Benchmarks'
1095
+ puts '=' * 70
1096
+
1097
+ # Generate recipient keypair
1098
+ recipient_sk, recipient_pk = Iodine::Base::Crypto::X25519.keypair
1099
+
1100
+ # Test data sizes
1101
+ sizes = {
1102
+ '64B' => 64,
1103
+ '1KB' => 1024,
1104
+ '16KB' => 16_384
1105
+ }
1106
+
1107
+ # Label width — wide enough for 'Iodine X25519.decrypt_aes256 (AES-256)'
1108
+ lw = 32
1109
+
1110
+ rbnacl_available = false
1111
+ rbnacl_pk = nil
1112
+ rbnacl_sk = nil
1113
+ begin
1114
+ rbnacl_sk = RbNaCl::PrivateKey.new(recipient_sk)
1115
+ rbnacl_pk = rbnacl_sk.public_key
1116
+ rbnacl_available = true
1117
+ rescue NameError, LoadError => e
1118
+ puts "RbNaCl not available (#{e.message}); skipping RbNaCl comparisons."
1119
+ end
1120
+
1121
+ sizes.each do |size_name, size|
1122
+ plaintext = Iodine::Utils.random(size)
1123
+
1124
+ puts "\n--- Payload Size: #{size_name} — all cipher variants ranked ---\n\n"
1125
+
1126
+ # ------------------------------------------------------------------
1127
+ # Pre-compute all ciphertexts outside the hot loop so that only the
1128
+ # decrypt operation itself is timed in the decrypt block below.
1129
+ # ------------------------------------------------------------------
1130
+ iodine_ct_chacha = Iodine::Base::Crypto::X25519.encrypt(plaintext, recipient_pk: recipient_pk)
1131
+ iodine_ct_aes128 = Iodine::Base::Crypto::X25519.encrypt_aes128(plaintext, recipient_pk: recipient_pk)
1132
+ iodine_ct_aes256 = Iodine::Base::Crypto::X25519.encrypt_aes256(plaintext, recipient_pk: recipient_pk)
1133
+ rbnacl_ciphertext = rbnacl_available ? RbNaCl::SimpleBox.from_public_key(rbnacl_pk).encrypt(plaintext) : nil
1134
+
1135
+ # ------------------------------------------------------------------
1136
+ # Unified encrypt block — all cipher variants compete side-by-side.
1137
+ # x.compare! prints a ranked table so the fastest cipher is clear.
1138
+ # ------------------------------------------------------------------
1139
+ puts "Encrypt — all cipher variants ranked:"
1140
+ ::Benchmark.ips do |x|
1141
+ x.report('Iodine X25519.encrypt (ChaCha20)'.ljust(lw)) do
1142
+ Iodine::Base::Crypto::X25519.encrypt(plaintext, recipient_pk: recipient_pk)
1143
+ end
1144
+ x.report('Iodine X25519.encrypt_aes128'.ljust(lw)) do
1145
+ Iodine::Base::Crypto::X25519.encrypt_aes128(plaintext, recipient_pk: recipient_pk)
1146
+ end
1147
+ x.report('Iodine X25519.encrypt_aes256'.ljust(lw)) do
1148
+ Iodine::Base::Crypto::X25519.encrypt_aes256(plaintext, recipient_pk: recipient_pk)
1149
+ end
1150
+ if rbnacl_available
1151
+ # RbNaCl SimpleBox uses XSalsa20-Poly1305 — different cipher, shown for reference
1152
+ x.report('RbNaCl SimpleBox seal'.ljust(lw)) do
1153
+ RbNaCl::SimpleBox.from_public_key(rbnacl_pk).encrypt(plaintext)
1154
+ end
1155
+ end
1156
+ x.compare!
1157
+ end
1158
+
1159
+ # ------------------------------------------------------------------
1160
+ # Unified decrypt block — all cipher variants compete side-by-side.
1161
+ # Ciphertexts were pre-computed above (outside the hot loop).
1162
+ # x.compare! prints a ranked table so the fastest cipher is clear.
1163
+ # ------------------------------------------------------------------
1164
+ puts "\nDecrypt — all cipher variants ranked:"
1165
+ ::Benchmark.ips do |x|
1166
+ x.report('Iodine X25519.decrypt (ChaCha20)'.ljust(lw)) do
1167
+ Iodine::Base::Crypto::X25519.decrypt(iodine_ct_chacha, secret_key: recipient_sk)
1168
+ end
1169
+ x.report('Iodine X25519.decrypt_aes128'.ljust(lw)) do
1170
+ Iodine::Base::Crypto::X25519.decrypt_aes128(iodine_ct_aes128, secret_key: recipient_sk)
1171
+ end
1172
+ x.report('Iodine X25519.decrypt_aes256'.ljust(lw)) do
1173
+ Iodine::Base::Crypto::X25519.decrypt_aes256(iodine_ct_aes256, secret_key: recipient_sk)
1174
+ end
1175
+ if rbnacl_available
1176
+ # RbNaCl SimpleBox uses XSalsa20-Poly1305 — different cipher, shown for reference
1177
+ x.report('RbNaCl SimpleBox open'.ljust(lw)) do
1178
+ RbNaCl::SimpleBox.from_keypair(rbnacl_pk, rbnacl_sk).decrypt(rbnacl_ciphertext)
1179
+ end
1180
+ end
1181
+ x.compare!
1182
+ end
1183
+ end
1184
+
1185
+ nil
1186
+ end
1187
+
1188
+ # Benchmarks Iodine's HKDF key derivation vs OpenSSL HKDF.
1189
+ #
1190
+ # Tests key derivation with various output lengths.
1191
+ #
1192
+ # Requires OpenSSL >= 1.1.0 for comparison benchmarks.
1193
+ def self.crypto_kdf
1194
+ require 'benchmark/ips'
1195
+ begin
1196
+ require 'openssl'
1197
+ rescue LoadError
1198
+ nil
1199
+ end
1200
+
1201
+ puts '=' * 70
1202
+ puts 'HKDF Key Derivation Benchmarks'
1203
+ puts '=' * 70
1204
+
1205
+ # Input keying material (e.g., from X25519 shared secret)
1206
+ ikm = Iodine::Utils.random(32)
1207
+ salt = Iodine::Utils.random(32)
1208
+ info = 'application specific context'
1209
+
1210
+ # Output lengths to test
1211
+ lengths = [16, 32, 64, 128]
1212
+
1213
+ # Check if OpenSSL HKDF is available
1214
+ openssl_hkdf_available = false
1215
+ if defined?(OpenSSL) && defined?(OpenSSL::KDF)
1216
+ begin
1217
+ OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: 32, hash: 'SHA256')
1218
+ openssl_hkdf_available = true
1219
+ rescue StandardError => e
1220
+ puts "OpenSSL HKDF not available: #{e.message}"
1221
+ end
1222
+ end
1223
+
1224
+ lengths.each do |length|
1225
+ puts "\n--- Output Length: #{length} bytes ---\n\n"
1226
+
1227
+ ::Benchmark.ips do |x|
1228
+ x.report('Iodine HKDF.derive (SHA-256)') do
1229
+ Iodine::Base::Crypto::HKDF.derive(ikm: ikm, salt: salt, info: info, length: length, sha384: false)
1230
+ end
1231
+
1232
+ x.report('Iodine HKDF.derive (SHA-384)') do
1233
+ Iodine::Base::Crypto::HKDF.derive(ikm: ikm, salt: salt, info: info, length: length, sha384: true)
1234
+ end
1235
+
1236
+ if openssl_hkdf_available
1237
+ x.report('OpenSSL HKDF (SHA-256)') do
1238
+ OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: 'SHA256')
1239
+ end
1240
+
1241
+ x.report('OpenSSL HKDF (SHA-384)') do
1242
+ OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: 'SHA384')
1243
+ end
1244
+ end
1245
+
1246
+ x.compare!
1247
+ end
1248
+ end
1249
+
1250
+ nil
1251
+ end
1252
+
1253
+ # Benchmarks Iodine's TOTP implementation vs ROTP gem.
1254
+ #
1255
+ # Tests TOTP generation and verification.
1256
+ #
1257
+ # Optional dependencies: rotp gem for comparison.
1258
+ #
1259
+ # FAIRNESS NOTES:
1260
+ # - Both implementations use the SAME underlying secret bytes. Iodine
1261
+ # accepts raw bytes; ROTP accepts Base32-encoded strings. The Base32
1262
+ # encoding of the raw secret is decoded by ROTP back to the same bytes,
1263
+ # so both implementations compute TOTP for the same secret.
1264
+ # - Verification windows are matched: Iodine window:1 checks ±1 interval;
1265
+ # ROTP drift_behind/drift_ahead:30 also checks ±1 interval (30 seconds).
1266
+ def self.crypto_totp
1267
+ require 'benchmark/ips'
1268
+ begin
1269
+ require 'rotp'
1270
+ rescue LoadError
1271
+ nil
1272
+ end
1273
+
1274
+ puts '=' * 70
1275
+ puts 'TOTP (Time-based One-Time Password) Benchmarks'
1276
+ puts '=' * 70
1277
+
1278
+ # Generate a raw 20-byte secret (the canonical TOTP secret).
1279
+ # Iodine::Utils.totp expects raw bytes.
1280
+ # ROTP expects a Base32-encoded string — we use the Base32 encoding of
1281
+ # the same raw bytes so both implementations operate on the same secret.
1282
+ raw_secret = Iodine::Utils.random(20)
1283
+
1284
+ # totp_secret returns a Base32-encoded string from random bytes.
1285
+ # We need the Base32 encoding of our specific raw_secret for ROTP.
1286
+ # Use Iodine's own Base32 encoder via totp_secret if possible, otherwise
1287
+ # fall back to ROTP's Base32 encoder.
1288
+ rotp_available = defined?(ROTP)
1289
+
1290
+ if rotp_available
1291
+ # Encode raw_secret to Base32 for ROTP
1292
+ base32_secret = ROTP::Base32.encode(raw_secret)
1293
+ rotp_totp = ROTP::TOTP.new(base32_secret)
1294
+ end
1295
+
1296
+ puts "\nTOTP Secret Generation:"
1297
+ ::Benchmark.ips do |x|
1298
+ x.report('Iodine totp_secret') do
1299
+ Iodine::Utils.totp_secret(len: 20)
1300
+ end
1301
+
1302
+ if rotp_available
1303
+ x.report('ROTP random_base32') do
1304
+ ROTP::Base32.random_base32(32)
1305
+ end
1306
+ end
1307
+
1308
+ x.compare!
1309
+ end
1310
+
1311
+ puts "\nTOTP Code Generation (same secret, same time window):"
1312
+ ::Benchmark.ips do |x|
1313
+ x.report('Iodine totp') do
1314
+ Iodine::Utils.totp(secret: raw_secret)
1315
+ end
1316
+
1317
+ if rotp_available
1318
+ x.report('ROTP now') do
1319
+ rotp_totp.now
1320
+ end
1321
+ end
1322
+
1323
+ x.compare!
1324
+ end
1325
+
1326
+ # Verification: use the same window size for both.
1327
+ # Iodine window:1 = check current ± 1 interval (3 codes total).
1328
+ # ROTP drift_behind:30, drift_ahead:30 = check current ± 1 interval (3 codes total).
1329
+ current_code = Iodine::Utils.totp(secret: raw_secret).to_i
1330
+
1331
+ puts "\nTOTP Verification (window: ±1 interval = 3 codes checked):"
1332
+ ::Benchmark.ips do |x|
1333
+ x.report('Iodine totp_verify') do
1334
+ Iodine::Utils.totp_verify(code: current_code, secret: raw_secret, window: 1)
1335
+ end
1336
+
1337
+ if rotp_available
1338
+ rotp_code = rotp_totp.now.to_s
1339
+ x.report('ROTP verify') do
1340
+ rotp_totp.verify(rotp_code, drift_behind: 30, drift_ahead: 30)
1341
+ end
1342
+ end
1343
+
1344
+ x.compare!
1345
+ end
1346
+
1347
+ nil
1348
+ end
1349
+
1350
+ # Benchmarks Iodine's compression vs Ruby's Zlib stdlib.
1351
+ #
1352
+ # Tests Deflate, Gzip, and Brotli compression/decompression with
1353
+ # various payload sizes and compressible content.
1354
+ #
1355
+ # FAIRNESS NOTES:
1356
+ # - Both Iodine and Zlib use the same compression level (6 = default).
1357
+ # - Payloads are compressible text (repeated English prose), not random
1358
+ # bytes. Random bytes are incompressible and would make compression
1359
+ # benchmarks meaningless.
1360
+ # - Brotli has no Ruby stdlib equivalent; it is shown solo with a note.
1361
+ # - Zlib::Deflate.deflate and Zlib::GzipWriter are the idiomatic one-shot
1362
+ # Ruby APIs and are used here.
1363
+ def self.compression
1364
+ require 'benchmark/ips'
1365
+ require 'zlib'
1366
+
1367
+ puts '=' * 70
1368
+ puts 'Compression Benchmarks'
1369
+ puts '=' * 70
1370
+ puts
1371
+ puts 'NOTE: Payloads are compressible English text (not random bytes).'
1372
+ puts 'Both Iodine and Zlib use compression level 6 (default).'
1373
+ puts
1374
+
1375
+ # Compressible payload: repeated English prose
1376
+ prose = "The quick brown fox jumps over the lazy dog. " \
1377
+ "Pack my box with five dozen liquor jugs. " \
1378
+ "How vexingly quick daft zebras jump! " \
1379
+ "The five boxing wizards jump quickly. "
1380
+
1381
+ sizes = {
1382
+ '1KB' => 1024,
1383
+ '16KB' => 16_384,
1384
+ '64KB' => 65_536
1385
+ }
1386
+
1387
+ # Label width — wide enough for 'Iodine Brotli.compress (no Zlib equiv)'
1388
+ lw = 30
1389
+
1390
+ sizes.each do |size_name, size|
1391
+ # Build a compressible payload of the target size
1392
+ plaintext = (prose * ((size / prose.bytesize) + 1))[0, size]
1393
+ plaintext.force_encoding(Encoding::BINARY)
1394
+
1395
+ puts "\n--- Payload Size: #{size_name} (compressible text) ---\n\n"
1396
+
1397
+ # ------------------------------------------------------------------
1398
+ # Pre-compute all compressed inputs outside the hot loop so that
1399
+ # decompression benchmarks only measure decompression, not compression.
1400
+ # ------------------------------------------------------------------
1401
+ iodine_deflated = Iodine::Base::Compression::Deflate.compress(plaintext, level: 6)
1402
+ zlib_deflated = Zlib::Deflate.deflate(plaintext, Zlib::DEFAULT_COMPRESSION)
1403
+
1404
+ iodine_gzipped = Iodine::Base::Compression::Gzip.compress(plaintext, level: 6)
1405
+ zlib_gzipped = begin
1406
+ sio = StringIO.new(String.new, 'wb')
1407
+ gz = Zlib::GzipWriter.new(sio, Zlib::DEFAULT_COMPRESSION)
1408
+ gz.write(plaintext)
1409
+ gz.close
1410
+ sio.string
1411
+ end
1412
+
1413
+ iodine_brotli = Iodine::Base::Compression::Brotli.compress(plaintext, quality: 4)
1414
+
1415
+ # ------------------------------------------------------------------
1416
+ # Unified compress block — all algorithms compete side-by-side.
1417
+ # Brotli has no Zlib equivalent but is included for ranking context.
1418
+ # x.compare! prints a ranked table so the fastest algorithm is clear.
1419
+ # ------------------------------------------------------------------
1420
+ puts "Compress — all algorithms ranked (level 6 / quality 4):"
1421
+ puts " (Brotli has no Zlib equivalent — included for ranking context)"
1422
+ ::Benchmark.ips do |x|
1423
+ x.report('Iodine Deflate.compress'.ljust(lw)) do
1424
+ Iodine::Base::Compression::Deflate.compress(plaintext, level: 6)
1425
+ end
1426
+ x.report('Zlib::Deflate.deflate'.ljust(lw)) do
1427
+ Zlib::Deflate.deflate(plaintext, Zlib::DEFAULT_COMPRESSION)
1428
+ end
1429
+ x.report('Iodine Gzip.compress'.ljust(lw)) do
1430
+ Iodine::Base::Compression::Gzip.compress(plaintext, level: 6)
1431
+ end
1432
+ x.report('Zlib gzip (StringIO)'.ljust(lw)) do
1433
+ sio = StringIO.new(String.new, 'wb')
1434
+ gz = Zlib::GzipWriter.new(sio, Zlib::DEFAULT_COMPRESSION)
1435
+ gz.write(plaintext)
1436
+ gz.close
1437
+ sio.string
1438
+ end
1439
+ # Brotli has no Zlib equivalent — included for throughput ranking context
1440
+ x.report('Iodine Brotli.compress (no Zlib equiv)'.ljust(lw)) do
1441
+ Iodine::Base::Compression::Brotli.compress(plaintext, quality: 4)
1442
+ end
1443
+ x.compare!
1444
+ end
1445
+
1446
+ # ------------------------------------------------------------------
1447
+ # Unified decompress block — all algorithms compete side-by-side.
1448
+ # Each implementation decompresses its own format (fair comparison).
1449
+ # Brotli has no Zlib equivalent but is included for ranking context.
1450
+ # ------------------------------------------------------------------
1451
+ puts "\nDecompress — all algorithms ranked:"
1452
+ puts " (Brotli has no Zlib equivalent — included for ranking context)"
1453
+ ::Benchmark.ips do |x|
1454
+ x.report('Iodine Deflate.decompress'.ljust(lw)) do
1455
+ Iodine::Base::Compression::Deflate.decompress(iodine_deflated)
1456
+ end
1457
+ x.report('Zlib::Inflate.inflate'.ljust(lw)) do
1458
+ Zlib::Inflate.inflate(zlib_deflated)
1459
+ end
1460
+ x.report('Iodine Gzip.decompress'.ljust(lw)) do
1461
+ Iodine::Base::Compression::Gzip.decompress(iodine_gzipped)
1462
+ end
1463
+ x.report('Zlib::GzipReader'.ljust(lw)) do
1464
+ Zlib::GzipReader.new(StringIO.new(zlib_gzipped)).read
1465
+ end
1466
+ # Brotli has no Zlib equivalent — included for throughput ranking context
1467
+ x.report('Iodine Brotli.decompress (no Zlib equiv)'.ljust(lw)) do
1468
+ Iodine::Base::Compression::Brotli.decompress(iodine_brotli)
1469
+ end
1470
+ x.compare!
1471
+ end
1472
+ end
1473
+
1474
+ nil
1475
+ end
1476
+
1477
+ # Benchmarks Iodine's non-cryptographic hash functions.
1478
+ #
1479
+ # Tests risky_hash (64-bit), risky256, risky512, and crc32 against
1480
+ # Ruby stdlib equivalents where available.
1481
+ #
1482
+ # FAIRNESS NOTES:
1483
+ # - risky_hash and String#hash serve the same purpose: seeding hash maps.
1484
+ # Ruby's Hash tables use String#hash (SipHash, seeded per-process).
1485
+ # Iodine's MiniMap uses risky_hash — a faster alternative to SipHash with
1486
+ # proven collision resistance and superior bit distribution.
1487
+ # - crc32 is compared against Zlib.crc32 (same polynomial: ITU-T V.42).
1488
+ # - risky256/risky512 have no stdlib equivalents; shown for reference only.
1489
+ def self.non_crypto_hashing
1490
+ require 'benchmark/ips'
1491
+ require 'zlib'
1492
+
1493
+ puts '=' * 70
1494
+ puts 'Non-Cryptographic Hash Benchmarks'
1495
+ puts '=' * 70
1496
+ puts
1497
+ puts 'NOTE: risky_hash and String#hash both seed hash maps — a direct'
1498
+ puts 'comparison. Ruby Hash uses String#hash (SipHash, per-process seed).'
1499
+ puts 'Iodine MiniMap uses risky_hash: faster than SipHash with proven'
1500
+ puts 'collision resistance and superior bit distribution.'
1501
+ puts
1502
+
1503
+ sizes = {
1504
+ '64B' => 64,
1505
+ '1KB' => 1024,
1506
+ '64KB' => 65_536
1507
+ }
1508
+
1509
+ # Label width — wide enough for 'Iodine risky512 (non-crypto, 64B)'
1510
+ lw = 32
1511
+
1512
+ sizes.each do |size_name, size|
1513
+ data = Iodine::Utils.random(size)
1514
+
1515
+ puts "\n--- Data Size: #{size_name} — all non-crypto hashes ranked together ---\n\n"
1516
+ puts " (risky256/risky512 have no stdlib equivalent; String#hash uses SipHash — same use case as risky_hash)"
1517
+ puts
1518
+
1519
+ # ------------------------------------------------------------------
1520
+ # Unified block: all non-crypto hash algorithms compete side-by-side.
1521
+ # x.compare! prints a ranked table so the fastest algorithm is clear.
1522
+ # ------------------------------------------------------------------
1523
+ ::Benchmark.ips do |x|
1524
+ x.report('Iodine risky_hash (64-bit)'.ljust(lw)) do
1525
+ Iodine::Utils.risky_hash(data)
1526
+ end
1527
+ x.report('Iodine crc32 (32-bit)'.ljust(lw)) do
1528
+ Iodine::Utils.crc32(data)
1529
+ end
1530
+ # risky256/risky512 have no stdlib equivalent — included for ranking context
1531
+ x.report('Iodine risky256 (non-crypto, 32B)'.ljust(lw)) do
1532
+ Iodine::Utils.risky256(data)
1533
+ end
1534
+ x.report('Iodine risky512 (non-crypto, 64B)'.ljust(lw)) do
1535
+ Iodine::Utils.risky512(data)
1536
+ end
1537
+ # Zlib.crc32 — direct equivalent for crc32 (same ITU-T V.42 polynomial)
1538
+ x.report('Zlib.crc32'.ljust(lw)) do
1539
+ Zlib.crc32(data)
1540
+ end
1541
+ # String#hash uses SipHash (seeded per-process) — same use case as
1542
+ # risky_hash: both are hash-map hash functions. Direct comparison.
1543
+ x.report('String#hash (SipHash, per-process)'.ljust(lw)) do
1544
+ data.hash
1545
+ end
1546
+ x.compare!
1547
+ end
1548
+ end
1549
+
1550
+ nil
1551
+ end
1552
+
1553
+ # Benchmarks Iodine's X25519+ML-KEM-768 post-quantum hybrid KEM.
1554
+ #
1555
+ # X25519MLKEM768 is a hybrid Key Encapsulation Mechanism that combines:
1556
+ # - Classical X25519 elliptic curve Diffie-Hellman
1557
+ # - ML-KEM-768 (formerly Kyber), a post-quantum lattice-based KEM
1558
+ #
1559
+ # The hybrid construction provides security against both classical and
1560
+ # quantum computer attacks. Even if one algorithm is broken, the other
1561
+ # still provides security.
1562
+ #
1563
+ # Key sizes:
1564
+ # - Secret key: 2432 bytes | Public key: 1216 bytes
1565
+ # - Ciphertext: 1120 bytes | Shared secret: 64 bytes
1566
+ #
1567
+ # There is no Ruby stdlib or OpenSSL equivalent for ML-KEM-768.
1568
+ # X25519.keypair is included for relative cost comparison only.
1569
+ #
1570
+ # FAIRNESS NOTES:
1571
+ # - Keypairs are generated OUTSIDE the encapsulate/decapsulate hot loops.
1572
+ # - X25519.keypair is shown alongside X25519MLKEM768.keypair so readers
1573
+ # can see the overhead of adding ML-KEM-768 to a classical X25519 key.
1574
+ # - No compare! for solo benchmarks (no stdlib equivalent).
1575
+ def self.crypto_pqkem
1576
+ require 'benchmark/ips'
1577
+
1578
+ puts '=' * 70
1579
+ puts 'Post-Quantum Hybrid KEM Benchmarks (X25519 + ML-KEM-768)'
1580
+ puts '=' * 70
1581
+ puts
1582
+ puts 'NOTE: X25519MLKEM768 is a hybrid KEM combining classical X25519 with'
1583
+ puts 'ML-KEM-768 (Kyber). No Ruby stdlib or OpenSSL equivalent exists.'
1584
+ puts 'X25519.keypair is shown for relative cost comparison only.'
1585
+ puts
1586
+
1587
+ # ------------------------------------------------------------------
1588
+ # Key generation — compare X25519MLKEM768 vs plain X25519
1589
+ # ------------------------------------------------------------------
1590
+ puts 'Key Generation (X25519MLKEM768 vs X25519 for relative cost):'
1591
+ ::Benchmark.ips do |x|
1592
+ x.report('Iodine X25519MLKEM768.keypair') do
1593
+ Iodine::Base::Crypto::X25519MLKEM768.keypair
1594
+ end
1595
+
1596
+ # X25519 keypair shown for relative cost reference only.
1597
+ # X25519MLKEM768 is expected to be significantly slower due to ML-KEM-768.
1598
+ x.report('Iodine X25519.keypair (classical, for reference)') do
1599
+ Iodine::Base::Crypto::X25519.keypair
1600
+ end
1601
+
1602
+ x.compare!
1603
+ end
1604
+
1605
+ # Generate a keypair outside the hot loop for encapsulate/decapsulate
1606
+ pqkem_sk, pqkem_pk = Iodine::Base::Crypto::X25519MLKEM768.keypair
1607
+
1608
+ # ------------------------------------------------------------------
1609
+ # Encapsulation — sender generates shared secret + data
1610
+ #
1611
+ # No stdlib equivalent; shown for throughput reference only.
1612
+ # ------------------------------------------------------------------
1613
+ puts "\nEncapsulation (sender generates shared secret + 1120-byte data):"
1614
+ puts " [No stdlib equivalent — shown for throughput reference only]"
1615
+ ::Benchmark.ips do |x|
1616
+ x.report('Iodine X25519MLKEM768.encapsulate') do
1617
+ Iodine::Base::Crypto::X25519MLKEM768.encapsulate(public_key: pqkem_pk)
1618
+ end
1619
+ # No compare! — no stdlib equivalent
1620
+ end
1621
+
1622
+ # Generate encapsulated data outside the hot loop for decapsulate
1623
+ pqkem_ct, = Iodine::Base::Crypto::X25519MLKEM768.encapsulate(public_key: pqkem_pk)
1624
+
1625
+ # ------------------------------------------------------------------
1626
+ # Decapsulation — recipient recovers shared secret from data
1627
+ #
1628
+ # No stdlib equivalent; shown for throughput reference only.
1629
+ # ------------------------------------------------------------------
1630
+ puts "\nDecapsulation (recipient recovers 64-byte shared secret from data):"
1631
+ puts " [No stdlib equivalent — shown for throughput reference only]"
1632
+ ::Benchmark.ips do |x|
1633
+ x.report('Iodine X25519MLKEM768.decapsulate') do
1634
+ Iodine::Base::Crypto::X25519MLKEM768.decapsulate(data: pqkem_ct, secret_key: pqkem_sk)
1635
+ end
1636
+ # No compare! — no stdlib equivalent
1637
+ end
1638
+
1639
+ nil
1640
+ end
1641
+
1642
+ # Runs all crypto benchmarks.
1643
+ #
1644
+ # This is a convenience method that runs all crypto-related benchmarks:
1645
+ # - hmac (keyed MACs)
1646
+ # - random (UUIDs, PRNG and secure random bytes)
1647
+ # - crypto_symmetric (AEAD ciphers)
1648
+ # - crypto_asymmetric (Ed25519, X25519)
1649
+ # - hashing (SHA-2, SHA-3, BLAKE2)
1650
+ # - crypto_ecies (public-key encryption)
1651
+ # - crypto_kdf (HKDF key derivation)
1652
+ # - crypto_totp (TOTP generation/verification)
1653
+ # - compression (Deflate, Gzip, Brotli)
1654
+ # - non_crypto_hashing (risky_hash, crc32, risky256, risky512)
1655
+ # - crypto_pqkem (X25519+ML-KEM-768 post-quantum hybrid KEM)
1656
+ def self.crypto_all
1657
+ puts "\n" + '=' * 70
1658
+ puts 'Running All Crypto Benchmarks'
1659
+ puts '=' * 70 + "\n"
1660
+
1661
+ hmac
1662
+ puts "\n"
1663
+ random
1664
+ puts "\n"
1665
+ crypto_symmetric
1666
+ puts "\n"
1667
+ crypto_asymmetric
1668
+ puts "\n"
1669
+ hashing
1670
+ puts "\n"
1671
+ crypto_ecies
1672
+ puts "\n"
1673
+ crypto_kdf
1674
+ puts "\n"
1675
+ crypto_totp
1676
+ puts "\n"
1677
+ compression
1678
+ puts "\n"
1679
+ non_crypto_hashing
1680
+ puts "\n"
1681
+ crypto_pqkem
1682
+
1683
+ puts "\n" + '=' * 70
1684
+ puts 'All Crypto Benchmarks Complete'
1685
+ puts '=' * 70
1686
+
1687
+ nil
1688
+ end
1689
+ end
1690
+ end