cmf 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d43c85f9f8a13b3ae30b2e40bb7a2a3e0e3c6b4e33b66601fe3046b97ac0fff
4
+ data.tar.gz: 60418ee593ef0ccb2ce0703391c0069416052d3de8197b26502cc2000e065f9e
5
+ SHA512:
6
+ metadata.gz: 501e955c266464d3511b50baf33cd48b0dc054db355f4ebad9c830549ce2c9c9fbec2bc60dd5ce0ccba13ac415f2fc1f6aa57f4d16a3b22ea5e7dcfe90edf522
7
+ data.tar.gz: '0597cae488986a886be443187350af2f385dd5e5a7b635942b64e6378d0d49a89051fe24b2fe282bff05be21b02692300e463a4f35b22b16a9d3fe9d617e528f'
checksums.yaml.gz.sig ADDED
Binary file
data.tar.gz.sig ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .ruby-version
2
+ .ruby-gemset
3
+ Gemfile.lock
4
+ *.gem
5
+ coverage/
6
+ doc/
7
+ .yardoc/
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ script: "rspec"
2
+ language: "ruby"
3
+ sudo: false
4
+ rvm:
5
+ - 2.0
6
+ - 2.1
7
+ - 2.2
8
+ - 2.3
9
+ - 2.4
10
+ - 2.5
11
+ - jruby
data/.yardopts ADDED
@@ -0,0 +1,7 @@
1
+ --readme README.md
2
+ --markup markdown
3
+ lib/cmf.rb
4
+ lib/cmf/*.rb
5
+ -
6
+ CHANGELOG.md
7
+ LICENSE
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ Change log
2
+ ====
3
+
4
+ This gem follows [Semantic Versioning 2.0.0](http://semver.org/spec/v2.0.0.html).
5
+ All classes and public methods are part of the public API.
6
+
7
+ 1.0.0
8
+ ----
9
+ Released on 2018-04-10
10
+
11
+ All core functionality is implemented:
12
+
13
+ - `CMF` module methods:
14
+ - `CMF.build`
15
+ - `CMF.build_hex`
16
+ - `CMF.parse`
17
+ - `CMF.parse_hex`
18
+ - `CMF::Builder` class, with the following methods:
19
+ - `add`
20
+ - `add_bool`
21
+ - `add_bytes`
22
+ - `add_double` (also aliased as `add_float`)
23
+ - `add_int`
24
+ - `add_string`
25
+ - `reset`
26
+ - `to_hex`
27
+ - `to_octet`
28
+ - `dictionary` read-only attribute
29
+ - `CMF::Parser` class, with the following methods:
30
+ - `each`
31
+ - `message=`
32
+ - `message_hex=`
33
+ - `next_pair`
34
+ - `parse`
35
+ - `parse_hex`
36
+ - `inverted_dictionary` read-only attribute
37
+ - `CMF::Varint` class, with the following methods:
38
+ - `deserialize`
39
+ - `serialize`
40
+ - `CMF::Dictionary.validate` helper method
41
+ - `CMF::Type` constants
42
+ - `MalformedMessageError` class
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2018 Stephen McCarthy
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # CMF
2
+
3
+ [![Build Status](https://travis-ci.org/jamoes/cmf.svg?branch=master)](https://travis-ci.org/jamoes/cmf)
4
+
5
+ ## Description
6
+
7
+ This library builds and parses messages in the [Compact Message Format (CMF)](http://flowee.org/docs/api/protocol-spec/).
8
+
9
+ CMF is a binary format that has the speed and compact size of a binary format combined with the provably correct markup and type-safety of formats like XML and JSON.
10
+
11
+ A CMF message is a flat list of tokens. Each token is comprised of 3 elements: a tag name, a type, and a value. Tag names are written to the message as numbers, so an external schema dictionary is typically used to map the tag numbers to names.
12
+
13
+ Each CMF token is completely self-contained. Even a reader that doesn't know the schema of the message it is parsing can still extract all tokens from the message. This makes the format exceptionally useful for extensibility because a reader can just skip over unknown tokens.
14
+
15
+ ## Installation
16
+
17
+ This library is distributed as a gem named [cmf](https://rubygems.org/gems/cmf)
18
+ at RubyGems.org. To install it, run:
19
+
20
+ gem install cmf
21
+
22
+ ## Usage
23
+
24
+ First, require the gem:
25
+
26
+ ```ruby
27
+ require 'cmf'
28
+ ```
29
+
30
+ Next, we'll build and parse a simple message with two tokens. The message will contain the string `"Proxima Centauri"`, associated with the tag `0`, and the floating point number `4.2421` associated with the tag `1`.
31
+
32
+ ```ruby
33
+ message = CMF.build({0 => "Proxima Centauri", 1 => 4.2421})
34
+ # => "\x02\x10Proxima Centauri\x0EGr\xF9\x0F\xE9\xF7\x10@"
35
+
36
+ CMF.parse(message)
37
+ # => {0=>"Proxima Centauri", 1=>4.2421}
38
+ ```
39
+
40
+ Rather than using the tags `0`, and `1`, we can define a schema dictionary which maps human-readable names to tag numbers.
41
+
42
+ ```ruby
43
+ dictionary = {star: 0, distance: 1}
44
+ message = CMF.build({star: "Proxima Centauri", distance: 4.2421}, dictionary)
45
+ # => "\x02\x10Proxima Centauri\x0EGr\xF9\x0F\xE9\xF7\x10@"
46
+
47
+ CMF.parse(message, dictionary)
48
+ # => {:star=>"Proxima Centauri", :distance=>4.2421}
49
+ ```
50
+
51
+ For more flexibility in parsing and building messages, you can use the `CMF::Builder` and `CMF::Parser` classes.
52
+
53
+ ```ruby
54
+ builder = CMF::Builder.new(dictionary)
55
+ builder.add(:star, "Proxima Centauri")
56
+ builder.add(:distance, 4.2421)
57
+ message = builder.to_octet
58
+
59
+ parser = CMF::Parser.new(dictionary)
60
+ parser.message = message
61
+ parser.next_pair # => [:star, "Proxima Centauri"]
62
+ parser.next_pair # => [:distance, 4.2421]
63
+ parser.next_pair # => nil
64
+
65
+ parser.message = message ##
66
+ parser.each do |tag, value| # star: Proxima Centauri
67
+ puts "#{tag}: #{value}" # distance: 4.2421
68
+ end ##
69
+ ```
70
+ ## Supported platforms
71
+
72
+ Ruby 2.0 and above, including jruby.
73
+
74
+ ## Documentation
75
+
76
+ For complete documentation, see the [CMF page on RubyDoc.info](http://rubydoc.info/gems/cmf).
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ task 'default' => 'spec'
2
+
3
+ desc 'Run specs'
4
+ task 'spec' do
5
+ sh 'rspec'
6
+ end
7
+
8
+ desc 'Run specs and generate coverage report'
9
+ task 'coverage' do
10
+ ENV['COVERAGE'] = 'Y'
11
+ Rake::Task['spec'].invoke
12
+ end
13
+
14
+ desc 'Print out lines of code and related statistics.'
15
+ task 'stats' do
16
+ puts 'Lines of code and comments (including blank lines):'
17
+ sh "find lib -type f | xargs wc -l"
18
+ puts "\nLines of code (excluding comments and blank lines):"
19
+ sh "find lib -type f | xargs cat | sed '/^\s*#/d;/^\s*$/d' | wc -l"
20
+ end
21
+
22
+ desc 'Generate documentation'
23
+ task 'doc' do
24
+ sh 'yardoc'
25
+ end
data/certs/jamoes.pem ADDED
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBBMRMwEQYDVQQDDApzam1j
3
+ Y2FydGh5MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj
4
+ b20wHhcNMTgwNDExMDAxMzI4WhcNMTkwNDExMDAxMzI4WjBBMRMwEQYDVQQDDApz
5
+ am1jY2FydGh5MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
6
+ FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOk2n/6xgIgrG7
7
+ avtiI8I9DtcdA326qWYpdQSDLhpSsLNiqiIpo8KF1Zfy3lnAj6JBBIbjiaUsbA/i
8
+ Wcip0307dHNXZjr+AgYcL7OEp8EBkfAeZaYWMcVBbjiSxkzYesDxm7nvTOaD317h
9
+ cThBfB9KW1vGEzazomTxSI9sgqCDtWrogMLGag7uTDJ7fKRK6YXz2xncI0uCsmGb
10
+ 7vekXpfn0xb6tr4ljSseCsPJHnXK7SKB4dzHsmQJ12A57aaV7C/bGqbQAC6odb6k
11
+ V8dw0fnmHC9OSYjV1b2Xr0VmoiT3YA4XsR0/LbeZvGOyQj8S4eHxgFg7wTVhCkCZ
12
+ D89+p8H5AgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
13
+ BBQffCJK6PE+9XaH56VJoFoCl3ECeDAfBgNVHREEGDAWgRRzam1jY2FydGh5QGdt
14
+ YWlsLmNvbTAfBgNVHRIEGDAWgRRzam1jY2FydGh5QGdtYWlsLmNvbTANBgkqhkiG
15
+ 9w0BAQUFAAOCAQEApvFGCB9uyF1mh1UV77YICagARejAIOhzOcZXjlpulI9xXjQY
16
+ 0QK6P1GdwwE/pgT7YjfJR7VNFobare4WdfCzoWCFc34t2vJwrqkkOB3U7v3TjB+p
17
+ z/o2pZKLpNEL4bYJBEbd+vAad/nP1v5e2sCmLm86vSoOwiyQnifmP6PSORObbJF4
18
+ 455zxYw1un6NfN0m+pnIKwvshKoOCgI05VJGtEolJoo42fnolmNxa2t6B30Mfmf+
19
+ kts216EGG4oP6dVuZmf2Ii2F4lQTBDdZM/cisW8jCkO7KeEzJAPhIw1JJwHltHya
20
+ 0TpOI3t2Mz/FJ+rudtz9PJ/d8QvhrF7M7+qH4w==
21
+ -----END CERTIFICATE-----
data/cmf.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ require File.expand_path('../lib/cmf/version', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'cmf'
5
+ s.version = CMF::VERSION
6
+ s.authors = ['Stephen McCarthy']
7
+ s.email = 'sjmccarthy@gmail.com'
8
+ s.summary = 'Builds and parses messages in the Compact Message Format (CMF)'
9
+ s.homepage = 'https://github.com/jamoes/cmf'
10
+ s.license = 'MIT'
11
+
12
+ s.cert_chain = ['certs/jamoes.pem']
13
+ s.signing_key = File.expand_path("~/.ssh/gem-private_key.pem") if $0 =~ /gem\z/
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ s.add_development_dependency 'bundler', '~> 1'
20
+ s.add_development_dependency 'rake', '~> 12'
21
+ s.add_development_dependency 'rspec', '~> 3.7'
22
+ s.add_development_dependency 'simplecov', '~> 0'
23
+ s.add_development_dependency 'yard', '~> 0.9.12'
24
+ s.add_development_dependency 'markdown', '~> 1'
25
+ s.add_development_dependency 'redcarpet', '~> 3' unless RUBY_PLATFORM == 'java'
26
+ end
data/lib/cmf.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'cmf/builder.rb'
2
+ require 'cmf/dictionary.rb'
3
+ require 'cmf/malformed_message_error.rb'
4
+ require 'cmf/parser.rb'
5
+ require 'cmf/type.rb'
6
+ require 'cmf/varint.rb'
7
+ require 'cmf/version.rb'
8
+
9
+ # The top-level module for the cmf gem.
10
+ module CMF
11
+ # Builds a CMF message from an object.
12
+ #
13
+ # @param obj [Hash,#each] The object to be built into a CMF message. Can be
14
+ # a hash, or any object that responds to `.each` and yields (tag, value)
15
+ # pairs.
16
+ # @param dictionary [Hash,Array] Optional. The dictionary mapping tag
17
+ # names to numbers. See {Dictionary.validate}.
18
+ # @return [String] An octet string, each character representing one byte of
19
+ # the CMF message.
20
+ def self.build(obj, dictionary = nil)
21
+ Builder.new(dictionary).build(obj).to_octet
22
+ end
23
+
24
+ # Builds hex-encoded a CMF message from an object.
25
+ #
26
+ # @see #CMF.build
27
+ # @return [String] A hex string, every 2 characters representing one byte of
28
+ # the CMF message.
29
+ def self.build_hex(obj, dictionary = nil)
30
+ Builder.new(dictionary).build(obj).to_hex
31
+ end
32
+
33
+ # Parses a CMF message into an object.
34
+ #
35
+ # @param message [String] A CMF message.
36
+ # @return [Hash] See {Parser.parse}.
37
+ def self.parse(message, dictionary = nil)
38
+ Parser.new(dictionary).parse(message)
39
+ end
40
+
41
+ # Parses a hex-encoded CMF message into an object.
42
+ #
43
+ # @param message_hex [String] A hex-encoded CMF message.
44
+ # @return [Hash] See {Parser.parse}.
45
+ def self.parse_hex(message_hex, dictionary = nil)
46
+ Parser.new(dictionary).parse_hex(message_hex)
47
+ end
48
+ end
@@ -0,0 +1,209 @@
1
+ module CMF
2
+
3
+ # Instances of the `Builder` class can create CMF messages.
4
+ #
5
+ # Basic usage:
6
+ #
7
+ # b = CMF::Builder.new
8
+ # b.add(0, "value0")
9
+ # b.add(1, "value1")
10
+ #
11
+ # The CMF message can be output in octet form (one character per byte):
12
+ #
13
+ # b.to_octet
14
+ #
15
+ # or hex form (two characters per byte):
16
+ #
17
+ # b.to_hex
18
+ #
19
+ # Method calls can be chained together:
20
+ #
21
+ # CMF::Builder.new.add(0, "value0").add(1, "value1").to_hex
22
+ #
23
+ # A dictionary can be used to refer to tags by name rather than number:
24
+ #
25
+ # b = CMF::Builder.new([:tag0, :tag1])
26
+ # b.add(:tag0, "value0")
27
+ # b.add(:tag1, "value1")
28
+ #
29
+ # Messages can be built from an object:
30
+ #
31
+ # b = CMF::Builder.new([:tag0, :tag1])
32
+ # b.build({tag0: "value0", tag1: "value1"})
33
+ class Builder
34
+ # @return {Hash} The dictionary mapping tag names to numbers.
35
+ attr_reader :dictionary
36
+
37
+ # Creates a new instance of {Builder}.
38
+ #
39
+ # @param dictionary [Hash,Array] Optional. The dictionary mapping tag
40
+ # names to numbers. See {Dictionary.validate}.
41
+ def initialize(dictionary = nil)
42
+ @dictionary = Dictionary.validate(dictionary)
43
+ reset
44
+ end
45
+
46
+ # Resets the CMF message.
47
+ #
48
+ # @return [Builder] self
49
+ def reset
50
+ @io = StringIO.new(String.new) # A StringIO with ASCII-8BIT encoding
51
+
52
+ self
53
+ end
54
+
55
+ # Adds multiple (tag, value) pairs to the CMF message.
56
+ #
57
+ # @param obj [Hash,#each] The object containing (tag, value) pairs. Can be
58
+ # a hash, or any object that responds to `.each` and yields
59
+ # (tag, value) pairs. Calls {add} for each (tag, value) pair.
60
+ # @return [Builder] self
61
+ # @see #add
62
+ def build(obj)
63
+ obj.each do |key, values|
64
+ Array(values).each do |value|
65
+ add(key, value)
66
+ end
67
+ end
68
+
69
+ self
70
+ end
71
+
72
+ # Adds a (tag, value) pair to the CMF message.
73
+ #
74
+ # @param tag [Integer,Object] Must be an integer or a key in the
75
+ # dictionary.
76
+ # @param value [String,Integer,Boolean,Float,Object] A string, integer,
77
+ # boolean, or float. All other types will be converted to a string by
78
+ # calling the `to_s` method on them. Strings with binary (ASCII-8BIT)
79
+ # encoding will be added as a {Type::BYTE_ARRAY}. All other strings
80
+ # will be added as a {Type::STRING}.
81
+ # @return [Builder] self
82
+ def add(tag, value)
83
+ case value
84
+ when Integer
85
+ add_int(tag, value)
86
+ when String
87
+ if value.encoding == Encoding::BINARY
88
+ add_bytes(tag, value)
89
+ else
90
+ add_string(tag, value)
91
+ end
92
+ when TrueClass, FalseClass
93
+ add_bool(tag, value)
94
+ when Float
95
+ add_double(tag, value)
96
+ else
97
+ add_string(tag, value)
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ # Adds a (tag, integer value) pair to the CMF message.
104
+ #
105
+ # @param tag [Integer,Object] Must be an integer or a key in the dictionary.
106
+ # @param value [Integer,Object] An integer value. Non-integer values will be
107
+ # converted to integers by calling the `to_i` method on them.
108
+ # @return [Builder] self
109
+ def add_int(tag, value)
110
+ value = value.to_i
111
+ type = Type::POSITIVE_NUMBER
112
+ if value < 0
113
+ type = Type::NEGATIVE_NUMBER
114
+ value *= -1
115
+ end
116
+
117
+ write_tag(tag, type)
118
+ Varint.serialize(@io, value.abs)
119
+
120
+ self
121
+ end
122
+
123
+ # Adds a (tag, string value) pair to the CMF message.
124
+ #
125
+ # @param tag [Integer,Object] Must be an integer or a key in the dictionary.
126
+ # @param value [String,Object] A string value. Non-string values will be
127
+ # converted to strings by calling the `to_s` method on them.
128
+ # @return [Builder] self
129
+ def add_string(tag, value)
130
+ value = value.to_s
131
+ write_tag(tag, Type::STRING)
132
+ Varint.serialize(@io, value.bytesize)
133
+ @io << value
134
+
135
+ self
136
+ end
137
+
138
+ # Adds a (tag, byte_array value) pair to the CMF message.
139
+ #
140
+ # @param tag [Integer,Object] Must be an integer or a key in the dictionary.
141
+ # @param value [String,Object] A string value. Non-string values will be
142
+ # converted to strings by calling the `to_s` method on them.
143
+ # @return [Builder] self
144
+ def add_bytes(tag, value)
145
+ value = value.to_s
146
+ write_tag(tag, Type::BYTE_ARRAY)
147
+ Varint.serialize(@io, value.bytesize)
148
+ @io << value
149
+
150
+ self
151
+ end
152
+
153
+ # Adds a (tag, boolean value) pair to the CMF message.
154
+ #
155
+ # @param tag [Integer,Object] Must be an integer or a key in the dictionary.
156
+ # @param value [Boolean,Object] A boolean value. Non-boolean values will be
157
+ # converted to boolean by testing their truthiness.
158
+ # @return [Builder] self
159
+ def add_bool(tag, value)
160
+ write_tag(tag, value ? Type::BOOL_TRUE : Type::BOOL_FALSE)
161
+
162
+ self
163
+ end
164
+
165
+ # Adds a (tag, float value) pair to the CMF message.
166
+ #
167
+ # @param tag [Integer,Object] Must be an integer or a key in the dictionary.
168
+ # @param value [Float,Object] A float value. Non-float values will be
169
+ # converted to floats by calling the `to_f` method on them.
170
+ # @return [Builder] self
171
+ def add_double(tag, value)
172
+ write_tag(tag, Type::DOUBLE)
173
+ @io << [value.to_f].pack('E')
174
+
175
+ self
176
+ end
177
+ alias_method :add_float, :add_double
178
+
179
+ # @return [String] An octet string, each character representing one byte
180
+ # of the CMF message.
181
+ def to_octet
182
+ @io.string
183
+ end
184
+
185
+ # @return [String] A hex string, every 2 characters representing one byte
186
+ # of the CMF message.
187
+ def to_hex
188
+ to_octet.unpack('H*').first
189
+ end
190
+
191
+ private
192
+
193
+ def write_tag(tag, type)
194
+ if !tag.is_a?(Integer)
195
+ @dictionary[tag] or raise ArgumentError, "Tag '#{tag}' not found in dictionary"
196
+ tag = @dictionary[tag]
197
+ end
198
+ tag >= 0 or raise ArgumentError, "Invalid tag value #{tag}. Must be >= 0"
199
+ (type >= 0 && type <= 6) or raise ArgumentError, "Invalid type"
200
+
201
+ if tag >= 31
202
+ @io.putc(type | 0xF8)
203
+ Varint.serialize(@io, tag)
204
+ else
205
+ @io.putc((tag << 3) + type)
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,45 @@
1
+ module CMF
2
+
3
+ # Provides functionality for validating the `dictionary` argument passed to {Builder} and {Parser}.
4
+ module Dictionary
5
+
6
+ # Validates a dictionary, and optionally converts it from array form to
7
+ # hash form.
8
+ #
9
+ # @param dictionary [Hash,Array] The dictionary mapping tag names to
10
+ # numbers. For example:
11
+ #
12
+ # {name: 0, address: 1, email: 2}
13
+ #
14
+ # Arrays will be converted to hashes with each array value mapping to
15
+ # its index. The following is equivalent to the above example:
16
+ #
17
+ # [:name, :address, :email]
18
+ #
19
+ # @return [Hash] A dictionary mapping tag names to numbers.
20
+ #
21
+ # @raise [TypeError] if any dictionary keys are integers.
22
+ # @raise [TypeError] if any dictionary values are not integers.
23
+ # @raise [ArgumentError] if dictionary values are not unique.
24
+ # @raise [ArgumentError] if any dictionary values are negative.
25
+ def self.validate(dictionary)
26
+ return {} if dictionary.nil?
27
+
28
+ if dictionary.is_a? Array
29
+ dictionary = dictionary.map.with_index {|s, i| [s, i]}.to_h
30
+ end
31
+
32
+ dictionary.is_a? Hash or raise TypeError, "Dictionary must be an Array or Hash"
33
+ dictionary.keys.each do |k|
34
+ !k.is_a?(Integer) or raise TypeError, "Invalid dictionary key #{k}. Must not be an integer"
35
+ end
36
+ dictionary.values.each do |v|
37
+ v.is_a?(Integer) or raise TypeError, "Invalid dictionary value #{v}. Must all be an integer"
38
+ v >= 0 or raise ArgumentError, "Invalid dictionary value #{v}. Must be >= 0"
39
+ end
40
+ dictionary.values.size == dictionary.values.uniq.size or raise ArgumentError, "Dictionary values must be unique"
41
+
42
+ dictionary
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ module CMF
2
+ # Raised whenever the {Parser} encounters a malformed CMF message.
3
+ class MalformedMessageError < StandardError
4
+ end
5
+ end
data/lib/cmf/parser.rb ADDED
@@ -0,0 +1,164 @@
1
+ module CMF
2
+
3
+ # Instances of the `Parser` class can parse CMF messages.
4
+ #
5
+ # Basic usage:
6
+ #
7
+ # p = CMF::Parser.new
8
+ # p.parse("\x04\r") # {0=>true, 1=>false}
9
+ # p.parse_hex("040d") # {0=>true, 1=>false}
10
+ #
11
+ # Using a dictionary:
12
+ #
13
+ # p = CMF::Parser.new([:tag0, :tag1])
14
+ # p.parse_hex("040d") # {:tag0=>true, :tag1=>false}
15
+ #
16
+ # If a tag is found multiple times in the message, its values will be stored
17
+ # in an array in the parsed object.
18
+ #
19
+ # CMF::Parser.new.parse_hex("040504") # {0=>[true, false, true]}
20
+ #
21
+ # Using {next_pair}:
22
+ #
23
+ # p = CMF::Parser.new
24
+ # p.message_hex = "040d"
25
+ # p.next_pair # [0, true]
26
+ # p.next_pair # [1, false]
27
+ # p.next_pair # nil
28
+ #
29
+ # Using {each}:
30
+ #
31
+ # p = CMF::Parser.new
32
+ # p.messge_hex = "040d"
33
+ # p.each { |k, v| puts "#{k}: #{v}" }
34
+ class Parser
35
+ # @return {Hash} The inverted dictionary, mapping numbers to tag names.
36
+ attr_reader :inverted_dictionary
37
+
38
+ # Creates a new instance of {Parser}.
39
+ #
40
+ # @param dictionary [Hash,Array] Optional. The dictionary mapping tag
41
+ # names to numbers. See {Dictionary.validate}.
42
+ def initialize(dictionary = nil)
43
+ @inverted_dictionary = Dictionary.validate(dictionary).invert
44
+ @io = StringIO.new
45
+ end
46
+
47
+ # Sets a new CMF message.
48
+ #
49
+ # @param message [String] The CMF message in octet form.
50
+ def message=(message)
51
+ @io = StringIO.new(message)
52
+ end
53
+
54
+ # Sets a new CMF message from a hex string.
55
+ #
56
+ # @param message_hex [String] The hex-encoded CMF message.
57
+ def message_hex=(message_hex)
58
+ self.message = [message_hex].pack('H*')
59
+ end
60
+
61
+ # Parses a CMF message into an object.
62
+ #
63
+ # @param message [String] The message to parse. If none provided, this
64
+ # parser's existing message (defined from {message=} or {message_hex=}
65
+ # will be parsed.
66
+ # @return [Hash] A hash mapping the messages tags to their values. For each
67
+ # tag, if the tag number is found in the dictionary, its associated
68
+ # tag name will be used as hash key. If a tag is found multiple times
69
+ # in the message, its values will be stored in an array in the parsed
70
+ # object.
71
+ def parse(message = nil)
72
+ self.message = message if message
73
+
74
+ obj = {}
75
+ each do |key, value|
76
+ if obj.has_key?(key)
77
+ obj[key] = Array(obj[key])
78
+ obj[key] << value
79
+ else
80
+ obj[key] = value
81
+ end
82
+ end
83
+
84
+ obj
85
+ end
86
+
87
+ # Parses a hex-encoded CMF message into an object.
88
+ #
89
+ # @param message_hex [String] A hex-encoded CMF message.
90
+ # @return [Hash] See {parse}.
91
+ def parse_hex(message_hex)
92
+ self.message_hex = message_hex
93
+
94
+ parse
95
+ end
96
+
97
+ # Calls the given block once for each pair found in the message.
98
+ #
99
+ # @yieldparam [Integer,Object] tag The pair's tag. An integer, or the
100
+ # associated value found in the dictionary.
101
+ # @yieldparam [String,Integer,Boolean,Float] value The pair's value.
102
+ # @return [Enumerator] If no block is given.
103
+ # @see #next_pair
104
+ def each
105
+ return to_enum(:each) unless block_given?
106
+ loop do
107
+ pair = next_pair
108
+ break if pair.nil?
109
+ yield(pair[0], pair[1])
110
+ end
111
+ end
112
+
113
+ # Returns the next pair in the message, or nil if the whole message has been
114
+ # parsed.
115
+ #
116
+ # @return [Array,nil] A (tag, value) pair. The tag will be an integer, or
117
+ # the associated value found in the dictionary. The value will be
118
+ # converted to the corresponding type defined in the message. If the
119
+ # value's type is {Type::STRING}, the value will be a string with UTF-8
120
+ # encoding. If the value's type is {Type::BYTE_ARRAY}, the value will
121
+ # be a string with binary (ASCII-8BIT) encoding.
122
+ #
123
+ # @raise [MalformedMessageError] if the CMF message cannot be parsed, or if
124
+ # it is malformed in any way.
125
+ def next_pair
126
+ return nil if @io.eof?
127
+
128
+ byte = @io.getbyte
129
+ type = byte & 0x07
130
+ tag = byte >> 3
131
+ if tag == 31
132
+ tag = Varint.deserialize(@io)
133
+ end
134
+
135
+ if @inverted_dictionary[tag]
136
+ tag = @inverted_dictionary[tag]
137
+ end
138
+
139
+ case type
140
+ when Type::POSITIVE_NUMBER
141
+ [tag, Varint.deserialize(@io)]
142
+ when Type::NEGATIVE_NUMBER
143
+ [tag, -Varint.deserialize(@io)]
144
+ when Type::STRING, Type::BYTE_ARRAY
145
+ length = Varint.deserialize(@io)
146
+ s = @io.read(length)
147
+ s.bytesize == length or raise MalformedMessageError, "Unexpected end of stream"
148
+ s = s.force_encoding(Encoding::UTF_8) if type == Type::STRING
149
+ [tag, s]
150
+ when Type::BOOL_TRUE
151
+ [tag, true]
152
+ when Type::BOOL_FALSE
153
+ [tag, false]
154
+ when Type::DOUBLE
155
+ s = @io.read(8)
156
+ s.bytesize == 8 or raise MalformedMessageError, "Unexpected end of stream"
157
+ [tag, s.unpack('E').first]
158
+ else
159
+ raise MalformedMessageError, "Unknown type"
160
+ end
161
+
162
+ end
163
+ end
164
+ end
data/lib/cmf/type.rb ADDED
@@ -0,0 +1,27 @@
1
+ module CMF
2
+
3
+ # Defines the enum value for all types that can be stored in a CMF message.
4
+ module Type
5
+ # Varint encoded integer.
6
+ POSITIVE_NUMBER = 0
7
+
8
+ # The value is multiplied by -1 and then serialized in the same manner
9
+ # as a POSITIVE_NUMBER.
10
+ NEGATIVE_NUMBER = 1
11
+
12
+ # Varint length (in bytes) first, then the UTF-8 encoded string.
13
+ STRING = 2
14
+
15
+ # Varint length (in bytes) first, then the binary encoded string.
16
+ BYTE_ARRAY = 3
17
+
18
+ # True value, no additional data stored.
19
+ BOOL_TRUE = 4
20
+
21
+ # False value, no additional data stored.
22
+ BOOL_FALSE = 5
23
+
24
+ # Double-precision (8 bytes) little-endian floating-point number.
25
+ DOUBLE = 6
26
+ end
27
+ end
data/lib/cmf/varint.rb ADDED
@@ -0,0 +1,45 @@
1
+ module CMF
2
+ # Provides functionaly for serializing and deserializing variable-width
3
+ # encoded integers (varint).
4
+ class Varint
5
+
6
+ # Serializes an integer into a varint.
7
+ #
8
+ # @param io [StringIO] The IO stream where the serialized varint will be
9
+ # written.
10
+ # @param n [Integer] The integer to serialize.
11
+ # @return [nil]
12
+ def self.serialize(io, n)
13
+ n.is_a?(Integer) or raise TypeError, "Invalid Varint value #{n}. Must be an integer"
14
+ n >= 0 or raise ArgumentError, "Invalid Varint value #{n}. Must be >= 0"
15
+
16
+ data = []
17
+ mask = 0
18
+ begin
19
+ data.push((n & 0x7F) | mask)
20
+ n = (n >> 7) - 1
21
+ mask = 0x80
22
+ end while n >= 0
23
+
24
+ data.reverse_each do |byte|
25
+ io.putc(byte)
26
+ end
27
+
28
+ nil
29
+ end
30
+
31
+ # Deserializes a varint into a integer.
32
+ #
33
+ # @param io [StringIO] The IO stream that will be read from to deserialize.
34
+ # @return [Integer] The deserialized integer.
35
+ def self.deserialize(io)
36
+ result = 0
37
+ io.each_byte do |byte|
38
+ result = (result << 7) | (byte & 0x7F)
39
+ return result if (byte & 0x80) == 0
40
+ result += 1
41
+ end
42
+ raise CMF::MalformedMessageError, "Unexpected end of stream"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,4 @@
1
+ module CMF
2
+ # This gem's version.
3
+ VERSION = '1.0.0'
4
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'messages'
3
+
4
+ describe CMF::Builder do
5
+
6
+ MESSAGES.each do |obj, encoded_output, dictionary|
7
+ it "Builds message #{encoded_output}" do
8
+ expect(CMF.build_hex(obj, dictionary)).to eq encoded_output
9
+ end
10
+ end
11
+
12
+ it 'Builds octet message' do
13
+ expect(CMF.build({0 => false})).to eq("\x05".force_encoding(Encoding::BINARY))
14
+ end
15
+
16
+ describe '#reset' do
17
+ it 'resets the message' do
18
+ builder = CMF::Builder.new
19
+ builder.add(0, 0)
20
+ builder.reset
21
+ expect(builder.to_hex).to eq ''
22
+ end
23
+ end
24
+
25
+ describe '#add' do
26
+ it 'raises when tag not found in dictionary' do
27
+ expect { CMF::Builder.new.add(:tag, 0) }.to raise_error(ArgumentError)
28
+ end
29
+
30
+ it 'raises when tag is negative' do
31
+ expect { CMF::Builder.new.add(-1, 0) }.to raise_error(ArgumentError)
32
+ end
33
+
34
+ it 'accepts chained calls' do
35
+ expect(CMF::Builder.new.add(0, true).add(0, false).to_hex).to eq "0405"
36
+ end
37
+
38
+ it 'converts unknown input type to a string' do
39
+ expect(CMF::Builder.new.add(0, []).to_hex).to eq "02025b5d"
40
+ end
41
+ end
42
+
43
+ describe '#add_int' do
44
+ it 'converts non-int input to an int' do
45
+ expect(CMF::Builder.new.add_int(15, "6512").to_hex).to eq "78b170"
46
+ expect(CMF::Builder.new.add_int(15, 6512.1).to_hex).to eq "78b170"
47
+ end
48
+ end
49
+
50
+ describe '#add_string' do
51
+ it 'converts non-string input to a string' do
52
+ expect(CMF::Builder.new.add_string(0, []).to_hex).to eq "02025b5d"
53
+ end
54
+ end
55
+
56
+ describe '#add_bytes' do
57
+ it 'converts non-string input to a string' do
58
+ expect(CMF::Builder.new.add_bytes(0, []).to_hex).to eq "03025b5d"
59
+ end
60
+ end
61
+
62
+ describe '#add_bool' do
63
+ it 'converts non-bool input to a bool' do
64
+ expect(CMF::Builder.new.add_bool(0, []).to_hex).to eq "04"
65
+ expect(CMF::Builder.new.add_bool(0, nil).to_hex).to eq "05"
66
+ end
67
+ end
68
+
69
+ describe '#add_double' do
70
+ it 'converts non-double input to a double' do
71
+ expect(CMF::Builder.new.add_double(0, "1.1").to_hex).to eq "069a9999999999f13f"
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMF::Dictionary do
4
+ describe '#validate' do
5
+ it 'validates nil dictionary' do
6
+ expect(CMF::Dictionary.validate(nil)).to eq({})
7
+ end
8
+
9
+ it 'validates array dictionary' do
10
+ expect(CMF::Dictionary.validate([:tag0, :tag1, :tag2])).to eq({tag0: 0, tag1: 1, tag2: 2})
11
+ end
12
+
13
+ it 'validates hash dictionary' do
14
+ expect(CMF::Dictionary.validate({tag0: 0, tag1: 1, tag2: 2})).to eq({tag0: 0, tag1: 1, tag2: 2})
15
+ end
16
+
17
+ it 'does not allow integer keys' do
18
+ expect { CMF::Dictionary.validate({100 => 0, tag1: 1}) }.to raise_error(TypeError)
19
+ end
20
+
21
+ it 'requires integer values' do
22
+ expect { CMF::Dictionary.validate({tag0: 0, tag1: "foo"}) }.to raise_error(TypeError)
23
+ end
24
+
25
+ it 'does not allow duplicate values' do
26
+ expect { CMF::Dictionary.validate({tag0: 0, tag1: 0}) }.to raise_error(ArgumentError)
27
+ end
28
+
29
+ it 'does not allow duplicate values' do
30
+ expect { CMF::Dictionary.validate({tag0: 0, tag1: 0}) }.to raise_error(ArgumentError)
31
+ end
32
+
33
+ end
34
+ end
data/spec/messages.rb ADDED
@@ -0,0 +1,30 @@
1
+ MESSAGES =
2
+ [
3
+ [{15 => 6512}, "78b170"],
4
+ [{tag15: 6512}, "78b170", {tag15: 15}],
5
+ [{129 => 6512}, "f88001b170"],
6
+ [{0 => -1}, "0101"],
7
+ [{0 => "text"}, "020474657874"],
8
+ [{0 => "text".force_encoding(Encoding::BINARY)}, "030474657874"],
9
+ [{0 => true}, "04"],
10
+ [{0 => false}, "05"],
11
+ [{0 => 3.1415}, "066f1283c0ca210940"],
12
+ [
13
+ {
14
+ 1 => "Föo",
15
+ 200 => "hihi".force_encoding(Encoding::BINARY),
16
+ 3 => true,
17
+ 40 => false,
18
+ },
19
+ "0a0446c3b66ffb804804686968691cfd28"
20
+ ],
21
+ [
22
+ {
23
+ 2 => true,
24
+ tag1: [false, 5, "text", "bytes".force_encoding(Encoding::BINARY), 1.11],
25
+ tag0: 100,
26
+ },
27
+ "140d08050a04746578740b0562797465730ec3f5285c8fc2f13f0064",
28
+ [:tag0, :tag1]
29
+ ]
30
+ ]
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'messages'
3
+
4
+ describe CMF::Parser do
5
+
6
+ MESSAGES.each do |obj, encoded_output, dictionary|
7
+ it "Parses message #{encoded_output}" do
8
+ expect(CMF.parse_hex(encoded_output, dictionary)).to eq obj
9
+ end
10
+ end
11
+
12
+ # Invalid messages:
13
+ [
14
+ '00', # Number type, but no data.
15
+ '00ff', # Number type, but invalid varint data.
16
+ '0700', # Unknown type.
17
+ '020261', # String type, but string data is less than length.
18
+ '069a9999999999b9', # Double type, but only 7 bytes of data.
19
+ ].each do |invalid_message|
20
+ it "Raises on invalid message: #{invalid_message}" do
21
+ expect { CMF.parse_hex(invalid_message) }.to raise_error(CMF::MalformedMessageError)
22
+ end
23
+ end
24
+
25
+ it 'Parses hex' do
26
+ p = CMF::Parser.new
27
+ p.message_hex = '05'
28
+
29
+ expect(p.parse).to eq({0 => false})
30
+ expect(p.parse_hex('05')).to eq({0 => false})
31
+ end
32
+
33
+ it 'Parses octet' do
34
+ expect(CMF.parse("\x04")).to eq({0 => true})
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ if ENV['COVERAGE']
2
+ require 'simplecov'
3
+ SimpleCov.start
4
+ end
5
+
6
+ require 'cmf'
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe CMF::Varint do
4
+
5
+ number_encodings = {
6
+ 0X7F => [0x7f],
7
+ 0X80 => [0x80, 0x00],
8
+ 0xFF => [0x80, 0x7F],
9
+ 0x407F => [0xFF, 0x7F],
10
+ 0X4080 => [0x80, 0x80, 0x00],
11
+ }
12
+
13
+ describe '#serialize' do
14
+ it 'serializes values correctly' do
15
+ number_encodings.each do |number, encoded_number|
16
+ io = StringIO.new(String.new)
17
+ CMF::Varint.serialize(io, number)
18
+
19
+ expect(io.string).to eq encoded_number.pack('c*')
20
+ end
21
+ end
22
+
23
+ it 'does not serialize negative numbers' do
24
+ io = StringIO.new(String.new)
25
+ expect { CMF::Varint.serialize(io, -1) }.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it 'Raises on invalid type' do
29
+ io = StringIO.new(String.new)
30
+ expect { CMF::Varint.serialize(io, "") }.to raise_error(TypeError)
31
+ end
32
+ end
33
+
34
+ describe '#deserialize' do
35
+ it 'deserializes values correctly' do
36
+ number_encodings.invert.each do |encoded_number, number|
37
+ io = StringIO.new(encoded_number.pack('c*'))
38
+ decoded_number = CMF::Varint.deserialize(io)
39
+
40
+ expect(number).to eq decoded_number
41
+ end
42
+ end
43
+
44
+ it 'raises an error if the stream ends early' do
45
+ io = StringIO.new([0x80].pack('c*'))
46
+ expect { CMF::Varint.deserialize(io) }.to raise_error(CMF::MalformedMessageError)
47
+ end
48
+ end
49
+
50
+ end
metadata ADDED
@@ -0,0 +1,193 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cmf
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen McCarthy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBBMRMwEQYDVQQDDApzam1j
14
+ Y2FydGh5MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj
15
+ b20wHhcNMTgwNDExMDAxMzI4WhcNMTkwNDExMDAxMzI4WjBBMRMwEQYDVQQDDApz
16
+ am1jY2FydGh5MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
17
+ FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOk2n/6xgIgrG7
18
+ avtiI8I9DtcdA326qWYpdQSDLhpSsLNiqiIpo8KF1Zfy3lnAj6JBBIbjiaUsbA/i
19
+ Wcip0307dHNXZjr+AgYcL7OEp8EBkfAeZaYWMcVBbjiSxkzYesDxm7nvTOaD317h
20
+ cThBfB9KW1vGEzazomTxSI9sgqCDtWrogMLGag7uTDJ7fKRK6YXz2xncI0uCsmGb
21
+ 7vekXpfn0xb6tr4ljSseCsPJHnXK7SKB4dzHsmQJ12A57aaV7C/bGqbQAC6odb6k
22
+ V8dw0fnmHC9OSYjV1b2Xr0VmoiT3YA4XsR0/LbeZvGOyQj8S4eHxgFg7wTVhCkCZ
23
+ D89+p8H5AgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
24
+ BBQffCJK6PE+9XaH56VJoFoCl3ECeDAfBgNVHREEGDAWgRRzam1jY2FydGh5QGdt
25
+ YWlsLmNvbTAfBgNVHRIEGDAWgRRzam1jY2FydGh5QGdtYWlsLmNvbTANBgkqhkiG
26
+ 9w0BAQUFAAOCAQEApvFGCB9uyF1mh1UV77YICagARejAIOhzOcZXjlpulI9xXjQY
27
+ 0QK6P1GdwwE/pgT7YjfJR7VNFobare4WdfCzoWCFc34t2vJwrqkkOB3U7v3TjB+p
28
+ z/o2pZKLpNEL4bYJBEbd+vAad/nP1v5e2sCmLm86vSoOwiyQnifmP6PSORObbJF4
29
+ 455zxYw1un6NfN0m+pnIKwvshKoOCgI05VJGtEolJoo42fnolmNxa2t6B30Mfmf+
30
+ kts216EGG4oP6dVuZmf2Ii2F4lQTBDdZM/cisW8jCkO7KeEzJAPhIw1JJwHltHya
31
+ 0TpOI3t2Mz/FJ+rudtz9PJ/d8QvhrF7M7+qH4w==
32
+ -----END CERTIFICATE-----
33
+ date: 2018-04-11 00:00:00.000000000 Z
34
+ dependencies:
35
+ - !ruby/object:Gem::Dependency
36
+ name: bundler
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1'
49
+ - !ruby/object:Gem::Dependency
50
+ name: rake
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '12'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '12'
63
+ - !ruby/object:Gem::Dependency
64
+ name: rspec
65
+ requirement: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '3.7'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '3.7'
77
+ - !ruby/object:Gem::Dependency
78
+ name: simplecov
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ - !ruby/object:Gem::Dependency
92
+ name: yard
93
+ requirement: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 0.9.12
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: 0.9.12
105
+ - !ruby/object:Gem::Dependency
106
+ name: markdown
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '1'
112
+ type: :development
113
+ prerelease: false
114
+ version_requirements: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '1'
119
+ - !ruby/object:Gem::Dependency
120
+ name: redcarpet
121
+ requirement: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '3'
126
+ type: :development
127
+ prerelease: false
128
+ version_requirements: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '3'
133
+ description:
134
+ email: sjmccarthy@gmail.com
135
+ executables: []
136
+ extensions: []
137
+ extra_rdoc_files: []
138
+ files:
139
+ - ".gitignore"
140
+ - ".travis.yml"
141
+ - ".yardopts"
142
+ - CHANGELOG.md
143
+ - Gemfile
144
+ - LICENSE
145
+ - README.md
146
+ - Rakefile
147
+ - certs/jamoes.pem
148
+ - cmf.gemspec
149
+ - lib/cmf.rb
150
+ - lib/cmf/builder.rb
151
+ - lib/cmf/dictionary.rb
152
+ - lib/cmf/malformed_message_error.rb
153
+ - lib/cmf/parser.rb
154
+ - lib/cmf/type.rb
155
+ - lib/cmf/varint.rb
156
+ - lib/cmf/version.rb
157
+ - spec/builder_spec.rb
158
+ - spec/dictionary_spec.rb
159
+ - spec/messages.rb
160
+ - spec/parser_spec.rb
161
+ - spec/spec_helper.rb
162
+ - spec/varint_spec.rb
163
+ homepage: https://github.com/jamoes/cmf
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ post_install_message:
168
+ rdoc_options: []
169
+ require_paths:
170
+ - lib
171
+ required_ruby_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - ">="
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ required_rubygems_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ requirements: []
182
+ rubyforge_project:
183
+ rubygems_version: 2.7.6
184
+ signing_key:
185
+ specification_version: 4
186
+ summary: Builds and parses messages in the Compact Message Format (CMF)
187
+ test_files:
188
+ - spec/builder_spec.rb
189
+ - spec/dictionary_spec.rb
190
+ - spec/messages.rb
191
+ - spec/parser_spec.rb
192
+ - spec/spec_helper.rb
193
+ - spec/varint_spec.rb
metadata.gz.sig ADDED
Binary file