1#! /usr/bin/env node
2'use strict';
3
4const
5 fs = require('fs'),
6 patches = { // to fix some known RFCs' ASN.1 syntax errors
7 0: [
8 [ /\n\n[A-Z].*\n\f\n[A-Z].*\n\n/g, '' ], // page change
9 ],
10 2459: [ // currently unsupported
11 [ 'videotex (8) } (0..ub-integer-options)', 'videotex (8) }' ],
12 [ /OBJECT IDENTIFIER \( id-qt-cps \| id-qt-unotice \)/g, 'OBJECT IDENTIFIER' ],
13 [ /SIGNED \{ (SEQUENCE \{[^}]+\})\s*\}/g, 'SEQUENCE { toBeSigned $1, algorithm AlgorithmIdentifier, signature BIT STRING }' ],
14 [ /EXTENSION\.&[^,]+/g, 'OBJECT IDENTIFIER'],
15 ],
16 3161: [ // actual syntax errors
17 [ /--.*}/g, '}' ],
18 [ /^( +)--.*\n(?:\1 .*\n)+/mg, '' ],
19 [ /addInfoNotAvailable \(17\)/g, '$&,' ],
20 ],
21 5280: [ // currently unsupported
22 [ 'videotex (8) } (0..ub-integer-options)', 'videotex (8) }' ],
23 [ /OBJECT IDENTIFIER \( id-qt-cps \| id-qt-unotice \)/g, 'OBJECT IDENTIFIER' ],
24 ],
25 };
26
27let asn1;
28let currentMod;
29
30function Parser(enc, pos) {
31 this.enc = enc;
32 this.pos = pos;
33 this.start = pos;
34}
35Parser.prototype.getChar = function (pos) {
36 if (pos === undefined)
37 pos = this.pos++;
38 if (pos >= this.enc.length)
39 throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length;
40 return this.enc.charAt(pos);
41};
42Parser.prototype.exception = function (s, pos) {
43 if (pos == undefined) pos = this.pos;
44 let from = Math.max(pos - 30, this.start);
45 let to = Math.min(pos + 30, this.enc.length);
46 let ctx = '';
47 let arrow = '';
48 let i = from;
49 for (; i < pos; ++i) {
50 ctx += this.getChar(i);
51 arrow += ' ';
52 }
53 ctx += this.getChar(i++);
54 arrow += '^';
55 for (; i < to; ++i)
56 ctx += this.getChar(i);
57 throw new Error('[position ' + pos + '] ' + s + '\n' + ctx.replace(/\s/g, ' ') + '\n' + arrow);
58};
59Parser.prototype.peek = function () {
60 return (typeof this.enc == "string") ? this.enc.charCodeAt(this.pos) : this.enc[this.pos];
61};
62Parser.prototype.peekChar = function () {
63 return (typeof this.enc == "string") ? this.enc.charAt(this.pos) : String.fromCharCode(this.enc[this.pos]);
64};
65Parser.prototype.isWhitespace = function () {
66 let c = this.peekChar();
67 return c == ' ' || c == '\n';
68};
69Parser.prototype.isDigit = function () {
70 let c = this.peekChar();
71 return c >= '0' && c <= '9';
72};
73// const reWhitespace = /(?:\s|--(?:[}-]?[^\n}-])*(?:\n|--))*/y;
74const reWhitespace = /(?:\s|--(?:-?[^\n-])*(?:\n|--))*/my;
75Parser.prototype.skipWhitespace = function () {
76 reWhitespace.lastIndex = this.pos;
77 let s = reWhitespace.exec(this.enc);
78 if (s)
79 this.pos = reWhitespace.lastIndex;
80};
81// DefStream.prototype.eat = function (str) {
82// for (let i = 0; i < str.length; ++i) {
83// let c = this.getChar();
84// if (c != str.charAt(i))
85// throw new Error("Found '" + c + "', was expecting '" + str.charAt(i) + "'");
86// }
87// };
88Parser.prototype.getRegEx = function (type, re) {
89 this.skipWhitespace();
90 re.lastIndex = this.pos;
91 let s = re.exec(this.enc); //TODO: does not work with typed arrays
92 if (!s)
93 this.exception("Found '" + this.peekChar() + "', was expecting a " + type);
94 s = s[0];
95 // console.log('[debug] getRexEx@' + this.pos + ' = ' + s);
96 this.pos = re.lastIndex;
97 this.skipWhitespace();
98 return s;
99};
100const reIdentifier = /[a-zA-Z](?:[-]?[a-zA-Z0-9])*/y;
101Parser.prototype.parseIdentifier = function () {
102 let id = this.getRegEx('identifier', reIdentifier);
103 // console.log('[debug] parseIdentifier = ' + id);
104 return id;
105};
106const reNumber = /0|[1-9][0-9]*/y;
107Parser.prototype.parseNumber = function () {
108 let id = this.getRegEx('number', reNumber);
109 // console.log('[debug] parseNumber = ' + id);
110 return id;
111};
112const reToken = /[(){},[\];]|::=|OPTIONAL|DEFAULT|NULL|TRUE|FALSE|\.\.|OF|SIZE|MIN|MAX|DEFINED BY|DEFINITIONS|TAGS|BEGIN|EXPORTS|IMPORTS|FROM|END/y;
113Parser.prototype.parseToken = function () {
114 let tok = this.getRegEx('token', reToken);
115 return tok;
116};
117Parser.prototype.tryToken = function (expect) {
118 let p = this.pos;
119 let t;
120 try { t = this.parseToken(); } catch (e) { /*ignore*/ }
121 // console.log('[debug] tryToken(' + expect + ') = ' + t);
122 if (t == expect)
123 return true;
124 else {
125 this.pos = p;
126 return false;
127 }
128};
129Parser.prototype.expectToken = function (expect) {
130 let p = this.pos;
131 let t;
132 try { t = this.parseToken(); }
133 catch (e) { console.log('[debug] expectToken', e); }
134 // console.log('[debug] expectToken(' + expect + ') = ' + t);
135 if (t != expect) {
136 this.pos = p;
137 this.exception("Found '" + t + "', was expecting '" + expect + "'");
138 }
139};
140Parser.prototype.parseNumberOrValue = function () {
141 if (this.isDigit())
142 return +this.parseNumber();
143 return this.parseIdentifier();
144};
145Parser.prototype.parseRange = function () {
146 let min = this.tryToken('MIN') ? 'MIN' : this.parseNumberOrValue();
147 if (this.tryToken('..')) {
148 let max = this.tryToken('MAX') ? 'MAX' : this.parseNumberOrValue();
149 return [min, max];
150 }
151 return min;
152};
153const reType = /ANY|BOOLEAN|INTEGER|(?:BIT|OCTET)\s+STRING|OBJECT\s+IDENTIFIER|SEQUENCE|SET|CHOICE|ENUMERATED|(?:Generalized|UTC)Time|(?:BMP|General|Graphic|IA5|ISO64|Numeric|Printable|Teletex|T61|Universal|UTF8|Videotex|Visible)String/y;
154Parser.prototype.parseBuiltinType = function () {
155 let x = {
156 name: this.getRegEx('type', reType),
157 type: 'builtin',
158 };
159 // console.log('[debug] parseType = ' + x.name);
160 try {
161 switch (x.name) {
162 case 'ANY':
163 if (this.tryToken('DEFINED BY'))
164 x.definedBy = this.parseIdentifier();
165 break;
166 case 'BOOLEAN':
167 case 'OCTET STRING':
168 case 'OBJECT IDENTIFIER':
169 break;
170 case 'CHOICE':
171 x.content = this.parseElementTypeList();
172 break;
173 case 'SEQUENCE':
174 case 'SET':
175 if (this.peekChar() == '{') {
176 x.content = this.parseElementTypeList();
177 } else {
178 x.typeOf = 1;
179 if (this.tryToken('SIZE')) {
180 this.expectToken('(');
181 x.size = this.parseRange();
182 this.expectToken(')');
183 }
184 this.expectToken('OF');
185 x.content = [this.parseType()];
186 }
187 break;
188 case 'INTEGER':
189 if (this.tryToken('(')) {
190 x.range = this.parseRange();
191 this.expectToken(')');
192 }
193 // falls through
194 case 'ENUMERATED':
195 case 'BIT STRING':
196 if (this.tryToken('{')) {
197 x.content = {};
198 do {
199 let id = this.parseIdentifier();
200 this.expectToken('(');
201 let val = this.parseNumber(); //TODO: signed
202 this.expectToken(')');
203 x.content[id] = +val;
204 } while (this.tryToken(','));
205 this.expectToken('}');
206 }
207 break;
208 case 'BMPString':
209 case 'GeneralString':
210 case 'GraphicString':
211 case 'IA5String':
212 case 'ISO646String':
213 case 'NumericString':
214 case 'PrintableString':
215 case 'TeletexString':
216 case 'T61String':
217 case 'UniversalString':
218 case 'UTF8String':
219 case 'VideotexString':
220 case 'VisibleString':
221 if (this.tryToken('(')) {
222 if (this.tryToken('SIZE')) {
223 this.expectToken('(');
224 x.size = this.parseRange();
225 this.expectToken(')');
226 }
227 this.expectToken(')');
228 }
229 break;
230 default:
231 x.content = 'TODO:unknown';
232 }
233 } catch (e) {
234 console.log('[debug] parseBuiltinType content', e);
235 x.content = 'TODO:exception';
236 }
237 return x;
238};
239const reTagClass = /UNIVERSAL|APPLICATION|PRIVATE|/y;
240const reTagType = /IMPLICIT|EXPLICIT|/y;
241Parser.prototype.parseTaggedType = function () {
242 this.expectToken('[');
243 let tagClass = this.getRegEx('class', reTagClass) || 'CONTEXT'; //TODO: use module defaults
244 let t = this.parseNumber();
245 this.expectToken(']');
246 let plicit = this.getRegEx('explicit/implicit', reTagType);
247 if (plicit == '') plicit = currentMod.tagDefault;
248 let x = this.parseType();
249 return {
250 name: '[' + t + ']',
251 type: 'tag',
252 "class": tagClass,
253 explicit: (plicit == 'EXPLICIT'),
254 content: [{ name: '', type: x }],
255 };
256};
257Parser.prototype.parseType = function () {
258 if (this.peekChar() == '[')
259 return this.parseTaggedType();
260 let p = this.pos;
261 try {
262 return this.parseBuiltinType();
263 } catch (e) {
264 // console.log('[debug] parseAssignment failed on parseType', e);
265 this.pos = p;
266 let x = {
267 name: this.parseIdentifier(),
268 type: 'defined',
269 };
270 // let from = searchImportedType(x.name);
271 // if (from)
272 // x.module = from;
273 return x;
274 //TODO "restricted string type"
275 }
276};
277Parser.prototype.parseValueBoolean = function () {
278 let p = this.pos;
279 let t = this.parseToken();
280 if (t == 'TRUE')
281 return true;
282 if (t == 'FALSE')
283 return false;
284 this.pos = p;
285 this.exception("Found '" + t + "', was expecting a boolean");
286};
287function searchImportedValue(id) {
288 for (let imp of Object.values(currentMod.imports))
289 for (let name of imp.types)
290 if (name == id) {
291 if (!(imp.oid in asn1))
292 throw new Error('Cannot find module: ' + imp.oid + ' ' + id);
293 if (id in asn1[imp.oid].values)
294 return asn1[imp.oid].values[id];
295 throw new Error('Cannot find imported value: ' + imp.oid + ' ' + id);
296 }
297 throw new Error('Cannot find imported value in any module: ' + id);
298}
299Parser.prototype.parseValueOID = function () {
300 this.expectToken('{');
301 let v = '';
302 while (!this.tryToken('}')) {
303 let p = this.pos;
304 let val;
305 if (this.isDigit())
306 val = this.parseNumber();
307 else {
308 this.pos = p;
309 let id = this.parseIdentifier();
310 if (this.tryToken('(')) {
311 val = this.parseNumber();
312 this.expectToken(')');
313 } else {
314 if (id in currentMod.values) // defined in local module
315 val = currentMod.values[id].value;
316 else
317 val = searchImportedValue(id);
318 }
319 }
320 if (v.length) v += '.';
321 v += val;
322 }
323 return v;
324};
325Parser.prototype.parseValue = function () {
326 let c = this.peekChar();
327 if (c == '{')
328 return this.parseValueOID();
329 if (c >= '0' && c <= '9')
330 return +this.parseNumber();
331 if (c == '-')
332 return -this.parseNumber();
333 let p = this.pos;
334 try {
335 switch (this.parseToken()) {
336 case 'TRUE':
337 return true;
338 case 'FALSE':
339 return false;
340 case 'NULL':
341 return null;
342 }
343 } catch (e) {
344 this.pos = p;
345 }
346 p = this.pos;
347 try {
348 return this.parseIdentifier();
349 } catch (e) {
350 this.pos = p;
351 }
352 this.exception('Unknown value type.');
353};
354/*DefStream.prototype.parseValue = function (type) {
355 console.log('[debug] parseValue type:', type);
356 if (type.type == 'defined') {
357 if (!(type.name in types))
358 this.exception("Missing type: " + type.name);
359 type = types[type.name];
360 }
361 switch (type.name) {
362 case 'BOOLEAN':
363 return this.parseValueBoolean();
364 case 'OBJECT IDENTIFIER':
365 return this.parseValueOID();
366 default:
367 console.log('[debug] parseValue unknown:', type);
368 return 'TODO:value';
369 }
370};*/
371Parser.prototype.parseElementType = function () {
372 let x = Object.assign({ id: this.parseIdentifier() }, this.parseType());
373 // console.log('[debug] parseElementType 1:', x);
374 if (this.tryToken('OPTIONAL'))
375 x.optional = true;
376 if (this.tryToken('DEFAULT'))
377 x.default = this.parseValue(x.type);
378 // console.log('[debug] parseElementType 2:', x);
379 return x;
380};
381Parser.prototype.parseElementTypeList = function () {
382 let v = [];
383 this.expectToken('{');
384 do {
385 v.push(this.parseElementType());
386 } while (this.tryToken(','));
387 this.expectToken('}');
388 return v;
389};
390Parser.prototype.parseAssignment = function () {
391 let name = this.parseIdentifier();
392 if (this.tryToken('::=')) { // type assignment
393 // console.log('type name', name);
394 let type = this.parseType();
395 currentMod.types[name] = { name, type };
396 return currentMod.types[name];
397 } else { // value assignment
398 // console.log('value name', name);
399 let type = this.parseType();
400 // console.log('[debug] parseAssignment type:', type);
401 this.expectToken('::=');
402 let value = this.parseValue(type);
403 currentMod.values[name] = { name, type, value };
404 return currentMod.values[name];
405 }
406};
407Parser.prototype.parseModuleIdentifier = function () {
408 return {
409 name: this.parseIdentifier(),
410 oid: this.parseValueOID(),
411 };
412};
413Parser.prototype.parseSymbolsImported = function () {
414 let imports = {};
415 do {
416 let l = [];
417 do {
418 l.push(this.parseIdentifier());
419 } while (this.tryToken(','));
420 this.expectToken('FROM');
421 let mod = this.parseModuleIdentifier();
422 mod.types = l;
423 imports[mod.oid] = mod;
424 } while (this.peekChar() != ';');
425 return imports;
426};
427const reTagDefault = /(AUTOMATIC|IMPLICIT|EXPLICIT) TAGS|/y;
428Parser.prototype.parseModuleDefinition = function (file) {
429 let mod = this.parseModuleIdentifier();
430 currentMod = mod; // for deeply nested parsers
431 mod.source = file;
432 this.expectToken('DEFINITIONS');
433 mod.tagDefault = this.getRegEx('tag default', reTagDefault).split(' ')[0];
434 this.expectToken('::=');
435 this.expectToken('BEGIN');
436 //TODO this.tryToken('EXPORTS')
437 if (this.tryToken('IMPORTS')) {
438 mod.imports = this.parseSymbolsImported();
439 this.expectToken(';');
440 }
441 mod.values = {};
442 mod.types = {};
443 while (!this.tryToken('END'))
444 this.parseAssignment();
445 return mod;
446};
447
448let s = fs.readFileSync(process.argv[2], 'utf8');
449let num = /^Request for Comments: ([0-9]+)/m.exec(s)[1];
450console.log('RFC:', num);
451for (let p of patches[0])
452 s = s.replace(p[0], p[1]);
453if (num in patches)
454 for (let p of patches[num])
455 s = s.replace(p[0], p[1]);
456// fs.writeFileSync('rfc3161_patched.txt', s, 'utf8');
457// console.log(s);
458asn1 = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
459const reModuleDefinition = /\s[A-Z](?:[-]?[a-zA-Z0-9])*\s*\{[^}]+\}\s*DEFINITIONS/gm;
460let m;
461while ((m = reModuleDefinition.exec(s))) {
462 new Parser(s, m.index).parseModuleDefinition(process.argv[2]);
463 console.log('Module:', currentMod.name);
464 // fs.writeFileSync('rfc' + num + '.json', JSON.stringify(currentMod, null, 2) + '\n', 'utf8');
465 asn1[currentMod.oid] = currentMod;
466}
467/*asn1 = Object.keys(asn1).sort().reduce(
468 (obj, key) => {
469 obj[key] = asn1[key];
470 return obj;
471 },
472 {}
473);*/
474fs.writeFileSync(process.argv[3], JSON.stringify(asn1, null, 2) + '\n', 'utf8');
475// console.log('Module:', mod);
476/*while ((idx = s.indexOf('::=', idx + 1)) >= 0) {
477 let line = s.lastIndexOf('\n', idx) + 1;
478 // console.log('[line] ' + s.slice(line, line+30));
479 try {
480 let a = new DefStream(s, line).parseAssignment();
481 // console.log('[assignment]', util.inspect(a, {showHidden: false, depth: null, colors: true}));
482 } catch (e) {
483 console.log('Error:', e);
484 }
485}*/
486console.log('Done.');