rasn2 0.16.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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +4 -0
- data/lib/rasn1/errors.rb +55 -0
- data/lib/rasn1/helpers/colorize.rb +76 -0
- data/lib/rasn1/model.rb +831 -0
- data/lib/rasn1/schema_parser.rb +470 -0
- data/lib/rasn1/tracer.rb +200 -0
- data/lib/rasn1/types/any.rb +98 -0
- data/lib/rasn1/types/base.rb +675 -0
- data/lib/rasn1/types/bit_string.rb +100 -0
- data/lib/rasn1/types/bmp_string.rb +22 -0
- data/lib/rasn1/types/boolean.rb +57 -0
- data/lib/rasn1/types/choice.rb +158 -0
- data/lib/rasn1/types/constrained.rb +51 -0
- data/lib/rasn1/types/constructed.rb +51 -0
- data/lib/rasn1/types/enumerated.rb +44 -0
- data/lib/rasn1/types/generalized_time.rb +156 -0
- data/lib/rasn1/types/ia5_string.rb +21 -0
- data/lib/rasn1/types/integer.rb +166 -0
- data/lib/rasn1/types/null.rb +43 -0
- data/lib/rasn1/types/numeric_string.rb +41 -0
- data/lib/rasn1/types/object_id.rb +64 -0
- data/lib/rasn1/types/octet_string.rb +52 -0
- data/lib/rasn1/types/primitive.rb +13 -0
- data/lib/rasn1/types/printable_string.rb +42 -0
- data/lib/rasn1/types/sequence.rb +105 -0
- data/lib/rasn1/types/sequence_of.rb +199 -0
- data/lib/rasn1/types/set.rb +28 -0
- data/lib/rasn1/types/set_of.rb +18 -0
- data/lib/rasn1/types/tag.rb +189 -0
- data/lib/rasn1/types/universal_string.rb +22 -0
- data/lib/rasn1/types/utc_time.rb +67 -0
- data/lib/rasn1/types/utf8_string.rb +21 -0
- data/lib/rasn1/types/visible_string.rb +30 -0
- data/lib/rasn1/types.rb +149 -0
- data/lib/rasn1/value_notation.rb +256 -0
- data/lib/rasn1/version.rb +6 -0
- data/lib/rasn1/wrapper.rb +279 -0
- data/lib/rasn1.rb +51 -0
- metadata +123 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RASN1
|
|
4
|
+
# Parser for ASN.1 text schema definitions.
|
|
5
|
+
#
|
|
6
|
+
# Parses ASN.1 module definitions and generates {Model} subclasses from them.
|
|
7
|
+
#
|
|
8
|
+
# @example Parse a schema file
|
|
9
|
+
# models = RASN1::SchemaParser.parse_file('path/to/schema.asn')
|
|
10
|
+
# # => { 'PersonnelRecord' => PersonnelRecord (class < RASN1::Model) }
|
|
11
|
+
#
|
|
12
|
+
# @example Parse a schema string
|
|
13
|
+
# schema = <<~ASN1
|
|
14
|
+
# MyModule DEFINITIONS ::=
|
|
15
|
+
# BEGIN
|
|
16
|
+
# MyRecord ::= SEQUENCE {
|
|
17
|
+
# name PrintableString,
|
|
18
|
+
# age INTEGER
|
|
19
|
+
# }
|
|
20
|
+
# END
|
|
21
|
+
# ASN1
|
|
22
|
+
# models = RASN1::SchemaParser.parse(schema)
|
|
23
|
+
#
|
|
24
|
+
# @author rickmark
|
|
25
|
+
# @since 0.16.0
|
|
26
|
+
module SchemaParser
|
|
27
|
+
# ASN.1 type name to RASN1 type DSL method mapping
|
|
28
|
+
TYPE_MAP = {
|
|
29
|
+
'BOOLEAN' => :boolean,
|
|
30
|
+
'INTEGER' => :integer,
|
|
31
|
+
'BIT STRING' => :bit_string,
|
|
32
|
+
'OCTET STRING' => :octet_string,
|
|
33
|
+
'NULL' => :null,
|
|
34
|
+
'OBJECT IDENTIFIER' => :objectid,
|
|
35
|
+
'ENUMERATED' => :enumerated,
|
|
36
|
+
'UTF8String' => :utf8_string,
|
|
37
|
+
'PrintableString' => :printable_string,
|
|
38
|
+
'IA5String' => :ia5_string,
|
|
39
|
+
'VisibleString' => :visible_string,
|
|
40
|
+
'NumericString' => :numeric_string,
|
|
41
|
+
'BMPString' => :bmp_string,
|
|
42
|
+
'UniversalString' => :universal_string,
|
|
43
|
+
'UTCTime' => :utc_time,
|
|
44
|
+
'GeneralizedTime' => :generalized_time
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Constructed type names
|
|
48
|
+
CONSTRUCTED_TYPES = %w[SEQUENCE SET].freeze
|
|
49
|
+
|
|
50
|
+
# Error raised when parsing an ASN.1 schema fails
|
|
51
|
+
class ParseError < RASN1::Error; end
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# Parse an ASN.1 schema from a file and generate Model classes.
|
|
55
|
+
# @param [String] filename path to the ASN.1 schema file
|
|
56
|
+
# @param [Module] namespace module in which to define the generated classes (default: Object)
|
|
57
|
+
# @return [Hash{String => Class}] hash mapping type names to generated Model subclasses
|
|
58
|
+
# @raise [ParseError] if the schema cannot be parsed
|
|
59
|
+
def parse_file(filename, namespace: Object)
|
|
60
|
+
parse(File.read(filename), namespace: namespace)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Parse an ASN.1 schema string and generate Model classes.
|
|
64
|
+
# @param [String] schema the ASN.1 schema text
|
|
65
|
+
# @param [Module] namespace module in which to define the generated classes (default: Object)
|
|
66
|
+
# @return [Hash{String => Class}] hash mapping type names to generated Model subclasses
|
|
67
|
+
# @raise [ParseError] if the schema cannot be parsed
|
|
68
|
+
def parse(schema, namespace: Object)
|
|
69
|
+
mod = parse_module(schema)
|
|
70
|
+
build_models(mod, namespace: namespace)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Parse the module header and body
|
|
76
|
+
# @return [Hash] parsed module structure with :name, :tag_default, :definitions
|
|
77
|
+
def parse_module(schema)
|
|
78
|
+
lines = schema.strip
|
|
79
|
+
# Match module header: ModuleName DEFINITIONS [tag_default] ::=
|
|
80
|
+
header_match = lines.match(/\A(\w+)\s+DEFINITIONS\s*(.*?)\s*::=\s*\n\s*BEGIN\s*\n(.*)\nEND\s*\z/m)
|
|
81
|
+
raise ParseError, 'Invalid ASN.1 module format' unless header_match
|
|
82
|
+
|
|
83
|
+
mod_name = header_match[1]
|
|
84
|
+
tag_options = header_match[2].strip
|
|
85
|
+
body = header_match[3]
|
|
86
|
+
|
|
87
|
+
tag_default = parse_tag_default(tag_options)
|
|
88
|
+
|
|
89
|
+
definitions = parse_definitions(body)
|
|
90
|
+
|
|
91
|
+
{ name: mod_name, tag_default: tag_default, definitions: definitions }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse tag default from module header
|
|
95
|
+
# @return [Symbol,nil] :implicit, :explicit, or nil
|
|
96
|
+
def parse_tag_default(tag_str)
|
|
97
|
+
case tag_str
|
|
98
|
+
when /IMPLICIT\s+TAGS/i then :implicit
|
|
99
|
+
when /EXPLICIT\s+TAGS/i then :explicit
|
|
100
|
+
when /AUTOMATIC\s+TAGS/i then :automatic
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Parse all type definitions from the module body
|
|
105
|
+
# @return [Array<Hash>] list of type definitions
|
|
106
|
+
def parse_definitions(body)
|
|
107
|
+
definitions = []
|
|
108
|
+
# Split on type assignments: TypeName ::= ...
|
|
109
|
+
# We need to handle multi-line definitions
|
|
110
|
+
remaining = body.strip
|
|
111
|
+
|
|
112
|
+
while remaining && !remaining.empty?
|
|
113
|
+
# Match: TypeName ::= TypeDefinition
|
|
114
|
+
match = remaining.match(/\A\s*(\w+)\s*::=\s*/m)
|
|
115
|
+
break unless match
|
|
116
|
+
|
|
117
|
+
type_name = match[1]
|
|
118
|
+
rest = match.post_match
|
|
119
|
+
|
|
120
|
+
type_def, rest = parse_type_definition(rest)
|
|
121
|
+
definitions << { name: type_name, type: type_def }
|
|
122
|
+
remaining = rest&.strip
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
definitions
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Parse a single type definition
|
|
129
|
+
# @return [Array(Hash, String)] the parsed type and remaining text
|
|
130
|
+
def parse_type_definition(text)
|
|
131
|
+
text = text.strip
|
|
132
|
+
|
|
133
|
+
# Check for constructed types: SEQUENCE { ... }, SET { ... }
|
|
134
|
+
case text
|
|
135
|
+
when /\A(SEQUENCE|SET)\s+OF\b/
|
|
136
|
+
parse_of_type(text)
|
|
137
|
+
when /\A(SEQUENCE|SET)\s*\{/
|
|
138
|
+
parse_constructed_type(text)
|
|
139
|
+
when /\A(CHOICE)\s*\{/
|
|
140
|
+
parse_choice_type(text)
|
|
141
|
+
else
|
|
142
|
+
parse_simple_type_definition(text)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Parse SEQUENCE { ... } or SET { ... }
|
|
147
|
+
# @return [Array(Hash, String)]
|
|
148
|
+
def parse_constructed_type(text)
|
|
149
|
+
match = text.match(/\A(SEQUENCE|SET)\s*\{/m)
|
|
150
|
+
raise ParseError, "Expected SEQUENCE or SET, got: #{text[0..20]}" unless match
|
|
151
|
+
|
|
152
|
+
kind = match[1].downcase.to_sym
|
|
153
|
+
rest = match.post_match
|
|
154
|
+
members, rest = parse_members(rest)
|
|
155
|
+
|
|
156
|
+
[{ kind: kind, members: members }, rest]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Parse CHOICE { ... }
|
|
160
|
+
# @return [Array(Hash, String)]
|
|
161
|
+
def parse_choice_type(text)
|
|
162
|
+
match = text.match(/\A(CHOICE)\s*\{/m)
|
|
163
|
+
raise ParseError, "Expected CHOICE, got: #{text[0..20]}" unless match
|
|
164
|
+
|
|
165
|
+
rest = match.post_match
|
|
166
|
+
members, rest = parse_members(rest)
|
|
167
|
+
|
|
168
|
+
[{ kind: :choice, members: members }, rest]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Parse SEQUENCE OF or SET OF
|
|
172
|
+
# @return [Array(Hash, String)]
|
|
173
|
+
def parse_of_type(text)
|
|
174
|
+
match = text.match(/\A(SEQUENCE|SET)\s+OF\s+/m)
|
|
175
|
+
raise ParseError, 'Expected SEQUENCE OF or SET OF' unless match
|
|
176
|
+
|
|
177
|
+
kind = :"#{match[1].downcase}_of"
|
|
178
|
+
rest = match.post_match
|
|
179
|
+
element_type, rest = parse_type_reference(rest)
|
|
180
|
+
|
|
181
|
+
[{ kind: kind, element_type: element_type }, rest]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Parse a simple (non-constructed) top-level type definition (type alias)
|
|
185
|
+
# @return [Array(Hash, String)]
|
|
186
|
+
def parse_simple_type_definition(text)
|
|
187
|
+
type_name, = parse_type_reference(text)
|
|
188
|
+
raise ParseError, "Unsupported simple type alias at top level: #{type_name}" unless TYPE_MAP.key?(type_name) || type_name.is_a?(Hash)
|
|
189
|
+
|
|
190
|
+
# Simple type aliases are not directly supported as Model subclasses
|
|
191
|
+
# They would need to be handled as constrained types
|
|
192
|
+
raise ParseError, 'Simple type aliases are not yet supported as top-level definitions'
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Parse the members inside { ... }
|
|
196
|
+
# @return [Array(Array<Hash>, String)]
|
|
197
|
+
def parse_members(text)
|
|
198
|
+
members = []
|
|
199
|
+
rest = text.strip
|
|
200
|
+
|
|
201
|
+
loop do
|
|
202
|
+
# Check for closing brace
|
|
203
|
+
if rest.match?(/\A\s*\}/)
|
|
204
|
+
rest = rest.sub(/\A\s*\}/, '')
|
|
205
|
+
break
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Remove leading comma
|
|
209
|
+
rest = rest.sub(/\A\s*,\s*/, '') unless members.empty?
|
|
210
|
+
|
|
211
|
+
member, rest = parse_member(rest.strip)
|
|
212
|
+
members << member if member
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
[members, rest]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Parse a single member: name Type [constraints] [DEFAULT value | OPTIONAL]
|
|
219
|
+
# @return [Array(Hash, String)]
|
|
220
|
+
def parse_member(text)
|
|
221
|
+
text = text.strip
|
|
222
|
+
return [nil, text] if text.empty? || text.start_with?('}')
|
|
223
|
+
|
|
224
|
+
# Match: fieldName Type
|
|
225
|
+
match = text.match(/\A(\w+)\s+/)
|
|
226
|
+
raise ParseError, "Expected field name, got: #{text[0..30]}" unless match
|
|
227
|
+
|
|
228
|
+
field_name = match[1]
|
|
229
|
+
rest = match.post_match
|
|
230
|
+
|
|
231
|
+
type_info, rest = parse_type_with_constraints(rest)
|
|
232
|
+
|
|
233
|
+
member = { name: field_name, type: type_info[:type], options: type_info[:options] }
|
|
234
|
+
[member, rest]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Parse a type followed by optional constraints, DEFAULT, or OPTIONAL
|
|
238
|
+
# @return [Array(Hash, String)]
|
|
239
|
+
def parse_type_with_constraints(text)
|
|
240
|
+
text = text.strip
|
|
241
|
+
type_name, rest = parse_type_reference(text)
|
|
242
|
+
|
|
243
|
+
options = {}
|
|
244
|
+
|
|
245
|
+
# Parse constraints like (0..120)
|
|
246
|
+
rest = rest.strip
|
|
247
|
+
if rest.start_with?('(')
|
|
248
|
+
_constraint, rest = parse_constraint(rest)
|
|
249
|
+
# Constraints are noted but not enforced at model level currently
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Parse DEFAULT or OPTIONAL
|
|
253
|
+
rest = rest.strip
|
|
254
|
+
if rest.match?(/\ADEFAULT\b/)
|
|
255
|
+
rest = rest.sub(/\ADEFAULT\s+/, '')
|
|
256
|
+
default_value, rest = parse_default_value(rest)
|
|
257
|
+
options[:default] = default_value
|
|
258
|
+
elsif rest.match?(/\AOPTIONAL\b/)
|
|
259
|
+
rest = rest.delete_prefix('OPTIONAL')
|
|
260
|
+
options[:optional] = true
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
[{ type: type_name, options: options }, rest]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Parse a type reference (simple type name or constructed inline type)
|
|
267
|
+
# @return [Array(String, String)]
|
|
268
|
+
def parse_type_reference(text)
|
|
269
|
+
text = text.strip
|
|
270
|
+
|
|
271
|
+
# Check for inline constructed types
|
|
272
|
+
case text
|
|
273
|
+
when /\A(SEQUENCE|SET)\s*\{/
|
|
274
|
+
type_def, rest = parse_constructed_type(text)
|
|
275
|
+
return [type_def, rest]
|
|
276
|
+
when /\A(SEQUENCE|SET)\s+OF\b/
|
|
277
|
+
type_def, rest = parse_of_type(text)
|
|
278
|
+
return [type_def, rest]
|
|
279
|
+
when /\A(CHOICE)\s*\{/
|
|
280
|
+
type_def, rest = parse_choice_type(text)
|
|
281
|
+
return [type_def, rest]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Multi-word types
|
|
285
|
+
['BIT STRING', 'OCTET STRING', 'OBJECT IDENTIFIER'].each do |multi|
|
|
286
|
+
return [multi, text[multi.length..]] if text.start_with?(multi)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Simple type name
|
|
290
|
+
match = text.match(/\A([A-Za-z][\w-]*)/)
|
|
291
|
+
raise ParseError, "Expected type name, got: #{text[0..20]}" unless match
|
|
292
|
+
|
|
293
|
+
[match[1], match.post_match]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Parse a constraint expression like (0..120)
|
|
297
|
+
# @return [Array(String, String)]
|
|
298
|
+
def parse_constraint(text)
|
|
299
|
+
depth = 0
|
|
300
|
+
idx = 0
|
|
301
|
+
text.each_char do |c|
|
|
302
|
+
depth += 1 if c == '('
|
|
303
|
+
depth -= 1 if c == ')'
|
|
304
|
+
idx += 1
|
|
305
|
+
break if depth.zero?
|
|
306
|
+
end
|
|
307
|
+
[text[0...idx], text[idx..]]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Parse a default value
|
|
311
|
+
# @return [Array(Object, String)]
|
|
312
|
+
def parse_default_value(text)
|
|
313
|
+
text = text.strip
|
|
314
|
+
|
|
315
|
+
case text
|
|
316
|
+
when /\ATRUE\b/
|
|
317
|
+
[true, text.delete_prefix('TRUE')]
|
|
318
|
+
when /\AFALSE\b/
|
|
319
|
+
[false, text.delete_prefix('FALSE')]
|
|
320
|
+
when /\ANULL\b/
|
|
321
|
+
[nil, text.delete_prefix('NULL')]
|
|
322
|
+
when /\A(-?\d+)\b/
|
|
323
|
+
[Regexp.last_match(1).to_i, Regexp.last_match.post_match]
|
|
324
|
+
when /\A"([^"]*)"/
|
|
325
|
+
[Regexp.last_match(1), Regexp.last_match.post_match]
|
|
326
|
+
else
|
|
327
|
+
# Try to capture an identifier (enum value, etc.)
|
|
328
|
+
match = text.match(/\A(\w+)/)
|
|
329
|
+
raise ParseError, "Cannot parse default value: #{text[0..20]}" unless match
|
|
330
|
+
|
|
331
|
+
[match[1], match.post_match]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Build Model classes from parsed definitions
|
|
336
|
+
# @return [Hash{String => Class}]
|
|
337
|
+
def build_models(mod, namespace:)
|
|
338
|
+
models = {}
|
|
339
|
+
|
|
340
|
+
mod[:definitions].each do |defn|
|
|
341
|
+
klass = build_model_class(defn[:name], defn[:type], mod, models, namespace)
|
|
342
|
+
models[defn[:name]] = klass
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
models
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Build a single Model subclass
|
|
349
|
+
# @return [Class]
|
|
350
|
+
def build_model_class(type_name, type_def, mod, existing_models, namespace)
|
|
351
|
+
klass = Class.new(RASN1::Model)
|
|
352
|
+
|
|
353
|
+
root_name = type_name.gsub(/([a-z])([A-Z])/, '\1_\2').gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').downcase.to_sym
|
|
354
|
+
|
|
355
|
+
case type_def[:kind]
|
|
356
|
+
when :sequence, :set
|
|
357
|
+
build_constructed(klass, root_name, type_def, mod, existing_models)
|
|
358
|
+
when :choice
|
|
359
|
+
build_choice(klass, root_name, type_def, mod, existing_models)
|
|
360
|
+
when :sequence_of, :set_of
|
|
361
|
+
build_of(klass, root_name, type_def, mod, existing_models)
|
|
362
|
+
else
|
|
363
|
+
raise ParseError, "Unsupported top-level type: #{type_def.inspect}"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Define the class in the namespace
|
|
367
|
+
namespace.const_set(type_name.to_sym, klass) unless namespace.const_defined?(type_name.to_sym, false)
|
|
368
|
+
|
|
369
|
+
klass
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Build a SEQUENCE or SET model
|
|
373
|
+
def build_constructed(klass, root_name, type_def, mod, existing_models)
|
|
374
|
+
members = type_def[:members]
|
|
375
|
+
mod[:tag_default]
|
|
376
|
+
|
|
377
|
+
klass.class_eval do
|
|
378
|
+
content_elems = members.map do |member|
|
|
379
|
+
field_name = member[:name].to_sym
|
|
380
|
+
type_ref = member[:type]
|
|
381
|
+
opts = (member[:options] || {}).dup
|
|
382
|
+
|
|
383
|
+
if type_ref.is_a?(Hash)
|
|
384
|
+
# Inline constructed type - not supported as a simple field
|
|
385
|
+
raise SchemaParser::ParseError, "Inline constructed types not yet supported for field #{field_name}"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
method_sym = SchemaParser.type_to_method(type_ref)
|
|
389
|
+
if method_sym
|
|
390
|
+
send(method_sym, field_name, **opts)
|
|
391
|
+
elsif existing_models[type_ref]
|
|
392
|
+
model(field_name, existing_models[type_ref])
|
|
393
|
+
else
|
|
394
|
+
raise SchemaParser::ParseError, "Unknown type: #{type_ref}"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
send(type_def[:kind], root_name, content: content_elems)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Build a CHOICE model
|
|
403
|
+
def build_choice(klass, root_name, type_def, _mod, existing_models)
|
|
404
|
+
members = type_def[:members]
|
|
405
|
+
|
|
406
|
+
klass.class_eval do
|
|
407
|
+
content_elems = members.map do |member|
|
|
408
|
+
field_name = member[:name].to_sym
|
|
409
|
+
type_ref = member[:type]
|
|
410
|
+
opts = (member[:options] || {}).dup
|
|
411
|
+
|
|
412
|
+
method_sym = SchemaParser.type_to_method(type_ref)
|
|
413
|
+
if method_sym
|
|
414
|
+
send(method_sym, field_name, **opts)
|
|
415
|
+
elsif existing_models[type_ref]
|
|
416
|
+
model(field_name, existing_models[type_ref])
|
|
417
|
+
else
|
|
418
|
+
raise SchemaParser::ParseError, "Unknown type: #{type_ref}"
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
choice(root_name, content: content_elems)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Build a SEQUENCE OF or SET OF model
|
|
427
|
+
def build_of(klass, root_name, type_def, _mod, existing_models)
|
|
428
|
+
element_type = type_def[:element_type]
|
|
429
|
+
|
|
430
|
+
klass.class_eval do
|
|
431
|
+
method_sym = SchemaParser.type_to_method(element_type)
|
|
432
|
+
if method_sym
|
|
433
|
+
# For OF types with primitive elements, use the type class
|
|
434
|
+
type_class = SchemaParser.type_to_class(element_type)
|
|
435
|
+
send(type_def[:kind], root_name, type_class)
|
|
436
|
+
elsif existing_models[element_type]
|
|
437
|
+
send(type_def[:kind], root_name, existing_models[element_type])
|
|
438
|
+
else
|
|
439
|
+
raise SchemaParser::ParseError, "Unknown type for #{type_def[:kind]}: #{element_type}"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Build Model classes from parsed definitions
|
|
445
|
+
# (end of private section)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Convert an ASN.1 type name to the corresponding RASN1 Model DSL method symbol
|
|
449
|
+
# @param [String] type_name
|
|
450
|
+
# @return [Symbol, nil]
|
|
451
|
+
def self.type_to_method(type_name)
|
|
452
|
+
TYPE_MAP[type_name]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Convert an ASN.1 type name to the corresponding RASN1::Types class
|
|
456
|
+
# @param [String] type_name
|
|
457
|
+
# @return [Class, nil]
|
|
458
|
+
def self.type_to_class(type_name)
|
|
459
|
+
method_sym = TYPE_MAP[type_name]
|
|
460
|
+
return nil unless method_sym
|
|
461
|
+
|
|
462
|
+
class_name = type_name.gsub(/\s+/, '')
|
|
463
|
+
begin
|
|
464
|
+
Types.const_get(class_name)
|
|
465
|
+
rescue NameError
|
|
466
|
+
nil
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
data/lib/rasn1/tracer.rb
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rainbow'
|
|
4
|
+
|
|
5
|
+
module RASN1
|
|
6
|
+
# @private
|
|
7
|
+
class Tracer
|
|
8
|
+
# @return [IO]
|
|
9
|
+
attr_reader :io
|
|
10
|
+
# @return [Integer]
|
|
11
|
+
attr_accessor :tracing_level
|
|
12
|
+
|
|
13
|
+
def color
|
|
14
|
+
@rainbow.enabled
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def color=(enabled)
|
|
18
|
+
@rainbow.enabled = enabled
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def colorize(str)
|
|
22
|
+
@rainbow ? @rainbow.wrap(str) : Rainbow.new.wrap(str)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
TRACED_CLASSES = [Types::Any, Types::Choice, Types::Sequence, Types::SequenceOf, Types::Base].freeze
|
|
26
|
+
|
|
27
|
+
# @param [IO] io
|
|
28
|
+
# @param [Boolean] color enable colorized output using pastel gem
|
|
29
|
+
def initialize(io, color: false)
|
|
30
|
+
@io = io
|
|
31
|
+
@tracing_level = 0
|
|
32
|
+
@rainbow = Rainbow.new
|
|
33
|
+
@rainbow.enabled = color
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Puts +msg+ onto {#io}.
|
|
37
|
+
# @param [String] msg
|
|
38
|
+
# @return [void]
|
|
39
|
+
def trace(msg)
|
|
40
|
+
@io.puts(indent << msg)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Return identation for given +level+. If +nil+, use {#tracing_level}.
|
|
44
|
+
# @param [Integer,nil] level
|
|
45
|
+
# @return [String]
|
|
46
|
+
def indent(level=nil)
|
|
47
|
+
level ||= @tracing_level
|
|
48
|
+
' ' * level
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Trace RASN1 parsing to +io+.
|
|
53
|
+
# All parsing methods called in block are traced to +io+. Each ASN.1 element is
|
|
54
|
+
# traced in a line showing element's id, its length and its data.
|
|
55
|
+
# @param [IO] io
|
|
56
|
+
# @param [Boolean] color enable colorized output using pastel gem (requires pastel to be installed)
|
|
57
|
+
# @example
|
|
58
|
+
# RASN1.trace do
|
|
59
|
+
# RASN1.parse("\x02\x01\x01") # puts "INTEGER id: 2 (0x02), len: 1 (0x01), data: 0x01"
|
|
60
|
+
# end
|
|
61
|
+
# RASN1.parse("\x01\x01\xff") # puts nothing onto STDOUT
|
|
62
|
+
# @example with color
|
|
63
|
+
# RASN1.trace(color: true) do
|
|
64
|
+
# RASN1.parse("\x02\x01\x01") # same output but with ANSI colors
|
|
65
|
+
# end
|
|
66
|
+
# @return [void]
|
|
67
|
+
def self.trace(io=$stdout, color: false)
|
|
68
|
+
self.tracer = Tracer.new(io, color: color)
|
|
69
|
+
Tracer::TRACED_CLASSES.each(&:start_tracing)
|
|
70
|
+
|
|
71
|
+
begin
|
|
72
|
+
yield self.tracer
|
|
73
|
+
ensure
|
|
74
|
+
Tracer::TRACED_CLASSES.reverse.each(&:stop_tracing)
|
|
75
|
+
self.tracer.io.flush
|
|
76
|
+
self.tracer = nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @private
|
|
81
|
+
def self.tracer
|
|
82
|
+
@tracer
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.tracer=(tracer)
|
|
86
|
+
@tracer = tracer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
module Types
|
|
90
|
+
class Base
|
|
91
|
+
class << self
|
|
92
|
+
# @private
|
|
93
|
+
# Patch {#do_parse} to add tracing ability
|
|
94
|
+
def start_tracing
|
|
95
|
+
alias_method :do_parse_without_tracing, :do_parse
|
|
96
|
+
alias_method :do_parse, :do_parse_with_tracing
|
|
97
|
+
alias_method :do_parse_explicit_without_tracing, :do_parse_explicit
|
|
98
|
+
alias_method :do_parse_explicit, :do_parse_explicit_with_tracing
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @private
|
|
102
|
+
# Unpatch {#do_parse} to remove tracing ability
|
|
103
|
+
def stop_tracing
|
|
104
|
+
alias_method :do_parse, :do_parse_without_tracing
|
|
105
|
+
alias_method :do_parse_explicit, :do_parse_explicit_without_tracing
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @private
|
|
110
|
+
# Parse +der+ with tracing abillity
|
|
111
|
+
# @see #parse!
|
|
112
|
+
def do_parse_with_tracing(der, ber:)
|
|
113
|
+
ret = do_parse_without_tracing(der, ber: ber)
|
|
114
|
+
tracer.trace(self.trace)
|
|
115
|
+
ret
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def do_parse_explicit_with_tracing(data)
|
|
119
|
+
tracer.tracing_level += 1
|
|
120
|
+
do_parse_explicit_without_tracing(data)
|
|
121
|
+
tracer.tracing_level -= 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class Choice
|
|
126
|
+
class << self
|
|
127
|
+
# @private
|
|
128
|
+
# Patch {#parse!} to add tracing ability
|
|
129
|
+
def start_tracing
|
|
130
|
+
alias_method :parse_without_tracing, :parse!
|
|
131
|
+
alias_method :parse!, :parse_with_tracing
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @private
|
|
135
|
+
# Unpatch {#parse!} to remove tracing ability
|
|
136
|
+
def stop_tracing
|
|
137
|
+
alias_method :parse!, :parse_without_tracing
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @private
|
|
142
|
+
# Parse +der+ with tracing abillity
|
|
143
|
+
# @see #parse!
|
|
144
|
+
def parse_with_tracing(der, ber: false)
|
|
145
|
+
RASN1.tracer.trace(self.trace)
|
|
146
|
+
parse_without_tracing(der, ber: ber)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class Sequence
|
|
151
|
+
class << self
|
|
152
|
+
# @private
|
|
153
|
+
# Patch {#der_to_value} to add tracing ability
|
|
154
|
+
def start_tracing
|
|
155
|
+
alias_method :der_to_value_without_tracing, :der_to_value
|
|
156
|
+
alias_method :der_to_value, :der_to_value_with_tracing
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @private
|
|
160
|
+
# Unpatch {#der_to_value} to remove tracing ability
|
|
161
|
+
def stop_tracing
|
|
162
|
+
alias_method :der_to_value, :der_to_value_without_tracing
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @private
|
|
167
|
+
# der_to_value +der+ with tracing abillity
|
|
168
|
+
def der_to_value_with_tracing(der, ber: false)
|
|
169
|
+
RASN1.tracer.tracing_level += 1
|
|
170
|
+
der_to_value_without_tracing(der, ber: ber)
|
|
171
|
+
RASN1.tracer.tracing_level -= 1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class SequenceOf
|
|
176
|
+
class << self
|
|
177
|
+
# @private
|
|
178
|
+
# Patch {#der_to_value} to add tracing ability
|
|
179
|
+
def start_tracing
|
|
180
|
+
alias_method :der_to_value_without_tracing, :der_to_value
|
|
181
|
+
alias_method :der_to_value, :der_to_value_with_tracing
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @private
|
|
185
|
+
# Unpatch {#der_to_value} to remove tracing ability
|
|
186
|
+
def stop_tracing
|
|
187
|
+
alias_method :der_to_value, :der_to_value_without_tracing
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @private
|
|
192
|
+
# der_to_value +der+ with tracing abillity
|
|
193
|
+
def der_to_value_with_tracing(der, ber: false)
|
|
194
|
+
RASN1.tracer.tracing_level += 1
|
|
195
|
+
der_to_value_without_tracing(der, ber: ber)
|
|
196
|
+
RASN1.tracer.tracing_level -= 1
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|