oxygene 0.0.1
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 +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +15 -0
- data/lib/oxygene/car_archive.rb +158 -0
- data/lib/oxygene/cid.rb +45 -0
- data/lib/oxygene/errors.rb +22 -0
- data/lib/oxygene/extensions.rb +36 -0
- data/lib/oxygene/version.rb +5 -0
- data/lib/oxygene.rb +8 -0
- data/sig/oxygene.rbs +4 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 16f8b923311bdcd734666a03e4beff53a786b13a131a29e2ae834996d6c8545e
|
|
4
|
+
data.tar.gz: c3c3ebc73bc19bdc937425757a1563c181e2790d6e3a87a76d086675540ed68c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 30e917d087c398a27af26984842f5a71901093599310244f12794614ec1f5326614a2465021d6d2936c633ca9429246be493fa21e13ef4464054e5b02f7096af
|
|
7
|
+
data.tar.gz: 58c550605cdaf83a38aeb0a49c67fa4badafeed1ac5f2991b969179e609db485e05f17aa21cc4c42e092a06ec44f791c7fa02e3bcd6225602d7b7fbf69086941
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The zlib License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Suder
|
|
4
|
+
|
|
5
|
+
This software is provided 'as-is', without any express or implied
|
|
6
|
+
warranty. In no event will the authors be held liable for any damages
|
|
7
|
+
arising from the use of this software.
|
|
8
|
+
|
|
9
|
+
Permission is granted to anyone to use this software for any purpose,
|
|
10
|
+
including commercial applications, and to alter it and redistribute it
|
|
11
|
+
freely, subject to the following restrictions:
|
|
12
|
+
|
|
13
|
+
1. The origin of this software must not be misrepresented; you must not
|
|
14
|
+
claim that you wrote the original software. If you use this software
|
|
15
|
+
in a product, an acknowledgment in the product documentation would be
|
|
16
|
+
appreciated but is not required.
|
|
17
|
+
|
|
18
|
+
2. Altered source versions must be plainly marked as such, and must not be
|
|
19
|
+
misrepresented as being the original software.
|
|
20
|
+
|
|
21
|
+
3. This notice may not be removed or altered from any source distribution.
|
data/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Oxygène
|
|
2
|
+
|
|
3
|
+
*(n) Gaz incolore, inodore et sans saveur qui compose 1/5ème de l'air atmosphérique.*
|
|
4
|
+
|
|
5
|
+
Various data decoding primitives for working with AT Protocol and the Atmosphere (including CAR, CID, CBOR). (Extracted from [Skyfall](https://tangled.org/mackuba.eu/skyfall), work in progress.)
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> Part of ATProto Ruby SDK: [ruby.sdk.blue](https://ruby.sdk.blue)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Credits
|
|
12
|
+
|
|
13
|
+
Copyright © 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
|
|
14
|
+
|
|
15
|
+
The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'cid'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
require_relative 'extensions'
|
|
6
|
+
|
|
7
|
+
require 'base64'
|
|
8
|
+
require 'cbor'
|
|
9
|
+
require 'stringio'
|
|
10
|
+
|
|
11
|
+
# CAR v1: https://ipld.io/specs/transport/car/carv1/
|
|
12
|
+
# multicodec codes: https://github.com/multiformats/multicodec/blob/master/table.csv
|
|
13
|
+
|
|
14
|
+
module Oxygene
|
|
15
|
+
class CARSection
|
|
16
|
+
attr_reader :cid
|
|
17
|
+
|
|
18
|
+
def initialize(cid, body_data)
|
|
19
|
+
@cid = cid
|
|
20
|
+
@body_data = body_data
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def body
|
|
24
|
+
@body ||= CARArchive.convert_data(CBOR.decode(@body_data))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class CARArchive
|
|
29
|
+
using Oxygene::Extensions
|
|
30
|
+
|
|
31
|
+
attr_reader :roots, :sections
|
|
32
|
+
|
|
33
|
+
def initialize(data)
|
|
34
|
+
@sections = []
|
|
35
|
+
@buffer = StringIO.new(data)
|
|
36
|
+
|
|
37
|
+
read_header(@buffer)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def section_with_cid(cid)
|
|
41
|
+
if section = @sections.detect { |s| s.cid == cid }
|
|
42
|
+
return section.body
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if @buffer
|
|
46
|
+
while !@buffer.eof?
|
|
47
|
+
section = read_section(@buffer)
|
|
48
|
+
return section.body if section.cid == cid
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@buffer = nil
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def sections
|
|
57
|
+
if @buffer
|
|
58
|
+
read_section(@buffer) while !@buffer.eof?
|
|
59
|
+
@buffer = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@sections
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.convert_data(object)
|
|
66
|
+
if object.is_a?(Hash)
|
|
67
|
+
object.each do |k, v|
|
|
68
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
|
69
|
+
convert_data(v)
|
|
70
|
+
elsif v.is_a?(CBOR::Tagged)
|
|
71
|
+
object[k] = make_cid_link(v)
|
|
72
|
+
elsif v.is_a?(String) && v.encoding == Encoding::ASCII_8BIT
|
|
73
|
+
object[k] = make_bytes(v)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
elsif object.is_a?(Array)
|
|
77
|
+
object.each_with_index do |v, i|
|
|
78
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
|
79
|
+
convert_data(v)
|
|
80
|
+
elsif v.is_a?(CBOR::Tagged)
|
|
81
|
+
object[i] = make_cid_link(v)
|
|
82
|
+
elsif v.is_a?(String) && v.encoding == Encoding::ASCII_8BIT
|
|
83
|
+
object[i] = make_bytes(v)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
raise DecodeError, "Unexpected value type in record: #{object}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.make_cid_link(cid)
|
|
92
|
+
{ '$link' => CID.from_cbor_tag(cid) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.make_bytes(data)
|
|
96
|
+
{ '$bytes' => Base64.encode64(data).chomp.gsub(/=+$/, '') }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inspect
|
|
100
|
+
vars = instance_variables.map { |v|
|
|
101
|
+
if v == :@sections && @buffer
|
|
102
|
+
"#{v}=[...]"
|
|
103
|
+
else
|
|
104
|
+
"#{v}=#{instance_variable_get(v).inspect}"
|
|
105
|
+
end
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
"#<#{self.class}:0x#{object_id} #{vars.join(", ")}>"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def read_header(buffer)
|
|
114
|
+
len = buffer.read_varint
|
|
115
|
+
|
|
116
|
+
header_data = buffer.read(len)
|
|
117
|
+
raise DecodeError.new("Header too short: #{header_data}") unless header_data.length == len
|
|
118
|
+
|
|
119
|
+
header = CBOR.decode(header_data)
|
|
120
|
+
raise UnsupportedError.new("Unexpected CAR version: #{header['version']}") unless header['version'] == 1
|
|
121
|
+
@roots = header['roots'].map { |x| CID.from_cbor_tag(x) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def read_section(buffer)
|
|
125
|
+
len = buffer.read_varint
|
|
126
|
+
|
|
127
|
+
section_data = buffer.read(len)
|
|
128
|
+
raise DecodeError.new("Section too short: #{section_data}") unless section_data.length == len
|
|
129
|
+
|
|
130
|
+
sbuffer = StringIO.new(section_data)
|
|
131
|
+
|
|
132
|
+
version = sbuffer.read_varint
|
|
133
|
+
raise UnsupportedError.new("Unexpected CID version: #{version}") unless version == 1
|
|
134
|
+
|
|
135
|
+
codec = sbuffer.read_varint
|
|
136
|
+
raise UnsupportedError.new("Unexpected CID codec: #{codec}") unless codec == 0x71 # dag-cbor
|
|
137
|
+
|
|
138
|
+
hash = sbuffer.read_varint
|
|
139
|
+
raise UnsupportedError.new("Unexpected CID hash: #{hash}") unless hash == 0x12 # sha2-256
|
|
140
|
+
|
|
141
|
+
clen = sbuffer.read_varint
|
|
142
|
+
raise UnsupportedError.new("Unexpected CID length: #{clen}") unless clen == 32
|
|
143
|
+
|
|
144
|
+
prefix = section_data[0...sbuffer.pos]
|
|
145
|
+
|
|
146
|
+
cid_data = sbuffer.read(clen)
|
|
147
|
+
raise DecodeError.new("CID too short: #{cid_data}") unless cid_data.length == clen
|
|
148
|
+
|
|
149
|
+
cid = CID.new(prefix + cid_data)
|
|
150
|
+
|
|
151
|
+
body_data = sbuffer.read
|
|
152
|
+
new_section = CARSection.new(cid, body_data)
|
|
153
|
+
|
|
154
|
+
@sections << new_section
|
|
155
|
+
new_section
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/oxygene/cid.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
|
|
5
|
+
require 'base32'
|
|
6
|
+
|
|
7
|
+
# CIDs in DAG-CBOR: https://ipld.io/specs/codecs/dag-cbor/spec/
|
|
8
|
+
# CIDs in JSON: https://ipld.io/specs/codecs/dag-json/spec/
|
|
9
|
+
# multibase: https://github.com/multiformats/multibase
|
|
10
|
+
|
|
11
|
+
module Oxygene
|
|
12
|
+
class CID
|
|
13
|
+
attr_reader :data
|
|
14
|
+
|
|
15
|
+
def self.from_cbor_tag(tag)
|
|
16
|
+
data = tag.value
|
|
17
|
+
raise DecodeError.new("Unexpected first byte of CID: #{data[0]}") unless data[0] == "\x00"
|
|
18
|
+
CID.new(data[1..-1])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_json(string)
|
|
22
|
+
raise DecodeError.new("Unexpected CID length") unless string.length == 59
|
|
23
|
+
raise DecodeError.new("Unexpected CID prefix") unless string[0] == 'b'
|
|
24
|
+
|
|
25
|
+
data = Base32.decode(string[1..-1].upcase)
|
|
26
|
+
CID.new(data)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(data)
|
|
30
|
+
@data = data
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
'b' + Base32.encode(@data).downcase.gsub(/=+$/, '')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inspect
|
|
38
|
+
"CID(\"#{to_s}\")"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(other)
|
|
42
|
+
other.is_a?(CID) && @data == other.data
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Oxygene
|
|
4
|
+
#
|
|
5
|
+
# Wrapper base class for Oxygene error classes.
|
|
6
|
+
#
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Raised when some part of the data being decoded has invalid format.
|
|
12
|
+
#
|
|
13
|
+
class DecodeError < Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Raised when a piece of data is technically formatted correctly, but not in a version
|
|
18
|
+
# that this library currently supports (or that is used in ATProto).
|
|
19
|
+
#
|
|
20
|
+
class UnsupportedError < Error
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cbor'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
module Oxygene
|
|
7
|
+
|
|
8
|
+
# @private
|
|
9
|
+
module Extensions
|
|
10
|
+
|
|
11
|
+
refine StringIO do
|
|
12
|
+
# https://en.wikipedia.org/wiki/LEB128
|
|
13
|
+
def read_varint
|
|
14
|
+
shift = 1
|
|
15
|
+
value = 0
|
|
16
|
+
|
|
17
|
+
loop do
|
|
18
|
+
byte = self.readbyte
|
|
19
|
+
value += byte % 128 * shift
|
|
20
|
+
break if byte < 128
|
|
21
|
+
shift *= 128
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
refine CBOR.singleton_class do
|
|
29
|
+
def decode_sequence(data)
|
|
30
|
+
unpacker = CBOR::Unpacker.new(StringIO.new(data))
|
|
31
|
+
unpacker.each.to_a
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/oxygene.rb
ADDED
data/sig/oxygene.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: oxygene
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kuba Suder
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base32
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: base64
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: cbor
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 0.5.9
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 0.5.9
|
|
54
|
+
email:
|
|
55
|
+
- jakub.suder@gmail.com
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- CHANGELOG.md
|
|
61
|
+
- LICENSE.txt
|
|
62
|
+
- README.md
|
|
63
|
+
- lib/oxygene.rb
|
|
64
|
+
- lib/oxygene/car_archive.rb
|
|
65
|
+
- lib/oxygene/cid.rb
|
|
66
|
+
- lib/oxygene/errors.rb
|
|
67
|
+
- lib/oxygene/extensions.rb
|
|
68
|
+
- lib/oxygene/version.rb
|
|
69
|
+
- sig/oxygene.rbs
|
|
70
|
+
homepage: https://ruby.sdk.blue
|
|
71
|
+
licenses:
|
|
72
|
+
- Zlib
|
|
73
|
+
metadata:
|
|
74
|
+
bug_tracker_uri: https://tangled.org/mackuba.eu/oxygene/issues
|
|
75
|
+
changelog_uri: https://tangled.org/mackuba.eu/oxygene/blob/master/CHANGELOG.md
|
|
76
|
+
source_code_uri: https://tangled.org/mackuba.eu/oxygene
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: 2.6.0
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 3.6.9
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Various data decoding primitives for ATProto (CAR, CID, CBOR)
|
|
95
|
+
test_files: []
|