| // Copyright 2024 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/variations/seed_reader_writer.h" |
| |
| #include "base/base64.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/important_file_writer.h" |
| #include "base/json/values_util.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/version_info/channel.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/variations/entropy_provider.h" |
| #include "components/variations/pref_names.h" |
| #include "third_party/zlib/google/compression_utils.h" |
| |
| namespace variations { |
| namespace { |
| |
| // A struct to hold the permanent country code and version. Because they're |
| // stored in a single pref, we need to read them together. |
| // TODO(crbug.com/411431524): Remove this once it's stored in the Seed File. |
| struct PermanentCountryVersion { |
| std::string_view country; |
| std::string_view version; |
| }; |
| |
| // Histogram suffix used by ImportantFileWriter for recording seed file write |
| // information. |
| constexpr char kSeedWriterHistogramSuffix[] = "VariationsSeedsV1"; |
| |
| // Serializes and returns seed data used during write to disk. Will be run |
| // asynchronously on a background thread. |
| std::optional<std::string> DoSerialize(StoredSeedInfo seed_info) { |
| // TODO(crbug.com/370480037): Begin doing seed compression here instead of in |
| // VariationsSeedStore. |
| return seed_info.data(); |
| } |
| |
| // Returns the file path used to store a seed. If `seed_file_dir` is empty, an |
| // empty file path is returned. |
| base::FilePath GetFilePath(const base::FilePath& seed_file_dir, |
| base::FilePath::StringViewType filename) { |
| return seed_file_dir.empty() ? base::FilePath() |
| : seed_file_dir.Append(filename); |
| } |
| |
| // Returns true if the client is eligible to participate in the seed file trial. |
| bool IsEligibleForSeedFileTrial(version_info::Channel channel, |
| const base::FilePath& seed_file_dir, |
| const EntropyProviders* entropy_providers) { |
| // Note platforms that should not participate in the experiment will |
| // deliberately pass an empty |seed_file_dir| and null |entropy_provider|. |
| if (seed_file_dir.empty() || entropy_providers == nullptr) { |
| return false; |
| } |
| return channel == version_info::Channel::CANARY || |
| channel == version_info::Channel::DEV; |
| } |
| |
| // Sets up the seed file experiment which only some clients are eligible for |
| // (see IsEligibleForSeedFileTrial()). |
| void SetUpSeedFileTrial( |
| const base::FieldTrial::EntropyProvider& entropy_provider, |
| version_info::Channel channel) { |
| // Verify that the field trial has not already been set up. This may be the |
| // case if a SeedReaderWriter associated with a safe seed calls this function |
| // before one associated with a latest seed or vice versa. |
| if (base::FieldTrialList::TrialExists(kSeedFileTrial)) { |
| return; |
| } |
| |
| // Only 1% of clients on stable should participate in the experiment. |
| base::FieldTrial::Probability group_probability = |
| channel == version_info::Channel::STABLE ? 1 : 50; |
| |
| scoped_refptr<base::FieldTrial> trial( |
| base::FieldTrialList::FactoryGetFieldTrial( |
| kSeedFileTrial, /*total_probability=*/100, kDefaultGroup, |
| entropy_provider)); |
| |
| trial->AppendGroup(kControlGroup, group_probability); |
| trial->AppendGroup(kSeedFilesGroup, group_probability); |
| } |
| |
| // Returns the permanent country code and version. For the safe seed, version |
| // always will be empty. |
| PermanentCountryVersion GetPermanentCountryVersion(PrefService* local_state, |
| std::string_view pref_name) { |
| // TODO(crbug.com/411431524): Remove this once it's stored in the Seed File. |
| // We need to check because the safe seed pref is a string while the latest |
| // seed pref is a list. |
| if (pref_name == prefs::kVariationsSafeSeedPermanentConsistencyCountry) { |
| return {.country = local_state->GetString(pref_name), .version = ""}; |
| } |
| const auto& list_value = local_state->GetList(pref_name); |
| PermanentCountryVersion result; |
| if (list_value.size() == 2) { |
| const std::string* stored_version = nullptr; |
| // We don't need to check the validity of the version here, as it's done |
| // later by |
| // VariationsFieldTrialCreatorBase::LoadPermanentConsistencyCountry(). |
| if ((stored_version = list_value[0].GetIfString())) { |
| result.version = *stored_version; |
| } |
| const std::string* stored_country = nullptr; |
| if ((stored_country = list_value[1].GetIfString())) { |
| result.country = *stored_country; |
| } |
| } |
| return result; |
| } |
| |
| // Stores the permanent country code and version in local state. For the safe |
| // seed, the version is always empty. |
| void SetPermanentCountryVersion(PrefService* local_state, |
| std::string_view pref_name, |
| std::string_view country_code, |
| std::string_view version) { |
| // TODO(crbug.com/411431524): Remove this once it's stored in the Seed File. |
| // We need to check because the safe seed pref is a string while the latest |
| // seed pref is a list. |
| const bool is_safe_seed = |
| pref_name == prefs::kVariationsSafeSeedPermanentConsistencyCountry; |
| if (is_safe_seed) { |
| local_state->SetString(pref_name, country_code); |
| } else { |
| base::Value::List list_value; |
| list_value.Append(version); |
| list_value.Append(country_code); |
| local_state->SetList(pref_name, std::move(list_value)); |
| } |
| } |
| |
| int64_t TimeToProtoTime(base::Time time) { |
| return time.ToDeltaSinceWindowsEpoch().InMicroseconds(); |
| } |
| |
| base::Time ProtoTimeToTime(int64_t proto_time) { |
| return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(proto_time)); |
| } |
| |
| bool ShouldStoreWithoutProcessing(std::string_view seed_data) { |
| return seed_data.empty() || seed_data == kIdenticalToSafeSeedSentinel; |
| } |
| |
| std::string ReadSeedFromFile(base::FilePath file_path) { |
| std::string seed_data; |
| bool success = base::ReadFileToString(file_path, &seed_data); |
| if (!success) { |
| return ""; |
| } |
| return seed_data; |
| } |
| |
| } // namespace |
| |
| const SeedFieldsPrefs kRegularSeedFieldsPrefs = { |
| .seed = prefs::kVariationsCompressedSeed, |
| .signature = prefs::kVariationsSeedSignature, |
| .milestone = prefs::kVariationsSeedMilestone, |
| .seed_date = prefs::kVariationsSeedDate, |
| .client_fetch_time = prefs::kVariationsLastFetchTime, |
| .session_country_code = prefs::kVariationsCountry, |
| .permanent_country_code_version = |
| prefs::kVariationsPermanentConsistencyCountry, |
| }; |
| |
| const SeedFieldsPrefs kSafeSeedFieldsPrefs = { |
| .seed = prefs::kVariationsSafeCompressedSeed, |
| .signature = prefs::kVariationsSafeSeedSignature, |
| .milestone = prefs::kVariationsSafeSeedMilestone, |
| .seed_date = prefs::kVariationsSafeSeedDate, |
| .client_fetch_time = prefs::kVariationsSafeSeedFetchTime, |
| .session_country_code = prefs::kVariationsSafeSeedSessionConsistencyCountry, |
| .permanent_country_code_version = |
| prefs::kVariationsSafeSeedPermanentConsistencyCountry, |
| }; |
| |
| SeedInfo::SeedInfo(std::string_view signature, |
| int milestone, |
| base::Time seed_date, |
| base::Time client_fetch_time, |
| std::string_view session_country_code, |
| std::string_view permanent_country_code, |
| std::string_view permanent_country_version) |
| : signature(signature), |
| milestone(milestone), |
| seed_date(seed_date), |
| client_fetch_time(client_fetch_time), |
| session_country_code(session_country_code), |
| permanent_country_code(permanent_country_code), |
| permanent_country_version(permanent_country_version) {} |
| |
| SeedInfo::~SeedInfo() = default; |
| |
| SeedInfo::SeedInfo(const SeedInfo& other) = default; |
| |
| SeedReaderWriter::SeedReaderWriter( |
| PrefService* local_state, |
| const base::FilePath& seed_file_dir, |
| base::FilePath::StringViewType seed_filename, |
| const SeedFieldsPrefs& fields_prefs, |
| version_info::Channel channel, |
| const EntropyProviders* entropy_providers, |
| scoped_refptr<base::SequencedTaskRunner> file_task_runner) |
| : local_state_(local_state), |
| fields_prefs_(fields_prefs), |
| file_task_runner_(std::move(file_task_runner)) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(local_state_) << "SeedReaderWriter needs a valid local state."; |
| if (!seed_file_dir.empty()) { |
| seed_writer_ = std::make_unique<base::ImportantFileWriter>( |
| GetFilePath(seed_file_dir, seed_filename), file_task_runner_, |
| kSeedWriterHistogramSuffix); |
| } |
| if (IsEligibleForSeedFileTrial(channel, seed_file_dir, entropy_providers)) { |
| SetUpSeedFileTrial(entropy_providers->default_entropy(), channel); |
| if (ShouldUseSeedFile()) { |
| ReadSeedFile(); |
| } |
| } else if (ShouldMigrateToLocalState(channel)) { |
| MigrateToLocalState(); |
| } |
| } |
| |
| SeedReaderWriter::~SeedReaderWriter() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (HasPendingWrite()) { |
| seed_writer_->DoScheduledWrite(); |
| } |
| } |
| |
| StoreSeedResult SeedReaderWriter::StoreValidatedSeedInfo( |
| ValidatedSeedInfo seed_info) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| return ScheduleSeedFileWrite(seed_info); |
| } else { |
| return ScheduleLocalStateWrite(seed_info); |
| } |
| } |
| |
| void SeedReaderWriter::ClearSeedInfo() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // TODO(crbug.com/372009105): Remove if-statements when experiment has ended. |
| if (ShouldUseSeedFile()) { |
| ScheduleSeedFileClear(); |
| } else { |
| local_state_->ClearPref(fields_prefs_->seed); |
| local_state_->ClearPref(fields_prefs_->signature); |
| local_state_->ClearPref(fields_prefs_->milestone); |
| local_state_->ClearPref(fields_prefs_->seed_date); |
| local_state_->ClearPref(fields_prefs_->client_fetch_time); |
| // Although only clients in the treatment group write seeds to dedicated |
| // seed files, attempt to delete the seed file for clients with |
| // Local-State-based seeds. If a client switches experiment groups or |
| // channels, their device could have a seed file with stale seed data. |
| if (seed_writer_) { |
| DeleteSeedFile(); |
| } |
| } |
| } |
| |
| void SeedReaderWriter::ClearSessionCountry() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| stored_seed_info_.clear_session_country_code(); |
| } |
| local_state_->ClearPref(fields_prefs_->session_country_code); |
| } |
| |
| SeedInfo SeedReaderWriter::GetSeedInfo() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| return SeedInfo( |
| /*signature=*/stored_seed_info_.signature(), |
| /*milestone=*/stored_seed_info_.milestone(), |
| /*seed_date=*/ProtoTimeToTime(stored_seed_info_.seed_date()), |
| /*client_fetch_time=*/ |
| ProtoTimeToTime(stored_seed_info_.client_fetch_time()), |
| /*session_country_code=*/stored_seed_info_.session_country_code(), |
| /*permanent_country_code=*/stored_seed_info_.permanent_country_code(), |
| /*permanent_country_version=*/stored_seed_info_.permanent_version()); |
| } else { |
| PermanentCountryVersion permanent_country_version = |
| GetPermanentCountryVersion( |
| local_state_, fields_prefs_->permanent_country_code_version); |
| return SeedInfo( |
| /*signature=*/local_state_->GetString(fields_prefs_->signature), |
| /*milestone=*/local_state_->GetInteger(fields_prefs_->milestone), |
| /*seed_date=*/local_state_->GetTime(fields_prefs_->seed_date), |
| /*client_fetch_time=*/ |
| local_state_->GetTime(fields_prefs_->client_fetch_time), |
| /*session_country_code=*/ |
| local_state_->GetString(fields_prefs_->session_country_code), |
| /*permanent_country_code=*/permanent_country_version.country, |
| /*permanent_country_version=*/permanent_country_version.version); |
| } |
| } |
| |
| void SeedReaderWriter::SetTimerForTesting(base::OneShotTimer* timer_override) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (seed_writer_) { |
| seed_writer_->SetTimerForTesting(timer_override); // IN-TEST |
| } |
| } |
| |
| void SeedReaderWriter::SetSeedDate(base::Time server_date_fetched) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Both groups write the seed date to local state. |
| // TODO(crbug.com/380465790): Update seed date in seed files instead of local |
| // state if the client is in the treatment group. |
| if (ShouldUseSeedFile()) { |
| stored_seed_info_.set_seed_date(TimeToProtoTime(server_date_fetched)); |
| } |
| local_state_->SetTime(fields_prefs_->seed_date, server_date_fetched); |
| } |
| |
| void SeedReaderWriter::SetFetchTime(base::Time fetch_time) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Both groups write the fetch time to local state. |
| // TODO(crbug.com/380465790): Update fetch time in seed files instead of local |
| // state if the client is in the treatment group. |
| if (ShouldUseSeedFile()) { |
| stored_seed_info_.set_client_fetch_time(TimeToProtoTime(fetch_time)); |
| } |
| local_state_->SetTime(fields_prefs_->client_fetch_time, fetch_time); |
| } |
| |
| bool SeedReaderWriter::HasPendingWrite() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return seed_writer_ && seed_writer_->HasPendingWrite(); |
| } |
| |
| void SeedReaderWriter::ClearPermanentConsistencyCountryAndVersion() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| // TODO(crbug.com/380465790): Clear the values from the seed file if the |
| // client is in the treatment group. |
| stored_seed_info_.clear_permanent_country_code(); |
| stored_seed_info_.clear_permanent_version(); |
| } |
| local_state_->ClearPref(fields_prefs_->permanent_country_code_version); |
| } |
| |
| void SeedReaderWriter::SetPermanentConsistencyCountryAndVersion( |
| const std::string_view country, |
| const std::string_view version) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| stored_seed_info_.set_permanent_country_code(country); |
| stored_seed_info_.set_permanent_version(version); |
| } |
| SetPermanentCountryVersion(local_state_, |
| fields_prefs_->permanent_country_code_version, |
| country, version); |
| } |
| |
| LoadSeedResult SeedReaderWriter::ReadSeedDataOnStartup( |
| std::string* seed_data, |
| std::string* base64_seed_signature) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // On startup, the seed data should always be kept in memory. |
| CHECK(!seed_purgeable_from_memory_) |
| << "Seed data should not be purgeable from memory on startup."; |
| SeedInfo stored_seed_info = GetSeedInfo(); |
| if (ShouldUseSeedFile()) { |
| return ProcessStoredSeedData( |
| SeedStorageFormat::kCompressed, stored_seed_data_.value_or(""), |
| stored_seed_info.signature, seed_data, base64_seed_signature); |
| } else { |
| return ProcessStoredSeedData( |
| SeedStorageFormat::kCompressedAndBase64Encoded, |
| /*stored_seed_data=*/local_state_->GetString(fields_prefs_->seed), |
| stored_seed_info.signature, seed_data, base64_seed_signature); |
| } |
| } |
| |
| void SeedReaderWriter::ReadSeedData( |
| SeedReaderWriter::ReadSeedDataCallback done_callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| GetSeedData( |
| base::BindOnce(&SeedReaderWriter::ProcessStoredSeedDataAndRunCallback, |
| weak_ptr_factory_.GetWeakPtr(), std::move(done_callback))); |
| } |
| |
| void SeedReaderWriter::StoreRawSeedForTesting(std::string seed_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| stored_seed_data_ = std::move(seed_data); |
| seed_writer_->ScheduleWriteWithBackgroundDataSerializer(this); |
| } else { |
| local_state_->SetString(fields_prefs_->seed, std::move(seed_data)); |
| } |
| } |
| |
| void SeedReaderWriter::StoreBase64EncodedSeedAndSignatureForTesting( |
| std::string base64_compressed_data, |
| std::string base64_signature) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::string decoded_seed_data; |
| CHECK(base::Base64Decode(base64_compressed_data, &decoded_seed_data)) |
| << "Failed to decode base64 compressed data"; |
| std::string uncompressed_seed_data; |
| CHECK(compression::GzipUncompress(decoded_seed_data, &uncompressed_seed_data)) |
| << "Failed to uncompress seed data"; |
| StoreValidatedSeedInfo( |
| ValidatedSeedInfo{.seed_data = std::move(uncompressed_seed_data), |
| .signature = std::move(base64_signature)}); |
| } |
| |
| bool SeedReaderWriter::IsIdenticalToSafeSeedSentinel() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldUseSeedFile()) { |
| return stored_seed_data_.value_or("") == kIdenticalToSafeSeedSentinel; |
| } else { |
| return local_state_->GetString(fields_prefs_->seed) == |
| kIdenticalToSafeSeedSentinel; |
| } |
| } |
| |
| void SeedReaderWriter::AllowToPurgeSeedDataFromMemory() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| seed_purgeable_from_memory_ = true; |
| if (ShouldClearSeedDataFromMemory()) { |
| stored_seed_data_ = std::nullopt; |
| } |
| } |
| |
| base::ImportantFileWriter::BackgroundDataProducerCallback |
| SeedReaderWriter::GetSerializedDataProducerForBackgroundSequence() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // DoSerialize() will be run on a background thread different than the one |
| // this function runs on, so `seed_info_.data` is passed as a copy to avoid |
| // potential race condition in which the `seed_info_.data is potentially |
| // modified at the same time DoSerialize() attempts to access it. We cannot |
| // use std::move here as we may attempt to read `seed_info_.data` from memory |
| // after a write and before we modify `seed_info_.data` again, in which case |
| // unexpected empty data would be read. |
| auto call_clear_seed_cb = |
| base::BindPostTask(base::SequencedTaskRunner::GetCurrentDefault(), |
| base::BindOnce(&SeedReaderWriter::OnSeedWriteComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| seed_writer_->RegisterOnNextWriteCallbacks(base::OnceClosure(), |
| std::move(call_clear_seed_cb)); |
| StoredSeedInfo stored_seed_info = stored_seed_info_; |
| // TODO(crbug.com/370539202): Potentially use std::move instead of copy if we |
| // are able to move seed data out of memory before the write completes. |
| stored_seed_info.set_data(stored_seed_data_.value_or("")); |
| return base::BindOnce(&DoSerialize, std::move(stored_seed_info)); |
| } |
| |
| bool SeedReaderWriter::ShouldClearSeedDataFromMemory() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return seed_purgeable_from_memory_ && !HasPendingWrite() && |
| stored_seed_data_.has_value() && !stored_seed_data_->empty() && |
| *stored_seed_data_ != kIdenticalToSafeSeedSentinel; |
| } |
| |
| void SeedReaderWriter::OnSeedWriteComplete(bool write_success) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (ShouldClearSeedDataFromMemory()) { |
| stored_seed_data_ = std::nullopt; |
| } |
| } |
| |
| StoreSeedResult SeedReaderWriter::ScheduleSeedFileWrite( |
| ValidatedSeedInfo seed_info) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Set `seed_info_.data`, this will be used later by the background |
| // serialization and can be changed multiple times before a scheduled write |
| // completes, in which case the background serializer will use the |
| // `seed_info_.data` set at the last call of this function. |
| std::string seed_data; |
| // If the seed data is empty or it's the sentinel value, store the given |
| // string without compressing. |
| if (ShouldStoreWithoutProcessing(seed_info.seed_data)) { |
| seed_data = seed_info.seed_data; |
| } else if (!compression::GzipCompress(seed_info.seed_data, &seed_data)) { |
| return StoreSeedResult::kFailedGzip; |
| } |
| stored_seed_data_ = std::move(seed_data); |
| stored_seed_info_.set_signature(seed_info.signature); |
| stored_seed_info_.set_milestone(seed_info.milestone); |
| stored_seed_info_.set_seed_date(TimeToProtoTime(seed_info.seed_date)); |
| stored_seed_info_.set_client_fetch_time( |
| TimeToProtoTime(seed_info.client_fetch_time)); |
| // Only update the latest country code if it is not empty. |
| if (!seed_info.session_country_code.empty()) { |
| stored_seed_info_.set_session_country_code(seed_info.session_country_code); |
| } |
| if (!seed_info.permanent_country_code.empty()) { |
| stored_seed_info_.set_permanent_country_code( |
| seed_info.permanent_country_code); |
| } |
| if (!seed_info.permanent_country_version.empty()) { |
| stored_seed_info_.set_permanent_version( |
| seed_info.permanent_country_version); |
| } |
| // `seed_writer_` will eventually call |
| // GetSerializedDataProducerForBackgroundSequence() on *this* object to get |
| // a callback that will be run asynchronously. This callback will be used to |
| // call the DoSerialize() function which will return the seed data to write |
| // to the file. This write will also be asynchronous and on a different |
| // thread. Note that it is okay to call this while a write is already |
| // occurring in a background thread and that this will result in a new write |
| // being scheduled. |
| seed_writer_->ScheduleWriteWithBackgroundDataSerializer(this); |
| // TODO(crbug.com/380465790): Seed-related info that has not yet been migrated |
| // to seed files must continue to be maintained in local state. Once the |
| // migration is complete, stop updating local state. |
| local_state_->SetString(fields_prefs_->signature, |
| stored_seed_info_.signature()); |
| local_state_->SetInteger(fields_prefs_->milestone, |
| stored_seed_info_.milestone()); |
| local_state_->SetTime(fields_prefs_->seed_date, |
| ProtoTimeToTime(stored_seed_info_.seed_date())); |
| local_state_->SetTime(fields_prefs_->client_fetch_time, |
| ProtoTimeToTime(stored_seed_info_.client_fetch_time())); |
| if (!seed_info.session_country_code.empty()) { |
| local_state_->SetString(fields_prefs_->session_country_code, |
| stored_seed_info_.session_country_code()); |
| } |
| // Version could be empty in case of the SafeSeed. |
| if (!seed_info.permanent_country_code.empty()) { |
| SetPermanentCountryVersion(local_state_, |
| fields_prefs_->permanent_country_code_version, |
| stored_seed_info_.permanent_country_code(), |
| stored_seed_info_.permanent_version()); |
| } |
| return StoreSeedResult::kSuccess; |
| } |
| |
| void SeedReaderWriter::ScheduleSeedFileClear() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Set `seed_info_.data`, this will be used later by the background |
| // serialization and can be changed multiple times before a scheduled write |
| // completes, in which case the background serializer will use the |
| // `seed_info_.data` set at the last call of this function. |
| stored_seed_data_ = ""; |
| stored_seed_info_.clear_signature(); |
| stored_seed_info_.clear_milestone(); |
| stored_seed_info_.clear_seed_date(); |
| stored_seed_info_.clear_client_fetch_time(); |
| // `seed_writer_` will eventually call |
| // GetSerializedDataProducerForBackgroundSequence() on *this* object to get |
| // a callback that will be run asynchronously. This callback will be used to |
| // call the DoSerialize() function which will return the seed data to write |
| // to the file. This write will also be asynchronous and on a different |
| // thread. Note that it is okay to call this while a write is already |
| // occurring in a background thread and that this will result in a new write |
| // being scheduled. |
| seed_writer_->ScheduleWriteWithBackgroundDataSerializer(this); |
| // TODO(crbug.com/380465790): Seed-related info that has not yet been migrated |
| // to seed files must continue to be maintained in local state. Once the |
| // migration is complete, stop updating local state. |
| local_state_->ClearPref(fields_prefs_->signature); |
| local_state_->ClearPref(fields_prefs_->milestone); |
| local_state_->ClearPref(fields_prefs_->seed_date); |
| local_state_->ClearPref(fields_prefs_->client_fetch_time); |
| } |
| |
| void SeedReaderWriter::DeleteSeedFile() { |
| file_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(base::IgnoreResult(&base::DeleteFile), |
| seed_writer_->path())); |
| } |
| |
| void SeedReaderWriter::ReadSeedFile() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| const std::string histogram_suffix = |
| base::Contains(seed_writer_->path().BaseName().MaybeAsASCII(), "Safe") |
| ? "Safe" |
| : "Latest"; |
| std::string seed_file_data; |
| const bool success = |
| base::ReadFileToString(seed_writer_->path(), &seed_file_data); |
| if (success) { |
| stored_seed_data_ = std::move(seed_file_data); |
| // TODO(crbug.com/380465790): Read other SeedInfo fields from the seed file |
| // once it's stored there. |
| stored_seed_info_.set_signature( |
| local_state_->GetString(fields_prefs_->signature)); |
| stored_seed_info_.set_milestone( |
| local_state_->GetInteger(fields_prefs_->milestone)); |
| stored_seed_info_.set_seed_date( |
| TimeToProtoTime(local_state_->GetTime(fields_prefs_->seed_date))); |
| stored_seed_info_.set_client_fetch_time(TimeToProtoTime( |
| local_state_->GetTime(fields_prefs_->client_fetch_time))); |
| stored_seed_info_.set_session_country_code( |
| local_state_->GetString(fields_prefs_->session_country_code)); |
| PermanentCountryVersion permanent_country_version = |
| GetPermanentCountryVersion( |
| local_state_, fields_prefs_->permanent_country_code_version); |
| stored_seed_info_.set_permanent_country_code( |
| permanent_country_version.country); |
| stored_seed_info_.set_permanent_version(permanent_country_version.version); |
| } else { |
| // Export seed data from Local State to a seed file in the following cases. |
| // 1. Seed file does not exist because this is the first run. For Windows, |
| // the first run seed may be stored in Local State, see |
| // https://crsrc.org/s?q=file:chrome_feature_list_creator.cc+symbol:SetupInitialPrefs. |
| // 2. Seed file does not exist because this is the first time a client is |
| // in the seed file experiment's treatment group. |
| // 3. Seed file exists and read failed. |
| std::string decoded_data; |
| std::string uncompressed_data; |
| bool decoded_successfully = base::Base64Decode( |
| local_state_->GetString(fields_prefs_->seed), &decoded_data); |
| // If the seed is empty, compression::GzipUncompress() will return false. |
| // However, we still want to write an empty seed to the file. |
| if (decoded_successfully && |
| (decoded_data.empty() || |
| compression::GzipUncompress(decoded_data, &uncompressed_data))) { |
| PermanentCountryVersion permanent_country_version = |
| GetPermanentCountryVersion( |
| local_state_, fields_prefs_->permanent_country_code_version); |
| ScheduleSeedFileWrite(ValidatedSeedInfo{ |
| .seed_data = uncompressed_data, |
| .signature = local_state_->GetString(fields_prefs_->signature), |
| .milestone = local_state_->GetInteger(fields_prefs_->milestone), |
| .seed_date = local_state_->GetTime(fields_prefs_->seed_date), |
| .client_fetch_time = |
| local_state_->GetTime(fields_prefs_->client_fetch_time), |
| .session_country_code = |
| local_state_->GetString(fields_prefs_->session_country_code), |
| .permanent_country_code = permanent_country_version.country, |
| .permanent_country_version = permanent_country_version.version, |
| }); |
| |
| // Record whether empty data is written to the seed file. This can happen |
| // in the following cases. |
| // 1. It is the first time a client is in the seed file experiment's |
| // treatment group. The seed file does not exist and the local state seed |
| // is empty. |
| // 2. It is not the first time a client is in the treatment group. A |
| // seed file exists, but cannot be read, and since local state is no |
| // longer maintained and has been cleared in previous runs, the local |
| // state seed written is cleared/ empty. |
| // 3. It is not the first time a client is in the treatment group. The |
| // seed file was deleted. |
| base::UmaHistogramBoolean( |
| base::StrCat( |
| {"Variations.SeedFileWriteEmptySeed.", histogram_suffix}), |
| decoded_data.empty()); |
| } else { |
| // If the seed data cannot be read from the Seed File or Local State, |
| // the seed data is empty. |
| stored_seed_data_ = ""; |
| } |
| } |
| |
| base::UmaHistogramBoolean( |
| base::StrCat({"Variations.SeedFileRead.", histogram_suffix}), success); |
| |
| // Clients using a seed file should clear seed from local state as it will no |
| // longer be used. |
| local_state_->ClearPref(fields_prefs_->seed); |
| } |
| |
| StoreSeedResult SeedReaderWriter::ScheduleLocalStateWrite( |
| ValidatedSeedInfo seed_info) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // If the seed data is empty or it's the sentinel value, store the given |
| // string without compressing. |
| std::string seed_data; |
| if (ShouldStoreWithoutProcessing(seed_info.seed_data)) { |
| seed_data = seed_info.seed_data; |
| } else { |
| std::string compressed_seed_data; |
| if (!compression::GzipCompress(seed_info.seed_data, |
| &compressed_seed_data)) { |
| return StoreSeedResult::kFailedGzip; |
| } |
| seed_data = base::Base64Encode(compressed_seed_data); |
| } |
| local_state_->SetString(fields_prefs_->seed, seed_data); |
| local_state_->SetString(fields_prefs_->signature, seed_info.signature); |
| local_state_->SetInteger(fields_prefs_->milestone, seed_info.milestone); |
| local_state_->SetTime(fields_prefs_->seed_date, seed_info.seed_date); |
| local_state_->SetTime(fields_prefs_->client_fetch_time, |
| seed_info.client_fetch_time); |
| if (!seed_info.session_country_code.empty()) { |
| local_state_->SetString(fields_prefs_->session_country_code, |
| seed_info.session_country_code); |
| } |
| // Version could be empty in case of the SafeSeed. |
| if (!seed_info.permanent_country_code.empty()) { |
| SetPermanentCountryVersion( |
| local_state_, fields_prefs_->permanent_country_code_version, |
| seed_info.permanent_country_code, seed_info.permanent_country_version); |
| } |
| return StoreSeedResult::kSuccess; |
| } |
| |
| bool SeedReaderWriter::ShouldUseSeedFile() const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Use the plain FieldTrialList API here because the trial is registered |
| // client-side in VariationsSeedStore SetUpSeedFileTrial(). |
| return seed_writer_ && |
| base::FieldTrialList::FindFullName(kSeedFileTrial) == kSeedFilesGroup; |
| } |
| |
| bool SeedReaderWriter::ShouldMigrateToLocalState( |
| version_info::Channel channel) const { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (channel == version_info::Channel::UNKNOWN || |
| channel == version_info::Channel::CANARY || |
| channel == version_info::Channel::DEV) { |
| return false; |
| } |
| return seed_writer_ && base::PathExists(seed_writer_->path()); |
| } |
| |
| void SeedReaderWriter::MigrateToLocalState() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::string seed_file_data; |
| const bool success = |
| base::ReadFileToString(seed_writer_->path(), &seed_file_data); |
| if (success && !seed_file_data.empty()) { |
| std::string base64_seed_data = base::Base64Encode(seed_file_data); |
| local_state_->SetString(fields_prefs_->seed, base64_seed_data); |
| } |
| DeleteSeedFile(); |
| } |
| |
| void SeedReaderWriter::ProcessStoredSeedDataAndRunCallback( |
| ReadSeedDataCallback done_callback, |
| SeedStorageFormat stored_seed_storage_format, |
| std::string stored_seed_data, |
| std::string stored_signature) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::string seed_data; |
| std::string base64_seed_signature; |
| LoadSeedResult result = ProcessStoredSeedData( |
| stored_seed_storage_format, stored_seed_data, stored_signature, |
| &seed_data, &base64_seed_signature); |
| std::move(done_callback) |
| .Run(ReadSeedDataResult{result, std::move(seed_data), |
| base64_seed_signature}); |
| } |
| |
| void SeedReaderWriter::GetSeedData(GetSeedDataCallback done_callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!ShouldUseSeedFile()) { |
| std::move(done_callback) |
| .Run(SeedStorageFormat::kCompressedAndBase64Encoded, |
| local_state_->GetString(fields_prefs_->seed), |
| local_state_->GetString(fields_prefs_->signature)); |
| return; |
| } |
| const std::string signature = |
| local_state_->GetString(fields_prefs_->signature); |
| if (stored_seed_data_.has_value()) { |
| // If the seed data is stored in memory, we can just run the callback |
| // with the stored data. This will copy the data, but can be run on the |
| // main thread. |
| std::move(done_callback) |
| .Run(SeedStorageFormat::kCompressed, stored_seed_data_.value(), |
| std::move(signature)); |
| return; |
| } |
| // If we're not keeping the seed data in memory, we need to read it from the |
| // file. This needs to be done asynchronously. |
| auto read_file_cb = [](GetSeedDataCallback done_callback, |
| std::string signature, std::string seed_data) { |
| std::move(done_callback) |
| .Run(SeedStorageFormat::kCompressed, std::move(seed_data), |
| std::move(signature)); |
| }; |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&ReadSeedFromFile, seed_writer_->path()), |
| base::BindOnce(read_file_cb, std::move(done_callback), |
| std::move(signature))); |
| } |
| |
| // TODO(crbug.com/433877973): Execute in background thread if sync is not |
| // required. |
| // static |
| LoadSeedResult SeedReaderWriter::ProcessStoredSeedData( |
| SeedStorageFormat storage_format, |
| std::string_view stored_seed_data, |
| std::string_view stored_seed_signature, |
| std::string* seed_data, |
| std::string* signature) { |
| if (stored_seed_data.empty()) { |
| return LoadSeedResult::kEmpty; |
| } |
| |
| // As a space optimization, the latest seed might not be stored directly, but |
| // rather aliased to the safe seed. We don't need to store the signature, |
| // since it is the same as the safe seed. |
| if (stored_seed_data == kIdenticalToSafeSeedSentinel) { |
| *seed_data = stored_seed_data; |
| return LoadSeedResult::kSuccess; |
| } |
| |
| std::string_view compressed_data; |
| std::string decoded_data; |
| switch (storage_format) { |
| case SeedStorageFormat::kCompressed: |
| compressed_data = stored_seed_data; |
| break; |
| // Because clients not using a seed file get seed data from local state |
| // instead, they need to decode the base64-encoded seed data first. |
| case SeedStorageFormat::kCompressedAndBase64Encoded: |
| if (!base::Base64Decode(stored_seed_data, &decoded_data)) { |
| return LoadSeedResult::kCorruptBase64; |
| } |
| compressed_data = decoded_data; |
| break; |
| } |
| |
| // A corrupt seed could result in a very large buffer being allocated which |
| // could crash the process. |
| // The maximum size of an uncompressed seed at 50 MiB. |
| constexpr std::size_t kMaxUncompressedSeedSize = 50 * 1024 * 1024; |
| if (compression::GetUncompressedSize(compressed_data) > |
| kMaxUncompressedSeedSize) { |
| return LoadSeedResult::kExceedsUncompressedSizeLimit; |
| } |
| if (!compression::GzipUncompress(compressed_data, seed_data)) { |
| return LoadSeedResult::kCorruptGzip; |
| } |
| |
| if (signature) { |
| *signature = stored_seed_signature; |
| } |
| |
| return LoadSeedResult::kSuccess; |
| } |
| |
| } // namespace variations |