| // Copyright 2025 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "components/persistent_cache/backend_params_manager.h" | 
 |  | 
 | #include <algorithm> | 
 | #include <array> | 
 | #include <cstdint> | 
 | #include <queue> | 
 | #include <string> | 
 | #include <string_view> | 
 |  | 
 | #include "base/files/file.h" | 
 | #include "base/files/file_enumerator.h" | 
 | #include "base/files/file_path.h" | 
 | #include "base/files/file_util.h" | 
 | #include "base/functional/bind.h" | 
 | #include "base/location.h" | 
 | #include "base/metrics/histogram_functions.h" | 
 | #include "base/sequence_checker.h" | 
 | #include "base/strings/strcat.h" | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "base/task/thread_pool.h" | 
 | #include "components/persistent_cache/sqlite/sqlite_backend_impl.h" | 
 |  | 
 | namespace { | 
 |  | 
 | #if BUILDFLAG(IS_WIN) | 
 | const uint32_t kMaxFilePathLength = MAX_PATH - 1; | 
 | #elif BUILDFLAG(IS_POSIX) || BUILDFLAG(IS_FUCHSIA) | 
 | const uint32_t kMaxFilePathLength = PATH_MAX - 1; | 
 | #endif | 
 |  | 
 | struct FilePathWithInfo { | 
 |   base::FilePath file_path; | 
 |   base::File::Info info; | 
 | }; | 
 |  | 
 | // Comparator to be used with priority_queue to make sure that smallest times | 
 | // representing the oldest files are at the top. | 
 | class FilePathWithInfoComparator { | 
 |  public: | 
 |   bool operator()(const FilePathWithInfo& a, const FilePathWithInfo& b) { | 
 |     return a.info.last_modified > b.info.last_modified; | 
 |   } | 
 | }; | 
 |  | 
 | const base::FilePath::CharType kDbFile[] = FILE_PATH_LITERAL(".db_file"); | 
 | const base::FilePath::CharType kJournalFile[] = | 
 |     FILE_PATH_LITERAL(".journal_file"); | 
 |  | 
 | constexpr size_t kLruCacheCapacity = 100; | 
 |  | 
 | // Character not allowed in keys or filenames (by itself). Used to mark the | 
 | // start of a replacement token. | 
 | constexpr char kTokenMarker = '`'; | 
 |  | 
 | // All characters allowed in filenames. | 
 | constexpr std::string_view kAllowedCharsInFilenames = | 
 |     "abcdefghijklmnopqrstuvwxyz0123456789-._~" | 
 |     "#[]@!$&'()+,;= "; | 
 |  | 
 | // Use to translate a character `c` viable for a filename into another arbitrary | 
 | // but equally viable character. To reverse the process the function is called | 
 | // with the opposite value for `forward`. If `c` is invalid empty is returned. | 
 | std::optional<char> RotateChar(char c, bool forward) { | 
 |   static_assert(kAllowedCharsInFilenames.length() < 128, | 
 |                 "Allowed chars are a subset of ASCII and overflow while " | 
 |                 "indexing should never be a worry"); | 
 |   size_t char_index = kAllowedCharsInFilenames.find(c); | 
 |  | 
 |   // Characters illegal in filenames are not handled in this function. | 
 |   if (char_index == std::string::npos) { | 
 |     return std::nullopt; | 
 |   } | 
 |  | 
 |   // Arbitrary offset to rotate index in the list of allowed characters. | 
 |   constexpr int64_t kRotationOffset = 37; | 
 |  | 
 |   // Use a rotating index to find a character to replace `c`. Using XOR is not | 
 |   // viable because it doesn't always give a character that is viable in a | 
 |   // filename. | 
 |   if (forward) { | 
 |     return kAllowedCharsInFilenames[(char_index + kRotationOffset) % | 
 |                                     kAllowedCharsInFilenames.length()]; | 
 |   } | 
 |   return kAllowedCharsInFilenames[(char_index + | 
 |                                    kAllowedCharsInFilenames.length() - | 
 |                                    kRotationOffset) % | 
 |                                   kAllowedCharsInFilenames.length()]; | 
 | } | 
 |  | 
 | // Mapping of characters illegal in filenames to a unique token to represent | 
 | // them in filenames. This prevents collisions by avoiding two characters get | 
 | // mapped to the same value. Ex: | 
 | // "*/" --> "`9`2" | 
 | // "><" --> "`5`4" | 
 | // | 
 | // Mapping both strings to "`1`1" for example would result in a valid filename | 
 | // but in backing files being shared for two keys which is not correct. | 
 | static_assert(kAllowedCharsInFilenames.find(kTokenMarker) == std::string::npos, | 
 |               "Space is not allowed in filenames by itself."); | 
 | using ConstStringPair = std::pair<char, const char*>; | 
 | std::array<ConstStringPair, 10> kCharacterToTokenMap{ | 
 |     ConstStringPair{'\\', "`1"}, ConstStringPair{'/', "`2"}, | 
 |     ConstStringPair{'|', "`3"},  ConstStringPair{'<', "`4"}, | 
 |     ConstStringPair{'>', "`5"},  ConstStringPair{':', "`6"}, | 
 |     ConstStringPair{'\"', "`7"}, ConstStringPair{'?', "`8"}, | 
 |     ConstStringPair{'*', "`9"},  ConstStringPair{'\n', "`0"}}; | 
 |  | 
 | // Use to get a token to insert in a filename if `c` is a character | 
 | // illegal in filenames and an empty string if it's not. | 
 | std::string_view FilenameIllegalCharToReplacementToken(char c) { | 
 |   for (const auto& pair : kCharacterToTokenMap) { | 
 |     if (c == pair.first) { | 
 |       return pair.second; | 
 |     } | 
 |   } | 
 |   return ""; | 
 | } | 
 |  | 
 | // Use to get a character associated with `token` if it exists and empty | 
 | // if it doesn't. | 
 | std::optional<char> ReplacementTokenToFilenameIllegalChar( | 
 |     std::string_view token) { | 
 |   for (const auto& pair : kCharacterToTokenMap) { | 
 |     if (token == pair.second) { | 
 |       return pair.first; | 
 |     } | 
 |   } | 
 |  | 
 |   return {}; | 
 | } | 
 |  | 
 | }  // namespace | 
 |  | 
 | namespace persistent_cache { | 
 |  | 
 | BackendParamsManager::BackendParamsManager(base::FilePath top_directory) | 
 |     : backend_params_map_(kLruCacheCapacity), | 
 |       top_directory_(std::move(top_directory)) { | 
 |   if (!base::PathExists(top_directory_)) { | 
 |     base::CreateDirectory(top_directory_); | 
 |   } | 
 | } | 
 | BackendParamsManager::~BackendParamsManager() = default; | 
 |  | 
 | void BackendParamsManager::GetParamsSyncOrCreateAsync( | 
 |     BackendType backend_type, | 
 |     const std::string& key, | 
 |     AccessRights access_rights, | 
 |     CompletedCallback callback) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |  | 
 |   auto it = backend_params_map_.Get( | 
 |       BackendParamsKey{.backend_type = backend_type, .key = key}); | 
 |   if (it != backend_params_map_.end()) { | 
 |     std::move(callback).Run(it->second); | 
 |     return; | 
 |   } | 
 |  | 
 |   std::string filename = FileNameFromKey(key); | 
 |   if (filename.empty()) { | 
 |     std::move(callback).Run(BackendParams()); | 
 |     return; | 
 |   } | 
 |  | 
 |   base::ThreadPool::PostTaskAndReplyWithResult( | 
 |       FROM_HERE, | 
 |       {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}, | 
 |       base::BindOnce(&BackendParamsManager::CreateParamsSync, top_directory_, | 
 |                      backend_type, filename, access_rights), | 
 |       base::BindOnce(&BackendParamsManager::SaveParams, | 
 |                      weak_factory_.GetWeakPtr(), key, std::move(callback))); | 
 | } | 
 |  | 
 | BackendParams BackendParamsManager::GetOrCreateParamsSync( | 
 |     BackendType backend_type, | 
 |     const std::string& key, | 
 |     AccessRights access_rights) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |  | 
 |   auto it = backend_params_map_.Get( | 
 |       BackendParamsKey{.backend_type = backend_type, .key = key}); | 
 |   if (it != backend_params_map_.end()) { | 
 |     return it->second.Copy(); | 
 |   } | 
 |  | 
 |   std::string filename = FileNameFromKey(key); | 
 |   if (filename.empty()) { | 
 |     return BackendParams(); | 
 |   } | 
 |  | 
 |   BackendParams new_params = | 
 |       CreateParamsSync(top_directory_, backend_type, filename, access_rights); | 
 |   SaveParams(key, CompletedCallback(), new_params.Copy()); | 
 |  | 
 |   return new_params; | 
 | } | 
 |  | 
 | void BackendParamsManager::DeleteAllFiles() { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |  | 
 |   // Clear params cache so they don't hold on to files or prevent their | 
 |   // deletion. BackendParam instances that were vended by this class and | 
 |   // retained somewhere else can still create problems and need to be handled | 
 |   // appropriately. | 
 |   backend_params_map_.Clear(); | 
 |  | 
 |   base::DeletePathRecursively(top_directory_); | 
 |  | 
 |   // Recreate the directory since the objective was to delete files only. | 
 |   base::CreateDirectory(top_directory_); | 
 | } | 
 |  | 
 | FootprintReductionResult BackendParamsManager::BringDownTotalFootprintOfFiles( | 
 |     int64_t target_footprint) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |  | 
 |   // Clear params cache so they don't hold on to files or prevent their | 
 |   // deletion. BackendParam instances that were vended by this class and | 
 |   // retained somewhere else can still create problems and need to be handled | 
 |   // appropriately. | 
 |   backend_params_map_.Clear(); | 
 |  | 
 |   int64_t total_footprint = 0; | 
 |  | 
 |   std::priority_queue<FilePathWithInfo, std::vector<FilePathWithInfo>, | 
 |                       FilePathWithInfoComparator> | 
 |       file_paths_with_info; | 
 |   base::FileEnumerator file_enumerator(top_directory_, /*recursive=*/false, | 
 |                                        base::FileEnumerator::FILES); | 
 |  | 
 |   file_enumerator.ForEach([&total_footprint, &file_paths_with_info]( | 
 |                               const base::FilePath& file_path) { | 
 |     base::File::Info info; | 
 |     base::GetFileInfo(file_path, &info); | 
 |  | 
 |     // Only target database files for deletion. | 
 |     if (file_path.MatchesFinalExtension(kDbFile)) { | 
 |       file_paths_with_info.emplace(file_path, info); | 
 |     } | 
 |  | 
 |     // All files count towards measured footprint. | 
 |     total_footprint += info.size; | 
 |   }); | 
 |  | 
 |   // Nothing to do. | 
 |   if (total_footprint <= target_footprint) { | 
 |     return FootprintReductionResult{.current_footprint = total_footprint, | 
 |                                     .number_of_bytes_deleted = 0}; | 
 |   } | 
 |  | 
 |   int64_t size_of_necessary_deletes = total_footprint - target_footprint; | 
 |   int64_t deleted_size = 0; | 
 |  | 
 |   while (!file_paths_with_info.empty()) { | 
 |     if (size_of_necessary_deletes <= deleted_size) { | 
 |       break; | 
 |     } | 
 |  | 
 |     const FilePathWithInfo& file_path_with_info = file_paths_with_info.top(); | 
 |  | 
 |     bool db_file_delete_success = | 
 |         base::DeleteFile(file_path_with_info.file_path); | 
 |     base::UmaHistogramBoolean( | 
 |         "PersistentCache.ParamsManager.DbFile.DeleteSucess", | 
 |         db_file_delete_success); | 
 |  | 
 |     if (db_file_delete_success) { | 
 |       deleted_size += file_path_with_info.info.size; | 
 |  | 
 |       base::FilePath journal_file_path = | 
 |           file_path_with_info.file_path.ReplaceExtension(kJournalFile); | 
 |       base::File::Info journal_file_info; | 
 |       base::GetFileInfo(journal_file_path, &journal_file_info); | 
 |  | 
 |       // TODO (https://crbug.com/377475540): Cleanup when deletion of journal | 
 |       // failed. | 
 |       bool journal_file_delete_success = base::DeleteFile(journal_file_path); | 
 |       base::UmaHistogramBoolean( | 
 |           "PersistentCache.ParamsManager.JournalFile.DeleteSucess", | 
 |           journal_file_delete_success); | 
 |  | 
 |       if (journal_file_delete_success) { | 
 |         deleted_size += journal_file_info.size; | 
 |       } | 
 |     }; | 
 |  | 
 |     file_paths_with_info.pop(); | 
 |   } | 
 |  | 
 |   return FootprintReductionResult{ | 
 |       .current_footprint = total_footprint - deleted_size, | 
 |       .number_of_bytes_deleted = deleted_size}; | 
 | } | 
 |  | 
 | // static | 
 | std::string BackendParamsManager::FileNameFromKey(const std::string& key) { | 
 |   std::string filename; | 
 |   filename.reserve(key.size()); | 
 |  | 
 |   for (char c : key) { | 
 |     std::string_view token = FilenameIllegalCharToReplacementToken(c); | 
 |     if (!token.empty()) { | 
 |       filename += token; | 
 |     } else { | 
 |       std::optional<char> rotated_char = RotateChar(c, true); | 
 |  | 
 |       if (!rotated_char.has_value()) { | 
 |         // There's no way to rotate an illegal character so return an empty | 
 |         // string. | 
 |         return ""; | 
 |       } | 
 |       filename += rotated_char.value(); | 
 |     } | 
 |   } | 
 |  | 
 |   return filename; | 
 | } | 
 |  | 
 | // static | 
 | std::string BackendParamsManager::KeyFromFileName(const std::string& filename) { | 
 |   std::string key; | 
 |   key.reserve(filename.size()); | 
 |  | 
 |   for (auto it = filename.begin(); it != filename.end(); ++it) { | 
 |     if (*it == kTokenMarker) { | 
 |       // Token markers cannot be by themselves in filenames. Return an empty | 
 |       // string instead of CHECKing here because it's not advisable to have a | 
 |       // crash because something renamed a file. | 
 |       if (it + 1 == filename.end()) { | 
 |         return ""; | 
 |       } | 
 |  | 
 |       std::optional<char> c = | 
 |           ReplacementTokenToFilenameIllegalChar(std::string_view(it, it + 2)); | 
 |       if (c.has_value()) { | 
 |         key += c.value(); | 
 |  | 
 |         // Skip the character already parsed. | 
 |         ++it; | 
 |         continue; | 
 |       } | 
 |  | 
 |       // If execution gets here it's that a token marker was followed by a | 
 |       // character that didn't resolve to anything. This means the file name is | 
 |       // invalid. | 
 |       return ""; | 
 |     } else { | 
 |       std::optional<char> rotated_char = RotateChar(*it, false); | 
 |  | 
 |       if (!rotated_char.has_value()) { | 
 |         // There's no way to rotate an illegal character so return an empty | 
 |         // string. | 
 |         return ""; | 
 |       } | 
 |  | 
 |       key += rotated_char.value(); | 
 |     } | 
 |   } | 
 |  | 
 |   return key; | 
 | } | 
 |  | 
 | // static | 
 | BackendParams BackendParamsManager::CreateParamsSync( | 
 |     base::FilePath directory, | 
 |     BackendType backend_type, | 
 |     const std::string& filename, | 
 |     AccessRights access_rights) { | 
 |   BackendParams params; | 
 |   params.type = backend_type; | 
 |  | 
 |   const bool writes_supported = (access_rights == AccessRights::kReadWrite); | 
 |   uint32_t flags = base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ; | 
 |  | 
 |   if (writes_supported) { | 
 |     flags |= base::File::FLAG_WRITE; | 
 |   } | 
 |  | 
 | #if BUILDFLAG(IS_WIN) | 
 |   // PersistentCache backing files are not executables. | 
 |   flags |= base::File::FLAG_WIN_NO_EXECUTE; | 
 |  | 
 |   // String conversion to wstring necessary on Windows. | 
 |   std::wstring filename_part = base::UTF8ToWide(filename); | 
 |   base::FilePath db_file_name = | 
 |       base::FilePath(base::StrCat({filename_part, kDbFile})); | 
 |   base::FilePath journal_file_name = | 
 |       base::FilePath(base::StrCat({filename_part, kJournalFile})); | 
 | #else | 
 |   base::FilePath db_file_name = | 
 |       base::FilePath(base::StrCat({filename, kDbFile})); | 
 |   base::FilePath journal_file_name = | 
 |       base::FilePath(base::StrCat({filename, kJournalFile})); | 
 | #endif | 
 |  | 
 |   base::FilePath db_file_full_path = directory.Append(db_file_name); | 
 |   params.db_file = base::File(db_file_full_path, flags); | 
 |   params.db_file_is_writable = writes_supported; | 
 |  | 
 |   base::FilePath journal_file_full_path = directory.Append(journal_file_name); | 
 |   params.journal_file = base::File(journal_file_full_path, flags); | 
 |   params.journal_file_is_writable = writes_supported; | 
 |  | 
 |   if (!params.db_file.IsValid() || !params.journal_file.IsValid()) { | 
 |     size_t smallest_path_length = | 
 |         std::min(db_file_full_path.value().length(), | 
 |                  journal_file_full_path.value().length()); | 
 |     if (smallest_path_length > kMaxFilePathLength) { | 
 |       base::UmaHistogramCounts100( | 
 |           "PersistentCache.ParamsManager.FilenameCharactersOverLimit", | 
 |           smallest_path_length - kMaxFilePathLength); | 
 |     } | 
 |   } | 
 |  | 
 |   return params; | 
 | } | 
 |  | 
 | void BackendParamsManager::SaveParams(const std::string& key, | 
 |                                       CompletedCallback callback, | 
 |                                       BackendParams backend_params) { | 
 |   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
 |  | 
 |   if (callback) { | 
 |     std::move(callback).Run(backend_params); | 
 |   } | 
 |  | 
 |   // Avoid saving invalid files. | 
 |   if (backend_params.db_file.IsValid() && | 
 |       backend_params.journal_file.IsValid()) { | 
 |     backend_params_map_.Put( | 
 |         BackendParamsKey{.backend_type = backend_params.type, .key = key}, | 
 |         std::move(backend_params)); | 
 |   } | 
 | } | 
 |  | 
 | // static | 
 | std::string BackendParamsManager::GetAllAllowedCharactersInKeysForTesting() { | 
 |   // Start with all characters allowed in both keys and filenames. | 
 |   std::string allowed_characters(kAllowedCharsInFilenames); | 
 |  | 
 |   // Add characters only allowed in keys. | 
 |   for (const auto& pair : kCharacterToTokenMap) { | 
 |     allowed_characters += pair.first; | 
 |   } | 
 |  | 
 |   return allowed_characters; | 
 | } | 
 |  | 
 | }  // namespace persistent_cache |