From ea1137b0ee015ecdf3682492b5f6a4d93a2e71ee Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Mon, 3 Apr 2017 23:42:33 +0200 Subject: [PATCH 1/5] rabbitmq parser: create erlang syntax lexer --- parslet.rb | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 parslet.rb diff --git a/parslet.rb b/parslet.rb new file mode 100644 index 000000000..f1f450e4c --- /dev/null +++ b/parslet.rb @@ -0,0 +1,177 @@ +require 'parslet' + +class ErlAx < Parslet::Parser + root :outermost + # only designed for rabbitmq config files for now: + rule(:outermost) { filler? >> array.maybe >> dot.maybe } + + rule(:expression) { + (tuple | array | binary | string | bool | identifier | float | integer) >> filler? + } + + rule(:array) { + str('[') >> filler? >> ( + expression >> + (comma >> expression).repeat + ).maybe.as(:array) >> str(']') >> filler? + } + + rule(:tuple) { + str('{') >> filler? >> ( + expression >> filler? >> + (comma >> expression).repeat + ).maybe.as(:tuple) >> str('}') >> filler? + } + + rule(:filler?) { space.repeat } + rule(:space) { match('\s+') | match["\n"] | comment } + + rule(:comment) { str('%') >> (match["\n\r"].absent? >> any).repeat } + rule(:comma) { str(',') >> filler? } + rule(:dot) { str('.') >> filler? } + rule(:bool) { str('true').as(:bool) | str('false').as(:bool) } + + rule(:identifier) { + (match('[a-zA-Z]') >> match('[a-zA-Z0-9_]').repeat).as(:identifier) >> filler? + } + + rule(:float) { + ( + integer >> ( + str('.') >> match('[0-9]').repeat(1) | + str('e') >> match('[0-9]').repeat(1) + ).as(:e) + ).as(:float) >> filler? + } + + rule(:integer) { + ((str('+') | str('-')).maybe >> match('[0-9]').repeat(1)).as(:integer) >> filler? + } + + rule(:string) { stringS | stringD } + + rule(:stringS) { + str("'") >> ( + str('\\') >> any | str("'").absent? >> any + ).repeat.as(:string) >> str("'") >> filler? + } + + rule(:stringD) { + str('"') >> ( + str('\\') >> any | str('"').absent? >> any + ).repeat.as(:string) >> str('"') >> filler? + } + + rule(:binary) { + str('<<') >> ( + str('>>').absent? >> any + ).repeat.as(:binary) >> str('>>') >> filler? + } +end + + + + +require 'minitest/autorun' +require 'minitest/spec' +describe ErlAx do + def parse(c) + ErlAx.new.parse(c) + end + + def parsestr(c) + parse(c).to_s + end + + def parse_file(f) + parse(File.read(f)) + end + + it 'parses an empty rabbit file' do + _(parsestr('')).must_equal '' + end + + it 'parses a file with a comment' do + _(parsestr("%% -*- mode: erlang -*-\n%%\n")).must_equal "%% -*- mode: erlang -*-\n%%\n" + end + + it 'parse simple root array' do + _(parsestr("[].\n")).must_equal "{:array=>nil}" + end + + it 'parses a root array with an int' do + _(parsestr('[1].')).must_equal '{:array=>{:integer=>"1"@1}}' + end + + it 'parses a root array with a float' do + _(parsestr('[1.1].')).must_equal '{:array=>{:float=>{:integer=>"1"@1, :e=>".1"@2}}}' + end + + it 'parses a root array with a double quoted string' do + _(parsestr('["st\"r"].')).must_equal '{:array=>{:string=>"st\\\\\\"r"@2}}' + end + + it 'parses a root array with a single quoted string' do + _(parsestr('[\'st\\\'r\'].')).must_equal '{:array=>{:string=>"st\\\\\'r"@2}}' + end + + it 'parses a root array with a binary' do + _(parsestr('[<<0, G, B>>].')).must_equal '{:array=>{:binary=>"0, G, B"@3}}' + end + + it 'parses a root array with a boolean' do + _(parsestr('[true].')).must_equal '{:array=>{:bool=>"true"@1}}' + end + + it 'parses a root array with an identifier' do + _(parsestr('[hello_world123].')).must_equal '{:array=>{:identifier=>"hello_world123"@1}}' + end + + it 'parses a root array with multiple elements' do + _(parsestr("[1, \"a\" %%\n, true\n\t].")).must_equal '{:array=>[{:integer=>"1"@1}, {:string=>"a"@5}, {:bool=>"true"@13}]}' + end + + it 'parses a root array with an tuple' do + _(parsestr('[{}].')).must_equal '{:array=>{:tuple=>nil}}' + end + + it 'parses a root array with an tuple' do + _(parsestr('[{handshake_timeout, 100}].')).must_equal '{:array=>{:tuple=>[{:identifier=>"handshake_timeout"@2}, {:integer=>"100"@21}]}}' + end + + it 'parses a root array with a stringy tuple' do + _(parsestr('[{ab, [{"c", 1},{"d",2}]}].')).must_equal '{:array=>{:tuple=>[{:identifier=>"ab"@2}, {:array=>[{:tuple=>[{:string=>"c"@9}, {:integer=>"1"@13}]}, {:tuple=>[{:string=>"d"@18}, {:integer=>"2"@21}]}]}]}}' + end + + it 'parses a complex deep array-tuple mix' do + _(parsestr('[{rabbit,[{two,[]}]}].')).must_equal '{:array=>{:tuple=>[{:identifier=>"rabbit"@2}, {:array=>{:tuple=>[{:identifier=>"two"@11}, {:array=>nil}]}}]}}' + end + + it 'parses a complex multi array tuple mix' do + _(parsestr('[{rabbit,[{two,[]}]}, 3, false].')).must_equal '{:array=>[{:tuple=>[{:identifier=>"rabbit"@2}, {:array=>{:tuple=>[{:identifier=>"two"@11}, {:array=>nil}]}}]}, {:integer=>"3"@22}, {:bool=>"false"@25}]}' + end + + it 'parses a complex array-tuple mix with comments' do + s = '%% -*- mode: erlang -*- +[ + {rabbit, + [%% + + %% some more text + {tcp_listeners, [5672]} + ]}]. +' + _(parsestr(s)).must_equal '{:array=>{:tuple=>[{:identifier=>"rabbit"@29}, {:array=>{:tuple=>[{:identifier=>"tcp_listeners"@75}, {:array=>{:integer=>"5672"@91}}]}}]}}' + end + + it 'parses a real rabbitmq config file' do + f = 'rabbitmq.config' + unless File.file?(f) + puts "NO #{f}, skipping this check!" + return + end + puts 'Wheeee, testing a real rabbitmq nasty config file' + c = File.read(f) + parsestr(c) + end +end From 860750d967c8fef013e4f57981dae634afb3d125 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Tue, 4 Apr 2017 10:39:05 +0200 Subject: [PATCH 2/5] rabbitmq parser: tree transformation to ruby objects --- parslet.rb | 116 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 21 deletions(-) diff --git a/parslet.rb b/parslet.rb index f1f450e4c..43168e1c7 100644 --- a/parslet.rb +++ b/parslet.rb @@ -5,21 +5,21 @@ class ErlAx < Parslet::Parser # only designed for rabbitmq config files for now: rule(:outermost) { filler? >> array.maybe >> dot.maybe } - rule(:expression) { + rule(:exp) { (tuple | array | binary | string | bool | identifier | float | integer) >> filler? } rule(:array) { str('[') >> filler? >> ( - expression >> - (comma >> expression).repeat + exp.repeat(1) >> + (comma >> exp).repeat ).maybe.as(:array) >> str(']') >> filler? } rule(:tuple) { str('{') >> filler? >> ( - expression >> filler? >> - (comma >> expression).repeat + exp.repeat(1) >> filler? >> + (comma >> exp).repeat ).maybe.as(:tuple) >> str('}') >> filler? } @@ -72,6 +72,7 @@ end + require 'minitest/autorun' require 'minitest/spec' describe ErlAx do @@ -96,35 +97,35 @@ describe ErlAx do end it 'parse simple root array' do - _(parsestr("[].\n")).must_equal "{:array=>nil}" + _(parsestr("[].\n")).must_equal '{:array=>nil}' end it 'parses a root array with an int' do - _(parsestr('[1].')).must_equal '{:array=>{:integer=>"1"@1}}' + _(parsestr('[1].')).must_equal '{:array=>[{:integer=>"1"@1}]}' end it 'parses a root array with a float' do - _(parsestr('[1.1].')).must_equal '{:array=>{:float=>{:integer=>"1"@1, :e=>".1"@2}}}' + _(parsestr('[1.1].')).must_equal '{:array=>[{:float=>{:integer=>"1"@1, :e=>".1"@2}}]}' end it 'parses a root array with a double quoted string' do - _(parsestr('["st\"r"].')).must_equal '{:array=>{:string=>"st\\\\\\"r"@2}}' + _(parsestr('["st\"r"].')).must_equal '{:array=>[{:string=>"st\\\\\\"r"@2}]}' end it 'parses a root array with a single quoted string' do - _(parsestr('[\'st\\\'r\'].')).must_equal '{:array=>{:string=>"st\\\\\'r"@2}}' + _(parsestr('[\'st\\\'r\'].')).must_equal '{:array=>[{:string=>"st\\\\\'r"@2}]}' end it 'parses a root array with a binary' do - _(parsestr('[<<0, G, B>>].')).must_equal '{:array=>{:binary=>"0, G, B"@3}}' + _(parsestr('[<<0, G, B>>].')).must_equal '{:array=>[{:binary=>"0, G, B"@3}]}' end it 'parses a root array with a boolean' do - _(parsestr('[true].')).must_equal '{:array=>{:bool=>"true"@1}}' + _(parsestr('[true].')).must_equal '{:array=>[{:bool=>"true"@1}]}' end it 'parses a root array with an identifier' do - _(parsestr('[hello_world123].')).must_equal '{:array=>{:identifier=>"hello_world123"@1}}' + _(parsestr('[hello_world123].')).must_equal '{:array=>[{:identifier=>"hello_world123"@1}]}' end it 'parses a root array with multiple elements' do @@ -132,23 +133,23 @@ describe ErlAx do end it 'parses a root array with an tuple' do - _(parsestr('[{}].')).must_equal '{:array=>{:tuple=>nil}}' + _(parsestr('[{}].')).must_equal '{:array=>[{:tuple=>nil}]}' end it 'parses a root array with an tuple' do - _(parsestr('[{handshake_timeout, 100}].')).must_equal '{:array=>{:tuple=>[{:identifier=>"handshake_timeout"@2}, {:integer=>"100"@21}]}}' + _(parsestr('[{handshake_timeout, 100}].')).must_equal '{:array=>[{:tuple=>[{:identifier=>"handshake_timeout"@2}, {:integer=>"100"@21}]}]}' end it 'parses a root array with a stringy tuple' do - _(parsestr('[{ab, [{"c", 1},{"d",2}]}].')).must_equal '{:array=>{:tuple=>[{:identifier=>"ab"@2}, {:array=>[{:tuple=>[{:string=>"c"@9}, {:integer=>"1"@13}]}, {:tuple=>[{:string=>"d"@18}, {:integer=>"2"@21}]}]}]}}' + _(parsestr('[{ab, [{"c", 1},{"d",2}]}].')).must_equal '{:array=>[{:tuple=>[{:identifier=>"ab"@2}, {:array=>[{:tuple=>[{:string=>"c"@9}, {:integer=>"1"@13}]}, {:tuple=>[{:string=>"d"@18}, {:integer=>"2"@21}]}]}]}]}' end it 'parses a complex deep array-tuple mix' do - _(parsestr('[{rabbit,[{two,[]}]}].')).must_equal '{:array=>{:tuple=>[{:identifier=>"rabbit"@2}, {:array=>{:tuple=>[{:identifier=>"two"@11}, {:array=>nil}]}}]}}' + _(parsestr('[{rabbit,[{two,[]}]}].')).must_equal '{:array=>[{:tuple=>[{:identifier=>"rabbit"@2}, {:array=>[{:tuple=>[{:identifier=>"two"@11}, {:array=>nil}]}]}]}]}' end it 'parses a complex multi array tuple mix' do - _(parsestr('[{rabbit,[{two,[]}]}, 3, false].')).must_equal '{:array=>[{:tuple=>[{:identifier=>"rabbit"@2}, {:array=>{:tuple=>[{:identifier=>"two"@11}, {:array=>nil}]}}]}, {:integer=>"3"@22}, {:bool=>"false"@25}]}' + _(parsestr('[{rabbit,[{two,[]}]}, 3, false].')).must_equal '{:array=>[{:tuple=>[{:identifier=>"rabbit"@2}, {:array=>[{:tuple=>[{:identifier=>"two"@11}, {:array=>nil}]}]}]}, {:integer=>"3"@22}, {:bool=>"false"@25}]}' end it 'parses a complex array-tuple mix with comments' do @@ -161,10 +162,81 @@ describe ErlAx do {tcp_listeners, [5672]} ]}]. ' - _(parsestr(s)).must_equal '{:array=>{:tuple=>[{:identifier=>"rabbit"@29}, {:array=>{:tuple=>[{:identifier=>"tcp_listeners"@75}, {:array=>{:integer=>"5672"@91}}]}}]}}' + _(parsestr(s)).must_equal '{:array=>[{:tuple=>[{:identifier=>"rabbit"@29}, {:array=>[{:tuple=>[{:identifier=>"tcp_listeners"@75}, {:array=>[{:integer=>"5672"@91}]}]}]}]}]}' + end +end + + + +class ErlEr < Parslet::Transform + class Tuple < Array; end + class Identifier < String; end + rule(string: simple(:x)) { x.to_s } + rule(string: []) { '' } + rule(integer: simple(:x)) { x.to_i } + rule(float: { integer: simple(:a), e: simple(:b) }) { (a+b).to_f } + rule(bool: 'true') { true } + rule(bool: 'false') { false } + rule(identifier: simple(:x)) { Identifier.new(x.to_s) } + # TODO: binary! + rule(array: subtree(:x)) { Array(x) } + rule(tuple: subtree(:x)) { + x.nil? ? Tuple.new : Tuple.new(x) + } +end + +describe ErlEr do + def parse(c) + ErlEr.new.apply(ErlAx.new.parse(c)) end - it 'parses a real rabbitmq config file' do + it 'transforms and empty file' do + _(parse('')).must_equal '' + end + + it 'transforms an empty array' do + _(parse('[].')).must_equal [] + end + + it 'transforms a simple array with one string' do + _(parse('["one"].')).must_equal ['one'] + end + + it 'transforms a simple array with an empty string' do + _(parse('[\'\'].')).must_equal [''] + end + + it 'transforms a simple array with multiple values' do + _(parse('[1, 1.1, true, false, "ok"].')).must_equal [1, 1.1, true, false, 'ok'] + end + + it 'transforms a deep array' do + _(parse('[[[[1]]]].')).must_equal [[[[1]]]] + end + + it 'transforms an empty tuple' do + _(parse('[{}].')).must_equal [ErlEr::Tuple.new] + end + + it 'transforms a tuple with one element' do + _(parse('[{1}].')).must_equal [ErlEr::Tuple.new([1])] + end + + it 'transforms a tuple with multiple elements' do + _(parse('[{id123, 1, 1.1}].')).must_equal [ErlEr::Tuple.new([ErlEr::Identifier.new('id123'), 1, 1.1])] + end + + it 'transforms a deep tuple' do + _(parse('[{{{1}}}].')).must_equal [ErlEr::Tuple.new([ErlEr::Tuple.new([ErlEr::Tuple.new([1])])])] + end + + it 'transforms a deep mix of tuple and array' do + _(parse('[{[{1}]}].')).must_equal [ErlEr::Tuple.new([[ErlEr::Tuple.new([1])]])] + end +end + +describe 'complex use-case' do + it 'parses a tricky rabbitmq config file' do f = 'rabbitmq.config' unless File.file?(f) puts "NO #{f}, skipping this check!" @@ -172,6 +244,8 @@ describe ErlAx do end puts 'Wheeee, testing a real rabbitmq nasty config file' c = File.read(f) - parsestr(c) + _(ErlAx.new.parse(c).to_s).must_be_instance_of String + res = ErlEr.new.apply(ErlAx.new.parse(c)) + # require "pry"; binding.pry end end From 4197b5a8f692925b880f6aac1d741eed0cb3a985 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Tue, 4 Apr 2017 11:15:00 +0200 Subject: [PATCH 3/5] rabbitmq parser: add simple binary handling --- parslet.rb | 56 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/parslet.rb b/parslet.rb index 43168e1c7..3ce9471cd 100644 --- a/parslet.rb +++ b/parslet.rb @@ -62,10 +62,12 @@ class ErlAx < Parslet::Parser ).repeat.as(:string) >> str('"') >> filler? } + rule(:binary_item) { (string | integer) >> filler? } rule(:binary) { - str('<<') >> ( - str('>>').absent? >> any - ).repeat.as(:binary) >> str('>>') >> filler? + str('<<') >> filler? >> ( + binary_item.repeat(1) >> + (comma >> binary_item).repeat + ).maybe.as(:binary) >> str('>>') >> filler? } end @@ -116,8 +118,16 @@ describe ErlAx do _(parsestr('[\'st\\\'r\'].')).must_equal '{:array=>[{:string=>"st\\\\\'r"@2}]}' end + it 'parses a root array with an empty binary' do + _(parsestr('[<<>>].')).must_equal '{:array=>[{:binary=>nil}]}' + end + it 'parses a root array with a binary' do - _(parsestr('[<<0, G, B>>].')).must_equal '{:array=>[{:binary=>"0, G, B"@3}]}' + _(parsestr('[<<0, 1, 2>>].')).must_equal '{:array=>[{:binary=>[{:integer=>"0"@3}, {:integer=>"1"@6}, {:integer=>"2"@9}]}]}' + end + + it 'parses a root array with a binary string' do + _(parsestr('[<<"pwd">>].')).must_equal '{:array=>[{:binary=>[{:string=>"pwd"@4}]}]}' end it 'parses a root array with a boolean' do @@ -168,15 +178,23 @@ end -class ErlEr < Parslet::Transform +class ErlOm < Parslet::Transform class Tuple < Array; end class Identifier < String; end + + def self.assemble_binary(seq) + seq.map { |i| + i.is_a?(String) ? i :[i].pack('C') + }.join('') + end + rule(string: simple(:x)) { x.to_s } rule(string: []) { '' } rule(integer: simple(:x)) { x.to_i } rule(float: { integer: simple(:a), e: simple(:b) }) { (a+b).to_f } rule(bool: 'true') { true } rule(bool: 'false') { false } + rule(binary: subtree(:x)) { x.nil? ? '' : ErlOm.assemble_binary(x) } rule(identifier: simple(:x)) { Identifier.new(x.to_s) } # TODO: binary! rule(array: subtree(:x)) { Array(x) } @@ -185,9 +203,9 @@ class ErlEr < Parslet::Transform } end -describe ErlEr do +describe ErlOm do def parse(c) - ErlEr.new.apply(ErlAx.new.parse(c)) + ErlOm.new.apply(ErlAx.new.parse(c)) end it 'transforms and empty file' do @@ -206,6 +224,18 @@ describe ErlEr do _(parse('[\'\'].')).must_equal [''] end + it 'transforms a simple array with an empty binary' do + _(parse('[<<>>].')).must_equal [''] + end + + it 'transforms a simple array with a binary string' do + _(parse('[<<"pwd">>].')).must_equal ['pwd'] + end + + it 'transforms a simple array with a binary sequence' do + _(parse('[<<97, 98, 99>>].')).must_equal ['abc'] + end + it 'transforms a simple array with multiple values' do _(parse('[1, 1.1, true, false, "ok"].')).must_equal [1, 1.1, true, false, 'ok'] end @@ -215,23 +245,23 @@ describe ErlEr do end it 'transforms an empty tuple' do - _(parse('[{}].')).must_equal [ErlEr::Tuple.new] + _(parse('[{}].')).must_equal [ErlOm::Tuple.new] end it 'transforms a tuple with one element' do - _(parse('[{1}].')).must_equal [ErlEr::Tuple.new([1])] + _(parse('[{1}].')).must_equal [ErlOm::Tuple.new([1])] end it 'transforms a tuple with multiple elements' do - _(parse('[{id123, 1, 1.1}].')).must_equal [ErlEr::Tuple.new([ErlEr::Identifier.new('id123'), 1, 1.1])] + _(parse('[{id123, 1, 1.1}].')).must_equal [ErlOm::Tuple.new([ErlOm::Identifier.new('id123'), 1, 1.1])] end it 'transforms a deep tuple' do - _(parse('[{{{1}}}].')).must_equal [ErlEr::Tuple.new([ErlEr::Tuple.new([ErlEr::Tuple.new([1])])])] + _(parse('[{{{1}}}].')).must_equal [ErlOm::Tuple.new([ErlOm::Tuple.new([ErlOm::Tuple.new([1])])])] end it 'transforms a deep mix of tuple and array' do - _(parse('[{[{1}]}].')).must_equal [ErlEr::Tuple.new([[ErlEr::Tuple.new([1])]])] + _(parse('[{[{1}]}].')).must_equal [ErlOm::Tuple.new([[ErlOm::Tuple.new([1])]])] end end @@ -245,7 +275,7 @@ describe 'complex use-case' do puts 'Wheeee, testing a real rabbitmq nasty config file' c = File.read(f) _(ErlAx.new.parse(c).to_s).must_be_instance_of String - res = ErlEr.new.apply(ErlAx.new.parse(c)) + res = ErlOm.new.apply(ErlAx.new.parse(c)) # require "pry"; binding.pry end end From 21258f72962dcc3d07169502ad77d9fa5f93c918 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Wed, 5 Apr 2017 15:46:22 +0200 Subject: [PATCH 4/5] rabbitmq parser: support complex bit-stream handling --- parslet.rb | 131 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 11 deletions(-) diff --git a/parslet.rb b/parslet.rb index 3ce9471cd..06b172cb2 100644 --- a/parslet.rb +++ b/parslet.rb @@ -62,7 +62,13 @@ class ErlAx < Parslet::Parser ).repeat.as(:string) >> str('"') >> filler? } - rule(:binary_item) { (string | integer) >> filler? } + rule(:binary_item) { + (string | integer) >> + (str(':') >> integer).maybe.as(:size) >> + (str('/') >> identifier).maybe.as(:type) >> + filler? + } + rule(:binary) { str('<<') >> filler? >> ( binary_item.repeat(1) >> @@ -122,12 +128,24 @@ describe ErlAx do _(parsestr('[<<>>].')).must_equal '{:array=>[{:binary=>nil}]}' end - it 'parses a root array with a binary' do - _(parsestr('[<<0, 1, 2>>].')).must_equal '{:array=>[{:binary=>[{:integer=>"0"@3}, {:integer=>"1"@6}, {:integer=>"2"@9}]}]}' + it 'parses a root array with a bit-stream with a string' do + _(parsestr('[<<"pwd">>].')).must_equal '{:array=>[{:binary=>[{:string=>"pwd"@4, :size=>nil, :type=>nil}]}]}' end - it 'parses a root array with a binary string' do - _(parsestr('[<<"pwd">>].')).must_equal '{:array=>[{:binary=>[{:string=>"pwd"@4}]}]}' + it 'parses a root array with a bit-stream with a string and type' do + _(parsestr('[<<"pwd"/utf8>>].')).must_equal '{:array=>[{:binary=>[{:string=>"pwd"@4, :size=>nil, :type=>{:identifier=>"utf8"@9}}]}]}' + end + + it 'parses a root array with a bit-stream of numbers' do + _(parsestr('[<<0, 1, 2>>].')).must_equal '{:array=>[{:binary=>[{:integer=>"0"@3, :size=>nil, :type=>nil}, {:integer=>"1"@6, :size=>nil, :type=>nil}, {:integer=>"2"@9, :size=>nil, :type=>nil}]}]}' + end + + it 'parses a root array with a mixed bit-stream of string+numbers' do + _(parsestr('[<<97, "b", 99>>].')).must_equal '{:array=>[{:binary=>[{:integer=>"97"@3, :size=>nil, :type=>nil}, {:string=>"b"@8, :size=>nil, :type=>nil}, {:integer=>"99"@12, :size=>nil, :type=>nil}]}]}' + end + + it 'parses a root array with a bit-stream of value:size' do + _(parsestr('[<<0, 1:8, "2":16>>].')).must_equal '{:array=>[{:binary=>[{:integer=>"0"@3, :size=>nil, :type=>nil}, {:integer=>"1"@6, :size=>{:integer=>"8"@8}, :type=>nil}, {:string=>"2"@12, :size=>{:integer=>"16"@15}, :type=>nil}]}]}' end it 'parses a root array with a boolean' do @@ -176,16 +194,84 @@ describe ErlAx do end end +class ErlangBitstream + def initialize + @data = [] # a stream of 8-bit numbers + @cur_bits = '' # a string of binary bits 10010010... + end + TYPES = { + 'integer' => 8, + 'float' => 8*8, + 'utf8' => 8, + 'utf16' => 8*2, + 'utf32' => 8*4, + }.freeze + + def bit_size(size, type) + raise 'Cannot specify size and type at the same time.' if !type.nil? && !size.nil? + return (size || 8).to_i if type.nil? + TYPES[type] || raise("Cannot handle binary-stream type #{type}") + end + + def add(i) + if i[:integer].nil? && i[:string].nil? + raise 'No data provided, internal error for binary-stream processing!' + end + s = bit_size(i[:size], i[:type]) + unless i[:string].nil? + str2int(i[:string].to_s, i[:type]).map { |e| add_bits(int2bits(e, 8)) } + else + add_int(i[:integer], s) + end + rescue RuntimeError => e + raise 'Error processing Erlang bit string '\ + "'#{i[:string] || i[:integer]}:#{i[:size]}/#{i[:type]}'. #{e.message}" + end + + def str2int(s, type) + case type + when 'utf8' then s.encode('utf-8').unpack('C*') + when 'utf16' then s.encode('utf-16').unpack('C*').drop(2) + when 'utf32' then s.encode('utf-32').unpack('C*').drop(4) + when 'integer', 'float' then raise "Cannot handle bit string as type #{type}" + else s.split('').map { |x| x.ord & 0xff } + end + end + + def int2bits(i, len) + format("%0#{len}b", i) + end + + def add_int(v, size) + x = v.to_i & (2**size - 1) # only get the bits specified in size + add_bits(int2bits(x, size)) + end + + def add_bits(s) + b = (@cur_bits + s).scan(/.{1,8}/) + @data += b[0..-2].map { |x| x.to_i(2) } + @cur_bits = b.last + end + + def value(encoding = 'utf-8') + # fill in the rest + rest = '0' * (8 - @cur_bits.length) + @cur_bits + arr = @data + [rest.to_i(2)] + s = arr.pack('C*') + s.force_encoding(encoding) unless encoding.nil? + s + end +end class ErlOm < Parslet::Transform class Tuple < Array; end class Identifier < String; end def self.assemble_binary(seq) - seq.map { |i| - i.is_a?(String) ? i :[i].pack('C') - }.join('') + b = ErlangBitstream.new + seq.each { |i| b.add(i) } + b.value end rule(string: simple(:x)) { x.to_s } @@ -196,7 +282,6 @@ class ErlOm < Parslet::Transform rule(bool: 'false') { false } rule(binary: subtree(:x)) { x.nil? ? '' : ErlOm.assemble_binary(x) } rule(identifier: simple(:x)) { Identifier.new(x.to_s) } - # TODO: binary! rule(array: subtree(:x)) { Array(x) } rule(tuple: subtree(:x)) { x.nil? ? Tuple.new : Tuple.new(x) @@ -229,11 +314,35 @@ describe ErlOm do end it 'transforms a simple array with a binary string' do - _(parse('[<<"pwd">>].')).must_equal ['pwd'] + _(parse('[<<"Hello world!">>].')).must_equal ['Hello world!'] end it 'transforms a simple array with a binary sequence' do - _(parse('[<<97, 98, 99>>].')).must_equal ['abc'] + _(parse('[<<97, "b", 99>>].')).must_equal ['abc'] + end + + it 'transforms a default ascii string' do + _(parse('[<<"Łぁ">>].')).must_equal ['AA'] + end + + it 'transforms a utf-8 string' do + _(parse('[<<"Łぁ"/utf8>>].')).must_equal ['Łぁ'] + end + + it 'transforms a utf-16 string' do + _(parse('[<<"Łぁ"/utf16>>].')).must_equal ["\u0001\u0041\u0030\u0041"] + end + + it 'transforms a utf-32 string' do + _(parse('[<<"Łぁ"/utf32>>].')).must_equal ["\u0000\u0000\u0001\u0041\u0000\u0000\u0030\u0041"] + end + + it 'transforms a partial bit number sequence' do + _(parse('[<<1:2,1:6>>].')).must_equal ['A'] + end + + it 'prevents mixing size and type for bit-streams' do + _(proc { parse('[<<1:8/utf8>>].') }).must_raise RuntimeError end it 'transforms a simple array with multiple values' do From 02e435b6d08a7a8999373ed9bde76fab1f3590e9 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Wed, 5 Apr 2017 16:09:03 +0200 Subject: [PATCH 5/5] add rabbitmq config resource Signed-off-by: Dominik Richter --- docs/resources/rabbitmq_config.md.erb | 55 +++++ inspec.gemspec | 1 + lib/inspec/resource.rb | 1 + lib/resources/rabbitmq_conf.rb | 53 +++++ lib/utils/erlang_parser.rb | 192 ++++++++++++++++ test/helper.rb | 1 + test/unit/mock/files/rabbitmq.config | 10 + test/unit/resources/rabbitmq_conf_test.rb | 17 ++ .../unit/utils/erlang_parser_test.rb | 215 ++---------------- 9 files changed, 344 insertions(+), 201 deletions(-) create mode 100644 docs/resources/rabbitmq_config.md.erb create mode 100644 lib/resources/rabbitmq_conf.rb create mode 100644 lib/utils/erlang_parser.rb create mode 100644 test/unit/mock/files/rabbitmq.config create mode 100644 test/unit/resources/rabbitmq_conf_test.rb rename parslet.rb => test/unit/utils/erlang_parser_test.rb (52%) diff --git a/docs/resources/rabbitmq_config.md.erb b/docs/resources/rabbitmq_config.md.erb new file mode 100644 index 000000000..1e11ad580 --- /dev/null +++ b/docs/resources/rabbitmq_config.md.erb @@ -0,0 +1,55 @@ +--- +title: About the rabbitmq_config Resource +--- + +# rabbitmq_config + +Use the `rabbitmq_config` InSpec audit resource to test configuration data for the RabbitMQ daemon located at `/etc/rabbitmq/rabbitmq.config` on Linux and Unix platforms. + +## Syntax + +A `rabbitmq_config` resource block declares the RabbitMQ configuration data to be tested: + + describe rabbitmq_config.params('rabbit', 'ssl_listeners') do + it { should cmp 5671 } + end + +where + +* `params` is the list of parameters configured in the RabbitMQ config file +* `{ should cmp 5671 }` tests the value of `rabbit.ssl_listeners` as read from `rabbitmq.config` versus the value declared in the test + + +## Matchers + +This InSpec audit resource has the following matchers: + +### be + +<%= partial "/shared/matcher_be" %> + +### cmp + +<%= partial "/shared/matcher_cmp" %> + +### eq + +<%= partial "/shared/matcher_eq" %> + +### include + +<%= partial "/shared/matcher_include" %> + +### match + +<%= partial "/shared/matcher_match" %> + +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test the list of TCP listeners + + describe rabbitmq_config.params('rabbit', 'tcp_listeners') do + it { should eq [5672] } + end diff --git a/inspec.gemspec b/inspec.gemspec index 73f7a09ce..7d36a3b9d 100644 --- a/inspec.gemspec +++ b/inspec.gemspec @@ -42,4 +42,5 @@ Gem::Specification.new do |spec| spec.add_dependency 'faraday', '>=0.9.0' spec.add_dependency 'toml', '~> 0.1' spec.add_dependency 'addressable', '~> 2.4' + spec.add_dependency 'parslet', '~> 1.5' end diff --git a/lib/inspec/resource.rb b/lib/inspec/resource.rb index 9c364d06f..f8c7ca385 100644 --- a/lib/inspec/resource.rb +++ b/lib/inspec/resource.rb @@ -120,6 +120,7 @@ require 'resources/postgres_conf' require 'resources/postgres_session' require 'resources/powershell' require 'resources/processes' +require 'resources/rabbitmq_conf' require 'resources/registry_key' require 'resources/security_policy' require 'resources/service' diff --git a/lib/resources/rabbitmq_conf.rb b/lib/resources/rabbitmq_conf.rb new file mode 100644 index 000000000..c4bc0355a --- /dev/null +++ b/lib/resources/rabbitmq_conf.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 +# author: Dominik Richter +# author: Christoph Hartmann + +require 'utils/erlang_parser' + +module Inspec::Resources + class RabbitmqConf < Inspec.resource(1) + name 'rabbitmq_config' + desc 'Use the rabbitmq_config InSpec resource to test configuration data '\ + 'for the RabbitMQ service located in /etc/rabbitmq/rabbitmq.config on '\ + 'Linux and UNIX platforms.' + example " + describe rabbitmq_config.params('rabbit', 'ssl_listeners') do + it { should cmp 5671 } + end + " + + def initialize(conf_path = nil) + @conf_path = conf_path || '/etc/rabbitmq/rabbitmq.config' + end + + def params(*opts) + opts.inject(read_params) do |res, nxt| + res.respond_to?(:key) ? res[nxt] : nil + end + end + + def to_s + "rabbitmq_config #{@conf_path}" + end + + private + + def read_content + return @content if defined?(@content) + file = inspec.file(@conf_path) + if !file.file? + return skip_resource "Can't find file \"#{@conf_path}\"" + end + + @content = file.content + end + + def read_params + return @params if defined?(@params) + return @params = {} if read_content.nil? + @params = ErlangConfigFile.parse(read_content) + rescue Parslet::ParseFailed + raise "Cannot parse RabbitMQ config: \"#{read_content}\"" + end + end +end diff --git a/lib/utils/erlang_parser.rb b/lib/utils/erlang_parser.rb new file mode 100644 index 000000000..fc716cbc0 --- /dev/null +++ b/lib/utils/erlang_parser.rb @@ -0,0 +1,192 @@ +# encoding: utf-8 +# author: Dominik Richter +# author: Christoph Hartmann + +require 'parslet' + +class ErlangParser < Parslet::Parser + root :outermost + # only designed for rabbitmq config files for now: + rule(:outermost) { filler? >> array.maybe >> dot.maybe } + + rule(:exp) { + (tuple | array | binary | string | bool | identifier | float | integer) >> filler? + } + + rule(:array) { + str('[') >> filler? >> ( + exp.repeat(1) >> + (comma >> exp).repeat + ).maybe.as(:array) >> str(']') >> filler? + } + + rule(:tuple) { + str('{') >> filler? >> ( + exp.repeat(1) >> filler? >> + (comma >> exp).repeat + ).maybe.as(:tuple) >> str('}') >> filler? + } + + rule(:filler?) { space.repeat } + rule(:space) { match('\s+') | match["\n"] | comment } + + rule(:comment) { str('%') >> (match["\n\r"].absent? >> any).repeat } + rule(:comma) { str(',') >> filler? } + rule(:dot) { str('.') >> filler? } + rule(:bool) { str('true').as(:bool) | str('false').as(:bool) } + + rule(:identifier) { + (match('[a-zA-Z]') >> match('[a-zA-Z0-9_]').repeat).as(:identifier) >> filler? + } + + rule(:float) { + ( + integer >> ( + str('.') >> match('[0-9]').repeat(1) | + str('e') >> match('[0-9]').repeat(1) + ).as(:e) + ).as(:float) >> filler? + } + + rule(:integer) { + ((str('+') | str('-')).maybe >> match('[0-9]').repeat(1)).as(:integer) >> filler? + } + + rule(:string) { stringS | stringD } + + rule(:stringS) { + str("'") >> ( + str('\\') >> any | str("'").absent? >> any + ).repeat.as(:string) >> str("'") >> filler? + } + + rule(:stringD) { + str('"') >> ( + str('\\') >> any | str('"').absent? >> any + ).repeat.as(:string) >> str('"') >> filler? + } + + rule(:binary_item) { + (string | integer) >> + (str(':') >> integer).maybe.as(:size) >> + (str('/') >> identifier).maybe.as(:type) >> + filler? + } + + rule(:binary) { + str('<<') >> filler? >> ( + binary_item.repeat(1) >> + (comma >> binary_item).repeat + ).maybe.as(:binary) >> str('>>') >> filler? + } +end + +class ErlangBitstream + def initialize + @data = [] # a stream of 8-bit numbers + @cur_bits = '' # a string of binary bits 10010010... + end + + TYPES = { + 'integer' => 8, + 'float' => 8*8, + 'utf8' => 8, + 'utf16' => 8*2, + 'utf32' => 8*4, + }.freeze + + def bit_size(size, type) + raise 'Cannot specify size and type at the same time.' if !type.nil? && !size.nil? + return (size || 8).to_i if type.nil? + TYPES[type] || raise("Cannot handle binary-stream type #{type}") + end + + def add(i) + if i[:integer].nil? && i[:string].nil? + raise 'No data provided, internal error for binary-stream processing!' + end + s = bit_size(i[:size], i[:type]) + unless i[:string].nil? + str2int(i[:string].to_s, i[:type]).map { |e| add_bits(int2bits(e, 8)) } + else + add_int(i[:integer], s) + end + rescue RuntimeError => e + raise 'Error processing Erlang bit string '\ + "'#{i[:string] || i[:integer]}:#{i[:size]}/#{i[:type]}'. #{e.message}" + end + + def str2int(s, type) + case type + when 'utf8' then s.encode('utf-8').unpack('C*') + when 'utf16' then s.encode('utf-16').unpack('C*').drop(2) + when 'utf32' then s.encode('utf-32').unpack('C*').drop(4) + when 'integer', 'float' then raise "Cannot handle bit string as type #{type}" + else s.split('').map { |x| x.ord & 0xff } + end + end + + def int2bits(i, len) + format("%0#{len}b", i) + end + + def add_int(v, size) + x = v.to_i & (2**size - 1) # only get the bits specified in size + add_bits(int2bits(x, size)) + end + + def add_bits(s) + b = (@cur_bits + s).scan(/.{1,8}/) + @data += b[0..-2].map { |x| x.to_i(2) } + @cur_bits = b.last + end + + def value(encoding = 'utf-8') + # fill in the rest + rest = '0' * (8 - @cur_bits.length) + @cur_bits + arr = @data + [rest.to_i(2)] + s = arr.pack('C*') + s.force_encoding(encoding) unless encoding.nil? + s + end +end + +class ErlangTransform < Parslet::Transform + class Tuple < Array; end + class Identifier < String; end + + def self.assemble_binary(seq) + b = ErlangBitstream.new + seq.each { |i| b.add(i) } + b.value + end + + rule(string: simple(:x)) { x.to_s } + rule(string: []) { '' } + rule(integer: simple(:x)) { x.to_i } + rule(float: { integer: simple(:a), e: simple(:b) }) { (a+b).to_f } + rule(bool: 'true') { true } + rule(bool: 'false') { false } + rule(binary: subtree(:x)) { x.nil? ? '' : ErlangTransform.assemble_binary(x) } + rule(identifier: simple(:x)) { Identifier.new(x.to_s) } + rule(array: subtree(:x)) { Array(x) } + rule(tuple: subtree(:x)) { + x.nil? ? Tuple.new : Tuple.new(x) + } +end + +class ErlangConfigFile + def self.parse(content) + lex = ErlangParser.new.parse(content) + tree = ErlangTransform.new.apply(lex) + turn_to_hash(tree) + end + + def self.turn_to_hash(t) + if t.is_a?(Array) && t.all? { |x| x.class == ErlangTransform::Tuple && x.length == 2 } + Hash[t.map { |i| [i[0], turn_to_hash(i[1])] }] + else + t + end + end +end diff --git a/test/helper.rb b/test/helper.rb index b1d1af813..abe1eebfa 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -115,6 +115,7 @@ class MockLoader '/etc/audit/auditd.conf' => mockfile.call('auditd.conf'), '/etc/mysql/my.cnf' => mockfile.call('mysql.conf'), '/etc/mysql/mysql2.conf' => mockfile.call('mysql2.conf'), + '/etc/rabbitmq/rabbitmq.config' => mockfile.call('rabbitmq.config'), 'kitchen.yml' => mockfile.call('kitchen.yml'), 'example.csv' => mockfile.call('example.csv'), 'policyfile.lock.json' => mockfile.call('policyfile.lock.json'), diff --git a/test/unit/mock/files/rabbitmq.config b/test/unit/mock/files/rabbitmq.config new file mode 100644 index 000000000..2861eeaed --- /dev/null +++ b/test/unit/mock/files/rabbitmq.config @@ -0,0 +1,10 @@ +%% -*- mode: erlang -*- +[ + {rabbit, + [%% some comments... + {ssl_listeners, [5671]}, + %% duplicate entries + {tcp_listeners, [5672]}, + {tcp_listeners, [{"127.0.0.1", 5672}, + {"::1", 5672}]} +]}]. diff --git a/test/unit/resources/rabbitmq_conf_test.rb b/test/unit/resources/rabbitmq_conf_test.rb new file mode 100644 index 000000000..81d9a8373 --- /dev/null +++ b/test/unit/resources/rabbitmq_conf_test.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 +# author: Dominik Richter +# author: Christoph Hartmann + +require 'helper' +require 'inspec/resource' + +describe 'Inspec::Resources::RabbitmqConf' do + + describe 'rabbitmq_config' do + it 'check rabbitmq config parsing' do + resource = load_resource('rabbitmq_config') + _(resource.params('rabbit', 'ssl_listeners')).must_equal [5671] + _(resource.params('rabbit', 'tcp_listeners')).must_equal({'127.0.0.1'=>5672, '::1'=>5672}) + end + end +end diff --git a/parslet.rb b/test/unit/utils/erlang_parser_test.rb similarity index 52% rename from parslet.rb rename to test/unit/utils/erlang_parser_test.rb index 06b172cb2..ca2957a78 100644 --- a/parslet.rb +++ b/test/unit/utils/erlang_parser_test.rb @@ -1,91 +1,13 @@ -require 'parslet' +# encoding: utf-8 +# author: Dominik Richter +# author: Christoph Hartmann -class ErlAx < Parslet::Parser - root :outermost - # only designed for rabbitmq config files for now: - rule(:outermost) { filler? >> array.maybe >> dot.maybe } +require 'helper' +require 'utils/erlang_parser' - rule(:exp) { - (tuple | array | binary | string | bool | identifier | float | integer) >> filler? - } - - rule(:array) { - str('[') >> filler? >> ( - exp.repeat(1) >> - (comma >> exp).repeat - ).maybe.as(:array) >> str(']') >> filler? - } - - rule(:tuple) { - str('{') >> filler? >> ( - exp.repeat(1) >> filler? >> - (comma >> exp).repeat - ).maybe.as(:tuple) >> str('}') >> filler? - } - - rule(:filler?) { space.repeat } - rule(:space) { match('\s+') | match["\n"] | comment } - - rule(:comment) { str('%') >> (match["\n\r"].absent? >> any).repeat } - rule(:comma) { str(',') >> filler? } - rule(:dot) { str('.') >> filler? } - rule(:bool) { str('true').as(:bool) | str('false').as(:bool) } - - rule(:identifier) { - (match('[a-zA-Z]') >> match('[a-zA-Z0-9_]').repeat).as(:identifier) >> filler? - } - - rule(:float) { - ( - integer >> ( - str('.') >> match('[0-9]').repeat(1) | - str('e') >> match('[0-9]').repeat(1) - ).as(:e) - ).as(:float) >> filler? - } - - rule(:integer) { - ((str('+') | str('-')).maybe >> match('[0-9]').repeat(1)).as(:integer) >> filler? - } - - rule(:string) { stringS | stringD } - - rule(:stringS) { - str("'") >> ( - str('\\') >> any | str("'").absent? >> any - ).repeat.as(:string) >> str("'") >> filler? - } - - rule(:stringD) { - str('"') >> ( - str('\\') >> any | str('"').absent? >> any - ).repeat.as(:string) >> str('"') >> filler? - } - - rule(:binary_item) { - (string | integer) >> - (str(':') >> integer).maybe.as(:size) >> - (str('/') >> identifier).maybe.as(:type) >> - filler? - } - - rule(:binary) { - str('<<') >> filler? >> ( - binary_item.repeat(1) >> - (comma >> binary_item).repeat - ).maybe.as(:binary) >> str('>>') >> filler? - } -end - - - - - -require 'minitest/autorun' -require 'minitest/spec' -describe ErlAx do +describe ErlangParser do def parse(c) - ErlAx.new.parse(c) + ErlangParser.new.parse(c) end def parsestr(c) @@ -194,103 +116,9 @@ describe ErlAx do end end -class ErlangBitstream - def initialize - @data = [] # a stream of 8-bit numbers - @cur_bits = '' # a string of binary bits 10010010... - end - - TYPES = { - 'integer' => 8, - 'float' => 8*8, - 'utf8' => 8, - 'utf16' => 8*2, - 'utf32' => 8*4, - }.freeze - - def bit_size(size, type) - raise 'Cannot specify size and type at the same time.' if !type.nil? && !size.nil? - return (size || 8).to_i if type.nil? - TYPES[type] || raise("Cannot handle binary-stream type #{type}") - end - - def add(i) - if i[:integer].nil? && i[:string].nil? - raise 'No data provided, internal error for binary-stream processing!' - end - s = bit_size(i[:size], i[:type]) - unless i[:string].nil? - str2int(i[:string].to_s, i[:type]).map { |e| add_bits(int2bits(e, 8)) } - else - add_int(i[:integer], s) - end - rescue RuntimeError => e - raise 'Error processing Erlang bit string '\ - "'#{i[:string] || i[:integer]}:#{i[:size]}/#{i[:type]}'. #{e.message}" - end - - def str2int(s, type) - case type - when 'utf8' then s.encode('utf-8').unpack('C*') - when 'utf16' then s.encode('utf-16').unpack('C*').drop(2) - when 'utf32' then s.encode('utf-32').unpack('C*').drop(4) - when 'integer', 'float' then raise "Cannot handle bit string as type #{type}" - else s.split('').map { |x| x.ord & 0xff } - end - end - - def int2bits(i, len) - format("%0#{len}b", i) - end - - def add_int(v, size) - x = v.to_i & (2**size - 1) # only get the bits specified in size - add_bits(int2bits(x, size)) - end - - def add_bits(s) - b = (@cur_bits + s).scan(/.{1,8}/) - @data += b[0..-2].map { |x| x.to_i(2) } - @cur_bits = b.last - end - - def value(encoding = 'utf-8') - # fill in the rest - rest = '0' * (8 - @cur_bits.length) + @cur_bits - arr = @data + [rest.to_i(2)] - s = arr.pack('C*') - s.force_encoding(encoding) unless encoding.nil? - s - end -end - -class ErlOm < Parslet::Transform - class Tuple < Array; end - class Identifier < String; end - - def self.assemble_binary(seq) - b = ErlangBitstream.new - seq.each { |i| b.add(i) } - b.value - end - - rule(string: simple(:x)) { x.to_s } - rule(string: []) { '' } - rule(integer: simple(:x)) { x.to_i } - rule(float: { integer: simple(:a), e: simple(:b) }) { (a+b).to_f } - rule(bool: 'true') { true } - rule(bool: 'false') { false } - rule(binary: subtree(:x)) { x.nil? ? '' : ErlOm.assemble_binary(x) } - rule(identifier: simple(:x)) { Identifier.new(x.to_s) } - rule(array: subtree(:x)) { Array(x) } - rule(tuple: subtree(:x)) { - x.nil? ? Tuple.new : Tuple.new(x) - } -end - -describe ErlOm do +describe ErlangTransform do def parse(c) - ErlOm.new.apply(ErlAx.new.parse(c)) + ErlangTransform.new.apply(ErlangParser.new.parse(c)) end it 'transforms and empty file' do @@ -354,37 +182,22 @@ describe ErlOm do end it 'transforms an empty tuple' do - _(parse('[{}].')).must_equal [ErlOm::Tuple.new] + _(parse('[{}].')).must_equal [ErlangTransform::Tuple.new] end it 'transforms a tuple with one element' do - _(parse('[{1}].')).must_equal [ErlOm::Tuple.new([1])] + _(parse('[{1}].')).must_equal [ErlangTransform::Tuple.new([1])] end it 'transforms a tuple with multiple elements' do - _(parse('[{id123, 1, 1.1}].')).must_equal [ErlOm::Tuple.new([ErlOm::Identifier.new('id123'), 1, 1.1])] + _(parse('[{id123, 1, 1.1}].')).must_equal [ErlangTransform::Tuple.new([ErlangTransform::Identifier.new('id123'), 1, 1.1])] end it 'transforms a deep tuple' do - _(parse('[{{{1}}}].')).must_equal [ErlOm::Tuple.new([ErlOm::Tuple.new([ErlOm::Tuple.new([1])])])] + _(parse('[{{{1}}}].')).must_equal [ErlangTransform::Tuple.new([ErlangTransform::Tuple.new([ErlangTransform::Tuple.new([1])])])] end it 'transforms a deep mix of tuple and array' do - _(parse('[{[{1}]}].')).must_equal [ErlOm::Tuple.new([[ErlOm::Tuple.new([1])]])] - end -end - -describe 'complex use-case' do - it 'parses a tricky rabbitmq config file' do - f = 'rabbitmq.config' - unless File.file?(f) - puts "NO #{f}, skipping this check!" - return - end - puts 'Wheeee, testing a real rabbitmq nasty config file' - c = File.read(f) - _(ErlAx.new.parse(c).to_s).must_be_instance_of String - res = ErlOm.new.apply(ErlAx.new.parse(c)) - # require "pry"; binding.pry + _(parse('[{[{1}]}].')).must_equal [ErlangTransform::Tuple.new([[ErlangTransform::Tuple.new([1])]])] end end