| // 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 |