borsh 0.1.0 → 0.2.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: f889d5e7f96680cfdb852425c23b8053c0c2b9b6c783e8cc2ba5929821091712
4
- data.tar.gz: 9fb32d11e1f054a12aa752bf33ee94ff109ca5dae8c0555e28c41bd7c6217201
3
+ metadata.gz: fcc938b4a07232e5c9b0cb6c770c45cd1eb71224f5436f137cb5854744324d06
4
+ data.tar.gz: 3da887c2ae7d8465c63d77b36a383a64f7c0b659e854d22b66ce630af232d5e5
5
5
  SHA512:
6
- metadata.gz: e5321fc35e88b65794c7c22670667713f036b0b5a5a6217d72af8142179bc91fa15ef1ba079b22bc07098f81cb9f2c3eea040239723b2a63e10a2ba5dd7eb180
7
- data.tar.gz: bb3dcf4db0c101e30450eaee3333745e5ba35829c35f17ee1c778fbde9a79b70673a977a61b7abf630195a1c893c90ed064dd239891d361b958e1ade0cf5557c
6
+ metadata.gz: 320fa7c330c3a6eb92e827bfa452df8111e3c9a5c2c6950bb6ca12630d4d4969f507f9ee27bdaabe41f64979e1c0587e1d55ad7c534917b34bf32124ad6d472f
7
+ data.tar.gz: a50d8f139195bb973a8b598f08f9b22daf6dc4d30220262259c3ef178d0dad234916eafce7bfa787122298c26ec7cdd37e81fa11d2f177c7ab25cbb3678d722e
data/CHANGES.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 0.2.0 - 2025-05-12
9
+ ### Changed
10
+ - Bump the Ruby requirement to Ruby 3.0+
11
+ - Serialize symbols as strings
12
+ ### Added
13
+ - Implement `Borsh::Sizer`
14
+ ### Fixed
15
+ - Force UTF-8 encoding on deserialized strings (#1)
16
+
8
17
  ## 0.1.0 - 2025-01-11
9
18
 
10
19
  ## 0.0.0 - 2025-01-09
data/README.md CHANGED
@@ -1,16 +1,28 @@
1
1
  # Borsh for Ruby
2
2
 
3
3
  [![License](https://img.shields.io/badge/license-Public%20Domain-blue.svg)](https://unlicense.org)
4
- [![Compatibility](https://img.shields.io/badge/ruby-2.6%2B-blue)](https://rubygems.org/gems/borsh)
4
+ [![Compatibility](https://img.shields.io/badge/ruby-3.0%2B-blue)](https://rubyreferences.github.io/rubychanges/3.0.html)
5
5
  [![Package](https://img.shields.io/gem/v/borsh)](https://rubygems.org/gems/borsh)
6
+ [![Documentation](https://img.shields.io/badge/rubydoc-latest-blue)](https://rubydoc.info/gems/borsh)
6
7
 
7
- A Ruby library for the [Borsh] binary serialization format.
8
+ **Borsh.rb** is a [Ruby] library for encoding and decoding data in the
9
+ [Borsh] binary serialization format designed for security-critical
10
+ projects where consistency, safety, and performance matter.
8
11
 
9
- [Borsh]: https://borsh.io
12
+ ## ✨ Features
13
+
14
+ - 100% pure Ruby with zero dependencies and no bloat.
15
+ - Implements the full Borsh specification with support for every type.
16
+ - Supports both in-memory buffers and I/O streams for serialization.
17
+ - Provides a simple and intuitive API for reading and writing data.
18
+ - Provides a convenient buffer interface layered on top of `StringIO`.
19
+ - Supports customizable serialization using the `#to_borsh` protocol.
20
+ - Plays nice with others: entirely contained in the `Borsh` module.
21
+ - 100% free and unencumbered public domain software.
10
22
 
11
23
  ## 🛠️ Prerequisites
12
24
 
13
- - [Ruby](https://ruby-lang.org) 2.6+
25
+ - [Ruby] 3.0+
14
26
 
15
27
  ## ⬇️ Installation
16
28
 
@@ -28,20 +40,6 @@ gem install borsh
28
40
  require 'borsh'
29
41
  ```
30
42
 
31
- ### Writing to an output stream
32
-
33
- ```ruby
34
- $stdout.extend(Borsh::Writable)
35
- $stdout.write_string("Hello, world!")
36
- ```
37
-
38
- ### Reading from an input stream
39
-
40
- ```ruby
41
- $stdin.extend(Borsh::Readable)
42
- p $stdin.read_string
43
- ```
44
-
45
43
  ### Writing to an in-memory buffer
46
44
 
47
45
  ```ruby
@@ -50,6 +48,8 @@ serialized_data = Borsh::Buffer.open do |buf|
50
48
  buf.write_bool(true)
51
49
  buf.write_u8(255)
52
50
  buf.write_i32(-12345)
51
+ buf.write_u128(2**100)
52
+ buf.write_i128(-2**100)
53
53
  buf.write_f64(3.14159)
54
54
  buf.write_string("Hello, Borsh!")
55
55
 
@@ -58,6 +58,12 @@ serialized_data = Borsh::Buffer.open do |buf|
58
58
 
59
59
  # Dynamic-sized array (array with a length prefix):
60
60
  buf.write_vector(['a', 'b', 'c'])
61
+
62
+ # Set of integers:
63
+ buf.write_set(Set.new([1, 2, 3]))
64
+
65
+ # Map with string keys and integer values:
66
+ buf.write_map({a: 1, b: 2})
61
67
  end
62
68
  ```
63
69
 
@@ -69,17 +75,39 @@ Borsh::Buffer.new(serialized_data) do |buf|
69
75
  bool_val = buf.read_bool # => true
70
76
  u8_val = buf.read_u8 # => 255
71
77
  i32_val = buf.read_i32 # => -12345
78
+ u128_val = buf.read_u128 # => 2**100
79
+ i128_val = buf.read_i128 # => -2**100
72
80
  f64_val = buf.read_f64 # => 3.14159
73
81
  string_val = buf.read_string # => "Hello, Borsh!"
74
82
 
75
83
  # Fixed-size array:
76
- array = buf.read_array(:i32, 3) # => [1, 2, 3]
84
+ array = buf.read_array(:i64, 3) # => [1, 2, 3]
77
85
 
78
86
  # Dynamic-sized array (array with a length prefix):
79
87
  vector = buf.read_vector(:string) # => ['a', 'b', 'c']
88
+
89
+ # Set of integers:
90
+ set = buf.read_set(:i64) # => Set.new([1, 2, 3])
91
+
92
+ # Map with string keys and integer values:
93
+ map = buf.read_map(:string, :i64) # => {'a' => 1, 'b' => 2}
80
94
  end
81
95
  ```
82
96
 
97
+ ### Writing to any output stream
98
+
99
+ ```ruby
100
+ $stdout.extend(Borsh::Writable)
101
+ $stdout.write_string("Hello, world!")
102
+ ```
103
+
104
+ ### Reading from any input stream
105
+
106
+ ```ruby
107
+ $stdin.extend(Borsh::Readable)
108
+ puts $stdin.read_string
109
+ ```
110
+
83
111
  ## 📚 Reference
84
112
 
85
113
  | Informal Type | `Borsh::Writable` | `Borsh::Readable` |
@@ -101,12 +129,12 @@ end
101
129
  | string | `write_string(x)` | `read_string()` |
102
130
  | array | `write_array(x)` | `read_array(element_type, count)` |
103
131
  | vector | `write_vector(x)` | `read_vector(element_type)` |
104
- | struct | `write_struct(x)` | `read_struct(struct_class)` |
105
- | enum | `write_enum(x)` | `read_enum(variants)` |
106
- | map/hash | `write_map(x)` | `read_map(key_type, value_type)` |
107
132
  | set | `write_set(x)` | `read_set(element_type)` |
133
+ | map/hash | `write_map(x)` | `read_map(key_type, value_type)` |
108
134
  | option | `write_option(x)` | `read_option(element_type)` |
109
- | result | `write_result(x)` | `read_result(ok_type, err_type)` |
135
+ | result | `write_result([ok, value])` | `read_result(ok_type, err_type)` |
136
+ | enum | `write_enum([ordinal, value])` | `read_enum(variants)` |
137
+ | struct | `write_struct(x)` | `read_struct(struct_class)` |
110
138
 
111
139
  ## 👨‍💻 Development
112
140
 
@@ -114,9 +142,13 @@ end
114
142
  git clone https://github.com/dryruby/borsh.rb.git
115
143
  ```
116
144
 
117
- - - -
145
+ ---
118
146
 
119
- [![Share on Twitter](https://img.shields.io/badge/share%20on-twitter-03A9F4?logo=twitter)](https://twitter.com/share?url=https://github.com/dryruby/borsh.rb&text=Borsh+for+Ruby)
147
+ [![Share on X](https://img.shields.io/badge/share%20on-x-03A9F4?logo=x)](https://x.com/intent/post?url=https://github.com/dryruby/borsh.rb&text=Borsh+for+Ruby)
120
148
  [![Share on Reddit](https://img.shields.io/badge/share%20on-reddit-red?logo=reddit)](https://reddit.com/submit?url=https://github.com/dryruby/borsh.rb&title=Borsh+for+Ruby)
121
- [![Share on Hacker News](https://img.shields.io/badge/share%20on-hacker%20news-orange?logo=ycombinator)](https://news.ycombinator.com/submitlink?u=https://github.com/dryruby/borsh.rb&t=Borsh+for+Ruby)
122
- [![Share on Facebook](https://img.shields.io/badge/share%20on-facebook-1976D2?logo=facebook)](https://www.facebook.com/sharer/sharer.php?u=https://github.com/dryruby/borsh.rb)
149
+ [![Share on Hacker News](https://img.shields.io/badge/share%20on-hn-orange?logo=ycombinator)](https://news.ycombinator.com/submitlink?u=https://github.com/dryruby/borsh.rb&t=Borsh+for+Ruby)
150
+ [![Share on Facebook](https://img.shields.io/badge/share%20on-fb-1976D2?logo=facebook)](https://www.facebook.com/sharer/sharer.php?u=https://github.com/dryruby/borsh.rb)
151
+ [![Share on LinkedIn](https://img.shields.io/badge/share%20on-linkedin-3949AB?logo=linkedin)](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/dryruby/borsh.rb)
152
+
153
+ [Borsh]: https://borsh.io
154
+ [Ruby]: https://ruby-lang.org
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
data/lib/borsh/buffer.rb CHANGED
@@ -7,11 +7,11 @@ require_relative 'writable'
7
7
  ##
8
8
  # A buffer for reading and writing Borsh data.
9
9
  class Borsh::Buffer
10
- include Borsh::Readable
11
10
  include Borsh::Writable
11
+ include Borsh::Readable
12
12
 
13
13
  ##
14
- # @param [String, nil] data
14
+ # @param [String, #to_s, nil] data
15
15
  # @yield [buffer]
16
16
  # @yieldreturn [Object]
17
17
  # @return [String]
@@ -22,12 +22,12 @@ class Borsh::Buffer
22
22
  end
23
23
 
24
24
  ##
25
- # @param [String] data
25
+ # @param [String, #to_s] data
26
26
  # @yield [buffer]
27
27
  # @yieldreturn [void]
28
28
  # @return [void]
29
29
  def initialize(data = '', &block)
30
- @buffer = StringIO.new(data)
30
+ @buffer = StringIO.new(data.to_s)
31
31
  @buffer.binmode
32
32
  block.call(self) if block_given?
33
33
  end
@@ -45,16 +45,21 @@ class Borsh::Buffer
45
45
  # Returns `true` if the buffer is closed.
46
46
  #
47
47
  # @return [Boolean]
48
- def closed?
49
- @buffer.closed?
50
- end
48
+ def closed?; @buffer.closed?; end
51
49
 
52
50
  ##
53
51
  # Closes the buffer.
54
52
  #
55
53
  # @return [void]
56
- def close;
57
- @buffer.close
54
+ def close; @buffer.close; end
55
+
56
+ ##
57
+ # Writes data to the buffer.
58
+ #
59
+ # @param [String, #to_s] data
60
+ # @return [Integer]
61
+ def write(data)
62
+ @buffer.write(data.to_s)
58
63
  end
59
64
 
60
65
  ##
@@ -65,13 +70,4 @@ class Borsh::Buffer
65
70
  def read(length)
66
71
  @buffer.read(length.to_i)
67
72
  end
68
-
69
- ##
70
- # Writes data to the buffer.
71
- #
72
- # @param [String, #to_s] data
73
- # @return [Integer]
74
- def write(data)
75
- @buffer.write(data.to_s)
76
- end
77
73
  end # Borsh::Buffer
@@ -105,7 +105,7 @@ module Borsh::Readable
105
105
  end
106
106
 
107
107
  def read_string
108
- self.read(self.read_u32)
108
+ self.read(self.read_u32).force_encoding('UTF-8')
109
109
  end
110
110
 
111
111
  def read_array(element_type, count)
@@ -118,19 +118,13 @@ module Borsh::Readable
118
118
  end
119
119
  alias_method :read_vec, :read_vector
120
120
 
121
- def read_struct(struct_class)
122
- values = struct_class.members.map { |member|
123
- type = struct_class::MEMBER_TYPES.fetch(member)
124
- self.read_object(type)
125
- }
126
- struct_class.new(*values)
127
- end
128
-
129
- def read_enum(variants)
130
- ordinal = self.read_u8
131
- type = variants.fetch(ordinal)
132
- value = self.read_object(type)
133
- [ordinal, value]
121
+ def read_set(element_type)
122
+ count = self.read_u32
123
+ result = Set.new
124
+ count.times do
125
+ result << self.read_object(element_type)
126
+ end
127
+ result
134
128
  end
135
129
 
136
130
  def read_map(key_type, value_type)
@@ -145,15 +139,6 @@ module Borsh::Readable
145
139
  end
146
140
  alias_method :read_hash, :read_map
147
141
 
148
- def read_set(element_type)
149
- count = self.read_u32
150
- result = Set.new
151
- count.times do
152
- result << self.read_object(element_type)
153
- end
154
- result
155
- end
156
-
157
142
  def read_option(element_type)
158
143
  if self.read_bool
159
144
  self.read_object(element_type)
@@ -162,10 +147,25 @@ module Borsh::Readable
162
147
  end
163
148
  end
164
149
 
165
- def read_result(ok_type, err_type)
150
+ def read_result(ok_type, err_type = ok_type)
166
151
  ok = self.read_bool
167
152
  type = ok ? ok_type : err_type
168
153
  value = self.read_object(type)
169
154
  [ok, value]
170
155
  end
156
+
157
+ def read_enum(variants)
158
+ ordinal = self.read_u8
159
+ type = variants.fetch(ordinal)
160
+ value = self.read_object(type)
161
+ [ordinal, value]
162
+ end
163
+
164
+ def read_struct(struct_class)
165
+ values = struct_class.members.map { |member|
166
+ type = struct_class::MEMBER_TYPES.fetch(member)
167
+ self.read_object(type)
168
+ }
169
+ struct_class.new(*values)
170
+ end
171
171
  end # Borsh::Readable
@@ -0,0 +1,67 @@
1
+ # This is free and unencumbered software released into the public domain.
2
+
3
+ require_relative 'writable'
4
+
5
+ ##
6
+ # A byte counter for writing Borsh data.
7
+ class Borsh::Sizer
8
+ include Borsh::Writable
9
+
10
+ ##
11
+ # @yield [buffer]
12
+ # @yieldparam [Borsh::Sizer] buffer
13
+ # @yieldreturn [void]
14
+ # @return [Integer]
15
+ def self.open(&block)
16
+ buffer = self.new(&block)
17
+ buffer.close
18
+ buffer.bytesize
19
+ end
20
+
21
+ ##
22
+ # @yield [buffer]
23
+ # @yieldparam [Borsh::Sizer] buffer
24
+ # @yieldreturn [void]
25
+ # @return [void]
26
+ def initialize(&block)
27
+ self.reset!
28
+ block.call(self) if block_given?
29
+ end
30
+
31
+ ##
32
+ # Returns the buffer size in bytes.
33
+ #
34
+ # @return [Integer]
35
+ attr_reader :bytesize
36
+ alias_method :size, :bytesize
37
+
38
+ ##
39
+ # Returns `true` if the buffer is closed.
40
+ #
41
+ # @return [Boolean]
42
+ def closed?; @closed; end
43
+
44
+ ##
45
+ # Closes the buffer.
46
+ #
47
+ # @return [void]
48
+ def close; @closed ||= true; end
49
+
50
+ ##
51
+ # Writes data to the buffer.
52
+ #
53
+ # @param [String, #to_s] data
54
+ # @return [Integer]
55
+ def write(data)
56
+ raise IOError, "closed stream" if self.closed?
57
+ @bytesize += data.to_s.bytesize
58
+ end
59
+
60
+ ##
61
+ # @return [Integer]
62
+ def reset!
63
+ result = @bytesize
64
+ @bytesize, @closed = 0, false
65
+ result
66
+ end
67
+ end # Borsh::Sizer
@@ -11,6 +11,7 @@ module Borsh::Writable
11
11
  when TrueClass then self.write_bool(x)
12
12
  when Integer then self.write_i64(x)
13
13
  when Float then self.write_f64(x)
14
+ when Symbol then self.write_string(x.to_s)
14
15
  when String then self.write_string(x)
15
16
  when Array then self.write_vector(x)
16
17
  when Hash then self.write_map(x)
@@ -100,23 +101,17 @@ module Borsh::Writable
100
101
  end
101
102
  alias_method :write_vec, :write_vector
102
103
 
103
- def write_struct(x)
104
- raise "value must be a Struct" unless x.is_a?(Struct)
105
-
106
- x.members.each do |k|
107
- self.write_object(x[k])
104
+ def write_set(x)
105
+ self.write_u32(x.size)
106
+ keys = case x
107
+ when Array then x
108
+ when Hash then x.keys
109
+ when Set then x.to_a
110
+ else raise "unsupported type: #{x.class}"
108
111
  end
109
- end
110
-
111
- def write_enum(x)
112
- # An enum should be represented as `[ordinal, value]`:
113
- unless x.is_a?(Array) && x.size == 2 && x[0].is_a?(Integer)
114
- raise "enum must be [ordinal, value]"
112
+ keys.sort.each do |k|
113
+ self.write_object(k)
115
114
  end
116
-
117
- ordinal, value = x
118
- self.write_u8(ordinal)
119
- self.write_object(value)
120
115
  end
121
116
 
122
117
  def write_map(x)
@@ -137,19 +132,6 @@ module Borsh::Writable
137
132
  end
138
133
  alias_method :write_hash, :write_map
139
134
 
140
- def write_set(x)
141
- self.write_u32(x.size)
142
- keys = case x
143
- when Array then x
144
- when Hash then x.keys
145
- when Set then x.to_a
146
- else raise "unsupported type: #{x.class}"
147
- end
148
- keys.sort.each do |k|
149
- self.write_object(k)
150
- end
151
- end
152
-
153
135
  def write_option(x)
154
136
  if !x.nil?
155
137
  self.write_u8(1)
@@ -169,4 +151,23 @@ module Borsh::Writable
169
151
  self.write_u8(ok ? 1 : 0)
170
152
  self.write_object(value)
171
153
  end
154
+
155
+ def write_enum(x)
156
+ # An enum should be represented as `[ordinal, value]`:
157
+ unless x.is_a?(Array) && x.size == 2 && x[0].is_a?(Integer)
158
+ raise "enum must be [ordinal, value]"
159
+ end
160
+
161
+ ordinal, value = x
162
+ self.write_u8(ordinal)
163
+ self.write_object(value)
164
+ end
165
+
166
+ def write_struct(x)
167
+ raise "value must be a Struct" unless x.is_a?(Struct)
168
+
169
+ x.members.each do |k|
170
+ self.write_object(x[k])
171
+ end
172
+ end
172
173
  end # Borsh::Writable
data/lib/borsh.rb CHANGED
@@ -4,4 +4,5 @@ module Borsh; end
4
4
 
5
5
  require_relative 'borsh/buffer'
6
6
  require_relative 'borsh/readable'
7
+ require_relative 'borsh/sizer'
7
8
  require_relative 'borsh/writable'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: borsh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arto Bendiken
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-11 00:00:00.000000000 Z
10
+ date: 2025-05-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rspec
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '3.12'
18
+ version: '3.13'
19
19
  type: :development
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '3.12'
25
+ version: '3.13'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: yard
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -37,7 +37,8 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.9'
40
- description: A Ruby library for the Borsh binary serialization format.
40
+ description: A Ruby library for encoding and decoding data in the Borsh binary serialization
41
+ format designed for security-critical projects.
41
42
  email: arto@bendiken.net
42
43
  executables: []
43
44
  extensions: []
@@ -51,6 +52,7 @@ files:
51
52
  - lib/borsh.rb
52
53
  - lib/borsh/buffer.rb
53
54
  - lib/borsh/readable.rb
55
+ - lib/borsh/sizer.rb
54
56
  - lib/borsh/writable.rb
55
57
  homepage: https://github.com/dryruby/borsh.rb
56
58
  licenses:
@@ -58,7 +60,7 @@ licenses:
58
60
  metadata:
59
61
  bug_tracker_uri: https://github.com/dryruby/borsh.rb/issues
60
62
  changelog_uri: https://github.com/dryruby/borsh.rb/blob/master/CHANGES.md
61
- documentation_uri: https://github.com/dryruby/borsh.rb/blob/master/README.md
63
+ documentation_uri: https://rubydoc.info/gems/borsh
62
64
  homepage_uri: https://github.com/dryruby/borsh.rb
63
65
  source_code_uri: https://github.com/dryruby/borsh.rb
64
66
  rdoc_options: []
@@ -68,14 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
70
  requirements:
69
71
  - - ">="
70
72
  - !ruby/object:Gem::Version
71
- version: '2.6'
73
+ version: '3.0'
72
74
  required_rubygems_version: !ruby/object:Gem::Requirement
73
75
  requirements:
74
76
  - - ">="
75
77
  - !ruby/object:Gem::Version
76
78
  version: '0'
77
79
  requirements: []
78
- rubygems_version: 3.6.2
80
+ rubygems_version: 3.6.3
79
81
  specification_version: 4
80
82
  summary: Borsh for Ruby
81
83
  test_files: []