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 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
@@ -0,0 +1,3 @@
1
+ ## [0.0.1] - 2026-08-05
2
+
3
+ - initial release - extracted `CID` and `CARArchive` from Skyfall
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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oxygene
4
+ VERSION = '0.0.1'
5
+ end
data/lib/oxygene.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'oxygene/car_archive'
4
+ require_relative 'oxygene/cid'
5
+ require_relative 'oxygene/version'
6
+
7
+ module Oxygene
8
+ end
data/sig/oxygene.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Oxygene
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []