// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/file_manager/extract_io_task.h"
#include <grp.h>
#include <optional>
#include <utility>
#include "base/check_op.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/system/sys_info.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/filesystem_api_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/fileapi/file_system_backend.h"
#include "chrome/browser/platform_util.h"
#include "components/file_access/scoped_file_access.h"
#include "components/services/unzip/content/unzip_service.h"
#include "components/services/unzip/public/mojom/unzipper.mojom.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/cros_system_api/constants/cryptohome.h"
#include "third_party/zlib/google/redact.h"
namespace file_manager {
namespace io_task {
void RecordUmaExtractStatus(ExtractStatus status) {
UMA_HISTOGRAM_ENUMERATION(kExtractTaskStatusHistogramName, status);
}
ExtractIOTask::ExtractIOTask(
std::vector<storage::FileSystemURL> source_urls,
std::string password,
storage::FileSystemURL parent_folder,
Profile* profile,
scoped_refptr<storage::FileSystemContext> file_system_context,
bool show_notification)
: IOTask(show_notification),
source_urls_(std::move(source_urls)),
password_(std::move(password)),
parent_folder_(std::move(parent_folder)),
profile_(profile),
file_system_context_(std::move(file_system_context)) {
progress_.type = OperationType::kExtract;
progress_.state = State::kQueued;
progress_.SetDestinationFolder(parent_folder_, profile);
progress_.bytes_transferred = 0;
progress_.total_bytes = 0;
// Store all the ZIP files in the selection so we have
// a proper count of how many need to be extracted.
for (const storage::FileSystemURL& source_url : source_urls_) {
const base::FilePath source_path = source_url.path();
if (source_path.MatchesExtension(".zip") &&
ash::FileSystemBackend::CanHandleURL(source_url)) {
progress_.sources.emplace_back(source_url, std::nullopt);
}
}
sizingCount_ = extractCount_ = progress_.sources.size();
}
ExtractIOTask::~ExtractIOTask() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
void ExtractIOTask::ZipListenerCallback(uint64_t bytes) {
if (speedometer_.Update(progress_.bytes_transferred += bytes)) {
const base::TimeDelta remaining_time = speedometer_.GetRemainingTime();
// Speedometer can produce infinite result which can't be serialized to JSON
// when sending the status via private API.
if (!remaining_time.is_inf()) {
progress_.remaining_seconds = remaining_time.InSecondsF();
}
}
progress_callback_.Run(progress_);
}
void ExtractIOTask::FinishedExtraction(base::FilePath directory, bool success) {
if (success) {
// Open a new window to show the extracted content.
platform_util::ShowItemInFolder(profile_, directory);
} else {
any_archive_failed_ = true;
}
DCHECK_GT(extractCount_, 0u);
if (--extractCount_ == 0) {
cancellation_chain_ = base::DoNothing();
progress_.state = any_archive_failed_ ? State::kError : State::kSuccess;
RecordUmaExtractStatus(any_archive_failed_ ? ExtractStatus::kUnknownError
: ExtractStatus::kSuccess);
Complete();
}
}
std::optional<gid_t> GetDirectoriesOwnerGid() {
struct group grp, *result = nullptr;
std::vector<char> buffer(16384);
getgrnam_r("chronos-access", &grp, buffer.data(), buffer.size(), &result);
if (!result) {
return std::nullopt;
}
return grp.gr_gid;
}
// Recursively walk directory and set 'u+rwx,g+rx,o+x'.
bool SetDirectoryPermissions(base::FilePath directory, bool success) {
// Always set permissions in case of error mid-extract.
base::FileEnumerator traversal(directory, true,
base::FileEnumerator::DIRECTORIES);
const std::optional<gid_t> owner_gid = GetDirectoriesOwnerGid();
for (base::FilePath current = traversal.Next(); !current.empty();
current = traversal.Next()) {
base::SetPosixFilePermissions(
current,
base::FILE_PERMISSION_READ_BY_USER | // "rwxr-x--x".
base::FILE_PERMISSION_WRITE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_USER |
base::FILE_PERMISSION_READ_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_OTHERS);
// Might not exist in tests.
if (owner_gid.has_value()) {
HANDLE_EINTR(chown(current.value().c_str(), -1, owner_gid.value()));
}
}
return success;
}
void ExtractIOTask::ZipExtractCallback(base::FilePath destination_directory,
bool success) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&SetDirectoryPermissions, destination_directory, success),
base::BindOnce(&ExtractIOTask::FinishedExtraction,
weak_ptr_factory_.GetWeakPtr(), destination_directory));
}
void ExtractIOTask::ExtractIntoNewDirectory(
base::FilePath destination_directory,
base::FilePath source_file,
bool created_ok) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (created_ok) {
// Accumulate the new cancellation callback into the cancellation chain.
cancellation_chain_ =
unzip::Unzip(unzip::LaunchUnzipper(), source_file,
destination_directory,
unzip::mojom::UnzipOptions::New("auto", password_),
unzip::AllContents(),
base::BindRepeating(&ExtractIOTask::ZipListenerCallback,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&ExtractIOTask::ZipExtractCallback,
weak_ptr_factory_.GetWeakPtr(),
destination_directory))
.Then(std::move(cancellation_chain_));
} else {
LOG(ERROR) << "Cannot create directory "
<< zip::Redact(destination_directory);
ZipExtractCallback(base::FilePath(), false);
}
}
bool CreateExtractionDirectory(const base::FilePath& destination_directory) {
if (!base::CreateDirectory(destination_directory)) {
return false;
}
if (base::StartsWith(destination_directory.value(),
file_manager::util::kFuseBoxMediaSlashPath)) {
// Fusebox files wrap Chromium's SBFS (//storage/browser/file_system) API
// and the SBFS cross-platform abstraction doesn't expose Unix-style rwx
// permission bits or owner:group chown-ership fields. The Fusebox server
// offers synthetic "rwxrwx---" mode bits (for directories) but trying to
// chmod that to something different will fail. Still, while "rwxrwx---" is
// not exactly equal to "rwxr-x--x", it's good enough for many purposes.
// For Fusebox paths, we just return early (with success) instead of trying
// to chmod and chown the freshly created directory.
return true;
}
// Make sure the directory is world readable.
if (!base::SetPosixFilePermissions(
destination_directory,
base::FILE_PERMISSION_READ_BY_USER | // "rwxr-x--x".
base::FILE_PERMISSION_WRITE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_USER |
base::FILE_PERMISSION_READ_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_GROUP |
base::FILE_PERMISSION_EXECUTE_BY_OTHERS)) {
return false;
}
const std::optional<gid_t> owner_gid = GetDirectoriesOwnerGid();
if (!owner_gid.has_value()) {
// Might not exist in tests.
} else if (HANDLE_EINTR(chown(destination_directory.value().c_str(), -1,
owner_gid.value()))) {
return false;
}
return true;
}
void ExtractIOTask::ExtractArchive(
size_t index,
base::FileErrorOr<storage::FileSystemURL> destination_result) {
DCHECK(index < progress_.sources.size());
const base::FilePath source_file = progress_.sources[index].url.path();
if (!destination_result.has_value()) {
ZipExtractCallback(base::FilePath(), false);
} else {
progress_.outputs.emplace_back(destination_result.value(), std::nullopt,
progress_.sources[index].url);
const base::FilePath destination_directory =
destination_result.value().path();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&CreateExtractionDirectory, destination_directory),
base::BindOnce(&ExtractIOTask::ExtractIntoNewDirectory,
weak_ptr_factory_.GetWeakPtr(), destination_directory,
source_file));
}
}
void ExtractIOTask::ExtractAllSources() {
for (size_t index = 0; index < progress_.sources.size(); ++index) {
const EntryStatus& source = progress_.sources[index];
const base::FilePath source_file = source.url.path().BaseName();
util::GenerateUnusedFilename(
parent_folder_, source_file.RemoveExtension(), file_system_context_,
base::BindOnce(&ExtractIOTask::ExtractArchive,
weak_ptr_factory_.GetWeakPtr(), index));
}
}
void ExtractIOTask::GotFreeDiskSpace(int64_t free_space) {
auto* drive_integration_service =
drive::util::GetIntegrationServiceByProfile(profile_);
if (progress_.GetDestinationFolder().filesystem_id() ==
util::GetDownloadsMountPointName(profile_) ||
(drive_integration_service &&
drive_integration_service->GetMountPointPath().IsParent(
progress_.GetDestinationFolder().path()))) {
free_space -= cryptohome::kMinFreeSpaceInBytes;
}
if (progress_.total_bytes > free_space) {
progress_.outputs.emplace_back(progress_.GetDestinationFolder(),
base::File::FILE_ERROR_NO_SPACE);
progress_.state = State::kError;
RecordUmaExtractStatus(ExtractStatus::kInsufficientDiskSpace);
Complete();
return;
}
if (have_encrypted_content_ && password_.empty()) {
if (uses_aes_encryption_) {
RecordUmaExtractStatus(ExtractStatus::kAesEncrypted);
} else {
RecordUmaExtractStatus(ExtractStatus::kPasswordError);
}
progress_.state = State::kNeedPassword;
Complete();
return;
}
speedometer_.SetTotalBytes(progress_.total_bytes);
ExtractAllSources();
}
void ExtractIOTask::ZipInfoCallback(unzip::mojom::InfoPtr info) {
DCHECK_GT(extractCount_, 0u);
if (info->size_is_valid) {
progress_.total_bytes += info->size;
}
have_encrypted_content_ = have_encrypted_content_ || info->is_encrypted;
uses_aes_encryption_ = info->uses_aes_encryption;
if (--sizingCount_ == 0) {
// After getting the size of all the ZIPs, check if we have
// enough available disk space, and if so, extract them.
if (!parent_folder_.TypeImpliesPathIsReal()) {
// Destination is a virtual filesystem, so skip the size check.
ExtractAllSources();
} else {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
parent_folder_.path()),
base::BindOnce(&ExtractIOTask::GotFreeDiskSpace,
weak_ptr_factory_.GetWeakPtr()));
}
}
}
void ExtractIOTask::GetExtractedSize(base::FilePath source_file) {
unzip::GetExtractedInfo(unzip::LaunchUnzipper(), source_file,
base::BindOnce(&ExtractIOTask::ZipInfoCallback,
weak_ptr_factory_.GetWeakPtr()));
}
void ExtractIOTask::CheckSizeThenExtract() {
for (const EntryStatus& source : progress_.sources) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ExtractIOTask::GetExtractedSize,
weak_ptr_factory_.GetWeakPtr(), source.url.path()));
}
}
void ExtractIOTask::GotScopedFileAccess(
file_access::ScopedFileAccess file_access) {
file_access_ = std::move(file_access);
CheckSizeThenExtract();
}
void ExtractIOTask::GetScopedFileAccess() {
std::vector<base::FilePath> zip_files;
for (const EntryStatus& source : progress_.sources) {
zip_files.push_back(source.url.path());
}
file_access::RequestFilesAccessForSystem(
{zip_files}, base::BindOnce(&ExtractIOTask::GotScopedFileAccess,
weak_ptr_factory_.GetWeakPtr()));
}
void ExtractIOTask::Execute(IOTask::ProgressCallback progress_callback,
IOTask::CompleteCallback complete_callback) {
progress_callback_ = std::move(progress_callback);
complete_callback_ = std::move(complete_callback);
DVLOG(1) << "Executing EXTRACT_ARCHIVE IO task";
progress_.state = State::kInProgress;
progress_callback_.Run(progress_);
// If the backend can't handle the folder to unpack into or
// there are no files to extract, finish the operation with an error.
if (!ash::FileSystemBackend::CanHandleURL(parent_folder_) ||
sizingCount_ == 0) {
progress_.state = State::kError;
RecordUmaExtractStatus(ExtractStatus::kUnknownError);
Complete();
} else {
GetScopedFileAccess();
}
}
void ExtractIOTask::Cancel() {
progress_.state = State::kCancelled;
RecordUmaExtractStatus(ExtractStatus::kCancelled);
std::move(cancellation_chain_).Run();
cancellation_chain_ = base::DoNothing();
}
// Calls the completion callback for the task. |progress_| should not be
// accessed after calling this.
void ExtractIOTask::Complete() {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(complete_callback_), std::move(progress_)));
}
} // namespace io_task
} // namespace file_manager