/* * Copyright (c) 2026-present, the Ladybird developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include namespace WebView { struct SessionHistoryMergeAnchor { size_t local_index { 0 }; size_t incoming_index { 0 }; }; enum class StepTranslationMode { TopLevel, Nested, }; struct DocumentStateIdTranslationState { HashMap translated_document_state_ids; u64 next_document_state_id { 1 }; }; struct DocumentStateIdCanonicalizationState { HashMap canonical_document_state_ids; u64 next_document_state_id { 1 }; }; static bool steps_are_valid(Vector const& steps) { Optional previous_step; for (auto const& step : steps) { if (step < 0) return false; if (previous_step.has_value() && step <= *previous_step) return false; previous_step = step; } return true; } static bool entries_are_valid(Vector const& entries) { Optional previous_step; for (auto const& entry : entries) { if (entry.step < 0) return false; if (previous_step.has_value() && entry.step <= *previous_step) return false; for (auto const& nested_history : entry.document_state.nested_histories) { if (!entries_are_valid(nested_history.entries)) return false; } previous_step = entry.step; } return true; } static bool nested_histories_match(Vector const& a, Vector const& b); static bool serialized_directives_match(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (a[i].name != b[i].name || a[i].value != b[i].value) return false; } return true; } static bool serialized_policies_match(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (!serialized_directives_match(a[i].directives, b[i].directives) || a[i].disposition != b[i].disposition || a[i].source != b[i].source || a[i].self_origin != b[i].self_origin || a[i].pre_parsed_policy_string != b[i].pre_parsed_policy_string) return false; } return true; } static bool embedder_policies_match(Web::HTML::EmbedderPolicy const& a, Web::HTML::EmbedderPolicy const& b) { return a.value == b.value && a.report_only_value == b.report_only_value && a.reporting_endpoint == b.reporting_endpoint && a.report_only_reporting_endpoint == b.report_only_reporting_endpoint; } static bool serialized_policy_containers_match(Web::HTML::SerializedPolicyContainer const& a, Web::HTML::SerializedPolicyContainer const& b) { return serialized_policies_match(a.csp_list, b.csp_list) && embedder_policies_match(a.embedder_policy, b.embedder_policy) && a.referrer_policy == b.referrer_policy; } static bool history_policy_containers_match(Variant const& a, Variant const& b) { if (auto const* a_serialized_policy_container = a.get_pointer()) { auto const* b_serialized_policy_container = b.get_pointer(); return b_serialized_policy_container && serialized_policy_containers_match(*a_serialized_policy_container, *b_serialized_policy_container); } return a.has() && b.has(); } static bool document_state_descriptors_match(Web::HTML::SessionHistoryDocumentStateDescriptor const& a, Web::HTML::SessionHistoryDocumentStateDescriptor const& b) { return a.id == b.id && history_policy_containers_match(a.history_policy_container, b.history_policy_container) && a.request_referrer == b.request_referrer && a.request_referrer_policy == b.request_referrer_policy && a.initiator_origin == b.initiator_origin && a.origin == b.origin && a.about_base_url == b.about_base_url && a.resource == b.resource && a.reload_pending == b.reload_pending && a.ever_populated == b.ever_populated && a.navigable_target_name == b.navigable_target_name && nested_histories_match(a.nested_histories, b.nested_histories); } static bool nested_histories_match_ignoring_document_state_ids(Vector const&, Vector const&); static bool document_state_descriptors_match_ignoring_id(Web::HTML::SessionHistoryDocumentStateDescriptor const& a, Web::HTML::SessionHistoryDocumentStateDescriptor const& b) { return history_policy_containers_match(a.history_policy_container, b.history_policy_container) && a.request_referrer == b.request_referrer && a.request_referrer_policy == b.request_referrer_policy && a.initiator_origin == b.initiator_origin && a.origin == b.origin && a.about_base_url == b.about_base_url && a.resource == b.resource && a.reload_pending == b.reload_pending && a.ever_populated == b.ever_populated && a.navigable_target_name == b.navigable_target_name && nested_histories_match_ignoring_document_state_ids(a.nested_histories, b.nested_histories); } static bool entry_descriptors_match(TraversableSessionHistory::Entry const& a, TraversableSessionHistory::Entry const& b) { return a.step == b.step && a.url == b.url && document_state_descriptors_match(a.document_state, b.document_state) && a.classic_history_api_state == b.classic_history_api_state && a.navigation_api_state == b.navigation_api_state && a.navigation_api_key == b.navigation_api_key && a.navigation_api_id == b.navigation_api_id && a.scroll_restoration_mode == b.scroll_restoration_mode && a.scroll_position_data == b.scroll_position_data; } static bool entry_descriptors_match_ignoring_document_state_id(TraversableSessionHistory::Entry const& a, TraversableSessionHistory::Entry const& b) { if (a.step != b.step) return false; if (a.url != b.url) return false; if (a.document_state.id != 0 && !document_state_descriptors_match_ignoring_id(a.document_state, b.document_state)) return false; if (a.classic_history_api_state != b.classic_history_api_state) return false; if (a.navigation_api_state != b.navigation_api_state || a.navigation_api_key != b.navigation_api_key || a.navigation_api_id != b.navigation_api_id) return false; if (a.scroll_restoration_mode != b.scroll_restoration_mode) return false; if (a.document_state.id != 0 && a.scroll_position_data != b.scroll_position_data) return false; return true; } static bool entries_match(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (entry_descriptors_match(a[i], b[i])) continue; return false; } return true; } static bool entries_match_ignoring_document_state_ids(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (entry_descriptors_match_ignoring_document_state_id(a[i], b[i])) continue; return false; } return true; } static bool nested_histories_match(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (a[i].id == b[i].id && entries_match(a[i].entries, b[i].entries)) continue; return false; } return true; } static bool nested_histories_match_ignoring_document_state_ids(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (a[i].id == b[i].id && entries_match_ignoring_document_state_ids(a[i].entries, b[i].entries)) continue; return false; } return true; } static bool seed_ack_nested_histories_match(Vector const&, Vector const&, Optional); static bool current_unknown_entry_seed_ack_matches(TraversableSessionHistory::Entry const& a, TraversableSessionHistory::Entry const& b) { if (a.step != b.step || a.url != b.url) return false; if (!a.classic_history_api_state.is_empty() && a.classic_history_api_state != b.classic_history_api_state) return false; if (!a.navigation_api_state.is_empty() && a.navigation_api_state != b.navigation_api_state) return false; if (!a.navigation_api_key.is_empty() && a.navigation_api_key != b.navigation_api_key) return false; if (!a.navigation_api_id.is_empty() && a.navigation_api_id != b.navigation_api_id) return false; if (a.scroll_restoration_mode != b.scroll_restoration_mode) return false; if (a.scroll_position_data.viewport_scroll_position.has_value() && a.scroll_position_data != b.scroll_position_data) return false; return seed_ack_nested_histories_match(a.document_state.nested_histories, b.document_state.nested_histories, {}); } static bool seed_ack_entries_match(Vector const& a, Vector const& b, Optional current_unknown_entry_index) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (current_unknown_entry_index.has_value() && i == *current_unknown_entry_index && current_unknown_entry_seed_ack_matches(a[i], b[i])) continue; if (entry_descriptors_match_ignoring_document_state_id(a[i], b[i])) continue; return false; } return true; } static bool seed_ack_nested_histories_match(Vector const& a, Vector const& b, Optional current_unknown_entry_index) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (a[i].id == b[i].id && seed_ack_entries_match(a[i].entries, b[i].entries, current_unknown_entry_index)) continue; return false; } return true; } static bool steps_match(Vector const& a, Vector const& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { if (a[i] == b[i]) continue; return false; } return true; } static bool entry_steps_match(TraversableSessionHistory::Entry const& a, TraversableSessionHistory::Entry const& b) { if (a.step != b.step) return false; if (a.document_state.nested_histories.size() != b.document_state.nested_histories.size()) return false; for (size_t i = 0; i < a.document_state.nested_histories.size(); ++i) { auto const& a_nested_history = a.document_state.nested_histories[i]; auto const& b_nested_history = b.document_state.nested_histories[i]; if (a_nested_history.id != b_nested_history.id || a_nested_history.entries.size() != b_nested_history.entries.size()) return false; for (size_t j = 0; j < a_nested_history.entries.size(); ++j) { if (!entry_steps_match(a_nested_history.entries[j], b_nested_history.entries[j])) return false; } } return true; } static bool entries_have_matching_nonzero_document_state_id(TraversableSessionHistory::Entry const& a, TraversableSessionHistory::Entry const& b) { return a.document_state.id != 0 && a.document_state.id == b.document_state.id; } // https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps static Vector get_all_used_history_steps(Vector const& traversable_session_history_entries) { // 1. Assert: this is running within traversable's session history traversal queue. // 2. Let steps be an empty ordered set of non-negative integers. OrderedHashTable steps; // 3. Let entryLists be the ordered set « traversable's session history entries ». Vector const*> entry_lists { &traversable_session_history_entries }; // 4. For each entryList of entryLists: while (!entry_lists.is_empty()) { auto const* entry_list = entry_lists.take_first(); // 1. For each entry of entryList: for (auto const& entry : *entry_list) { // 1. Append entry's step to steps. steps.set(entry.step); // 2. For each nestedHistory of entry's document state's nested histories, append // nestedHistory's entries list to entryLists. for (auto const& nested_history : entry.document_state.nested_histories) entry_lists.append(&nested_history.entries); } } // 5. Return steps, sorted. auto sorted_steps = steps.values(); quick_sort(sorted_steps); return sorted_steps; } static bool entries_and_used_steps_are_consistent(Vector const& entries, Vector const& used_steps) { return steps_match(get_all_used_history_steps(entries), used_steps); } static bool entries_have_nested_histories(Vector const& entries) { for (auto const& entry : entries) { if (!entry.document_state.nested_histories.is_empty()) return true; } return false; } static void recompute_used_steps(Vector const& entries, Vector& used_steps, Optional& current_used_step_index, i32 current_step) { used_steps = get_all_used_history_steps(entries); current_used_step_index = used_steps.find_first_index(current_step); } static Optional top_level_entry_index_for_step(Vector const& entries, i32 step) { Optional result; for (size_t i = 0; i < entries.size(); ++i) { if (entries[i].step > step) break; result = i; } return result; } static TraversableSessionHistory::Entry const* top_level_entry_for_step(Vector const& entries, i32 step) { auto index = top_level_entry_index_for_step(entries, step); if (!index.has_value()) return nullptr; return &entries[*index]; } static TraversableSessionHistory::Entry const* entry_for_step_in_entry_list(Vector const& entries, i32 step) { TraversableSessionHistory::Entry const* result = nullptr; for (auto const& entry : entries) { if (entry.step > step) break; result = &entry; } return result; } static bool nested_histories_need_restore_after_loading_entry(TraversableSessionHistory::Entry const& entry, i32 step) { for (auto const& nested_history : entry.document_state.nested_histories) { auto target_entry = entry_for_step_in_entry_list(nested_history.entries, step); if (!target_entry) continue; if (target_entry->step != entry.step) return true; if (nested_histories_need_restore_after_loading_entry(*target_entry, step)) return true; } return false; } static void update_next_document_state_id(Vector const& entries, u64& next_document_state_id) { for (auto const& entry : entries) { if (entry.document_state.id >= next_document_state_id) next_document_state_id = entry.document_state.id + 1; for (auto const& nested_history : entry.document_state.nested_histories) update_next_document_state_id(nested_history.entries, next_document_state_id); } } static u64 translate_incoming_document_state_id(u64 incoming_document_state_id, DocumentStateIdTranslationState& translation_state) { if (incoming_document_state_id == 0) return 0; if (auto translated_document_state_id = translation_state.translated_document_state_ids.get(incoming_document_state_id); translated_document_state_id.has_value()) return *translated_document_state_id; auto translated_document_state_id = translation_state.next_document_state_id++; VERIFY(translated_document_state_id != 0); translation_state.translated_document_state_ids.set(incoming_document_state_id, translated_document_state_id); return translated_document_state_id; } static void canonicalize_document_state_ids(Vector& entries, DocumentStateIdCanonicalizationState& canonicalization_state) { for (auto& entry : entries) { if (entry.document_state.id != 0) { auto canonical_document_state_id = canonicalization_state.canonical_document_state_ids.get(entry.document_state.id); if (canonical_document_state_id.has_value()) { entry.document_state.id = *canonical_document_state_id; } else { auto new_document_state_id = canonicalization_state.next_document_state_id++; VERIFY(new_document_state_id != 0); canonicalization_state.canonical_document_state_ids.set(entry.document_state.id, new_document_state_id); entry.document_state.id = new_document_state_id; } } for (auto& nested_history : entry.document_state.nested_histories) canonicalize_document_state_ids(nested_history.entries, canonicalization_state); } } static void canonicalize_document_state_ids(Vector& entries) { DocumentStateIdCanonicalizationState canonicalization_state; canonicalize_document_state_ids(entries, canonicalization_state); } static Optional translate_incoming_step(i32 incoming_step, i32 incoming_anchor_step, i32 local_anchor_step, StepTranslationMode mode = StepTranslationMode::TopLevel, Optional nested_history_step_floor = {}) { // AD-HOC: WebContent snapshots can describe only the history slice known to the current process. When merging // such a partial snapshot into the UI-owned traversable session history mirror, translate the incoming // step coordinates around the entry that anchors both histories. if (mode == StepTranslationMode::Nested && incoming_step < incoming_anchor_step && (!nested_history_step_floor.has_value() || incoming_step < *nested_history_step_floor)) return local_anchor_step; auto step_delta = static_cast(incoming_step) - static_cast(incoming_anchor_step); if (step_delta < 0) { if (mode != StepTranslationMode::Nested || !nested_history_step_floor.has_value() || incoming_step < *nested_history_step_floor) return {}; if (-step_delta > local_anchor_step) return {}; } if (step_delta > NumericLimits::max() - local_anchor_step) return {}; return local_anchor_step + static_cast(step_delta); } static Optional translate_incoming_nested_history(Web::HTML::SessionHistoryNestedHistoryDescriptor const&, i32 incoming_anchor_step, i32 local_anchor_step, DocumentStateIdTranslationState&, Optional nested_history_step_floor); static Optional translate_incoming_document_state_descriptor(Web::HTML::SessionHistoryDocumentStateDescriptor document_state, i32 incoming_anchor_step, i32 local_anchor_step, DocumentStateIdTranslationState& document_state_id_translation_state, Optional nested_history_step_floor) { document_state.id = translate_incoming_document_state_id(document_state.id, document_state_id_translation_state); Vector nested_histories; nested_histories.ensure_capacity(document_state.nested_histories.size()); for (auto const& nested_history : document_state.nested_histories) { auto translated_nested_history = translate_incoming_nested_history(nested_history, incoming_anchor_step, local_anchor_step, document_state_id_translation_state, nested_history_step_floor); if (!translated_nested_history.has_value()) return {}; nested_histories.unchecked_append(translated_nested_history.release_value()); } document_state.nested_histories = move(nested_histories); return document_state; } static Optional translate_incoming_entry(TraversableSessionHistory::Entry const& entry, i32 incoming_anchor_step, i32 local_anchor_step, DocumentStateIdTranslationState& document_state_id_translation_state, StepTranslationMode mode = StepTranslationMode::TopLevel, Optional nested_history_step_floor = {}) { auto translated_step = translate_incoming_step(entry.step, incoming_anchor_step, local_anchor_step, mode, nested_history_step_floor); if (!translated_step.has_value()) return {}; auto translated_document_state = translate_incoming_document_state_descriptor(entry.document_state, incoming_anchor_step, local_anchor_step, document_state_id_translation_state, nested_history_step_floor); if (!translated_document_state.has_value()) return {}; return TraversableSessionHistory::Entry { .step = *translated_step, .url = entry.url, .document_state = translated_document_state.release_value(), .classic_history_api_state = entry.classic_history_api_state, .navigation_api_state = entry.navigation_api_state, .navigation_api_key = entry.navigation_api_key, .navigation_api_id = entry.navigation_api_id, .scroll_restoration_mode = entry.scroll_restoration_mode, .scroll_position_data = entry.scroll_position_data, }; } static Optional translate_incoming_nested_history(Web::HTML::SessionHistoryNestedHistoryDescriptor const& nested_history, i32 incoming_anchor_step, i32 local_anchor_step, DocumentStateIdTranslationState& document_state_id_translation_state, Optional nested_history_step_floor) { Vector entries; entries.ensure_capacity(nested_history.entries.size()); Optional previous_step; for (auto const& entry : nested_history.entries) { auto translated_entry = translate_incoming_entry(entry, incoming_anchor_step, local_anchor_step, document_state_id_translation_state, StepTranslationMode::Nested, nested_history_step_floor); if (!translated_entry.has_value()) return {}; if (previous_step.has_value() && translated_entry->step <= *previous_step) return {}; previous_step = translated_entry->step; entries.unchecked_append(translated_entry.release_value()); } return Web::HTML::SessionHistoryNestedHistoryDescriptor { .id = nested_history.id, .entries = move(entries), }; } static void clear_forward_session_history_entries(Vector& entries, i32 step) { // https://html.spec.whatwg.org/multipage/browsing-the-web.html#clear-the-forward-session-history // 1. Assert: this is running within navigable's session history traversal queue. // 2. Let step be the navigable's current session history step. // 3. Let entryLists be the ordered set « navigable's session history entries ». Vector*> entry_lists { &entries }; // 4. For each entryList of entryLists: while (!entry_lists.is_empty()) { auto* entry_list = entry_lists.take_first(); // 1. Remove every session history entry from entryList that has a step greater than step. entry_list->remove_all_matching([step](auto const& entry) { return entry.step > step; }); // 2. For each entry of entryList: for (auto& entry : *entry_list) { // 1. For each nestedHistory of entry's document state's nested histories, append // nestedHistory's entries list to entryLists. for (auto& nested_history : entry.document_state.nested_histories) entry_lists.append(&nested_history.entries); } } } static TraversableSessionHistory::Entry create_ui_process_session_history_entry( i32 step, URL::URL url, Variant document_resource) { return { .step = step, .url = move(url), .document_state = { .id = 0, .history_policy_container = Web::HTML::DocumentState::Client::Tag, .request_referrer = Web::Fetch::Infrastructure::Request::Referrer::Client, .request_referrer_policy = Web::ReferrerPolicy::DEFAULT_REFERRER_POLICY, .initiator_origin = {}, .origin = {}, .about_base_url = {}, .resource = move(document_resource), .reload_pending = false, .ever_populated = false, .navigable_target_name = {}, .nested_histories = {}, }, .classic_history_api_state = {}, .navigation_api_state = {}, .navigation_api_key = {}, .navigation_api_id = {}, .scroll_restoration_mode = Web::HTML::ScrollRestorationMode::Auto, .scroll_position_data = {}, }; } void TraversableSessionHistory::navigate(URL::URL url) { navigate(move(url), Empty {}); } void TraversableSessionHistory::navigate(URL::URL url, Variant document_resource) { forget_web_content_state(); if (!m_current_used_step_index.has_value()) { m_entries.clear(); m_used_steps.clear(); m_entries.append(create_ui_process_session_history_entry(0, move(url), move(document_resource))); m_used_steps.append(0); m_current_used_step_index = 0; return; } auto current_step = m_used_steps[*m_current_used_step_index]; VERIFY(current_step < NumericLimits::max()); clear_forward_session_history_entries(m_entries, current_step); auto step = current_step + 1; m_used_steps.remove_all_matching([current_step](auto const& used_step) { return used_step > current_step; }); m_entries.append(create_ui_process_session_history_entry(step, move(url), move(document_resource))); m_used_steps.append(step); m_current_used_step_index = m_used_steps.size() - 1; } void TraversableSessionHistory::clear() { m_entries.clear(); m_used_steps.clear(); m_current_used_step_index.clear(); forget_web_content_state(); } void TraversableSessionHistory::replace_current_entry_url(URL::URL url) { forget_web_content_state(); if (!m_current_used_step_index.has_value()) { navigate(move(url)); return; } auto current_top_level_entry_index = this->current_top_level_entry_index(); VERIFY(current_top_level_entry_index.has_value()); m_entries[*current_top_level_entry_index].url = move(url); } void TraversableSessionHistory::replace_current_entry(URL::URL url, Variant document_resource) { forget_web_content_state(); if (!m_current_used_step_index.has_value()) { navigate(move(url), move(document_resource)); return; } auto current_top_level_entry_index = this->current_top_level_entry_index(); VERIFY(current_top_level_entry_index.has_value()); auto current_step = m_used_steps[*m_current_used_step_index]; m_entries[*current_top_level_entry_index] = create_ui_process_session_history_entry( current_step, move(url), move(document_resource)); recompute_used_steps(m_entries, m_used_steps, m_current_used_step_index, current_step); VERIFY(m_current_used_step_index.has_value()); } void TraversableSessionHistory::mark_current_entry_reload_pending() { forget_web_content_state(); auto current_top_level_entry_index = this->current_top_level_entry_index(); if (!current_top_level_entry_index.has_value()) return; // https://html.spec.whatwg.org/multipage/browsing-the-web.html#reload // Set navigable's active session history entry's document state's reload // pending to true. m_entries[*current_top_level_entry_index].document_state.reload_pending = true; } void TraversableSessionHistory::clear_current_entry_reload_pending() { auto current_top_level_entry_index = this->current_top_level_entry_index(); if (!current_top_level_entry_index.has_value()) return; m_entries[*current_top_level_entry_index].document_state.reload_pending = false; } Optional TraversableSessionHistory::current_top_level_entry_index() const { if (!m_current_used_step_index.has_value()) return {}; return top_level_entry_index_for_step(m_entries, m_used_steps[*m_current_used_step_index]); } TraversableSessionHistory::UpdateResult TraversableSessionHistory::update_from_web_content(Vector entries, Vector used_steps, size_t current_used_step_index) { auto invalid_snapshot = [&] { forget_web_content_state(); return UpdateResult::InvalidSnapshot; }; if (entries.is_empty() || used_steps.is_empty() || current_used_step_index >= used_steps.size() || !entries_are_valid(entries) || !steps_are_valid(used_steps) || !entries_and_used_steps_are_consistent(entries, used_steps)) return invalid_snapshot(); auto incoming_current_top_level_entry_index = top_level_entry_index_for_step(entries, used_steps[current_used_step_index]); if (!incoming_current_top_level_entry_index.has_value()) return invalid_snapshot(); if (m_entries.is_empty()) { canonicalize_document_state_ids(entries); m_entries.clear_with_capacity(); m_entries.ensure_capacity(entries.size()); for (auto& entry : entries) m_entries.unchecked_append(move(entry)); m_used_steps = move(used_steps); m_current_used_step_index = current_used_step_index; m_web_content_known_entries = m_entries; m_web_content_known_used_steps = m_used_steps; m_web_content_current_step = m_used_steps[*m_current_used_step_index]; m_web_content_uses_ui_step_coordinates = true; return UpdateResult::CompleteSnapshot; } VERIFY(m_current_used_step_index.has_value()); VERIFY(!m_used_steps.is_empty()); if (m_entries.size() == entries.size() && m_used_steps.size() == used_steps.size()) { if (entries_match(m_entries, entries) && steps_match(m_used_steps, used_steps)) { m_current_used_step_index = current_used_step_index; m_web_content_known_entries = m_entries; m_web_content_known_used_steps = m_used_steps; m_web_content_current_step = m_used_steps[*m_current_used_step_index]; m_web_content_uses_ui_step_coordinates = true; return UpdateResult::CompleteSnapshot; } } auto local_current_top_level_entry_index = current_top_level_entry_index(); if (!local_current_top_level_entry_index.has_value()) return invalid_snapshot(); if (m_entries.size() == entries.size() + 1 && m_used_steps.size() == used_steps.size() + 1 && *local_current_top_level_entry_index == m_entries.size() - 1 && *m_current_used_step_index == m_used_steps.size() - 1 && *local_current_top_level_entry_index > 0 && *incoming_current_top_level_entry_index > 0 && m_entries[*local_current_top_level_entry_index - 1].step == used_steps[current_used_step_index] && m_entries[*local_current_top_level_entry_index].url == entries[*incoming_current_top_level_entry_index].url) { canonicalize_document_state_ids(entries); m_entries = move(entries); m_used_steps = move(used_steps); m_current_used_step_index = current_used_step_index; m_web_content_known_entries = m_entries; m_web_content_known_used_steps = m_used_steps; m_web_content_current_step = m_used_steps[*m_current_used_step_index]; m_web_content_uses_ui_step_coordinates = true; return UpdateResult::CompleteSnapshot; } auto find_merge_anchor = [&](auto entry_can_anchor) -> Optional { for (size_t local_index = *local_current_top_level_entry_index + 1; local_index > 0; --local_index) { auto candidate_local_index = local_index - 1; for (size_t incoming_index = *incoming_current_top_level_entry_index + 1; incoming_index > 0; --incoming_index) { auto candidate_incoming_index = incoming_index - 1; if (!entry_can_anchor(m_entries[candidate_local_index], entries[candidate_incoming_index])) continue; return SessionHistoryMergeAnchor { .local_index = candidate_local_index, .incoming_index = candidate_incoming_index, }; } } return {}; }; auto merge_anchor = Optional {}; if (m_web_content_uses_ui_step_coordinates && m_web_content_current_step.has_value()) { merge_anchor = find_merge_anchor([&](Entry const& local_entry, Entry const& incoming_entry) { if (local_entry.step != incoming_entry.step) return false; if (local_entry.url == incoming_entry.url) return false; if (local_entry.step != *m_web_content_current_step) return false; if (!m_web_content_known_used_steps.contains_slow(local_entry.step)) return false; auto const* web_content_known_entry = WebView::top_level_entry_for_step(m_web_content_known_entries, local_entry.step); return web_content_known_entry && web_content_known_entry->step == local_entry.step; }); } if (!merge_anchor.has_value()) { merge_anchor = find_merge_anchor([](Entry const& local_entry, Entry const& incoming_entry) { return local_entry.url == incoming_entry.url && entries_have_matching_nonzero_document_state_id(local_entry, incoming_entry); }); } if (!merge_anchor.has_value()) { merge_anchor = find_merge_anchor([](Entry const& local_entry, Entry const& incoming_entry) { return local_entry.url == incoming_entry.url; }); } if (!merge_anchor.has_value()) return invalid_snapshot(); // AD-HOC: The HTML algorithms operate on one traversable session history. Ladybird's UI process can instead // receive a valid partial WebContent snapshot after a process swap or fallback load, so merge the suffix // WebContent knows about into the durable UI mirror while preserving any still-valid forward history. auto local_index = merge_anchor->local_index; auto incoming_index = merge_anchor->incoming_index; size_t common_suffix_size = 0; while (local_index + common_suffix_size < m_entries.size() && incoming_index + common_suffix_size < entries.size()) { if (m_entries[local_index + common_suffix_size].url != entries[incoming_index + common_suffix_size].url) break; ++common_suffix_size; } if (common_suffix_size == 0 && m_web_content_uses_ui_step_coordinates && m_entries[merge_anchor->local_index].step == entries[merge_anchor->incoming_index].step) common_suffix_size = 1; auto local_anchor_step = m_entries[merge_anchor->local_index].step; auto incoming_anchor_step = entries[merge_anchor->incoming_index].step; Optional nested_history_step_floor; if (auto incoming_anchor_document_state_id = entries[merge_anchor->incoming_index].document_state.id; incoming_anchor_document_state_id != 0) { for (size_t i = 0; i < merge_anchor->incoming_index; ++i) { if (entries[i].document_state.id != incoming_anchor_document_state_id) continue; nested_history_step_floor = entries[i].step; break; } } DocumentStateIdTranslationState document_state_id_translation_state; update_next_document_state_id(m_entries, document_state_id_translation_state.next_document_state_id); if (entries[merge_anchor->incoming_index].document_state.id != 0 && m_entries[merge_anchor->local_index].document_state.id != 0) document_state_id_translation_state.translated_document_state_ids.set(entries[merge_anchor->incoming_index].document_state.id, m_entries[merge_anchor->local_index].document_state.id); auto incoming_has_used_steps_after_anchor = false; for (auto const& used_step : used_steps) { if (used_step > incoming_anchor_step) { incoming_has_used_steps_after_anchor = true; break; } } Vector merged_entries; auto web_content_uses_ui_step_coordinates = true; auto incoming_suffix_size = entries.size() - incoming_index; auto preserves_existing_forward_history = common_suffix_size == incoming_suffix_size && !incoming_has_used_steps_after_anchor; auto merged_size = preserves_existing_forward_history ? m_entries.size() : merge_anchor->local_index + incoming_suffix_size; merged_entries.ensure_capacity(merged_size); for (size_t i = 0; i < merge_anchor->local_index; ++i) merged_entries.unchecked_append(m_entries[i]); for (size_t i = merge_anchor->incoming_index; i < entries.size(); ++i) { auto translated_entry = translate_incoming_entry(entries[i], incoming_anchor_step, local_anchor_step, document_state_id_translation_state, StepTranslationMode::TopLevel, nested_history_step_floor); if (!translated_entry.has_value()) return invalid_snapshot(); if (!entry_steps_match(entries[i], *translated_entry)) web_content_uses_ui_step_coordinates = false; merged_entries.unchecked_append(translated_entry.release_value()); } if (preserves_existing_forward_history) { for (size_t i = merge_anchor->local_index + incoming_suffix_size; i < m_entries.size(); ++i) merged_entries.unchecked_append(m_entries[i]); } Vector merged_used_steps; if (preserves_existing_forward_history) { merged_used_steps.ensure_capacity(m_used_steps.size()); for (auto const& used_step : m_used_steps) merged_used_steps.unchecked_append(used_step); } else { merged_used_steps.ensure_capacity(m_used_steps.size() + used_steps.size()); for (auto const& used_step : m_used_steps) { if (used_step > local_anchor_step) break; merged_used_steps.append(used_step); } for (auto const& incoming_used_step : used_steps) { if (incoming_used_step <= incoming_anchor_step) continue; auto translated_step = translate_incoming_step(incoming_used_step, incoming_anchor_step, local_anchor_step); if (!translated_step.has_value()) return invalid_snapshot(); if (*translated_step != incoming_used_step) web_content_uses_ui_step_coordinates = false; if (!merged_used_steps.is_empty() && *translated_step <= merged_used_steps.last()) return invalid_snapshot(); merged_used_steps.append(*translated_step); } } Vector translated_web_content_used_steps; translated_web_content_used_steps.ensure_capacity(used_steps.size()); for (auto const& incoming_used_step : used_steps) { auto translation_mode = StepTranslationMode::TopLevel; if (incoming_used_step < incoming_anchor_step) { if (!nested_history_step_floor.has_value() || incoming_used_step < *nested_history_step_floor) continue; translation_mode = StepTranslationMode::Nested; } auto translated_step = translate_incoming_step(incoming_used_step, incoming_anchor_step, local_anchor_step, translation_mode, nested_history_step_floor); if (!translated_step.has_value()) return invalid_snapshot(); if (*translated_step != incoming_used_step) web_content_uses_ui_step_coordinates = false; if (!translated_web_content_used_steps.is_empty() && *translated_step <= translated_web_content_used_steps.last()) return invalid_snapshot(); translated_web_content_used_steps.append(*translated_step); } auto current_step_translation_mode = StepTranslationMode::TopLevel; if (used_steps[current_used_step_index] < incoming_anchor_step && nested_history_step_floor.has_value() && used_steps[current_used_step_index] >= *nested_history_step_floor) current_step_translation_mode = StepTranslationMode::Nested; auto translated_current_step = translate_incoming_step(used_steps[current_used_step_index], incoming_anchor_step, local_anchor_step, current_step_translation_mode, nested_history_step_floor); if (!translated_current_step.has_value()) return invalid_snapshot(); if (*translated_current_step != used_steps[current_used_step_index]) web_content_uses_ui_step_coordinates = false; if (!translated_web_content_used_steps.contains_slow(*translated_current_step)) return invalid_snapshot(); auto current_used_step_index_after_merge = merged_used_steps.find_first_index(*translated_current_step); if (!current_used_step_index_after_merge.has_value()) return invalid_snapshot(); if (!entries_and_used_steps_are_consistent(merged_entries, merged_used_steps)) return invalid_snapshot(); canonicalize_document_state_ids(merged_entries); Vector translated_web_content_entries; auto web_content_known_entries_start_index = merge_anchor->local_index; if (nested_history_step_floor.has_value()) { auto same_document_entry_index = top_level_entry_index_for_step(merged_entries, *nested_history_step_floor); if (same_document_entry_index.has_value()) web_content_known_entries_start_index = *same_document_entry_index; } translated_web_content_entries.ensure_capacity(merge_anchor->local_index + incoming_suffix_size - web_content_known_entries_start_index); for (size_t i = web_content_known_entries_start_index; i < merge_anchor->local_index + incoming_suffix_size; ++i) translated_web_content_entries.unchecked_append(merged_entries[i]); auto entries_in_ui_document_state_id_order = entries; canonicalize_document_state_ids(entries_in_ui_document_state_id_order); auto web_content_matches_mirror = entries_match(merged_entries, entries_in_ui_document_state_id_order) && steps_match(merged_used_steps, used_steps); m_entries = move(merged_entries); m_used_steps = move(merged_used_steps); m_current_used_step_index = *current_used_step_index_after_merge; VERIFY(*m_current_used_step_index < m_used_steps.size()); VERIFY(current_top_level_entry_index().has_value()); if (web_content_matches_mirror) { m_web_content_known_entries = m_entries; m_web_content_known_used_steps = m_used_steps; m_web_content_uses_ui_step_coordinates = true; } else { m_web_content_known_entries = move(translated_web_content_entries); m_web_content_known_used_steps = move(translated_web_content_used_steps); m_web_content_uses_ui_step_coordinates = web_content_uses_ui_step_coordinates; } m_web_content_current_step = *translated_current_step; return web_content_matches_mirror ? UpdateResult::CompleteSnapshot : UpdateResult::MergedPartialSnapshot; } void TraversableSessionHistory::did_seed_web_content_from_ui_process(size_t current_top_level_entry_index) { VERIFY(current_top_level_entry_index < m_entries.size()); m_web_content_known_entries = m_entries; m_web_content_known_used_steps = m_used_steps; m_web_content_current_step = m_entries[current_top_level_entry_index].step; m_web_content_uses_ui_step_coordinates = true; } bool TraversableSessionHistory::did_seed_web_content_from_ui_process(Vector entries, Vector used_steps, size_t current_used_step_index) { if (m_entries.is_empty() || entries.is_empty() || used_steps.is_empty() || current_used_step_index >= used_steps.size() || !entries_are_valid(entries) || !steps_are_valid(used_steps) || !entries_and_used_steps_are_consistent(entries, used_steps)) return false; auto current_top_level_entry_index = this->current_top_level_entry_index(); if (!current_top_level_entry_index.has_value()) return false; if (used_steps[current_used_step_index] != m_entries[*current_top_level_entry_index].step) return false; auto entries_from_web_content = entries; auto expected_entries = m_entries; canonicalize_document_state_ids(expected_entries); canonicalize_document_state_ids(entries); if (!steps_match(m_used_steps, used_steps)) return false; if (!entries_match(expected_entries, entries)) { Optional current_unknown_entry_index; if (m_entries[*current_top_level_entry_index].document_state.id == 0) current_unknown_entry_index = *current_top_level_entry_index; if (!seed_ack_entries_match(m_entries, entries_from_web_content, current_unknown_entry_index)) return false; m_entries = move(entries_from_web_content); } m_web_content_known_entries = m_entries; m_web_content_known_used_steps = m_used_steps; m_web_content_current_step = used_steps[current_used_step_index]; m_web_content_uses_ui_step_coordinates = true; return true; } bool TraversableSessionHistory::did_restore_web_content_to_current_step(i32 step) { if (!m_current_used_step_index.has_value()) return false; if (m_used_steps[*m_current_used_step_index] != step) return false; if (!m_web_content_uses_ui_step_coordinates) return false; if (!m_web_content_known_used_steps.contains_slow(step)) return false; auto const* web_content_current_top_level_entry = WebView::top_level_entry_for_step(m_web_content_known_entries, step); auto const* ui_current_top_level_entry = current_entry(); if (!web_content_current_top_level_entry || !ui_current_top_level_entry) return false; if (!entry_descriptors_match(*web_content_current_top_level_entry, *ui_current_top_level_entry)) return false; m_web_content_current_step = step; return true; } bool TraversableSessionHistory::did_apply_web_content_traversal_to_step(i32 step) { auto target = traversal_target_for_step(step); if (!target.has_value()) return false; if (!web_content_can_traverse_to(*target)) return false; // https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step // Set traversable's current session history step to targetStep. m_current_used_step_index = target->target_step_index; m_web_content_current_step = step; return true; } bool TraversableSessionHistory::web_content_history_matches_mirror() const { if (!m_web_content_current_step.has_value() || !m_current_used_step_index.has_value()) return false; if (!m_web_content_uses_ui_step_coordinates) return false; if (*m_web_content_current_step != m_used_steps[*m_current_used_step_index]) return false; return entries_match(m_entries, m_web_content_known_entries) && steps_match(m_used_steps, m_web_content_known_used_steps); } void TraversableSessionHistory::forget_web_content_state() { m_web_content_known_entries.clear(); m_web_content_known_used_steps.clear(); m_web_content_current_step.clear(); m_web_content_uses_ui_step_coordinates = false; } Vector TraversableSessionHistory::entries() const { Vector entries; entries.ensure_capacity(m_entries.size()); for (auto const& entry : m_entries) entries.unchecked_append(entry); return entries; } Vector TraversableSessionHistory::used_steps() const { return m_used_steps; } Vector TraversableSessionHistory::web_content_known_entries() const { return m_web_content_known_entries; } Vector TraversableSessionHistory::web_content_known_used_steps() const { return m_web_content_known_used_steps; } Optional TraversableSessionHistory::web_content_current_step() const { return m_web_content_current_step; } bool TraversableSessionHistory::can_go_back() const { return m_current_used_step_index.has_value() && *m_current_used_step_index > 0; } bool TraversableSessionHistory::can_go_forward() const { return m_current_used_step_index.has_value() && *m_current_used_step_index + 1 < m_used_steps.size(); } bool TraversableSessionHistory::has_only_top_level_used_steps() const { if (entries_have_nested_histories(m_entries)) return false; if (m_entries.size() != m_used_steps.size()) return false; for (size_t i = 0; i < m_entries.size(); ++i) { if (m_entries[i].step != m_used_steps[i]) return false; } return true; } bool TraversableSessionHistory::current_step_is_top_level_entry() const { if (!m_current_used_step_index.has_value()) return false; return entry_for_step(m_used_steps[*m_current_used_step_index]) != nullptr; } Optional TraversableSessionHistory::current_step_to_restore_after_loading_top_level_entry() const { if (!m_current_used_step_index.has_value()) return {}; auto current_step = m_used_steps[*m_current_used_step_index]; auto const* current_entry = entry_for_step(current_step); if (current_entry) { if (nested_histories_need_restore_after_loading_entry(*current_entry, current_step)) return current_step; return {}; } return current_step; } bool TraversableSessionHistory::web_content_can_traverse_to(TraversalTarget const& target) const { if (!m_current_used_step_index.has_value() || !m_web_content_current_step.has_value()) return false; if (!m_web_content_uses_ui_step_coordinates) return false; if (*m_web_content_current_step != m_used_steps[*m_current_used_step_index]) return false; if (!m_web_content_known_used_steps.contains_slow(target.target_step)) return false; auto const* web_content_target_top_level_entry = WebView::top_level_entry_for_step(m_web_content_known_entries, target.target_step); if (!web_content_target_top_level_entry || !target.target_top_level_entry) return false; return entry_descriptors_match(*web_content_target_top_level_entry, *target.target_top_level_entry); } Optional TraversableSessionHistory::traversal_target_for_delta(int delta) const { auto target_step_index = target_step_index_for_delta(delta); if (!target_step_index.has_value()) return {}; auto target_step = step_at(*target_step_index); VERIFY(target_step.has_value()); return traversal_target_for_step(*target_step); } Optional TraversableSessionHistory::traversal_target_for_step(i32 step) const { auto target_step_index = m_used_steps.find_first_index(step); if (!target_step_index.has_value()) return {}; auto const* target_top_level_entry = top_level_entry_for_step(step); VERIFY(target_top_level_entry); auto const* current_top_level_entry = current_entry(); VERIFY(current_top_level_entry); return TraversalTarget { .target_step_index = *target_step_index, .target_step = step, .target_top_level_entry = target_top_level_entry, .target_step_is_top_level_entry = entry_for_step(step) != nullptr, .changes_top_level_entry = target_top_level_entry != current_top_level_entry, }; } Optional TraversableSessionHistory::target_step_index_for_delta(int delta) const { // https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta // Let allSteps be the result of getting all used history steps. Let // targetStepIndex be currentStepIndex plus delta. If allSteps[targetStepIndex] // does not exist, then abort these steps. if (!m_current_used_step_index.has_value() || delta == 0) return {}; if (delta < 0) { auto magnitude = static_cast(-static_cast(delta)); if (magnitude > *m_current_used_step_index) return {}; return *m_current_used_step_index - magnitude; } auto target_index = *m_current_used_step_index + static_cast(delta); if (target_index >= m_used_steps.size()) return {}; return target_index; } Optional TraversableSessionHistory::step_at(size_t index) const { if (index >= m_used_steps.size()) return {}; return m_used_steps[index]; } TraversableSessionHistory::Entry const* TraversableSessionHistory::current_entry() const { if (!m_current_used_step_index.has_value()) return nullptr; return top_level_entry_for_step(m_used_steps[*m_current_used_step_index]); } TraversableSessionHistory::Entry const* TraversableSessionHistory::entry_at(size_t index) const { if (index >= m_entries.size()) return nullptr; return &m_entries[index]; } TraversableSessionHistory::Entry const* TraversableSessionHistory::entry_for_step(i32 step) const { for (auto const& entry : m_entries) { if (entry.step == step) return &entry; } return nullptr; } TraversableSessionHistory::Entry const* TraversableSessionHistory::top_level_entry_for_step(i32 step) const { auto index = top_level_entry_index_for_step(m_entries, step); if (!index.has_value()) return nullptr; return &m_entries[*index]; } void TraversableSessionHistory::traverse_to(size_t index) { VERIFY(index < m_used_steps.size()); m_current_used_step_index = index; forget_web_content_state(); } }