[ChromeDriver] Add Cookie spec compliance

Make Add Cookie command compliant to W3C spec. Also added a set of
helper functions to help parsing some property types used in W3C spec.
ChromeDriver now passes all WPT WebDriver tests related to cookie.

Bug: chromedriver:2002
Change-Id: I3779a14e010bca865498a9d40710974e38ede829
Reviewed-on: https://chromium-review.googlesource.com/c/1366925
Commit-Queue: John Chen <johnchen@chromium.org>
Reviewed-by: Caleb Rouleau <crouleau@chromium.org>
Cr-Commit-Position: refs/heads/master@{#615539}
diff --git a/chrome/test/chromedriver/chrome/web_view_impl.cc b/chrome/test/chromedriver/chrome/web_view_impl.cc
index d82a06c0..44c8189 100644
--- a/chrome/test/chromedriver/chrome/web_view_impl.cc
+++ b/chrome/test/chromedriver/chrome/web_view_impl.cc
@@ -569,8 +569,8 @@
   params.SetString("path", path);
   params.SetBoolean("secure", secure);
   params.SetBoolean("httpOnly", httpOnly);
-  params.SetDouble("expirationDate", expiry);
-  params.SetDouble("expires", expiry);
+  if (expiry >= 0)
+    params.SetDouble("expires", expiry);
 
   std::unique_ptr<base::DictionaryValue> result;
   Status status =
diff --git a/chrome/test/chromedriver/util.cc b/chrome/test/chromedriver/util.cc
index 21294b4..182ea30f 100644
--- a/chrome/test/chromedriver/util.cc
+++ b/chrome/test/chromedriver/util.cc
@@ -430,3 +430,110 @@
   }
   return Status(kOk);
 }
+
+namespace {
+
+template <typename T>
+bool GetOptionalValue(const base::DictionaryValue* dict,
+                      base::StringPiece path,
+                      T* out_value,
+                      bool* has_value,
+                      bool (base::Value::*getter)(T*) const) {
+  if (has_value != nullptr)
+    *has_value = false;
+  const base::Value* value;
+  if (!dict->Get(path, &value) || value->is_none())
+    return true;
+  if ((value->*getter)(out_value)) {
+    if (has_value != nullptr)
+      *has_value = true;
+    return true;
+  }
+  return false;
+}
+
+}  // namespace
+
+bool GetOptionalBool(const base::DictionaryValue* dict,
+                     base::StringPiece path,
+                     bool* out_value,
+                     bool* has_value) {
+  return GetOptionalValue(dict, path, out_value, has_value,
+                          &base::Value::GetAsBoolean);
+}
+
+bool GetOptionalInt(const base::DictionaryValue* dict,
+                    base::StringPiece path,
+                    int* out_value,
+                    bool* has_value) {
+  if (GetOptionalValue(dict, path, out_value, has_value,
+                       &base::Value::GetAsInteger)) {
+    return true;
+  }
+  // See if we have a double that contains an int value.
+  double d;
+  if (!dict->GetDouble(path, &d))
+    return false;
+  int i = static_cast<int>(d);
+  if (i == d) {
+    *out_value = i;
+    if (has_value != nullptr)
+      *has_value = true;
+    return true;
+  }
+  return false;
+}
+
+bool GetOptionalDouble(const base::DictionaryValue* dict,
+                       base::StringPiece path,
+                       double* out_value,
+                       bool* has_value) {
+  // base::Value::GetAsDouble already converts int to double if needed.
+  return GetOptionalValue(dict, path, out_value, has_value,
+                          &base::Value::GetAsDouble);
+}
+
+bool GetOptionalString(const base::DictionaryValue* dict,
+                       base::StringPiece path,
+                       std::string* out_value,
+                       bool* has_value) {
+  return GetOptionalValue(dict, path, out_value, has_value,
+                          &base::Value::GetAsString);
+}
+
+bool GetOptionalSafeInt(const base::DictionaryValue* dict,
+                        base::StringPiece path,
+                        int64_t* out_value,
+                        bool* has_value) {
+  // Check if we have a normal int, which is always a safe int.
+  int temp_int;
+  bool temp_has_value;
+  if (GetOptionalValue(dict, path, &temp_int, &temp_has_value,
+                       &base::Value::GetAsInteger)) {
+    if (has_value != nullptr)
+      *has_value = temp_has_value;
+    if (temp_has_value)
+      *out_value = temp_int;
+    return true;
+  }
+
+  // Check if we have a double, which may or may not contain a safe int value.
+  double temp_double;
+  if (!dict->GetDouble(path, &temp_double))
+    return false;
+
+  // Verify that the value is an integer.
+  int64_t temp_int64 = static_cast<int64_t>(temp_double);
+  if (temp_int64 != temp_double)
+    return false;
+
+  // Verify that the value is in the range for safe integer.
+  if (temp_int64 >= (1ll << 53) || temp_int64 <= -(1ll << 53))
+    return false;
+
+  // Got a good value.
+  *out_value = temp_int64;
+  if (has_value != nullptr)
+    *has_value = true;
+  return true;
+}
diff --git a/chrome/test/chromedriver/util.h b/chrome/test/chromedriver/util.h
index 111ced48..65477c1 100644
--- a/chrome/test/chromedriver/util.h
+++ b/chrome/test/chromedriver/util.h
@@ -7,6 +7,8 @@
 
 #include <string>
 
+#include "base/values.h"
+
 namespace base {
 class FilePath;
 class ListValue;
@@ -45,4 +47,45 @@
 Status NotifyCommandListenersBeforeCommand(Session* session,
                                            const std::string& command_name);
 
+// Functions to get an optional value of the given type from a dictionary.
+// Each function has three different outcomes:
+// * Value exists and is of right type:
+//   returns true, *has_value = true, *out_value gets the actual value.
+// * Value does not exist, or exists with value of undefined:
+//   returns true, *has_value = false, *out_value is unchanged.
+// * Value exists but is of wrong type (error condition):
+//   returns false, *has_value undefined, *out_value is unchanged.
+// In addition to provide a convenient way to fetch optional values that are
+// common in W3C WebDriver spec, some of these functions also handles the
+// differences in the definition of an integer:
+// * base::Value uses a definition similar to C++, thus 2.0 is not an integer.
+//   Also, integers are limited to 32-bit.
+// * WebDriver spec (https://www.w3.org/TR/webdriver/#dfn-integer) defines
+//   integer to be a number that is unchanged under the ToInteger operation,
+//   thus 2.0 is an integer. Also, the spec sometimes use "safe integer"
+//   (https://www.w3.org/TR/webdriver/#dfn-maximum-safe-integer), whose
+//   absolute value can occupy up to 53 bits.
+bool GetOptionalBool(const base::DictionaryValue* dict,
+                     base::StringPiece path,
+                     bool* out_value,
+                     bool* has_value = nullptr);
+bool GetOptionalInt(const base::DictionaryValue* dict,
+                    base::StringPiece path,
+                    int* out_value,
+                    bool* has_value = nullptr);
+bool GetOptionalDouble(const base::DictionaryValue* dict,
+                       base::StringPiece path,
+                       double* out_value,
+                       bool* has_value = nullptr);
+bool GetOptionalString(const base::DictionaryValue* dict,
+                       base::StringPiece path,
+                       std::string* out_value,
+                       bool* has_value = nullptr);
+// Handles "safe integer" mentioned in W3C spec,
+// https://www.w3.org/TR/webdriver/#dfn-maximum-safe-integer.
+bool GetOptionalSafeInt(const base::DictionaryValue* dict,
+                        base::StringPiece path,
+                        int64_t* out_value,
+                        bool* has_value = nullptr);
+
 #endif  // CHROME_TEST_CHROMEDRIVER_UTIL_H_
diff --git a/chrome/test/chromedriver/util_unittest.cc b/chrome/test/chromedriver/util_unittest.cc
index 95cdc9b5..b7c9dbb7b 100644
--- a/chrome/test/chromedriver/util_unittest.cc
+++ b/chrome/test/chromedriver/util_unittest.cc
@@ -48,3 +48,225 @@
   ASSERT_TRUE(base::ReadFileToString(file, &contents));
   ASSERT_STREQ("COW\n", contents.c_str());
 }
+
+namespace {
+
+const base::StringPiece key = "key";
+const int64_t max_safe_int = (1ll << 53) - 1;
+
+void DictNoInit(base::DictionaryValue* dict) {}
+
+void DictInitNull(base::DictionaryValue* dict) {
+  dict->Set(key, std::make_unique<base::Value>());
+}
+
+class DictInitBool {
+  bool init_value;
+
+ public:
+  explicit DictInitBool(bool v) : init_value(v) {}
+  void operator()(base::DictionaryValue* dict) {
+    dict->SetBoolean(key, init_value);
+  }
+};
+
+class DictInitInt {
+  int init_value;
+
+ public:
+  explicit DictInitInt(int v) : init_value(v) {}
+  void operator()(base::DictionaryValue* dict) {
+    dict->SetInteger(key, init_value);
+  }
+};
+
+class DictInitDouble {
+  double init_value;
+
+ public:
+  explicit DictInitDouble(double v) : init_value(v) {}
+  void operator()(base::DictionaryValue* dict) {
+    dict->SetDouble(key, init_value);
+  }
+};
+
+class DictInitString {
+  std::string init_value;
+
+ public:
+  explicit DictInitString(const std::string& v) : init_value(v) {}
+  void operator()(base::DictionaryValue* dict) {
+    dict->SetString(key, init_value);
+  }
+};
+
+template <typename ResultType, typename DictInitFunc>
+void TestGetOptionalValue(bool (*func_to_test)(const base::DictionaryValue*,
+                                               base::StringPiece,
+                                               ResultType*,
+                                               bool*),
+                          DictInitFunc dict_init_func,
+                          const ResultType& init_result_value,
+                          const ResultType& expected_result_value,
+                          bool expected_return_value,
+                          bool expected_has_value) {
+  base::DictionaryValue dict;
+  dict_init_func(&dict);
+
+  ResultType result_value = init_result_value;
+  bool has_value;
+  bool return_value = func_to_test(&dict, key, &result_value, &has_value);
+  ASSERT_EQ(return_value, expected_return_value);
+  ASSERT_EQ(result_value, expected_result_value);
+  if (return_value)
+    ASSERT_EQ(has_value, expected_has_value);
+
+  result_value = init_result_value;
+  return_value = func_to_test(&dict, key, &result_value, nullptr);
+  ASSERT_EQ(return_value, expected_return_value);
+  ASSERT_EQ(result_value, expected_result_value);
+}
+
+}  // namespace
+
+TEST(GetOptionalValue, BoolNone) {
+  TestGetOptionalValue<bool>(GetOptionalBool, DictNoInit, true, true, true,
+                             false);
+}
+
+TEST(GetOptionalValue, IntNone) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictNoInit, 12345, 12345, true,
+                            false);
+}
+
+TEST(GetOptionalValue, DoubleNone) {
+  TestGetOptionalValue<double>(GetOptionalDouble, DictNoInit, 123.45, 123.45,
+                               true, false);
+}
+
+TEST(GetOptionalValue, StringNone) {
+  TestGetOptionalValue<std::string>(GetOptionalString, DictNoInit, "abcde",
+                                    "abcde", true, false);
+}
+
+TEST(GetOptionalValue, SafeIntNone) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt, DictNoInit, 12345, 12345,
+                                true, false);
+}
+
+TEST(GetOptionalValue, BoolNull) {
+  TestGetOptionalValue<bool>(GetOptionalBool, DictInitNull, true, true, true,
+                             false);
+}
+
+TEST(GetOptionalValue, IntNull) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictInitNull, 12345, 12345, true,
+                            false);
+}
+
+TEST(GetOptionalValue, DoubleNull) {
+  TestGetOptionalValue<double>(GetOptionalDouble, DictInitNull, 123.45, 123.45,
+                               true, false);
+}
+
+TEST(GetOptionalValue, StringNull) {
+  TestGetOptionalValue<std::string>(GetOptionalString, DictInitNull, "abcde",
+                                    "abcde", true, false);
+}
+
+TEST(GetOptionalValue, SafeIntNull) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt, DictInitNull, 12345, 12345,
+                                true, false);
+}
+
+TEST(GetOptionalValue, BoolWrongType) {
+  TestGetOptionalValue<bool>(GetOptionalBool, DictInitString("test"), true,
+                             true, false, false);
+}
+
+TEST(GetOptionalValue, IntWrongType) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictInitString("test"), 12345,
+                            12345, false, false);
+}
+
+TEST(GetOptionalValue, DoubleWrongType) {
+  TestGetOptionalValue<double>(GetOptionalDouble, DictInitString("test"),
+                               123.45, 123.45, false, false);
+}
+
+TEST(GetOptionalValue, StringWrongType) {
+  TestGetOptionalValue<std::string>(GetOptionalString, DictInitBool(false),
+                                    "abcde", "abcde", false, false);
+}
+
+TEST(GetOptionalValue, SafeIntWrongType) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt, DictInitString("test"),
+                                12345, 12345, false, false);
+}
+
+TEST(GetOptionalValue, BoolNoConversion) {
+  TestGetOptionalValue<bool>(GetOptionalBool, DictInitBool(false), true, false,
+                             true, true);
+}
+
+TEST(GetOptionalValue, IntNoConversion) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictInitInt(54321), 12345, 54321,
+                            true, true);
+}
+
+TEST(GetOptionalValue, DoubleNoConversion) {
+  TestGetOptionalValue<double>(GetOptionalDouble, DictInitDouble(54.321),
+                               123.45, 54.321, true, true);
+}
+
+TEST(GetOptionalValue, StringNoConversion) {
+  TestGetOptionalValue<std::string>(GetOptionalString, DictInitString("xyz"),
+                                    "abcde", "xyz", true, true);
+}
+
+TEST(GetOptionalValue, SafeIntNoConversion) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt, DictInitInt(54321), 12345,
+                                54321, true, true);
+}
+
+TEST(GetOptionalValue, DoubleToInt) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictInitDouble(54321.0), 12345,
+                            54321, true, true);
+}
+
+TEST(GetOptionalValue, DoubleToSafeInt) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt, DictInitDouble(54321.0),
+                                12345, 54321, true, true);
+}
+
+TEST(GetOptionalValue, DoubleToIntError) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictInitDouble(5432.1), 12345,
+                            12345, false, false);
+}
+
+TEST(GetOptionalValue, DoubleToSafeIntError) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt, DictInitDouble(5432.1),
+                                12345, 12345, false, false);
+}
+
+TEST(GetOptionalValue, IntToDouble) {
+  TestGetOptionalValue<double>(GetOptionalDouble, DictInitInt(54), 123.45, 54.0,
+                               true, true);
+}
+
+TEST(GetOptionalValue, SafeIntMax) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt,
+                                DictInitDouble(max_safe_int), 12345,
+                                max_safe_int, true, true);
+}
+
+TEST(GetOptionalValue, SafeIntMaxToIntError) {
+  TestGetOptionalValue<int>(GetOptionalInt, DictInitDouble(max_safe_int), 12345,
+                            12345, false, false);
+}
+
+TEST(GetOptionalValue, SafeIntTooLarge) {
+  TestGetOptionalValue<int64_t>(GetOptionalSafeInt,
+                                DictInitDouble(max_safe_int + 1), 12345, 12345,
+                                false, false);
+}
diff --git a/chrome/test/chromedriver/window_commands.cc b/chrome/test/chromedriver/window_commands.cc
index 86f4d808..ba7b15d 100644
--- a/chrome/test/chromedriver/window_commands.cc
+++ b/chrome/test/chromedriver/window_commands.cc
@@ -47,6 +47,7 @@
 static const char kUnreachableWebDataURL[] = "chrome-error://chromewebdata/";
 const char kDeprecatedUnreachableWebDataURL[] = "data:text/html,chromewebdata";
 
+// TODO(johnchen@chromium.org): Remove when we stop supporting legacy protocol.
 // Defaults to 20 years into the future when adding a cookie.
 const double kDefaultCookieExpiryTime = 20*365*24*60*60;
 
@@ -1560,17 +1561,37 @@
   if (status.IsError())
     return status;
   std::string domain;
-  cookie->GetString("domain", &domain);
+  if (!GetOptionalString(cookie, "domain", &domain))
+    return Status(kInvalidArgument, "invalid 'domain'");
   std::string path("/");
-  cookie->GetString("path", &path);
+  if (!GetOptionalString(cookie, "path", &path))
+    return Status(kInvalidArgument, "invalid 'path'");
   bool secure = false;
-  cookie->GetBoolean("secure", &secure);
+  if (!GetOptionalBool(cookie, "secure", &secure))
+    return Status(kInvalidArgument, "invalid 'secure'");
   bool httpOnly = false;
-  cookie->GetBoolean("httpOnly", &httpOnly);
+  if (!GetOptionalBool(cookie, "httpOnly", &httpOnly))
+    return Status(kInvalidArgument, "invalid 'httpOnly'");
   double expiry;
-  if (!cookie->GetDouble("expiry", &expiry))
-    expiry = (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() +
-              kDefaultCookieExpiryTime;
+  bool has_value;
+  if (session->w3c_compliant) {
+    // W3C spec says expiry is a safe integer.
+    int64_t expiry_int64;
+    if (!GetOptionalSafeInt(cookie, "expiry", &expiry_int64, &has_value) ||
+        (has_value && expiry_int64 < 0))
+      return Status(kInvalidArgument, "invalid 'expiry'");
+    // Use negative value to indicate expiry not specified.
+    expiry = has_value ? static_cast<double>(expiry_int64) : -1.0;
+  } else {
+    // JSON wire protocol didn't specify the type of expiry, but ChromeDriver
+    // has always accepted double, so we keep that in legacy mode.
+    if (!GetOptionalDouble(cookie, "expiry", &expiry, &has_value) ||
+        (has_value && expiry < 0))
+      return Status(kInvalidArgument, "invalid 'expiry'");
+    if (!has_value)
+      expiry = (base::Time::Now() - base::Time::UnixEpoch()).InSeconds() +
+               kDefaultCookieExpiryTime;
+  }
   return web_view->AddCookie(name, url, cookie_value, domain, path,
       secure, httpOnly, expiry);
 }
diff --git a/docs/chromedriver_status.md b/docs/chromedriver_status.md
index 70952ae..38534a1a 100644
--- a/docs/chromedriver_status.md
+++ b/docs/chromedriver_status.md
@@ -47,7 +47,7 @@
 | POST   | /session/{session id}/execute/async                            | Execute Async Script       | Partially Complete | [2398](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2398) [2556](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2556)
 | GET    | /session/{session id}/cookie                                   | Get All Cookies            | Complete           |
 | GET    | /session/{session id}/cookie/{name}                            | Get Named Cookie           | Complete           |
-| POST   | /session/{session id}/cookie                                   | Add Cookie                 | Partially Complete | [2002](https://bugs.chromium.org/p/chromedriver/issues/detail?id=2002)
+| POST   | /session/{session id}/cookie                                   | Add Cookie                 | Complete           |
 | DELETE | /session/{session id}/cookie/{name}                            | Delete Cookie              | Complete           |
 | DELETE | /session/{session id)/cookie                                   | Delete All Cookies         | Complete           |
 | POST   | /session/{session id}/actions                                  | Perform Actions            | Incomplete         | [1897](https://bugs.chromium.org/p/chromedriver/issues/detail?id=1897)