blob: 57992de7b78f79088cd308b6fc7abc8182948287 [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/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/sequenced_task_runner.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"
namespace variations {
namespace {
// 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(std::string seed_data) {
// TODO(crbug.com/370480037): Begin doing seed compression here instead of in
// VariationsSeedStore.
return seed_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 ||
channel == version_info::Channel::BETA ||
channel == version_info::Channel::STABLE;
}
// 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);
}
} // namespace
SeedReaderWriter::SeedReaderWriter(
PrefService* local_state,
const base::FilePath& seed_file_dir,
base::FilePath::StringViewType seed_filename,
std::string_view seed_pref,
version_info::Channel channel,
const EntropyProviders* entropy_providers,
scoped_refptr<base::SequencedTaskRunner> file_task_runner)
: local_state_(local_state),
seed_pref_(seed_pref),
file_task_runner_(std::move(file_task_runner)) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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();
}
}
}
SeedReaderWriter::~SeedReaderWriter() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (seed_writer_ && seed_writer_->HasPendingWrite()) {
seed_writer_->DoScheduledWrite();
}
}
void SeedReaderWriter::StoreValidatedSeed(std::string_view compressed_seed_data,
std::string_view base64_seed_data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (ShouldUseSeedFile()) {
ScheduleSeedFileWrite(compressed_seed_data);
} else {
local_state_->SetString(seed_pref_, base64_seed_data);
}
}
void SeedReaderWriter::ClearSeed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(crbug.com/372009105): Remove if-statements when experiment has ended.
if (ShouldUseSeedFile()) {
ScheduleSeedFileWrite(std::string());
} else {
local_state_->ClearPref(seed_pref_);
// 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();
}
}
}
StoredSeed SeedReaderWriter::GetSeedData() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (ShouldUseSeedFile()) {
return StoredSeed{.storage_format = StoredSeed::StorageFormat::kCompressed,
.data = seed_info_.data};
} else {
return StoredSeed{
.storage_format =
StoredSeed::StorageFormat::kCompressedAndBase64Encoded,
.data = local_state_->GetString(seed_pref_)};
}
}
void SeedReaderWriter::SetTimerForTesting(base::OneShotTimer* timer_override) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (seed_writer_) {
seed_writer_->SetTimerForTesting(timer_override); // IN-TEST
}
}
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.
// TODO(crbug.com/370539202) Potentially use std::move instead of copy if we
// are able to move seed data out of memory.
return base::BindOnce(&DoSerialize, seed_info_.data);
}
void SeedReaderWriter::ScheduleSeedFileWrite(std::string_view seed_data) {
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.
seed_info_.data = seed_data;
// `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);
}
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) {
seed_info_.data = std::move(seed_file_data);
} 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;
if (base::Base64Decode(local_state_->GetString(seed_pref_),
&decoded_data)) {
// Write will only occur if ShouldUseSeedFile() is true.
ScheduleSeedFileWrite(decoded_data);
// 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());
}
}
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(seed_pref_);
}
bool SeedReaderWriter::ShouldUseSeedFile() const {
// Use the plain FieldTrialList API here because the trial is registered
// client-side in VariationsSeedStore SetUpSeedFileTrial().
return seed_writer_ &&
base::FieldTrialList::FindFullName(kSeedFileTrial) == kSeedFilesGroup;
}
} // namespace variations