'use strict';

// Explained here: https://groups.google.com/d/msg/nodejs/AjkHSYmiGYs/1LfNHbMhd48J

var fs = require('fs')
  , path = require('path')
  , request = require('request')
  , CERTDB_URL = 'https://mxr.mozilla.org/nss/source/lib/ckfw/builtins/certdata.txt?raw=1'
  , HEADER
  , outputFile
  , outputPemsDir
  ;

HEADER =
  "/**\n" +
  " * Mozilla's root CA store\n" +
  " *\n" +
  " * generated from " + CERTDB_URL + "\n" +
  " */\n\n";

function Certificate() {
  this.name = null;
  this.body = '';
  this.trusted = true;
}

Certificate.prototype.quasiPEM = function quasiPEM() {
  var bytes = this.body.split('\\')
    , offset = 0
    , converted
    ;

  bytes.shift();
  converted = new Buffer(bytes.length);
  while(bytes.length > 0) {
    converted.writeUInt8(parseInt(bytes.shift(), 8), offset++);
  }

  return {
    name: this.name
  , value: '  // ' + this.name + '\n'
      + '  "-----BEGIN CERTIFICATE-----\\n" +\n'
      + converted.toString('base64').replace(/(.{1,76})/g, '  "$1\\n" +\n')
      + '  "-----END CERTIFICATE-----\\n"'
  };
};

function parseBody(current, lines) {
  var line
    ;

  while (lines.length > 0) {
    line = lines.shift();
    if (line.match(/^END/)) { break; }
    current.body += line;
  }

  while (lines.length > 0) {
    line = lines.shift();
    if (line.match(/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/)) { break; }
  }

  while (lines.length > 0) {
    line = lines.shift();
    if (line.match(/^#|^\s*$/)) { break; }
    if (line.match(/^CKA_TRUST_SERVER_AUTH\s+CK_TRUST\s+CKT_NSS_NOT_TRUSTED$/) ||
        line.match(/^CKA_TRUST_SERVER_AUTH\s+CK_TRUST\s+CKT_NSS_TRUST_UNKNOWN$/)) {
      current.trusted = false;
    }
  }

  if (current.trusted) return current;
}

function parseCertData(lines) {
  var certs = []
    , line
    , current
    , skipped = 0
    , match
    , finished
    ;

  while (lines.length > 0) {
    line = lines.shift();

    // nuke whitespace and comments
    if (line.match(/^#|^\s*$/)) continue;

    if (line.match(/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/)) {
      current = new Certificate();
    }

    if (current) {
      match = line.match(/^CKA_LABEL UTF8 \"(.*)\"/);
      if (match) {
        current.name = match[1];
      }

      if (line.match(/^CKA_VALUE MULTILINE_OCTAL/)) {
        finished = parseBody(current, lines);
        if (finished) {
          certs.push(finished);
        }
        else {
          skipped++;
        }
        current = null;
      }
    }
  }

  console.info("Skipped %s untrusted certificates.", skipped);
  console.info("Processed %s certificates.", certs.length);

  return certs;
}

function dumpCerts(certs, filename, pemsDir) {
  certs.forEach(function (cert) {
    var pem = cert.quasiPEM()
      , pemName = pem.name.toLowerCase().replace(/\W/g, '-').replace(/-+/g, '-')
      , pemsFile = path.join(pemsDir, pemName + '.pem')
      ;

    fs.writeFileSync(pemsFile, pem.value);
  });
  console.info("Wrote " + certs.length + " certificates in '"
    + path.join(__dirname, 'pems/').replace(/'/g, "\\'") + "'.");

  fs.writeFileSync(
    filename
  , HEADER
      + 'var cas = module.exports = [\n'
      + certs.map(function (cert) { return cert.quasiPEM().value; }).join(',\n\n')
      + '\n];\n'
      + "module.exports.rootCas = cas;\n"
      + "module.exports.inject = function () {\n"
      + "  var opts = require('https').globalAgent.options;\n"
      + "  if (!opts.ca || !opts.ca.__injected) { opts.ca = (opts.ca||[]).concat(cas); }\n"
      + "  opts.ca.__injected = true;\n"
      + "  return module.exports;\n"
      + "};\n"
      + "module.exports.addFile = function (filepath) {\n"
      + "  var opts = require('https').globalAgent.options;\n"
      + "  var root = filepath[0] === '/' ? '/' : '';\n"
      + "  var filepaths = filepath.split(/\\//g);\n"
      + "  if (root) { filepaths.unshift(root); }\n"
      + "  opts.ca = opts.ca || [];\n"
      + "  opts.ca.push(require('fs').readFileSync(require('path').join.apply(null, filepaths)));\n"
      + "  return module.exports;\n"
      + "};\n"
  );
  console.info("Wrote '" + filename.replace(/'/g, "\\'") + "'.");
}

if (process.argv[2] == null) {
  console.error("Error: No file specified");
  console.info("Usage: %s <outputfile>", process.argv[1]);
  console.info("   where <outputfile> is the name of the file to write to, relative to %s", process.argv[1]);
  console.info("Note that a 'pems/' directory will also be created at the same location as the <outputfile>, containing individual .pem files.");
  process.exit(3);
}

// main (combined) output file location, relative to this script's location
outputFile = path.resolve(__dirname, process.argv[2]);

// pems/ output directory, in the same directory as the outputFile
outputPemsDir = path.resolve(outputFile, '../pems');


console.info("Loading latest certificates from " + CERTDB_URL);
request(CERTDB_URL, function (error, response, body) {
  if (error) {
    console.error(error.stacktrace);
    process.exit(1);
  }

  if (response.statusCode !== 200) {
    console.error("Fetching failed with status code %s", response.statusCode);
    process.exit(2);
  }

  var lines = body.split("\n")
    , certs = parseCertData(lines)
    ;

  dumpCerts(certs, outputFile, outputPemsDir);
});

@@ -1777,19 +1777,8 @@
  %5B%5D%0A
-    , line%0A
     
@@ -1845,61 +1845,34 @@
 %0A%0A  
-while (lines.length %3E 0) %7B%0A    line = lines.shift();%0A
+function parseLine(line) %7B
 %0A   
@@ -1940,17 +1940,29 @@
 /)) 
-continue;
+%7B%0A      return;%0A    %7D
 %0A%0A  
@@ -2458,16 +2458,80 @@
  %7D%0A  %7D%0A%0A
+  while (lines.length %3E 0) %7B%0A    parseLine(lines.shift());%0A  %7D%0A%0A
   consol

move loop to function