///|
/// x509dump prints the contents of an X.509 certificate, a PKCS#10
/// certification request or a certificate revocation list.
///
/// Usage:
///
///     moon run cmd/x509dump -- 
///
/// The file may hold PEM or raw DER.

///|
/// Formats bytes as colon separated lowercase hex, the way certificate
/// fingerprints are usually written.
fn format_hex_colons(bytes : Bytes) -> String {
  let encoded = @hex.encode(bytes[:])
  let buffer = StringBuilder()
  let mut index = 0
  while index < encoded.length() {
    if index > 0 {
      buffer.write_string(":")
    }
    buffer.write_substring(encoded, index, 2)
    index = index + 2
  }
  buffer.to_string()
}

///|
/// Computes a SHA-256 digest.
fn sha256(bytes : Bytes) -> Bytes {
  Bytes::from_array(@crypto.sha256(bytes[:])[:])
}

///|
/// Computes a SHA-1 digest.
fn sha1(bytes : Bytes) -> Bytes {
  Bytes::from_array(@crypto.sha1(bytes[:])[:])
}

///|
/// What the tool found in the input.
pub(all) enum Dumpable {
  Certificate(@x509.Certificate)
  Chain(Array[@x509.Certificate])
  Request(@x509.CertificationRequest)
  RevocationList(@x509.CertificateList)
}

///|
/// Tries to read the DER bytes as a certificate.
fn try_certificate(der : Bytes) -> Dumpable? {
  Some(Certificate(@x509.parse_certificate(der[:]))) catch {
    _ => None
  }
}

///|
/// Tries to read the DER bytes as a certification request.
fn try_request(der : Bytes) -> Dumpable? {
  Some(Request(@x509.parse_certification_request(der[:]))) catch {
    _ => None
  }
}

///|
/// Tries to read the DER bytes as a certificate revocation list.
fn try_certificate_list(der : Bytes) -> Dumpable? {
  Some(RevocationList(@x509.parse_certificate_list(der[:]))) catch {
    _ => None
  }
}

///|
/// Returns the longest chain that can be built from a bundle.
fn longest_chain(
  certificates : Array[@x509.Certificate],
) -> Array[@x509.Certificate] {
  let mut best : Array[@x509.Certificate] = []
  for certificate in certificates {
    let chain = @x509.build_chain(certificate, certificates)
    if chain.length() > best.length() {
      best = chain
    }
  }
  best
}

///|
/// Parses the input, accepting PEM or raw DER.
///
/// A certificate is attempted first because the two structures share their
/// outer shape; a request and a revocation list only parse once the earlier
/// attempts fail.
fn parse_input(bytes : Bytes) -> Dumpable? {
  let text = @utf8.decode_lossy(bytes[:])
  if @pem.is_pem(text) {
    let certificates = @x509.parse_certificate_bundle(text) catch { _ => [] }
    if certificates.length() > 1 {
      return Some(Chain(longest_chain(certificates)))
    }
    let block = @pem.decode_pem_single(text) catch { _ => return None }
    return parse_der(block.data)
  }
  parse_der(bytes)
}

///|
/// Tries the DER payload as a certificate, a request and finally a CRL.
fn parse_der(der : Bytes) -> Dumpable? {
  match try_certificate(der) {
    Some(value) => Some(value)
    None =>
      match try_request(der) {
        Some(value) => Some(value)
        None => try_certificate_list(der)
      }
  }
}

///|
/// Appends the fingerprints of an encoding.
fn write_fingerprints(buffer : StringBuilder, der : Bytes) -> Unit {
  buffer.write_string("  SHA-256 fingerprint: ")
  buffer.write_string(format_hex_colons(sha256(der)))
  buffer.write_string("\n")
  buffer.write_string("  SHA-1 fingerprint: ")
  buffer.write_string(format_hex_colons(sha1(der)))
  buffer.write_string("\n")
}

///|
/// Renders a certificate, its fingerprints and the extension details that the
/// summary does not cover.
fn render_certificate(certificate : @x509.Certificate) -> String {
  let buffer = StringBuilder()
  buffer.write_string(certificate.to_string())
  write_fingerprints(buffer, certificate.raw)
  match certificate.extended_key_usage() {
    Some(usages) => {
      buffer.write_string("  Extended key usage (")
      buffer.write_string(usages.length().to_string())
      buffer.write_string(")\n")
      for usage in usages {
        buffer.write_string("    ")
        buffer.write_string(@x509.extended_key_usage_display_name(usage))
        buffer.write_string(" (")
        buffer.write_string(usage.to_string())
        buffer.write_string(")\n")
      }
    }
    None => ()
  }
  buffer.to_string()
}

///|
/// Renders a certification request with its fingerprints.
fn render_request(request : @x509.CertificationRequest) -> String {
  let buffer = StringBuilder()
  buffer.write_string(request.to_string())
  write_fingerprints(buffer, request.raw)
  buffer.to_string()
}

///|
/// Renders a certificate revocation list with its fingerprints.
fn render_revocation_list(crl : @x509.CertificateList) -> String {
  let buffer = StringBuilder()
  buffer.write_string(crl.to_string())
  write_fingerprints(buffer, crl.raw)
  buffer.to_string()
}

///|
/// Renders a certificate chain, leaf first.
fn render_chain(chain : Array[@x509.Certificate]) -> String {
  let buffer = StringBuilder()
  buffer.write_string("Certificate chain (")
  buffer.write_string(chain.length().to_string())
  buffer.write_string(")\n")
  buffer.write_string(@x509.describe_chain(chain))
  for i, certificate in chain {
    buffer.write_string("  ")
    buffer.write_string(i.to_string())
    buffer.write_string(" SHA-256 fingerprint: ")
    buffer.write_string(format_hex_colons(sha256(certificate.raw)))
    buffer.write_string("\n")
  }
  buffer.to_string()
}

///|
/// Renders whatever was found in the input.
fn render(dumpable : Dumpable) -> String {
  match dumpable {
    Certificate(certificate) => render_certificate(certificate)
    Chain(chain) => render_chain(chain)
    Request(request) => render_request(request)
    RevocationList(crl) => render_revocation_list(crl)
  }
}

///|
/// Prints usage information.
fn print_usage() -> Unit {
  println("x509dump - print a certificate, a request or a revocation list")
  println("")
  println("Usage: x509dump ")
  println("")
  println("The file may hold PEM or raw DER.")
  println("")
  println("Options:")
  println("  -h, --help    print this message")
}

///|
/// Prints an error and terminates with a non-zero status.
fn fail(message : String) -> Unit {
  println("x509dump: " + message)
  @sys.exit(1)
}

///|
fn main {
  let arguments = @env.args()
  let mut path : String? = None
  for i in 1.. {
        print_usage()
        return
      }
      unknown => {
        if unknown.has_prefix("-") {
          fail("unknown option " + unknown)
          return
        }
        path = Some(unknown)
      }
    }
  }
  let path = match path {
    Some(path) => path
    None => {
      print_usage()
      @sys.exit(2)
      return
    }
  }
  if !@fs.path_exists(path) {
    fail("no such file: " + path)
    return
  }
  let bytes = @fs.read_file_to_bytes(path) catch {
    _ => {
      fail("could not read " + path)
      return
    }
  }
  let dumpable = match parse_input(bytes) {
    Some(dumpable) => dumpable
    None => {
      fail("no certificate, request or revocation list found in " + path)
      return
    }
  }
  println(render(dumpable))
}