// 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.
#ifndef CHROME_BROWSER_ASH_FUSEBOX_FUSEBOX_SERVER_H_
#define CHROME_BROWSER_ASH_FUSEBOX_FUSEBOX_SERVER_H_
#include <string>
#include "base/containers/circular_deque.h"
#include "base/files/file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/threading/sequence_bound.h"
#include "base/values.h"
#include "chrome/browser/ash/fusebox/fusebox.pb.h"
#include "chrome/browser/ash/fusebox/fusebox_histograms.h"
#include "chrome/browser/ash/fusebox/fusebox_moniker.h"
#include "chrome/browser/ash/system_web_apps/apps/files_internals_debug_json_provider.h"
#include "storage/browser/file_system/async_file_util.h"
#include "storage/browser/file_system/file_system_context.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
class Profile;
namespace fusebox {
class ReadWriter;
class Server : public ash::FilesInternalsDebugJSONProvider {
public:
struct Delegate {
// These methods cause D-Bus signals to be sent that a storage unit (as
// named by the "subdir" in "/media/fuse/fusebox/subdir") has been attached
// or detached.
virtual void OnRegisterFSURLPrefix(const std::string& subdir) = 0;
virtual void OnUnregisterFSURLPrefix(const std::string& subdir) = 0;
};
// Returns a pointer to the global Server instance.
static Server* GetInstance();
// Returns POSIX style (S_IFREG | rwxr-x---) bits.
static uint32_t MakeModeBits(bool is_directory, bool read_only);
// The delegate should live longer than the server.
explicit Server(Delegate* delegate);
Server(const Server&) = delete;
Server& operator=(const Server&) = delete;
~Server() override;
// Manages monikers in the context of the Server's MonikerMap.
fusebox::Moniker CreateMoniker(const storage::FileSystemURL& target,
bool read_only);
void DestroyMoniker(fusebox::Moniker moniker);
void RegisterFSURLPrefix(const std::string& subdir,
const std::string& fs_url_prefix,
bool read_only);
void UnregisterFSURLPrefix(const std::string& subdir);
// Converts a FuseBox filename (e.g. "/media/fuse/fusebox/subdir/p/q.txt") to
// a storage::FileSystemURL, substituting the fs_url_prefix for "/etc/subdir"
// according to previous RegisterFSURLPrefix calls. The "/p/q.txt" suffix may
// be empty but "subdir" (and everything prior) must be present.
//
// If "subdir" mapped to "filesystem:origin/external/mount_name/xxx/yyy" then
// this returns "filesystem:origin/external/mount_name/xxx/yyy/p/q.txt" in
// storage::FileSystemURL form.
//
// It returns an invalid storage::FileSystemURL if the filename doesn't match
// "/media/fuse/fusebox/subdir/etc" or the "subdir" wasn't registered.
storage::FileSystemURL ResolveFilename(Profile* profile,
const std::string& filename);
// Performs the inverse of ResolveFilename. It converts a FileSystemURL like
// "filesystem:origin/external/mount_name/xxx/yyy/p/q.txt" to a FuseBox
// filename like "/media/fuse/fusebox/subdir/p/q.txt".
//
// It returns an empty base::FilePath on failure, such as when there was no
// previously registered (subdir, fs_url_prefix) that matched.
base::FilePath InverseResolveFSURL(const storage::FileSystemURL& fs_url);
// Chains GetInstance and InverseResolveFSURL, returning an empty
// base::FilePath when there is no instance.
static base::FilePath SubstituteFuseboxFilePath(
const storage::FileSystemURL& fs_url) {
Server* server = GetInstance();
return server ? server->InverseResolveFSURL(fs_url) : base::FilePath();
}
// ash::FilesInternalsDebugJSONProvider overrides.
void GetDebugJSONForKey(
std::string_view key,
base::OnceCallback<void(JSONKeyValuePair)> callback) override;
// These methods map 1:1 to the D-Bus methods implemented by
// fusebox_service_provider.cc.
//
// For the "file operation D-Bus methods" (below until "Meta D-Bus methods")
// in terms of semantics, they're roughly equivalent to the C standard
// library functions of the same name. For example, the Stat method here
// corresponds to the standard stat function described by "man 2 stat".
//
// These methods all take a protobuf argument and return (via a callback)
// another protobuf. Many of the request protos have a string-typed
// file_system_url field, roughly equivalent to a POSIX filename for a file
// or directory. These used to be full storage::FileSystemURL strings (e.g.
// "filesystem:chrome://file-manager/external/foo/com.bar/baz/p/q.txt") but
// today look like "subdir/p/q.txt". The PrefixMap is used to resolve the
// "subdir" prefix to recreate the storage::FileSystemURL.
//
// See system_api/dbus/fusebox/fusebox.proto for more commentary.
// Close2 closes a virtual file opened by Open2.
using Close2Callback =
base::OnceCallback<void(const Close2ResponseProto& response)>;
void Close2(const Close2RequestProto& request, Close2Callback callback);
// Create creates a file (not a directory).
using CreateCallback =
base::OnceCallback<void(const CreateResponseProto& response)>;
void Create(const CreateRequestProto& request, CreateCallback callback);
// Flush flushes a file, like the C standard library's fsync.
using FlushCallback =
base::OnceCallback<void(const FlushResponseProto& response)>;
void Flush(const FlushRequestProto& request, FlushCallback callback);
// MkDir is analogous to "/usr/bin/mkdir".
using MkDirCallback =
base::OnceCallback<void(const MkDirResponseProto& response)>;
void MkDir(const MkDirRequestProto& request, MkDirCallback callback);
// Open2 opens a virtual file for reading and/or writing.
using Open2Callback =
base::OnceCallback<void(const Open2ResponseProto& response)>;
void Open2(const Open2RequestProto& request, Open2Callback callback);
// Rename is analogous to "/usr/bin/mv".
using RenameCallback =
base::OnceCallback<void(const RenameResponseProto& response)>;
void Rename(const RenameRequestProto& request, RenameCallback callback);
// Read2 reads from a virtual file opened by Open2.
using Read2Callback =
base::OnceCallback<void(const Read2ResponseProto& response)>;
void Read2(const Read2RequestProto& request, Read2Callback callback);
// ReadDir2 lists the directory's children.
using ReadDir2Callback =
base::OnceCallback<void(const ReadDir2ResponseProto& response)>;
void ReadDir2(const ReadDir2RequestProto& request, ReadDir2Callback callback);
// RmDir is analogous to "/usr/bin/rmdir".
using RmDirCallback =
base::OnceCallback<void(const RmDirResponseProto& response)>;
void RmDir(const RmDirRequestProto& request, RmDirCallback callback);
// Stat2 returns the file or directory's metadata.
using Stat2Callback =
base::OnceCallback<void(const Stat2ResponseProto& response)>;
void Stat2(const Stat2RequestProto& request, Stat2Callback callback);
// Truncate sets a file's size.
using TruncateCallback =
base::OnceCallback<void(const TruncateResponseProto& response)>;
void Truncate(const TruncateRequestProto& request, TruncateCallback callback);
// Unlink deletes a file.
using UnlinkCallback =
base::OnceCallback<void(const UnlinkResponseProto& response)>;
void Unlink(const UnlinkRequestProto& request, UnlinkCallback callback);
// Write2 writes to a virtual file opened by Open2.
using Write2Callback =
base::OnceCallback<void(const Write2ResponseProto& response)>;
void Write2(const Write2RequestProto& request, Write2Callback callback);
// File operation D-Bus methods above. Meta D-Bus methods below, which do not
// map 1:1 to FUSE or C standard library file operations.
// ListStorages returns the active subdir names. Active means passed to
// RegisterFSURLPrefix without a subsequent UnregisterFSURLPrefix.
using ListStoragesCallback =
base::OnceCallback<void(const ListStoragesResponseProto& response)>;
void ListStorages(const ListStoragesRequestProto& request,
ListStoragesCallback callback);
// MakeTempDir makes a temporary directory that has two file paths: an
// underlying one (e.g. "/tmp/.foo") and a fusebox one (e.g.
// "/media/fuse/fusebox/tmp.foo"). The fusebox one is conceptually similar to
// a symbolic link, in that after a "touch /tmp/.foo/bar", bar should be
// visible at "/media/fuse/fusebox/tmp.foo/bar", but the 'symbolic link' is
// not resolved directly by the kernel.
//
// Instead, file I/O under the /media/fuse/fusebox mount point goes through
// the FuseBox daemon (via FUSE) to Chromium (via D-Bus) to the kernel (as
// Chromium storage::FileSystemURL code sees storage::kFileSystemTypeLocal
// files living under the underlying file path).
//
// That sounds convoluted (and overkill for 'sym-linking' a directory on the
// local file system), and it is, but it is essentially the same code paths
// that FuseBox uses to surface Chromium virtual file systems (VFSs) that are
// not otherwise visible on the kernel-level file system. Note that Chromium
// VFSs are not the same as Linux kernel VFSs.
//
// The purpose of these Make/Remove methods is to facilitate testing these
// FuseBox code paths (backed by an underlying tmpfs file system) without the
// extra complexity of fake VFSs, such as a fake ADP (Android Documents
// Provider) or fake MTP (Media Transfer Protocol) back-end.
//
// MakeTempDir is like "mkdir" (except the callee randomly generates the file
// path). RemoveTempDir is like "rm -rf".
using MakeTempDirCallback =
base::OnceCallback<void(const std::string& error_message,
const std::string& fusebox_file_path,
const std::string& underlying_file_path)>;
void MakeTempDir(MakeTempDirCallback callback);
void RemoveTempDir(const std::string& fusebox_file_path);
// ----
using PendingFlush = std::pair<FlushRequestProto, FlushCallback>;
using PendingRead2 = std::pair<Read2RequestProto, Read2Callback>;
using PendingWrite2 = std::pair<Write2RequestProto, Write2Callback>;
using PendingOp = absl::variant<PendingFlush, PendingRead2, PendingWrite2>;
struct FuseFileMapEntry {
FuseFileMapEntry(scoped_refptr<storage::FileSystemContext> fs_context_arg,
storage::FileSystemURL fs_url_arg,
const std::string& profile_path_arg,
bool readable_arg,
bool writable_arg,
bool use_temp_file_arg,
bool temp_file_starts_with_copy_arg);
FuseFileMapEntry(FuseFileMapEntry&&);
~FuseFileMapEntry();
void DoFlush(const FlushRequestProto& request, FlushCallback callback);
void DoRead2(const Read2RequestProto& request, Read2Callback callback);
void DoWrite2(const Write2RequestProto& request, Write2Callback callback);
void Do(PendingOp& op,
base::WeakPtr<Server> weak_ptr_server,
uint64_t fuse_handle);
const scoped_refptr<storage::FileSystemContext> fs_context_;
const HistogramEnumFileSystemType histogram_enum_file_system_type_;
const bool readable_;
const bool writable_;
bool has_in_flight_op_ = false;
base::circular_deque<PendingOp> pending_ops_;
base::SequenceBound<ReadWriter> seqbnd_read_writer_;
};
// Maps from fuse_handle uint64_t values to FileStreamReader /
// FileStreamWriter state.
using FuseFileMap = std::map<uint64_t, FuseFileMapEntry>;
struct PrefixMapEntry {
PrefixMapEntry(std::string fs_url_prefix_arg, bool read_only_arg);
std::string fs_url_prefix;
bool read_only;
};
// Maps from a subdir to a storage::FileSystemURL prefix in string form (and
// other metadata). For example, the subdir could be the "foo" in the
// "/media/fuse/fusebox/foo/bar/baz.txt" filename, which gets mapped to
// "fs_url_prefix/bar/baz.txt" before that whole string is parsed as a
// storage::FileSystemURL.
//
// Neither subdir nor fs_url_prefix should have a trailing slash.
using PrefixMap = std::map<std::string, PrefixMapEntry>;
struct ReadDir2MapEntry {
explicit ReadDir2MapEntry(ReadDir2Callback callback);
ReadDir2MapEntry(ReadDir2MapEntry&&);
~ReadDir2MapEntry();
// Returns whether the final response was sent.
bool Reply(uint64_t cookie, ReadDir2Callback callback);
int32_t posix_error_code_ = 0;
ReadDir2ResponseProto response_;
bool has_more_ = true;
ReadDir2Callback callback_;
};
// Maps from ReadDir2 cookies to a pair of (1) a buffer of upstream results
// from Chromium's storage layer and (2) a possibly-hasnt-arrived-yet pending
// downstream ReadDir2Callback (i.e. a D-Bus RPC response).
//
// If the upstream layer sends its results first then we need to buffer until
// we have a downstream callback to pass those results onto.
//
// If the downstream layer sends its callback first then we need to hold onto
// it until we have results to pass on.
//
// Note that the upstream API works with a base::RepeatingCallback model (one
// request, multiple responses) but the downstream API (i.e. D-Bus) works
// with a base::OnceCallback model (N requests, N responses).
using ReadDir2Map = std::map<uint64_t, ReadDir2MapEntry>;
// Maps from a fusebox_file_path (like "/media/fuse/fusebox/tmp.foo") to the
// ScopedTempDir that will clean up (in its destructor) the underlying
// temporary directory.
using TempSubdirMap = std::map<std::string, base::ScopedTempDir>;
// ----
private:
void ReplyToMakeTempDir(base::ScopedTempDir scoped_temp_dir,
bool create_succeeded,
MakeTempDirCallback callback);
void OnFlush(uint64_t fuse_handle,
FlushCallback callback,
const FlushResponseProto& response);
void OnRead2(uint64_t fuse_handle,
Read2Callback callback,
const Read2ResponseProto& response);
void OnReadDirectory(scoped_refptr<storage::FileSystemContext> fs_context,
bool read_only,
uint64_t cookie,
base::File::Error error_code,
storage::AsyncFileUtil::EntryList entry_list,
bool has_more);
void OnWrite2(uint64_t fuse_handle,
Write2Callback callback,
const Write2ResponseProto& response);
// Removes the entry (if present) for the given map key.
void EraseFuseFileMapEntry(uint64_t fuse_handle);
// Returns the fuse_handle that is the map key.
uint64_t InsertFuseFileMapEntry(FuseFileMapEntry&& entry);
raw_ptr<Delegate> delegate_;
FuseFileMap fuse_file_map_;
fusebox::MonikerMap moniker_map_;
PrefixMap prefix_map_;
ReadDir2Map read_dir_2_map_;
TempSubdirMap temp_subdir_map_;
base::WeakPtrFactory<Server> weak_ptr_factory_{this};
};
} // namespace fusebox
#endif // CHROME_BROWSER_ASH_FUSEBOX_FUSEBOX_SERVER_H_