ob64 0.6.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8b76a774595914a6124939fe2410fe4ae5bb0687286315ab4ec1f05b8569d70
4
- data.tar.gz: 662b74f7505f28362436d44771e51720884cf0ea48076b12e59094eacf4e87b6
3
+ metadata.gz: 93d2eaeaa970d18c2ab92ff9f5bb4621c22135c16faa0f1ef09ac71babe94b95
4
+ data.tar.gz: 7bad60e17ca1b3189634cb2043d58bf1ffa85c701deff0b99886dde4a34082e1
5
5
  SHA512:
6
- metadata.gz: b0e357acff49af3e42b36cb41b521393ed4f6604fc4f5943c491a504741b985d5a47e63217acd27e0896cb2b23a8df2412fe133fff1d76d275decc5cf0a39f7f
7
- data.tar.gz: 560f3439cff65bc442f16a8815afa729422e6286f6305a044f92035bf28666b80c80c8471ffd7ac0b83379ea999643a531ef9a5905449093dafe10cff36490e9
6
+ metadata.gz: fc8c44a79e33c420a13c1d4460a2773ed6d60ff0e4eab47ea5ad0a1db4365d85c4e642e442dfa4a2753ef0153f8bde876dca4867005f28c124f4b7251b06e55d
7
+ data.tar.gz: 61b2550bc2035667a86d6bfd87149fcba0bd3faa33d9e29c6f42f89cef8be2b5e5df6a36a35682e2cb9d193ea1252ee7d37bcb1d787d0c62cfa78d1618218144
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Installs Intel SDE if this CPU can't run the avx512 codec, and exports SDE,
3
+ # a command prefix that emulates a CPU that can.
4
+ set -euo pipefail
5
+
6
+ missing=
7
+ for flag in avx512vl avx512vbmi avx512_vbmi2; do
8
+ grep -qw "$flag" /proc/cpuinfo || missing=1
9
+ done
10
+ [ -n "$missing" ] || exit 0
11
+
12
+ url=https://downloadmirror.intel.com/924984/sde-external-10.13.1-2026-07-28-lin.tar.xz
13
+ sha256=94e97d623fec54385686e1e7ba65ebc9941748c05ee451423948334892bf2b50
14
+ dir="$RUNNER_TEMP/sde"
15
+
16
+ curl -fsSL -o "$dir.tar.xz" "$url"
17
+ echo "$sha256 $dir.tar.xz" | sha256sum -c --quiet
18
+ mkdir -p "$dir"
19
+ tar xJf "$dir.tar.xz" -C "$dir" --strip-components=1
20
+ # Ice Lake: AVX512 with VBMI and VBMI2
21
+ echo "SDE=$dir/sde64 -icl --" >>"$GITHUB_ENV"
@@ -36,8 +36,15 @@ jobs:
36
36
  ruby: ["3.3", "3.4", "4.0"]
37
37
  os: [ubuntu-24.04, ubuntu-24.04-arm]
38
38
  cc: [gcc, clang]
39
+ include:
40
+ # Ruby built with AddressSanitizer, x86-64 only: memory errors in the
41
+ # C code fail the specs. ubsan adds UndefinedBehaviorSanitizer.
42
+ - ruby: asan-release
43
+ os: ubuntu-24.04
44
+ cc: clang
45
+ ubsan: true
39
46
  runs-on: ${{ matrix.os }}
40
- timeout-minutes: 15
47
+ timeout-minutes: 30
41
48
  steps:
42
49
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43
50
  with:
@@ -55,14 +62,57 @@ jobs:
55
62
  - name: Compile
56
63
  env:
57
64
  # Same warnings for both compilers; Ruby's defaults are GCC-only.
58
- MAKEFLAGS: 'CC=${{ matrix.cc }} warnflags=-Wall\ -Wextra\ -Wno-unused-parameter\ -Werror'
65
+ MAKEFLAGS: >-
66
+ CC=${{ matrix.cc }}${{ matrix.ubsan && '\ -fsanitize=undefined\ -fno-sanitize-recover=all' || '' }}
67
+ warnflags=-Wall\ -Wextra\ -Wno-unused-parameter\ -Werror
59
68
  run: bundle exec rake compile
60
69
 
70
+ - name: Install Intel SDE if the CPU lacks AVX512
71
+ if: runner.arch == 'X64'
72
+ run: .github/scripts/install-sde
73
+
61
74
  - name: Show codecs
62
- run: bundle exec ruby -Ilib -rob64 -e 'p Ob64.codecs, Ob64.codec'
75
+ run: $SDE bundle exec ruby -Ilib -rob64 -e 'p Ob64.codecs, Ob64.codec'
76
+
77
+ - name: Test
78
+ env:
79
+ # Fails the codec spec if one is missing
80
+ OB64_CODECS: ${{ runner.arch == 'X64' && 'scalar,avx2,avx512' || 'scalar,neon64' }}
81
+ run: $SDE bundle exec rake spec
82
+
83
+ codecs:
84
+ # The C codecs alone under ASan and UBSan, on exact-size buffers.
85
+ strategy:
86
+ fail-fast: false
87
+ matrix:
88
+ os: [ubuntu-24.04, ubuntu-24.04-arm]
89
+ cc: [gcc, clang]
90
+ runs-on: ${{ matrix.os }}
91
+ # Long under SDE, which emulates AVX512 slowly
92
+ timeout-minutes: 90
93
+ steps:
94
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
95
+ with:
96
+ persist-credentials: false
97
+
98
+ - uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
99
+ with:
100
+ ruby-version: "3.4"
101
+ bundler-cache: true
102
+
103
+ - name: Install Clang
104
+ if: matrix.cc == 'clang'
105
+ run: command -v clang || { sudo apt-get update && sudo apt-get install -y clang; }
106
+
107
+ - name: Install Intel SDE if the CPU lacks AVX512
108
+ if: runner.arch == 'X64'
109
+ run: .github/scripts/install-sde
63
110
 
64
111
  - name: Test
65
- run: bundle exec rake spec
112
+ env:
113
+ CC: ${{ matrix.cc }}
114
+ OB64_CODECS: ${{ runner.arch == 'X64' && 'scalar,avx2,avx512' || 'scalar,neon64' }}
115
+ run: bundle exec rake spec:c
66
116
 
67
117
  package:
68
118
  runs-on: ubuntu-24.04
@@ -89,7 +139,8 @@ jobs:
89
139
  require "ob64"
90
140
  require "ob64/core_ext"
91
141
  s = Random.bytes(4096)
92
- abort "Base64 round trip failed" unless Ob64::Base64.decode(Ob64::Base64.encode(s)) == s
142
+ abort "RFC 4648 round trip failed" unless Ob64::Base64::RFC4648.decode(Ob64::Base64::RFC4648.encode(s)) == s
143
+ abort "RFC 2045 round trip failed" unless Ob64::Base64::RFC2045.decode(Ob64::Base64::RFC2045.encode(s)) == s
93
144
  abort "UU round trip failed" unless Ob64::UU.decode(Ob64::UU.encode(s)) == s
94
145
  abort "pack round trip failed" unless [s].pack("m0").unpack1("m0") == s
95
146
  '
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.8.0] - 2026-10-27
2
+
3
+ - Fix undefined behavior in AVX512 detection.
4
+ - `Ob64::Base64::RFC4648.encode` and `encode_urlsafe` return US-ASCII Strings, like `pack("m0")` (were BINARY).
5
+
6
+ ## [0.7.0] - 2026-10-26
7
+
8
+ - **Breaking:** Split `Ob64::Base64` into one module per RFC. `Ob64::Base64::RFC4648` has `encode`, `decode`, `encode_urlsafe`, `decode_urlsafe`, `encoded_length_of` and `decoded_length_of` (were `Ob64::Base64.encode`, `decode`, `urlsafe_encode`, `urlsafe_decode`, `encoded_length_of` and `decoded_length_of`).
9
+ - Add `Ob64::Base64::RFC2045` with `encode` (optionally with CRLF line breaks), `decode`, `encoded_length_of` and `decoded_length_of`, like `pack("m")` and `unpack1("m")`.
10
+
1
11
  ## [0.6.0] - 2026-10-26
2
12
 
3
13
  - **Breaking:** `ob64/core_ext` patches `Array#pack`, `String#unpack` and `String#unpack1` instead of `Base64`.
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  gem "benchmark-ips"
8
- gem "byebug"
8
+ gem "debug"
9
9
  gem "guard"
10
10
  gem "guard-rspec"
11
11
  gem "rake", "~> 13.0"
data/README.md CHANGED
@@ -2,13 +2,56 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ob64.svg)](http://rubygems.org/gems/ob64)
4
4
  [![CI](https://github.com/jcmfernandes/ob64/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/jcmfernandes/ob64/actions/workflows/main.yml)
5
- [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/ob64/0.6.0)
5
+ [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/ob64/0.8.0)
6
6
 
7
7
  A *fast* Base64 and uuencode encoder and decoder as a Ruby gem.
8
8
 
9
- How fast? Try it yourself, execute:
9
+ How fast? Times faster than Ruby's own `pack` / `unpack1`, per call, measured with `benchmark.rb` on an AMD Ryzen 9 9900X (AVX512) with Ruby 3.4:
10
10
 
11
- $ rake benchmark
11
+ ```mermaid
12
+ %%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#2a78d6"}}}}%%
13
+ xychart-beta
14
+ title "1 kB input: times faster than Ruby"
15
+ x-axis ["m0 enc", "m0 dec", "url enc", "url dec", "m enc", "m dec", "u enc", "u dec"]
16
+ y-axis "times faster" 0 --> 45
17
+ bar [3.1, 4.8, 11.1, 12.6, 5.2, 3.9, 5.9, 4.2]
18
+ ```
19
+
20
+ ```mermaid
21
+ %%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#2a78d6"}}}}%%
22
+ xychart-beta
23
+ title "4 kB input: times faster than Ruby"
24
+ x-axis ["m0 enc", "m0 dec", "url enc", "url dec", "m enc", "m dec", "u enc", "u dec"]
25
+ y-axis "times faster" 0 --> 45
26
+ bar [6.3, 8.4, 17.6, 22.9, 8.4, 6.1, 7.5, 5.2]
27
+ ```
28
+
29
+ ```mermaid
30
+ %%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#2a78d6"}}}}%%
31
+ xychart-beta
32
+ title "16 kB input: times faster than Ruby"
33
+ x-axis ["m0 enc", "m0 dec", "url enc", "url dec", "m enc", "m dec", "u enc", "u dec"]
34
+ y-axis "times faster" 0 --> 45
35
+ bar [11.1, 12.6, 24.3, 32.8, 6.0, 7.0, 7.2, 5.3]
36
+ ```
37
+
38
+ ```mermaid
39
+ %%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#2a78d6"}}}}%%
40
+ xychart-beta
41
+ title "512 kB input: times faster than Ruby"
42
+ x-axis ["m0 enc", "m0 dec", "url enc", "url dec", "m enc", "m dec", "u enc", "u dec"]
43
+ y-axis "times faster" 0 --> 45
44
+ bar [14.0, 13.8, 39.2, 42.0, 7.7, 7.6, 7.3, 5.3]
45
+ ```
46
+
47
+ - `m0`: RFC 4648 (`Ob64::Base64::RFC4648.encode` / `decode`)
48
+ - `url`: RFC 4648 URL-safe (`encode_urlsafe` / `decode_urlsafe`), against Ruby's `pack("m0")` plus `tr`
49
+ - `m`: RFC 2045 (`Ob64::Base64::RFC2045.encode` / `decode`)
50
+ - `u`: uuencode (`Ob64::UU.encode` / `decode`)
51
+
52
+ Try it yourself:
53
+
54
+ $ bin/rake compile benchmark
12
55
 
13
56
  ## Acknowledgements
14
57
 
@@ -40,26 +83,34 @@ require "ob64"
40
83
 
41
84
  ### Base64
42
85
 
86
+ One module per RFC, each with `encode`, `decode`, `encoded_length_of` and `decoded_length_of`.
87
+
88
+ RFC 4648, no line breaks, same output as `pack("m0")` / `unpack1("m0")`:
89
+
43
90
  ```ruby
44
91
  bin1 = "This is data!"
45
- string = Ob64::Base64.encode(bin1)
92
+ string = Ob64::Base64::RFC4648.encode(bin1)
46
93
  # string = "VGhpcyBpcyBkYXRhIQ=="
47
94
 
48
- bin2 = Ob64::Base64.decode(string)
95
+ bin2 = Ob64::Base64::RFC4648.decode(string)
49
96
  puts "Same? #{bin1 == bin2}"
50
97
  # "Same? true"
98
+
99
+ Ob64::Base64::RFC4648.decode("VGhpcyBpcyBkYXRhIQ", loose: true) # padding optional
100
+ Ob64::Base64::RFC4648.encode_urlsafe(bin1, padding: false) # '-' and '_' instead of '+' and '/'
101
+ Ob64::Base64::RFC4648.decode_urlsafe(string)
51
102
  ```
52
103
 
53
- Decode input with or without padding:
104
+ Bad padding, non-zero trailing bits and non-alphabet characters raise `ArgumentError`.
105
+
106
+ RFC 2045 (MIME), line-wrapped, same output as `pack("m")` / `unpack1("m")`:
54
107
 
55
108
  ```ruby
56
- Ob64::Base64.decode("VGhpcyBpcyBkYXRhIQ", loose: true)
109
+ Ob64::Base64::RFC2045.encode(bin) # 60-char lines, each ending in "\n"
110
+ Ob64::Base64::RFC2045.encode(bin, 76, crlf: true)
111
+ Ob64::Base64::RFC2045.decode(string) # skips line breaks and other non-alphabet characters
57
112
  ```
58
113
 
59
- Bad padding, non-zero trailing bits and non-alphabet characters still raise `ArgumentError`.
60
-
61
- URL-safe alphabet: `Ob64::Base64.urlsafe_encode(bin, padding: false)`, `Ob64::Base64.urlsafe_decode(string)`.
62
-
63
114
  ### uuencode
64
115
 
65
116
  Same output as `pack("u")` / `unpack1("u")`:
@@ -107,7 +158,7 @@ See [{file:CHANGELOG.md}](CHANGELOG.md).
107
158
 
108
159
  ## Development
109
160
 
110
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
161
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests, and `rake spec:c` to check the C codecs for memory errors. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
111
162
 
112
163
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
113
164
 
@@ -118,8 +169,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jcmfer
118
169
  ## TODO
119
170
 
120
171
  * Streaming interface
121
- * RFC 2045 (MIME) encoding/decoding
122
- * Windows support
123
172
 
124
173
  ## License
125
174
 
data/Rakefile CHANGED
@@ -14,6 +14,17 @@ Rake::ExtensionTask.new("ob64_ext") do |ext|
14
14
  ext.ext_dir = "ext/ob64"
15
15
  end
16
16
 
17
+ desc "Check the SIMD codecs against scalar in C, under ASan and UBSan; SEED=n repeats a run"
18
+ task "spec:c" do
19
+ mkdir_p "tmp"
20
+ sh "#{ENV.fetch("CC", "cc")} -std=gnu99 -O3 -g -fsanitize=address,undefined -fno-sanitize-recover=all " \
21
+ "-fno-omit-frame-pointer -Wall -Wextra -Wno-unused-parameter -Werror -Iext/ob64 -o tmp/codecs_test " \
22
+ "spec/c/codecs_test.c ext/ob64/pack_base64.c ext/ob64/pack_uuencode.c"
23
+ # SDE: an emulator prefix, see .github/scripts/install-sde. It must run the test
24
+ # itself: under SDE, ASan programs started by another one exit 0 unrun.
25
+ sh "#{ENV["SDE"]} tmp/codecs_test #{ENV["SEED"]}"
26
+ end
27
+
17
28
  desc "Run benchmark"
18
29
  task :benchmark do
19
30
  load "#{__dir__}/benchmark.rb"
data/benchmark.rb CHANGED
@@ -41,28 +41,30 @@ each_size do |bin|
41
41
 
42
42
  compare(
43
43
  "Ruby pack(\"m0\")" => -> { NATIVE_PACK.bind_call([bin], "m0") },
44
- "ob64 Base64.encode" => -> { Ob64::Base64.encode(bin) },
44
+ "ob64 Base64::RFC4648.encode" => -> { Ob64::Base64::RFC4648.encode(bin) },
45
45
  "ob64 pack(\"m0\")" => -> { [bin].pack("m0") }
46
46
  )
47
47
  compare(
48
48
  "Ruby unpack1(\"m0\")" => -> { NATIVE_UNPACK1.bind_call(m0, "m0") },
49
- "ob64 Base64.decode" => -> { Ob64::Base64.decode(m0) },
49
+ "ob64 Base64::RFC4648.decode" => -> { Ob64::Base64::RFC4648.decode(m0) },
50
50
  "ob64 unpack1(\"m0\")" => -> { m0.unpack1("m0") }
51
51
  )
52
52
  compare(
53
53
  "Ruby pack(\"m0\").tr" => -> { NATIVE_PACK.bind_call([bin], "m0").tr("+/", "-_") },
54
- "ob64 Base64.urlsafe_encode" => -> { Ob64::Base64.urlsafe_encode(bin) }
54
+ "ob64 Base64::RFC4648.encode_urlsafe" => -> { Ob64::Base64::RFC4648.encode_urlsafe(bin) }
55
55
  )
56
56
  compare(
57
57
  "Ruby tr.unpack1(\"m0\")" => -> { NATIVE_UNPACK1.bind_call(urlsafe.tr("-_", "+/"), "m0") },
58
- "ob64 Base64.urlsafe_decode" => -> { Ob64::Base64.urlsafe_decode(urlsafe) }
58
+ "ob64 Base64::RFC4648.decode_urlsafe" => -> { Ob64::Base64::RFC4648.decode_urlsafe(urlsafe) }
59
59
  )
60
60
  compare(
61
61
  "Ruby pack(\"m\")" => -> { NATIVE_PACK.bind_call([bin], "m") },
62
+ "ob64 Base64::RFC2045.encode" => -> { Ob64::Base64::RFC2045.encode(bin) },
62
63
  "ob64 pack(\"m\")" => -> { [bin].pack("m") }
63
64
  )
64
65
  compare(
65
66
  "Ruby unpack1(\"m\")" => -> { NATIVE_UNPACK1.bind_call(m, "m") },
67
+ "ob64 Base64::RFC2045.decode" => -> { Ob64::Base64::RFC2045.decode(m) },
66
68
  "ob64 unpack1(\"m\")" => -> { m.unpack1("m") }
67
69
  )
68
70
  end if $run_base64_benchmark
data/bin/rake ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rake", "rake")
data/ext/ob64/ob64_ext.c CHANGED
@@ -1,5 +1,6 @@
1
1
  #include <stdbool.h>
2
2
  #include "ob64.h"
3
+ #include "ruby/encoding.h"
3
4
 
4
5
  VALUE mOb64 = Qnil;
5
6
  VALUE mOb64_Ext = Qnil;
@@ -51,6 +52,8 @@ VALUE Ob64_encode_string(VALUE self, VALUE bin, VALUE urlsafe, VALUE padding)
51
52
 
52
53
  pack_base64_encode(RSTRING_PTR(bin), bin_len, RSTRING_PTR(result), flags);
53
54
  rb_str_set_len(result, __encoded_length_of(bin_len, RTEST(padding)));
55
+ /* US-ASCII, as pack("m0") */
56
+ ENCODING_CODERANGE_SET(result, rb_usascii_encindex(), ENC_CODERANGE_7BIT);
54
57
  RB_GC_GUARD(bin);
55
58
 
56
59
  return result;
data/ext/ob64/ob64_pack.c CHANGED
@@ -60,6 +60,20 @@ uuencode(VALUE from, long line_len)
60
60
  return ascii_result(res, n);
61
61
  }
62
62
 
63
+ /* line_len: bytes per line, a multiple of 3; crlf: lines end in "\r\n" */
64
+ static VALUE
65
+ base64_encode_rfc2045(VALUE from, long line_len, int crlf)
66
+ {
67
+ long plen = RSTRING_LEN(from);
68
+ /* A line break per line */
69
+ long lines = (plen + line_len - 1) / line_len;
70
+ VALUE res = rb_str_buf_new((plen + 2) / 3 * 4 + lines * (crlf ? 2 : 1));
71
+ size_t n = pack_base64_encode_rfc2045(RSTRING_PTR(from), (size_t)plen, RSTRING_PTR(res), (int)line_len, crlf);
72
+ RB_GC_GUARD(from);
73
+
74
+ return ascii_result(res, n);
75
+ }
76
+
63
77
  static VALUE
64
78
  Ob64_pack(int argc, VALUE *argv, VALUE ary)
65
79
  {
@@ -75,31 +89,22 @@ Ob64_pack(int argc, VALUE *argv, VALUE ary)
75
89
  return rb_call_super_kw(argc, argv, RB_PASS_CALLED_KEYWORDS);
76
90
  }
77
91
 
78
- VALUE res;
79
- size_t n;
80
-
81
92
  if (type == 'm' && count == 0) {
82
- res = rb_str_buf_new((plen + 2) / 3 * 4);
83
- n = pack_base64_encode(RSTRING_PTR(from), (size_t)plen, RSTRING_PTR(res), 0);
84
- }
85
- else {
86
- long len = count;
93
+ VALUE res = rb_str_buf_new((plen + 2) / 3 * 4);
94
+ size_t n = pack_base64_encode(RSTRING_PTR(from), (size_t)plen, RSTRING_PTR(res), 0);
95
+ RB_GC_GUARD(from);
87
96
 
88
- /* '*' gets 45, as natively */
89
- if (len <= 2) len = 45;
90
- else if (len > 63 && type == 'u') len = 63;
91
- else len = len / 3 * 3;
97
+ return ascii_result(res, n);
98
+ }
92
99
 
93
- if (type == 'u') return uuencode(from, len);
100
+ long len = count;
94
101
 
95
- /* A newline per line */
96
- long lines = (plen + len - 1) / len;
97
- res = rb_str_buf_new((plen + 2) / 3 * 4 + lines);
98
- n = pack_base64_encode_rfc2045(RSTRING_PTR(from), (size_t)plen, RSTRING_PTR(res), (int)len);
99
- }
100
- RB_GC_GUARD(from);
102
+ /* '*' gets 45, as natively */
103
+ if (len <= 2) len = 45;
104
+ else if (len > 63 && type == 'u') len = 63;
105
+ else len = len / 3 * 3;
101
106
 
102
- return ascii_result(res, n);
107
+ return type == 'u' ? uuencode(from, len) : base64_encode_rfc2045(from, len, 0);
103
108
  }
104
109
 
105
110
  static VALUE
@@ -174,6 +179,58 @@ Ob64_unpack1(int argc, VALUE *argv, VALUE str)
174
179
  return res;
175
180
  }
176
181
 
182
+ /* Bytes per line for a line_length in chars; validates both arguments */
183
+ static long
184
+ rfc2045_line_bytes(VALUE bin, VALUE line_length)
185
+ {
186
+ long chars = NUM2LONG(line_length);
187
+
188
+ StringValue(bin);
189
+ if (chars < 4 || chars % 4 || chars / 4 * 3 > INT_MAX) {
190
+ rb_raise(rb_eArgError, "line length must be a positive multiple of 4: %ld", chars);
191
+ }
192
+ /* Bound the output size */
193
+ if (RSTRING_LEN(bin) > LONG_MAX / 4) rb_raise(rb_eArgError, "argument too big");
194
+
195
+ return chars / 4 * 3;
196
+ }
197
+
198
+ static VALUE
199
+ Ob64_encode_rfc_2045(VALUE self, VALUE bin, VALUE line_length, VALUE crlf)
200
+ {
201
+ long line_bytes = rfc2045_line_bytes(bin, line_length);
202
+
203
+ return base64_encode_rfc2045(bin, line_bytes, RTEST(crlf));
204
+ }
205
+
206
+ static VALUE
207
+ Ob64_decode_rfc_2045(VALUE self, VALUE string)
208
+ {
209
+ StringValue(string);
210
+
211
+ return base64_decode(string, 1);
212
+ }
213
+
214
+ static VALUE
215
+ Ob64_encoded_length_of_rfc_2045(VALUE self, VALUE bin, VALUE line_length, VALUE crlf)
216
+ {
217
+ long line_bytes = rfc2045_line_bytes(bin, line_length);
218
+ long plen = RSTRING_LEN(bin);
219
+
220
+ /* 4 chars per 3 bytes, a line break per line */
221
+ return LONG2NUM((plen + 2) / 3 * 4 + (plen + line_bytes - 1) / line_bytes * (RTEST(crlf) ? 2 : 1));
222
+ }
223
+
224
+ static VALUE
225
+ Ob64_decoded_length_of_rfc_2045(VALUE self, VALUE string)
226
+ {
227
+ StringValue(string);
228
+ size_t n = pack_base64_decoded_length_rfc2045(RSTRING_PTR(string), (size_t)RSTRING_LEN(string));
229
+ RB_GC_GUARD(string);
230
+
231
+ return SIZET2NUM(n);
232
+ }
233
+
177
234
  static VALUE
178
235
  Ob64_uuencode(VALUE self, VALUE bin)
179
236
  {
@@ -221,6 +278,10 @@ Init_ob64_pack(VALUE mExt)
221
278
  rb_define_method(mUnpack, "unpack", Ob64_unpack, -1);
222
279
  rb_define_method(mUnpack, "unpack1", Ob64_unpack1, -1);
223
280
 
281
+ rb_define_private_method(mExt, "__encode_rfc_2045", Ob64_encode_rfc_2045, 3);
282
+ rb_define_private_method(mExt, "__decode_rfc_2045", Ob64_decode_rfc_2045, 1);
283
+ rb_define_private_method(mExt, "__encoded_length_of_rfc_2045", Ob64_encoded_length_of_rfc_2045, 3);
284
+ rb_define_private_method(mExt, "__decoded_length_of_rfc_2045", Ob64_decoded_length_of_rfc_2045, 1);
224
285
  rb_define_private_method(mExt, "__uuencode", Ob64_uuencode, 1);
225
286
  rb_define_private_method(mExt, "__uudecode", Ob64_uudecode, 1);
226
287
  rb_define_private_method(mExt, "__uuencoded_length_of", Ob64_uuencoded_length_of, 1);
@@ -4359,7 +4359,7 @@ pack_base64_xgetbv (uint32_t index)
4359
4359
  }
4360
4360
 
4361
4361
  #ifndef bit_AVX512vl
4362
- #define bit_AVX512vl (1 << 31)
4362
+ #define bit_AVX512vl (1u << 31)
4363
4363
  #endif
4364
4364
  #ifndef bit_AVX512vbmi
4365
4365
  #define bit_AVX512vbmi (1 << 1)
@@ -4993,7 +4993,7 @@ pack_base64_decode (const char *src, size_t srclen, char *out, size_t *outlen, i
4993
4993
  * ====================================================================== */
4994
4994
 
4995
4995
  static size_t
4996
- encode_rfc2045_plain(const char *src, size_t srclen, char *out, int bytes_per_line)
4996
+ encode_rfc2045_plain(const char *src, size_t srclen, char *out, int bytes_per_line, int crlf)
4997
4997
  {
4998
4998
  const uint8_t *s = (const uint8_t *)src;
4999
4999
  char *o = out;
@@ -5012,6 +5012,7 @@ encode_rfc2045_plain(const char *src, size_t srclen, char *out, int bytes_per_li
5012
5012
  srclen -= 3;
5013
5013
  line_bytes += 3;
5014
5014
  if (line_bytes >= bytes_per_line) {
5015
+ if (crlf) *o++ = '\r';
5015
5016
  *o++ = '\n';
5016
5017
  line_bytes = 0;
5017
5018
  }
@@ -5022,6 +5023,7 @@ encode_rfc2045_plain(const char *src, size_t srclen, char *out, int bytes_per_li
5022
5023
  *o++ = base64_table_enc_6bit[((s[0] & 0x03) << 4) | (s[1] >> 4)];
5023
5024
  *o++ = base64_table_enc_6bit[(s[1] & 0x0F) << 2];
5024
5025
  *o++ = '=';
5026
+ if (crlf) *o++ = '\r';
5025
5027
  *o++ = '\n';
5026
5028
  }
5027
5029
  else if (srclen == 1) {
@@ -5029,9 +5031,11 @@ encode_rfc2045_plain(const char *src, size_t srclen, char *out, int bytes_per_li
5029
5031
  *o++ = base64_table_enc_6bit[(s[0] & 0x03) << 4];
5030
5032
  *o++ = '=';
5031
5033
  *o++ = '=';
5034
+ if (crlf) *o++ = '\r';
5032
5035
  *o++ = '\n';
5033
5036
  }
5034
5037
  else if (line_bytes > 0) {
5038
+ if (crlf) *o++ = '\r';
5035
5039
  *o++ = '\n';
5036
5040
  }
5037
5041
 
@@ -5052,13 +5056,13 @@ encode_whole (const struct codec *codec, const char *src, size_t srclen, char *o
5052
5056
  }
5053
5057
 
5054
5058
  size_t
5055
- pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_per_line)
5059
+ pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_per_line, int crlf)
5056
5060
  {
5057
5061
  const struct codec *codec = codec_get();
5058
5062
 
5059
5063
  bytes_per_line = bytes_per_line / 3 * 3;
5060
5064
  if (bytes_per_line == 0) bytes_per_line = 45;
5061
- if (!codec->enc_loop) return encode_rfc2045_plain(src, srclen, out, bytes_per_line);
5065
+ if (!codec->enc_loop) return encode_rfc2045_plain(src, srclen, out, bytes_per_line, crlf);
5062
5066
 
5063
5067
  size_t line = (size_t)bytes_per_line;
5064
5068
  size_t chars = line / 3 * 4;
@@ -5072,6 +5076,7 @@ pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_
5072
5076
  if (lines_per_batch == 0) {
5073
5077
  /* Lines longer than the buffer: encode in place */
5074
5078
  o += encode_whole(codec, src, line, o);
5079
+ if (crlf) *o++ = '\r';
5075
5080
  *o++ = '\n';
5076
5081
  lines = 1;
5077
5082
  }
@@ -5081,6 +5086,7 @@ pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_
5081
5086
  for (size_t i = 0; i < lines; i++) {
5082
5087
  memcpy(o, buf + i * chars, chars);
5083
5088
  o += chars;
5089
+ if (crlf) *o++ = '\r';
5084
5090
  *o++ = '\n';
5085
5091
  }
5086
5092
  }
@@ -5088,7 +5094,7 @@ pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_
5088
5094
  srclen -= lines * line;
5089
5095
  }
5090
5096
 
5091
- return (size_t)(o - out) + encode_rfc2045_plain(src, srclen, o, bytes_per_line);
5097
+ return (size_t)(o - out) + encode_rfc2045_plain(src, srclen, o, bytes_per_line, crlf);
5092
5098
  }
5093
5099
 
5094
5100
  /*
@@ -5180,3 +5186,24 @@ pack_base64_decode_rfc2045(const char *src, size_t srclen, char *out)
5180
5186
  if (n == 0) return (size_t)(o - (uint8_t *)out) + decode_rfc2045_from(s, end, o, NULL, 0);
5181
5187
  return (size_t)(o - (uint8_t *)out) + decode_rfc2045_from(s, end, o, buf, n);
5182
5188
  }
5189
+
5190
+ size_t
5191
+ pack_base64_decoded_length_rfc2045(const char *src, size_t srclen)
5192
+ {
5193
+ const uint8_t *s = (const uint8_t *)src;
5194
+ const uint8_t *end = s + srclen;
5195
+ size_t n = 0;
5196
+
5197
+ /* Counts what pack_base64_decode_rfc2045 decodes: alphabet chars up to a '=' in group position 3-4 */
5198
+ for (; s < end; s++) {
5199
+ const uint8_t v = base64_table_dec_8bit[*s];
5200
+
5201
+ if (v == 255) continue;
5202
+ if (v == 254) {
5203
+ if (n % 4 >= 2) break;
5204
+ continue;
5205
+ }
5206
+ n++;
5207
+ }
5208
+ return n / 4 * 3 + (n % 4 == 3 ? 2 : n % 4 == 2 ? 1 : 0);
5209
+ }
@@ -27,7 +27,10 @@ size_t pack_base64_encode(const char *src, size_t srclen, char *out, int flags);
27
27
  int pack_base64_decode(const char *src, size_t srclen, char *out, size_t *outlen, int flags);
28
28
 
29
29
  /* RFC 2045 codec (m with line wrapping, lenient decode) */
30
- size_t pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_per_line);
30
+ /* Lines end in "\r\n" if crlf, "\n" otherwise */
31
+ size_t pack_base64_encode_rfc2045(const char *src, size_t srclen, char *out, int bytes_per_line, int crlf);
31
32
  size_t pack_base64_decode_rfc2045(const char *src, size_t srclen, char *out);
33
+ /* Length pack_base64_decode_rfc2045 would return; reads all of src */
34
+ size_t pack_base64_decoded_length_rfc2045(const char *src, size_t srclen);
32
35
 
33
36
  #endif /* PACK_BASE64_H */
data/lib/ob64/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ob64
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/ob64.rb CHANGED
@@ -3,7 +3,8 @@
3
3
  require_relative "ob64/version"
4
4
  require_relative "ob64_ext"
5
5
 
6
- # Fast base64 and uuencode encoding and decoding: see {Base64} and {UU}.
6
+ # Fast base64 and uuencode encoding and decoding: see {Base64::RFC4648},
7
+ # {Base64::RFC2045} and {UU}.
7
8
  module Ob64
8
9
  # The glue between Ruby and the C codecs. See +../ext/ob64/ob64_ext.c+.
9
10
  module Ext; end
@@ -44,88 +45,150 @@ module Ob64
44
45
  codecs.last
45
46
  end
46
47
 
47
- # Methods for base64-encoding and -decoding strings.
48
+ # Base64 encoding and decoding, one module per RFC: {RFC4648} and {RFC2045}.
48
49
  module Base64
49
- include Ext
50
- extend Ext
51
-
52
- module_function
53
-
54
- # Returns the Base64-encoded version of +bin+.
55
- # This method complies with RFC 4648.
56
- # No line feeds are added.
57
- #
58
- # @param bin [String]
59
- # @return [String]
60
- def encode(bin)
61
- __encode(bin, false, true)
62
- end
63
-
64
- # Returns the Base64-decoded version of +string+.
65
- # This method complies with RFC 4648.
66
- # ArgumentError is raised if +string+ is incorrectly padded or contains
67
- # non-alphabet characters. Note that CR or LF are also rejected.
68
- #
69
- # With +loose+, the padding character is optional: both correctly-padded
70
- # and unpadded input are accepted. Incorrectly-padded input is still
71
- # rejected.
72
- #
73
- # @param string [String]
74
- # @param loose [Boolean] - if unpadded input is accepted
75
- # @return [String]
76
- # @raise [ArgumentError] if +string+ cannot be decoded
77
- def decode(string, loose: false)
78
- __decode(string, false, loose)
79
- end
80
-
81
- # Returns the Base64-encoded version of +bin+.
82
- # This method complies with ``Base 64 Encoding with URL and Filename Safe
83
- # Alphabet'' in RFC 4648.
84
- # The alphabet uses '-' instead of '+' and '_' instead of '/'.
85
- # Note that the result can still contain '='.
86
- # You can remove the padding by setting +padding+ as false.
87
- #
88
- # @param bin [String]
89
- # @param padding [Boolean] - if the output must be padded
90
- # @return [String]
91
- def urlsafe_encode(bin, padding: true)
92
- __encode(bin, true, padding)
93
- end
94
-
95
- # Returns the Base64-decoded version of +string+.
96
- # This method complies with ``Base 64 Encoding with URL and Filename Safe
97
- # Alphabet'' in RFC 4648.
98
- # The alphabet uses '-' instead of '+' and '_' instead of '/'.
99
- #
100
- # The padding character is optional.
101
- # This method accepts both correctly-padded and unpadded input.
102
- # Note that it still rejects incorrectly-padded input.
103
- #
104
- # @param string [String]
105
- # @return [String]
106
- # @raise [ArgumentError] if +string+ cannot be decoded
107
- def urlsafe_decode(string)
108
- __decode(string, true, true)
50
+ # Base64 as in RFC 4648: no line breaks, strict decoding.
51
+ module RFC4648
52
+ include Ext
53
+ extend Ext
54
+
55
+ module_function
56
+
57
+ # Returns the Base64-encoded version of +bin+, like
58
+ # <tt>[bin].pack("m0")</tt>. No line feeds are added.
59
+ #
60
+ # @param bin [String]
61
+ # @return [String]
62
+ def encode(bin)
63
+ __encode(bin, false, true)
64
+ end
65
+
66
+ # Returns the Base64-decoded version of +string+, like
67
+ # <tt>string.unpack1("m0")</tt>.
68
+ # ArgumentError is raised if +string+ is incorrectly padded or contains
69
+ # non-alphabet characters. Note that CR or LF are also rejected.
70
+ #
71
+ # With +loose+, the padding character is optional: both correctly-padded
72
+ # and unpadded input are accepted. Incorrectly-padded input is still
73
+ # rejected.
74
+ #
75
+ # @param string [String]
76
+ # @param loose [Boolean] - if unpadded input is accepted
77
+ # @return [String]
78
+ # @raise [ArgumentError] if +string+ cannot be decoded
79
+ def decode(string, loose: false)
80
+ __decode(string, false, loose)
81
+ end
82
+
83
+ # Returns the Base64-encoded version of +bin+ with the ``URL and Filename
84
+ # Safe'' alphabet: '-' instead of '+' and '_' instead of '/'.
85
+ # Note that the result can still contain '='.
86
+ # You can remove the padding by setting +padding+ as false.
87
+ #
88
+ # @param bin [String]
89
+ # @param padding [Boolean] - if the output must be padded
90
+ # @return [String]
91
+ def encode_urlsafe(bin, padding: true)
92
+ __encode(bin, true, padding)
93
+ end
94
+
95
+ # Returns the Base64-decoded version of +string+ with the ``URL and
96
+ # Filename Safe'' alphabet: '-' instead of '+' and '_' instead of '/'.
97
+ #
98
+ # The padding character is optional.
99
+ # This method accepts both correctly-padded and unpadded input.
100
+ # Note that it still rejects incorrectly-padded input.
101
+ #
102
+ # @param string [String]
103
+ # @return [String]
104
+ # @raise [ArgumentError] if +string+ cannot be decoded
105
+ def decode_urlsafe(string)
106
+ __decode(string, true, true)
107
+ end
108
+
109
+ # Returns the length of {encode} (or, with the same +padding+,
110
+ # {encode_urlsafe}) of +bin+.
111
+ #
112
+ # Fast: computed from +bin+'s length.
113
+ #
114
+ # @param bin [String]
115
+ # @param padding [Boolean] - if the Base64-encoded version of +bin+ will be padded
116
+ # @return [Integer]
117
+ def encoded_length_of(bin, padding: true)
118
+ __encoded_length_of(bin, padding)
119
+ end
120
+
121
+ # Returns the length of {decode} (or {decode_urlsafe}) of +string+,
122
+ # padded or not.
123
+ #
124
+ # Fast: computed from +string+'s length and its last two characters. It
125
+ # doesn't validate the rest, so decoding may still raise.
126
+ #
127
+ # @param string [String]
128
+ # @return [Integer]
129
+ # @raise [ArgumentError] if +string+ has an invalid length
130
+ def decoded_length_of(string)
131
+ __decoded_length_of(string)
132
+ end
109
133
  end
110
134
 
111
- # Returns the length of the Base64-encoded version of +bin+.
112
- #
113
- # @param bin [String]
114
- # @param padding [Boolean] - if the Base64-encoded version of +bin+ will be padded
115
- # @return [Integer]
116
- def encoded_length_of(bin, padding: true)
117
- __encoded_length_of(bin, padding)
118
- end
119
-
120
- # Returns the length of the Base64-decoded version of +string+.
121
- #
122
- # ArgumentError is raised if +string+ has an invalid length.
123
- #
124
- # @param string [String]
125
- # @return [Integer]
126
- # @raise [ArgumentError] if +string+ has an invalid length
127
- def decoded_length_of(string)
128
- __decoded_length_of(string)
135
+ # Base64 as in RFC 2045 (MIME): line breaks, lenient decoding.
136
+ module RFC2045
137
+ include Ext
138
+ extend Ext
139
+
140
+ module_function
141
+
142
+ # Returns the Base64-encoded version of +bin+ in lines of +line_length+
143
+ # characters, each ending in a line break. Like <tt>[bin].pack("m")</tt>
144
+ # by default; with +crlf+, lines end in "\r\n", as MIME requires.
145
+ # RFC 2045 allows lines of up to 76 characters; longer ones are allowed
146
+ # here, as in +pack+, but break the RFC.
147
+ #
148
+ # @param bin [String]
149
+ # @param line_length [Integer] - characters per line, a positive multiple of 4
150
+ # @param crlf [Boolean] - if lines end in "\r\n" instead of "\n"
151
+ # @return [String]
152
+ # @raise [ArgumentError] if +line_length+ is not a positive multiple of 4
153
+ def encode(bin, line_length = 60, crlf: false)
154
+ __encode_rfc_2045(bin, line_length, crlf)
155
+ end
156
+
157
+ # Returns the Base64-decoded version of +string+, like
158
+ # <tt>string.unpack1("m")</tt>: characters outside the alphabet, such
159
+ # as line breaks, are skipped. Decoding is lenient, as in Ruby, and
160
+ # never raises for malformed input.
161
+ #
162
+ # @param string [String]
163
+ # @return [String]
164
+ def decode(string)
165
+ __decode_rfc_2045(string)
166
+ end
167
+
168
+ # Returns the length of {encode} of +bin+.
169
+ #
170
+ # Fast: computed from +bin+'s length.
171
+ #
172
+ # @param bin [String]
173
+ # @param line_length [Integer] - characters per line, a positive multiple of 4
174
+ # @param crlf [Boolean] - if lines end in "\r\n" instead of "\n"
175
+ # @return [Integer]
176
+ # @raise [ArgumentError] if +line_length+ is not a positive multiple of 4
177
+ def encoded_length_of(bin, line_length = 60, crlf: false)
178
+ __encoded_length_of_rfc_2045(bin, line_length, crlf)
179
+ end
180
+
181
+ # Returns the length of {decode} of +string+.
182
+ #
183
+ # Costly: reads all of +string+, since skipped characters and +=+ decide
184
+ # the length. To size a buffer, <tt>string.bytesize * 3 / 4</tt> is
185
+ # always enough and fast.
186
+ #
187
+ # @param string [String]
188
+ # @return [Integer]
189
+ def decoded_length_of(string)
190
+ __decoded_length_of_rfc_2045(string)
191
+ end
129
192
  end
130
193
  end
131
194
 
@@ -155,7 +218,9 @@ module Ob64
155
218
  __uudecode(string)
156
219
  end
157
220
 
158
- # Returns the length of the uuencoded version of +bin+.
221
+ # Returns the length of {encode} of +bin+.
222
+ #
223
+ # Fast: computed from +bin+'s length.
159
224
  #
160
225
  # @param bin [String]
161
226
  # @return [Integer]
@@ -163,9 +228,13 @@ module Ob64
163
228
  __uuencoded_length_of(bin)
164
229
  end
165
230
 
166
- # Returns the length of the uudecoded version of +string+.
231
+ # Returns the length of {decode} of +string+.
167
232
  # Like {decode}, never raises for malformed input.
168
233
  #
234
+ # Costly: reads all of +string+, since each line's length character
235
+ # decides the length. To size a buffer, <tt>string.bytesize * 3 / 4</tt>
236
+ # is always enough and fast.
237
+ #
169
238
  # @param string [String]
170
239
  # @return [Integer]
171
240
  def decoded_length_of(string)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ob64
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - João Fernandes
@@ -19,6 +19,7 @@ extensions:
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - ".github/dependabot.yml"
22
+ - ".github/scripts/install-sde"
22
23
  - ".github/workflows/main.yml"
23
24
  - ".gitignore"
24
25
  - ".rspec"
@@ -35,6 +36,7 @@ files:
35
36
  - bin/_guard-core
36
37
  - bin/console
37
38
  - bin/guard
39
+ - bin/rake
38
40
  - bin/rspec
39
41
  - bin/setup
40
42
  - bin/standardrb
@@ -58,8 +60,8 @@ licenses:
58
60
  metadata:
59
61
  homepage_uri: https://github.com/jcmfernandes/ob64
60
62
  changelog_uri: https://github.com/jcmfernandes/ob64/blob/master/CHANGELOG.md
61
- documentation_uri: https://www.rubydoc.info/gems/ob64/0.6.0
62
- source_code_uri: https://github.com/jcmfernandes/ob64/tree/v0.6.0
63
+ documentation_uri: https://www.rubydoc.info/gems/ob64/0.8.0
64
+ source_code_uri: https://github.com/jcmfernandes/ob64/tree/v0.8.0
63
65
  rdoc_options: []
64
66
  require_paths:
65
67
  - lib
@@ -74,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
76
  - !ruby/object:Gem::Version
75
77
  version: '0'
76
78
  requirements: []
77
- rubygems_version: 3.6.9
79
+ rubygems_version: 4.0.23
78
80
  specification_version: 4
79
81
  summary: A fast Base64 and uuencode encoder and decoder.
80
82
  test_files: []