[io_util] Add create_directory helper

In fuchsia.io, there is no equivalent to mkdir(2), and clients are
expected to use Open with special flags set (mainly (mode & S_IFDIR)).
Add a helper create_directory to encapsulate this.

Tested: Unit.
Change-Id: Iff08132e4abe75b71aa11461e12a77327fd2f5c0
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/474637
Fuchsia-Auto-Submit: James Sullivan <jfsulliv@google.com>
Reviewed-by: Stephen Demos <sdemos@google.com>
Commit-Queue: James Sullivan <jfsulliv@google.com>
diff --git a/garnet/lib/rust/io_util/src/directory.rs b/garnet/lib/rust/io_util/src/directory.rs
index 338f7c2..14cbd91 100644
--- a/garnet/lib/rust/io_util/src/directory.rs
+++ b/garnet/lib/rust/io_util/src/directory.rs
@@ -67,6 +67,35 @@
     node::verify_directory_describe_event(dir).await
 }
 
+/// Creates a directory named `path` within the `parent` directory.
+pub async fn create_directory(
+    parent: &DirectoryProxy,
+    path: &str,
+    flags: u32,
+) -> Result<DirectoryProxy, OpenError> {
+    let (dir, server_end) =
+        fidl::endpoints::create_proxy::<DirectoryMarker>().map_err(OpenError::CreateProxy)?;
+
+    // NB: POSIX does not allow open(2) to create dirs, but fuchsia.io does not have an equivalent
+    // of mkdir(2), so on Fuchsia we're expected to call open on a DirectoryMarker with (flags &
+    // OPEN_FLAG_CREATE) set.
+    // (mode & MODE_TYPE_DIRECTORY) is also required, although it is redundant (the fact that we
+    // opened a DirectoryMarker is the main way that the underlying filesystem understands our
+    // intention.)
+    let flags = flags
+        | fidl_fuchsia_io::OPEN_FLAG_CREATE
+        | fidl_fuchsia_io::OPEN_FLAG_DIRECTORY
+        | fidl_fuchsia_io::OPEN_FLAG_DESCRIBE;
+    let mode = fidl_fuchsia_io::MODE_TYPE_DIRECTORY;
+
+    parent
+        .open(flags, mode, path, ServerEnd::new(server_end.into_channel()))
+        .map_err(OpenError::SendOpenRequest)?;
+
+    // wait for the directory to open and report success.
+    node::verify_directory_describe_event(dir).await
+}
+
 /// Opens the given `path` from the given `parent` directory as a [`FileProxy`]. The target is not
 /// verified to be any particular type and may not implement the fuchsia.io.File protocol.
 pub fn open_file_no_describe(
@@ -160,10 +189,14 @@
 mod tests {
     use {
         super::*,
-        crate::{OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE},
-        fidl_fuchsia_io as fio, fuchsia_async as fasync,
+        fidl_fuchsia_io as fio,
+        fidl_fuchsia_io::{
+            OPEN_FLAG_CREATE, OPEN_FLAG_CREATE_IF_ABSENT, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE,
+        },
+        fuchsia_async as fasync,
         futures::prelude::*,
         matches::assert_matches,
+        tempfile::TempDir,
         vfs::{
             directory::entry::DirectoryEntry,
             execution_scope::ExecutionScope,
@@ -178,6 +211,16 @@
         open_in_namespace("/pkg", OPEN_RIGHT_READABLE).unwrap()
     }
 
+    fn open_tmp() -> (TempDir, DirectoryProxy) {
+        let tempdir = TempDir::new().expect("failed to create tmp dir");
+        let proxy = open_in_namespace(
+            tempdir.path().to_str().unwrap(),
+            OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE,
+        )
+        .unwrap();
+        (tempdir, proxy)
+    }
+
     // open_in_namespace
 
     #[fasync::run_singlethreaded(test)]
@@ -247,6 +290,52 @@
         );
     }
 
+    // create_directory
+
+    #[fasync::run_singlethreaded(test)]
+    async fn create_directory_simple() {
+        let (_tmp, proxy) = open_tmp();
+        let dir = create_directory(&proxy, "dir", OPEN_RIGHT_READABLE).await.unwrap();
+        crate::directory::close(dir).await.unwrap();
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn create_directory_add_file() {
+        let (_tmp, proxy) = open_tmp();
+        let dir = create_directory(&proxy, "dir", OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE)
+            .await
+            .unwrap();
+        let file = open_file(
+            &dir,
+            "data",
+            OPEN_FLAG_CREATE | OPEN_FLAG_CREATE_IF_ABSENT | OPEN_RIGHT_READABLE,
+        )
+        .await
+        .unwrap();
+        crate::file::close(file).await.unwrap();
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn create_directory_existing_dir_opens() {
+        let (_tmp, proxy) = open_tmp();
+        let dir = create_directory(&proxy, "dir", OPEN_RIGHT_READABLE).await.unwrap();
+        crate::directory::close(dir).await.unwrap();
+        create_directory(&proxy, "dir", OPEN_RIGHT_READABLE).await.unwrap();
+    }
+
+    #[fasync::run_singlethreaded(test)]
+    async fn create_directory_existing_dir_fails_if_flag_set() {
+        let (_tmp, proxy) = open_tmp();
+        let dir = create_directory(&proxy, "dir", OPEN_FLAG_CREATE_IF_ABSENT | OPEN_RIGHT_READABLE)
+            .await
+            .unwrap();
+        crate::directory::close(dir).await.unwrap();
+        assert_matches!(
+            create_directory(&proxy, "dir", OPEN_FLAG_CREATE_IF_ABSENT | OPEN_RIGHT_READABLE).await,
+            Err(_)
+        );
+    }
+
     // open_file_no_describe
 
     #[fasync::run_singlethreaded(test)]
@@ -423,10 +512,7 @@
 
         assert_matches!(
             stream.next().await,
-            Some(Ok(fio::DirectoryRequest::Clone {
-                flags: 42,
-                ..
-            }))
+            Some(Ok(fio::DirectoryRequest::Clone { flags: 42, .. }))
         );
     }
 }