0

Resolve android child Content-URIs by display-name

In GetFile(), GetDirectory() and Remove(), we must resolve the child
path by calling to ContentUriGetChildDocumentOrQuery(). If the path
already exists, we can use it as the backing path of the handle.

If the path does not exist, it is not possible to create the document
until all access checks have been done.  So to handle this, a
query URI is created which encodes the relevant information. This URI
will eventually be used by NativeFileUtil::EnsureFileExists() or
CreateDirectory() to call ContentUriGetDocumentFromQuery() to
create the document.

In DidGetFile() and DidGetDirectory() we check via
ContentUriIsCreateChildDocumentQuery() to detect if a query URI is
being used and we update the handle with the valid URI of the
newly created document.

Bug: 376097631
Change-Id: I5e4b22f6668e49bd1ddb760a044fb17fd0f02a7e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5981535
Reviewed-by: Nathan Memmott <memmott@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1379263}
This commit is contained in:
Joel Hockey
2024-11-06 21:39:05 +00:00
committed by Chromium LUCI CQ
parent 463c2f330b
commit 531424b7e3
5 changed files with 284 additions and 37 deletions

@ -14,7 +14,9 @@
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/uuid.h"
#include "build/build_config.h"
@ -37,6 +39,7 @@
#if BUILDFLAG(IS_ANDROID)
#include "base/android/content_uri_utils.h"
#include "net/base/mime_util.h"
#endif
using blink::mojom::FileSystemAccessEntry;
@ -130,6 +133,32 @@ void FileSystemAccessDirectoryHandleImpl::GetFile(const std::string& basename,
GetFileCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
#if BUILDFLAG(IS_ANDROID)
// Lookup content-URI by display-name. If the file does not exist, and
// `create` is set, ContentUriGetChildDocumentOrQuery() will return a
// create-child-document query URI which is used in
// NativeFileUtil::EnsureFileExists() to call ContentUriGetDocumentFromQuery()
// and create the document. DidGetFile() will then update the child path
// before creating the returned handle.
if (url().virtual_path().IsContentUri()) {
std::string mime_type;
std::string ext = base::FilePath(basename).Extension();
if (ext.empty() ||
!net::GetWellKnownMimeTypeFromExtension(ext.substr(1), &mime_type)) {
mime_type = "application/octet-stream";
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&base::ContentUriGetChildDocumentOrQuery,
url().virtual_path(), basename, mime_type,
/*is_directory=*/false, create),
base::BindOnce(
&FileSystemAccessDirectoryHandleImpl::OnGetFileContentUri,
weak_factory_.GetWeakPtr(), basename, create, std::move(callback)));
return;
}
#endif
storage::FileSystemURL child_url;
blink::mojom::FileSystemAccessErrorPtr get_child_url_result =
GetChildURL(basename, &child_url);
@ -137,6 +166,25 @@ void FileSystemAccessDirectoryHandleImpl::GetFile(const std::string& basename,
std::move(get_child_url_result), std::move(child_url));
}
#if BUILDFLAG(IS_ANDROID)
void FileSystemAccessDirectoryHandleImpl::OnGetFileContentUri(
std::string basename,
bool create,
GetFileCallback callback,
base::FilePath child_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (child_path.empty()) {
std::move(callback).Run(file_system_access_error::FromFileError(
base::File::FILE_ERROR_NOT_FOUND),
mojo::NullRemote());
return;
}
GetFileResolved(basename, create, std::move(callback),
file_system_access_error::Ok(), CreateChildURL(child_path));
}
#endif
void FileSystemAccessDirectoryHandleImpl::GetFileResolved(
const std::string& basename,
bool create,
@ -224,6 +272,27 @@ void FileSystemAccessDirectoryHandleImpl::GetDirectory(
GetDirectoryCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
#if BUILDFLAG(IS_ANDROID)
// Lookup content-URI by display-name. If the directory does not exist, and
// `create` is set, ContentUriGetChildDocumentOrQuery() will return a
// create-child-document query URI which is used in
// NativeFileUtil::CreateDirectory() to call ContentUriGetDocumentFromQuery()
// and create the document. DidGetDirectory() will then update the child path
// before creating the returned handle.
if (url().virtual_path().IsContentUri()) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&base::ContentUriGetChildDocumentOrQuery,
url().virtual_path(), basename,
/*mime_type=*/std::string(),
/*is_directory=*/true, create),
base::BindOnce(
&FileSystemAccessDirectoryHandleImpl::OnGetDirectoryContentUri,
weak_factory_.GetWeakPtr(), basename, create, std::move(callback)));
return;
}
#endif
storage::FileSystemURL child_url;
blink::mojom::FileSystemAccessErrorPtr get_child_url_result =
GetChildURL(basename, &child_url);
@ -231,6 +300,26 @@ void FileSystemAccessDirectoryHandleImpl::GetDirectory(
std::move(get_child_url_result), std::move(child_url));
}
#if BUILDFLAG(IS_ANDROID)
void FileSystemAccessDirectoryHandleImpl::OnGetDirectoryContentUri(
std::string basename,
bool create,
GetDirectoryCallback callback,
base::FilePath child_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (child_path.empty()) {
std::move(callback).Run(file_system_access_error::FromFileError(
base::File::FILE_ERROR_NOT_FOUND),
mojo::NullRemote());
return;
}
GetDirectoryResolved(basename, create, std::move(callback),
file_system_access_error::Ok(),
CreateChildURL(child_path));
}
#endif
void FileSystemAccessDirectoryHandleImpl::GetDirectoryResolved(
const std::string& basename,
bool create,
@ -342,6 +431,23 @@ void FileSystemAccessDirectoryHandleImpl::RemoveEntry(
RemoveEntryCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
#if BUILDFLAG(IS_ANDROID)
// Lookup content-URI by display-name.
if (url().virtual_path().IsContentUri()) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&base::ContentUriGetChildDocumentOrQuery,
url().virtual_path(), basename,
/*mime_type=*/std::string(),
/*is_directory=*/false, /*create=*/false),
base::BindOnce(
&FileSystemAccessDirectoryHandleImpl::OnRemoveEntryContentUri,
weak_factory_.GetWeakPtr(), basename, recurse,
std::move(callback)));
return;
}
#endif
storage::FileSystemURL child_url;
blink::mojom::FileSystemAccessErrorPtr get_child_url_result =
GetChildURL(basename, &child_url);
@ -349,6 +455,25 @@ void FileSystemAccessDirectoryHandleImpl::RemoveEntry(
std::move(get_child_url_result), std::move(child_url));
}
#if BUILDFLAG(IS_ANDROID)
void FileSystemAccessDirectoryHandleImpl::OnRemoveEntryContentUri(
std::string basename,
bool recurse,
RemoveEntryCallback callback,
base::FilePath child_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (child_path.empty()) {
std::move(callback).Run(file_system_access_error::FromFileError(
base::File::FILE_ERROR_NOT_FOUND));
return;
}
RemoveEntryResolved(basename, recurse, std::move(callback),
file_system_access_error::Ok(),
CreateChildURL(child_path));
}
#endif
void FileSystemAccessDirectoryHandleImpl::RemoveEntryResolved(
const std::string& basename,
bool recurse,
@ -481,11 +606,29 @@ void FileSystemAccessDirectoryHandleImpl::GetFileWithWritePermission(
}
void FileSystemAccessDirectoryHandleImpl::DidGetFile(
const storage::FileSystemURL& url,
storage::FileSystemURL child_url,
GetFileCallback callback,
base::File::Error result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
#if BUILDFLAG(IS_ANDROID)
// If getFile() was called with `create` for a non-existing file, then
// `child_url` will be a create-child-document URI which would have been used
// in NativeFileUtils::EnsureFileExists() to create the document, and we need
// to lookup the document URI.
base::FilePath child_path = child_url.path();
if (result == base::File::FILE_OK &&
base::ContentUriIsCreateChildDocumentQuery(child_path)) {
child_path =
base::ContentUriGetDocumentFromQuery(child_path, /*create=*/false);
if (child_path.empty()) {
result = base::File::FILE_ERROR_NOT_FOUND;
} else {
child_url = CreateChildURL(child_path);
}
}
#endif
if (result != base::File::FILE_OK) {
std::move(callback).Run(file_system_access_error::FromFileError(result),
mojo::NullRemote());
@ -494,7 +637,7 @@ void FileSystemAccessDirectoryHandleImpl::DidGetFile(
std::move(callback).Run(
file_system_access_error::Ok(),
manager()->CreateFileHandle(context(), url, handle_state()));
manager()->CreateFileHandle(context(), child_url, handle_state()));
}
void FileSystemAccessDirectoryHandleImpl::GetDirectoryWithWritePermission(
@ -514,11 +657,29 @@ void FileSystemAccessDirectoryHandleImpl::GetDirectoryWithWritePermission(
}
void FileSystemAccessDirectoryHandleImpl::DidGetDirectory(
const storage::FileSystemURL& url,
storage::FileSystemURL child_url,
GetDirectoryCallback callback,
base::File::Error result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
#if BUILDFLAG(IS_ANDROID)
// If getDirectory() was called with `create` for a non-existing file, then
// `child_url` will be a create-child-document URI which would have been used
// in NativeFileUtils::CreateDirectory() to create the dir, and we need
// to lookup the document URI.
base::FilePath child_path = child_url.path();
if (result == base::File::FILE_OK &&
base::ContentUriIsCreateChildDocumentQuery(child_path)) {
child_path =
base::ContentUriGetDocumentFromQuery(child_path, /*create=*/false);
if (child_path.empty()) {
result = base::File::FILE_ERROR_NOT_FOUND;
} else {
child_url = CreateChildURL(child_path);
}
}
#endif
if (result != base::File::FILE_OK) {
std::move(callback).Run(file_system_access_error::FromFileError(result),
mojo::NullRemote());
@ -527,7 +688,7 @@ void FileSystemAccessDirectoryHandleImpl::DidGetDirectory(
std::move(callback).Run(
file_system_access_error::Ok(),
manager()->CreateDirectoryHandle(context(), url, handle_state()));
manager()->CreateDirectoryHandle(context(), child_url, handle_state()));
}
void FileSystemAccessDirectoryHandleImpl::DidReadDirectory(
@ -722,13 +883,21 @@ FileSystemAccessDirectoryHandleImpl::GetChildURL(
base::FilePath child_path =
parent.virtual_path().Append(base::FilePath::FromUTF8Unsafe(basename));
#endif
*result = file_system_context()->CreateCrackedFileSystemURL(
parent.storage_key(), parent.mount_type(), child_path);
*result = CreateChildURL(child_path);
return file_system_access_error::Ok();
}
storage::FileSystemURL FileSystemAccessDirectoryHandleImpl::CreateChildURL(
const base::FilePath& child_path) {
const storage::FileSystemURL& parent = url();
storage::FileSystemURL child_url =
file_system_context()->CreateCrackedFileSystemURL(
parent.storage_key(), parent.mount_type(), child_path);
// Child URLs inherit their parent's storage bucket.
if (parent.bucket()) {
result->SetBucket(parent.bucket().value());
child_url.SetBucket(parent.bucket().value());
}
return file_system_access_error::Ok();
return child_url;
}
FileSystemAccessEntryPtr FileSystemAccessDirectoryHandleImpl::CreateEntry(

@ -85,6 +85,12 @@ class CONTENT_EXPORT FileSystemAccessDirectoryHandleImpl
storage::FileSystemURL* result);
private:
#if BUILDFLAG(IS_ANDROID)
void OnGetFileContentUri(std::string basename,
bool create,
GetFileCallback callback,
base::FilePath child_path);
#endif
void GetFileResolved(
const std::string& basename,
bool create,
@ -96,13 +102,19 @@ class CONTENT_EXPORT FileSystemAccessDirectoryHandleImpl
void GetFileWithWritePermission(const storage::FileSystemURL& child_url,
GetFileCallback callback);
void DoGetFile(bool create,
storage::FileSystemURL url,
storage::FileSystemURL child_url,
GetFileCallback callback,
FileSystemAccessPermissionContext::SensitiveEntryResult
sensitive_entry_result);
void DidGetFile(const storage::FileSystemURL& url,
void DidGetFile(storage::FileSystemURL child_url,
GetFileCallback callback,
base::File::Error result);
#if BUILDFLAG(IS_ANDROID)
void OnGetDirectoryContentUri(std::string basename,
bool create,
GetDirectoryCallback callback,
base::FilePath child_path);
#endif
void GetDirectoryResolved(
const std::string& basename,
bool create,
@ -113,7 +125,7 @@ class CONTENT_EXPORT FileSystemAccessDirectoryHandleImpl
// is the implementation for passing create=true to GetDirectory.
void GetDirectoryWithWritePermission(const storage::FileSystemURL& child_url,
GetDirectoryCallback callback);
void DidGetDirectory(const storage::FileSystemURL& url,
void DidGetDirectory(storage::FileSystemURL child_url,
GetDirectoryCallback callback,
base::File::Error result);
void DidReadDirectory(
@ -122,6 +134,12 @@ class CONTENT_EXPORT FileSystemAccessDirectoryHandleImpl
base::File::Error result,
std::vector<filesystem::mojom::DirectoryEntry> file_list,
bool has_more_entries);
#if BUILDFLAG(IS_ANDROID)
void OnRemoveEntryContentUri(std::string basename,
bool recurse,
RemoveEntryCallback callback,
base::FilePath child_path);
#endif
void RemoveEntryResolved(
const std::string& basename,
bool recurse,
@ -150,6 +168,8 @@ class CONTENT_EXPORT FileSystemAccessDirectoryHandleImpl
std::vector<blink::mojom::FileSystemAccessEntryPtr>)> final_callback,
std::vector<blink::mojom::FileSystemAccessEntryPtr> entries);
storage::FileSystemURL CreateChildURL(const base::FilePath& child_path);
// Helper to create a blink::mojom::FileSystemAccessEntry struct.
blink::mojom::FileSystemAccessEntryPtr CreateEntry(
const base::SafeBaseName& basename,

@ -56,6 +56,7 @@
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/content_uri_utils.h"
#include "base/android/path_utils.h"
#include "base/strings/escape.h"
#include "base/test/android/content_uri_test_utils.h"
@ -187,9 +188,14 @@ class FileSystemAccessFileHandleImplTest : public testing::Test {
: base::FilePath::FromUTF8Unsafe("test");
#if BUILDFLAG(IS_ANDROID)
if (use_content_uri) {
base::FilePath content_uri =
*base::test::android::GetContentUriFromCacheDirFilePath(
test_file_path);
base::FilePath parent =
*base::test::android::GetInMemoryContentTreeUriFromCacheDirDirectory(
dir_.GetPath());
base::FilePath content_uri = base::ContentUriGetChildDocumentOrQuery(
parent, test_file_path.BaseName().value(), "text/plain",
/*is_directory=*/false,
/*create=*/true);
ASSERT_TRUE(base::ContentUriIsCreateChildDocumentQuery(content_uri));
test_file_path = content_uri;
}
#endif

@ -184,6 +184,18 @@ base::File NativeFileUtil::CreateOrOpen(const base::FilePath& path,
base::File::Error NativeFileUtil::EnsureFileExists(const base::FilePath& path,
bool* created) {
#if !BUILDFLAG(IS_ANDROID)
if (!base::DirectoryExists(path.DirName())) {
// If its parent does not exist, should return NOT_FOUND error.
return base::File::FILE_ERROR_NOT_FOUND;
}
#endif
// If |path| is a directory, return an error.
if (base::DirectoryExists(path)) {
return base::File::FILE_ERROR_NOT_A_FILE;
}
#if BUILDFLAG(IS_ANDROID)
if (path.IsContentUri()) {
if (base::PathExists(path)) {
@ -192,27 +204,17 @@ base::File::Error NativeFileUtil::EnsureFileExists(const base::FilePath& path,
}
return base::File::FILE_OK;
}
base::File file(path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
if (file.IsValid()) {
if (created) {
*created = true;
}
return base::File::FILE_OK;
}
base::FilePath result =
base::ContentUriGetDocumentFromQuery(path, /*create=*/true);
if (created) {
*created = false;
*created = !result.empty();
}
return base::File::FILE_ERROR_NOT_FOUND;
if (result.empty()) {
return base::File::FILE_ERROR_FAILED;
}
return base::File::FILE_OK;
}
#endif
if (!base::DirectoryExists(path.DirName()))
// If its parent does not exist, should return NOT_FOUND error.
return base::File::FILE_ERROR_NOT_FOUND;
// If |path| is a directory, return an error.
if (base::DirectoryExists(path))
return base::File::FILE_ERROR_NOT_A_FILE;
// Tries to create the |path| exclusively. This should fail
// with base::File::FILE_ERROR_EXISTS if the path already exists.
@ -237,20 +239,39 @@ base::File::Error NativeFileUtil::EnsureFileExists(const base::FilePath& path,
base::File::Error NativeFileUtil::CreateDirectory(const base::FilePath& path,
bool exclusive,
bool recursive) {
#if !BUILDFLAG(IS_ANDROID)
// If parent dir of file doesn't exist.
if (!recursive && !base::PathExists(path.DirName()))
return base::File::FILE_ERROR_NOT_FOUND;
#endif
bool path_exists = base::PathExists(path);
if (exclusive && path_exists)
return base::File::FILE_ERROR_EXISTS;
// If file exists at the path.
if (path_exists && !base::DirectoryExists(path))
bool directory_exists = base::DirectoryExists(path);
if (path_exists && !directory_exists) {
return base::File::FILE_ERROR_NOT_A_DIRECTORY;
}
if (!base::CreateDirectory(path))
return base::File::FILE_ERROR_FAILED;
if (!directory_exists) {
#if BUILDFLAG(IS_ANDROID)
if (path.IsContentUri()) {
base::FilePath result =
base::ContentUriGetDocumentFromQuery(path, /*create=*/true);
if (result.empty()) {
return base::File::FILE_ERROR_FAILED;
}
} else if (!base::CreateDirectory(path)) {
return base::File::FILE_ERROR_FAILED;
}
#else
if (!base::CreateDirectory(path)) {
return base::File::FILE_ERROR_FAILED;
}
#endif
}
if (!SetPlatformSpecificDirectoryPermissions(path)) {
// Since some file systems don't support permission setting, we do not treat

@ -16,6 +16,7 @@
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/content_uri_utils.h"
#include "base/test/android/content_uri_test_utils.h"
#endif
@ -125,9 +126,13 @@ TEST_F(NativeFileUtilTest, EnsureFileExists) {
#if BUILDFLAG(IS_ANDROID)
// Delete file and recreate using content-URI rather than path.
ASSERT_TRUE(base::DeleteFile(file_name));
base::FilePath content_uri =
*base::test::android::GetContentUriFromCacheDirFilePath(file_name);
base::FilePath parent =
*base::test::android::GetInMemoryContentTreeUriFromCacheDirDirectory(
Path());
base::FilePath content_uri = base::ContentUriGetChildDocumentOrQuery(
parent, "foobar", "text/plain", /*is_directory=*/false,
/*create=*/true);
ASSERT_FALSE(content_uri.empty());
EXPECT_EQ(base::File::FILE_OK,
NativeFileUtil::EnsureFileExists(content_uri, &created));
@ -158,6 +163,32 @@ TEST_F(NativeFileUtilTest, CreateAndDeleteDirectory) {
ASSERT_EQ(base::File::FILE_OK, NativeFileUtil::DeleteDirectory(dir_name));
EXPECT_FALSE(base::DirectoryExists(dir_name));
EXPECT_FALSE(NativeFileUtil::DirectoryExists(dir_name));
#if BUILDFLAG(IS_ANDROID)
base::FilePath parent =
*base::test::android::GetInMemoryContentTreeUriFromCacheDirDirectory(
Path());
base::FilePath query = base::ContentUriGetChildDocumentOrQuery(
parent, "test_dir", "", /*is_directory=*/true, /*create=*/true);
ASSERT_FALSE(query.empty());
ASSERT_EQ(base::File::FILE_OK,
NativeFileUtil::CreateDirectory(query, false /* exclusive */,
false /* recursive */));
base::FilePath content_uri =
base::ContentUriGetDocumentFromQuery(query, /*create=*/false);
ASSERT_FALSE(content_uri.empty());
EXPECT_TRUE(NativeFileUtil::DirectoryExists(content_uri));
EXPECT_TRUE(base::DirectoryExists(dir_name));
ASSERT_EQ(base::File::FILE_ERROR_EXISTS,
NativeFileUtil::CreateDirectory(content_uri, true /* exclusive */,
false /* recursive */));
ASSERT_EQ(base::File::FILE_OK, NativeFileUtil::DeleteDirectory(content_uri));
EXPECT_FALSE(NativeFileUtil::DirectoryExists(content_uri));
EXPECT_FALSE(base::DirectoryExists(dir_name));
#endif
}
// TODO(crbug.com/40511450): Remove this test once last_access_time has