blob: 022500ac2e766da98c868257658e0496be956dc4 [file] [log] [blame]
// 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