ciborium 0.1.0-x86_64-linux-gnu
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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
module Compat
|
|
6
|
+
# CBOR::Unpacker of the cbor gem, reading through Ciborium::Decoder. Input passes through
|
|
7
|
+
# buffer, which also reads from io when more is needed.
|
|
8
|
+
class Unpacker
|
|
9
|
+
# @rbs @buffer: Buffer
|
|
10
|
+
# @rbs @decoder: Ciborium::Decoder
|
|
11
|
+
|
|
12
|
+
attr_reader :buffer #: Buffer
|
|
13
|
+
|
|
14
|
+
# options takes symbolize_keys, and the Buffer options.
|
|
15
|
+
#
|
|
16
|
+
#: (?untyped io, ?Hash[Symbol, untyped]? options) -> void
|
|
17
|
+
def initialize(io = nil, options = nil)
|
|
18
|
+
if options.nil? && io.is_a?(Hash)
|
|
19
|
+
options = io
|
|
20
|
+
io = nil
|
|
21
|
+
end
|
|
22
|
+
@buffer = Buffer.new(io, options)
|
|
23
|
+
@decoder = Ciborium::Decoder.new(**Compat.decode_options, symbolize_keys: !!options&.[](:symbolize_keys))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (String data) -> self
|
|
27
|
+
def feed(data)
|
|
28
|
+
@buffer << data
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: () -> untyped
|
|
33
|
+
def read
|
|
34
|
+
with_input { @decoder.read }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias_method :unpack, :read
|
|
38
|
+
|
|
39
|
+
#: () -> nil
|
|
40
|
+
def skip
|
|
41
|
+
with_input { @decoder.skip }
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether the next item is null. Consumes nothing.
|
|
46
|
+
#
|
|
47
|
+
#: () -> bool
|
|
48
|
+
def skip_nil
|
|
49
|
+
with_input { @decoder.next_nil? }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#: () -> Integer
|
|
53
|
+
def read_array_header
|
|
54
|
+
with_input { @decoder.read_array_header }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#: () -> Integer
|
|
58
|
+
def read_map_header
|
|
59
|
+
with_input { @decoder.read_map_header }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Yields each item until the input runs out, leaving a partial item buffered.
|
|
63
|
+
#
|
|
64
|
+
#: () { (untyped) -> void } -> nil
|
|
65
|
+
#: () -> Enumerator[untyped, nil]
|
|
66
|
+
def each
|
|
67
|
+
return enum_for(:each) unless block_given?
|
|
68
|
+
|
|
69
|
+
loop do
|
|
70
|
+
item = begin
|
|
71
|
+
read
|
|
72
|
+
rescue ::EOFError
|
|
73
|
+
break
|
|
74
|
+
end
|
|
75
|
+
yield item
|
|
76
|
+
end
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#: (String data) { (untyped) -> void } -> nil
|
|
81
|
+
def feed_each(data, &block)
|
|
82
|
+
feed(data)
|
|
83
|
+
each(&block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
#: () -> nil
|
|
87
|
+
def reset
|
|
88
|
+
@buffer.clear
|
|
89
|
+
@decoder.reset
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Runs the block on the decoder, moving more input into it until the block stops raising
|
|
94
|
+
# IncompleteError or the input ends.
|
|
95
|
+
#
|
|
96
|
+
#: [T] () { () -> T } -> T
|
|
97
|
+
private def with_input
|
|
98
|
+
Compat.translate_errors do
|
|
99
|
+
@decoder.feed(@buffer.read_all(@buffer.size)) unless @buffer.empty?
|
|
100
|
+
yield
|
|
101
|
+
rescue IncompleteError
|
|
102
|
+
chunk = @buffer.read(Buffer::DEFAULT_IO_BUFFER_SIZE)
|
|
103
|
+
raise unless chunk
|
|
104
|
+
|
|
105
|
+
@decoder.feed(chunk)
|
|
106
|
+
retry
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require_relative "../ciborium"
|
|
5
|
+
require_relative "compat/errors"
|
|
6
|
+
require_relative "compat/buffer"
|
|
7
|
+
require_relative "compat/packer"
|
|
8
|
+
require_relative "compat/unpacker"
|
|
9
|
+
|
|
10
|
+
module Ciborium
|
|
11
|
+
# The interface of the cbor gem (CBOR), implemented on top of Ciborium. Tag extensions follow
|
|
12
|
+
# Compat.extensions, so a tag that is not allowlisted decodes to Tagged. require
|
|
13
|
+
# "ciborium/replace" to install it as CBOR.
|
|
14
|
+
module Compat
|
|
15
|
+
Tagged = Ciborium::Tagged
|
|
16
|
+
Simple = Ciborium::Simple
|
|
17
|
+
|
|
18
|
+
# Nothing is allowlisted until the application opts in, as with Ciborium itself.
|
|
19
|
+
DEFAULT_EXTENSIONS = [] #: Array[extension]
|
|
20
|
+
DEFAULT_EXTENSIONS.freeze
|
|
21
|
+
|
|
22
|
+
# @rbs self.@extensions: Array[extension]
|
|
23
|
+
@extensions = DEFAULT_EXTENSIONS
|
|
24
|
+
|
|
25
|
+
# The extensions every Compat entry point uses, process-wide.
|
|
26
|
+
#
|
|
27
|
+
#: () -> Array[extension]
|
|
28
|
+
def self.extensions = @extensions
|
|
29
|
+
|
|
30
|
+
#: (Array[extension] extensions) -> void
|
|
31
|
+
def self.extensions=(extensions)
|
|
32
|
+
Extensions.resolve(extensions)
|
|
33
|
+
@extensions = extensions.dup.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Options for Ciborium::Encoder that reproduce the cbor gem's encoding.
|
|
37
|
+
#
|
|
38
|
+
#: () -> Hash[Symbol, untyped]
|
|
39
|
+
def self.encode_options
|
|
40
|
+
{extensions:, fallback: FALLBACK}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Options for Ciborium::Decoder that reproduce the cbor gem's decoding.
|
|
44
|
+
#
|
|
45
|
+
#: () -> Hash[Symbol, untyped]
|
|
46
|
+
def self.decode_options
|
|
47
|
+
{extensions:, duplicate_keys: :last_wins}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Encodes object, or writes it to io and returns nil when io is given.
|
|
51
|
+
#
|
|
52
|
+
#: (untyped object, ?untyped io) -> String?
|
|
53
|
+
def self.encode(object, io = nil)
|
|
54
|
+
bytes = Ciborium.encode(object, **encode_options)
|
|
55
|
+
return bytes unless io
|
|
56
|
+
|
|
57
|
+
io.write(bytes)
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Decodes a single item from a String, or from all that an IO holds.
|
|
62
|
+
#
|
|
63
|
+
# options is {symbolize_keys: true}, or :keys_as_symbols.
|
|
64
|
+
#
|
|
65
|
+
#: (untyped source, ?(Hash[Symbol, untyped] | Symbol)? options) -> untyped
|
|
66
|
+
def self.decode(source, options = nil)
|
|
67
|
+
symbolize_keys = case options
|
|
68
|
+
when nil
|
|
69
|
+
false
|
|
70
|
+
when :keys_as_symbols
|
|
71
|
+
true
|
|
72
|
+
when Hash
|
|
73
|
+
!!options[:symbolize_keys]
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError, "expected a Hash for options, got #{options.inspect}"
|
|
76
|
+
end
|
|
77
|
+
bytes = source.respond_to?(:read) ? source.read : source
|
|
78
|
+
translate_errors { Ciborium.decode(bytes, **decode_options, symbolize_keys:) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
alias_method :pack, :encode
|
|
83
|
+
alias_method :dump, :encode
|
|
84
|
+
alias_method :unpack, :decode
|
|
85
|
+
alias_method :load, :decode
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Encodes an object Ciborium cannot represent through its own to_cbor(packer), as the cbor
|
|
89
|
+
# gem does.
|
|
90
|
+
FALLBACK = ->(object) do
|
|
91
|
+
unless object.respond_to?(:to_cbor) && object.method(:to_cbor).owner != CoreExt
|
|
92
|
+
raise EncodeError, "cannot encode an instance of #{object.class}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
packer = Packer.new
|
|
96
|
+
object.to_cbor(packer)
|
|
97
|
+
Encoded.new(packer.to_s)
|
|
98
|
+
end #: ^(untyped) -> untyped
|
|
99
|
+
|
|
100
|
+
# to_cbor for the classes the cbor gem extends. Installed by ciborium/replace.
|
|
101
|
+
module CoreExt
|
|
102
|
+
#: (?untyped packer_or_io) -> untyped
|
|
103
|
+
def to_cbor(packer_or_io = nil)
|
|
104
|
+
return packer_or_io.write(self) if packer_or_io.is_a?(Packer)
|
|
105
|
+
|
|
106
|
+
Compat.encode(self, packer_or_io)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
DEFAULT_MAX_DEPTH = 128 #: Integer
|
|
6
|
+
MAX_DEPTH_LIMIT = 10_000 #: Integer
|
|
7
|
+
|
|
8
|
+
# Encoding rules a profile enforces beyond well-formedness. :ctap2 is the CTAP2 canonical CBOR
|
|
9
|
+
# encoding form that WebAuthn requires: no tags, no indefinite lengths, arguments in their
|
|
10
|
+
# shortest form, and map keys ordered without duplicates.
|
|
11
|
+
PROFILES = [:ctap2].freeze #: Array[Symbol]
|
|
12
|
+
|
|
13
|
+
# Decoding options, validated and packed into the arguments the native decoder takes.
|
|
14
|
+
DecodeOptions = Data.define(
|
|
15
|
+
:max_depth, #: Integer
|
|
16
|
+
:flags, #: Integer
|
|
17
|
+
:handlers, #: Hash[Integer, ^(Integer, untyped) -> untyped]?
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
class DecodeOptions
|
|
21
|
+
# extensions lists the allowlisted extensions (see Extensions); every other tag decodes to
|
|
22
|
+
# Tagged. profile rejects input that breaks one of PROFILES.
|
|
23
|
+
#
|
|
24
|
+
#: (?extensions: Array[extension], ?symbolize_keys: bool, ?max_depth: Integer, ?duplicate_keys: Symbol, ?profile: Symbol?) -> DecodeOptions
|
|
25
|
+
def self.build(extensions: [], symbolize_keys: false, max_depth: DEFAULT_MAX_DEPTH, duplicate_keys: :error,
|
|
26
|
+
profile: nil)
|
|
27
|
+
unless max_depth.is_a?(Integer) && max_depth.between?(1, MAX_DEPTH_LIMIT)
|
|
28
|
+
raise ArgumentError, "max_depth must be an Integer in 1..#{MAX_DEPTH_LIMIT}, got #{max_depth.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
flags = 0
|
|
32
|
+
flags |= Native::FLAG_SYMBOLIZE_KEYS if symbolize_keys
|
|
33
|
+
case duplicate_keys
|
|
34
|
+
when :error
|
|
35
|
+
# rejected by default
|
|
36
|
+
when :last_wins
|
|
37
|
+
flags |= Native::FLAG_LAST_WINS
|
|
38
|
+
else
|
|
39
|
+
raise ArgumentError, "duplicate_keys must be :error or :last_wins, got #{duplicate_keys.inspect}"
|
|
40
|
+
end
|
|
41
|
+
flags |= Native::FLAG_CTAP2 if DecodeOptions.check_profile(profile, extensions)
|
|
42
|
+
new(max_depth:, flags:, handlers: Extensions.tag_handlers(Extensions.resolve(extensions)))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns whether profile is :ctap2.
|
|
46
|
+
#
|
|
47
|
+
#: (Symbol? profile, untyped extensions) -> bool
|
|
48
|
+
def self.check_profile(profile, extensions)
|
|
49
|
+
case profile
|
|
50
|
+
when nil
|
|
51
|
+
return false
|
|
52
|
+
when *PROFILES
|
|
53
|
+
raise ArgumentError, "the #{profile} profile allows no tags, so takes no extensions" unless extensions.empty?
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "profile must be one of #{PROFILES.inspect}, got #{profile.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
# Decodes data items from input that arrives in parts. Nothing is consumed until a whole item
|
|
6
|
+
# has arrived, so reads that raise IncompleteError can be retried after feeding more.
|
|
7
|
+
class Decoder
|
|
8
|
+
include Enumerable #[untyped]
|
|
9
|
+
|
|
10
|
+
# Consumed bytes are dropped from the buffer once they exceed this size.
|
|
11
|
+
COMPACT_THRESHOLD = 4096 #: Integer
|
|
12
|
+
|
|
13
|
+
# @rbs @options: DecodeOptions
|
|
14
|
+
# @rbs @buffer: String
|
|
15
|
+
# @rbs @pos: Integer
|
|
16
|
+
# @rbs @scan_state: Native::ScanState
|
|
17
|
+
|
|
18
|
+
# Options are those of DecodeOptions.build.
|
|
19
|
+
#
|
|
20
|
+
#: (**untyped options) -> void
|
|
21
|
+
def initialize(**options)
|
|
22
|
+
@options = DecodeOptions.build(**options)
|
|
23
|
+
@buffer = "".b
|
|
24
|
+
@pos = 0
|
|
25
|
+
@scan_state = Native::ScanState.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: (String bytes) -> self
|
|
29
|
+
def feed(bytes)
|
|
30
|
+
compact
|
|
31
|
+
@buffer << bytes.b
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Decodes the next item. Raises IncompleteError when it has not fully arrived.
|
|
36
|
+
#
|
|
37
|
+
#: () -> untyped
|
|
38
|
+
def read
|
|
39
|
+
item_end
|
|
40
|
+
value, pos = Native.decode(@buffer, @pos, @options.max_depth, @options.flags, @options.handlers)
|
|
41
|
+
advance(pos)
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Yields each item that has fully arrived, leaving a partial one buffered.
|
|
46
|
+
#
|
|
47
|
+
#: () { (untyped) -> void } -> self
|
|
48
|
+
#: () -> Enumerator[untyped, self]
|
|
49
|
+
def each
|
|
50
|
+
return enum_for(:each) unless block_given?
|
|
51
|
+
|
|
52
|
+
yield read while buffered_bytes > 0 && Native.scan(@buffer, @pos, @options.max_depth, @scan_state)
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Skips the next item without decoding it.
|
|
57
|
+
#
|
|
58
|
+
#: () -> self
|
|
59
|
+
def skip
|
|
60
|
+
advance(item_end)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Reads the head of a definite-length array and returns its length, leaving the elements to
|
|
65
|
+
# be read one by one.
|
|
66
|
+
#
|
|
67
|
+
#: () -> Integer
|
|
68
|
+
def read_array_header
|
|
69
|
+
read_container_head(4, "array")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reads the head of a definite-length map and returns its number of pairs.
|
|
73
|
+
#
|
|
74
|
+
#: () -> Integer
|
|
75
|
+
def read_map_header
|
|
76
|
+
read_container_head(5, "map")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Whether the next item is null. Consumes nothing.
|
|
80
|
+
#
|
|
81
|
+
#: () -> bool
|
|
82
|
+
def next_nil?
|
|
83
|
+
byte = @buffer.getbyte(@pos)
|
|
84
|
+
raise IncompleteError, "unexpected end of input" unless byte
|
|
85
|
+
|
|
86
|
+
byte == 0xf6
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#: () -> Integer
|
|
90
|
+
def buffered_bytes
|
|
91
|
+
@buffer.bytesize - @pos
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Drops all buffered input.
|
|
95
|
+
#
|
|
96
|
+
#: () -> self
|
|
97
|
+
def reset
|
|
98
|
+
@buffer = "".b
|
|
99
|
+
advance(0)
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#: () -> Integer
|
|
104
|
+
private def item_end
|
|
105
|
+
Native.scan(@buffer, @pos, @options.max_depth, @scan_state) or
|
|
106
|
+
raise IncompleteError, "unexpected end of input"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Moves to the next item, whose scan starts afresh.
|
|
110
|
+
#
|
|
111
|
+
#: (Integer pos) -> void
|
|
112
|
+
private def advance(pos)
|
|
113
|
+
@pos = pos
|
|
114
|
+
@scan_state.clear
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#: (Integer major, String name) -> Integer
|
|
118
|
+
private def read_container_head(major, name)
|
|
119
|
+
head = Native.read_head(@buffer, @pos)
|
|
120
|
+
raise IncompleteError, "unexpected end of input" unless head
|
|
121
|
+
|
|
122
|
+
actual, length, pos = head
|
|
123
|
+
unless actual == major && length
|
|
124
|
+
raise UnexpectedTypeError, "expected a definite-length #{name}, got major type #{actual}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
advance(pos)
|
|
128
|
+
length
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The scan state is relative to @pos, so it stays valid.
|
|
132
|
+
#
|
|
133
|
+
#: () -> void
|
|
134
|
+
private def compact
|
|
135
|
+
return if @pos < COMPACT_THRESHOLD
|
|
136
|
+
|
|
137
|
+
@buffer = @buffer.byteslice(@pos..) || "".b
|
|
138
|
+
@pos = 0
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
# Encoding options, validated into the arguments the native encoder takes.
|
|
6
|
+
EncodeOptions = Data.define(
|
|
7
|
+
:max_depth, #: Integer
|
|
8
|
+
:flags, #: Integer
|
|
9
|
+
:fallback, #: Proc?
|
|
10
|
+
:class_handlers, #: Array[[Module, ^(untyped) -> untyped]]?
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class EncodeOptions
|
|
14
|
+
# extensions lists the allowlisted extensions (see Extensions). fallback is called with each
|
|
15
|
+
# object neither the encoder nor an extension can represent, and returns an object to encode
|
|
16
|
+
# in its place. profile makes the output follow one of PROFILES, sorting map keys.
|
|
17
|
+
#
|
|
18
|
+
#: (?extensions: Array[extension], ?max_depth: Integer, ?fallback: (^(untyped) -> untyped)?, ?profile: Symbol?) -> EncodeOptions
|
|
19
|
+
def self.build(extensions: [], max_depth: DEFAULT_MAX_DEPTH, fallback: nil, profile: nil)
|
|
20
|
+
unless max_depth.is_a?(Integer) && max_depth.between?(1, MAX_DEPTH_LIMIT)
|
|
21
|
+
raise ArgumentError, "max_depth must be an Integer in 1..#{MAX_DEPTH_LIMIT}, got #{max_depth.inspect}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
new(
|
|
25
|
+
max_depth:,
|
|
26
|
+
flags: DecodeOptions.check_profile(profile, extensions) ? Native::FLAG_CTAP2 : 0,
|
|
27
|
+
fallback: fallback && wrap_fallback(fallback),
|
|
28
|
+
class_handlers: Extensions.class_handlers(Extensions.resolve(extensions)),
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (^(untyped) -> untyped fallback) -> Proc
|
|
33
|
+
private_class_method def self.wrap_fallback(fallback)
|
|
34
|
+
->(object) do
|
|
35
|
+
fallback.call(object)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
raise EncodeError, "fallback failed for an instance of #{object.class}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
# A data item that is already encoded, written out as-is by the encoder. The encoder checks
|
|
6
|
+
# that bytes hold exactly one well-formed item.
|
|
7
|
+
Encoded = Data.define(
|
|
8
|
+
:bytes, #: String
|
|
9
|
+
)
|
|
10
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
# Writes data items one after another into a buffer, for output built in parts.
|
|
6
|
+
class Encoder
|
|
7
|
+
# @rbs @options: EncodeOptions
|
|
8
|
+
# @rbs @buffer: String
|
|
9
|
+
|
|
10
|
+
# Options are those of EncodeOptions.build.
|
|
11
|
+
#
|
|
12
|
+
#: (**untyped options) -> void
|
|
13
|
+
def initialize(**options)
|
|
14
|
+
@options = EncodeOptions.build(**options)
|
|
15
|
+
@buffer = "".b
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: (untyped object) -> self
|
|
19
|
+
def write(object)
|
|
20
|
+
@buffer << Native.encode(object, @options.max_depth, @options.flags, @options.fallback, @options.class_handlers)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Writes the head of an array whose length items the caller writes next.
|
|
25
|
+
#
|
|
26
|
+
#: (Integer length) -> self
|
|
27
|
+
def write_array_header(length)
|
|
28
|
+
write_head(4, length)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Writes the head of a map whose length key-value pairs the caller writes next.
|
|
32
|
+
#
|
|
33
|
+
#: (Integer length) -> self
|
|
34
|
+
def write_map_header(length)
|
|
35
|
+
write_head(5, length)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Writes a tag head; the caller writes the tag content next.
|
|
39
|
+
#
|
|
40
|
+
#: (Integer tag) -> self
|
|
41
|
+
def write_tag(tag)
|
|
42
|
+
raise EncodeError, "tags are not allowed by the ctap2 profile" if @options.flags & Native::FLAG_CTAP2 != 0
|
|
43
|
+
|
|
44
|
+
write_head(6, tag)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A copy of everything written so far.
|
|
48
|
+
#
|
|
49
|
+
#: () -> String
|
|
50
|
+
def bytes
|
|
51
|
+
@buffer.dup
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#: () -> Integer
|
|
55
|
+
def size
|
|
56
|
+
@buffer.bytesize
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#: () -> bool
|
|
60
|
+
def empty?
|
|
61
|
+
@buffer.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: () -> self
|
|
65
|
+
def clear
|
|
66
|
+
@buffer.clear
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#: (Integer major, Integer argument) -> self
|
|
71
|
+
private def write_head(major, argument)
|
|
72
|
+
unless argument.is_a?(Integer) && argument.between?(0, Tagged::MAX_TAG)
|
|
73
|
+
raise ArgumentError, "argument must be an Integer in 0..2**64-1, got #{argument.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@buffer << Native.encode_head(major, argument)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
class DecodeError < Error; end
|
|
8
|
+
|
|
9
|
+
# The input is not well-formed CBOR, or breaks a rule the decoder enforces.
|
|
10
|
+
class MalformedError < DecodeError; end
|
|
11
|
+
|
|
12
|
+
# The input ends in the middle of a data item.
|
|
13
|
+
class IncompleteError < DecodeError; end
|
|
14
|
+
|
|
15
|
+
# Containers and tags are nested deeper than max_depth.
|
|
16
|
+
class DepthError < DecodeError; end
|
|
17
|
+
|
|
18
|
+
# An allowlisted extension failed to decode a tag's content. The cause holds the original error.
|
|
19
|
+
class ExtensionError < DecodeError; end
|
|
20
|
+
|
|
21
|
+
# A Decoder head reader found an item of another type.
|
|
22
|
+
class UnexpectedTypeError < DecodeError; end
|
|
23
|
+
|
|
24
|
+
class EncodeError < Error; end
|
|
25
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
module Extensions
|
|
6
|
+
# Tags 2 and 3 (RFC 8949 section 3.4.3): integers outside -2**64..2**64-1 as byte strings.
|
|
7
|
+
class BigInteger
|
|
8
|
+
#: () -> Array[Integer]
|
|
9
|
+
def tags = [2, 3]
|
|
10
|
+
|
|
11
|
+
#: () -> Array[Module]
|
|
12
|
+
def classes = [Integer]
|
|
13
|
+
|
|
14
|
+
#: (Integer tag, untyped value) -> Integer
|
|
15
|
+
def decode(tag, value)
|
|
16
|
+
unless value.is_a?(String) && value.encoding == Encoding::BINARY
|
|
17
|
+
raise ArgumentError, "tag #{tag} content must be a byte string"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# unpack1 is typed as any directive's result; "H*" always yields a String.
|
|
21
|
+
hex = value.unpack1("H*") #: String
|
|
22
|
+
n = hex.empty? ? 0 : hex.to_i(16)
|
|
23
|
+
tag == 2 ? n : -1 - n
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (Integer object) -> Tagged
|
|
27
|
+
def encode(object)
|
|
28
|
+
tag, n = object.negative? ? [3, -1 - object] : [2, object]
|
|
29
|
+
hex = n.to_s(16)
|
|
30
|
+
hex = "0#{hex}" if hex.size.odd?
|
|
31
|
+
Tagged.new(tag, [hex].pack("H*"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Ciborium
|
|
5
|
+
module Extensions
|
|
6
|
+
# Tag 35 as Regexp. Compiling a pattern from untrusted input risks ReDoS, so pass a timeout
|
|
7
|
+
# unless Regexp.timeout is set.
|
|
8
|
+
class Regexp
|
|
9
|
+
FLAGS = ::Regexp::IGNORECASE | ::Regexp::EXTENDED | ::Regexp::MULTILINE #: Integer
|
|
10
|
+
|
|
11
|
+
# @rbs @timeout: Float?
|
|
12
|
+
|
|
13
|
+
#: (?timeout: Float?) -> void
|
|
14
|
+
def initialize(timeout: nil)
|
|
15
|
+
@timeout = timeout
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: () -> Array[Integer]
|
|
19
|
+
def tags = [35]
|
|
20
|
+
|
|
21
|
+
#: () -> Array[Module]
|
|
22
|
+
def classes = [::Regexp]
|
|
23
|
+
|
|
24
|
+
#: (Integer tag, untyped value) -> ::Regexp
|
|
25
|
+
def decode(tag, value)
|
|
26
|
+
raise ArgumentError, "tag #{tag} content must be text" unless value.is_a?(String)
|
|
27
|
+
|
|
28
|
+
::Regexp.new(value, timeout: @timeout)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Tag 35 has no place for flags, so a Regexp with any is refused rather than changed.
|
|
32
|
+
#
|
|
33
|
+
#: (::Regexp object) -> Tagged
|
|
34
|
+
def encode(object)
|
|
35
|
+
raise ArgumentError, "cannot encode #{object.inspect}: tag 35 carries no flags" if object.options & FLAGS != 0
|
|
36
|
+
|
|
37
|
+
Tagged.new(35, object.source)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Ciborium
|
|
7
|
+
module Extensions
|
|
8
|
+
# Tag 0 (RFC 3339 text) and tag 1 (seconds since the epoch) as Time.
|
|
9
|
+
class Time
|
|
10
|
+
#: () -> Array[Integer]
|
|
11
|
+
def tags = [0, 1]
|
|
12
|
+
|
|
13
|
+
#: () -> Array[Module]
|
|
14
|
+
def classes = [::Time]
|
|
15
|
+
|
|
16
|
+
#: (Integer tag, untyped value) -> ::Time
|
|
17
|
+
def decode(tag, value)
|
|
18
|
+
if tag == 0
|
|
19
|
+
raise ArgumentError, "tag 0 content must be text" unless value.is_a?(String)
|
|
20
|
+
|
|
21
|
+
return ::Time.iso8601(value)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless value.is_a?(Integer) || (value.is_a?(Float) && value.finite?)
|
|
25
|
+
raise ArgumentError, "tag 1 content must be an Integer or a finite Float"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
::Time.at(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whole seconds are written as an Integer, anything finer as a Float.
|
|
32
|
+
#
|
|
33
|
+
#: (::Time object) -> Tagged
|
|
34
|
+
def encode(object)
|
|
35
|
+
Tagged.new(1, object.subsec.zero? ? object.to_i : object.to_f)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|