blob: af7a21bb043f8bb2859cfbde3432e8036117a37d [file] [log] [blame]
/*
* Copyright (C) 2017-2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#if ENABLE(APPLICATION_MANIFEST)
#include <JavaScriptCore/InitializeThreading.h>
#include <WebCore/ApplicationManifestParser.h>
#include <wtf/RunLoop.h>
using namespace WebCore;
namespace WebCore {
static inline std::ostream& operator<<(std::ostream& os, const ApplicationManifest::Display& display)
{
switch (display) {
case ApplicationManifest::Display::Browser:
return os << "ApplicationManifest::Display::Browser";
case ApplicationManifest::Display::MinimalUI:
return os << "ApplicationManifest::Display::MinimalUI";
case ApplicationManifest::Display::Standalone:
return os << "ApplicationManifest::Display::Standalone";
case ApplicationManifest::Display::Fullscreen:
return os << "ApplicationManifest::Display::Fullscreen";
}
}
} // namespace WebCore
class ApplicationManifestParserTest : public testing::Test {
public:
URL m_manifestURL;
URL m_documentURL;
virtual void SetUp()
{
JSC::initialize();
WTF::initializeMainThread();
m_manifestURL = { { }, "https://example.com/manifest.json" };
m_documentURL = { { }, "https://example.com/" };
}
ApplicationManifest parseString(const String& data)
{
return ApplicationManifestParser::parse(data, m_manifestURL, m_documentURL);
}
ApplicationManifest parseTopLevelProperty(const String& key, const String& value)
{
String manifestContent = "{ \"" + key + "\" : " + value + " }";
return parseString(manifestContent);
}
ApplicationManifest parseIconFirstTopLevelProperty(const String& key, const String& value)
{
String manifestContent = "{ \"icons\": [{\"" + key + "\": " + value + ", \"src\": \"icon/example.png\" }]}";
return parseString(manifestContent);
}
ApplicationManifest parseIconFirstTopLevelPropertyForSrc(const String& key, const String& value)
{
String manifestContent = "{ \"icons\": [{\"" + key + "\": " + value + " }]}";
return parseString(manifestContent);
}
void testStartURL(const String& rawJSON, const String& expectedValue)
{
testStartURL(rawJSON, { { }, expectedValue });
}
void testStartURL(const String& rawJSON, const URL& expectedValue)
{
auto manifest = parseTopLevelProperty("start_url", rawJSON);
auto value = manifest.startURL;
EXPECT_STREQ(expectedValue.string().utf8().data(), value.string().utf8().data());
}
void testDisplay(const String& rawJSON, ApplicationManifest::Display expectedValue)
{
auto manifest = parseTopLevelProperty("display", rawJSON);
auto value = manifest.display;
EXPECT_EQ(expectedValue, value);
}
void testName(const String& rawJSON, const String& expectedValue)
{
auto manifest = parseTopLevelProperty("name", rawJSON);
auto value = manifest.name;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testDescription(const String& rawJSON, const String& expectedValue)
{
auto manifest = parseTopLevelProperty("description", rawJSON);
auto value = manifest.description;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testShortName(const String& rawJSON, const String& expectedValue)
{
auto manifest = parseTopLevelProperty("short_name", rawJSON);
auto value = manifest.shortName;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testScope(const String& rawJSON, const String& startURL, const String& expectedValue)
{
String manifestContent = "{ \"scope\" : " + rawJSON + ", \"start_url\" : \"" + startURL + "\" }";
auto manifest = parseString(manifestContent);
auto value = manifest.scope;
EXPECT_STREQ(expectedValue.utf8().data(), value.string().utf8().data());
}
void testScope(const String& rawJSON, const String& expectedValue)
{
testScope(rawJSON, String(), expectedValue);
}
void testThemeColor(const String& rawJSON, const Color& expectedValue)
{
auto manifest = parseTopLevelProperty("theme_color", rawJSON);
auto value = manifest.themeColor;
EXPECT_EQ(expectedValue, value);
}
void testIconsSrc(const String& rawJSON, const URL& expectedValue)
{
auto manifest = parseIconFirstTopLevelPropertyForSrc("src", rawJSON);
auto value = manifest.icons[0].src;
EXPECT_STREQ(expectedValue.string().utf8().data(), value.string().utf8().data());
}
void testIconsType(const String &rawJSON, const String& expectedValue)
{
auto manifest = parseIconFirstTopLevelProperty("type", rawJSON);
auto value = manifest.icons[0].type;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testIconsSizes(const String &rawJSON, size_t expectedCount, size_t testIndex, const String& expectedValue)
{
auto manifest = parseIconFirstTopLevelProperty("sizes", rawJSON);
auto value = manifest.icons[0].sizes;
EXPECT_EQ(expectedCount, value.size());
EXPECT_TRUE(testIndex < value.size());
EXPECT_STREQ(expectedValue.utf8().data(), value[testIndex].utf8().data());
}
void testIconsPurposes(const String &rawJSON, OptionSet<ApplicationManifest::Icon::Purpose> expectedValues)
{
auto manifest = parseIconFirstTopLevelProperty("purpose", rawJSON);
auto value = manifest.icons[0].purposes;
EXPECT_EQ(expectedValues, value);
}
};
static void assertManifestHasDefaultValues(const URL& manifestURL, const URL& documentURL, const ApplicationManifest& manifest)
{
EXPECT_TRUE(manifest.name.isNull());
EXPECT_TRUE(manifest.shortName.isNull());
EXPECT_TRUE(manifest.description.isNull());
EXPECT_STREQ("https://example.com/", manifest.scope.string().utf8().data());
EXPECT_STREQ(documentURL.string().utf8().data(), manifest.startURL.string().utf8().data());
}
TEST_F(ApplicationManifestParserTest, DefaultManifest)
{
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString(String()));
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString(""));
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString("{ }"));
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString("This is 100% not JSON."));
}
TEST_F(ApplicationManifestParserTest, StartURL)
{
m_documentURL = { { }, "https://example.com/home" };
m_manifestURL = { { }, "https://example.com/manifest.json" };
testStartURL("123", m_documentURL);
testStartURL("null", m_documentURL);
testStartURL("true", m_documentURL);
testStartURL("{ }", m_documentURL);
testStartURL("[ ]", m_documentURL);
testStartURL("[ \"http://example.com/somepage\" ]", m_documentURL);
testStartURL("\"\"", m_documentURL);
testStartURL("\"http:?\"", m_documentURL);
testStartURL("\"appstartpage\"", "https://example.com/appstartpage");
testStartURL("\"a/b/cdefg\"", "https://example.com/a/b/cdefg");
m_documentURL = { { }, "https://example.com/subfolder/home" };
m_manifestURL = { { }, "https://example.com/resources/manifest.json" };
testStartURL("\"resource-relative-to-manifest-url\"", "https://example.com/resources/resource-relative-to-manifest-url");
testStartURL("\"http://different-page.com/12/34\"", m_documentURL);
m_documentURL = { { }, "https://example.com/home" };
m_manifestURL = { { }, "https://other-domain.com/manifiest.json" };
testStartURL("\"resource_on_other_domain\"", m_documentURL);
testStartURL("\"http://example.com/scheme-does-not-match-document\"", m_documentURL);
testStartURL("\"https://example.com:123/port-does-not-match-document", m_documentURL);
testStartURL("\"https://example.com/page2\"", "https://example.com/page2");
testStartURL("\"//example.com/page2\"", "https://example.com/page2");
m_documentURL = { { }, "https://example.com/a" };
m_manifestURL = { { }, "https://example.com/z/manifest.json" };
testStartURL("\"b/c\"", "https://example.com/z/b/c");
testStartURL("\"/b/c\"", "https://example.com/b/c");
testStartURL("\"?query\"", "https://example.com/z/manifest.json?query");
m_documentURL = { { }, "https://example.com/dir1/dir2/page1" };
m_manifestURL = { { }, "https://example.com/dir3/manifest.json" };
testStartURL("\"../page2\"", "https://example.com/page2");
}
TEST_F(ApplicationManifestParserTest, Display)
{
testDisplay("123", ApplicationManifest::Display::Browser);
testDisplay("null", ApplicationManifest::Display::Browser);
testDisplay("true", ApplicationManifest::Display::Browser);
testDisplay("{ }", ApplicationManifest::Display::Browser);
testDisplay("[ ]", ApplicationManifest::Display::Browser);
testDisplay("\"\"", ApplicationManifest::Display::Browser);
testDisplay("\"garbage string\"", ApplicationManifest::Display::Browser);
testDisplay("\"browser\"", ApplicationManifest::Display::Browser);
testDisplay("\"standalone\"", ApplicationManifest::Display::Standalone);
testDisplay("\"minimal-ui\"", ApplicationManifest::Display::MinimalUI);
testDisplay("\"fullscreen\"", ApplicationManifest::Display::Fullscreen);
testDisplay("\"\t\nMINIMAL-UI \"", ApplicationManifest::Display::MinimalUI);
}
TEST_F(ApplicationManifestParserTest, Name)
{
testName("123", String());
testName("null", String());
testName("true", String());
testName("{ }", String());
testName("[ ]", String());
testName("\"\"", "");
testName("\"example\"", "example");
testName("\"\\t Hello\\nWorld\\t \"", "Hello\nWorld");
}
TEST_F(ApplicationManifestParserTest, Description)
{
testDescription("123", String());
testDescription("null", String());
testDescription("true", String());
testDescription("{ }", String());
testDescription("[ ]", String());
testDescription("\"\"", "");
testDescription("\"example\"", "example");
testDescription("\"\\t Hello\\nWorld\\t \"", "Hello\nWorld");
}
TEST_F(ApplicationManifestParserTest, ShortName)
{
testShortName("123", String());
testShortName("null", String());
testShortName("true", String());
testShortName("{ }", String());
testShortName("[ ]", String());
testShortName("\"\"", "");
testShortName("\"example\"", "example");
testShortName("\"\\t Hello\\nWorld\\t \"", "Hello\nWorld");
}
TEST_F(ApplicationManifestParserTest, Scope)
{
// If the scope is not a string or not a valid URL, return the default scope (the parent path of the start URL).
m_documentURL = { { }, "https://example.com/a/page?queryParam=value#fragment" };
m_manifestURL = { { }, "https://example.com/manifest.json" };
testScope("123", "https://example.com/a/");
testScope("null", "https://example.com/a/");
testScope("true", "https://example.com/a/");
testScope("{ }", "https://example.com/a/");
testScope("[ ]", "https://example.com/a/");
testScope("\"\"", "https://example.com/a/");
testScope("\"http:?\"", "https://example.com/a/");
m_documentURL = { { }, "https://example.com/a/pageEndingWithSlash/" };
testScope("null", "https://example.com/a/pageEndingWithSlash/");
// If scope URL is not same origin as document URL, return the default scope.
m_documentURL = { { }, "https://example.com/home" };
m_manifestURL = { { }, "https://other-site.com/manifest.json" };
testScope("\"https://other-site.com/some-scope\"", "https://example.com/");
m_documentURL = { { }, "https://example.com/app/home" };
m_manifestURL = { { }, "https://example.com/app/manifest.json" };
// If start URL is not within scope of scope URL, return the default scope.
testScope("\"https://example.com/subdirectory\"", "https://example.com/app/");
testScope("\"https://example.com/app\"", "https://example.com/app");
testScope("\"https://example.com/APP\"", "https://example.com/app/");
testScope("\"https://example.com/a\"", "https://example.com/a");
m_documentURL = { { }, "https://example.com/a/b/c/index" };
m_manifestURL = { { }, "https://example.com/a/manifest.json" };
testScope("\"./b/c/index\"", "https://example.com/a/b/c/index");
testScope("\"b/somewhere-else/../c\"", "https://example.com/a/b/c");
testScope("\"b\"", "https://example.com/a/b");
testScope("\"b/\"", "https://example.com/a/b/");
m_documentURL = { { }, "https://example.com/documents/home" };
m_manifestURL = { { }, "https://example.com/resources/manifest.json" };
// It's fine if the document URL or manifest URL aren't within the application scope - only the start URL needs to be.
testScope("\"https://example.com/other\"", String("https://example.com/other/start-url"), "https://example.com/other");
}
TEST_F(ApplicationManifestParserTest, ThemeColor)
{
testThemeColor("123", Color());
testThemeColor("null", Color());
testThemeColor("true", Color());
testThemeColor("{ }", Color());
testThemeColor("[ ]", Color());
testThemeColor("\"\"", Color());
testThemeColor("\"garbage string\"", Color());
testThemeColor("\"red\"", Color::red);
testThemeColor("\"#f00\"", Color::red);
testThemeColor("\"#ff0000\"", Color::red);
testThemeColor("\"#ff0000ff\"", Color::red);
testThemeColor("\"rgb(255, 0, 0)\"", Color::red);
testThemeColor("\"rgba(255, 0, 0, 1)\"", Color::red);
testThemeColor("\"hsl(0, 100%, 50%)\"", Color::red);
testThemeColor("\"hsla(0, 100%, 50%, 1)\"", Color::red);
}
TEST_F(ApplicationManifestParserTest, Whitespace)
{
auto manifest = parseString(" { \"name\": \"PASS\" }\n"_s);
EXPECT_STREQ("PASS", manifest.name.utf8().data());
}
TEST_F(ApplicationManifestParserTest, Icons)
{
URL srcURL = { { }, "https://example.com/icon.jpg" };
testIconsSrc("\"icon.jpg\"", srcURL);
testIconsType("\"image/webp\"", "image/webp");
testIconsSizes("\"256x256\"", 1, 0, "256x256");
testIconsSizes("\"72x72 96x96\"", 2, 0, "72x72");
testIconsSizes("\"72x72 96x96\"", 2, 1, "96x96");
OptionSet<ApplicationManifest::Icon::Purpose> purposeAny { ApplicationManifest::Icon::Purpose::Any };
OptionSet<ApplicationManifest::Icon::Purpose> purposeMonochrome { ApplicationManifest::Icon::Purpose::Monochrome };
OptionSet<ApplicationManifest::Icon::Purpose> purposeMaskable { ApplicationManifest::Icon::Purpose::Maskable };
testIconsPurposes("\"monochrome\"", purposeMonochrome);
testIconsPurposes("\"maskable\"", purposeMaskable);
testIconsPurposes("\"any\"", purposeAny);
testIconsPurposes("\"\tMONOCHROME\"", purposeMonochrome);
testIconsPurposes("123", purposeAny);
testIconsPurposes("null", purposeAny);
testIconsPurposes("true", purposeAny);
testIconsPurposes("{ }", purposeAny);
testIconsPurposes("[ ]", purposeAny);
OptionSet<ApplicationManifest::Icon::Purpose> purposeMonochromeAny { ApplicationManifest::Icon::Purpose::Monochrome, ApplicationManifest::Icon::Purpose::Any };
testIconsPurposes("\"monochrome any\"", purposeMonochromeAny);
}
#endif