Implement WebSocketExtensionParser

Implement WebSocketExtensionParser, which parses Sec-WebSocket-Extension
header value as specified in RFC6455.

BUG=280910

Review URL: https://chromiumcodereview.appspot.com/23872029

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@224102 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/net/net.gyp b/net/net.gyp
index 905f70b..7f56c9b 100644
--- a/net/net.gyp
+++ b/net/net.gyp
@@ -1106,6 +1106,10 @@
         'websockets/websocket_deflater.cc',
         'websockets/websocket_errors.cc',
         'websockets/websocket_errors.h',
+        'websockets/websocket_extension.cc',
+        'websockets/websocket_extension.h',
+        'websockets/websocket_extension_parser.cc',
+        'websockets/websocket_extension_parser.h',
         'websockets/websocket_frame.cc',
         'websockets/websocket_frame.h',
         'websockets/websocket_frame_parser.cc',
@@ -1879,6 +1883,7 @@
         'websockets/websocket_channel_test.cc',
         'websockets/websocket_deflater_test.cc',
         'websockets/websocket_errors_test.cc',
+        'websockets/websocket_extension_parser_test.cc',
         'websockets/websocket_frame_parser_test.cc',
         'websockets/websocket_frame_test.cc',
         'websockets/websocket_handshake_handler_test.cc',
diff --git a/net/websockets/README b/net/websockets/README
index 25b3e84..1d1e1c3 100644
--- a/net/websockets/README
+++ b/net/websockets/README
@@ -40,6 +40,11 @@
 websocket_deflater_test.cc
 websocket_errors.cc
 websocket_errors.h
+websocket_extension.cc
+websocket_extension.h
+websocket_extension_parser.cc
+websocket_extension_parser.h
+websocket_extension_parser_test.cc
 websocket_errors_test.cc
 websocket_event_interface.h
 websocket_frame.cc
diff --git a/net/websockets/websocket_extension.cc b/net/websockets/websocket_extension.cc
new file mode 100644
index 0000000..edcd8e8
--- /dev/null
+++ b/net/websockets/websocket_extension.cc
@@ -0,0 +1,43 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/websockets/websocket_extension.h"
+
+#include <string>
+
+#include "base/logging.h"
+
+namespace net {
+
+WebSocketExtension::Parameter::Parameter(const std::string& name)
+    : name_(name) {}
+
+WebSocketExtension::Parameter::Parameter(const std::string& name,
+                                         const std::string& value)
+    : name_(name), value_(value) {
+  DCHECK(!value.empty());
+}
+
+bool WebSocketExtension::Parameter::Equals(const Parameter& other) const {
+  return name_ == other.name_ && value_ == other.value_;
+}
+
+WebSocketExtension::WebSocketExtension() {}
+
+WebSocketExtension::WebSocketExtension(const std::string& name)
+    : name_(name) {}
+
+WebSocketExtension::~WebSocketExtension() {}
+
+bool WebSocketExtension::Equals(const WebSocketExtension& other) const {
+  if (name_ != other.name_) return false;
+  if (parameters_.size() != other.parameters_.size()) return false;
+  for (size_t i = 0; i < other.parameters_.size(); ++i) {
+    if (!parameters_[i].Equals(other.parameters_[i]))
+      return false;
+  }
+  return true;
+}
+
+}  // namespace net
diff --git a/net/websockets/websocket_extension.h b/net/websockets/websocket_extension.h
new file mode 100644
index 0000000..5af4023
--- /dev/null
+++ b/net/websockets/websocket_extension.h
@@ -0,0 +1,57 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_WEBSOCKETS_WEBSOCKET_EXTENSION_H_
+#define NET_WEBSOCKETS_WEBSOCKET_EXTENSION_H_
+
+#include <string>
+#include <vector>
+
+#include "net/base/net_export.h"
+
+namespace net {
+
+// A WebSocketExtension instance represents a WebSocket extension specified
+// in RFC6455.
+class NET_EXPORT_PRIVATE WebSocketExtension {
+ public:
+  // Note that RFC6455 does not allow a parameter with an empty value.
+  class NET_EXPORT_PRIVATE Parameter {
+   public:
+    // Construct a parameter which does not have a value.
+    explicit Parameter(const std::string& name);
+    // Construct a parameter with a non-empty value.
+    Parameter(const std::string& name, const std::string& value);
+
+    bool HasValue() const { return !value_.empty(); }
+    const std::string& name() const { return name_; }
+    const std::string& value() const { return value_; }
+    bool Equals(const Parameter& other) const;
+
+    // The default copy constructor and the assignment operator are defined:
+    // we need them.
+   private:
+    std::string name_;
+    std::string value_;
+  };
+
+  WebSocketExtension();
+  explicit WebSocketExtension(const std::string& name);
+  ~WebSocketExtension();
+
+  void Add(const Parameter& parameter) { parameters_.push_back(parameter); }
+  const std::string& name() const { return name_; }
+  const std::vector<Parameter>& parameters() const { return parameters_; }
+  bool Equals(const WebSocketExtension& other) const;
+
+  // The default copy constructor and the assignment operator are defined:
+  // we need them.
+ private:
+  std::string name_;
+  std::vector<Parameter> parameters_;
+};
+
+}  // namespace net
+
+#endif  // NET_WEBSOCKETS_WEBSOCKET_EXTENSION_H_
diff --git a/net/websockets/websocket_extension_parser.cc b/net/websockets/websocket_extension_parser.cc
new file mode 100644
index 0000000..28a2db1
--- /dev/null
+++ b/net/websockets/websocket_extension_parser.cc
@@ -0,0 +1,158 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/websockets/websocket_extension_parser.h"
+
+#include "base/strings/string_util.h"
+
+namespace net {
+
+WebSocketExtensionParser::WebSocketExtensionParser() {}
+
+WebSocketExtensionParser::~WebSocketExtensionParser() {}
+
+void WebSocketExtensionParser::Parse(const char* data, size_t size) {
+  current_ = data;
+  end_ = data + size;
+  has_error_ = false;
+
+  ConsumeExtension(&extension_);
+  if (has_error_) return;
+  ConsumeSpaces();
+  has_error_ = has_error_ || (current_ != end_);
+}
+
+void WebSocketExtensionParser::Consume(char c) {
+  DCHECK(!has_error_);
+  ConsumeSpaces();
+  DCHECK(!has_error_);
+  if (current_ == end_ || c != current_[0]) {
+    has_error_ = true;
+    return;
+  }
+  ++current_;
+}
+
+void WebSocketExtensionParser::ConsumeExtension(WebSocketExtension* extension) {
+  DCHECK(!has_error_);
+  base::StringPiece name;
+  ConsumeToken(&name);
+  if (has_error_) return;
+  *extension = WebSocketExtension(name.as_string());
+
+  while (ConsumeIfMatch(';')) {
+    WebSocketExtension::Parameter parameter((std::string()));
+    ConsumeExtensionParameter(&parameter);
+    if (has_error_) return;
+    extension->Add(parameter);
+  }
+}
+
+void WebSocketExtensionParser::ConsumeExtensionParameter(
+    WebSocketExtension::Parameter* parameter) {
+  DCHECK(!has_error_);
+  base::StringPiece name, value;
+  std::string value_string;
+
+  ConsumeToken(&name);
+  if (has_error_) return;
+  if (!ConsumeIfMatch('=')) {
+    *parameter = WebSocketExtension::Parameter(name.as_string());
+    return;
+  }
+
+  if (Lookahead('\"')) {
+    ConsumeQuotedToken(&value_string);
+  } else {
+    ConsumeToken(&value);
+    value_string = value.as_string();
+  }
+  if (has_error_) return;
+  *parameter = WebSocketExtension::Parameter(name.as_string(), value_string);
+}
+
+void WebSocketExtensionParser::ConsumeToken(base::StringPiece* token) {
+  DCHECK(!has_error_);
+  ConsumeSpaces();
+  DCHECK(!has_error_);
+  const char* head = current_;
+  while (current_ < end_ &&
+         !IsControl(current_[0]) && !IsSeparator(current_[0]))
+    ++current_;
+  if (current_ == head) {
+    has_error_ = true;
+    return;
+  }
+  *token = base::StringPiece(head, current_ - head);
+}
+
+void WebSocketExtensionParser::ConsumeQuotedToken(std::string* token) {
+  DCHECK(!has_error_);
+  Consume('"');
+  if (has_error_) return;
+  *token = "";
+  while (current_ < end_ && !IsControl(current_[0])) {
+    if (UnconsumedBytes() >= 2 && current_[0] == '\\') {
+      char next = current_[1];
+      if (IsControl(next) || IsSeparator(next)) break;
+      *token += next;
+      current_ += 2;
+    } else if (IsSeparator(current_[0])) {
+      break;
+    } else {
+      *token += current_[0];
+      ++current_;
+    }
+  }
+  // We can't use Consume here because we don't want to consume spaces.
+  if (current_ < end_ && current_[0] == '"')
+    ++current_;
+  else
+    has_error_ = true;
+  has_error_ = has_error_ || token->empty();
+}
+
+void WebSocketExtensionParser::ConsumeSpaces() {
+  DCHECK(!has_error_);
+  while (current_ < end_ && (current_[0] == ' ' || current_[0] == '\t'))
+    ++current_;
+  return;
+}
+
+bool WebSocketExtensionParser::Lookahead(char c) {
+  DCHECK(!has_error_);
+  const char* head = current_;
+
+  Consume(c);
+  bool result = !has_error_;
+  current_ = head;
+  has_error_ = false;
+  return result;
+}
+
+bool WebSocketExtensionParser::ConsumeIfMatch(char c) {
+  DCHECK(!has_error_);
+  const char* head = current_;
+
+  Consume(c);
+  if (has_error_) {
+    current_ = head;
+    has_error_ = false;
+    return false;
+  }
+  return true;
+}
+
+// static
+bool WebSocketExtensionParser::IsControl(char c) {
+  return (0 <= c && c <= 31) || c == 127;
+}
+
+// static
+bool WebSocketExtensionParser::IsSeparator(char c) {
+  const char separators[] = "()<>@,;:\\\"/[]?={} \t";
+  return strchr(separators, c) != NULL;
+}
+
+}  // namespace net
diff --git a/net/websockets/websocket_extension_parser.h b/net/websockets/websocket_extension_parser.h
new file mode 100644
index 0000000..ef7fe03
--- /dev/null
+++ b/net/websockets/websocket_extension_parser.h
@@ -0,0 +1,59 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef NET_WEBSOCKETS_WEBSOCKET_EXTENSION_PARSER_H_
+#define NET_WEBSOCKETS_WEBSOCKET_EXTENSION_PARSER_H_
+
+#include <string>
+
+#include "base/strings/string_piece.h"
+#include "net/base/net_export.h"
+#include "net/websockets/websocket_extension.h"
+
+namespace net {
+
+class NET_EXPORT_PRIVATE WebSocketExtensionParser {
+ public:
+  WebSocketExtensionParser();
+  ~WebSocketExtensionParser();
+
+  // Parses the given string as a WebSocket extension header value.
+  // This parser assumes some preprocesses are made.
+  //  - The parser parses single extension at a time. This means that
+  //    the parser parses |extension| in RFC6455 9.1, not |extension-list|.
+  //  - There is no newline characters in the input. LWS-concatenation must
+  //    have already been done.
+  void Parse(const char* data, size_t size);
+  void Parse(const std::string& data) {
+    Parse(data.data(), data.size());
+  }
+
+  bool has_error() const { return has_error_; }
+  const WebSocketExtension& extension() const { return extension_; }
+
+ private:
+  void Consume(char c);
+  void ConsumeExtension(WebSocketExtension* extension);
+  void ConsumeExtensionParameter(WebSocketExtension::Parameter* parameter);
+  void ConsumeToken(base::StringPiece* token);
+  void ConsumeQuotedToken(std::string* token);
+  void ConsumeSpaces();
+  bool Lookahead(char c);
+  bool ConsumeIfMatch(char c);
+  size_t UnconsumedBytes() const { return end_ - current_; }
+
+  static bool IsControl(char c);
+  static bool IsSeparator(char c);
+
+  const char* current_;
+  const char* end_;
+  bool has_error_;
+  WebSocketExtension extension_;
+
+  DISALLOW_COPY_AND_ASSIGN(WebSocketExtensionParser);
+};
+
+}  // namespace net
+
+#endif  // NET_WEBSOCKETS_WEBSOCKET_EXTENSION_PARSER_H_
diff --git a/net/websockets/websocket_extension_parser_test.cc b/net/websockets/websocket_extension_parser_test.cc
new file mode 100644
index 0000000..dc7dc85
--- /dev/null
+++ b/net/websockets/websocket_extension_parser_test.cc
@@ -0,0 +1,122 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/websockets/websocket_extension_parser.h"
+
+#include <string>
+
+#include "net/websockets/websocket_extension.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+
+namespace {
+
+TEST(WebSocketExtensionParserTest, ParseEmpty) {
+  WebSocketExtensionParser parser;
+  parser.Parse("", 0);
+
+  EXPECT_TRUE(parser.has_error());
+}
+
+TEST(WebSocketExtensionParserTest, ParseSimple) {
+  WebSocketExtensionParser parser;
+  WebSocketExtension expected("foo");
+
+  parser.Parse("foo");
+
+  ASSERT_FALSE(parser.has_error());
+  EXPECT_TRUE(expected.Equals(parser.extension()));
+}
+
+TEST(WebSocketExtensionParserTest, ParseOneExtensionWithOneParamWithoutValue) {
+  WebSocketExtensionParser parser;
+  WebSocketExtension expected("foo");
+  expected.Add(WebSocketExtension::Parameter("bar"));
+
+  parser.Parse("\tfoo ; bar");
+
+  ASSERT_FALSE(parser.has_error());
+  EXPECT_TRUE(expected.Equals(parser.extension()));
+}
+
+TEST(WebSocketExtensionParserTest, ParseOneExtensionWithOneParamWithValue) {
+  WebSocketExtensionParser parser;
+  WebSocketExtension expected("foo");
+  expected.Add(WebSocketExtension::Parameter("bar", "baz"));
+
+  parser.Parse("foo ; bar= baz\t");
+
+  ASSERT_FALSE(parser.has_error());
+  EXPECT_TRUE(expected.Equals(parser.extension()));
+}
+
+TEST(WebSocketExtensionParserTest, ParseOneExtensionWithParams) {
+  WebSocketExtensionParser parser;
+  WebSocketExtension expected("foo");
+  expected.Add(WebSocketExtension::Parameter("bar", "baz"));
+  expected.Add(WebSocketExtension::Parameter("hoge", "fuga"));
+
+  parser.Parse("foo ; bar= baz;\t \thoge\t\t=fuga");
+
+  ASSERT_FALSE(parser.has_error());
+  EXPECT_TRUE(expected.Equals(parser.extension()));
+}
+
+TEST(WebSocketExtensionParserTest, InvalidPatterns) {
+  const char* patterns[] = {
+    "fo\ao",  // control in extension name
+    "fo\x01o",  // control in extension name
+    "fo<o",  // separator in extension name
+    "foo/",  // separator in extension name
+    ";bar",  // empty extension name
+    "foo bar",  // missing ';'
+    "foo;",  // extension parameter without name and value
+    "foo; b\ar",  // control in parameter name
+    "foo; b\x7fr",  // control in parameter name
+    "foo; b[r",  // separator in parameter name
+    "foo; ba:",  // separator in parameter name
+    "foo; =baz",  // empty parameter name
+    "foo; bar=",  // empty parameter value
+    "foo; =",  // empty parameter name and value
+    "foo; bar=b\x02z",  // control in parameter value
+    "foo; bar=b@z",  // separator in parameter value
+    "foo; bar=b\\z",  // separator in parameter value
+    "foo; bar=b?z",  // separator in parameter value
+    "\"foo\"",  // quoted extension name
+    "foo; \"bar\"",  // quoted parameter name
+    "foo; bar=\"\a2\"",  // control in quoted parameter value
+    "foo; bar=\"b@z\"",  // separator in quoted parameter value
+    "foo; bar=\"b\\\\z\"",  // separator in quoted parameter value
+    "foo; bar=\"\"",  // quoted empty parameter value
+    "foo; bar=\"baz",  // unterminated quoted string
+    "foo; bar=\"baz \"",  // space in quoted string
+    "foo; bar baz",  // mising '='
+    "foo; bar - baz",  // '-' instead of '=' (note: "foo; bar-baz" is valid).
+    "foo; bar=\r\nbaz",  // CRNL not followed by a space
+    "foo; bar=\r\n baz",  // CRNL followed by a space
+    "foo, bar"  // multiple extensions
+  };
+
+  for (size_t i = 0; i < arraysize(patterns); ++i) {
+    WebSocketExtensionParser parser;
+    parser.Parse(patterns[i]);
+    EXPECT_TRUE(parser.has_error());
+  }
+}
+
+TEST(WebSocketExtensionParserTest, QuotedParameterValue) {
+  WebSocketExtensionParser parser;
+  WebSocketExtension expected("foo");
+  expected.Add(WebSocketExtension::Parameter("bar", "baz"));
+
+  parser.Parse("foo; bar = \"ba\\z\" ");
+
+  ASSERT_FALSE(parser.has_error());
+  EXPECT_TRUE(expected.Equals(parser.extension()));
+}
+
+}  // namespace
+
+}  // namespace net