cmf 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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