ios_parser 0.5.1-java
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
- data/.gitignore +37 -0
- data/.rubocop.yml +39 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +30 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +9 -0
- data/Guardfile +15 -0
- data/LICENSE.txt +675 -0
- data/README.md +90 -0
- data/Rakefile +20 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/state_machine.graphviz +41 -0
- data/doc/state_machine.png +0 -0
- data/ext/ios_parser/c_lexer/extconf.rb +4 -0
- data/ext/ios_parser/c_lexer/lexer.c +507 -0
- data/fixtures/complex_banner.txt +24 -0
- data/ios_parser.gemspec +25 -0
- data/lib/ios_parser/ios/command.rb +91 -0
- data/lib/ios_parser/ios/document.rb +54 -0
- data/lib/ios_parser/ios/queryable.rb +219 -0
- data/lib/ios_parser/ios.rb +73 -0
- data/lib/ios_parser/lexer.rb +327 -0
- data/lib/ios_parser/pure.rb +2 -0
- data/lib/ios_parser/version.rb +7 -0
- data/lib/ios_parser.rb +37 -0
- data/spec/lib/ios_parser/ios/queryable_spec.rb +157 -0
- data/spec/lib/ios_parser/ios_spec.rb +337 -0
- data/spec/lib/ios_parser/lexer_spec.rb +290 -0
- data/spec/lib/ios_parser_spec.rb +96 -0
- data/spec/spec_helper.rb +19 -0
- metadata +121 -0
@@ -0,0 +1,327 @@
|
|
1
|
+
module IOSParser
|
2
|
+
class PureLexer
|
3
|
+
LexError = IOSParser::LexError
|
4
|
+
|
5
|
+
attr_accessor :tokens, :token, :indents, :indent, :state, :char,
|
6
|
+
:string_terminator
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@text = ''
|
10
|
+
@token = ''
|
11
|
+
@tokens = []
|
12
|
+
@indent = 0
|
13
|
+
@indents = [0]
|
14
|
+
@state = :root
|
15
|
+
@token_char = 0
|
16
|
+
@this_char = -1
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(input_text)
|
20
|
+
@text = input_text
|
21
|
+
|
22
|
+
input_text.each_char.with_index do |c, i|
|
23
|
+
@this_char = i
|
24
|
+
self.char = c
|
25
|
+
send(state)
|
26
|
+
end
|
27
|
+
|
28
|
+
finalize
|
29
|
+
end
|
30
|
+
|
31
|
+
ROOT_TRANSITIONS = [
|
32
|
+
:space,
|
33
|
+
:banner_begin,
|
34
|
+
:certificate_begin,
|
35
|
+
:newline,
|
36
|
+
:comment,
|
37
|
+
:integer,
|
38
|
+
:quoted_string,
|
39
|
+
:word
|
40
|
+
].freeze
|
41
|
+
|
42
|
+
def root
|
43
|
+
@token_start ||= @this_char
|
44
|
+
|
45
|
+
ROOT_TRANSITIONS.each do |meth|
|
46
|
+
return send(meth) if send(:"#{meth}?")
|
47
|
+
end
|
48
|
+
|
49
|
+
raise LexError, "Unknown character #{char.inspect}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def root_line_start
|
53
|
+
if lead_comment?
|
54
|
+
comment
|
55
|
+
else
|
56
|
+
root
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def make_token(value, pos: nil)
|
61
|
+
pos ||= @token_start || @this_char
|
62
|
+
@token_start = nil
|
63
|
+
[pos, value]
|
64
|
+
end
|
65
|
+
|
66
|
+
def comment
|
67
|
+
self.state = :comment
|
68
|
+
update_indentation
|
69
|
+
self.state = :root if newline?
|
70
|
+
end
|
71
|
+
|
72
|
+
def comment?
|
73
|
+
char == '!'
|
74
|
+
end
|
75
|
+
|
76
|
+
def lead_comment?
|
77
|
+
char == '#' || char == '!'
|
78
|
+
end
|
79
|
+
|
80
|
+
def banner_begin
|
81
|
+
self.state = :banner
|
82
|
+
tokens << make_token(:BANNER_BEGIN)
|
83
|
+
@token_start = @this_char + 2
|
84
|
+
@banner_delimiter = char == "\n" ? 'EOF' : char
|
85
|
+
end
|
86
|
+
|
87
|
+
def banner_begin?
|
88
|
+
tokens[-2] && tokens[-2].last == 'banner'
|
89
|
+
end
|
90
|
+
|
91
|
+
def banner
|
92
|
+
if banner_end_char?
|
93
|
+
banner_end_char
|
94
|
+
elsif banner_end_string?
|
95
|
+
banner_end_string
|
96
|
+
else
|
97
|
+
token << char
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def banner_end_string
|
102
|
+
self.state = :root
|
103
|
+
token.chomp!(@banner_delimiter[0..-2])
|
104
|
+
tokens << make_token(token) << make_token(:BANNER_END)
|
105
|
+
self.token = ''
|
106
|
+
end
|
107
|
+
|
108
|
+
def banner_end_string?
|
109
|
+
@banner_delimiter.size > 1 && (token + char).end_with?(@banner_delimiter)
|
110
|
+
end
|
111
|
+
|
112
|
+
def banner_end_char
|
113
|
+
self.state = :root
|
114
|
+
banner_end_clean_token
|
115
|
+
tokens << make_token(token) << make_token(:BANNER_END)
|
116
|
+
self.token = ''
|
117
|
+
end
|
118
|
+
|
119
|
+
def banner_end_char?
|
120
|
+
char == @banner_delimiter && (@text[@this_char - 1] == "\n" ||
|
121
|
+
@text[@this_char + 1] == "\n")
|
122
|
+
end
|
123
|
+
|
124
|
+
def banner_end_clean_token
|
125
|
+
token.slice!(0) if token[0] == 'C'
|
126
|
+
token.slice!(0) if ["\n", ' '].include?(token[0])
|
127
|
+
end
|
128
|
+
|
129
|
+
def scrub_banner_garbage
|
130
|
+
tokens.each_index do |i|
|
131
|
+
next unless tokens[i + 1]
|
132
|
+
tokens.slice!(i + 1) if banner_garbage?(i)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def banner_garbage?(pos)
|
137
|
+
tokens[pos].last == :BANNER_END && tokens[pos + 1].last == 'C'
|
138
|
+
end
|
139
|
+
|
140
|
+
def certificate_begin?
|
141
|
+
tokens[-6] && tokens[-6].last == :INDENT &&
|
142
|
+
tokens[-5] && tokens[-5].last == 'certificate'
|
143
|
+
end
|
144
|
+
|
145
|
+
def certificate_begin
|
146
|
+
self.state = :certificate
|
147
|
+
indents.pop
|
148
|
+
tokens[-2..-1] = [make_token(:CERTIFICATE_BEGIN, pos: tokens[-1][0])]
|
149
|
+
certificate
|
150
|
+
end
|
151
|
+
|
152
|
+
def certificate
|
153
|
+
token[-5..-1] == "quit\n" ? certificate_end : token << char
|
154
|
+
end
|
155
|
+
|
156
|
+
def certificate_end
|
157
|
+
tokens.concat certificate_end_tokens
|
158
|
+
update_indentation
|
159
|
+
@token_start = @this_char
|
160
|
+
|
161
|
+
@token = ''
|
162
|
+
self.state = :line_start
|
163
|
+
self.indent = 0
|
164
|
+
root
|
165
|
+
end
|
166
|
+
|
167
|
+
def certificate_end_tokens
|
168
|
+
[
|
169
|
+
make_token(token[0..-6].gsub!(/\s+/, ' ').strip, pos: tokens[-1][0]),
|
170
|
+
make_token(:CERTIFICATE_END, pos: @this_char),
|
171
|
+
make_token(:EOL, pos: @this_char)
|
172
|
+
]
|
173
|
+
end
|
174
|
+
|
175
|
+
def integer
|
176
|
+
self.state = :integer
|
177
|
+
if dot? then decimal
|
178
|
+
elsif digit? then token << char
|
179
|
+
elsif word? then word
|
180
|
+
else root
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def integer_token
|
185
|
+
token[0] == '0' ? word_token : make_token(Integer(token))
|
186
|
+
end
|
187
|
+
|
188
|
+
def digit?
|
189
|
+
('0'..'9').cover? char
|
190
|
+
end
|
191
|
+
alias integer? digit?
|
192
|
+
|
193
|
+
def dot?
|
194
|
+
char == '.'
|
195
|
+
end
|
196
|
+
|
197
|
+
def decimal
|
198
|
+
self.state = :decimal
|
199
|
+
if digit? then token << char
|
200
|
+
elsif dot? then token << char
|
201
|
+
elsif word? then word
|
202
|
+
else root
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def decimal_token
|
207
|
+
if token.count('.') > 1 || token[-1] == '.'
|
208
|
+
word_token
|
209
|
+
else
|
210
|
+
make_token(Float(token))
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def decimal?
|
215
|
+
dot? || digit?
|
216
|
+
end
|
217
|
+
|
218
|
+
def word
|
219
|
+
self.state = :word
|
220
|
+
word? ? token << char : root
|
221
|
+
end
|
222
|
+
|
223
|
+
def word_token
|
224
|
+
make_token(token)
|
225
|
+
end
|
226
|
+
|
227
|
+
def word?
|
228
|
+
digit? || dot? ||
|
229
|
+
('a'..'z').cover?(char) ||
|
230
|
+
('A'..'Z').cover?(char) ||
|
231
|
+
['-', '+', '$', ':', '/', ',', '(', ')', '|', '*', '#', '=', '<', '>',
|
232
|
+
'!', '"', '&', '@', ';', '%', '~', '{', '}', "'", '?', '[', ']', '_',
|
233
|
+
'^', '\\', '`'].include?(char)
|
234
|
+
end
|
235
|
+
|
236
|
+
def space
|
237
|
+
delimit
|
238
|
+
self.indent += 1 if tokens.last && tokens.last.last == :EOL
|
239
|
+
end
|
240
|
+
|
241
|
+
def space?
|
242
|
+
char == ' ' || char == "\t" || char == "\r"
|
243
|
+
end
|
244
|
+
|
245
|
+
def quoted_string
|
246
|
+
self.state = :quoted_string
|
247
|
+
token << char
|
248
|
+
if string_terminator.nil?
|
249
|
+
self.string_terminator = char
|
250
|
+
elsif char == string_terminator
|
251
|
+
delimit
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def quoted_string_token
|
256
|
+
make_token(token)
|
257
|
+
end
|
258
|
+
|
259
|
+
def quoted_string?
|
260
|
+
char == '"' || char == "'"
|
261
|
+
end
|
262
|
+
|
263
|
+
def newline
|
264
|
+
delimit
|
265
|
+
return banner_begin if banner_begin?
|
266
|
+
self.state = :line_start
|
267
|
+
self.indent = 0
|
268
|
+
tokens << make_token(:EOL)
|
269
|
+
end
|
270
|
+
|
271
|
+
def newline?
|
272
|
+
char == "\n"
|
273
|
+
end
|
274
|
+
|
275
|
+
def line_start
|
276
|
+
if space?
|
277
|
+
self.indent += 1
|
278
|
+
else
|
279
|
+
update_indentation
|
280
|
+
root_line_start
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def delimit
|
285
|
+
return if token.empty?
|
286
|
+
|
287
|
+
unless respond_to?(:"#{state}_token")
|
288
|
+
pos = @token_start || @this_char
|
289
|
+
raise LexError, "Unterminated #{state} starting at #{pos}: "\
|
290
|
+
"#{@text[pos..pos + 20].inspect}"
|
291
|
+
end
|
292
|
+
|
293
|
+
tokens << send(:"#{state}_token")
|
294
|
+
self.state = :root
|
295
|
+
self.token = ''
|
296
|
+
end
|
297
|
+
|
298
|
+
def update_indentation
|
299
|
+
pop_dedent while 1 < indents.size && indent <= indents[-2]
|
300
|
+
push_indent if indent > indents.last
|
301
|
+
self.indent = 0
|
302
|
+
end
|
303
|
+
|
304
|
+
def pop_dedent
|
305
|
+
tokens << make_token(:DEDENT)
|
306
|
+
indents.pop
|
307
|
+
end
|
308
|
+
|
309
|
+
def push_indent
|
310
|
+
tokens << make_token(:INDENT)
|
311
|
+
indents.push(indent)
|
312
|
+
end
|
313
|
+
|
314
|
+
def finalize
|
315
|
+
if state == :quoted_string
|
316
|
+
pos = @text.rindex(string_terminator)
|
317
|
+
raise LexError, "Unterminated quoted string starting at #{pos}: "\
|
318
|
+
"#{@text[pos..pos + 20]}"
|
319
|
+
end
|
320
|
+
|
321
|
+
delimit
|
322
|
+
update_indentation
|
323
|
+
scrub_banner_garbage
|
324
|
+
tokens
|
325
|
+
end
|
326
|
+
end # class PureLexer
|
327
|
+
end # module IOSParser
|
data/lib/ios_parser.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module IOSParser
|
4
|
+
class LexError < StandardError; end
|
5
|
+
|
6
|
+
def self.lexer
|
7
|
+
if const_defined?(:PureLexer)
|
8
|
+
PureLexer
|
9
|
+
else
|
10
|
+
require_relative 'ios_parser/c_lexer'
|
11
|
+
CLexer
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
require 'ios_parser/lexer'
|
15
|
+
PureLexer
|
16
|
+
end
|
17
|
+
|
18
|
+
Lexer = lexer
|
19
|
+
end
|
20
|
+
|
21
|
+
require_relative 'ios_parser/ios'
|
22
|
+
|
23
|
+
module IOSParser
|
24
|
+
class << self
|
25
|
+
def parse(input)
|
26
|
+
IOSParser::IOS.new.call(input)
|
27
|
+
end
|
28
|
+
|
29
|
+
def hash_to_ios(hash)
|
30
|
+
IOSParser::IOS::Document.from_hash(hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def json_to_ios(text)
|
34
|
+
hash_to_ios JSON.parse(text)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
require 'ios_parser'
|
3
|
+
|
4
|
+
module IOSParser
|
5
|
+
class IOS
|
6
|
+
describe Queryable do
|
7
|
+
let(:input) { <<-END.unindent }
|
8
|
+
policy-map mypolicy_in
|
9
|
+
class some_service
|
10
|
+
police 300000000 1000000 exceed-action policed-dscp-transmit
|
11
|
+
set dscp cs1
|
12
|
+
class my_service
|
13
|
+
police 600000000 1000000 exceed-action policed-dscp-transmit
|
14
|
+
set dscp cs2
|
15
|
+
command_with_no_args
|
16
|
+
END
|
17
|
+
|
18
|
+
let(:expectation) { 'set dscp cs1' }
|
19
|
+
let(:parsed) { IOSParser.parse(input) }
|
20
|
+
subject { parsed.find(matcher.freeze).line }
|
21
|
+
|
22
|
+
describe '#find' do
|
23
|
+
context 'shortcut matcher' do
|
24
|
+
describe String do
|
25
|
+
let(:matcher) { 'set dscp cs1'.freeze }
|
26
|
+
it { should == expectation }
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Regexp do
|
30
|
+
let(:matcher) { /set .* cs1/ }
|
31
|
+
it { should == expectation }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Proc do
|
35
|
+
let(:expectation) { 'command_with_no_args' }
|
36
|
+
let(:matcher) { ->(c) { c.args.count == 1 } }
|
37
|
+
it { should == expectation }
|
38
|
+
end
|
39
|
+
end # context 'shortcut matcher' do
|
40
|
+
|
41
|
+
context 'explicit matcher form of shortcut matcher' do
|
42
|
+
describe String do
|
43
|
+
let(:matcher) { { starts_with: 'set dscp cs1'.freeze }.freeze }
|
44
|
+
it { should == expectation }
|
45
|
+
end
|
46
|
+
|
47
|
+
describe Regexp do
|
48
|
+
let(:matcher) { { line: /set .* cs1/ }.freeze }
|
49
|
+
it { should == expectation }
|
50
|
+
end
|
51
|
+
|
52
|
+
describe Proc do
|
53
|
+
let(:expectation) { 'command_with_no_args' }
|
54
|
+
let(:matcher) { { procedure: ->(c) { c.args.count == 1 } }.freeze }
|
55
|
+
it { should == expectation }
|
56
|
+
end
|
57
|
+
end # context 'explicit matcher form of shortcut matcher' do
|
58
|
+
|
59
|
+
context 'matcher: contains' do
|
60
|
+
describe String do
|
61
|
+
let(:matcher) { { contains: 'dscp cs1'.freeze }.freeze }
|
62
|
+
it { should == expectation }
|
63
|
+
end
|
64
|
+
|
65
|
+
describe Array do
|
66
|
+
let(:matcher) do
|
67
|
+
{ contains: ['dscp'.freeze, 'cs1'.freeze].freeze }.freeze
|
68
|
+
end
|
69
|
+
it { should == expectation }
|
70
|
+
end
|
71
|
+
end # context 'matcher: contains' do
|
72
|
+
|
73
|
+
context 'matcher: ends_with' do
|
74
|
+
let(:expectation) { 'class my_service' }
|
75
|
+
|
76
|
+
describe String do
|
77
|
+
let(:matcher) { { ends_with: 'my_service'.freeze }.freeze }
|
78
|
+
it { should == expectation }
|
79
|
+
end
|
80
|
+
|
81
|
+
describe Array do
|
82
|
+
let(:matcher) { { ends_with: ['my_service'.freeze].freeze }.freeze }
|
83
|
+
it { should == expectation }
|
84
|
+
end
|
85
|
+
end # context 'matcher: ends_with' do
|
86
|
+
|
87
|
+
context 'matcher: all' do
|
88
|
+
let(:matcher) { { all: ['set'.freeze, /cs1/].freeze }.freeze }
|
89
|
+
it { should == expectation }
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'matcher: parent' do
|
93
|
+
let(:matcher) { { parent: /police 3/ }.freeze }
|
94
|
+
it { should == expectation }
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'matcher: any' do
|
98
|
+
let(:matcher) { { any: [/asdf/, /cs1/, /qwerwqe/].freeze }.freeze }
|
99
|
+
it { should == expectation }
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'matcher: any (with a hash)' do
|
103
|
+
let(:matcher) do
|
104
|
+
{
|
105
|
+
any: { depth: 0, procedure: ->(c) { c.args.count == 1 } }.freeze
|
106
|
+
}.freeze
|
107
|
+
end
|
108
|
+
|
109
|
+
it do
|
110
|
+
expect(parsed.find_all(matcher).map(&:line))
|
111
|
+
.to eq ['policy-map mypolicy_in', 'command_with_no_args']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'matcher: depth' do
|
116
|
+
let(:matcher) { { depth: 3 }.freeze }
|
117
|
+
it { should == expectation }
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'matcher: none' do
|
121
|
+
let(:matcher) do
|
122
|
+
{ none: [/policy/, /class/, /police/].freeze }.freeze
|
123
|
+
end
|
124
|
+
it { should == expectation }
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'matcher: not_all' do
|
128
|
+
let(:matcher) do
|
129
|
+
{
|
130
|
+
all: {
|
131
|
+
none: /policy/,
|
132
|
+
not: /class/,
|
133
|
+
not_all: /police/
|
134
|
+
}.freeze
|
135
|
+
}.freeze
|
136
|
+
end
|
137
|
+
|
138
|
+
it do
|
139
|
+
expect(parsed.find(not_all: [/policy/, /class/]).line)
|
140
|
+
.to eq 'policy-map mypolicy_in'
|
141
|
+
end
|
142
|
+
it { should == expectation }
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'matcher: any_child' do
|
146
|
+
let(:matcher) { { not: { any_child: /dscp/ }.freeze }.freeze }
|
147
|
+
it { should == expectation }
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'matcher: no_child' do
|
151
|
+
let(:matcher) { { no_child: /dscp/ }.freeze }
|
152
|
+
it { should == expectation }
|
153
|
+
end
|
154
|
+
end # describe '#find' do
|
155
|
+
end # describe Queryable
|
156
|
+
end # class IOS
|
157
|
+
end # module IOSParser
|