ciborium 0.1.0-x64-mingw-ucrt
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 +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.txt +21 -0
- data/NOTICE.md +26 -0
- data/README.md +221 -0
- data/Rakefile +46 -0
- data/lib/ciborium/3.3/ciborium_native.so +0 -0
- data/lib/ciborium/3.4/ciborium_native.so +0 -0
- data/lib/ciborium/4.0/ciborium_native.so +0 -0
- data/lib/ciborium/compat/buffer.rb +190 -0
- data/lib/ciborium/compat/errors.rb +30 -0
- data/lib/ciborium/compat/packer.rb +90 -0
- data/lib/ciborium/compat/unpacker.rb +111 -0
- data/lib/ciborium/compat.rb +110 -0
- data/lib/ciborium/decode_options.rb +61 -0
- data/lib/ciborium/decoder.rb +141 -0
- data/lib/ciborium/encode_options.rb +41 -0
- data/lib/ciborium/encoded.rb +10 -0
- data/lib/ciborium/encoder.rb +80 -0
- data/lib/ciborium/errors.rb +25 -0
- data/lib/ciborium/extensions/bignum.rb +35 -0
- data/lib/ciborium/extensions/regexp.rb +41 -0
- data/lib/ciborium/extensions/time.rb +39 -0
- data/lib/ciborium/extensions/uri.rb +29 -0
- data/lib/ciborium/extensions.rb +92 -0
- data/lib/ciborium/native.rb +25 -0
- data/lib/ciborium/replace.rb +105 -0
- data/lib/ciborium/simple.rb +21 -0
- data/lib/ciborium/tagged.rb +18 -0
- data/lib/ciborium/version.rb +6 -0
- data/lib/ciborium.rb +65 -0
- data/licenses/cfg-if-LICENSE-APACHE +201 -0
- data/licenses/cfg-if-LICENSE-MIT +25 -0
- data/licenses/ciborium-io-LICENSE +201 -0
- data/licenses/ciborium-ll-LICENSE +201 -0
- data/licenses/half-LICENSE-APACHE +176 -0
- data/licenses/half-LICENSE-MIT +19 -0
- data/licenses/magnus-LICENSE +21 -0
- data/licenses/rb-sys-LICENSE-APACHE +190 -0
- data/licenses/rb-sys-LICENSE-MIT +21 -0
- data/licenses/zerocopy-LICENSE-APACHE +202 -0
- data/licenses/zerocopy-LICENSE-BSD +24 -0
- data/licenses/zerocopy-LICENSE-MIT +26 -0
- data/sig/generated/ciborium/compat/buffer.rbs +95 -0
- data/sig/generated/ciborium/compat/errors.rbs +23 -0
- data/sig/generated/ciborium/compat/packer.rbs +53 -0
- data/sig/generated/ciborium/compat/unpacker.rbs +59 -0
- data/sig/generated/ciborium/compat.rbs +57 -0
- data/sig/generated/ciborium/decode_options.rbs +41 -0
- data/sig/generated/ciborium/decoder.rbs +85 -0
- data/sig/generated/ciborium/encode_options.rbs +33 -0
- data/sig/generated/ciborium/encoded.rbs +16 -0
- data/sig/generated/ciborium/encoder.rbs +50 -0
- data/sig/generated/ciborium/errors.rbs +32 -0
- data/sig/generated/ciborium/extensions/bignum.rbs +20 -0
- data/sig/generated/ciborium/extensions/regexp.rbs +30 -0
- data/sig/generated/ciborium/extensions/time.rbs +22 -0
- data/sig/generated/ciborium/extensions/uri.rbs +20 -0
- data/sig/generated/ciborium/extensions.rbs +35 -0
- data/sig/generated/ciborium/native.rbs +10 -0
- data/sig/generated/ciborium/replace.rbs +37 -0
- data/sig/generated/ciborium/version.rbs +5 -0
- data/sig/generated/ciborium.rbs +30 -0
- data/sig/manual/ciborium/extensions.rbs +12 -0
- data/sig/manual/ciborium/native.rbs +23 -0
- data/sig/manual/ciborium/tagged.rbs +25 -0
- metadata +115 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8f29d51c0cd4eed79549b18990c61d7da8b410432dd8192f683d75a77e504021
|
|
4
|
+
data.tar.gz: 4c8236bd9bb86fba40e8c6823f7e6b9bdf13bbd7a56f3850ef15c5954ea8c3af
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 72ed8d457a4b7d73e61fd8bc875f1bca032b719872cb199cf49e08007573450d28fa20b74fe815f5db44f01f2284904ff905634092beb962c468a635164f1cf7
|
|
7
|
+
data.tar.gz: fa70c6c9c1baf19c4f836fc535fa4e7c0f65b44b81e90585cc35c9f80f531d99f0c5a043a6c7ca9c6e0a6b83f840696c2f7a3716c76b641496cdf33374d73611
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-11)
|
|
4
|
+
|
|
5
|
+
- Initial release: a CBOR encoder and decoder backed by a Rust extension, where tags carry meaning only through allowlisted extensions.
|
|
6
|
+
- Built-in extensions for bignums, Time, URI and Regexp.
|
|
7
|
+
- `profile: :ctap2` for the CTAP2 canonical CBOR encoding form that WebAuthn uses.
|
|
8
|
+
- `Ciborium::Compat` and `ciborium/replace`, a stand-in for the cbor gem's interface.
|
|
9
|
+
- RBS signatures under `sig/`.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sorah Fukumori
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/NOTICE.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Notices
|
|
2
|
+
|
|
3
|
+
## Compiled into the native extension
|
|
4
|
+
|
|
5
|
+
| Crate | License |
|
|
6
|
+
|---|---|
|
|
7
|
+
| [ciborium-ll](https://github.com/enarx/ciborium), ciborium-io | Apache-2.0 |
|
|
8
|
+
| [magnus](https://github.com/matsadler/magnus) | MIT |
|
|
9
|
+
| [rb-sys](https://github.com/oxidize-rb/rb-sys) | MIT OR Apache-2.0 |
|
|
10
|
+
| [half](https://github.com/VoidStarKat/half-rs) | MIT OR Apache-2.0 |
|
|
11
|
+
| [cfg-if](https://github.com/rust-lang/cfg-if) | MIT OR Apache-2.0 |
|
|
12
|
+
| [zerocopy](https://github.com/google/zerocopy) | BSD-2-Clause OR Apache-2.0 OR MIT |
|
|
13
|
+
|
|
14
|
+
The source gem builds these from crates.io. Precompiled gems carry their license texts under
|
|
15
|
+
`licenses/`.
|
|
16
|
+
|
|
17
|
+
## Test fixtures
|
|
18
|
+
|
|
19
|
+
`spec/fixtures/webauthn/` holds attestation objects taken from the test suite of
|
|
20
|
+
[webauthn-ruby](https://github.com/cedarcode/webauthn-ruby) (`spec/support/seeds.rb` at v3.5.0),
|
|
21
|
+
which is available under the MIT License.
|
|
22
|
+
|
|
23
|
+
`spec/upstream/` holds the specs of [cbor-ruby](https://github.com/cabo/cbor-ruby) (the cbor gem,
|
|
24
|
+
`spec/` at commit 89e94f3b059deb58478eab51996f764fcc6ecd82), copied unmodified except for
|
|
25
|
+
`spec_helper.rb`, which is replaced. cbor-ruby is available under the Apache License 2.0, a copy
|
|
26
|
+
of which is in `spec/upstream/LICENSE.txt`.
|
data/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Ciborium: CBOR for Ruby without surprises from the input
|
|
2
|
+
|
|
3
|
+
Ciborium is a [CBOR](https://www.rfc-editor.org/rfc/rfc8949) encoder and decoder for Ruby, backed by a Rust extension built on [ciborium-ll](https://crates.io/crates/ciborium-ll).
|
|
4
|
+
|
|
5
|
+
It also ships a compatibility layer for the [cbor](https://rubygems.org/gems/cbor) gem. `require "ciborium/replace"` makes `CBOR` refer to Ciborium, so gems written against the cbor gem, such as [webauthn](https://github.com/cedarcode/webauthn-ruby) and [cose](https://github.com/cedarcode/cose-ruby), run on Ciborium unchanged.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
The cbor gem is a fork of msgpack-ruby's C extension with CBOR support added on top, and has not followed the memory-safety fixes msgpack-ruby made since.
|
|
10
|
+
|
|
11
|
+
Ciborium is a fresh implementation:
|
|
12
|
+
|
|
13
|
+
- **Memory safety**: parsing and serialization happen in Rust. Nesting is bounded by `max_depth` in both directions, and the decoder needs no recursion.
|
|
14
|
+
- **Strict by default**: tags are not interpreted unless an extension is allowlisted, and invalid UTF-8, malformed simple values, duplicate map keys and trailing bytes all raise.
|
|
15
|
+
- **CTAP2 canonical form**: `profile: :ctap2` validates and produces the encoding WebAuthn requires.
|
|
16
|
+
- **RBS signatures** are shipped in `sig/`.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
bundle add ciborium
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Ciborium requires Ruby 3.3 or later.
|
|
25
|
+
|
|
26
|
+
| Platform | Precompiled gem |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Linux x86_64 and aarch64, glibc 2.34 or later | `x86_64-linux-gnu`, `aarch64-linux-gnu` |
|
|
29
|
+
| Linux x86_64 and aarch64, musl | `x86_64-linux-musl`, `aarch64-linux-musl` |
|
|
30
|
+
| Linux armv7, glibc 2.31 or later | `arm-linux-gnu` |
|
|
31
|
+
| macOS 11 or later on Apple silicon | `arm64-darwin` |
|
|
32
|
+
| Windows x64 | `x64-mingw-ucrt` |
|
|
33
|
+
|
|
34
|
+
The precompiled gems carry the extension for Ruby 3.3, 3.4 and 4.0. Anywhere else the source gem builds the extension, which needs a Rust toolchain and libclang (for [rb-sys](https://github.com/oxidize-rb/rb-sys)).
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "ciborium"
|
|
40
|
+
|
|
41
|
+
bytes = Ciborium.encode({"name" => "ciborium", "tags" => [1, 2.5, nil]})
|
|
42
|
+
Ciborium.decode(bytes) #=> {"name" => "ciborium", "tags" => [1, 2.5, nil]}
|
|
43
|
+
|
|
44
|
+
Ciborium.decode("\xd8\x23\x63a*b".b) #=> #<data Ciborium::Tagged tag=35, value="a*b">
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`decode` raises when bytes follow the item. `decode_prefix` returns the item and the number of bytes it took instead, and `decode_sequence` reads an [RFC 8742](https://www.rfc-editor.org/rfc/rfc8742) sequence:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
Ciborium.decode_prefix("\x82\x01\x02\xff".b) #=> [[1, 2], 3]
|
|
51
|
+
Ciborium.decode_sequence("\x01\x02".b) #=> [1, 2]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Decoding options
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Ciborium.decode(bytes,
|
|
58
|
+
extensions: [], # allowlisted extensions, see below
|
|
59
|
+
symbolize_keys: false, # text map keys as (garbage-collectable) Symbols
|
|
60
|
+
max_depth: 128, # nesting limit for arrays, maps and tags
|
|
61
|
+
duplicate_keys: :error, # or :last_wins
|
|
62
|
+
profile: nil, # or :ctap2
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Encoding
|
|
67
|
+
|
|
68
|
+
| Ruby | CBOR |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `nil`, `true`, `false` | simple values 22, 21, 20 |
|
|
71
|
+
| `Integer` in `-2**64...2**64` | major type 0 or 1, in the shortest form |
|
|
72
|
+
| `Float` | the narrowest float that keeps the value exactly |
|
|
73
|
+
| `String` in `ASCII-8BIT` | byte string |
|
|
74
|
+
| other `String`, `Symbol` | text string, transcoded to UTF-8 |
|
|
75
|
+
| `Array`, `Hash` | array, map, in insertion order |
|
|
76
|
+
| `Ciborium::Tagged`, `Ciborium::Simple` | tag, simple value |
|
|
77
|
+
| `Ciborium::Encoded` | its bytes as-is, after checking they hold one well-formed item |
|
|
78
|
+
|
|
79
|
+
Anything else raises `Ciborium::EncodeError`, unless an extension encodes it or a `fallback` returns something to encode in its place:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Ciborium.encode(Date.today, fallback: ->(date) { date.iso8601 })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Streaming
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
decoder = Ciborium::Decoder.new(symbolize_keys: true)
|
|
89
|
+
until io.eof?
|
|
90
|
+
decoder.feed(io.readpartial(4096))
|
|
91
|
+
decoder.each { |item| handle(item) } # yields only complete items
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
encoder = Ciborium::Encoder.new
|
|
95
|
+
encoder.write_array_header(2).write(1).write("two")
|
|
96
|
+
encoder.bytes #=> "\x82\x01ctwo"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`Decoder` also reads headers (`read_array_header`, `read_map_header`), and offers `skip` and `next_nil?`.
|
|
100
|
+
|
|
101
|
+
### Errors
|
|
102
|
+
|
|
103
|
+
| Class | Raised when |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `Ciborium::MalformedError` | the input is not well-formed, breaks a strictness rule or the profile, or has trailing bytes |
|
|
106
|
+
| `Ciborium::IncompleteError` | the input ends inside an item |
|
|
107
|
+
| `Ciborium::DepthError` | nesting exceeds `max_depth` |
|
|
108
|
+
| `Ciborium::ExtensionError` | an extension failed to decode a tag; the original error is the `cause` |
|
|
109
|
+
| `Ciborium::UnexpectedTypeError` | a `Decoder` header reader found another type |
|
|
110
|
+
| `Ciborium::EncodeError` | an object cannot be encoded, or an extension or fallback failed |
|
|
111
|
+
|
|
112
|
+
All of them inherit from `Ciborium::Error`, and the decoding ones also from `Ciborium::DecodeError`.
|
|
113
|
+
|
|
114
|
+
## Extensions
|
|
115
|
+
|
|
116
|
+
An extension gives a tag meaning in both directions. Pass the built-in ones by name:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Ciborium.decode(bytes, extensions: [:bignum, :time])
|
|
120
|
+
Ciborium.encode(Time.now, extensions: [:time])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Name | Tags | Ruby |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `:bignum` | 2, 3 | `Integer` outside `-2**64...2**64` |
|
|
126
|
+
| `:time` | 0, 1 | `Time`. It encodes as tag 1, using an Integer for whole seconds and a Float otherwise |
|
|
127
|
+
| `:uri` | 32 | `URI::Generic` |
|
|
128
|
+
| `:regexp` | 35 | `Regexp` without flags. `Ciborium::Extensions::Regexp.new(timeout: 0.1)` sets a match timeout |
|
|
129
|
+
|
|
130
|
+
An extension builds Ruby objects from the input, so allowlist only those you trust the input with.
|
|
131
|
+
|
|
132
|
+
Any object that responds to `tags`, `classes`, `decode(tag, value)` and `encode(object)` is an extension too:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class PointExtension
|
|
136
|
+
def tags = [40_000]
|
|
137
|
+
def classes = [Point]
|
|
138
|
+
def decode(_tag, (x, y)) = Point.new(x, y)
|
|
139
|
+
def encode(point) = Ciborium::Tagged.new(40_000, [point.x, point.y])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Ciborium.decode(bytes, extensions: [PointExtension.new])
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## WebAuthn and the CTAP2 profile
|
|
146
|
+
|
|
147
|
+
WebAuthn encodes everything in the CTAP2 canonical CBOR encoding form ([CTAP 2.2 section 8](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#ctap2-canonical-cbor-encoding-form)). That form uses no tags at all, so WebAuthn data needs no extension. With `profile: :ctap2`, decoding rejects anything outside that form: tags, indefinite lengths, arguments longer than needed, and map keys that are out of order or duplicated. Encoding sorts map keys into canonical order and refuses tags.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
attestation = Ciborium.decode(attestation_object, profile: :ctap2)
|
|
151
|
+
|
|
152
|
+
# The credential public key in authenticator data is followed by extension data.
|
|
153
|
+
cose_key, length = Ciborium.decode_prefix(credential_public_key_and_rest, profile: :ctap2)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Replacing the cbor gem
|
|
157
|
+
|
|
158
|
+
`Ciborium::Compat` implements the cbor gem's interface on top of Ciborium:
|
|
159
|
+
|
|
160
|
+
- `CBOR.encode`, `CBOR.decode` and their `pack`, `dump`, `unpack` and `load` aliases
|
|
161
|
+
- `Packer`, `Unpacker` and `Buffer`
|
|
162
|
+
- `Tagged`, `Simple`, its error classes, and `to_cbor`
|
|
163
|
+
|
|
164
|
+
`ciborium/replace` installs it as `CBOR` and marks the cbor gem as loaded, so a later `require "cbor"` does nothing. Keep whatever depends on the cbor gem in the Gemfile, and add ciborium without letting Bundler require it:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# Gemfile
|
|
168
|
+
gem "webauthn"
|
|
169
|
+
gem "ciborium", require: false
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Then require `ciborium/replace` after Bundler sets up the load path, and before anything requires `cbor`. In a Rails app, that is between these two lines of `config/application.rb`:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# config/application.rb
|
|
176
|
+
require_relative "boot"
|
|
177
|
+
require "rails/all"
|
|
178
|
+
|
|
179
|
+
require "ciborium/replace"
|
|
180
|
+
Bundler.require(*Rails.groups)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Outside Rails, put it after `require "bundler/setup"` and before `Bundler.require` or the first `require "webauthn"`. If the cbor gem was loaded first anyway, `ciborium/replace` warns and replaces `CBOR` regardless.
|
|
184
|
+
|
|
185
|
+
Because callers of the cbor gem cannot pass options, Compat takes its extensions from one process-wide setting. Nothing is allowlisted by default, so big Integers (tags 2 and 3) decode to `Tagged` until `:bignum` is allowlisted:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Ciborium::Compat.extensions = [:bignum, :time]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The specs of webauthn-ruby and cose-ruby run against Compat in CI with this default, so neither needs any tag extension. The cbor gem's own specs run too, both with no extensions and with every built-in one allowlisted.
|
|
192
|
+
|
|
193
|
+
### Behaviour of Compat
|
|
194
|
+
|
|
195
|
+
Compat follows Ciborium's rules rather than reproducing the cbor gem exactly:
|
|
196
|
+
|
|
197
|
+
- Tags decode to `Tagged` unless allowlisted in `Compat.extensions`, and Integers outside `-2**64...2**64` encode only with `:bignum` allowlisted.
|
|
198
|
+
- Input that is not well-formed or breaks Ciborium's strictness rules raises `MalformedFormatError`, as does an extension failure, with the original error as the `cause`.
|
|
199
|
+
- Encoding raises `EncodeError` on invalid UTF-8, and on a `Regexp` with flags.
|
|
200
|
+
- `Time` encodes subseconds as a Float.
|
|
201
|
+
- `Tagged` and `Simple` are immutable `Data`.
|
|
202
|
+
- `Unpacker#buffer` holds input that has not yet reached the decoder.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
mise install
|
|
208
|
+
bin/setup
|
|
209
|
+
bundle exec rake compile spec spec:upstream steep
|
|
210
|
+
hk check --all
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The Rust extension lives in `ext/ciborium`. Signatures are written inline with rbs-inline and generated into `sig/generated` by `rake rbs`. That directory is committed, and CI checks it is current.
|
|
214
|
+
|
|
215
|
+
## Contributing
|
|
216
|
+
|
|
217
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/ciborium-ruby.
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT License, see [LICENSE.txt](./LICENSE.txt). Third-party code compiled into the extension, and test fixtures taken from other projects, are listed in [NOTICE.md](./NOTICE.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "rake/extensiontask"
|
|
6
|
+
|
|
7
|
+
GEMSPEC = Bundler.load_gemspec("ciborium.gemspec")
|
|
8
|
+
|
|
9
|
+
# No gemspec: precompiled gems are assembled by rakelib/native.rake.
|
|
10
|
+
Rake::ExtensionTask.new("ciborium_native") do |ext|
|
|
11
|
+
ext.ext_dir = "ext/ciborium"
|
|
12
|
+
ext.lib_dir = "lib/ciborium"
|
|
13
|
+
ext.source_pattern = "{Cargo.toml,Cargo.lock,build.rs,src/**/*.rs}"
|
|
14
|
+
# Platforms without a native CI runner are cross-compiled inside rb-sys-dock
|
|
15
|
+
# images (see .github/workflows/native.yml).
|
|
16
|
+
ext.cross_compile = true
|
|
17
|
+
ext.cross_platform = %w[x86_64-linux-musl aarch64-linux-musl arm-linux-gnueabihf]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
21
|
+
task spec: :compile
|
|
22
|
+
|
|
23
|
+
namespace :spec do
|
|
24
|
+
desc "Run the cbor gem's specs against Ciborium::Compat, with default and with all extensions"
|
|
25
|
+
task upstream: :compile do
|
|
26
|
+
["default", "all"].each do |mode|
|
|
27
|
+
sh(
|
|
28
|
+
{"CIBORIUM_UPSTREAM_EXTENSIONS" => mode}, "rspec", "--options", "spec/upstream/.rspec", "-I", "spec/upstream",
|
|
29
|
+
"--default-path", "spec/upstream", "spec/upstream"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "Regenerate inline RBS signatures under sig/generated"
|
|
36
|
+
task :rbs do
|
|
37
|
+
sh "rbs-inline --output=sig/generated lib"
|
|
38
|
+
sh "rbs -r time -r uri -I sig/generated -I sig/manual -I sig-stub validate"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Type check lib against the RBS signatures"
|
|
42
|
+
task steep: :rbs do
|
|
43
|
+
sh "steep check"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
task default: [:spec, "spec:upstream", :steep]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
module Compat
|
|
6
|
+
# A byte queue in front of an optional IO, as CBOR::Buffer of the cbor gem: reads refill from
|
|
7
|
+
# the IO when the queue runs dry, and writes go out to it once the queue fills.
|
|
8
|
+
class Buffer
|
|
9
|
+
DEFAULT_IO_BUFFER_SIZE = 32 * 1024 #: Integer
|
|
10
|
+
|
|
11
|
+
# @rbs @io: untyped
|
|
12
|
+
# @rbs @io_buffer_size: Integer
|
|
13
|
+
# @rbs @data: String
|
|
14
|
+
# @rbs @pos: Integer
|
|
15
|
+
|
|
16
|
+
attr_reader :io #: untyped
|
|
17
|
+
|
|
18
|
+
# The cbor gem's read_reference_threshold and write_reference_threshold tune its chunk
|
|
19
|
+
# sharing, which this buffer does not do, so they are accepted and ignored.
|
|
20
|
+
#
|
|
21
|
+
#: (?untyped io, ?Hash[Symbol, untyped]? options) -> void
|
|
22
|
+
def initialize(io = nil, options = nil)
|
|
23
|
+
if options.nil? && io.is_a?(Hash)
|
|
24
|
+
options = io
|
|
25
|
+
io = nil
|
|
26
|
+
end
|
|
27
|
+
@io = io
|
|
28
|
+
@io_buffer_size = Integer(options ? options.fetch(:io_buffer_size, DEFAULT_IO_BUFFER_SIZE) : DEFAULT_IO_BUFFER_SIZE)
|
|
29
|
+
@data = "".b
|
|
30
|
+
@pos = 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: () -> nil
|
|
34
|
+
def clear
|
|
35
|
+
@data = "".b
|
|
36
|
+
@pos = 0
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#: () -> Integer
|
|
41
|
+
def size
|
|
42
|
+
@data.bytesize - @pos
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#: () -> bool
|
|
46
|
+
def empty?
|
|
47
|
+
size.zero?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: (String data) -> Integer
|
|
51
|
+
def write(data)
|
|
52
|
+
@data << data.b
|
|
53
|
+
flush if @io && size >= @io_buffer_size
|
|
54
|
+
data.bytesize
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#: (String data) -> self
|
|
58
|
+
def <<(data)
|
|
59
|
+
write(data)
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Consumes up to n bytes, or everything without n, refilling from the IO until it ends.
|
|
64
|
+
# Returns nil when n is positive and nothing is left.
|
|
65
|
+
#
|
|
66
|
+
#: (?Integer? n, ?String? out) -> String?
|
|
67
|
+
def read(n = nil, out = nil)
|
|
68
|
+
return read_all(nil, out) if n.nil?
|
|
69
|
+
return replace(out, "".b) if n.zero?
|
|
70
|
+
|
|
71
|
+
fill(n)
|
|
72
|
+
return if empty?
|
|
73
|
+
|
|
74
|
+
replace(out, take(n))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Consumes exactly n bytes, or everything without n. Raises EOFError, consuming nothing,
|
|
78
|
+
# when fewer than n bytes are left.
|
|
79
|
+
#
|
|
80
|
+
#: (?Integer? n, ?String? out) -> String
|
|
81
|
+
def read_all(n = nil, out = nil)
|
|
82
|
+
if n.nil?
|
|
83
|
+
fill(nil)
|
|
84
|
+
return replace(out, take(size))
|
|
85
|
+
end
|
|
86
|
+
return replace(out, "".b) if n.zero?
|
|
87
|
+
|
|
88
|
+
fill(n)
|
|
89
|
+
raise ::EOFError, "end of buffer reached" if size < n
|
|
90
|
+
|
|
91
|
+
replace(out, take(n))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Consumes up to n bytes and returns how many it consumed.
|
|
95
|
+
#
|
|
96
|
+
#: (Integer n) -> Integer
|
|
97
|
+
def skip(n)
|
|
98
|
+
fill(n)
|
|
99
|
+
take(n).bytesize
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Consumes exactly n bytes, or raises EOFError consuming nothing.
|
|
103
|
+
#
|
|
104
|
+
#: (Integer n) -> self
|
|
105
|
+
def skip_all(n)
|
|
106
|
+
fill(n)
|
|
107
|
+
raise ::EOFError, "end of buffer reached" if size < n
|
|
108
|
+
|
|
109
|
+
take(n)
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#: () -> String
|
|
114
|
+
def to_str
|
|
115
|
+
@data.byteslice(@pos..) || "".b
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
alias_method :to_s, :to_str
|
|
119
|
+
|
|
120
|
+
#: () -> Array[String]
|
|
121
|
+
def to_a
|
|
122
|
+
[to_str]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Writes the queued bytes to the IO, if there is one.
|
|
126
|
+
#
|
|
127
|
+
#: () -> self
|
|
128
|
+
def flush
|
|
129
|
+
write_to(@io) if @io
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
#: () -> nil
|
|
134
|
+
def close
|
|
135
|
+
@io&.close
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Writes the queued bytes to io and consumes them.
|
|
140
|
+
#
|
|
141
|
+
#: (untyped io) -> Integer
|
|
142
|
+
def write_to(io)
|
|
143
|
+
data = take(size)
|
|
144
|
+
io.write(data)
|
|
145
|
+
data.bytesize
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#: (Integer? n) -> void
|
|
149
|
+
private def fill(n)
|
|
150
|
+
return unless @io
|
|
151
|
+
|
|
152
|
+
while n.nil? || size < n
|
|
153
|
+
chunk = read_io
|
|
154
|
+
break unless chunk
|
|
155
|
+
|
|
156
|
+
@data << chunk.b
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#: () -> String?
|
|
161
|
+
private def read_io
|
|
162
|
+
if @io.respond_to?(:readpartial)
|
|
163
|
+
@io.readpartial(@io_buffer_size)
|
|
164
|
+
else
|
|
165
|
+
@io.read(@io_buffer_size)
|
|
166
|
+
end
|
|
167
|
+
rescue ::EOFError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
#: (Integer n) -> String
|
|
172
|
+
private def take(n)
|
|
173
|
+
data = @data.byteslice(@pos, n) || "".b
|
|
174
|
+
@pos += data.bytesize
|
|
175
|
+
if @pos == @data.bytesize
|
|
176
|
+
@data = "".b
|
|
177
|
+
@pos = 0
|
|
178
|
+
end
|
|
179
|
+
data
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#: (String? out, String data) -> String
|
|
183
|
+
private def replace(out, data)
|
|
184
|
+
return data unless out
|
|
185
|
+
|
|
186
|
+
out.replace(data)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
module Compat
|
|
6
|
+
class UnpackError < StandardError; end
|
|
7
|
+
|
|
8
|
+
class MalformedFormatError < UnpackError; end
|
|
9
|
+
|
|
10
|
+
class StackError < UnpackError; end
|
|
11
|
+
|
|
12
|
+
# Raised by Unpacker#read_array_header and #read_map_header on an item of another type.
|
|
13
|
+
class TypeError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Raises the cbor gem's exception for a Ciborium decoding failure, keeping it as the cause.
|
|
16
|
+
#
|
|
17
|
+
#: [T] () { () -> T } -> T
|
|
18
|
+
def self.translate_errors
|
|
19
|
+
yield
|
|
20
|
+
rescue IncompleteError
|
|
21
|
+
raise ::EOFError, "end of buffer reached"
|
|
22
|
+
rescue DepthError
|
|
23
|
+
raise StackError, "nesting too deep"
|
|
24
|
+
rescue UnexpectedTypeError
|
|
25
|
+
raise TypeError, "unexpected type"
|
|
26
|
+
rescue DecodeError
|
|
27
|
+
raise MalformedFormatError, "malformed CBOR"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
module Compat
|
|
6
|
+
# CBOR::Packer of the cbor gem, writing through Ciborium::Encoder.
|
|
7
|
+
class Packer
|
|
8
|
+
# @rbs @buffer: Buffer
|
|
9
|
+
# @rbs @encoder: Ciborium::Encoder
|
|
10
|
+
|
|
11
|
+
attr_reader :buffer #: Buffer
|
|
12
|
+
|
|
13
|
+
#: (?untyped io, ?Hash[Symbol, untyped]? options) -> void
|
|
14
|
+
def initialize(io = nil, options = nil)
|
|
15
|
+
@buffer = Buffer.new(io, options)
|
|
16
|
+
@encoder = Ciborium::Encoder.new(**Compat.encode_options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: (untyped object) -> self
|
|
20
|
+
def write(object)
|
|
21
|
+
@encoder.write(object)
|
|
22
|
+
drain
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias_method :pack, :write
|
|
26
|
+
|
|
27
|
+
#: () -> self
|
|
28
|
+
def write_nil
|
|
29
|
+
write(nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (Integer length) -> self
|
|
33
|
+
def write_array_header(length)
|
|
34
|
+
@encoder.write_array_header(length)
|
|
35
|
+
drain
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: (Integer length) -> self
|
|
39
|
+
def write_map_header(length)
|
|
40
|
+
@encoder.write_map_header(length)
|
|
41
|
+
drain
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: () -> self
|
|
45
|
+
def flush
|
|
46
|
+
@buffer.flush
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: () -> nil
|
|
51
|
+
def clear
|
|
52
|
+
@buffer.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: () -> Integer
|
|
56
|
+
def size
|
|
57
|
+
@buffer.size
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: () -> bool
|
|
61
|
+
def empty?
|
|
62
|
+
@buffer.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: (untyped io) -> Integer
|
|
66
|
+
def write_to(io)
|
|
67
|
+
@buffer.write_to(io)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#: () -> String
|
|
71
|
+
def to_str
|
|
72
|
+
@buffer.to_str
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
alias_method :to_s, :to_str
|
|
76
|
+
|
|
77
|
+
#: () -> Array[String]
|
|
78
|
+
def to_a
|
|
79
|
+
@buffer.to_a
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#: () -> self
|
|
83
|
+
private def drain
|
|
84
|
+
@buffer << @encoder.bytes
|
|
85
|
+
@encoder.clear
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|