tinycode 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Tinycode
2
+
3
+ This gem provides an encoder and decoder for tinycode, which is a bencode like
4
+ encoding that supports more data-types and slightly smaller encoded sizes.
5
+
6
+ The encoder supports arrays, hashes, strings, symbols, and integers, as well as
7
+ specialized fixed-width integers: `Tinycode::U8`, `Tinycode::U16`,
8
+ `Tinycode::U32`, and `Tinycode::U64`.
9
+ Like [YAML] but unlike [JSON], this encoder maintains a distinction between
10
+ strings and symbols.
11
+
12
+ The encoding is inspired by [BEncode]'s tag-length-value construction for strings, which eliminates the need for escaping in Strings.
13
+ This makes this encoding particularly appropriate for serializing binary strings
14
+ directly.
15
+
16
+ [YAML]: https://ruby-doc.org/stdlib-3.0.2/libdoc/yaml/rdoc/YAML.html
17
+ [JSON]: https://ruby-doc.org/stdlib-3.0.2/libdoc/json/rdoc/JSON.html
18
+ [BEncode]: https://en.wikipedia.org/wiki/Bencode
19
+
20
+ ## Demo
21
+
22
+ Consider the following data structure:
23
+
24
+ ```ruby
25
+ [
26
+ {
27
+ location: 'Chicago',
28
+ currently: {
29
+ temperature: 72,
30
+ condition: ['Partly cloudy', 'Chance of rain']
31
+ },
32
+ narrative: "High: 81, low: 56\nCooler than yesterday.",
33
+ high: 81,
34
+ low: 56
35
+ },
36
+ {
37
+ location: 'New York',
38
+ currently: {
39
+ temperature: 45,
40
+ condition: ['Rainy']
41
+ },
42
+ narrative: "High: 56, low: -2\nWinter Weather Alert in effect.",
43
+ high: 56,
44
+ low: -2
45
+ }
46
+ ]
47
+ ```
48
+
49
+ The resulting structure looks something like this. In actuality, there is not
50
+ this much whitespace, and the \xNN codes represent binary values.
51
+
52
+ ```plain
53
+ [
54
+ {
55
+ currently:{
56
+ condition:[
57
+ Partly cloudy'
58
+ Chance of rain'
59
+ ]
60
+ temperature:+\x48
61
+ }
62
+ high:+\x51
63
+ location:Chicago'
64
+ low:+\x38
65
+ narrative:'\x28High: 81, low: 56\nCooler than yesterday.
66
+ }
67
+ {
68
+ currently:{condition:[Rainy']temperature:+\x2D}
69
+ high:+\x38
70
+ location:New York'
71
+ low:-\x02
72
+ narrative:'\x31High: 56, low: -2\nWinter Weather Alert in effect.
73
+ }
74
+ ]
75
+ ```
76
+
77
+ ## Installation
78
+
79
+ Add this line to your application's Gemfile:
80
+
81
+ ```ruby
82
+ gem 'tinycode'
83
+ ```
84
+
85
+ And then execute:
86
+
87
+ ```sh
88
+ $ bundle install
89
+ ```
90
+
91
+ Or install it yourself as:
92
+
93
+ ```sh
94
+ $ gem install tinycode
95
+ ```
96
+
97
+ ## Usage
98
+
99
+ ### Dumping a data structure
100
+
101
+ ```ruby
102
+ require 'tinycode'
103
+
104
+ 42.tinycode
105
+ ['some', 'array'].tinycode
106
+ # and so on
107
+ ```
108
+
109
+ Alternatively you can use `Tinycode.dump`:
110
+
111
+ ```ruby
112
+ # can use 'tinycode/no_core_ext' instead if class extensions are unwanted
113
+ require 'tinycode'
114
+
115
+ Tinycode.dump(42)
116
+ Tinycode.dump(['some', 'array'])
117
+ # and so on
118
+ ```
119
+
120
+ ### Restoring a data structure
121
+
122
+ ```ruby
123
+ require 'tinycode'
124
+
125
+ Tinycode.load("+\x2A".b) # => 42
126
+ Tinycode.load("[some'array']".b) # => ['some', 'array']
127
+ # and so on
128
+ ```
129
+
130
+ ## Development
131
+
132
+ After checking out the repo, run `bundle install` to install dependencies. Then,
133
+ run `rake watch` to run yard, tests, and linting on every file save.
134
+
135
+ To install this gem onto your local machine, run `rake install`.
136
+
137
+ To release a new version, update the version number in `version.rb`.
138
+ Then run `rake release`, which will create/push a git tag for the version and
139
+ publish the `.gem` file to [rubygems.org].
140
+
141
+ [rubygems.org]: https://rubygems.org
142
+
143
+ ## License
144
+
145
+ All copyright rights are held by the authors as recorded in git.
146
+ See the commit history for a comprehensive listing of copyright holders and
147
+ copyright dates.
148
+
149
+ This program is free software: you can redistribute it and/or modify
150
+ it under the terms of the GNU General Public License as published by
151
+ the Free Software Foundation, either version 3 of the License, or
152
+ (at your option) any later version.
153
+
154
+ This program is distributed in the hope that it will be useful,
155
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
156
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
157
+ GNU General Public License for more details.
158
+
159
+ See LICENSE in this repository for the full license text.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+ require 'yard'
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+ RuboCop::RakeTask.new(:lint) do |task|
11
+ task.options = %w[--except Metrics]
12
+ end
13
+ YARD::Rake::YardocTask.new
14
+
15
+ task :watch do
16
+ sh 'rerun -p "{**/*.{rb,gemspec},exe/**/*,Gemfile*,Rakefile}" -x rake yard spec lint:auto_correct'
17
+ end
18
+
19
+ task default: [:yard, :spec, :lint]
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/Documentation
4
+
5
+
6
+ class Array
7
+ def tinycode
8
+ Tinycode.dump(self)
9
+ end
10
+ end
11
+
12
+ class Hash
13
+ def tinycode
14
+ Tinycode.dump(self)
15
+ end
16
+ end
17
+
18
+ class String
19
+ def tinycode
20
+ Tinycode.dump(self)
21
+ end
22
+ end
23
+
24
+ class Symbol
25
+ def tinycode
26
+ Tinycode.dump(self)
27
+ end
28
+ end
29
+
30
+ class Integer
31
+ def tinycode
32
+ Tinycode.dump(self)
33
+ end
34
+ end
35
+
36
+ class TrueClass
37
+ def tinycode
38
+ Tinycode.dump(self)
39
+ end
40
+ end
41
+
42
+ class FalseClass
43
+ def tinycode
44
+ Tinycode.dump(self)
45
+ end
46
+ end
47
+
48
+ class NilClass
49
+ def tinycode
50
+ Tinycode.dump(self)
51
+ end
52
+ end
53
+
54
+ # rubocop:enable Style/Documentation
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinycode
4
+ # Mixin module for U8, U16, U32, and U64 classes
5
+ module FixedInteger
6
+ attr_reader :value
7
+ alias to_i value
8
+ alias to_int value
9
+
10
+ def initialize(value)
11
+ raise ArgumentError, "#{value.class} cannot be converted to integer" unless value.respond_to?(:to_int)
12
+
13
+ @value = value.to_int
14
+
15
+ raise RangeError, "#{@value.inspect} out of range for #{self.class}" unless self.class.range.include?(@value)
16
+ end
17
+
18
+ def tinycode
19
+ Tinycode.dump(self)
20
+ end
21
+
22
+ def to_s
23
+ value.to_s
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class} value=#{value.inspect}>"
28
+ end
29
+
30
+ def ==(other)
31
+ value == other.value
32
+ end
33
+ end
34
+
35
+ # an unsigned integer that takes up exactly 8 bits
36
+ class U8
37
+ include FixedInteger
38
+ def self.range
39
+ 0x00..0xFF
40
+ end
41
+ end
42
+
43
+ # an unsigned integer that takes up exactly 16 bits
44
+ class U16
45
+ include FixedInteger
46
+ def self.range
47
+ 0x0000..0xFFFF
48
+ end
49
+ end
50
+
51
+ # an unsigned integer that takes up exactly 32 bits
52
+ class U32
53
+ include FixedInteger
54
+ def self.range
55
+ 0x00000000..0xFFFFFFFF
56
+ end
57
+ end
58
+
59
+ # an unsigned integer that takes up exactly 64 bits
60
+ class U64
61
+ include FixedInteger
62
+ def self.range
63
+ 0x00000000_00000000..0xFFFFFFFF_FFFFFFFF
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fixed_integer'
4
+ require_relative 'version'
5
+
6
+ # An encoder and decoder for tinycode, which is a bencode like encoding that
7
+ # supports more data-types and slightly smaller encoded sizes.
8
+ module Tinycode
9
+ class ParseError < StandardError; end
10
+
11
+ class << self
12
+ def dump(obj)
13
+ case obj
14
+ when Array then dump_array(obj)
15
+ when Hash then dump_hash(obj)
16
+ when String then dump_string(obj)
17
+ when Symbol then dump_symbol(obj)
18
+ when Integer then dump_integer(obj)
19
+ when U8 then dump_fixed_integer(obj, code: '%', pack: 'aC')
20
+ when U16 then dump_fixed_integer(obj, code: '$', pack: 'aS>')
21
+ when U32 then dump_fixed_integer(obj, code: '!', pack: 'aL>')
22
+ when U64 then dump_fixed_integer(obj, code: '#', pack: 'aQ>')
23
+ when nil then "\0".b
24
+ when true then '?T'.b
25
+ when false then '?F'.b
26
+ else raise ArgumentError, "Don't know how to dump #{obj.class}"
27
+ end
28
+ end
29
+
30
+ def load(tinycode)
31
+ raise ArgumentError, "Cannot convert #{Tinycode.class} to String" if !tinycode.respond_to?(:to_str)
32
+
33
+ tinycode = tinycode.to_str.b
34
+ struct, length = load_with_length(tinycode)
35
+ raise ParseError, 'Input is longer than expected' if length != tinycode.length
36
+
37
+ struct
38
+ end
39
+
40
+ private
41
+
42
+ def load_with_length(tinycode)
43
+ case tinycode[0]
44
+ when /[\w \t\n]/ then load_id(tinycode)
45
+ when '[' then load_array(tinycode)
46
+ when '{' then load_hash(tinycode)
47
+ when "'" then load_string(tinycode)
48
+ when ':' then load_symbol(tinycode)
49
+ when '-', '+' then load_integer(tinycode)
50
+ when '%' then load_fixed_integer(tinycode, U8, length: 1, unpack: 'C')
51
+ when '$' then load_fixed_integer(tinycode, U16, length: 2, unpack: 'S>')
52
+ when '!' then load_fixed_integer(tinycode, U32, length: 4, unpack: 'L>')
53
+ when '#' then load_fixed_integer(tinycode, U64, length: 8, unpack: 'Q>')
54
+ when "\0" then [nil, 1]
55
+ when '?' then load_boolean(tinycode)
56
+ else raise ParseError, "Unexpected character: #{tinycode[0]}"
57
+ end
58
+ end
59
+
60
+ def dump_array(ary)
61
+ "[#{ary.map(&method(:dump)).join}]".b
62
+ end
63
+
64
+ def load_array(tinycode)
65
+ offset = 1
66
+ result = []
67
+ until tinycode[offset] == ']'
68
+ value, length = load_with_length(tinycode[offset..])
69
+ result << value
70
+ offset += length
71
+ end
72
+ [result, offset + 1]
73
+ end
74
+
75
+ def dump_hash(hash)
76
+ "{#{hash.sort.flatten(1).map(&method(:dump)).join}}".b
77
+ end
78
+
79
+ def load_hash(tinycode)
80
+ offset = 1
81
+ result = {}
82
+ until tinycode[offset] == '}'
83
+ key, key_length = load_with_length(tinycode[offset..])
84
+ offset += key_length
85
+ value, value_length = load_with_length(tinycode[offset..])
86
+ offset += value_length
87
+ raise if result.key?(key)
88
+
89
+ result[key] = value
90
+ end
91
+ [result, offset + 1]
92
+ end
93
+
94
+ def dump_string(str, code: "'")
95
+ str = str.encode('UTF-8').b unless str.encoding == Encoding::ASCII_8BIT
96
+ if str =~ /\A[\w \t\n]+\z/
97
+ [str, code].pack('a*a')
98
+ else
99
+ [code, str.length, str].pack('awa*')
100
+ end
101
+ end
102
+
103
+ def load_id(tinycode)
104
+ length = tinycode.index(/[':]/)
105
+ value = tinycode[...length]
106
+ value = if tinycode[length] == ':'
107
+ value.to_sym
108
+ else
109
+ value.force_encoding('UTF-8')
110
+ end
111
+ [value, length + 1]
112
+ end
113
+
114
+ def load_string(tinycode)
115
+ str_length, offset = load_varint(tinycode[1..])
116
+ [tinycode[(1 + offset)...(1 + offset + str_length)].force_encoding('UTF-8'), 1 + offset + str_length]
117
+ end
118
+
119
+ def dump_symbol(sym)
120
+ dump_string(sym.name, code: ':')
121
+ end
122
+
123
+ def load_symbol(sym)
124
+ value, offset = load_string(sym)
125
+ [value.to_sym, offset]
126
+ end
127
+
128
+ def dump_integer(num)
129
+ if num < 0
130
+ ['-', -num].pack('aw')
131
+ else
132
+ ['+', num].pack('aw')
133
+ end
134
+ end
135
+
136
+ def load_integer(tinycode)
137
+ value, offset = load_varint(tinycode[1..])
138
+ value = -value if tinycode[0] == '-'
139
+
140
+ [value, 1 + offset]
141
+ end
142
+
143
+ def dump_fixed_integer(num, code:, pack:)
144
+ [code, num].pack(pack)
145
+ end
146
+
147
+ def load_fixed_integer(tinycode, clazz, length:, unpack:)
148
+ [clazz.new(tinycode[1...(1 + length)].unpack1(unpack)), 1 + length]
149
+ end
150
+
151
+ def load_varint(tinycode)
152
+ length = tinycode.each_byte.lazy.take_while {|b| b > 0x7F }.count + 1
153
+ [tinycode[...length].unpack1('w'), length]
154
+ end
155
+
156
+ def load_boolean(tinycode)
157
+ case tinycode[1]
158
+ when 'T' then [true, 2]
159
+ when 'F' then [false, 2]
160
+ else raise ParseError, 'Malformed boolean'
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'module'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tinycode
4
+ VERSION = '0.1.0'
5
+ end
data/lib/tinycode.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tinycode/core_ext'
4
+ require_relative 'tinycode/module'
data/tinycode.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/tinycode/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'tinycode'
7
+ spec.version = Tinycode::VERSION
8
+ spec.summary = 'An encoder and decoder for tinycode'
9
+ spec.description = 'tinycode is a bencode like encoding that supports more ' \
10
+ 'data-types and slightly smaller encoded sizes.'
11
+ spec.authors = ['Alex Gittemeier']
12
+ spec.license = 'GPL-3.0'
13
+ spec.email = ['me@a.lexg.dev']
14
+ spec.metadata['source_code_uri'] = 'https://gitlab.com/windows93/tinycode'
15
+ spec.metadata['rubygems_mfa_required'] = 'true'
16
+ # spec.metadata['changelog_uri'] =
17
+
18
+ spec.homepage = spec.metadata['source_code_uri']
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) {|f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.required_ruby_version = '>= 3.0.0'
33
+
34
+ spec.add_development_dependency 'rake', '~> 13.0'
35
+ spec.add_development_dependency 'rerun', '~> 0.13'
36
+ spec.add_development_dependency 'rspec', '~> 3.0'
37
+ spec.add_development_dependency 'rubocop', '~> 1.21'
38
+ spec.add_development_dependency 'rubocop-rake', '~> 0.6'
39
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.6'
40
+ spec.add_development_dependency 'simplecov', '~> 0.21'
41
+ spec.add_development_dependency 'yard', '~> 0.9'
42
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tinycode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Gittemeier
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rerun
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.21'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.21'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.6'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.21'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.21'
111
+ - !ruby/object:Gem::Dependency
112
+ name: yard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.9'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.9'
125
+ description: tinycode is a bencode like encoding that supports more data-types and
126
+ slightly smaller encoded sizes.
127
+ email:
128
+ - me@a.lexg.dev
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".rspec"
134
+ - ".rubocop.yml"
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - LICENSE
138
+ - README.md
139
+ - Rakefile
140
+ - lib/tinycode.rb
141
+ - lib/tinycode/core_ext.rb
142
+ - lib/tinycode/fixed_integer.rb
143
+ - lib/tinycode/module.rb
144
+ - lib/tinycode/no_core_ext.rb
145
+ - lib/tinycode/version.rb
146
+ - tinycode.gemspec
147
+ homepage: https://gitlab.com/windows93/tinycode
148
+ licenses:
149
+ - GPL-3.0
150
+ metadata:
151
+ source_code_uri: https://gitlab.com/windows93/tinycode
152
+ rubygems_mfa_required: 'true'
153
+ homepage_uri: https://gitlab.com/windows93/tinycode
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 3.0.0
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 3.2.22
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: An encoder and decoder for tinycode
173
+ test_files: []