/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsSecurityHeaderParser.h"
#include "mozilla/Logging.h"

// The character classes in this file are informed by [RFC2616], Section 2.2.
// signed char is a signed data type one byte (8 bits) wide, so its value can
// never be greater than 127. The following implicitly makes use of this.

// A token is one or more CHAR except CTLs or separators.
// A CHAR is any US-ASCII character (octets 0 - 127).
// A CTL is any US-ASCII control character (octets 0 - 31) and DEL (127).
// A separator is one of ()<>@,;:\"/[]?={} as well as space and
// horizontal-tab (32 and 9, respectively).
// So, this returns true if chr is any octet 33-126 except ()<>@,;:\"/[]?={}
bool IsTokenSymbol(signed char chr) {
  if (chr < 33 || chr == 127 || chr == '(' || chr == ')' || chr == '<' ||
      chr == '>' || chr == '@' || chr == ',' || chr == ';' || chr == ':' ||
      chr == '"' || chr == '/' || chr == '[' || chr == ']' || chr == '?' ||
      chr == '=' || chr == '{' || chr == '}' || chr == '\\') {
    return false;
  }
  return true;
}

// A quoted-string consists of a quote (") followed by any amount of
// qdtext or quoted-pair, followed by a quote.
// qdtext is any TEXT except a quote.
// TEXT is any 8-bit octet except CTLs, but including LWS.
// quoted-pair is a backslash (\) followed by a CHAR.
// So, it turns out, \ can't really be a qdtext symbol for our purposes.
// This returns true if chr is any octet 9,10,13,32-126 except <"> or "\"
bool IsQuotedTextSymbol(signed char chr) {
  return ((chr >= 32 && chr != '"' && chr != '\\' && chr != 127) ||
          chr == 0x9 || chr == 0xa || chr == 0xd);
}

// The octet following the "\" in a quoted pair can be anything 0-127.
bool IsQuotedPairSymbol(signed char chr) { return (chr >= 0); }

static mozilla::LazyLogModule sSHParserLog("nsSecurityHeaderParser");

#define SHPARSERLOG(args) MOZ_LOG(sSHParserLog, mozilla::LogLevel::Debug, args)

nsSecurityHeaderParser::nsSecurityHeaderParser(const nsCString& aHeader)
    : mCursor(aHeader.get()), mDirective(nullptr), mError(false) {}

nsSecurityHeaderParser::~nsSecurityHeaderParser() {
  nsSecurityHeaderDirective* directive;
  while ((directive = mDirectives.popFirst())) {
    delete directive;
  }
}

mozilla::LinkedList<nsSecurityHeaderDirective>*
nsSecurityHeaderParser::GetDirectives() {
  return &mDirectives;
}

nsresult nsSecurityHeaderParser::Parse() {
  MOZ_ASSERT(mDirectives.isEmpty());
  SHPARSERLOG(("trying to parse '%s'", mCursor));

  Header();

  // if we didn't consume the entire input, we were unable to parse it => error
  if (mError || *mCursor) {
    return NS_ERROR_FAILURE;
  } else {
    return NS_OK;
  }
}

bool nsSecurityHeaderParser::Accept(char aChr) {
  if (*mCursor == aChr) {
    Advance();
    return true;
  }

  return false;
}

bool nsSecurityHeaderParser::Accept(bool (*aClassifier)(signed char)) {
  if (aClassifier(*mCursor)) {
    Advance();
    return true;
  }

  return false;
}

void nsSecurityHeaderParser::Expect(char aChr) {
  if (*mCursor != aChr) {
    mError = true;
  } else {
    Advance();
  }
}

void nsSecurityHeaderParser::Advance() {
  // Technically, 0 is valid in quoted-pair, but we were handed a
  // null-terminated const char *, so this doesn't handle that.
  if (*mCursor) {
    mOutput.Append(*mCursor);
    mCursor++;
  } else {
    mError = true;
  }
}

void nsSecurityHeaderParser::Header() {
  Directive();
  while (Accept(';')) {
    Directive();
  }
}

void nsSecurityHeaderParser::Directive() {
  mDirective = new nsSecurityHeaderDirective();
  LWSMultiple();
  DirectiveName();
  LWSMultiple();
  if (Accept('=')) {
    LWSMultiple();
    DirectiveValue();
    LWSMultiple();
  }
  mDirectives.insertBack(mDirective);
  SHPARSERLOG(("read directive name '%s', value '%s'", mDirective->mName.Data(),
               mDirective->mValue.Data()));
}

void nsSecurityHeaderParser::DirectiveName() {
  mOutput.Truncate(0);
  Token();
  mDirective->mName.Assign(mOutput);
}

void nsSecurityHeaderParser::DirectiveValue() {
  mOutput.Truncate(0);
  if (Accept(IsTokenSymbol)) {
    Token();
    mDirective->mValue.Assign(mOutput);
  } else if (Accept('"')) {
    // Accept advances the cursor if successful, which appends a character to
    // mOutput. The " is not part of what we want to capture, so truncate
    // mOutput again.
    mOutput.Truncate(0);
    QuotedString();
    mDirective->mValue.Assign(mOutput);
    Expect('"');
  }
}

void nsSecurityHeaderParser::Token() {
  while (Accept(IsTokenSymbol))
    ;
}

void nsSecurityHeaderParser::QuotedString() {
  while (true) {
    if (Accept(IsQuotedTextSymbol)) {
      QuotedText();
    } else if (Accept('\\')) {
      QuotedPair();
    } else {
      break;
    }
  }
}

void nsSecurityHeaderParser::QuotedText() {
  while (Accept(IsQuotedTextSymbol))
    ;
}

void nsSecurityHeaderParser::QuotedPair() { Accept(IsQuotedPairSymbol); }

void nsSecurityHeaderParser::LWSMultiple() {
  while (true) {
    if (Accept('\r')) {
      LWSCRLF();
    } else if (Accept(' ') || Accept('\t')) {
      LWS();
    } else {
      break;
    }
  }
}

void nsSecurityHeaderParser::LWSCRLF() {
  Expect('\n');
  if (!(Accept(' ') || Accept('\t'))) {
    mError = true;
  }
  LWS();
}

void nsSecurityHeaderParser::LWS() {
  // Note that becaue of how we're called, we don't have to check for
  // the mandatory presense of at least one of SP or HT.
  while (Accept(' ') || Accept('\t'))
    ;
}
