Use a test helper executable to open files
In the test, we used a trick to open files from the shell, the pass the
FDs to fd_server. It will not work for directories.
This change replaces the trick with a test helper executable, which
supports opening directories as FD, in order to make the future test
cases that involved directory FD possible.
Bug: 203251769
Test: AuthFsHostTest
Change-Id: I77fdfa463f168bce99d1fccc91d0739bbc6ba00a
diff --git a/authfs/tests/open_then_run.rs b/authfs/tests/open_then_run.rs
new file mode 100644
index 0000000..ba3ed38
--- /dev/null
+++ b/authfs/tests/open_then_run.rs
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! This is a test helper program that opens files and/or directories, then passes the file
+//! descriptors to the specified command. When passing the file descriptors, they are mapped to the
+//! specified numbers in the child process.
+
+use anyhow::{bail, Context, Result};
+use clap::{App, Arg, Values};
+use command_fds::{CommandFdExt, FdMapping};
+use log::{debug, error};
+use nix::{dir::Dir, fcntl::OFlag, sys::stat::Mode};
+use std::fs::{File, OpenOptions};
+use std::os::unix::io::{AsRawFd, RawFd};
+use std::process::Command;
+
+// `PseudoRawFd` is just an integer and not necessarily backed by a real FD. It is used to denote
+// the expecting FD number, when trying to set up FD mapping in the child process. The intention
+// with this alias is to improve readability by distinguishing from actual RawFd.
+type PseudoRawFd = RawFd;
+
+struct FileMapping<T: AsRawFd> {
+ file: T,
+ target_fd: PseudoRawFd,
+}
+
+impl<T: AsRawFd> FileMapping<T> {
+ fn as_fd_mapping(&self) -> FdMapping {
+ FdMapping { parent_fd: self.file.as_raw_fd(), child_fd: self.target_fd }
+ }
+}
+
+struct Args {
+ ro_files: Vec<FileMapping<File>>,
+ rw_files: Vec<FileMapping<File>>,
+ dir_files: Vec<FileMapping<Dir>>,
+ cmdline_args: Vec<String>,
+}
+
+fn parse_and_create_file_mapping<F, T>(
+ values: Option<Values<'_>>,
+ opener: F,
+) -> Result<Vec<FileMapping<T>>>
+where
+ F: Fn(&str) -> Result<T>,
+ T: AsRawFd,
+{
+ if let Some(options) = values {
+ options
+ .map(|option| {
+ // Example option: 10:/some/path
+ let strs: Vec<&str> = option.split(':').collect();
+ if strs.len() != 2 {
+ bail!("Invalid option: {}", option);
+ }
+ let fd = strs[0].parse::<PseudoRawFd>().context("Invalid FD format")?;
+ let path = strs[1];
+ Ok(FileMapping { target_fd: fd, file: opener(path)? })
+ })
+ .collect::<Result<_>>()
+ } else {
+ Ok(Vec::new())
+ }
+}
+
+fn parse_args() -> Result<Args> {
+ #[rustfmt::skip]
+ let matches = App::new("open_then_run")
+ .arg(Arg::with_name("open-ro")
+ .long("open-ro")
+ .value_name("FD:PATH")
+ .help("Open <PATH> read-only to pass as fd <FD>")
+ .multiple(true)
+ .number_of_values(1))
+ .arg(Arg::with_name("open-rw")
+ .long("open-rw")
+ .value_name("FD:PATH")
+ .help("Open/create <PATH> read-write to pass as fd <FD>")
+ .multiple(true)
+ .number_of_values(1))
+ .arg(Arg::with_name("open-dir")
+ .long("open-dir")
+ .value_name("FD:DIR")
+ .help("Open <DIR> to pass as fd <FD>")
+ .multiple(true)
+ .number_of_values(1))
+ .arg(Arg::with_name("args")
+ .help("Command line to execute with pre-opened FD inherited")
+ .last(true)
+ .required(true)
+ .multiple(true))
+ .get_matches();
+
+ let ro_files = parse_and_create_file_mapping(matches.values_of("open-ro"), |path| {
+ OpenOptions::new().read(true).open(path).with_context(|| format!("Open {} read-only", path))
+ })?;
+
+ let rw_files = parse_and_create_file_mapping(matches.values_of("open-rw"), |path| {
+ OpenOptions::new()
+ .read(true)
+ .write(true)
+ .create(true)
+ .open(path)
+ .with_context(|| format!("Open {} read-write", path))
+ })?;
+
+ let dir_files = parse_and_create_file_mapping(matches.values_of("open-dir"), |path| {
+ Dir::open(path, OFlag::O_DIRECTORY | OFlag::O_RDWR, Mode::S_IRWXU)
+ .with_context(|| format!("Open {} directory", path))
+ })?;
+
+ let cmdline_args: Vec<_> = matches.values_of("args").unwrap().map(|s| s.to_string()).collect();
+
+ Ok(Args { ro_files, rw_files, dir_files, cmdline_args })
+}
+
+fn try_main() -> Result<()> {
+ let args = parse_args()?;
+
+ let mut command = Command::new(&args.cmdline_args[0]);
+ command.args(&args.cmdline_args[1..]);
+
+ // Set up FD mappings in the child process.
+ let mut fd_mappings = Vec::new();
+ fd_mappings.extend(args.ro_files.iter().map(FileMapping::as_fd_mapping));
+ fd_mappings.extend(args.rw_files.iter().map(FileMapping::as_fd_mapping));
+ fd_mappings.extend(args.dir_files.iter().map(FileMapping::as_fd_mapping));
+ command.fd_mappings(fd_mappings)?;
+
+ debug!("Spawning {:?}", command);
+ command.spawn()?;
+ Ok(())
+}
+
+fn main() {
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("open_then_run")
+ .with_min_level(log::Level::Debug),
+ );
+
+ if let Err(e) = try_main() {
+ error!("Failed with {:?}", e);
+ std::process::exit(1);
+ }
+}