| // 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 "chrome/browser/downgrade/snapshot_manager.h" |
| |
| #include <utility> |
| |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/trace_event/trace_event.h" |
| #include "chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.h" |
| #include "chrome/browser/downgrade/downgrade_utils.h" |
| #include "chrome/browser/downgrade/snapshot_file_collector.h" |
| #include "chrome/browser/downgrade/user_data_downgrade.h" |
| #include "chrome/common/chrome_constants.h" |
| |
| namespace downgrade { |
| |
| namespace { |
| |
| constexpr base::FilePath::StringPieceType kSQLiteJournalSuffix( |
| FILE_PATH_LITERAL("-journal")); |
| constexpr base::FilePath::StringPieceType kSQLiteWalSuffix( |
| FILE_PATH_LITERAL("-wal")); |
| constexpr base::FilePath::StringPieceType kSQLiteShmSuffix( |
| FILE_PATH_LITERAL("-shm")); |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class SnapshotOperationResult { |
| kSuccess = 0, |
| kPartialSuccess = 1, |
| kFailure = 2, |
| kFailedToCreateSnapshotDirectory = 3, |
| kMaxValue = kFailedToCreateSnapshotDirectory |
| }; |
| |
| // Copies the item at |user_data_dir|/|relative_path| to |
| // |snapshot_dir|/|relative_path| if the item exists. This also copies all files |
| // related to items that are SQLite databases. Returns |true| if the item was |
| // found at the source and successfully copied. Returns |false| if the item was |
| // found at the source but not successfully copied. Returns no value if the file |
| // was not at the source. |
| base::Optional<bool> CopyItemToSnapshotDirectory( |
| const base::FilePath& relative_path, |
| const base::FilePath& user_data_dir, |
| const base::FilePath& snapshot_dir, |
| bool is_directory) { |
| const auto source = user_data_dir.Append(relative_path); |
| const auto destination = snapshot_dir.Append(relative_path); |
| |
| // If nothing exists to be moved, do not consider it a success or a failure. |
| if (!base::PathExists(source)) |
| return base::nullopt; |
| |
| bool copy_success = is_directory ? base::CopyDirectory(source, destination, |
| /*recursive=*/true) |
| : base::CopyFile(source, destination); |
| |
| if (is_directory) |
| return copy_success; |
| |
| // Copy SQLite journal, WAL and SHM files associated with the files that are |
| // snapshotted if they exist. |
| for (const auto& suffix : |
| {kSQLiteJournalSuffix, kSQLiteWalSuffix, kSQLiteShmSuffix}) { |
| const auto sqlite_file_path = |
| base::FilePath(source.value() + base::FilePath::StringType(suffix)); |
| if (!base::PathExists(sqlite_file_path)) |
| continue; |
| |
| const auto destination_journal = base::FilePath( |
| destination.value() + base::FilePath::StringType(suffix)); |
| copy_success &= base::CopyFile(sqlite_file_path, destination_journal); |
| } |
| |
| return copy_success; |
| } |
| |
| // Returns true if |base_name| matches a user profile directory's format. This |
| // function will ignore the "System Profile" directory. |
| bool IsProfileDir(const base::FilePath& base_name) { |
| // The initial profile ("Default") is an easy one. |
| if (base_name == base::FilePath().AppendASCII(chrome::kInitialProfile)) |
| return true; |
| // Other profile dirs begin with "Profile " and end with a number. |
| const base::FilePath prefix( |
| base::FilePath().AppendASCII(chrome::kMultiProfileDirPrefix)); |
| int number; |
| return base::StartsWith(base_name.value(), prefix.value(), |
| base::CompareCase::SENSITIVE) && |
| base::StringToInt(base::FilePath::StringPieceType(base_name.value()) |
| .substr(prefix.value().length()), |
| &number); |
| } |
| |
| // Returns a list of profile directory base names under |user_data_dir|. |
| std::vector<base::FilePath> GetUserProfileDirectories( |
| const base::FilePath& user_data_dir) { |
| std::vector<base::FilePath> profile_dirs; |
| base::FileEnumerator enumerator(user_data_dir, /*recursive=*/false, |
| base::FileEnumerator::DIRECTORIES); |
| |
| for (base::FilePath path = enumerator.Next(); !path.empty(); |
| path = enumerator.Next()) { |
| const auto base_name = path.BaseName(); |
| if (IsProfileDir(base_name)) |
| profile_dirs.push_back(std::move(base_name)); |
| } |
| return profile_dirs; |
| } |
| |
| // Moves the |source| directory to |target| to be deleted later. If the move |
| // initially fails, move the contents of the directory. |
| void MoveFolderForLaterDeletion(const base::FilePath& source, |
| const base::FilePath& target, |
| const char* move_result_histogram, |
| const char* failure_count_histogram) { |
| const bool move_result = MoveWithoutFallback(source, target); |
| base::UmaHistogramBoolean(move_result_histogram, move_result); |
| if (move_result) |
| return; |
| auto failure_count = |
| MoveContents(source, base::GetUniquePath(target), ExclusionPredicate()); |
| if (failure_count.has_value() && |
| !base::DeleteFile(source, /*recursive=*/false)) { |
| failure_count = failure_count.value() + 1; |
| // Report precise values rather than an exponentially bucketed |
| // histogram. Bucket 0 means that the target directory could not be |
| // created. All other buckets are a count of files/directories left |
| // behind. |
| base::UmaHistogramExactLinear(failure_count_histogram, |
| failure_count.value_or(0), 50); |
| } |
| } |
| |
| } // namespace |
| |
| SnapshotManager::SnapshotManager(const base::FilePath& user_data_dir) |
| : user_data_dir_(user_data_dir) {} |
| |
| SnapshotManager::~SnapshotManager() = default; |
| |
| void SnapshotManager::TakeSnapshot(const base::Version& version) { |
| TRACE_EVENT0("browser", "SnapshotManager::TakeSnapshot"); |
| DCHECK(version.IsValid()); |
| base::FilePath snapshot_dir = |
| user_data_dir_.Append(kSnapshotsDir).AppendASCII(version.GetString()); |
| |
| // If the target snapshot directory already exists, try marking it for |
| // deletion. In case of failure, try moving the contents then keep going. |
| if (base::PathExists(snapshot_dir)) { |
| auto move_target_dir = user_data_dir_.Append(kSnapshotsDir) |
| .AddExtension(kDowngradeDeleteSuffix); |
| base::CreateDirectory(move_target_dir); |
| MoveFolderForLaterDeletion( |
| snapshot_dir, move_target_dir.AppendASCII(version.GetString()), |
| "Downgrade.TakeSnapshot.MoveExistingSnapshot.Result", |
| "Downgrade.TakeSnapshot.MoveExistingSnapshot.FailureCount"); |
| } |
| |
| size_t success_count = 0; |
| size_t error_count = 0; |
| auto record_success_error = [&success_count, |
| &error_count](base::Optional<bool> success) { |
| if (!success.has_value()) |
| return; |
| if (success.value()) |
| ++success_count; |
| else |
| ++error_count; |
| }; |
| |
| // Abort the snapshot if the snapshot directory could not be created. |
| if (!base::CreateDirectory(snapshot_dir)) { |
| base::UmaHistogramEnumeration( |
| "Downgrade.TakeSnapshot.Result", |
| SnapshotOperationResult::kFailedToCreateSnapshotDirectory); |
| return; |
| } |
| |
| // Copy items to be preserved at the top-level of User Data. |
| for (const auto& file : GetUserSnapshotItemDetails()) { |
| record_success_error( |
| CopyItemToSnapshotDirectory(base::FilePath(file.path), user_data_dir_, |
| snapshot_dir, file.is_directory)); |
| } |
| |
| const auto profile_snapshot_item_details = GetProfileSnapshotItemDetails(); |
| |
| // Copy items to be preserved in each Profile directory. |
| for (const auto& profile_dir : GetUserProfileDirectories(user_data_dir_)) { |
| bool profile_dir_created = |
| base::CreateDirectory(snapshot_dir.Append(profile_dir)); |
| base::UmaHistogramBoolean( |
| "Downgrade.TakeSnapshot.ProfileDirectoryCreation.Result", |
| profile_dir_created); |
| // Abort the current profile snapshot if the profile directory could not be |
| // created. |
| if (!profile_dir_created) { |
| ++error_count; |
| continue; |
| } |
| for (const auto& file : profile_snapshot_item_details) { |
| record_success_error(CopyItemToSnapshotDirectory( |
| profile_dir.Append(file.path), user_data_dir_, snapshot_dir, |
| file.is_directory)); |
| } |
| } |
| |
| // Copy the "Last Version" file to the snapshot directory last since it is the |
| // file that determines, by its presence in the snapshot directory, if the |
| // snapshot is complete. |
| record_success_error(CopyItemToSnapshotDirectory( |
| base::FilePath(kDowngradeLastVersionFile), user_data_dir_, snapshot_dir, |
| /*is_directory=*/false)); |
| |
| auto snapshot_result = SnapshotOperationResult::kFailure; |
| if (error_count == 0) |
| snapshot_result = SnapshotOperationResult::kSuccess; |
| else if (success_count > 0) |
| snapshot_result = SnapshotOperationResult::kPartialSuccess; |
| |
| if (error_count > 0) { |
| base::UmaHistogramExactLinear("Downgrade.TakeSnapshot.FailureCount", |
| error_count, 100); |
| } |
| |
| base::UmaHistogramEnumeration("Downgrade.TakeSnapshot.Result", |
| snapshot_result); |
| } |
| |
| void SnapshotManager::RestoreSnapshot(const base::Version& version) { |
| TRACE_EVENT0("browser", "SnapshotManager::RestoreSnapshot"); |
| DCHECK(version.IsValid()); |
| auto snapshot_version = GetSnapshotToRestore(version, user_data_dir_); |
| if (!snapshot_version) |
| return; |
| |
| // The snapshot folder needs to be moved if it matches the current chrome |
| // milestone. However, if it comes from an earlier version, it should be |
| // copied so that a future downgrade to that version stays possible. |
| const bool move_snapshot = snapshot_version == version; |
| |
| auto snapshot_dir = user_data_dir_.Append(kSnapshotsDir) |
| .AppendASCII(snapshot_version->GetString()); |
| |
| size_t success_count = 0; |
| size_t error_count = 0; |
| auto record_success_error = [&success_count, &error_count](bool success) { |
| if (success) |
| ++success_count; |
| else |
| ++error_count; |
| }; |
| base::FileEnumerator enumerator( |
| snapshot_dir, /*recursive=*/false, |
| base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES); |
| |
| // Move or copy the contents of the selected snapshot directory into User |
| // Data. |
| for (base::FilePath path = enumerator.Next(); !path.empty(); |
| path = enumerator.Next()) { |
| const auto item_info = enumerator.GetInfo(); |
| const auto target_path = user_data_dir_.Append(path.BaseName()); |
| if (move_snapshot) |
| record_success_error(base::Move(path, target_path)); |
| else if (enumerator.GetInfo().IsDirectory()) |
| record_success_error( |
| base::CopyDirectory(path, target_path, /*recursive=*/true)); |
| else |
| record_success_error(base::CopyFile(path, target_path)); |
| } |
| auto snapshot_result = SnapshotOperationResult::kFailure; |
| if (error_count == 0) |
| snapshot_result = SnapshotOperationResult::kSuccess; |
| else if (success_count > 0) |
| snapshot_result = SnapshotOperationResult::kPartialSuccess; |
| |
| if (error_count > 0) { |
| base::UmaHistogramExactLinear("Downgrade.RestoreSnapshot.FailureCount", |
| error_count, 100); |
| } |
| base::UmaHistogramEnumeration("Downgrade.RestoreSnapshot.Result", |
| snapshot_result); |
| |
| // Mark the snapshot directory for later deletion if its contents were moved |
| // into User Data. If the snapshot directory cannot be renamed, fallback to |
| // moving its contents. |
| if (move_snapshot) { |
| auto move_target = |
| GetTempDirNameForDelete(user_data_dir_, base::FilePath(kSnapshotsDir)) |
| .Append(snapshot_dir.BaseName()); |
| |
| // Cleans up the remnants of the moved snapshot directory. If moving the |
| // folder fails, delete the "Last Version File" so that this snapshot is |
| // considered incomplete and deleted later. |
| MoveFolderForLaterDeletion(snapshot_dir, move_target, |
| "Downgrade.InvalidSnapshotMove.Result", |
| "Downgrade.InvalidSnapshotMove.FailureCount"); |
| |
| auto last_version_file_path = |
| snapshot_dir.Append(kDowngradeLastVersionFile); |
| base::DeleteFile(last_version_file_path, /*recursive=*/false); |
| |
| base::UmaHistogramBoolean( |
| "Downgrade.RestoreSnapshot.CleanupAfterFailure.Result", |
| !base::PathExists(snapshot_dir)); |
| } |
| } |
| |
| void SnapshotManager::PurgeInvalidAndOldSnapshots( |
| int max_number_of_snapshots, |
| base::Optional<uint32_t> milestone) const { |
| const auto snapshot_dir = user_data_dir_.Append(kSnapshotsDir); |
| |
| // Move the invalid snapshots within from Snapshots/NN to Snapshots.DELETE/NN. |
| const base::FilePath target = |
| snapshot_dir.AddExtension(kDowngradeDeleteSuffix); |
| base::CreateDirectory(target); |
| |
| // Moves all the invalid snapshots for later deletion. |
| auto invalid_snapshots = GetInvalidSnapshots(snapshot_dir); |
| for (const auto& path : invalid_snapshots) { |
| MoveFolderForLaterDeletion(path, target.Append(path.BaseName()), |
| "Downgrade.InvalidSnapshotMove.Result", |
| "Downgrade.InvalidSnapshotMove.FailureCount"); |
| } |
| |
| base::flat_set<base::Version> available_snapshots = |
| GetAvailableSnapshots(snapshot_dir); |
| if (milestone.has_value()) { |
| // Only consider versions for the specified milestone. |
| available_snapshots.erase(available_snapshots.upper_bound( |
| base::Version({*milestone + 1, 0, 0, 0})), |
| available_snapshots.end()); |
| available_snapshots.erase( |
| available_snapshots.begin(), |
| available_snapshots.lower_bound(base::Version({*milestone, 0, 0, 0}))); |
| } |
| |
| if (available_snapshots.size() <= |
| base::checked_cast<size_t>(max_number_of_snapshots)) { |
| return; |
| } |
| |
| size_t number_of_snapshots_to_delete = |
| available_snapshots.size() - max_number_of_snapshots; |
| |
| // Moves all the older snapshots for later deletion. |
| for (const auto& snapshot : available_snapshots) { |
| auto snapshot_path = snapshot_dir.AppendASCII(snapshot.GetString()); |
| MoveFolderForLaterDeletion(snapshot_path, |
| target.Append(snapshot_path.BaseName()), |
| "Downgrade.InvalidSnapshotMove.Result", |
| "Downgrade.InvalidSnapshotMove.FailureCount"); |
| if (--number_of_snapshots_to_delete == 0) |
| break; |
| } |
| } |
| |
| void SnapshotManager::DeleteSnapshotDataForProfile( |
| base::Time delete_begin, |
| const base::FilePath& profile_base_name, |
| int remove_mask) { |
| using DataType = ChromeBrowsingDataRemoverDelegate; |
| |
| bool delete_all = |
| (((remove_mask & DataType::WIPE_PROFILE) == DataType::WIPE_PROFILE) || |
| ((remove_mask & DataType::ALL_DATA_TYPES) == |
| DataType::ALL_DATA_TYPES)) && |
| delete_begin.is_null(); |
| std::vector<base::FilePath> files_to_delete; |
| if (!delete_all) { |
| for (const auto& item : CollectProfileItems()) { |
| if (item.data_types & remove_mask) |
| files_to_delete.push_back(item.path); |
| } |
| } |
| |
| if (!delete_all && files_to_delete.empty()) |
| return; |
| |
| const auto snapshot_dir = user_data_dir_.Append(kSnapshotsDir); |
| auto available_snapshots = GetAvailableSnapshots(snapshot_dir); |
| |
| base::File::Info file_info; |
| for (const auto& snapshot : available_snapshots) { |
| auto snapshot_path = snapshot_dir.AppendASCII(snapshot.GetString()); |
| // If we are not able to get the file info, it probably has been deleted. |
| if (!base::GetFileInfo(snapshot_path, &file_info)) |
| continue; |
| auto profile_absolute_path = snapshot_path.Append(profile_base_name); |
| // Deletes the whole profile from the snapshots if it is being wiped |
| // regardless of |delete_begin|, otherwise deletes the required files from |
| // the snapshot if it was created after |delete_begin|. |
| if (delete_all) { |
| base::DeleteFile(profile_absolute_path, /*recursive=*/true); |
| } else if (delete_begin <= file_info.creation_time && |
| base::PathExists(profile_absolute_path)) { |
| for (const auto& filename : files_to_delete) { |
| base::DeleteFile(profile_absolute_path.Append(filename), |
| /*recursive=*/true); |
| } |
| // Non recursive deletion will fail if the directory is not empty. In this |
| // case we only want to delete the directory if it is empty. |
| base::DeleteFile(profile_absolute_path, /*recursive=*/false); |
| } |
| } |
| } |
| |
| std::vector<SnapshotItemDetails> |
| SnapshotManager::GetProfileSnapshotItemDetails() const { |
| return CollectProfileItems(); |
| } |
| |
| std::vector<SnapshotItemDetails> SnapshotManager::GetUserSnapshotItemDetails() |
| const { |
| return CollectUserDataItems(); |
| } |
| |
| } // namespace downgrade |