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