| // 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 <math.h> |
| |
| #include "base/base64.h" |
| #include "base/file_util.h" |
| #include "base/path_service.h" |
| #include "base/pickle.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "content/common/page_state_serialization.h" |
| #include "content/public/common/content_paths.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace content { |
| namespace { |
| |
| #if defined(OS_WIN) |
| inline bool isnan(double num) { return !!_isnan(num); } |
| #endif |
| |
| base::NullableString16 NS16(const char* s) { |
| return s ? base::NullableString16(ASCIIToUTF16(s), false) : |
| base::NullableString16(); |
| } |
| |
| //----------------------------------------------------------------------------- |
| |
| template <typename T> |
| void ExpectEquality(const T& a, const T& b) { |
| EXPECT_EQ(a, b); |
| } |
| |
| template <typename T> |
| void ExpectEquality(const std::vector<T>& a, const std::vector<T>& b) { |
| EXPECT_EQ(a.size(), b.size()); |
| for (size_t i = 0; i < std::min(a.size(), b.size()); ++i) |
| ExpectEquality(a[i], b[i]); |
| } |
| |
| template <> |
| void ExpectEquality(const ExplodedHttpBodyElement& a, |
| const ExplodedHttpBodyElement& b) { |
| EXPECT_EQ(a.type, b.type); |
| EXPECT_EQ(a.data, b.data); |
| EXPECT_EQ(a.file_path, b.file_path); |
| EXPECT_EQ(a.url, b.url); |
| EXPECT_EQ(a.file_start, b.file_start); |
| EXPECT_EQ(a.file_length, b.file_length); |
| if (!(isnan(a.file_modification_time) && isnan(b.file_modification_time))) |
| EXPECT_DOUBLE_EQ(a.file_modification_time, b.file_modification_time); |
| } |
| |
| template <> |
| void ExpectEquality(const ExplodedHttpBody& a, const ExplodedHttpBody& b) { |
| EXPECT_EQ(a.http_content_type, b.http_content_type); |
| EXPECT_EQ(a.identifier, b.identifier); |
| EXPECT_EQ(a.contains_passwords, b.contains_passwords); |
| EXPECT_EQ(a.is_null, b.is_null); |
| ExpectEquality(a.elements, b.elements); |
| } |
| |
| template <> |
| void ExpectEquality(const ExplodedFrameState& a, const ExplodedFrameState& b) { |
| EXPECT_EQ(a.url_string, b.url_string); |
| EXPECT_EQ(a.original_url_string, b.original_url_string); |
| EXPECT_EQ(a.referrer, b.referrer); |
| EXPECT_EQ(a.target, b.target); |
| EXPECT_EQ(a.parent, b.parent); |
| EXPECT_EQ(a.title, b.title); |
| EXPECT_EQ(a.alternate_title, b.alternate_title); |
| EXPECT_EQ(a.state_object, b.state_object); |
| ExpectEquality(a.document_state, b.document_state); |
| EXPECT_EQ(a.scroll_offset, b.scroll_offset); |
| EXPECT_EQ(a.item_sequence_number, b.item_sequence_number); |
| EXPECT_EQ(a.document_sequence_number, b.document_sequence_number); |
| EXPECT_EQ(a.visit_count, b.visit_count); |
| EXPECT_EQ(a.visited_time, b.visited_time); |
| EXPECT_EQ(a.page_scale_factor, b.page_scale_factor); |
| EXPECT_EQ(a.is_target_item, b.is_target_item); |
| ExpectEquality(a.http_body, b.http_body); |
| ExpectEquality(a.children, b.children); |
| } |
| |
| void ExpectEquality(const ExplodedPageState& a, const ExplodedPageState& b) { |
| ExpectEquality(a.referenced_files, b.referenced_files); |
| ExpectEquality(a.top, b.top); |
| } |
| |
| //----------------------------------------------------------------------------- |
| |
| class PageStateSerializationTest : public testing::Test { |
| public: |
| void PopulateFrameState(ExplodedFrameState* frame_state) { |
| // Invent some data for the various fields. |
| frame_state->url_string = NS16("http://dev.chromium.org/"); |
| frame_state->original_url_string = frame_state->url_string; |
| frame_state->referrer = NS16("https://www.google.com/search?q=dev.chromium.org"); |
| frame_state->target = NS16("foo"); |
| frame_state->parent = NS16("bar"); |
| frame_state->title = NS16("The Chromium Projects"); |
| frame_state->alternate_title = NS16(NULL); |
| frame_state->state_object = NS16(NULL); |
| frame_state->document_state.push_back(NS16("1")); |
| frame_state->document_state.push_back(NS16("q")); |
| frame_state->document_state.push_back(NS16("text")); |
| frame_state->document_state.push_back(NS16("dev.chromium.org")); |
| frame_state->scroll_offset = gfx::Point(0, 100); |
| frame_state->item_sequence_number = 1; |
| frame_state->document_sequence_number = 2; |
| frame_state->visit_count = 10; |
| frame_state->visited_time = 12345.0; |
| frame_state->page_scale_factor = 2.0; |
| frame_state->is_target_item = true; |
| } |
| |
| void PopulateHttpBody(ExplodedHttpBody* http_body, |
| std::vector<base::NullableString16>* referenced_files) { |
| http_body->is_null = false; |
| http_body->identifier = 12345; |
| http_body->contains_passwords = false; |
| http_body->http_content_type = NS16("text/foo"); |
| |
| ExplodedHttpBodyElement e1; |
| e1.type = WebKit::WebHTTPBody::Element::TypeData; |
| e1.data = "foo"; |
| http_body->elements.push_back(e1); |
| |
| ExplodedHttpBodyElement e2; |
| e2.type = WebKit::WebHTTPBody::Element::TypeFile; |
| e2.file_path = NS16("file.txt"); |
| e2.file_start = 100; |
| e2.file_length = 1024; |
| e2.file_modification_time = 9999.0; |
| http_body->elements.push_back(e2); |
| |
| referenced_files->push_back(e2.file_path); |
| } |
| |
| void PopulateFrameStateForBackwardsCompatTest( |
| ExplodedFrameState* frame_state, |
| bool is_child) { |
| frame_state->url_string = NS16("http://chromium.org/"); |
| frame_state->original_url_string = frame_state->url_string; |
| frame_state->referrer = NS16("http://google.com/"); |
| if (!is_child) |
| frame_state->target = NS16("target"); |
| frame_state->parent = NS16("parent"); |
| frame_state->title = NS16("title"); |
| frame_state->alternate_title = NS16("alternateTitle"); |
| frame_state->scroll_offset = gfx::Point(42, -42); |
| frame_state->item_sequence_number = 123; |
| frame_state->document_sequence_number = 456; |
| frame_state->visit_count = 42*42; |
| frame_state->visited_time = 13.37; |
| frame_state->page_scale_factor = 2.0f; |
| frame_state->is_target_item = true; |
| |
| frame_state->document_state.push_back( |
| NS16("\n\r?% WebKit serialized form state version 8 \n\r=&")); |
| frame_state->document_state.push_back(NS16("form key")); |
| frame_state->document_state.push_back(NS16("1")); |
| frame_state->document_state.push_back(NS16("foo")); |
| frame_state->document_state.push_back(NS16("file")); |
| frame_state->document_state.push_back(NS16("2")); |
| frame_state->document_state.push_back(NS16("file.txt")); |
| frame_state->document_state.push_back(NS16("displayName")); |
| |
| if (!is_child) { |
| frame_state->http_body.http_content_type = NS16("foo/bar"); |
| frame_state->http_body.identifier = 789; |
| frame_state->http_body.is_null = false; |
| |
| ExplodedHttpBodyElement e1; |
| e1.type = WebKit::WebHTTPBody::Element::TypeData; |
| e1.data = "first data block"; |
| frame_state->http_body.elements.push_back(e1); |
| |
| ExplodedHttpBodyElement e2; |
| e2.type = WebKit::WebHTTPBody::Element::TypeFile; |
| e2.file_path = NS16("file.txt"); |
| frame_state->http_body.elements.push_back(e2); |
| |
| ExplodedHttpBodyElement e3; |
| e3.type = WebKit::WebHTTPBody::Element::TypeData; |
| e3.data = "data the second"; |
| frame_state->http_body.elements.push_back(e3); |
| |
| ExplodedFrameState child_state; |
| PopulateFrameStateForBackwardsCompatTest(&child_state, true); |
| frame_state->children.push_back(child_state); |
| } |
| } |
| |
| void PopulatePageStateForBackwardsCompatTest(ExplodedPageState* page_state) { |
| page_state->referenced_files.push_back(NS16("file.txt")); |
| PopulateFrameStateForBackwardsCompatTest(&page_state->top, false); |
| } |
| |
| void TestBackwardsCompat(int version) { |
| const char* suffix = ""; |
| |
| #if defined(OS_ANDROID) |
| // Unfortunately, the format of version 11 is different on Android, so we |
| // need to use a special reference file. |
| if (version == 11) |
| suffix = "_android"; |
| #endif |
| |
| base::FilePath path; |
| PathService::Get(content::DIR_TEST_DATA, &path); |
| path = path.AppendASCII("page_state").AppendASCII( |
| base::StringPrintf("serialized_v%d%s.dat", version, suffix)); |
| |
| std::string file_contents; |
| if (!file_util::ReadFileToString(path, &file_contents)) { |
| ADD_FAILURE() << "File not found: " << path.value(); |
| return; |
| } |
| |
| std::string trimmed_contents; |
| EXPECT_TRUE(RemoveChars(file_contents, "\r\n", &trimmed_contents)); |
| |
| std::string encoded; |
| EXPECT_TRUE(base::Base64Decode(trimmed_contents, &encoded)); |
| |
| ExplodedPageState output; |
| #if defined(OS_ANDROID) |
| // Because version 11 of the file format unfortunately bakes in the device |
| // scale factor on Android, perform this test by assuming a preset device |
| // scale factor, ignoring the device scale factor of the current device. |
| const float kPresetDeviceScaleFactor = 2.0f; |
| EXPECT_TRUE(DecodePageStateWithDeviceScaleFactorForTesting( |
| encoded, |
| kPresetDeviceScaleFactor, |
| &output)); |
| #else |
| EXPECT_TRUE(DecodePageState(encoded, &output)); |
| #endif |
| |
| ExplodedPageState expected; |
| PopulatePageStateForBackwardsCompatTest(&expected); |
| |
| ExpectEquality(expected, output); |
| } |
| }; |
| |
| TEST_F(PageStateSerializationTest, BasicEmpty) { |
| ExplodedPageState input; |
| |
| std::string encoded; |
| EXPECT_TRUE(EncodePageState(input, &encoded)); |
| |
| ExplodedPageState output; |
| EXPECT_TRUE(DecodePageState(encoded, &output)); |
| |
| ExpectEquality(input, output); |
| } |
| |
| TEST_F(PageStateSerializationTest, BasicFrame) { |
| ExplodedPageState input; |
| PopulateFrameState(&input.top); |
| |
| std::string encoded; |
| EXPECT_TRUE(EncodePageState(input, &encoded)); |
| |
| ExplodedPageState output; |
| EXPECT_TRUE(DecodePageState(encoded, &output)); |
| |
| ExpectEquality(input, output); |
| } |
| |
| TEST_F(PageStateSerializationTest, BasicFramePOST) { |
| ExplodedPageState input; |
| PopulateFrameState(&input.top); |
| PopulateHttpBody(&input.top.http_body, &input.referenced_files); |
| |
| std::string encoded; |
| EXPECT_TRUE(EncodePageState(input, &encoded)); |
| |
| ExplodedPageState output; |
| EXPECT_TRUE(DecodePageState(encoded, &output)); |
| |
| ExpectEquality(input, output); |
| } |
| |
| TEST_F(PageStateSerializationTest, BasicFrameSet) { |
| ExplodedPageState input; |
| PopulateFrameState(&input.top); |
| |
| // Add some child frames. |
| for (int i = 0; i < 4; ++i) { |
| ExplodedFrameState child_state; |
| PopulateFrameState(&child_state); |
| input.top.children.push_back(child_state); |
| } |
| |
| std::string encoded; |
| EXPECT_TRUE(EncodePageState(input, &encoded)); |
| |
| ExplodedPageState output; |
| EXPECT_TRUE(DecodePageState(encoded, &output)); |
| |
| ExpectEquality(input, output); |
| } |
| |
| TEST_F(PageStateSerializationTest, BasicFrameSetPOST) { |
| ExplodedPageState input; |
| PopulateFrameState(&input.top); |
| |
| // Add some child frames. |
| for (int i = 0; i < 4; ++i) { |
| ExplodedFrameState child_state; |
| PopulateFrameState(&child_state); |
| |
| // Simulate a form POST on a subframe. |
| if (i == 2) |
| PopulateHttpBody(&child_state.http_body, &input.referenced_files); |
| |
| input.top.children.push_back(child_state); |
| } |
| |
| std::string encoded; |
| EncodePageState(input, &encoded); |
| |
| ExplodedPageState output; |
| DecodePageState(encoded, &output); |
| |
| ExpectEquality(input, output); |
| } |
| |
| TEST_F(PageStateSerializationTest, BadMessagesTest1) { |
| Pickle p; |
| // Version 14 |
| p.WriteInt(14); |
| // Empty strings. |
| for (int i = 0; i < 6; ++i) |
| p.WriteInt(-1); |
| // Bad real number. |
| p.WriteInt(-1); |
| |
| std::string s(static_cast<const char*>(p.data()), p.size()); |
| |
| ExplodedPageState output; |
| EXPECT_FALSE(DecodePageState(s, &output)); |
| } |
| |
| TEST_F(PageStateSerializationTest, BadMessagesTest2) { |
| double d = 0; |
| Pickle p; |
| // Version 14 |
| p.WriteInt(14); |
| // Empty strings. |
| for (int i = 0; i < 6; ++i) |
| p.WriteInt(-1); |
| // More misc fields. |
| p.WriteData(reinterpret_cast<const char*>(&d), sizeof(d)); |
| p.WriteInt(1); |
| p.WriteInt(1); |
| p.WriteInt(0); |
| p.WriteInt(0); |
| p.WriteInt(-1); |
| p.WriteInt(0); |
| // WebForm |
| p.WriteInt(1); |
| p.WriteInt(WebKit::WebHTTPBody::Element::TypeData); |
| |
| std::string s(static_cast<const char*>(p.data()), p.size()); |
| |
| ExplodedPageState output; |
| EXPECT_FALSE(DecodePageState(s, &output)); |
| } |
| |
| TEST_F(PageStateSerializationTest, DumpExpectedPageStateForBackwardsCompat) { |
| // Comment out this return statement to enable this code. Use this code to |
| // generate data, based on the current serialization format, for the |
| // BackwardsCompat_vXX tests. |
| return; |
| |
| ExplodedPageState state; |
| PopulatePageStateForBackwardsCompatTest(&state); |
| |
| std::string encoded; |
| EXPECT_TRUE(EncodePageState(state, &encoded)); |
| |
| std::string base64; |
| EXPECT_TRUE(base::Base64Encode(encoded, &base64)); |
| |
| base::FilePath path; |
| PathService::Get(base::DIR_TEMP, &path); |
| path = path.AppendASCII("expected.dat"); |
| |
| FILE* fp = file_util::OpenFile(path, "wb"); |
| ASSERT_TRUE(fp); |
| |
| const size_t kRowSize = 76; |
| for (size_t offset = 0; offset < base64.size(); offset += kRowSize) { |
| size_t length = std::min(base64.size() - offset, kRowSize); |
| std::string segment(&base64[offset], length); |
| segment.push_back('\n'); |
| fwrite(segment.data(), segment.size(), 1, fp); |
| } |
| |
| fclose(fp); |
| } |
| |
| #if !defined(OS_ANDROID) |
| // TODO(darin): Re-enable for Android once this test accounts for systems with |
| // a device scale factor not equal to 2. |
| TEST_F(PageStateSerializationTest, BackwardsCompat_v11) { |
| TestBackwardsCompat(11); |
| } |
| #endif |
| |
| TEST_F(PageStateSerializationTest, BackwardsCompat_v12) { |
| TestBackwardsCompat(12); |
| } |
| |
| TEST_F(PageStateSerializationTest, BackwardsCompat_v13) { |
| TestBackwardsCompat(13); |
| } |
| |
| TEST_F(PageStateSerializationTest, BackwardsCompat_v14) { |
| TestBackwardsCompat(14); |
| } |
| |
| } // namespace |
| } // namespace content |