JavaScript generic ASN.1 parser (mirror)
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.');