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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gitignore +7 -0
- data/.travis.yml +11 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +76 -0
- data/Rakefile +25 -0
- data/certs/jamoes.pem +21 -0
- data/cmf.gemspec +26 -0
- data/lib/cmf.rb +48 -0
- data/lib/cmf/builder.rb +209 -0
- data/lib/cmf/dictionary.rb +45 -0
- data/lib/cmf/malformed_message_error.rb +5 -0
- data/lib/cmf/parser.rb +164 -0
- data/lib/cmf/type.rb +27 -0
- data/lib/cmf/varint.rb +45 -0
- data/lib/cmf/version.rb +4 -0
- data/spec/builder_spec.rb +74 -0
- data/spec/dictionary_spec.rb +34 -0
- data/spec/messages.rb +30 -0
- data/spec/parser_spec.rb +36 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/varint_spec.rb +50 -0
- metadata +193 -0
- metadata.gz.sig +0 -0
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
data/.travis.yml
ADDED
data/.yardopts
ADDED
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
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
|
+
[](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
|
data/lib/cmf/builder.rb
ADDED
@@ -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
|
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
|
data/lib/cmf/version.rb
ADDED
@@ -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
|
+
]
|
data/spec/parser_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
data/spec/varint_spec.rb
ADDED
@@ -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
|