Prototype of Files service.
Defines two important interfaces, Directory and File. Provides a basic,
single-threaded implementation of the latter (with a few omissions), an
extremely incomplete implementation of the former (more complete
implementation to follow soon, with tests), and a root Files
interface/application.
Limitations:
* Directory isn't really implemented.
* Makes no security guarantees -- i.e., definitely insecure (doesn't
prevent path traversal "out of" a "file system").
* Not implemented: file streaming (read/write), file mapping, file
re-opening.
* Various other flags not implemented (e.g., recursive delete).
* Theoretical implementation: many things as yet totally untested.
* Single-threaded. Should be easy enough to move to a thread pool (but
note that operations still need to be sequenced within each
"object"/message pipe).
* Still not specified (but definitely desired): directory streaming.
* Still need more error codes.
* Still needs more comments and documentation.
Changes over previous iterations:
* Temporarily removed most of (totally untested) Directory
implementation.
* "file manager" -> "files".
* Introduced a Timespec struct (and nanosecond resolution).
* Turned persistent, shared "user" directory into a "debug" directory.
* Various other requested features added/changes made.
Open questions:
* There's some duplication between Directory and File (e.g., they both
have Stat()). Perhaps it'd be more POSIX-y to combine them?
* Semantics for "chroot"-type operations and "re-open" operations.
* (Lots of thinking with respect to security in general.)
R=qsr@chromium.org
Review URL: https://codereview.chromium.org/875643004
diff --git a/services/files/BUILD.gn b/services/files/BUILD.gn
new file mode 100644
index 0000000..e5ae2a7
--- /dev/null
+++ b/services/files/BUILD.gn
@@ -0,0 +1,60 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/mojo_application.gni")
+import("//mojo/public/tools/bindings/mojom.gni")
+
+mojo_native_application("files") {
+ sources = [
+ "directory_impl.cc",
+ "directory_impl.h",
+ "file_impl.cc",
+ "file_impl.h",
+ "files_impl.cc",
+ "files_impl.h",
+ "futimens.h",
+ "futimens_android.cc",
+ "main.cc",
+ "util.cc",
+ "util.h",
+ ]
+
+ deps = [
+ ":bindings",
+ "//base",
+ "//mojo/application",
+ "//mojo/common",
+ "//mojo/public/cpp/application",
+ "//mojo/public/cpp/system",
+ ]
+}
+
+mojom("bindings") {
+ sources = [
+ "directory.mojom",
+ "file.mojom",
+ "files.mojom",
+ "types.mojom",
+ ]
+}
+
+mojo_native_application("apptests") {
+ output_name = "files_apptests"
+
+ testonly = true
+
+ sources = [
+ "files_apptest.cc",
+ ]
+
+ deps = [
+ ":bindings",
+ "//base",
+ "//mojo/application",
+ "//mojo/application:test_support",
+ "//mojo/public/cpp/bindings",
+ ]
+
+ data_deps = [ ":files" ]
+}
diff --git a/services/files/directory.mojom b/services/files/directory.mojom
new file mode 100644
index 0000000..8be25da
--- /dev/null
+++ b/services/files/directory.mojom
@@ -0,0 +1,63 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo.files;
+
+import "services/files/file.mojom";
+import "services/files/types.mojom";
+
+// This interface provides access to a directory in a "file system", providing
+// operations such as creating/opening/removing/renaming files/directories
+// within it. Note that all relative |path| arguments are relative to "this"
+// directory (i.e., "this" directory functions as the current working directory
+// for the various operations).
+// TODO(vtl): Paths may be relative; should they allowed to be absolute?
+// (Currently not.)
+interface Directory {
+ // Operations about "this" |Directory|:
+
+ // Reads the contents of this directory.
+ // TODO(vtl): Clarify error codes versus |directory_contents|.
+ Read() => (Error error, array<DirectoryEntry>? directory_contents);
+
+ // Gets information about this directory. On success, |file_information| is
+ // non-null and will contain this information.
+ Stat() => (Error error, FileInformation? file_information);
+
+ // Updates this directory's atime and/or mtime to the time specified by
+ // |atime| (or |mtime|, respectively), which may also indicate "now". If
+ // |atime| or |mtime| is null, then the corresponding time is not modified.
+ Touch(TimespecOrNow? atime, TimespecOrNow? mtime) => (Error error);
+
+ // Operations *in* "this" |Directory|:
+
+ // Opens the file specified by |path| with the given |open_flags|. |file| is
+ // optional, mainly for consistency with |OpenDirectory()| (but may be useful,
+ // together with |kOpenFlagCreate|, for "touching" a file).
+ OpenFile(string path, File&? file, uint32 open_flags)
+ => (Error error);
+
+ // Opens the directory specified by |path|. |directory| is optional, so that
+ // this may be used as a simple "mkdir()" with |kOpenFlagCreate|.
+ OpenDirectory(string path,
+ Directory&? directory,
+ uint32 open_flags) => (Error error);
+
+ // Renames/moves the file/directory given by |path| to |new_path|.
+ Rename(string path, string new_path) => (Error error);
+
+ // Deletes the given path, which may be a file or a directory (see
+ // |kDeleteFlag...| for details).
+ Delete(string path, uint32 delete_flags) => (Error error);
+
+ // TODO(vtl): directory "streaming"?
+ // TODO(vtl): "make root" (i.e., prevent cd-ing, etc., to parent); note that
+ // this would require a much more complicated implementation (e.g., it needs
+ // to be "inherited" by OpenDirectory(), and the enforcement needs to be valid
+ // even if the opened directory is subsequently moved -- e.g., closer to the
+ // "root")
+ // TODO(vtl): Add a "watch"?
+ // TODO(vtl): Should we have a "close" method?
+ // TODO(vtl): Add Dup() and Reopen() (like File)?
+};
diff --git a/services/files/directory_impl.cc b/services/files/directory_impl.cc
new file mode 100644
index 0000000..c346ce6
--- /dev/null
+++ b/services/files/directory_impl.cc
@@ -0,0 +1,194 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/files/directory_impl.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/logging.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/posix/eintr_wrapper.h"
+#include "build/build_config.h"
+#include "services/files/file_impl.h"
+#include "services/files/util.h"
+
+namespace mojo {
+namespace files {
+
+namespace {
+
+Error ValidateOpenFlags(uint32_t open_flags, bool is_directory) {
+ // Treat unknown flags as "unimplemented".
+ if ((open_flags &
+ ~(kOpenFlagRead | kOpenFlagWrite | kOpenFlagCreate | kOpenFlagExclusive |
+ kOpenFlagAppend | kOpenFlagTruncate)))
+ return ERROR_UNIMPLEMENTED;
+
+ // At least one of |kOpenFlagRead| or |kOpenFlagWrite| must be set.
+ if (!(open_flags & (kOpenFlagRead | kOpenFlagWrite)))
+ return ERROR_INVALID_ARGUMENT;
+
+ // |kOpenFlagCreate| requires |kOpenFlagWrite|.
+ if ((open_flags & kOpenFlagCreate) && !(open_flags & kOpenFlagWrite))
+ return ERROR_INVALID_ARGUMENT;
+
+ // |kOpenFlagExclusive| requires |kOpenFlagCreate|.
+ if ((open_flags & kOpenFlagExclusive) && !(open_flags & kOpenFlagCreate))
+ return ERROR_INVALID_ARGUMENT;
+
+ if (is_directory) {
+ // Check that file-only flags aren't set.
+ if ((open_flags & (kOpenFlagAppend | kOpenFlagTruncate)))
+ return ERROR_INVALID_ARGUMENT;
+ return ERROR_OK;
+ }
+
+ // File-only flags:
+
+ // |kOpenFlagAppend| requires |kOpenFlagWrite|.
+ if ((open_flags & kOpenFlagAppend) && !(open_flags & kOpenFlagWrite))
+ return ERROR_INVALID_ARGUMENT;
+
+ // |kOpenFlagTruncate| requires |kOpenFlagWrite|.
+ if ((open_flags & kOpenFlagTruncate) && !(open_flags & kOpenFlagWrite))
+ return ERROR_INVALID_ARGUMENT;
+
+ return ERROR_OK;
+}
+
+} // namespace
+
+DirectoryImpl::DirectoryImpl(InterfaceRequest<Directory> request,
+ base::ScopedFD dir_fd,
+ scoped_ptr<base::ScopedTempDir> temp_dir)
+ : binding_(this, request.Pass()),
+ dir_fd_(dir_fd.Pass()),
+ temp_dir_(temp_dir.Pass()) {
+ DCHECK(dir_fd_.is_valid());
+}
+
+DirectoryImpl::~DirectoryImpl() {
+}
+
+void DirectoryImpl::Read(
+ const Callback<void(Error, Array<DirectoryEntryPtr>)>& callback) {
+ // TODO(vtl): FIXME sooner
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED, Array<DirectoryEntryPtr>());
+}
+
+void DirectoryImpl::Stat(
+ const Callback<void(Error, FileInformationPtr)>& callback) {
+ // TODO(vtl): FIXME sooner
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED, nullptr);
+}
+
+void DirectoryImpl::Touch(TimespecOrNowPtr atime,
+ TimespecOrNowPtr mtime,
+ const Callback<void(Error)>& callback) {
+ // TODO(vtl): FIXME sooner
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED);
+}
+
+// TODO(vtl): Move the implementation to a thread pool.
+void DirectoryImpl::OpenFile(const String& path,
+ InterfaceRequest<File> file,
+ uint32_t open_flags,
+ const Callback<void(Error)>& callback) {
+ DCHECK(!path.is_null());
+ DCHECK(dir_fd_.is_valid());
+
+ if (Error error = IsPathValid(path)) {
+ callback.Run(error);
+ return;
+ }
+ // TODO(vtl): Make sure the path doesn't exit this directory (if appropriate).
+ // TODO(vtl): Maybe allow absolute paths?
+
+ if (Error error = ValidateOpenFlags(open_flags, false)) {
+ callback.Run(error);
+ return;
+ }
+
+ int flags = 0;
+ if ((open_flags & kOpenFlagRead))
+ flags |= (open_flags & kOpenFlagWrite) ? O_RDWR : O_RDONLY;
+ else
+ flags |= O_WRONLY;
+ if ((open_flags & kOpenFlagCreate))
+ flags |= O_CREAT;
+ if ((open_flags & kOpenFlagExclusive))
+ flags |= O_EXCL;
+ if ((open_flags & kOpenFlagAppend))
+ flags |= O_APPEND;
+ if ((open_flags & kOpenFlagTruncate))
+ flags |= O_TRUNC;
+
+ base::ScopedFD file_fd(
+ HANDLE_EINTR(openat(dir_fd_.get(), path.get().c_str(), flags, 0600)));
+ if (!file_fd.is_valid()) {
+ callback.Run(ErrnoToError(errno));
+ return;
+ }
+
+ if (file.is_pending())
+ new FileImpl(file.Pass(), file_fd.Pass());
+ callback.Run(ERROR_OK);
+}
+
+void DirectoryImpl::OpenDirectory(const String& path,
+ InterfaceRequest<Directory> directory,
+ uint32_t open_flags,
+ const Callback<void(Error)>& callback) {
+ // TODO(vtl): FIXME sooner
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED);
+}
+
+void DirectoryImpl::Rename(const String& path,
+ const String& new_path,
+ const Callback<void(Error)>& callback) {
+ DCHECK(!path.is_null());
+ DCHECK(!new_path.is_null());
+ DCHECK(dir_fd_.is_valid());
+
+ if (Error error = IsPathValid(path)) {
+ callback.Run(error);
+ return;
+ }
+ if (Error error = IsPathValid(new_path)) {
+ callback.Run(error);
+ return;
+ }
+ // TODO(vtl): see TODOs about |path| in OpenFile()
+
+ if (renameat(dir_fd_.get(), path.get().c_str(), dir_fd_.get(),
+ new_path.get().c_str())) {
+ callback.Run(ErrnoToError(errno));
+ return;
+ }
+
+ callback.Run(ERROR_OK);
+}
+
+void DirectoryImpl::Delete(const String& path,
+ uint32_t delete_flags,
+ const Callback<void(Error)>& callback) {
+ // TODO(vtl): FIXME sooner
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED);
+}
+
+} // namespace files
+} // namespace mojo
diff --git a/services/files/directory_impl.h b/services/files/directory_impl.h
new file mode 100644
index 0000000..933247a
--- /dev/null
+++ b/services/files/directory_impl.h
@@ -0,0 +1,64 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_FILES_DIRECTORY_IMPL_H_
+#define SERVICES_FILES_DIRECTORY_IMPL_H_
+
+#include "base/files/scoped_file.h"
+#include "base/macros.h"
+#include "base/memory/scoped_ptr.h"
+#include "mojo/public/cpp/bindings/interface_request.h"
+#include "mojo/public/cpp/bindings/strong_binding.h"
+#include "services/files/directory.mojom.h"
+
+namespace base {
+class ScopedTempDir;
+} // namespace base
+
+namespace mojo {
+namespace files {
+
+class DirectoryImpl : public Directory {
+ public:
+ // Set |temp_dir| only if there's a temporary directory that should be deleted
+ // when this object is destroyed.
+ DirectoryImpl(InterfaceRequest<Directory> request,
+ base::ScopedFD dir_fd,
+ scoped_ptr<base::ScopedTempDir> temp_dir);
+ ~DirectoryImpl() override;
+
+ // |Directory| implementation:
+ void Read(
+ const Callback<void(Error, Array<DirectoryEntryPtr>)>& callback) override;
+ void Stat(const Callback<void(Error, FileInformationPtr)>& callback) override;
+ void Touch(TimespecOrNowPtr atime,
+ TimespecOrNowPtr mtime,
+ const Callback<void(Error)>& callback) override;
+ void OpenFile(const String& path,
+ InterfaceRequest<File> file,
+ uint32_t open_flags,
+ const Callback<void(Error)>& callback) override;
+ void OpenDirectory(const String& path,
+ InterfaceRequest<Directory> directory,
+ uint32_t open_flags,
+ const Callback<void(Error)>& callback) override;
+ void Rename(const String& path,
+ const String& new_path,
+ const Callback<void(Error)>& callback) override;
+ void Delete(const String& path,
+ uint32_t delete_flags,
+ const Callback<void(Error)>& callback) override;
+
+ private:
+ StrongBinding<Directory> binding_;
+ base::ScopedFD dir_fd_;
+ scoped_ptr<base::ScopedTempDir> temp_dir_;
+
+ DISALLOW_COPY_AND_ASSIGN(DirectoryImpl);
+};
+
+} // namespace files
+} // namespace mojo
+
+#endif // SERVICES_FILES_DIRECTORY_IMPL_H_
diff --git a/services/files/file.mojom b/services/files/file.mojom
new file mode 100644
index 0000000..14a3883
--- /dev/null
+++ b/services/files/file.mojom
@@ -0,0 +1,85 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO(vtl): notes to self:
+// - file offsets, file positions, and file sizes are int64 (though positions
+// and sizes must always be non-negative)
+// - buffer size parameters (for read/write) are uint32
+
+module mojo.files;
+
+import "services/files/types.mojom";
+
+// TODO(vtl): Write comments.
+interface File {
+ // Flushes/closes this file; no operations may be performed on this file after
+ // this. Note that any error code is strictly informational -- the close may
+ // not be retried.
+ Close() => (Error err);
+
+ // Reads (at most) |num_bytes_to_read| from the location specified by
+ // |offset|/|whence|. On success, |bytes_read| is set to the data read.
+ // TODO(vtl): Define/clarify behavior when less than |num_bytes_to_read| bytes
+ // are read.
+ // TODO(vtl): Clarify when (for what values of |offset|/|whence|) this
+ // modifies the file position. Or maybe there should be a flag?
+ Read(uint32 num_bytes_to_read, int64 offset, Whence whence)
+ => (Error error, array<uint8>? bytes_read);
+
+ // Writes |bytes_to_write| to the location specified by |offset|/|whence|.
+ // TODO(vtl): Clarify behavior when |num_bytes_written| is less than the size
+ // of |bytes_to_write|.
+ Write(array<uint8> bytes_to_write, int64 offset, Whence whence)
+ => (Error error, uint32 num_bytes_written);
+
+ // TODO(vtl): We definitely want 64 bits for |num_bytes_to_read|; but do we
+ // want it to be signed (this is consistent with |size| values, but
+ // inconsistent with 32-bit |num_bytes_to_read| values)? Do we want to have
+ // separate "read to end" versus "tail" (i.e., keep on reading as more data is
+ // appended) modes, and how would those be signalled?
+ ReadToStream(handle<data_pipe_producer> source,
+ int64 offset,
+ Whence whence,
+ int64 num_bytes_to_read) => (Error error);
+ WriteFromStream(handle<data_pipe_consumer> sink, int64 offset, Whence whence)
+ => (Error error);
+
+ // Gets the current file position. On success, |position| is the current
+ // offset (in bytes) from the beginning of the file).
+ Tell() => (Error error, int64 position);
+
+ // Sets the current file position to that specified by |offset|/|whence|. On
+ // success, |position| is the offset (in bytes) from the beginning of the
+ // file.
+ Seek(int64 offset, Whence whence) => (Error error, int64 position);
+
+ // Gets information about this file. On success, |file_information| is
+ // non-null and will contain this information.
+ Stat() => (Error error, FileInformation? file_information);
+
+ // Truncates this file to the size specified by |size| (in bytes).
+ Truncate(int64 size) => (Error error);
+
+ // Updates this file's atime and/or mtime to the time specified by |atime| (or
+ // |mtime|, respectively), which may also indicate "now". If |atime| or
+ // |mtime| is null, then the corresponding time is not modified.
+ Touch(TimespecOrNow? atime, TimespecOrNow? mtime) => (Error error);
+
+ // Creates a new |File| instance, which shares the same "file description".
+ // I.e., the access mode, etc. (as specified to |Directory::OpenFile()| by the
+ // |open_flags| argument) as well as file position.
+ Dup(File& file) => (Error error);
+
+ // TODO(vtl): What are the rules for reopening (w.r.t. changing mode/flags).
+ // E.g., obviously can go from "read-write" to "read", but reverse? (probably
+ // not), can remove "append"? (probably not?). Do we allow "truncate"?
+ Reopen(File& file, uint32 open_flags) => (Error error);
+
+ // TODO(vtl): probably should have access flags (but also exec?); how do these
+ // relate to access mode?
+ AsBuffer() => (Error error, handle<shared_buffer>? buffer);
+
+ // TODO(vtl): Add a "watch"?
+ // TODO(vtl): Add something analogous to fsync(2)?
+};
diff --git a/services/files/file_impl.cc b/services/files/file_impl.cc
new file mode 100644
index 0000000..721efb7
--- /dev/null
+++ b/services/files/file_impl.cc
@@ -0,0 +1,364 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/files/file_impl.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <limits>
+
+#include "base/files/scoped_file.h"
+#include "base/logging.h"
+#include "base/posix/eintr_wrapper.h"
+#include "services/files/futimens.h"
+#include "services/files/util.h"
+
+static_assert(sizeof(off_t) <= sizeof(int64_t), "off_t too big");
+static_assert(sizeof(size_t) >= sizeof(uint32_t), "size_t too small");
+
+namespace mojo {
+namespace files {
+
+const size_t kMaxReadSize = 1 * 1024 * 1024; // 1 MB.
+
+FileImpl::FileImpl(InterfaceRequest<File> request, base::ScopedFD file_fd)
+ : binding_(this, request.Pass()), file_fd_(file_fd.Pass()) {
+ DCHECK(file_fd_.is_valid());
+}
+
+FileImpl::~FileImpl() {
+}
+
+void FileImpl::Close(const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+ int fd_to_try_to_close = file_fd_.release();
+ // POSIX.1 (2013) leaves the validity of the FD undefined on EINTR and EIO. On
+ // Linux, the FD is always invalidated, so we'll pretend that the close
+ // succeeded. (On other Unixes, the situation may be different and possibly
+ // totally broken; see crbug.com/269623.)
+ if (IGNORE_EINTR(close(fd_to_try_to_close)) != 0) {
+ // Save errno, since we do a few things and we don't want it trampled.
+ int error = errno;
+ CHECK_NE(error, EBADF); // This should never happen.
+ DCHECK_NE(error, EINTR); // We already ignored EINTR.
+ // I don't know what Linux does on EIO (or any other errors) -- POSIX leaves
+ // it undefined -- so report the error and hope that the FD was invalidated.
+ callback.Run(ErrnoToError(error));
+ return;
+ }
+
+ callback.Run(ERROR_OK);
+}
+
+// TODO(vtl): Move the implementation to a thread pool.
+void FileImpl::Read(uint32_t num_bytes_to_read,
+ int64_t offset,
+ Whence whence,
+ const Callback<void(Error, Array<uint8_t>)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED, Array<uint8_t>());
+ return;
+ }
+ if (num_bytes_to_read > kMaxReadSize) {
+ callback.Run(ERROR_OUT_OF_RANGE, Array<uint8_t>());
+ return;
+ }
+ if (Error error = IsOffsetValid(offset)) {
+ callback.Run(error, Array<uint8_t>());
+ return;
+ }
+ if (Error error = IsWhenceValid(whence)) {
+ callback.Run(error, Array<uint8_t>());
+ return;
+ }
+
+ if (offset != 0 || whence != WHENCE_FROM_CURRENT) {
+ // TODO(vtl): Use |pread()| below in the |WHENCE_FROM_START| case. This
+ // implementation is obviously not atomic. (If someone seeks simultaneously,
+ // we'll end up writing somewhere else. Or, well, we would if we were
+ // multithreaded.) Maybe we should do an |ftell()| and always use |pread()|.
+ // TODO(vtl): Possibly, at least sometimes we should not change the file
+ // position. See TODO in file.mojom.
+ if (lseek(file_fd_.get(), static_cast<off_t>(offset),
+ WhenceToStandardWhence(whence)) < 0) {
+ callback.Run(ErrnoToError(errno), Array<uint8_t>());
+ return;
+ }
+ }
+
+ Array<uint8_t> bytes_read(num_bytes_to_read);
+ ssize_t num_bytes_read = HANDLE_EINTR(
+ read(file_fd_.get(), &bytes_read.front(), num_bytes_to_read));
+ if (num_bytes_read < 0) {
+ callback.Run(ErrnoToError(errno), Array<uint8_t>());
+ return;
+ }
+
+ DCHECK_LE(static_cast<size_t>(num_bytes_read), num_bytes_to_read);
+ bytes_read.resize(static_cast<size_t>(num_bytes_read));
+ callback.Run(ERROR_OK, bytes_read.Pass());
+}
+
+// TODO(vtl): Move the implementation to a thread pool.
+void FileImpl::Write(Array<uint8_t> bytes_to_write,
+ int64_t offset,
+ Whence whence,
+ const Callback<void(Error, uint32_t)>& callback) {
+ DCHECK(!bytes_to_write.is_null());
+
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED, 0);
+ return;
+ }
+ // Who knows what |write()| would return if the size is that big (and it
+ // actually wrote that much).
+ if (bytes_to_write.size() >
+ static_cast<size_t>(std::numeric_limits<ssize_t>::max())) {
+ callback.Run(ERROR_OUT_OF_RANGE, 0);
+ return;
+ }
+ if (Error error = IsOffsetValid(offset)) {
+ callback.Run(error, 0);
+ return;
+ }
+ if (Error error = IsWhenceValid(whence)) {
+ callback.Run(error, 0);
+ return;
+ }
+
+ if (offset != 0 || whence != WHENCE_FROM_CURRENT) {
+ // TODO(vtl): Use |pwrite()| below in the |WHENCE_FROM_START| case. This
+ // implementation is obviously not atomic. (If someone seeks simultaneously,
+ // we'll end up writing somewhere else. Or, well, we would if we were
+ // multithreaded.) Maybe we should do an |ftell()| and always use
+ // |pwrite()|.
+ // TODO(vtl): Possibly, at least sometimes we should not change the file
+ // position. See TODO in file.mojom.
+ if (lseek(file_fd_.get(), static_cast<off_t>(offset),
+ WhenceToStandardWhence(whence)) < 0) {
+ callback.Run(ErrnoToError(errno), 0);
+ return;
+ }
+ }
+
+ const void* buf =
+ (bytes_to_write.size() > 0) ? &bytes_to_write.front() : nullptr;
+ ssize_t num_bytes_written =
+ HANDLE_EINTR(write(file_fd_.get(), buf, bytes_to_write.size()));
+ if (num_bytes_written < 0) {
+ callback.Run(ErrnoToError(errno), 0);
+ return;
+ }
+
+ DCHECK_LE(static_cast<size_t>(num_bytes_written),
+ std::numeric_limits<uint32_t>::max());
+ callback.Run(ERROR_OK, static_cast<uint32_t>(num_bytes_written));
+}
+
+void FileImpl::ReadToStream(ScopedDataPipeProducerHandle source,
+ int64_t offset,
+ Whence whence,
+ int64_t num_bytes_to_read,
+ const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+ if (Error error = IsOffsetValid(offset)) {
+ callback.Run(error);
+ return;
+ }
+ if (Error error = IsWhenceValid(whence)) {
+ callback.Run(error);
+ return;
+ }
+
+ // TODO(vtl): FIXME soon
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED);
+}
+
+void FileImpl::WriteFromStream(ScopedDataPipeConsumerHandle sink,
+ int64_t offset,
+ Whence whence,
+ const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+ if (Error error = IsOffsetValid(offset)) {
+ callback.Run(error);
+ return;
+ }
+ if (Error error = IsWhenceValid(whence)) {
+ callback.Run(error);
+ return;
+ }
+
+ // TODO(vtl): FIXME soon
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED);
+}
+
+void FileImpl::Tell(const Callback<void(Error, int64_t)>& callback) {
+ Seek(0, WHENCE_FROM_CURRENT, callback);
+}
+
+void FileImpl::Seek(int64_t offset,
+ Whence whence,
+ const Callback<void(Error, int64_t)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED, 0);
+ return;
+ }
+ if (Error error = IsOffsetValid(offset)) {
+ callback.Run(error, 0);
+ return;
+ }
+ if (Error error = IsWhenceValid(whence)) {
+ callback.Run(error, 0);
+ return;
+ }
+
+ off_t position = lseek(file_fd_.get(), static_cast<off_t>(offset),
+ WhenceToStandardWhence(whence));
+ if (position < 0) {
+ callback.Run(ErrnoToError(errno), 0);
+ return;
+ }
+
+ callback.Run(ERROR_OK, static_cast<int64>(position));
+}
+
+void FileImpl::Stat(const Callback<void(Error, FileInformationPtr)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED, nullptr);
+ return;
+ }
+
+ struct stat buf;
+ if (fstat(file_fd_.get(), &buf) != 0) {
+ callback.Run(ErrnoToError(errno), nullptr);
+ return;
+ }
+
+ FileInformationPtr file_info(FileInformation::New());
+ file_info->size = static_cast<int64_t>(buf.st_size);
+ file_info->atime = Timespec::New();
+ file_info->mtime = Timespec::New();
+#if defined(OS_ANDROID)
+ file_info->atime->seconds = static_cast<int64_t>(buf.st_atime);
+ file_info->atime->nanoseconds = static_cast<int32_t>(buf.st_atime_nsec);
+ file_info->mtime->seconds = static_cast<int64_t>(buf.st_mtime);
+ file_info->mtime->nanoseconds = static_cast<int32_t>(buf.st_mtime_nsec);
+#else
+ file_info->atime->seconds = static_cast<int64_t>(buf.st_atim.tv_sec);
+ file_info->atime->nanoseconds = static_cast<int32_t>(buf.st_atim.tv_nsec);
+ file_info->mtime->seconds = static_cast<int64_t>(buf.st_mtim.tv_sec);
+ file_info->mtime->nanoseconds = static_cast<int32_t>(buf.st_mtim.tv_nsec);
+#endif
+
+ callback.Run(ERROR_OK, file_info.Pass());
+}
+
+void FileImpl::Truncate(int64_t size, const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+ if (size < 0) {
+ callback.Run(ERROR_INVALID_ARGUMENT);
+ return;
+ }
+ if (Error error = IsOffsetValid(size)) {
+ callback.Run(error);
+ return;
+ }
+
+ if (ftruncate(file_fd_.get(), static_cast<off_t>(size)) != 0) {
+ callback.Run(ErrnoToError(errno));
+ return;
+ }
+
+ callback.Run(ERROR_OK);
+}
+
+void FileImpl::Touch(TimespecOrNowPtr atime,
+ TimespecOrNowPtr mtime,
+ const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+
+ struct timespec times[2];
+ if (Error error = TimespecOrNowToStandardTimespec(atime.get(), ×[0])) {
+ callback.Run(error);
+ return;
+ }
+ if (Error error = TimespecOrNowToStandardTimespec(mtime.get(), ×[1])) {
+ callback.Run(error);
+ return;
+ }
+
+ if (futimens(file_fd_.get(), times) != 0) {
+ callback.Run(ErrnoToError(errno));
+ return;
+ }
+
+ callback.Run(ERROR_OK);
+}
+
+void FileImpl::Dup(InterfaceRequest<File> file,
+ const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+
+ base::ScopedFD file_fd(dup(file_fd_.get()));
+ if (!file_fd.is_valid()) {
+ callback.Run(ErrnoToError(errno));
+ return;
+ }
+
+ new FileImpl(file.Pass(), file_fd.Pass());
+ callback.Run(ERROR_OK);
+}
+
+void FileImpl::Reopen(InterfaceRequest<File> file,
+ uint32_t open_flags,
+ const Callback<void(Error)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED);
+ return;
+ }
+
+ // TODO(vtl): FIXME soon
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED);
+}
+
+void FileImpl::AsBuffer(
+ const Callback<void(Error, ScopedSharedBufferHandle)>& callback) {
+ if (!file_fd_.is_valid()) {
+ callback.Run(ERROR_CLOSED, ScopedSharedBufferHandle());
+ return;
+ }
+
+ // TODO(vtl): FIXME soon
+ NOTIMPLEMENTED();
+ callback.Run(ERROR_UNIMPLEMENTED, ScopedSharedBufferHandle());
+}
+
+} // namespace files
+} // namespace mojo
diff --git a/services/files/file_impl.h b/services/files/file_impl.h
new file mode 100644
index 0000000..dc6a18a
--- /dev/null
+++ b/services/files/file_impl.h
@@ -0,0 +1,69 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_FILES_FILE_IMPL_H_
+#define SERVICES_FILES_FILE_IMPL_H_
+
+#include "base/files/scoped_file.h"
+#include "base/macros.h"
+#include "mojo/public/cpp/bindings/interface_request.h"
+#include "mojo/public/cpp/bindings/strong_binding.h"
+#include "services/files/directory.mojom.h"
+
+namespace mojo {
+namespace files {
+
+class FileImpl : public File {
+ public:
+ // TODO(vtl): Will need more for, e.g., |Reopen()|.
+ FileImpl(InterfaceRequest<File> request, base::ScopedFD file_fd);
+ ~FileImpl() override;
+
+ // |File| implementation:
+ void Close(const Callback<void(Error)>& callback) override;
+ void Read(uint32_t num_bytes_to_read,
+ int64_t offset,
+ Whence whence,
+ const Callback<void(Error, Array<uint8_t>)>& callback) override;
+ void Write(Array<uint8_t> bytes_to_write,
+ int64_t offset,
+ Whence whence,
+ const Callback<void(Error, uint32_t)>& callback) override;
+ void ReadToStream(ScopedDataPipeProducerHandle source,
+ int64_t offset,
+ Whence whence,
+ int64_t num_bytes_to_read,
+ const Callback<void(Error)>& callback) override;
+ void WriteFromStream(ScopedDataPipeConsumerHandle sink,
+ int64_t offset,
+ Whence whence,
+ const Callback<void(Error)>& callback) override;
+ void Tell(const Callback<void(Error, int64_t)>& callback) override;
+ void Seek(int64_t offset,
+ Whence whence,
+ const Callback<void(Error, int64_t)>& callback) override;
+ void Stat(const Callback<void(Error, FileInformationPtr)>& callback) override;
+ void Truncate(int64_t size, const Callback<void(Error)>& callback) override;
+ void Touch(TimespecOrNowPtr atime,
+ TimespecOrNowPtr mtime,
+ const Callback<void(Error)>& callback) override;
+ void Dup(InterfaceRequest<File> file,
+ const Callback<void(Error)>& callback) override;
+ void Reopen(InterfaceRequest<File> file,
+ uint32_t open_flags,
+ const Callback<void(Error)>& callback) override;
+ void AsBuffer(
+ const Callback<void(Error, ScopedSharedBufferHandle)>& callback) override;
+
+ private:
+ StrongBinding<File> binding_;
+ base::ScopedFD file_fd_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileImpl);
+};
+
+} // namespace files
+} // namespace mojo
+
+#endif // SERVICES_FILES_FILE_IMPL_H_
diff --git a/services/files/files.mojom b/services/files/files.mojom
new file mode 100644
index 0000000..0e7b61d
--- /dev/null
+++ b/services/files/files.mojom
@@ -0,0 +1,22 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo.files;
+
+import "services/files/directory.mojom";
+import "services/files/types.mojom";
+
+enum FileSystem {
+ // A per-instance (i.e., one for each call to |OpenFileSystem()|) temporary
+ // file system.
+ TEMPORARY,
+
+ // A persistent, shared file system (rooted at ~/MojoDebug, which must already
+ // exist) only available in Debug builds.
+ DEBUG,
+};
+
+interface Files {
+ OpenFileSystem(FileSystem file_system, Directory& directory) => (Error error);
+};
diff --git a/services/files/files_apptest.cc b/services/files/files_apptest.cc
new file mode 100644
index 0000000..c5dc5a6
--- /dev/null
+++ b/services/files/files_apptest.cc
@@ -0,0 +1,461 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <vector>
+
+#include "base/macros.h"
+#include "mojo/public/cpp/application/application_impl.h"
+#include "mojo/public/cpp/application/application_test_base.h"
+#include "mojo/public/cpp/bindings/interface_request.h"
+#include "mojo/public/cpp/bindings/type_converter.h"
+#include "services/files/directory.mojom.h"
+#include "services/files/file.mojom.h"
+#include "services/files/files.mojom.h"
+
+namespace mojo {
+namespace files {
+namespace {
+
+// TODO(vtl): Stuff copied from mojo/public/cpp/bindings/lib/template_util.h.
+template <class T, T v>
+struct IntegralConstant {
+ static const T value = v;
+};
+
+template <class T, T v>
+const T IntegralConstant<T, v>::value;
+
+typedef IntegralConstant<bool, true> TrueType;
+typedef IntegralConstant<bool, false> FalseType;
+
+template <class T>
+struct IsConst : FalseType {};
+template <class T>
+struct IsConst<const T> : TrueType {};
+
+template <bool B, typename T = void>
+struct EnableIf {};
+
+template <typename T>
+struct EnableIf<true, T> {
+ typedef T type;
+};
+
+typedef char YesType;
+
+struct NoType {
+ YesType dummy[2];
+};
+
+template <typename T>
+struct IsMoveOnlyType {
+ template <typename U>
+ static YesType Test(const typename U::MoveOnlyTypeForCPP03*);
+
+ template <typename U>
+ static NoType Test(...);
+
+ static const bool value =
+ sizeof(Test<T>(0)) == sizeof(YesType) && !IsConst<T>::value;
+};
+
+template <typename T>
+typename EnableIf<!IsMoveOnlyType<T>::value, T>::type& Forward(T& t) {
+ return t;
+}
+
+template <typename T>
+typename EnableIf<IsMoveOnlyType<T>::value, T>::type Forward(T& t) {
+ return t.Pass();
+}
+// TODO(vtl): (End of stuff copied from template_util.h.)
+
+template <typename T1>
+Callback<void(T1)> Capture(T1* t1) {
+ return [t1](T1 got_t1) { *t1 = Forward(got_t1); };
+}
+
+template <typename T1, typename T2>
+Callback<void(T1, T2)> Capture(T1* t1, T2* t2) {
+ return [t1, t2](T1 got_t1, T2 got_t2) {
+ *t1 = Forward(got_t1);
+ *t2 = Forward(got_t2);
+ };
+}
+
+class FilesAppTest : public test::ApplicationTestBase {
+ public:
+ FilesAppTest() {}
+ ~FilesAppTest() override {}
+
+ void SetUp() override {
+ test::ApplicationTestBase::SetUp();
+ application_impl()->ConnectToService("mojo:files", &files_);
+ }
+
+ FilesPtr& files() { return files_; }
+
+ private:
+ FilesPtr files_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesAppTest);
+};
+
+TEST_F(FilesAppTest, CreateWriteCloseRenameOpenRead) {
+ // Get a temporary root directory.
+ DirectoryPtr directory;
+ Error error = ERROR_INTERNAL;
+ files()->OpenFileSystem(FILE_SYSTEM_TEMPORARY, GetProxy(&directory),
+ Capture(&error));
+ ASSERT_TRUE(files().WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ {
+ // Create my_file.
+ FilePtr file;
+ error = ERROR_INTERNAL;
+ directory->OpenFile("my_file", GetProxy(&file),
+ kOpenFlagWrite | kOpenFlagCreate, Capture(&error));
+ ASSERT_TRUE(directory.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Write to it.
+ std::vector<uint8_t> bytes_to_write;
+ bytes_to_write.push_back(static_cast<uint8_t>('h'));
+ bytes_to_write.push_back(static_cast<uint8_t>('e'));
+ bytes_to_write.push_back(static_cast<uint8_t>('l'));
+ bytes_to_write.push_back(static_cast<uint8_t>('l'));
+ bytes_to_write.push_back(static_cast<uint8_t>('o'));
+ error = ERROR_INTERNAL;
+ uint32_t num_bytes_written = 0;
+ file->Write(Array<uint8_t>::From(bytes_to_write), 0, WHENCE_FROM_CURRENT,
+ Capture(&error, &num_bytes_written));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(bytes_to_write.size(), num_bytes_written);
+
+ // Close it.
+ error = ERROR_INTERNAL;
+ file->Close(Capture(&error));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ }
+
+ // Rename it.
+ error = ERROR_INTERNAL;
+ directory->Rename("my_file", "your_file", Capture(&error));
+ ASSERT_TRUE(directory.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ {
+ // Open my_file again.
+ FilePtr file;
+ error = ERROR_INTERNAL;
+ directory->OpenFile("your_file", GetProxy(&file), kOpenFlagRead,
+ Capture(&error));
+ ASSERT_TRUE(directory.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Read from it.
+ Array<uint8_t> bytes_read;
+ error = ERROR_INTERNAL;
+ file->Read(3, 1, WHENCE_FROM_START, Capture(&error, &bytes_read));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ ASSERT_EQ(3u, bytes_read.size());
+ EXPECT_EQ(static_cast<uint8_t>('e'), bytes_read[0]);
+ EXPECT_EQ(static_cast<uint8_t>('l'), bytes_read[1]);
+ EXPECT_EQ(static_cast<uint8_t>('l'), bytes_read[2]);
+ }
+
+ // TODO(vtl): Test various open options.
+ // TODO(vtl): Test read/write offset options.
+}
+
+// Note: Ignore nanoseconds, since it may not always be supported. We expect at
+// least second-resolution support though.
+TEST_F(FilesAppTest, StatTouch) {
+ // Get a temporary root directory.
+ DirectoryPtr directory;
+ Error error = ERROR_INTERNAL;
+ files()->OpenFileSystem(FILE_SYSTEM_TEMPORARY, GetProxy(&directory),
+ Capture(&error));
+ ASSERT_TRUE(files().WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Create my_file.
+ FilePtr file;
+ error = ERROR_INTERNAL;
+ directory->OpenFile("my_file", GetProxy(&file),
+ kOpenFlagWrite | kOpenFlagCreate, Capture(&error));
+ ASSERT_TRUE(directory.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Stat it.
+ error = ERROR_INTERNAL;
+ FileInformationPtr file_info;
+ file->Stat(Capture(&error, &file_info));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ ASSERT_FALSE(file_info.is_null());
+ EXPECT_EQ(0, file_info->size);
+ ASSERT_FALSE(file_info->atime.is_null());
+ EXPECT_GT(file_info->atime->seconds, 0); // Expect that it's not 1970-01-01.
+ ASSERT_FALSE(file_info->mtime.is_null());
+ EXPECT_GT(file_info->mtime->seconds, 0);
+ int64_t first_mtime = file_info->mtime->seconds;
+
+ // Touch only the atime.
+ error = ERROR_INTERNAL;
+ TimespecOrNowPtr t(TimespecOrNow::New());
+ t->now = false;
+ t->timespec = Timespec::New();
+ const int64_t kPartyTime1 = 1234567890; // Party like it's 2009-02-13.
+ t->timespec->seconds = kPartyTime1;
+ file->Touch(t.Pass(), nullptr, Capture(&error));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Stat again.
+ error = ERROR_INTERNAL;
+ file_info.reset();
+ file->Stat(Capture(&error, &file_info));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ ASSERT_FALSE(file_info.is_null());
+ ASSERT_FALSE(file_info->atime.is_null());
+ EXPECT_EQ(kPartyTime1, file_info->atime->seconds);
+ ASSERT_FALSE(file_info->mtime.is_null());
+ EXPECT_EQ(first_mtime, file_info->mtime->seconds);
+
+ // Touch only the mtime.
+ t = TimespecOrNow::New();
+ t->now = false;
+ t->timespec = Timespec::New();
+ const int64_t kPartyTime2 = 1425059525; // No time like the present.
+ t->timespec->seconds = kPartyTime2;
+ file->Touch(nullptr, t.Pass(), Capture(&error));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Stat again.
+ error = ERROR_INTERNAL;
+ file_info.reset();
+ file->Stat(Capture(&error, &file_info));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ ASSERT_FALSE(file_info.is_null());
+ ASSERT_FALSE(file_info->atime.is_null());
+ EXPECT_EQ(kPartyTime1, file_info->atime->seconds);
+ ASSERT_FALSE(file_info->mtime.is_null());
+ EXPECT_EQ(kPartyTime2, file_info->mtime->seconds);
+
+ // TODO(vtl): Also test non-zero file size.
+ // TODO(vtl): Also test Touch() "now" options.
+ // TODO(vtl): Also test touching both atime and mtime.
+
+ // Close it.
+ error = ERROR_INTERNAL;
+ file->Close(Capture(&error));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+}
+
+TEST_F(FilesAppTest, TellSeek) {
+ // Get a temporary root directory.
+ DirectoryPtr directory;
+ Error error = ERROR_INTERNAL;
+ files()->OpenFileSystem(FILE_SYSTEM_TEMPORARY, GetProxy(&directory),
+ Capture(&error));
+ ASSERT_TRUE(files().WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Create my_file.
+ FilePtr file;
+ error = ERROR_INTERNAL;
+ directory->OpenFile("my_file", GetProxy(&file),
+ kOpenFlagWrite | kOpenFlagCreate, Capture(&error));
+ ASSERT_TRUE(directory.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Write to it.
+ std::vector<uint8_t> bytes_to_write(1000, '!');
+ error = ERROR_INTERNAL;
+ uint32_t num_bytes_written = 0;
+ file->Write(Array<uint8_t>::From(bytes_to_write), 0, WHENCE_FROM_CURRENT,
+ Capture(&error, &num_bytes_written));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(bytes_to_write.size(), num_bytes_written);
+ const int size = static_cast<int>(num_bytes_written);
+
+ // Tell.
+ error = ERROR_INTERNAL;
+ int64_t position = -1;
+ file->Tell(Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ // Should be at the end.
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(size, position);
+
+ // Seek back 100.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file->Seek(-100, WHENCE_FROM_CURRENT, Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(size - 100, position);
+
+ // Tell.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file->Tell(Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(size - 100, position);
+
+ // Seek to 123 from start.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file->Seek(123, WHENCE_FROM_START, Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(123, position);
+
+ // Tell.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file->Tell(Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(123, position);
+
+ // Seek to 123 back from end.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file->Seek(-123, WHENCE_FROM_END, Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(size - 123, position);
+
+ // Tell.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file->Tell(Capture(&error, &position));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(size - 123, position);
+
+ // TODO(vtl): Check that seeking actually affects reading/writing.
+ // TODO(vtl): Check that seeking can extend the file?
+
+ // Close it.
+ error = ERROR_INTERNAL;
+ file->Close(Capture(&error));
+ ASSERT_TRUE(file.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+}
+
+TEST_F(FilesAppTest, Dup) {
+ // Get a temporary root directory.
+ DirectoryPtr directory;
+ Error error = ERROR_INTERNAL;
+ files()->OpenFileSystem(FILE_SYSTEM_TEMPORARY, GetProxy(&directory),
+ Capture(&error));
+ ASSERT_TRUE(files().WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Create my_file.
+ FilePtr file1;
+ error = ERROR_INTERNAL;
+ directory->OpenFile("my_file", GetProxy(&file1),
+ kOpenFlagRead | kOpenFlagWrite | kOpenFlagCreate,
+ Capture(&error));
+ ASSERT_TRUE(directory.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Write to it.
+ std::vector<uint8_t> bytes_to_write;
+ bytes_to_write.push_back(static_cast<uint8_t>('h'));
+ bytes_to_write.push_back(static_cast<uint8_t>('e'));
+ bytes_to_write.push_back(static_cast<uint8_t>('l'));
+ bytes_to_write.push_back(static_cast<uint8_t>('l'));
+ bytes_to_write.push_back(static_cast<uint8_t>('o'));
+ error = ERROR_INTERNAL;
+ uint32_t num_bytes_written = 0;
+ file1->Write(Array<uint8_t>::From(bytes_to_write), 0, WHENCE_FROM_CURRENT,
+ Capture(&error, &num_bytes_written));
+ ASSERT_TRUE(file1.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(bytes_to_write.size(), num_bytes_written);
+ const int end_hello_pos = static_cast<int>(num_bytes_written);
+
+ // Dup it.
+ FilePtr file2;
+ error = ERROR_INTERNAL;
+ file1->Dup(GetProxy(&file2), Capture(&error));
+ ASSERT_TRUE(file1.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // |file2| should have the same position.
+ error = ERROR_INTERNAL;
+ int64_t position = -1;
+ file2->Tell(Capture(&error, &position));
+ ASSERT_TRUE(file2.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(end_hello_pos, position);
+
+ // Write using |file2|.
+ std::vector<uint8_t> more_bytes_to_write;
+ more_bytes_to_write.push_back(static_cast<uint8_t>('w'));
+ more_bytes_to_write.push_back(static_cast<uint8_t>('o'));
+ more_bytes_to_write.push_back(static_cast<uint8_t>('r'));
+ more_bytes_to_write.push_back(static_cast<uint8_t>('l'));
+ more_bytes_to_write.push_back(static_cast<uint8_t>('d'));
+ error = ERROR_INTERNAL;
+ num_bytes_written = 0;
+ file2->Write(Array<uint8_t>::From(more_bytes_to_write), 0,
+ WHENCE_FROM_CURRENT, Capture(&error, &num_bytes_written));
+ ASSERT_TRUE(file2.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(more_bytes_to_write.size(), num_bytes_written);
+ const int end_world_pos = end_hello_pos + static_cast<int>(num_bytes_written);
+
+ // |file1| should have the same position.
+ error = ERROR_INTERNAL;
+ position = -1;
+ file1->Tell(Capture(&error, &position));
+ ASSERT_TRUE(file1.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ EXPECT_EQ(end_world_pos, position);
+
+ // Close |file1|.
+ error = ERROR_INTERNAL;
+ file1->Close(Capture(&error));
+ ASSERT_TRUE(file1.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // Read everything using |file2|.
+ Array<uint8_t> bytes_read;
+ error = ERROR_INTERNAL;
+ file2->Read(1000, 0, WHENCE_FROM_START, Capture(&error, &bytes_read));
+ ASSERT_TRUE(file2.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+ ASSERT_EQ(static_cast<size_t>(end_world_pos), bytes_read.size());
+ // Just check the first and last bytes.
+ EXPECT_EQ(static_cast<uint8_t>('h'), bytes_read[0]);
+ EXPECT_EQ(static_cast<uint8_t>('d'), bytes_read[end_world_pos - 1]);
+
+ // Close |file2|.
+ error = ERROR_INTERNAL;
+ file2->Close(Capture(&error));
+ ASSERT_TRUE(file2.WaitForIncomingMethodCall());
+ EXPECT_EQ(ERROR_OK, error);
+
+ // TODO(vtl): Test that |file2| has the same open options as |file1|.
+}
+
+} // namespace
+} // namespace files
+} // namespace mojo
diff --git a/services/files/files_impl.cc b/services/files/files_impl.cc
new file mode 100644
index 0000000..af48155
--- /dev/null
+++ b/services/files/files_impl.cc
@@ -0,0 +1,99 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/files/files_impl.h"
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "base/files/file_path.h"
+#include "base/files/scoped_file.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/logging.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/posix/eintr_wrapper.h"
+#include "services/files/directory_impl.h"
+
+namespace mojo {
+namespace files {
+
+namespace {
+
+base::ScopedFD CreateAndOpenTemporaryDirectory(
+ scoped_ptr<base::ScopedTempDir>* temp_dir) {
+ (*temp_dir).reset(new base::ScopedTempDir());
+ CHECK((*temp_dir)->CreateUniqueTempDir());
+
+ base::ScopedFD temp_dir_fd(HANDLE_EINTR(
+ open((*temp_dir)->path().value().c_str(), O_RDONLY | O_DIRECTORY, 0)));
+ PCHECK(temp_dir_fd.is_valid());
+ DVLOG(1) << "Made a temporary directory: " << (*temp_dir)->path().value();
+ return temp_dir_fd.Pass();
+}
+
+#ifndef NDEBUG
+base::ScopedFD OpenMojoDebugDirectory() {
+ const char* home_dir_name = getenv("HOME");
+ if (!home_dir_name || !home_dir_name[0]) {
+ LOG(ERROR) << "HOME not set";
+ return base::ScopedFD();
+ }
+ base::FilePath mojo_debug_dir_name =
+ base::FilePath(home_dir_name).Append("MojoDebug");
+ return base::ScopedFD(HANDLE_EINTR(
+ open(mojo_debug_dir_name.value().c_str(), O_RDONLY | O_DIRECTORY, 0)));
+}
+#endif
+
+} // namespace
+
+FilesImpl::FilesImpl(ApplicationConnection* connection,
+ InterfaceRequest<Files> request)
+ : binding_(this, request.Pass()) {
+ // TODO(vtl): record other app's URL
+}
+
+FilesImpl::~FilesImpl() {
+}
+
+void FilesImpl::OpenFileSystem(FileSystem file_system,
+ InterfaceRequest<Directory> directory,
+ const Callback<void(Error)>& callback) {
+ base::ScopedFD dir_fd;
+ // Set only if the |DirectoryImpl| will own a temporary directory.
+ scoped_ptr<base::ScopedTempDir> temp_dir;
+ switch (file_system) {
+ case FILE_SYSTEM_TEMPORARY:
+ // TODO(vtl): ScopedGeneric (hence ScopedFD) doesn't have an operator=!
+ dir_fd.reset(CreateAndOpenTemporaryDirectory(&temp_dir).release());
+ DCHECK(temp_dir);
+ break;
+ case FILE_SYSTEM_DEBUG:
+#ifdef NDEBUG
+ LOG(WARNING) << "~/MojoDebug only available in Debug builds";
+#else
+ // TODO(vtl): ScopedGeneric (hence ScopedFD) doesn't have an operator=!
+ dir_fd.reset(OpenMojoDebugDirectory().release());
+#endif
+ if (!dir_fd.is_valid()) {
+ LOG(ERROR) << "~/MojoDebug unavailable";
+ callback.Run(ERROR_UNAVAILABLE);
+ return;
+ }
+ break;
+ default:
+ LOG(ERROR) << "Unknown file system type: " << file_system;
+ callback.Run(ERROR_UNIMPLEMENTED);
+ return;
+ }
+
+ new DirectoryImpl(directory.Pass(), dir_fd.Pass(), temp_dir.Pass());
+ callback.Run(ERROR_OK);
+}
+
+} // namespace files
+} // namespace mojo
diff --git a/services/files/files_impl.h b/services/files/files_impl.h
new file mode 100644
index 0000000..afd947b
--- /dev/null
+++ b/services/files/files_impl.h
@@ -0,0 +1,38 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_FILES_FILES_IMPL_H_
+#define SERVICES_FILES_FILES_IMPL_H_
+
+#include "base/macros.h"
+#include "mojo/public/cpp/bindings/interface_request.h"
+#include "mojo/public/cpp/bindings/strong_binding.h"
+#include "services/files/files.mojom.h"
+
+namespace mojo {
+
+class ApplicationConnection;
+
+namespace files {
+
+class FilesImpl : public Files {
+ public:
+ FilesImpl(ApplicationConnection* connection, InterfaceRequest<Files> request);
+ ~FilesImpl() override;
+
+ // |Files| implementation:
+ void OpenFileSystem(FileSystem file_system,
+ InterfaceRequest<Directory> directory,
+ const Callback<void(Error)>& callback) override;
+
+ private:
+ StrongBinding<Files> binding_;
+
+ DISALLOW_COPY_AND_ASSIGN(FilesImpl);
+};
+
+} // namespace files
+} // namespace mojo
+
+#endif // SERVICES_FILES_FILES_IMPL_H_
diff --git a/services/files/futimens.h b/services/files/futimens.h
new file mode 100644
index 0000000..9b7a8ae
--- /dev/null
+++ b/services/files/futimens.h
@@ -0,0 +1,26 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_FILES_FUTIMENS_H_
+#define SERVICES_FILES_FUTIMENS_H_
+
+#include <sys/stat.h>
+
+#include "build/build_config.h"
+
+// For Android: The NDK/C library we currently build against doesn't have
+// |futimens()|, but has |utimensat()|. Provide the former (in terms of the
+// latter) for now. Remove this file and futimens_android.cc when |futimens()|
+// becomes generally available (see
+// https://android-review.googlesource.com/#/c/63321/).
+
+#if defined(OS_ANDROID)
+extern "C" {
+
+int futimens(int fd, const struct timespec times[2]);
+
+} // extern "C"
+#endif
+
+#endif // SERVICES_FILES_FUTIMENS_H_
diff --git a/services/files/futimens_android.cc b/services/files/futimens_android.cc
new file mode 100644
index 0000000..035f61d
--- /dev/null
+++ b/services/files/futimens_android.cc
@@ -0,0 +1,13 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/files/futimens.h"
+
+extern "C" {
+
+int futimens(int fd, const struct timespec times[2]) {
+ return utimensat(fd, nullptr, times, 0);
+}
+
+} // extern "C"
diff --git a/services/files/main.cc b/services/files/main.cc
new file mode 100644
index 0000000..9e457c0
--- /dev/null
+++ b/services/files/main.cc
@@ -0,0 +1,44 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/macros.h"
+#include "mojo/application/application_runner_chromium.h"
+#include "mojo/public/c/system/main.h"
+#include "mojo/public/cpp/application/application_connection.h"
+#include "mojo/public/cpp/application/application_delegate.h"
+#include "mojo/public/cpp/application/interface_factory.h"
+#include "services/files/files.mojom.h"
+#include "services/files/files_impl.h"
+
+namespace mojo {
+namespace files {
+
+class FilesApp : public ApplicationDelegate, public InterfaceFactory<Files> {
+ public:
+ FilesApp() {}
+ ~FilesApp() override {}
+
+ private:
+ // |ApplicationDelegate| override:
+ bool ConfigureIncomingConnection(ApplicationConnection* connection) override {
+ connection->AddService<Files>(this);
+ return true;
+ }
+
+ // |InterfaceFactory<Files>| implementation:
+ void Create(ApplicationConnection* connection,
+ InterfaceRequest<Files> request) override {
+ new FilesImpl(connection, request.Pass());
+ }
+
+ DISALLOW_COPY_AND_ASSIGN(FilesApp);
+};
+
+} // namespace files
+} // namespace mojo
+
+MojoResult MojoMain(MojoHandle shell_handle) {
+ mojo::ApplicationRunnerChromium runner(new mojo::files::FilesApp());
+ return runner.Run(shell_handle);
+}
diff --git a/services/files/types.mojom b/services/files/types.mojom
new file mode 100644
index 0000000..4f5f3b4
--- /dev/null
+++ b/services/files/types.mojom
@@ -0,0 +1,99 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo.files;
+
+// Error codes used by the file manager.
+// TODO(vtl): Add more (to, e.g., cover all of errno).
+enum Error {
+ OK = 0,
+ UNKNOWN,
+ INVALID_ARGUMENT,
+ PERMISSION_DENIED,
+ OUT_OF_RANGE,
+ UNIMPLEMENTED,
+ CLOSED,
+ UNAVAILABLE,
+ INTERNAL,
+};
+
+// Used to explain the meaning of an offset within a file.
+enum Whence {
+ // Offset is from current position in the file.
+ FROM_CURRENT = 0,
+ // Offset is relative to the beginning of the file.
+ FROM_START,
+ // Offset is relative to the end of the file.
+ FROM_END,
+};
+
+// Describes (idealized) wall-clock time, since Unix epoch (i.e., since
+// "1970-01-01 00:00 UTC", ignoring leap seconds and that UTC as we know it
+// started in 1972).
+// TODO(vtl): Should probably be moved out of mojo.files (maybe to mojo.time?).
+struct Timespec {
+ int64 seconds;
+ int32 nanoseconds; // Always in the interval [0, 10^9).
+};
+
+// Used for |Touch()| calls. If |now| is set, |timespec| must be null (the time
+// "now" will be used). Otherwise, |timespec| must not be null.
+// TODO(vtl): Use a union instead, when that becomes possible.
+struct TimespecOrNow {
+ bool now;
+ Timespec? timespec;
+};
+
+// Describes various information about a file or directory (for |Stat()|). Note
+// that access/modification times may be set arbitrarily (by those with
+// appropriate capabilities) and may not reflect reality.
+struct FileInformation {
+ // Size of the file, in bytes. Zero for directories.
+ int64 size;
+ // Last access time, if available/supported.
+ Timespec? atime;
+ // Last modification time, if available/supported.
+ Timespec? mtime;
+};
+
+// File and directory open flags (at least one of |kOpenFlagRead| and
+// |kOpenFlagWrite| is required):
+// Opens the file/directory for reading.
+const uint32 kOpenFlagRead = 0x1;
+// Opens the file/directory for writing.
+const uint32 kOpenFlagWrite = 0x2;
+// Only meaningful together with |kOpenFlagWrite|: creates the file if
+// necessary.
+const uint32 kOpenFlagCreate = 0x4;
+// Only meaningful together with |kOpenFlagCreate|: requires file/directory to
+// be created, failing if it already exists.
+const uint32 kOpenFlagExclusive = 0x8;
+// Only meaningful for files, together with |kOpenFlagWrite|: writes will always
+// append to the file.
+const uint32 kOpenFlagAppend = 0x10;
+// Only meaningful for files, together with |kOpenFlagWrite|: truncates the
+// file.
+const uint32 kOpenFlagTruncate = 0x20;
+
+// File types.
+enum FileType {
+ UNKNOWN = 0,
+ REGULAR_FILE,
+ DIRECTORY,
+};
+
+// Describes a directory entry (i.e., a single member of a directory).
+struct DirectoryEntry {
+ FileType type;
+ string name;
+};
+
+// Deletion flags:
+// Only delete if the path refers to a file/non-directory (by default, will
+// delete files and directories).
+const uint32 kDeleteFlagFileOnly = 0x1;
+// Only delete if the path refers to a directory.
+const uint32 kDeleteFlagDirectoryOnly = 0x2;
+// Recursively delete (neither of the two flags above may be specified).
+const uint32 kDeleteFlagRecursive = 0x4;
diff --git a/services/files/util.cc b/services/files/util.cc
new file mode 100644
index 0000000..d2af8cb
--- /dev/null
+++ b/services/files/util.cc
@@ -0,0 +1,103 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/files/util.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include <limits>
+
+#include "base/logging.h"
+#include "base/strings/string_util.h"
+#include "mojo/public/cpp/bindings/string.h"
+
+namespace mojo {
+namespace files {
+
+Error IsPathValid(const String& path) {
+ DCHECK(!path.is_null());
+ if (!base::IsStringUTF8(path.get()))
+ return ERROR_INVALID_ARGUMENT;
+ if (path.size() > 0 && path[0] == '/')
+ return ERROR_PERMISSION_DENIED;
+ return ERROR_OK;
+}
+
+Error IsWhenceValid(Whence whence) {
+ return (whence == WHENCE_FROM_CURRENT || whence == WHENCE_FROM_START ||
+ whence == WHENCE_FROM_END)
+ ? ERROR_OK
+ : ERROR_UNIMPLEMENTED;
+}
+
+Error IsOffsetValid(int64_t offset) {
+ return (offset >= std::numeric_limits<off_t>::min() &&
+ offset <= std::numeric_limits<off_t>::max())
+ ? ERROR_OK
+ : ERROR_OUT_OF_RANGE;
+}
+
+Error ErrnoToError(int errno_value) {
+ // TODO(vtl)
+ return ERROR_UNKNOWN;
+}
+
+int WhenceToStandardWhence(Whence whence) {
+ DCHECK_EQ(IsWhenceValid(whence), ERROR_OK);
+ switch (whence) {
+ case WHENCE_FROM_CURRENT:
+ return SEEK_CUR;
+ case WHENCE_FROM_START:
+ return SEEK_SET;
+ case WHENCE_FROM_END:
+ return SEEK_END;
+ }
+ NOTREACHED();
+ return 0;
+}
+
+Error TimespecToStandardTimespec(const Timespec* in, struct timespec* out) {
+ if (!in) {
+ out->tv_sec = 0;
+ out->tv_nsec = UTIME_OMIT;
+ return ERROR_OK;
+ }
+
+ static_assert(sizeof(int64_t) >= sizeof(time_t), "whoa, time_t is huge");
+ if (in->seconds < std::numeric_limits<time_t>::min() ||
+ in->seconds > std::numeric_limits<time_t>::max())
+ return ERROR_OUT_OF_RANGE;
+
+ if (in->nanoseconds < 0 || in->nanoseconds >= 1000000000)
+ return ERROR_INVALID_ARGUMENT;
+
+ out->tv_sec = static_cast<time_t>(in->seconds);
+ out->tv_nsec = static_cast<long>(in->nanoseconds);
+ return ERROR_OK;
+}
+
+Error TimespecOrNowToStandardTimespec(const TimespecOrNow* in,
+ struct timespec* out) {
+ if (!in) {
+ out->tv_sec = 0;
+ out->tv_nsec = UTIME_OMIT;
+ return ERROR_OK;
+ }
+
+ if (in->now) {
+ if (!in->timespec.is_null())
+ return ERROR_INVALID_ARGUMENT;
+ out->tv_sec = 0;
+ out->tv_nsec = UTIME_NOW;
+ return ERROR_OK;
+ }
+
+ return TimespecToStandardTimespec(in->timespec.get(), out);
+}
+
+} // namespace files
+} // namespace mojo
diff --git a/services/files/util.h b/services/files/util.h
new file mode 100644
index 0000000..324781d
--- /dev/null
+++ b/services/files/util.h
@@ -0,0 +1,55 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_FILES_UTIL_H_
+#define SERVICES_FILES_UTIL_H_
+
+#include "services/files/types.mojom.h"
+
+namespace mojo {
+
+class String;
+
+namespace files {
+
+// Validation functions (typically used to check arguments; they return
+// |ERROR_OK| if valid, else the standard/recommended error for the validation
+// error):
+
+// Checks if |path|, which must be non-null, is (looks like) a valid (relative)
+// path. (On failure, returns |ERROR_INVALID_ARGUMENT| if |path| is not UTF-8,
+// or |ERROR_PERMISSION_DENIED| if it is not relative.)
+Error IsPathValid(const String& path);
+
+// Checks if |whence| is a valid (known) |Whence| value. (On failure, returns
+// |ERROR_UNIMPLEMENTED|.)
+Error IsWhenceValid(Whence whence);
+
+// Checks if |offset| is a valid file offset (from some point); this is
+// implementation-dependent (typically checking if |offset| fits in an |off_t|).
+// (On failure, returns |ERROR_OUT_OF_RANGE|.)
+Error IsOffsetValid(int64_t offset);
+
+// Conversion functions:
+
+// Converts a standard errno value (|E...|) to an |Error| value.
+Error ErrnoToError(int errno_value);
+
+// Converts a |Whence| value to a standard whence value (|SEEK_...|).
+int WhenceToStandardWhence(Whence whence);
+
+// Converts a |Timespec| to a |struct timespec|. If |in| is null, |out->tv_nsec|
+// is set to |UTIME_OMIT|.
+Error TimespecToStandardTimespec(const Timespec* in, struct timespec* out);
+
+// Converts a |TimespecOrNow| to a |struct timespec|. If |in| is null,
+// |out->tv_nsec| is set to |UTIME_OMIT|; if |in->now| is set, |out->tv_nsec| is
+// set to |UTIME_NOW|.
+Error TimespecOrNowToStandardTimespec(const TimespecOrNow* in,
+ struct timespec* out);
+
+} // namespace files
+} // namespace mojo
+
+#endif // SERVICES_FILES_UTIL_H_