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