Commit 48a14165 authored by Miras Myrzakerey's avatar Miras Myrzakerey Committed by Chromium LUCI CQ

Implement WebBundler.

Implement encoding part of Save-Page-As-WebBundle feature by
introducing a WebBundleBuilder class.
This CL will encode a series of snapshots (from a single web page)
into a correct WebBundle.

Bug: 1040752

Change-Id: I3ab17deb1d658b3098ba32916e8832ea0c829e25
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2215543
Commit-Queue: Miras Myrzakerey <myrzakereyms@google.com>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarKunihiko Sakamoto <ksakamoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#840658}
parent 4fa672d3
...@@ -26,6 +26,11 @@ gen-bundle \ ...@@ -26,6 +26,11 @@ gen-bundle \
-manifestURL https://test.example.org/manifest.webmanifest \ -manifestURL https://test.example.org/manifest.webmanifest \
-o hello.wbn -o hello.wbn
gen-bundle \
-har simple.har \
-o simple.wbn \
-primaryURL https://test.example.org/ \
sign-bundle \ sign-bundle \
-i hello.wbn \ -i hello.wbn \
-certificate $sxg_test_data_dir/test.example.org.public.pem.cbor \ -certificate $sxg_test_data_dir/test.example.org.public.pem.cbor \
......
{
"log": {
"version": "1.2",
"entries": [
{
"request": {
"method": "GET",
"url": "https://test.example.org/"
},
"response": {
"status": 200,
"headers": [
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
}
],
"content": {
"text": "<a href='index.html'>click for web bundles</a>"
}
}
},
{
"request": {
"method": "GET",
"url": "https://test.example.org/index.html"
},
"response": {
"status": 200,
"headers": [
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
}
],
"content": {
"text": "<p>Hello Web Bundles!</p>"
}
}
}
]
}
}
...@@ -5,18 +5,23 @@ ...@@ -5,18 +5,23 @@
#include "base/bind.h" #include "base/bind.h"
#include "base/files/scoped_temp_dir.h" #include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h" #include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/sequenced_task_runner_handle.h" #include "base/threading/sequenced_task_runner_handle.h"
#include "content/browser/download/download_manager_impl.h" #include "content/browser/download/download_manager_impl.h"
#include "content/browser/download/save_package.h" #include "content/browser/download/save_package.h"
#include "content/browser/web_package/web_bundle_utils.h"
#include "content/public/browser/browser_context.h" #include "content/public/browser/browser_context.h"
#include "content/public/browser/download_manager.h" #include "content/public/browser/download_manager.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h" #include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h" #include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/download_test_observer.h" #include "content/public/test/download_test_observer.h"
#include "content/shell/browser/shell.h" #include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_download_manager_delegate.h" #include "content/shell/browser/shell_download_manager_delegate.h"
#include "net/base/filename_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/embedded_test_server.h"
namespace content { namespace content {
...@@ -35,7 +40,12 @@ class TestShellDownloadManagerDelegate : public ShellDownloadManagerDelegate { ...@@ -35,7 +40,12 @@ class TestShellDownloadManagerDelegate : public ShellDownloadManagerDelegate {
const base::FilePath::StringType& default_extension, const base::FilePath::StringType& default_extension,
bool can_save_as_complete, bool can_save_as_complete,
SavePackagePathPickedCallback callback) override { SavePackagePathPickedCallback callback) override {
std::move(callback).Run(suggested_path, save_page_type_, base::FilePath suggested_path_copy = suggested_path;
if (save_page_type_ == SAVE_PAGE_TYPE_AS_WEB_BUNDLE) {
suggested_path_copy =
suggested_path_copy.ReplaceExtension(FILE_PATH_LITERAL("wbn"));
}
std::move(callback).Run(suggested_path_copy, save_page_type_,
SavePackageDownloadCreatedCallback()); SavePackageDownloadCreatedCallback());
} }
...@@ -77,36 +87,38 @@ class DownloadicidalObserver : public DownloadManager::Observer { ...@@ -77,36 +87,38 @@ class DownloadicidalObserver : public DownloadManager::Observer {
base::OnceClosure after_closure_; base::OnceClosure after_closure_;
}; };
class DownloadCancelObserver : public DownloadManager::Observer { class DownloadCompleteObserver : public DownloadManager::Observer {
public: public:
explicit DownloadCancelObserver(base::OnceClosure canceled_closure, explicit DownloadCompleteObserver(base::OnceClosure completed_closure)
std::string* mime_type_out) : completed_closure_(std::move(completed_closure)) {}
: canceled_closure_(std::move(canceled_closure)), DownloadCompleteObserver(const DownloadCompleteObserver&) = delete;
mime_type_out_(mime_type_out) {} DownloadCompleteObserver& operator=(const DownloadCompleteObserver&) = delete;
DownloadCancelObserver(const DownloadCancelObserver&) = delete;
DownloadCancelObserver& operator=(const DownloadCancelObserver&) = delete;
void OnDownloadCreated(DownloadManager* manager, void OnDownloadCreated(DownloadManager* manager,
download::DownloadItem* item) override { download::DownloadItem* item) override {
*mime_type_out_ = item->GetMimeType(); DCHECK(!item_observer_);
DCHECK(!item_cancel_observer_); mime_type_ = item->GetMimeType();
item_cancel_observer_ = std::make_unique<DownloadItemCancelObserver>( target_file_path_ = item->GetTargetFilePath();
item, std::move(canceled_closure_)); item_observer_ = std::make_unique<DownloadItemCompleteObserver>(
item, std::move(completed_closure_));
} }
const std::string& mime_type() const { return mime_type_; }
const base::FilePath& target_file_path() const { return target_file_path_; }
private: private:
class DownloadItemCancelObserver : public download::DownloadItem::Observer { class DownloadItemCompleteObserver : public download::DownloadItem::Observer {
public: public:
DownloadItemCancelObserver(download::DownloadItem* item, DownloadItemCompleteObserver(download::DownloadItem* item,
base::OnceClosure canceled_closure) base::OnceClosure completed_closure)
: item_(item), canceled_closure_(std::move(canceled_closure)) { : item_(item), completed_closure_(std::move(completed_closure)) {
item_->AddObserver(this); item_->AddObserver(this);
} }
DownloadItemCancelObserver(const DownloadItemCancelObserver&) = delete; DownloadItemCompleteObserver(const DownloadItemCompleteObserver&) = delete;
DownloadItemCancelObserver& operator=(const DownloadItemCancelObserver&) = DownloadItemCompleteObserver& operator=(
delete; const DownloadItemCompleteObserver&) = delete;
~DownloadItemCancelObserver() override { ~DownloadItemCompleteObserver() override {
if (item_) if (item_)
item_->RemoveObserver(this); item_->RemoveObserver(this);
} }
...@@ -114,8 +126,8 @@ class DownloadCancelObserver : public DownloadManager::Observer { ...@@ -114,8 +126,8 @@ class DownloadCancelObserver : public DownloadManager::Observer {
private: private:
void OnDownloadUpdated(download::DownloadItem* item) override { void OnDownloadUpdated(download::DownloadItem* item) override {
DCHECK_EQ(item_, item); DCHECK_EQ(item_, item);
if (item_->GetState() == download::DownloadItem::CANCELLED) if (item_->GetState() == download::DownloadItem::COMPLETE)
std::move(canceled_closure_).Run(); std::move(completed_closure_).Run();
} }
void OnDownloadDestroyed(download::DownloadItem* item) override { void OnDownloadDestroyed(download::DownloadItem* item) override {
...@@ -125,12 +137,13 @@ class DownloadCancelObserver : public DownloadManager::Observer { ...@@ -125,12 +137,13 @@ class DownloadCancelObserver : public DownloadManager::Observer {
} }
download::DownloadItem* item_; download::DownloadItem* item_;
base::OnceClosure canceled_closure_; base::OnceClosure completed_closure_;
}; };
std::unique_ptr<DownloadItemCancelObserver> item_cancel_observer_; std::unique_ptr<DownloadItemCompleteObserver> item_observer_;
base::OnceClosure canceled_closure_; base::OnceClosure completed_closure_;
std::string* mime_type_out_; std::string mime_type_;
base::FilePath target_file_path_;
}; };
} // namespace } // namespace
...@@ -234,15 +247,26 @@ IN_PROC_BROWSER_TEST_F(SavePackageBrowserTest, DownloadItemCanceled) { ...@@ -234,15 +247,26 @@ IN_PROC_BROWSER_TEST_F(SavePackageBrowserTest, DownloadItemCanceled) {
RunAndCancelSavePackageDownload(SAVE_PAGE_TYPE_AS_MHTML, false); RunAndCancelSavePackageDownload(SAVE_PAGE_TYPE_AS_MHTML, false);
} }
// Currently, SavePageAsWebBundle feature is not implemented yet. class SavePackageWebBundleBrowserTest : public SavePackageBrowserTest {
// WebContentsImpl::GenerateWebBundle() will call the passed callback with 0 public:
// file size and WebBundlerError::kNotImplemented via WebBundler in the utility void SetUp() override {
// process which means it cancels all SavePageAsWebBundle requests. So this test std::vector<base::Feature> enable_features;
// checks that the request is successfully canceled. std::vector<base::Feature> disabled_features;
// TODO(crbug.com/1040752): Implement WebBundler and update this test. enable_features.push_back(features::kSavePageAsWebBundle);
IN_PROC_BROWSER_TEST_F(SavePackageBrowserTest, SaveAsWebBundleCanceled) { enable_features.push_back(features::kWebBundles);
feature_list_.InitWithFeatures(enable_features, disabled_features);
SavePackageBrowserTest::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(SavePackageWebBundleBrowserTest, OnePageSimple) {
ASSERT_TRUE(embedded_test_server()->Start()); ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/page_with_iframe.html"); GURL url = embedded_test_server()->GetURL(
"/web_bundle/save_page_as_web_bundle/one_page_simple.html");
EXPECT_TRUE(NavigateToURL(shell(), url)); EXPECT_TRUE(NavigateToURL(shell(), url));
auto* download_manager = auto* download_manager =
static_cast<DownloadManagerImpl*>(BrowserContext::GetDownloadManager( static_cast<DownloadManagerImpl*>(BrowserContext::GetDownloadManager(
...@@ -253,21 +277,30 @@ IN_PROC_BROWSER_TEST_F(SavePackageBrowserTest, SaveAsWebBundleCanceled) { ...@@ -253,21 +277,30 @@ IN_PROC_BROWSER_TEST_F(SavePackageBrowserTest, SaveAsWebBundleCanceled) {
auto* old_delegate = download_manager->GetDelegate(); auto* old_delegate = download_manager->GetDelegate();
download_manager->SetDelegate(delegate.get()); download_manager->SetDelegate(delegate.get());
GURL wbn_file_url;
{ {
base::RunLoop run_loop; base::RunLoop run_loop;
std::string mime_type; DownloadCompleteObserver observer(run_loop.QuitClosure());
DownloadCancelObserver observer(run_loop.QuitClosure(), &mime_type);
download_manager->AddObserver(&observer); download_manager->AddObserver(&observer);
scoped_refptr<SavePackage> save_package( scoped_refptr<SavePackage> save_package(
new SavePackage(shell()->web_contents())); new SavePackage(shell()->web_contents()));
save_package->GetSaveInfo(); save_package->GetSaveInfo();
run_loop.Run(); run_loop.Run();
download_manager->RemoveObserver(&observer); download_manager->RemoveObserver(&observer);
EXPECT_TRUE(save_package->canceled()); EXPECT_TRUE(save_package->finished());
EXPECT_EQ("application/webbundle", mime_type); EXPECT_EQ("application/webbundle", observer.mime_type());
wbn_file_url = net::FilePathToFileURL(observer.target_file_path());
} }
download_manager->SetDelegate(old_delegate); download_manager->SetDelegate(old_delegate);
EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
base::string16 expected_title = base::ASCIIToUTF16("Hello");
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
EXPECT_TRUE(NavigateToURL(
shell(), wbn_file_url,
web_bundle_utils::GetSynthesizedUrlForWebBundle(wbn_file_url, url)));
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
} }
} // namespace content } // namespace content
...@@ -271,12 +271,9 @@ IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest, ...@@ -271,12 +271,9 @@ IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,
ASSERT_TRUE(CreateSaveDir()); ASSERT_TRUE(CreateSaveDir());
const auto file_path = const auto file_path =
save_dir_.GetPath().Append(FILE_PATH_LITERAL("test.wbn")); save_dir_.GetPath().Append(FILE_PATH_LITERAL("test.wbn"));
// Currently WebBundler in the data decoder service is not implemented yet, const auto result = GenerateWebBundle(file_path);
// and just returns kNotImplemented. EXPECT_GT(std::get<0>(result), 0lu);
// TODO(crbug.com/1040752): Implement WebBundler and update test. EXPECT_EQ(std::get<1>(result), data_decoder::mojom::WebBundlerError::kOK);
EXPECT_EQ(
std::make_tuple(0, data_decoder::mojom::WebBundlerError::kNotImplemented),
GenerateWebBundle(file_path));
} }
IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest, IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,
......
...@@ -13,6 +13,8 @@ source_set("lib") { ...@@ -13,6 +13,8 @@ source_set("lib") {
"gzipper.h", "gzipper.h",
"json_parser_impl.cc", "json_parser_impl.cc",
"json_parser_impl.h", "json_parser_impl.h",
"web_bundle_builder.cc",
"web_bundle_builder.h",
"web_bundler.cc", "web_bundler.cc",
"web_bundler.h", "web_bundler.h",
"xml_parser.cc", "xml_parser.cc",
...@@ -31,6 +33,7 @@ source_set("lib") { ...@@ -31,6 +33,7 @@ source_set("lib") {
deps = [ deps = [
"//base", "//base",
"//build:chromeos_buildflags", "//build:chromeos_buildflags",
"//components/cbor",
"//components/web_package", "//components/web_package",
"//mojo/public/cpp/bindings", "//mojo/public/cpp/bindings",
"//net", "//net",
...@@ -62,6 +65,7 @@ source_set("tests") { ...@@ -62,6 +65,7 @@ source_set("tests") {
"public/cpp/json_sanitizer_unittest.cc", "public/cpp/json_sanitizer_unittest.cc",
"public/cpp/safe_web_bundle_parser_unittest.cc", "public/cpp/safe_web_bundle_parser_unittest.cc",
"public/cpp/safe_xml_parser_unittest.cc", "public/cpp/safe_xml_parser_unittest.cc",
"web_bundle_builder_unittest.cc",
"xml_parser_unittest.cc", "xml_parser_unittest.cc",
] ]
......
include_rules = [ include_rules = [
"+components/cbor",
"+components/web_package", "+components/web_package",
"+device/bluetooth/public", "+device/bluetooth/public",
"+gin", "+gin",
......
...@@ -9,9 +9,10 @@ import "mojo/public/mojom/base/file.mojom"; ...@@ -9,9 +9,10 @@ import "mojo/public/mojom/base/file.mojom";
enum WebBundlerError { enum WebBundlerError {
kOK, kOK,
kNotImplemented, kConnectionError,
kFileOpenFailed, kFileOpenFailed,
kWebBundlerConnectionError, kWebBundlerConnectionError,
kInvalidInput,
}; };
// Bundler interface to generate a web bundle from snapshots. // Bundler interface to generate a web bundle from snapshots.
......
// Copyright 2020 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 "services/data_decoder/web_bundle_builder.h"
#include "base/big_endian.h"
#include "components/cbor/writer.h"
namespace data_decoder {
namespace {
// TODO(myrzakereyms): replace this method with cbor::writer::GetNumUintBytes.
uint64_t GetNumUintBytes(uint64_t value) {
if (value < 24) {
return 0;
} else if (value <= 0xFF) {
return 1;
} else if (value <= 0xFFFF) {
return 2;
} else if (value <= 0xFFFFFFFF) {
return 4;
}
return 8;
}
uint64_t GetEncodedByteSizeOfString(uint64_t size) {
return 1 + GetNumUintBytes(size);
}
uint64_t GetEncodedByteSizeOfHeaders(const WebBundleBuilder::Headers& headers) {
uint64_t byte_size = 1 + GetNumUintBytes(headers.size());
for (const auto& header : headers) {
byte_size +=
GetEncodedByteSizeOfString(header.first.size()) + header.first.size() +
GetEncodedByteSizeOfString(header.second.size()) + header.second.size();
}
return byte_size;
}
uint64_t GetEncodedByteSizeOfResponse(const WebBundleBuilder::Headers& headers,
uint64_t body_size) {
uint64_t encoded_header_map_size = GetEncodedByteSizeOfHeaders(headers);
return 1 /* size of header of array(2) */ +
GetEncodedByteSizeOfString(encoded_header_map_size) +
encoded_header_map_size + GetEncodedByteSizeOfString(body_size) +
body_size;
}
cbor::Value CreateByteString(base::StringPiece s) {
return cbor::Value(base::as_bytes(base::make_span(s)));
}
cbor::Value CreateHeaderMap(const WebBundleBuilder::Headers& headers) {
cbor::Value::MapValue map;
for (const auto& pair : headers)
map.insert({CreateByteString(pair.first), CreateByteString(pair.second)});
return cbor::Value(std::move(map));
}
std::vector<uint8_t> Encode(const cbor::Value& value) {
return *cbor::Writer::Write(value);
}
int64_t EncodedLength(const cbor::Value& value) {
return Encode(value).size();
}
} // namespace
WebBundleBuilder::WebBundleBuilder(const std::string& fallback_url)
: fallback_url_(fallback_url) {}
WebBundleBuilder::~WebBundleBuilder() = default;
void WebBundleBuilder::SetExchanges(
std::vector<mojom::SerializedResourceInfoPtr> resources,
std::vector<base::Optional<mojo_base::BigBuffer>> bodies) {
CHECK_EQ(resources.size(), bodies.size());
int64_t responses_offset = 1 + GetNumUintBytes(resources.size());
for (size_t i = 0; i < resources.size(); ++i) {
const auto& info = resources[i];
const auto& body = bodies[i];
Headers headers = {{":status", "200"}, {"content-type", info->mime_type}};
uint64_t response_length =
GetEncodedByteSizeOfResponse(headers, body ? body->size() : 0);
ResponseLocation location = {responses_offset, response_length};
responses_offset += response_length;
cbor::Value::ArrayValue response_array;
response_array.emplace_back(Encode(CreateHeaderMap(headers)));
response_array.emplace_back(CreateByteString(
body ? base::StringPiece(reinterpret_cast<const char*>(body->data()),
body->size())
: ""));
cbor::Value response(response_array);
responses_.emplace_back(std::move(response));
GURL url = info->url;
GURL::Replacements replacements;
replacements.ClearRef();
url = url.ReplaceComponents(replacements);
AddIndexEntry(url.spec(), "", {location});
}
}
void WebBundleBuilder::AddIndexEntry(
base::StringPiece url,
base::StringPiece variants_value,
std::vector<ResponseLocation> response_locations) {
cbor::Value::ArrayValue index_value_array;
index_value_array.emplace_back(CreateByteString(variants_value));
for (const auto& location : response_locations) {
index_value_array.emplace_back(location.offset);
index_value_array.emplace_back(location.length);
}
index_.insert({cbor::Value(url), cbor::Value(index_value_array)});
}
void WebBundleBuilder::AddSection(base::StringPiece name, cbor::Value section) {
section_lengths_.emplace_back(name);
section_lengths_.emplace_back(EncodedLength(section));
sections_.emplace_back(std::move(section));
}
std::vector<uint8_t> WebBundleBuilder::CreateBundle(
std::vector<mojom::SerializedResourceInfoPtr> resources,
std::vector<base::Optional<mojo_base::BigBuffer>> bodies) {
SetExchanges(std::move(resources), std::move(bodies));
AddSection("index", cbor::Value(index_));
AddSection("responses", cbor::Value(responses_));
return CreateTopLevel();
}
std::vector<uint8_t> WebBundleBuilder::CreateTopLevel() {
cbor::Value::ArrayValue toplevel_array;
toplevel_array.emplace_back(
CreateByteString(u8"\U0001F310\U0001F4E6")); // "🌐📦"
toplevel_array.emplace_back(CreateByteString(base::StringPiece("b1\0\0", 4)));
toplevel_array.emplace_back(cbor::Value(fallback_url_));
toplevel_array.emplace_back(Encode(cbor::Value(section_lengths_)));
toplevel_array.emplace_back(sections_);
// Put a dummy 8-byte bytestring.
toplevel_array.emplace_back(cbor::Value::BinaryValue(8, 0));
std::vector<uint8_t> bundle = Encode(cbor::Value(toplevel_array));
char encoded[8];
base::WriteBigEndian(encoded, static_cast<uint64_t>(bundle.size()));
// Overwrite the dummy bytestring with the actual size.
memcpy(bundle.data() + bundle.size() - 8, encoded, 8);
return bundle;
}
} // namespace data_decoder
// Copyright 2020 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 SERVICES_DATA_DECODER_WEB_BUNDLE_BUILDER_H_
#define SERVICES_DATA_DECODER_WEB_BUNDLE_BUILDER_H_
#include <vector>
#include "base/files/file.h"
#include "base/optional.h"
#include "base/strings/string_piece.h"
#include "components/cbor/values.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "services/data_decoder/public/mojom/resource_snapshot_for_web_bundle.mojom.h"
namespace data_decoder {
class WebBundleBuilder {
public:
using Headers = std::vector<std::pair<std::string, std::string>>;
struct ResponseLocation {
// /components/cbor uses int64_t for integer types.
int64_t offset;
int64_t length;
};
explicit WebBundleBuilder(const std::string& fallback_url);
~WebBundleBuilder();
WebBundleBuilder(const WebBundleBuilder&) = delete;
WebBundleBuilder& operator=(const WebBundleBuilder&) = delete;
std::vector<uint8_t> CreateBundle(
std::vector<mojom::SerializedResourceInfoPtr> resources,
std::vector<base::Optional<mojo_base::BigBuffer>> bodies);
private:
void SetExchanges(std::vector<mojom::SerializedResourceInfoPtr> resources,
std::vector<base::Optional<mojo_base::BigBuffer>> bodies);
void AddIndexEntry(base::StringPiece url,
base::StringPiece variants_value,
std::vector<ResponseLocation> response_locations);
void AddSection(base::StringPiece name, cbor::Value section);
void WriteBundleLength(uint8_t bundle_length);
std::vector<uint8_t> CreateTopLevel();
std::string fallback_url_;
cbor::Value::ArrayValue section_lengths_;
cbor::Value::ArrayValue sections_;
cbor::Value::MapValue index_;
cbor::Value::ArrayValue responses_;
};
} // namespace data_decoder
#endif // SERVICES_DATA_DECODER_WEB_BUNDLE_BUILDER_H_
// Copyright 2020 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 "services/data_decoder/web_bundle_builder.h"
#include "base/big_endian.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace data_decoder {
namespace {
std::string kFallbackUrl = "https://test.example.org/";
std::string GetTestFileContents(const base::FilePath& path) {
base::FilePath test_data_dir;
base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir);
test_data_dir = test_data_dir.Append(
base::FilePath(FILE_PATH_LITERAL("components/test/data/web_package")));
std::string contents;
EXPECT_TRUE(base::ReadFileToString(test_data_dir.Append(path), &contents));
return contents;
}
std::vector<uint8_t> GetStringAsBytes(base::StringPiece contents) {
auto bytes = base::as_bytes(base::make_span(contents));
return std::vector<uint8_t>(bytes.begin(), bytes.end());
}
} // namespace
class WebBundleBuilderTest : public testing::Test {
private:
base::test::TaskEnvironment task_environment_;
};
TEST_F(WebBundleBuilderTest, CorrectWebBundleSizeIsWritten) {
WebBundleBuilder builder(kFallbackUrl);
std::vector<mojom::SerializedResourceInfoPtr> exchanges;
mojom::SerializedResourceInfoPtr ptr = mojom::SerializedResourceInfo::New(
GURL("https://test.example.org/index.html"), "text/html", 0);
exchanges.emplace_back(std::move(ptr));
std::vector<base::Optional<mojo_base::BigBuffer>> bodies;
bodies.emplace_back();
std::vector<uint8_t> bundle =
builder.CreateBundle(std::move(exchanges), std::move(bodies));
char written_size[8];
memcpy(written_size, bundle.data() + bundle.size() - 8, 8);
uint64_t written_size_int;
base::ReadBigEndian(written_size, &written_size_int);
EXPECT_EQ(bundle.size(), written_size_int);
}
TEST_F(WebBundleBuilderTest, ByteByByteComparison) {
WebBundleBuilder builder(kFallbackUrl);
std::vector<mojom::SerializedResourceInfoPtr> exchanges;
std::vector<base::Optional<mojo_base::BigBuffer>> bodies;
exchanges.emplace_back(mojom::SerializedResourceInfo::New(
GURL("https://test.example.org/"), "text/html; charset=UTF-8", 46));
bodies.emplace_back(base::Optional<mojo_base::BigBuffer>(
GetStringAsBytes("<a href='index.html'>click for web bundles</a>")));
exchanges.emplace_back(mojom::SerializedResourceInfo::New(
GURL("https://test.example.org/index.html"), "text/html; charset=UTF-8",
25));
bodies.emplace_back(base::Optional<mojo_base::BigBuffer>(
GetStringAsBytes("<p>Hello Web Bundles!</p>")));
std::vector<uint8_t> bundle =
builder.CreateBundle(std::move(exchanges), std::move(bodies));
std::vector<uint8_t> expected_bundle = GetStringAsBytes(
GetTestFileContents(base::FilePath(FILE_PATH_LITERAL("simple.wbn"))));
EXPECT_EQ(bundle, expected_bundle);
}
} // namespace data_decoder
...@@ -4,16 +4,121 @@ ...@@ -4,16 +4,121 @@
#include "services/data_decoder/web_bundler.h" #include "services/data_decoder/web_bundler.h"
#include "base/big_endian.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_piece.h"
#include "web_bundle_builder.h"
namespace data_decoder { namespace data_decoder {
// WebBundler does not permit body size larder than ~1GB.
const uint64_t kMaxBodySize = (1 << 30);
WebBundler::WebBundler() = default;
WebBundler::~WebBundler() = default;
void WebBundler::Generate( void WebBundler::Generate(
std::vector<mojo::PendingRemote<mojom::ResourceSnapshotForWebBundle>> std::vector<mojo::PendingRemote<mojom::ResourceSnapshotForWebBundle>>
snapshots, snapshots,
base::File file, base::File file,
GenerateCallback callback) { GenerateCallback callback) {
// The Web Bundle generation logic is not implemented yet. DCHECK(snapshots_.empty());
// TODO(crbug.com/1040752): Implement this. DCHECK(!snapshots.empty());
std::move(callback).Run(0, mojom::WebBundlerError::kNotImplemented); for (auto& pending_snapshot : snapshots) {
mojo::Remote<mojom::ResourceSnapshotForWebBundle> snapshot(
std::move(pending_snapshot));
snapshot.set_disconnect_handler(
base::BindOnce(&WebBundler::OnConnectionError, base::Unretained(this)));
snapshots_.emplace_back(std::move(snapshot));
}
file_ = std::move(file);
callback_ = std::move(callback);
GetNextResourceCount();
}
void WebBundler::OnConnectionError() {
if (callback_) {
std::move(callback_).Run(0, mojom::WebBundlerError::kConnectionError);
}
}
void WebBundler::GetNextResourceCount() {
if (snapshots_.size() == resources_.size()) {
WriteWebBundleIndex();
return;
}
snapshots_[resources_.size()]->GetResourceCount(
base::BindOnce(&WebBundler::OnGetResourceCount, base::Unretained(this)));
}
void WebBundler::OnGetResourceCount(uint64_t count) {
pending_resource_count_ = count;
resources_.emplace_back();
bodies_.emplace_back();
GetNextResourceInfo();
}
void WebBundler::GetNextResourceInfo() {
if (pending_resource_count_ == 0) {
GetNextResourceCount();
return;
}
snapshots_[resources_.size() - 1]->GetResourceInfo(
resources_.rbegin()->size(),
base::BindOnce(&WebBundler::OnGetResourceInfo, base::Unretained(this)));
}
void WebBundler::OnGetResourceInfo(mojom::SerializedResourceInfoPtr info) {
resources_.rbegin()->emplace_back(std::move(info));
snapshots_[bodies_.size() - 1]->GetResourceBody(
bodies_.rbegin()->size(),
base::BindOnce(&WebBundler::OnGetResourceBody, base::Unretained(this)));
}
void WebBundler::OnGetResourceBody(base::Optional<mojo_base::BigBuffer> body) {
if (body->size() > kMaxBodySize) {
std::move(callback_).Run(0, mojom::WebBundlerError::kInvalidInput);
return;
}
bodies_.rbegin()->emplace_back(std::move(body));
--pending_resource_count_;
GetNextResourceInfo();
}
void WebBundler::WriteWebBundleIndex() {
if (!callback_) {
return;
}
GURL url = resources_[0][0]->url;
GURL::Replacements replacements;
replacements.ClearRef();
url = url.ReplaceComponents(replacements);
WebBundleBuilder builder(url.spec());
std::set<GURL> url_set;
CHECK_EQ(resources_.size(), bodies_.size());
std::vector<mojom::SerializedResourceInfoPtr> resources;
std::vector<base::Optional<mojo_base::BigBuffer>> bodies;
for (size_t i = 0; i < resources_.size(); ++i) {
auto& info_list = resources_[i];
auto& body_list = bodies_[i];
CHECK_EQ(info_list.size(), body_list.size());
for (size_t j = 0; j < info_list.size(); ++j) {
auto& info = info_list[j];
auto& body = body_list[j];
if (url_set.find(info->url) == url_set.end() && info->url.is_valid() &&
info->url.SchemeIsHTTPOrHTTPS()) {
url_set.insert(info->url);
resources.emplace_back(std::move(info));
bodies.emplace_back(std::move(body));
}
}
}
std::vector<uint8_t> bundle =
builder.CreateBundle(std::move(resources), std::move(bodies));
int written_size = file_.WriteAtCurrentPos(
reinterpret_cast<const char*>(bundle.data()), bundle.size());
DCHECK_EQ(static_cast<int>(bundle.size()), written_size);
std::move(callback_).Run(written_size, mojom::WebBundlerError::kOK);
} }
} // namespace data_decoder } // namespace data_decoder
...@@ -8,8 +8,10 @@ ...@@ -8,8 +8,10 @@
#include <vector> #include <vector>
#include "base/files/file.h" #include "base/files/file.h"
#include "base/optional.h"
#include "mojo/public/cpp/base/big_buffer.h" #include "mojo/public/cpp/base/big_buffer.h"
#include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/data_decoder/public/mojom/resource_snapshot_for_web_bundle.mojom.h" #include "services/data_decoder/public/mojom/resource_snapshot_for_web_bundle.mojom.h"
#include "services/data_decoder/public/mojom/web_bundler.mojom.h" #include "services/data_decoder/public/mojom/web_bundler.mojom.h"
...@@ -17,8 +19,8 @@ namespace data_decoder { ...@@ -17,8 +19,8 @@ namespace data_decoder {
class WebBundler : public mojom::WebBundler { class WebBundler : public mojom::WebBundler {
public: public:
WebBundler() = default; WebBundler();
~WebBundler() override = default; ~WebBundler() override;
WebBundler(const WebBundler&) = delete; WebBundler(const WebBundler&) = delete;
WebBundler& operator=(const WebBundler&) = delete; WebBundler& operator=(const WebBundler&) = delete;
...@@ -30,6 +32,21 @@ class WebBundler : public mojom::WebBundler { ...@@ -30,6 +32,21 @@ class WebBundler : public mojom::WebBundler {
snapshots, snapshots,
base::File file, base::File file,
GenerateCallback callback) override; GenerateCallback callback) override;
void OnConnectionError();
void GetNextResourceCount();
void OnGetResourceCount(uint64_t count);
void GetNextResourceInfo();
void OnGetResourceInfo(mojom::SerializedResourceInfoPtr info);
void OnGetResourceBody(base::Optional<mojo_base::BigBuffer> body);
void WriteWebBundleIndex();
std::vector<mojo::Remote<mojom::ResourceSnapshotForWebBundle>> snapshots_;
base::File file_;
GenerateCallback callback_;
std::vector<std::vector<mojom::SerializedResourceInfoPtr>> resources_;
std::vector<std::vector<base::Optional<mojo_base::BigBuffer>>> bodies_;
uint64_t pending_resource_count_;
}; };
} // namespace data_decoder } // namespace data_decoder
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment