Add support for Manifest.icons.sizes

This is re-implementing the HTML icon sizes parsing algorithm
given that it is missing a re-usable place for it. There is
a follow-up bug opened at http://crbug.com/416477

BUG=366145

Review URL: https://codereview.chromium.org/591073002

Cr-Commit-Position: refs/heads/master@{#296386}
diff --git a/content/common/manifest_manager_messages.h b/content/common/manifest_manager_messages.h
index a447de9..be80405 100644
--- a/content/common/manifest_manager_messages.h
+++ b/content/common/manifest_manager_messages.h
@@ -21,6 +21,7 @@
   IPC_STRUCT_TRAITS_MEMBER(src)
   IPC_STRUCT_TRAITS_MEMBER(type)
   IPC_STRUCT_TRAITS_MEMBER(density)
+  IPC_STRUCT_TRAITS_MEMBER(sizes)
 IPC_STRUCT_TRAITS_END()
 
 IPC_STRUCT_TRAITS_BEGIN(content::Manifest)
diff --git a/content/public/common/manifest.h b/content/public/common/manifest.h
index 949c610..957f007 100644
--- a/content/public/common/manifest.h
+++ b/content/public/common/manifest.h
@@ -10,6 +10,7 @@
 #include "base/strings/nullable_string16.h"
 #include "content/common/content_export.h"
 #include "third_party/WebKit/public/platform/WebScreenOrientationLockType.h"
+#include "ui/gfx/geometry/size.h"
 #include "url/gurl.h"
 
 namespace content {
@@ -45,6 +46,10 @@
     // Default value is 1.0 if the value is missing or invalid.
     double density;
 
+    // Empty if the parsing failed, the field was not present or empty.
+    // The special value "any" is represented by gfx::Size(0, 0).
+    std::vector<gfx::Size> sizes;
+
     // Default density. Set to 1.0.
     static const double kDefaultDensity;
   };
diff --git a/content/renderer/manifest/manifest_parser.cc b/content/renderer/manifest/manifest_parser.cc
index 3f2f9893..dc1624a 100644
--- a/content/renderer/manifest/manifest_parser.cc
+++ b/content/renderer/manifest/manifest_parser.cc
@@ -6,10 +6,13 @@
 
 #include "base/json/json_reader.h"
 #include "base/strings/nullable_string16.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/values.h"
 #include "content/public/common/manifest.h"
+#include "ui/gfx/geometry/size.h"
 
 namespace content {
 
@@ -165,6 +168,80 @@
   return density;
 }
 
+// Helper function that returns whether the given |str| is a valid width or
+// height value for an icon sizes per:
+// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-sizes
+bool IsValidIconWidthOrHeight(const std::string& str) {
+  if (str.empty() || str[0] == '0')
+    return false;
+  for (size_t i = 0; i < str.size(); ++i)
+    if (!IsAsciiDigit(str[i]))
+      return false;
+  return true;
+}
+
+// Parses the 'sizes' attribute of an icon as described in the HTML spec:
+// https://html.spec.whatwg.org/multipage/semantics.html#attr-link-sizes
+// Return a vector of gfx::Size that contains the valid sizes found. "Any" is
+// represented by gfx::Size(0, 0).
+// TODO(mlamouri): this is implemented as a separate function because it should
+// be refactored with the other icon sizes parsing implementations, see
+// http://crbug.com/416477
+std::vector<gfx::Size> ParseIconSizesHTML(const base::string16& sizes_str16) {
+  if (!base::IsStringASCII(sizes_str16))
+    return std::vector<gfx::Size>();
+
+  std::vector<gfx::Size> sizes;
+  std::string sizes_str =
+      base::StringToLowerASCII(base::UTF16ToUTF8(sizes_str16));
+  std::vector<std::string> sizes_str_list;
+  base::SplitStringAlongWhitespace(sizes_str, &sizes_str_list);
+
+  for (size_t i = 0; i < sizes_str_list.size(); ++i) {
+    std::string& size_str = sizes_str_list[i];
+    if (size_str == "any") {
+      sizes.push_back(gfx::Size(0, 0));
+      continue;
+    }
+
+    // It is expected that [0] => width and [1] => height after the split.
+    std::vector<std::string> size_list;
+    base::SplitStringDontTrim(size_str, L'x', &size_list);
+    if (size_list.size() != 2)
+      continue;
+    if (!IsValidIconWidthOrHeight(size_list[0]) ||
+        !IsValidIconWidthOrHeight(size_list[1])) {
+      continue;
+    }
+
+    int width, height;
+    if (!base::StringToInt(size_list[0], &width) ||
+        !base::StringToInt(size_list[1], &height)) {
+      continue;
+    }
+
+    sizes.push_back(gfx::Size(width, height));
+  }
+
+  return sizes;
+}
+
+// Parses the 'sizes' field of an icon, as defined in:
+// http://w3c.github.io/manifest/#dfn-steps-for-processing-a-sizes-member-of-an-icon
+// Returns a vector of gfx::Size with the successfully parsed sizes, if any. An
+// empty vector if the field was not present or empty. "Any" is represented by
+// gfx::Size(0, 0).
+std::vector<gfx::Size> ParseIconSizes(const base::DictionaryValue& icon) {
+  base::NullableString16 sizes_str = ParseString(icon, "sizes", NoTrim);
+
+  return sizes_str.is_null() ? std::vector<gfx::Size>()
+                             : ParseIconSizesHTML(sizes_str.string());
+}
+
+// Parses the 'icons' field of a Manifest, as defined in:
+// http://w3c.github.io/manifest/#dfn-steps-for-processing-the-icons-member
+// Returns a vector of Manifest::Icon with the successfully parsed icons, if
+// any. An empty vector if the field was not present or empty.
 std::vector<Manifest::Icon> ParseIcons(const base::DictionaryValue& dictionary,
                                        const GURL& manifest_url) {
   std::vector<Manifest::Icon> icons;
@@ -190,7 +267,7 @@
       continue;
     icon.type = ParseIconType(*icon_dictionary);
     icon.density = ParseIconDensity(*icon_dictionary);
-    // TODO(mlamouri): icon.sizes
+    icon.sizes = ParseIconSizes(*icon_dictionary);
 
     icons.push_back(icon);
   }
diff --git a/content/renderer/manifest/manifest_parser_unittest.cc b/content/renderer/manifest/manifest_parser_unittest.cc
index c2ce8da7..32730f7 100644
--- a/content/renderer/manifest/manifest_parser_unittest.cc
+++ b/content/renderer/manifest/manifest_parser_unittest.cc
@@ -492,4 +492,92 @@
   }
 }
 
+TEST_F(ManifestParserTest, IconSizesParseRules) {
+  // Smoke test.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"42x42\" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 1u);
+  }
+
+  // Trim whitespaces.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"  42x42  \" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 1u);
+  }
+
+  // Don't parse if name isn't a string.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": {} } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 0u);
+  }
+
+  // Don't parse if name isn't a string.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": 42 } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 0u);
+  }
+
+  // Smoke test: value correctly parsed.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"42x42  48x48\" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes[0], gfx::Size(42, 42));
+    EXPECT_EQ(manifest.icons[0].sizes[1], gfx::Size(48, 48));
+  }
+
+  // <WIDTH>'x'<HEIGHT> and <WIDTH>'X'<HEIGHT> are equivalent.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"42X42  48X48\" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes[0], gfx::Size(42, 42));
+    EXPECT_EQ(manifest.icons[0].sizes[1], gfx::Size(48, 48));
+  }
+
+  // Twice the same value is parsed twice.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"42X42  42x42\" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes[0], gfx::Size(42, 42));
+    EXPECT_EQ(manifest.icons[0].sizes[1], gfx::Size(42, 42));
+  }
+
+  // Width or height can't start with 0.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"004X007  042x00\" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 0u);
+  }
+
+  // Width and height MUST contain digits.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"e4X1.0  55ax1e10\" } ] }");
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 0u);
+  }
+
+  // 'any' is correctly parsed and transformed to gfx::Size(0,0).
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"any AnY ANY aNy\" } ] }");
+    gfx::Size any = gfx::Size(0, 0);
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 4u);
+    EXPECT_EQ(manifest.icons[0].sizes[0], any);
+    EXPECT_EQ(manifest.icons[0].sizes[1], any);
+    EXPECT_EQ(manifest.icons[0].sizes[2], any);
+    EXPECT_EQ(manifest.icons[0].sizes[3], any);
+  }
+
+  // Some invalid width/height combinations.
+  {
+    Manifest manifest = ParseManifest("{ \"icons\": [ {\"src\": \"\","
+        "\"sizes\": \"x 40xx 1x2x3 x42 42xx42\" } ] }");
+    gfx::Size any = gfx::Size(0, 0);
+    EXPECT_EQ(manifest.icons[0].sizes.size(), 0u);
+  }
+}
+
 } // namespace content