ios_parser 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ require 'json'
2
+ require_relative 'queryable'
3
+
4
+ module IOSParser
5
+ class IOS
6
+ class Command
7
+ include Queryable, Enumerable
8
+ attr_accessor :args, :commands, :parent, :pos, :document
9
+
10
+ def initialize(args: [], commands: [],
11
+ parent: nil, pos: nil, document: nil)
12
+ @args = args
13
+ @commands = commands
14
+ @parent = parent
15
+ @pos = pos
16
+ @document = document
17
+ end
18
+
19
+ def name
20
+ args[0]
21
+ end
22
+
23
+ def ==(other)
24
+ args == other.args && commands == other.commands
25
+ end
26
+
27
+ def eql?(other)
28
+ self == other && self.class == other.class
29
+ end
30
+
31
+ def line
32
+ args.join(' ')
33
+ end
34
+
35
+ def path
36
+ parent ? parent.path + [parent.line] : []
37
+ end
38
+
39
+ def indentation(base: 0)
40
+ ' ' * (path.length - base)
41
+ end
42
+
43
+ def each
44
+ yield self
45
+ commands.each { |command| command.each { |cmd| yield cmd } }
46
+ end
47
+
48
+ def inspect
49
+ "<IOSParser::IOS::Command:0x#{object_id.to_s(16)} "\
50
+ "@args=#{args.inspect}, "\
51
+ "@commands=#{commands.inspect}, "\
52
+ "@pos=#{pos.inspect}, "\
53
+ "@document=<IOSParser::IOS::Document:0x#{document.object_id.to_s(16)}>>"
54
+ end
55
+
56
+ def to_s(dedent: false)
57
+ indent_opts = { base: dedent ? path.length : 0 }
58
+ map { |cmd| "#{cmd.indentation(indent_opts)}#{cmd.line}\n" }.join
59
+ end
60
+
61
+ def to_hash
62
+ {
63
+ args: args,
64
+ commands: commands.map(&:to_hash),
65
+ pos: pos
66
+ }
67
+ end
68
+
69
+ def to_json
70
+ JSON.dump(to_hash)
71
+ end
72
+
73
+ class << self
74
+ def from_hash(hash, parent = nil)
75
+ hash[:parent] = parent
76
+ [:args, :commands, :pos].each do |key|
77
+ val = hash.delete(key.to_s)
78
+ hash[key] ||= val
79
+ end
80
+
81
+ hash[:commands] ||= []
82
+ hash[:commands].each_index do |i|
83
+ hash[:commands][i] = from_hash(hash[:commands][i])
84
+ end
85
+ new(hash)
86
+ end
87
+ end
88
+ end # class Command
89
+ end # class IOS
90
+ end # module IOSParser
@@ -0,0 +1,53 @@
1
+ require 'json'
2
+ require_relative 'queryable'
3
+ require_relative 'command'
4
+
5
+ module IOSParser
6
+ class IOS
7
+ class Document
8
+ include Queryable, Enumerable
9
+ attr_accessor :commands, :parent, :source
10
+
11
+ def initialize(source)
12
+ @commands = []
13
+ @parent = nil
14
+ @source = source
15
+ end
16
+
17
+ [:[], :push].each do |method|
18
+ define_method(method) { |*args| commands.send(method, *args) }
19
+ end
20
+
21
+ def each
22
+ commands.each { |command| command.each { |cmd| yield cmd } }
23
+ end
24
+
25
+ def to_s(dedent: false)
26
+ base = dedent ? indentation : 0
27
+ map { |cmd| "#{cmd.indentation(base: base)}#{cmd.line}\n" }.join
28
+ end
29
+
30
+ def to_hash
31
+ { commands: commands.map(&:to_hash) }
32
+ end
33
+
34
+ def to_json
35
+ JSON.dump(to_hash)
36
+ end
37
+
38
+ class << self
39
+ def from_hash(hash)
40
+ hash[:parent] = parent
41
+ [:commands, :source].each do |key|
42
+ val = hash.delete(key.to_s)
43
+ hash[key] = val unless hash.key?(key)
44
+ end
45
+
46
+ new(source).tap do |doc|
47
+ doc.push(*(hash[:commands].map { |c| Command.from_hash(c) }))
48
+ end
49
+ end
50
+ end # class << self
51
+ end # class Document
52
+ end # class IOS
53
+ end # class IOSParser
@@ -0,0 +1,218 @@
1
+ module IOSParser
2
+ class IOS
3
+ module Queryable
4
+ def find_all(expr, &blk)
5
+ _find_all(MatcherReader.query_expression(expr), &blk)
6
+ end
7
+
8
+ def find(expr, &blk)
9
+ _find(MatcherReader.query_expression(expr), &blk)
10
+ end
11
+
12
+ def _find_all(expr, &blk)
13
+ [].tap do |ret|
14
+ commands.each do |command|
15
+ if match_expr(expr, command)
16
+ ret << command
17
+ yield(command) if blk
18
+ end
19
+
20
+ ret.push(*command._find_all(expr, &blk))
21
+ end
22
+ end
23
+ end
24
+
25
+ def match_expr(expr, command)
26
+ expr.each_pair.all? { |pred, arg| Matcher.send(pred, arg, command) }
27
+ end
28
+
29
+ def _find(expr, &blk)
30
+ _find_all(expr) do |command|
31
+ yield(command) if blk
32
+ return command
33
+ end
34
+ nil
35
+ end
36
+
37
+ module MatcherReader
38
+ class << self
39
+ def query_expression(raw)
40
+ case raw
41
+ when Hash then query_expression_hash(raw)
42
+ when Proc then { procedure: procedure(raw) }
43
+ when Regexp then { line: line(raw) }
44
+ when String, Array then { starts_with: starts_with(raw) }
45
+ else raise("Invalid query: #{raw.inspect}")
46
+ end
47
+ end
48
+ alias parent query_expression
49
+ alias any_child query_expression
50
+ alias no_child query_expression
51
+
52
+ def query_expression_hash(raw)
53
+ raw.each_pair { |pred, arg| raw[pred] &&= send(pred, arg) }
54
+ raw
55
+ end
56
+
57
+ def name(expr)
58
+ expr
59
+ end
60
+
61
+ def starts_with(expr)
62
+ case expr
63
+ when String then expr.split
64
+ when Array then expr
65
+ else raise("Invalid #{__method__} condition in query: #{expr}")
66
+ end
67
+ end
68
+ alias contains starts_with
69
+ alias ends_with starts_with
70
+
71
+ def procedure(expr)
72
+ unless expr.respond_to?(:call)
73
+ raise("Invalid procedure in query: #{expr}")
74
+ end
75
+ expr
76
+ end
77
+
78
+ def line(expr)
79
+ case expr
80
+ when String, Regexp then expr
81
+ when Array then expr.join(' ')
82
+ else raise("Invalid line condition in query: #{expr}")
83
+ end
84
+ end
85
+
86
+ def array_wrap_and_map(expr)
87
+ (expr.respond_to?(:map) && !expr.is_a?(Hash) ? expr : [expr])
88
+ .map { |e| query_expression(e) }
89
+ end
90
+ alias any array_wrap_and_map
91
+ alias all array_wrap_and_map
92
+ alias none array_wrap_and_map
93
+ alias not array_wrap_and_map
94
+ alias not_all array_wrap_and_map
95
+
96
+ def depth(expr)
97
+ unless expr.is_a?(Integer) || (expr.is_a?(Range) &&
98
+ expr.first.is_a?(Integer) &&
99
+ expr.last.is_a?(Integer))
100
+ raise("Invalid depth constraint in query: #{expr}")
101
+ end
102
+ expr
103
+ end
104
+ end # class << self
105
+ end # module MatcherReader
106
+
107
+ module Matcher
108
+ class << self
109
+ def name(expr, command)
110
+ expr === command.name
111
+ end
112
+
113
+ def starts_with(req_ary, command)
114
+ (0..req_ary.length - 1).all? do |i|
115
+ compare_string_or_case(req_ary[i], command.args[i])
116
+ end
117
+ end
118
+
119
+ def contains(req_ary, command)
120
+ (0..command.args.length - req_ary.length).any? do |j|
121
+ (0..req_ary.length - 1).all? do |i|
122
+ compare_string_or_case(req_ary[i], command.args[i + j])
123
+ end
124
+ end
125
+ end
126
+
127
+ def ends_with(req_ary, command)
128
+ (1..req_ary.length).all? do |i|
129
+ compare_string_or_case(req_ary[-i], command.args[-1])
130
+ end
131
+ end
132
+
133
+ def compare_string_or_case(a, b)
134
+ case a
135
+ when String
136
+ a == b.to_s
137
+ else
138
+ a === b
139
+ end
140
+ end
141
+
142
+ def procedure(expr, command)
143
+ expr.call(command)
144
+ end
145
+
146
+ def line(expr, command)
147
+ expr === command.line
148
+ end
149
+
150
+ def parent(expr, command)
151
+ expr.each_pair.all? do |pred, arg|
152
+ command.parent && send(pred, arg, command.parent)
153
+ end
154
+ end
155
+
156
+ def any_child(expr, command)
157
+ command.find(expr)
158
+ end
159
+
160
+ def no_child(expr, command)
161
+ !command.find(expr)
162
+ end
163
+
164
+ def any(expressions, command)
165
+ expressions.any? do |expr|
166
+ expr.each_pair.any? do |pred, arg|
167
+ send(pred, arg, command)
168
+ end
169
+ end
170
+ end
171
+
172
+ def all(expressions, command)
173
+ expressions.all? do |expr|
174
+ expr.each_pair.all? do |pred, arg|
175
+ send(pred, arg, command)
176
+ end
177
+ end
178
+ end
179
+
180
+ def not_all(expressions, command)
181
+ !expressions.all? { |expr| all([expr], command) }
182
+ end
183
+ alias not not_all
184
+
185
+ def none(expressions, command)
186
+ !expressions.any? { |expr| all([expr], command) }
187
+ end
188
+
189
+ def depth(expr, command)
190
+ case expr
191
+ when Integer then depth_exact(expr, command)
192
+ when Range then depth_range(expr, command)
193
+ end
194
+ end
195
+
196
+ def depth_exact(expr, command)
197
+ _depth(expr, command) == expr
198
+ end
199
+
200
+ def depth_range(expr, command)
201
+ _depth(expr.last, command) > expr.first
202
+ end
203
+
204
+ def _depth(max, command)
205
+ level = 0
206
+ ptr = command
207
+ while ptr.parent
208
+ ptr = ptr.parent
209
+ level += 1
210
+ return Float::MIN if level > max
211
+ end
212
+ level
213
+ end
214
+ end # class << self
215
+ end # module Matcher
216
+ end # module Queryable
217
+ end # class IOS
218
+ end # module IOSParser
@@ -0,0 +1,70 @@
1
+ require_relative 'ios/document'
2
+
3
+ module IOSParser
4
+ class IOS
5
+ attr_accessor :lexer, :tokens, :source, :document
6
+
7
+ def initialize(parent: nil, lexer: IOSParser::Lexer.new)
8
+ @document = Document.new(nil)
9
+ @parent = parent
10
+ @lexer = lexer
11
+ end
12
+
13
+ def tokens
14
+ @tokens ||= lexer.call(@source)
15
+ end
16
+
17
+ def call(source)
18
+ unless source.respond_to? :each_char
19
+ raise ArgumentError, 'Provided configuration source is invalid.'
20
+ end
21
+ @source = source
22
+ @document.source = source
23
+ @document.push(*section) until tokens.empty?
24
+ @document
25
+ end
26
+
27
+ def section(parent = nil)
28
+ [].tap do |commands|
29
+ until tokens.empty? || tokens.first.last == :DEDENT
30
+ commands.push(command(parent, @document))
31
+ end
32
+ tokens.shift # discard :DEDENT
33
+ end
34
+ end
35
+
36
+ def command(parent = nil, document = nil)
37
+ pos = tokens.first.first
38
+ opts = { args: arguments, parent: parent, document: document, pos: pos }
39
+
40
+ Command.new(opts).tap do |cmd|
41
+ cmd.commands = subsections(cmd)
42
+ end
43
+ end
44
+
45
+ def arguments_to_discard
46
+ [:INDENT, :DEDENT,
47
+ :CERTIFICATE_BEGIN, :CERTIFICATE_END,
48
+ :BANNER_BEGIN, :BANNER_END]
49
+ end
50
+
51
+ def arguments
52
+ [].tap do |args|
53
+ until tokens.empty? || tokens.first.last == :EOL
54
+ _, arg = tokens.shift
55
+ args << arg unless arguments_to_discard.include?(arg)
56
+ end
57
+ tokens.shift # discard :EOL
58
+ end
59
+ end
60
+
61
+ def subsections(parent = nil)
62
+ if !tokens.empty? && tokens.first.last == :INDENT
63
+ tokens.shift # discard :INDENT
64
+ section(parent)
65
+ else
66
+ []
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,278 @@
1
+ module IOSParser
2
+ class PureLexer
3
+ attr_accessor :tokens, :token, :indents, :indent, :state, :char,
4
+ :string_terminator
5
+
6
+ def initialize
7
+ @text = ''
8
+ @token = ''
9
+ @tokens = []
10
+ @indent = 0
11
+ @indents = [0]
12
+ @state = :root
13
+ @token_char = 0
14
+ @this_char = -1
15
+ end
16
+
17
+ def call(input_text)
18
+ initialize
19
+
20
+ input_text.each_char.with_index do |c, i|
21
+ @this_char = i
22
+ self.char = c
23
+ send(state)
24
+ end
25
+
26
+ delimit
27
+ update_indentation
28
+ scrub_banner_garbage
29
+ tokens
30
+ end
31
+
32
+ ROOT_TRANSITIONS = [
33
+ :space,
34
+ :banner_begin,
35
+ :certificate_begin,
36
+ :newline,
37
+ :comment,
38
+ :integer,
39
+ :quoted_string,
40
+ :word
41
+ ].freeze
42
+
43
+ def root
44
+ @token_start ||= @this_char
45
+
46
+ ROOT_TRANSITIONS.each do |meth|
47
+ return send(meth) if send(:"#{meth}?")
48
+ end
49
+
50
+ raise LexError, "Unknown character #{char.inspect}"
51
+ end
52
+
53
+ def make_token(value, pos: nil)
54
+ pos ||= @token_start || @this_char
55
+ @token_start = nil
56
+ [pos, value]
57
+ end
58
+
59
+ def comment
60
+ self.state = :comment
61
+ update_indentation
62
+ self.state = :root if newline?
63
+ end
64
+
65
+ def comment?
66
+ char == '#' || char == '!'
67
+ end
68
+
69
+ def banner_begin
70
+ self.state = :banner
71
+ tokens << make_token(:BANNER_BEGIN)
72
+ @token_start = @this_char + 2
73
+ @banner_delimiter = char
74
+ end
75
+
76
+ def banner_begin?
77
+ tokens[-2] && tokens[-2].last == 'banner'
78
+ end
79
+
80
+ def banner
81
+ char == @banner_delimiter ? banner_end : token << char
82
+ end
83
+
84
+ def banner_end
85
+ self.state = :root
86
+ banner_end_clean_token
87
+ tokens << make_token(token) << make_token(:BANNER_END)
88
+ self.token = ''
89
+ end
90
+
91
+ def banner_end_clean_token
92
+ token.slice!(0) if token[0] == 'C'
93
+ token.slice!(0) if ["\n", ' '].include?(token[0])
94
+ token.chomp!("\n")
95
+ end
96
+
97
+ def scrub_banner_garbage
98
+ tokens.each_index do |i|
99
+ next unless tokens[i + 1]
100
+ tokens.slice!(i + 1) if banner_garbage?(i)
101
+ end
102
+ end
103
+
104
+ def banner_garbage?(i)
105
+ tokens[i].last == :BANNER_END && tokens[i + 1].last == 'C'
106
+ end
107
+
108
+ def certificate_begin?
109
+ tokens[-6] && tokens[-6].last == :INDENT &&
110
+ tokens[-5] && tokens[-5].last == 'certificate'
111
+ end
112
+
113
+ def certificate_begin
114
+ self.state = :certificate
115
+ indents.pop
116
+ tokens[-2..-1] = [make_token(:CERTIFICATE_BEGIN, pos: tokens[-1][0])]
117
+ certificate
118
+ end
119
+
120
+ def certificate
121
+ token[-5..-1] == "quit\n" ? certificate_end : token << char
122
+ end
123
+
124
+ def certificate_end
125
+ tokens.concat certificate_end_tokens
126
+ update_indentation
127
+ @token_start = @this_char
128
+
129
+ @token = ''
130
+ self.state = :line_start
131
+ self.indent = 0
132
+ root
133
+ end
134
+
135
+ def certificate_end_tokens
136
+ [
137
+ make_token(token[0..-6].gsub!(/\s+/, ' ').strip, pos: tokens[-1][0]),
138
+ make_token(:CERTIFICATE_END, pos: @this_char),
139
+ make_token(:EOL, pos: @this_char)
140
+ ]
141
+ end
142
+
143
+ def integer
144
+ self.state = :integer
145
+ case
146
+ when dot? then decimal
147
+ when digit? then token << char
148
+ when word? then word
149
+ else root
150
+ end
151
+ end
152
+
153
+ def integer_token
154
+ token[0] == '0' ? word_token : make_token(Integer(token))
155
+ end
156
+
157
+ def digit?
158
+ ('0'..'9').cover? char
159
+ end
160
+ alias integer? digit?
161
+
162
+ def dot?
163
+ char == '.'
164
+ end
165
+
166
+ def decimal
167
+ self.state = :decimal
168
+ case
169
+ when digit? then token << char
170
+ when dot? then token << char
171
+ when word? then word
172
+ else root
173
+ end
174
+ end
175
+
176
+ def decimal_token
177
+ if token.count('.') > 1 || token[-1] == '.'
178
+ word_token
179
+ else
180
+ make_token(Float(token))
181
+ end
182
+ end
183
+
184
+ def decimal?
185
+ dot? || digit?
186
+ end
187
+
188
+ def word
189
+ self.state = :word
190
+ word? ? token << char : root
191
+ end
192
+
193
+ def word_token
194
+ make_token(token)
195
+ end
196
+
197
+ def word?
198
+ digit? || dot? ||
199
+ ('a'..'z').cover?(char) ||
200
+ ('A'..'Z').cover?(char) ||
201
+ ['-', '+', '$', ':', '/', ',', '(', ')', '|', '*', '#', '=', '<', '>',
202
+ '!', '"', '&', '@', ';', '%', '~', '{', '}', "'", '?', '[', ']', '_',
203
+ '^', '\\'].include?(char)
204
+ end
205
+
206
+ def space
207
+ delimit
208
+ self.indent += 1 if tokens.last && tokens.last.last == :EOL
209
+ end
210
+
211
+ def space?
212
+ char == ' ' || char == "\t" || char == "\r"
213
+ end
214
+
215
+ def quoted_string
216
+ self.state = :quoted_string
217
+ token << char
218
+ if string_terminator.nil?
219
+ self.string_terminator = char
220
+ elsif char == string_terminator
221
+ delimit
222
+ end
223
+ end
224
+
225
+ def quoted_string_token
226
+ make_token(token)
227
+ end
228
+
229
+ def quoted_string?
230
+ char == '"' || char == "'"
231
+ end
232
+
233
+ def newline
234
+ delimit
235
+ self.state = :line_start
236
+ self.indent = 0
237
+ tokens << make_token(:EOL)
238
+ end
239
+
240
+ def newline?
241
+ char == "\n"
242
+ end
243
+
244
+ def line_start
245
+ if space?
246
+ self.indent += 1
247
+ else
248
+ update_indentation
249
+ root
250
+ end
251
+ end
252
+
253
+ def delimit
254
+ return if token.empty?
255
+ tokens << send(:"#{state}_token")
256
+ self.state = :root
257
+ self.token = ''
258
+ end
259
+
260
+ def update_indentation
261
+ pop_dedent while 1 < indents.size && indent <= indents[-2]
262
+ push_indent if indent > indents.last
263
+ self.indent = 0
264
+ end
265
+
266
+ def pop_dedent
267
+ tokens << make_token(:DEDENT)
268
+ indents.pop
269
+ end
270
+
271
+ def push_indent
272
+ tokens << make_token(:INDENT)
273
+ indents.push(indent)
274
+ end
275
+
276
+ class LexError < StandardError; end
277
+ end # class PureLexer
278
+ end # module IOSParser
@@ -0,0 +1,2 @@
1
+ require_relative 'lexer'
2
+ require_relative '../ios_parser'
@@ -0,0 +1,7 @@
1
+ module IOSParser
2
+ class << self
3
+ def version
4
+ '0.3.0'
5
+ end
6
+ end
7
+ end