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