blob: fe119d4a728aea7c9fe4d97b81e0ba100c1cb12a [file] [log] [blame]
// 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 "weblayer/browser/persistence/minimal_browser_persister.h"
#include "base/containers/contains.h"
#include "base/containers/cxx20_erase.h"
#include "components/sessions/content/content_serialized_navigation_builder.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/sessions/core/session_command.h"
#include "components/sessions/core/session_constants.h"
#include "components/sessions/core/session_id.h"
#include "components/sessions/core/session_service_commands.h"
#include "components/sessions/core/session_types.h"
#include "weblayer/browser/browser_impl.h"
#include "weblayer/browser/persistence/browser_persistence_common.h"
#include "weblayer/browser/tab_impl.h"
using id_type = sessions::SessionCommand::id_type;
using size_type = sessions::SessionCommand::size_type;
using SessionCommands = std::vector<std::unique_ptr<sessions::SessionCommand>>;
namespace weblayer {
namespace {
// Max size used for saving state. Android caps the max at 1MB (although it can
// vary between version and phone). Android does not offer a define for this and
// further if the max is exceeded an exception is thrown. To be on the safe side
// this uses 512k.
constexpr int kMaxSizeInBytes = 512 * 1024;
// Size of the header, in bytes. This is used for versioning.
constexpr int kHeaderSizeInBytes = 4;
// Value used for the header.
constexpr int kHeaderValue = 1;
// This accumulates the SessionCommands needed to restore a Browser and
// ultimately generates a byte array from those commands.
class MinimalPersister {
public:
explicit MinimalPersister(int max_size_in_bytes)
: max_size_in_bytes_(max_size_in_bytes) {}
MinimalPersister(const MinimalPersister&) = delete;
MinimalPersister& operator=(const MinimalPersister&) = delete;
~MinimalPersister() = default;
// Convenience for adding a single command.
bool AppendIfFits(std::unique_ptr<sessions::SessionCommand> command) {
std::vector<std::unique_ptr<sessions::SessionCommand>> commands;
commands.push_back(std::move(command));
return AppendIfFits(std::move(commands));
}
// Returns true if all |commands| were successfully added. A return value of
// false indicates the max size has been reached and no more commands will be
// accepted.
bool AppendIfFits(SessionCommands commands) WARN_UNUSED_RESULT {
// The number of commands is written out as |size_type|, make sure the
// count isn't exceeded.
const int commands_size = CalculateSizeForCommands(commands);
if (current_size_ + commands_size > max_size_in_bytes_ ||
(commands.size() + commands_.size()) >
std::numeric_limits<size_type>::max()) {
return false;
}
current_size_ += commands_size;
commands_.insert(commands_.end(), std::make_move_iterator(commands.begin()),
std::make_move_iterator(commands.end()));
return true;
}
// Converts the commands to a byte array.
std::vector<uint8_t> ToByteArray() const {
std::vector<uint8_t> result(current_size_);
uint8_t* result_ptr = &result.front();
const uint32_t header = kHeaderValue;
memcpy(result_ptr, &header, kHeaderSizeInBytes);
result_ptr += kHeaderSizeInBytes;
// Number of commands.
const size_type num_commands = commands_.size();
memcpy(result_ptr, &num_commands, sizeof(size_type));
result_ptr += sizeof(size_type);
// And the commands.
for (auto& command : commands_) {
const size_type total_command_size = command->GetSerializedSize();
memcpy(result_ptr, &total_command_size, sizeof(size_type));
result_ptr += sizeof(size_type);
const id_type command_id = command->id();
memcpy(result_ptr, &command_id, sizeof(id_type));
result_ptr += sizeof(id_type);
const size_type command_size = total_command_size - sizeof(id_type);
if (command_size > 0) {
memcpy(result_ptr, command->contents(), command_size);
result_ptr += command_size;
}
}
DCHECK_EQ(result_ptr - &(result.front()), current_size_);
return result;
}
private:
int CalculateSizeForCommands(const SessionCommands& commands) const {
int commands_size = 0;
for (auto& command : commands)
commands_size += command->GetSerializedSize();
// Each command is preceded by it's size.
return commands_size + commands.size() * sizeof(size_type);
}
const int max_size_in_bytes_;
int current_size_ = kHeaderSizeInBytes + sizeof(size_type);
SessionCommands commands_;
};
// Used to restore the state created via MinimalPersister.
class MinimalRestorer {
public:
explicit MinimalRestorer(const std::vector<uint8_t>& value)
: value_ptr_(&value.front()), value_ptr_end_(value_ptr_ + value.size()) {}
MinimalRestorer(const MinimalRestorer&) = delete;
MinimalRestorer& operator=(const MinimalRestorer&) = delete;
~MinimalRestorer() = default;
// Creates SessionCommands from the previously generated state. An empty
// vector is returned if there is an error in decoding.
SessionCommands RestoreCommands() {
uint32_t header = 0;
if (!Extract(&header, kHeaderSizeInBytes) || header != kHeaderValue)
return {};
size_type num_commands = 0;
if (!Extract(&num_commands, sizeof(size_type)) || num_commands == 0)
return {};
SessionCommands commands;
for (int i = 0; i < num_commands; ++i) {
size_type command_size = 0;
if (!Extract(&command_size, sizeof(size_type)) ||
!HasAvailable(command_size)) {
return {};
}
id_type command_id = 0;
if (!Extract(&command_id, sizeof(id_type)))
return {};
command_size -= sizeof(id_type);
std::unique_ptr<sessions::SessionCommand> command =
std::make_unique<sessions::SessionCommand>(command_id, command_size);
if (command_size > 0 && !Extract(command->contents(), command_size))
return {};
commands.push_back(std::move(command));
}
return commands;
}
private:
// If there is |bytes| available to be read, it is copied to |dest| and true
// is returned.
bool Extract(void* dest, int bytes) {
if (!HasAvailable(bytes))
return false;
memcpy(dest, value_ptr_, bytes);
value_ptr_ += bytes;
return true;
}
// Returns true if there are |bytes| more bytes available to read.
bool HasAvailable(int bytes) const {
return value_ptr_ + bytes <= value_ptr_end_;
}
const uint8_t* value_ptr_;
const uint8_t* value_ptr_end_;
};
// Iterates over the NavigationEntries of a tab in the order they should be
// written.
class NavigationEntryIterator {
public:
explicit NavigationEntryIterator(Tab* tab)
: controller_(
static_cast<TabImpl*>(tab)->web_contents()->GetController()),
at_pending_(controller_.GetPendingEntry() != nullptr &&
controller_.GetPendingEntryIndex() != -1),
entry_index_(at_pending_ ? controller_.GetPendingEntryIndex()
: controller_.GetCurrentEntryIndex()) {
// GetPendingEntryIndex() returns -1 for new entries, which this implicitly
// skips (Chrome's persistence code does the same).
}
NavigationEntryIterator(const NavigationEntryIterator&) = delete;
NavigationEntryIterator& operator=(const NavigationEntryIterator&) = delete;
~NavigationEntryIterator() = default;
// Returns the index of the current entry.
int entry_index() const { return entry_index_; }
// Returns the current entry.
content::NavigationEntry* entry() {
if (at_pending_)
return controller_.GetPendingEntry();
return entry_index_ == -1 ? nullptr
: controller_.GetEntryAtIndex(entry_index_);
}
// Returns true if the end has been reached.
bool at_end() const { return !at_pending_ && entry_index_ == -1; }
// advances to the next entry, returning true if there is one.
bool Next() {
if (at_end())
return false;
if (at_pending_) {
at_pending_ = false;
entry_index_ = controller_.GetCurrentEntryIndex();
if (entry_index_ == controller_.GetPendingEntryIndex())
--entry_index_;
} else if (entry_index_ != -1) {
--entry_index_;
}
return !at_end();
}
private:
content::NavigationController& controller_;
bool at_pending_;
int entry_index_ = -1;
};
// The first pass persists the pending or current entry. Returns true if room
// for more commands, false if size exceeded.
bool PersistTabStatePrimaryPass(const SessionID& browser_session_id,
Tab* tab,
MinimalPersister* builder) {
NavigationEntryIterator iterator(tab);
if (iterator.at_end())
return true;
const SessionID& session_id = GetSessionIDForTab(tab);
BrowserImpl* browser = static_cast<TabImpl*>(tab)->browser();
auto tabs = browser->GetTabs();
DCHECK(base::Contains(tabs, tab));
const int tab_index =
static_cast<int>(std::find(tabs.begin(), tabs.end(), tab) - tabs.begin());
if (!builder->AppendIfFits(BuildCommandsForTabConfiguration(
browser_session_id, static_cast<TabImpl*>(tab), tab_index))) {
return false;
}
const sessions::SerializedNavigationEntry serialized_entry =
sessions::ContentSerializedNavigationBuilder::FromNavigationEntry(
iterator.entry_index(), iterator.entry());
return builder->AppendIfFits(
CreateUpdateTabNavigationCommand(session_id, serialized_entry));
}
// The second pass persists two more navigations. Returns true if room for more
// commands, false if size exceeded.
bool PersistTabStateSecondaryPass(const SessionID& browser_session_id,
Tab* tab,
MinimalPersister* builder) {
NavigationEntryIterator iterator(tab);
if (iterator.at_end())
return true;
const SessionID& session_id = GetSessionIDForTab(tab);
for (int i = 0; i < 2; ++i) {
// Skips the navigation that was written during the first pass.
if (!iterator.Next())
return true;
const sessions::SerializedNavigationEntry serialized_entry =
sessions::ContentSerializedNavigationBuilder::FromNavigationEntry(
iterator.entry_index(), iterator.entry());
if (!builder->AppendIfFits(
CreateUpdateTabNavigationCommand(session_id, serialized_entry))) {
return false;
}
}
return true;
}
// Returns the tabs in the order they should be persisted.
std::vector<Tab*> GetTabsInPersistOrder(BrowserImpl* browser) {
// Move the active tab to be first.
std::vector<Tab*> tabs = browser->GetTabs();
Tab* active_tab = browser->GetActiveTab();
if (tabs.size() <= 1 || !active_tab)
return tabs;
base::Erase(tabs, active_tab);
tabs.insert(tabs.begin(), active_tab);
return tabs;
}
// Returns the index of active tab.
int GetActiveTabIndex(BrowserImpl* browser) {
if (!browser->GetActiveTab())
return -1;
const std::vector<Tab*>& tabs = browser->GetTabs();
return static_cast<int>(
std::find(tabs.begin(), tabs.end(), browser->GetActiveTab()) -
tabs.begin());
}
} // namespace
std::vector<uint8_t> PersistMinimalState(BrowserImpl* browser,
int max_size_in_bytes) {
MinimalPersister builder(max_size_in_bytes == 0 ? kMaxSizeInBytes
: max_size_in_bytes);
const SessionID browser_session_id = SessionID::NewUnique();
if (!builder.AppendIfFits(sessions::CreateSetWindowTypeCommand(
browser_session_id,
sessions::SessionWindow::WindowType::TYPE_NORMAL))) {
return {};
}
const int active_tab_index = GetActiveTabIndex(browser);
if (active_tab_index != -1 &&
!builder.AppendIfFits(sessions::CreateSetSelectedTabInWindowCommand(
browser_session_id, active_tab_index))) {
return {};
}
// As the size available to write commands is limited, this generates commands
// in the following order:
// . active tabs pending navigation entry, if no pending then last committed.
// . remaining tabs pending navigation entry or last committed if no pending.
// . active tabs last committed and one navigation before it.
// . remaining tabs last committed and one navigation before it.
std::vector<Tab*> tabs = GetTabsInPersistOrder(browser);
for (Tab* tab : tabs) {
if (!PersistTabStatePrimaryPass(browser_session_id, tab, &builder))
return builder.ToByteArray();
}
for (Tab* tab : tabs) {
if (!PersistTabStateSecondaryPass(browser_session_id, tab, &builder))
return builder.ToByteArray();
}
return builder.ToByteArray();
}
void RestoreMinimalState(BrowserImpl* browser,
const std::vector<uint8_t>& value) {
MinimalRestorer restorer(value);
RestoreBrowserState(browser, restorer.RestoreCommands());
}
} // namespace weblayer