ios_parser 0.3.0

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