blob: f91304f5860b7b45c8a40af6638db263de8db6aa [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use fidl::endpoints::create_proxy;
use fidl_fuchsia_dash::LauncherError;
use fidl_fuchsia_io as fio;
use fidl_fuchsia_kernel as fkernel;
use fuchsia_component::client::connect_to_protocol;
use fuchsia_url::{AbsoluteComponentUrl, AbsolutePackageUrl};
use fuchsia_zircon as zx;
use indexmap::IndexMap;
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use vfs::directory::entry_container::Directory;
use vfs::directory::helper::DirectlyMutable;
use vfs::directory::mutable::connection::MutableConnection;
use vfs::directory::simple::Simple;
use vfs::file::vmo;
// The location of the added trampolines. The path will be of the form:
// `/.dash/tools/<package-name>/<trampoline-name>`.
const BASE_TOOLS_PATH: &str = "/.dash/tools";
// For each package, PkgDir holds the URL and its resolved directory.
#[derive(Debug)] // Necessary for PkgDir to be used in assert_matches!().
struct PkgDir {
pkg_url: AbsolutePackageUrl,
dir: fio::DirectoryProxy,
resource: Option<String>,
}
fn parse_url(url: &str) -> Result<(AbsolutePackageUrl, Option<String>), LauncherError> {
if let Ok(comp_url) = url.parse::<AbsoluteComponentUrl>() {
return Ok((comp_url.package_url().clone(), Some(comp_url.resource().to_string())));
}
Ok((url.parse::<AbsolutePackageUrl>().map_err(|_| LauncherError::BadUrl)?, None))
}
// For each of the given packages, resolve them and create a PkgDir with its URL and DirectoryProxy.
async fn get_pkg_dirs(
package_resolver: &crate::package_resolver::PackageResolver,
tool_urls: Vec<String>,
) -> Result<Vec<PkgDir>, LauncherError> {
let mut dirs: Vec<PkgDir> = vec![];
for url in tool_urls {
let (pkg_url, resource) = parse_url(&url)?;
let dir = package_resolver
.resolve(&pkg_url.to_string())
.await
.map_err(|e| e.while_resolving_tool_package())?;
dirs.push(PkgDir { pkg_url, dir, resource });
}
Ok(dirs)
}
// A Trampoline holds the resolve script contents and the name that the used to run it.
#[derive(Clone)]
struct Trampoline {
contents: String,
binary_name: String,
}
// Using a BTreeSet will ensure that binaries are unique by name and are stored in insertion order.
// The Hash, Ord, PartialOrd, PartialEq, and Eq implementations enable Trampoline to be used in
// BTreeSet and are defined only on the `binary_name` field.
impl Hash for Trampoline {
fn hash<H: Hasher>(&self, state: &mut H) {
self.binary_name.hash(state);
}
}
impl Ord for Trampoline {
fn cmp(&self, other: &Self) -> Ordering {
self.binary_name.cmp(&other.binary_name)
}
}
impl PartialOrd for Trampoline {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Trampoline {
fn eq(&self, other: &Self) -> bool {
self.binary_name == other.binary_name
}
}
impl Eq for Trampoline {}
// A package can have multiple binaries, so Trampolines contains the trampolines for each package.
struct Trampolines {
// Use an IndexMap to maintain the keys in insertion order.
map: IndexMap<String, BTreeSet<Trampoline>>,
}
impl Trampolines {
fn new() -> Self {
Trampolines { map: IndexMap::new() }
}
fn is_empty(&self) -> bool {
self.map.is_empty()
}
// Insert the given set of trampolines into the map. The trampolines are merged in to common
// package names. The trampoline names within packages are not repeated. Return a
// NonUniqueBinaryName if an attempt is made to add a duplicate trampoline name within a
// package.
fn insert(
&mut self,
pkg_name: String,
trampolines: BTreeSet<Trampoline>,
) -> Result<(), LauncherError> {
match self.map.get_mut(&pkg_name) {
None => {
self.map.insert(pkg_name, trampolines);
}
Some(set) => {
for t in &trampolines {
if !set.insert(t.clone()) {
return Err(LauncherError::NonUniqueBinaryName);
}
}
}
}
Ok(())
}
// For each of the entries in the map, create an executable VMO file in a directory named after
// the package. Accumulate these into a tools directory, updating a path string as well. Return
// the directory and the path.
fn make_tools_dir(
&self,
resource: zx::Resource,
) -> Result<(Arc<Simple<MutableConnection>>, String), LauncherError> {
let tools_dir = vfs::mut_pseudo_directory! {};
let mut path = String::new();
for (pkg_name, trampolines) in &self.map {
if !path.is_empty() {
path.push(':');
}
path.push_str(&format!("{}/{}", BASE_TOOLS_PATH, &pkg_name));
let pkg_dir = vfs::mut_pseudo_directory! {};
for trampoline in trampolines {
let read_exec_vmo = make_executable_vmo_file(&resource, &trampoline.contents)?;
pkg_dir
.add_entry(&trampoline.binary_name, read_exec_vmo)
.map_err(|_| LauncherError::Internal)?;
}
tools_dir.add_entry(pkg_name, pkg_dir).map_err(|_| LauncherError::Internal)?;
}
Ok((tools_dir, path))
}
}
// For each package, create the trampoline specifications for each of its binaries.
// For the user's information, a directory entry will be created even if there are no
// binaries found.
async fn create_trampolines(pkg_dirs: &Vec<PkgDir>) -> Result<Trampolines, LauncherError> {
let mut trampolines = Trampolines::new();
for pkg_dir in pkg_dirs {
match &pkg_dir.resource {
Some(res) => {
let contents = format!("#!resolve {}#{}\n", &pkg_dir.pkg_url, res);
let binary_name =
res.split('/').next_back().ok_or_else(|| LauncherError::BadUrl)?.to_string();
let pkg_name = pkg_dir.pkg_url.name().to_string();
let mut set = BTreeSet::new();
set.insert(Trampoline { contents, binary_name });
trampolines.insert(pkg_name, set)?;
}
None => {
// Read the package binaries.
let bin_dir = fuchsia_fs::directory::open_directory(
&pkg_dir.dir,
"bin",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)
.await
.map_err(|_| LauncherError::ToolsBinaryRead)?;
let entries = fuchsia_fs::directory::readdir(&bin_dir)
.await
.map_err(|_| LauncherError::Internal)?;
// Create the trampoline specifications.
let mut pkg_trampolines = BTreeSet::new();
for entry in entries {
if entry.kind == fio::DirentType::File {
let contents =
format!("#!resolve {}#bin/{}\n", &pkg_dir.pkg_url, entry.name);
let binary_name = entry.name;
pkg_trampolines.insert(Trampoline { contents, binary_name });
}
}
// Package names are used in the directory layout to allow binary names to repeat
// among packages. However, the pkg_name uses only the URL's name(), dropping the
// host, variant, and hash, if present. The result is that the paths shown to the
// user will be readable, like `/.dash/tools/hello-world/hello_world_rust` and
// `/.dash/tools/debug-dash-launcher/ls`. However, as only unique package names can
// be added to a directory, this means that binaries cannot be added from packages
// differing only the host, variant, or hash. The simple workaround is to add such
// packages in separate invocations of `ffx component explore`.
let pkg_name = pkg_dir.pkg_url.name().to_string();
trampolines.insert(pkg_name, pkg_trampolines)?;
}
}
}
Ok(trampolines)
}
async fn create_vmex_resource() -> Result<zx::Resource, LauncherError> {
let vmex_proxy = connect_to_protocol::<fkernel::VmexResourceMarker>()
.map_err(|_| LauncherError::VmexResource)?;
let vmex_resource = vmex_proxy.get().await.map_err(|_| LauncherError::VmexResource)?;
Ok(zx::Resource::from(vmex_resource))
}
fn make_executable_vmo_file(
resource: &zx::Resource,
contents: &str,
) -> Result<Arc<vmo::VmoFile>, LauncherError> {
let vmo = zx::Vmo::create(contents.len() as u64).map_err(|_| LauncherError::Internal)?;
vmo.write(contents.as_bytes(), 0).map_err(|_| LauncherError::Internal)?;
// Make it into a VMO that can be loaded as an executable.
let exec_vmo = vmo.replace_as_executable(&resource).map_err(|_| LauncherError::Internal)?;
let exec_file = vmo::VmoFile::new(
exec_vmo, /*readable*/ true, /*writable*/ false, /*executable*/ true,
);
Ok(exec_file)
}
// Return a proxy to the given directory, first opening it as executable.
fn directory_to_proxy(
dir: Arc<Simple<MutableConnection>>,
) -> Result<fio::DirectoryProxy, LauncherError> {
let (client, server) =
create_proxy::<fio::DirectoryMarker>().map_err(|_| LauncherError::Internal)?;
let scope = vfs::execution_scope::ExecutionScope::new();
dir.open(
scope.clone(),
fio::OpenFlags::RIGHT_READABLE
| fio::OpenFlags::RIGHT_WRITABLE
| fio::OpenFlags::RIGHT_EXECUTABLE,
vfs::path::Path::dot(),
server.into_channel().into(),
);
Ok(client)
}
// Given a list of package trampoline specifications, create the executable files and add them into
// a new directory. Give each package its own subdirectory based on the package name, preventing
// binary name collisions. Binary names can repeat if they are found in different packages. However,
// only the package name is considered, so packages cannot differ only in host, variant, or hash.
//
// The package directories are added in the order they are given on the command line. The enclosing
// directory is prepended to the path environment variable. The resulting preference order for
// same-name binaries is therefore first packages in load order, then built-ins. To avoid shadowing,
// use the complete path to the binary: $ .dash/tools/<pkg>/<binary>
//
// Return the VFS directory containing the executables and the associated path environment variable.
async fn make_trampoline_vfs(
trampolines: Trampolines,
) -> Result<(Option<fio::DirectoryProxy>, Option<String>), LauncherError> {
if trampolines.is_empty() {
return Ok((None, None));
}
let resource = create_vmex_resource().await?;
let (tools_dir, path) = trampolines.make_tools_dir(resource)?;
// Return it as an executable directory.
let dir = directory_to_proxy(tools_dir)?;
Ok((Some(dir), Some(path)))
}
// Given the URLs of some packages, return a directory containing their binaries as trampolines.
pub async fn create_trampolines_from_packages(
package_resolver: &crate::package_resolver::PackageResolver,
pkg_urls: Vec<String>,
) -> Result<(Option<fio::DirectoryProxy>, Option<String>), LauncherError> {
if pkg_urls.is_empty() {
return Ok((None, None));
}
let pkg_dirs = get_pkg_dirs(package_resolver, pkg_urls).await?;
let trampolines = create_trampolines(&pkg_dirs).await?;
make_trampoline_vfs(trampolines).await
}
// Create a PATH environment variable from the tools_path if present.
pub fn create_env_path(tools_path: Option<String>) -> String {
let mut path_envvar = "".to_string();
if let Some(tp) = tools_path {
path_envvar.push_str("PATH=");
path_envvar.push_str(&tp.to_string());
}
path_envvar
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use fidl::endpoints::create_proxy_and_stream;
use fidl_fuchsia_pkg as fpkg;
use fio::OpenFlags;
use fuchsia_async as fasync;
use fuchsia_fs::directory::open_file;
use fuchsia_fs::file::read_to_string;
use futures::StreamExt;
use std::fmt;
use vfs::directory::immutable::connection::ImmutableConnection;
use vfs::execution_scope::ExecutionScope;
use vfs::file::vmo::read_only;
#[fuchsia::test]
async fn parse_url_test() {
assert_matches!(parse_url(""), Err(LauncherError::BadUrl));
assert_matches!(parse_url("f"), Err(LauncherError::BadUrl));
assert_matches!(parse_url("fuchsia-pk://h/n#foo"), Err(LauncherError::BadUrl));
assert_matches!(parse_url("fuchsia-pkg#://h/n#"), Err(LauncherError::BadUrl));
assert_matches!(parse_url("fuchsia-pkg#://h/n"), Err(LauncherError::BadUrl));
assert_matches!(parse_url("fuchsia-pkg://h"), Err(LauncherError::BadUrl));
assert_matches!(parse_url("fuchsia-pkg://h/#"), Err(LauncherError::BadUrl));
assert_matches!(parse_url("fuchsia-pkg://📡/n#foo"), Err(LauncherError::BadUrl));
let (abs_url, res) = parse_url("fuchsia-pkg://h/n").unwrap();
assert_eq!(&abs_url.to_string(), "fuchsia-pkg://h/n");
assert_eq!(res, None);
// Multiple #'s allowed by fuchsia_url's parser!
let (abs_url, res) = parse_url("fuchsia-pkg://h/n###foo").unwrap();
assert_eq!(&abs_url.to_string(), "fuchsia-pkg://h/n");
assert_eq!(res, Some("##foo".to_string()));
let (abs_url, res) = parse_url("fuchsia-pkg://h/n#foo/bar").unwrap();
assert_eq!(&abs_url.to_string(), "fuchsia-pkg://h/n");
assert_eq!(res, Some("foo/bar".to_string()));
let (abs_url, res) = parse_url("fuchsia-pkg://earth.org/spacex_pkg/variant0#foo").unwrap();
assert_eq!(&abs_url.to_string(), "fuchsia-pkg://earth.org/spacex_pkg/variant0");
assert_eq!(res, Some("foo".to_string()));
let (abs_url, res) = parse_url("fuchsia-pkg://earth.org/spacex_pkg?hash=0000000000000000000000000000000000000000000000000000000000000000#foo").unwrap();
assert_eq!(&abs_url.to_string(), "fuchsia-pkg://earth.org/spacex_pkg?hash=0000000000000000000000000000000000000000000000000000000000000000");
assert_eq!(res, Some("foo".to_string()));
let (abs_url, res) = parse_url("fuchsia-pkg://earth.org/spacex_pkg/variant0?hash=1000000000000000000000000000000000000000000000000000000000000000#foo").unwrap();
assert_eq!(&abs_url.to_string(), "fuchsia-pkg://earth.org/spacex_pkg/variant0?hash=1000000000000000000000000000000000000000000000000000000000000000");
assert_eq!(res, Some("foo".to_string()));
}
#[fuchsia::test]
async fn get_pkg_dirs_test() {
let (resolver, mut stream) =
create_proxy_and_stream::<fpkg::PackageResolverMarker>().unwrap();
// Spawn a task to handle the stream of requests
fasync::Task::spawn(async move {
while let Some(Ok(request)) = stream.next().await {
match request {
fpkg::PackageResolverRequest::Resolve { responder, package_url: _, dir: _ } => {
responder
.send(Ok(&fidl_fuchsia_pkg::ResolutionContext { bytes: vec![] }))
.unwrap();
}
_ => {}
}
}
})
.detach();
let resolver = crate::package_resolver::PackageResolver::new_test(resolver);
// Empty package list.
assert!(get_pkg_dirs(&resolver, vec![]).await.unwrap().is_empty());
// Non-empty package list, but with a malformed URL.
assert_matches!(
get_pkg_dirs(&resolver, vec!["".to_string()]).await,
Err(LauncherError::BadUrl)
);
// Valid package list.
let v = get_pkg_dirs(&resolver, vec!["fuchsia-pkg://h/n".to_string()]).await.unwrap();
assert!(v.len() == 1);
assert_eq!(v[0].pkg_url.host().to_string(), "h".to_string());
assert_eq!(v[0].pkg_url.name().to_string(), "n".to_string());
assert_eq!(v[0].pkg_url.variant(), None);
assert_eq!(v[0].pkg_url.hash(), None);
}
// Required for assert_matches!().
impl fmt::Debug for Trampolines {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "map size: {}", self.map.len())
}
}
impl Trampolines {
fn len(&self) -> usize {
self.map.len()
}
fn get_nth_package(&self, i: usize) -> Option<(&String, &BTreeSet<Trampoline>)> {
self.map.iter().nth(i)
}
}
async fn make_pkg(url: &str, name: &str, root: &Arc<Simple<ImmutableConnection>>) -> PkgDir {
let (pkg_url, resource) = parse_url(&url.to_string()).unwrap();
let (dir, server_end) =
create_proxy::<fio::DirectoryMarker>().expect("Failed to create connection endpoints");
let flags = fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE;
root.clone().open(
ExecutionScope::new(),
flags,
name.try_into().unwrap(),
server_end.into_channel().into(),
);
PkgDir { pkg_url, dir, resource }
}
fn check_trampoline(
trampolines: &BTreeSet<Trampoline>,
bin: usize,
contents: &str,
binary_name: &str,
) {
let trampoline = trampolines.iter().nth(bin).unwrap();
assert_eq!(trampoline.contents, contents);
assert_eq!(trampoline.binary_name, binary_name);
}
#[fuchsia::test]
async fn create_trampolines_test() {
async fn create_test_directory_proxies() -> Vec<PkgDir> {
let root = vfs::pseudo_directory! {
"Nasa" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {
"go2moon_v1969" => read_only(b"Apollo"),
"go2moon_v2024" => read_only(b"Artemis"),
},
},
"SpaceX" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {
"go2orbit" => read_only(b"Falcon 9"),
"go2mars" => read_only(b"Starship"),
"bogus_dir" => vfs::pseudo_directory! {"bogus_file" => read_only(b"bogus_content"),}
},
},
"BlueOrigin" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {},
},
};
vec![
make_pkg("fuchsia-pkg://earth.org/nasa_pkg", "Nasa", &root).await,
make_pkg("fuchsia-pkg://earth.org/spacex_pkg", "SpaceX", &root).await,
make_pkg("fuchsia-pkg://earth.org/blueorigin_pkg", "BlueOrigin", &root).await,
]
}
let pkg_dirs = create_test_directory_proxies().await;
let pkg_trampolines = create_trampolines(&pkg_dirs).await.unwrap();
assert_eq!(pkg_trampolines.len(), 3); // Includes one for "blueorigin_pkg", which had no binaries.
let (name, list) = pkg_trampolines.get_nth_package(0).unwrap();
assert_eq!(list.len(), 2);
assert_eq!(name, "nasa_pkg");
check_trampoline(
&list,
0,
"#!resolve fuchsia-pkg://earth.org/nasa_pkg#bin/go2moon_v1969\n",
"go2moon_v1969",
);
check_trampoline(
&list,
1,
"#!resolve fuchsia-pkg://earth.org/nasa_pkg#bin/go2moon_v2024\n",
"go2moon_v2024",
);
let (name, list) = pkg_trampolines.get_nth_package(1).unwrap();
assert_eq!(list.len(), 2);
assert_eq!(name, "spacex_pkg");
check_trampoline(
&list,
0,
"#!resolve fuchsia-pkg://earth.org/spacex_pkg#bin/go2mars\n",
"go2mars",
);
check_trampoline(
&list,
1,
"#!resolve fuchsia-pkg://earth.org/spacex_pkg#bin/go2orbit\n",
"go2orbit",
);
let (name, list) = pkg_trampolines.get_nth_package(2).unwrap();
assert_eq!(list.len(), 0);
assert_eq!(name, "blueorigin_pkg");
}
#[fuchsia::test]
async fn trampolines_binary_collisions_test() {
// Create two packages containing the same binary name.
let root = vfs::pseudo_directory! {
"Nasa" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {
"collision" => read_only(b"Apollo"),
},
},
"SpaceX" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {
"collision" => read_only(b"Falcon 9"),
},
},
};
// But use URLs that have unique URLs.
let pkg_dirs = vec![
make_pkg("fuchsia-pkg://earth.org/nasa_pkg", "Nasa", &root).await,
make_pkg("fuchsia-pkg://earth.org/spacex_pkg", "SpaceX", &root).await,
];
let pkg_trampolines = create_trampolines(&pkg_dirs).await.unwrap();
// Below, the trampolines are both named `collision`. However, they are in different
// packages, so do not conflict.
let (name, list) = pkg_trampolines.get_nth_package(0).unwrap();
assert_eq!(name, "nasa_pkg");
assert_eq!(list.len(), 1);
check_trampoline(
&list,
0,
"#!resolve fuchsia-pkg://earth.org/nasa_pkg#bin/collision\n",
"collision",
);
let (name, list) = pkg_trampolines.get_nth_package(1).unwrap();
assert_eq!(name, "spacex_pkg");
assert_eq!(list.len(), 1);
check_trampoline(
&list,
0,
"#!resolve fuchsia-pkg://earth.org/spacex_pkg#bin/collision\n",
"collision",
);
// This next set includes hashes and variants, which are ignored. So these collide.
assert_matches!(
create_trampolines(&vec![
make_pkg("fuchsia-pkg://earth.org/spacex_pkg", "SpaceX", &root).await,
make_pkg("fuchsia-pkg://earth.org/spacex_pkg/variant0", "SpaceX", &root).await,
])
.await,
Err(LauncherError::NonUniqueBinaryName)
);
assert_matches!(
create_trampolines(&vec![
make_pkg("fuchsia-pkg://earth.org/spacex_pkg", "SpaceX", &root).await,
make_pkg("fuchsia-pkg://earth.org/spacex_pkg?hash=0000000000000000000000000000000000000000000000000000000000000000", "SpaceX", &root).await,
]).await,
Err(LauncherError::NonUniqueBinaryName)
);
assert_matches!(
create_trampolines(&vec![
make_pkg("fuchsia-pkg://earth.org/spacex_pkg", "SpaceX", &root).await,
make_pkg("fuchsia-pkg://earth.org/spacex_pkg/variant0?hash=1000000000000000000000000000000000000000000000000000000000000000", "SpaceX", &root).await,
]).await,
Err(LauncherError::NonUniqueBinaryName)
);
assert_matches!(
create_trampolines(&vec![
make_pkg("fuchsia-pkg://earth.org/spacex_pkg/variant0", "SpaceX", &root).await,
make_pkg("fuchsia-pkg://earth.org/spacex_pkg/variant0?hash=1000000000000000000000000000000000000000000000000000000000000000", "SpaceX", &root).await,
]).await,
Err(LauncherError::NonUniqueBinaryName)
);
}
#[fuchsia::test]
async fn trampolines_package_collisions_test() {
// Create two packages containing the same package name, but different binaries.
let root = vfs::pseudo_directory! {
"UpGoer" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {
"apollo" => read_only(b"Apollo"),
},
}
};
let root1 = vfs::pseudo_directory! {
"UpGoer" => vfs::pseudo_directory! {
"bin" => vfs::pseudo_directory! {
"falcon9" => read_only(b"Falcon 9"),
},
}
};
// The package names, being the same, are merged. The binaries are different, so do not
// conflict.
let pkg_trampolines = create_trampolines(&vec![
make_pkg("fuchsia-pkg://earth.org/upgoer_pkg", "UpGoer", &root).await,
make_pkg("fuchsia-pkg://earth.org/upgoer_pkg", "UpGoer", &root1).await,
])
.await
.unwrap();
let (name, list) = pkg_trampolines.get_nth_package(0).unwrap();
assert_eq!(name, "upgoer_pkg");
assert_eq!(list.len(), 2);
check_trampoline(
&list,
0,
"#!resolve fuchsia-pkg://earth.org/upgoer_pkg#bin/apollo\n",
"apollo",
);
check_trampoline(
&list,
1,
"#!resolve fuchsia-pkg://earth.org/upgoer_pkg#bin/falcon9\n",
"falcon9",
);
}
#[fuchsia::test]
async fn make_trampoline_vfs_test_empty() {
let (dirs, path) = make_trampoline_vfs(Trampolines::new()).await.unwrap();
assert_matches!(dirs, None);
assert_matches!(path, None);
}
#[fuchsia::test]
async fn make_trampoline_vfs_test_if_package_repeated() {
let mut set = BTreeSet::new();
set.insert(Trampoline { contents: "foo".to_string(), binary_name: "bar".to_string() });
let mut set2 = BTreeSet::new();
set2.insert(Trampoline { contents: "foo2".to_string(), binary_name: "bar2".to_string() });
let mut trampolines = Trampolines::new();
trampolines.insert("pkg_foobar".to_string(), set).unwrap();
trampolines.insert("pkg_foobar".to_string(), set2).unwrap();
let (dir, path) = make_trampoline_vfs(trampolines).await.unwrap();
assert!(dir.is_some());
// There is only one entry in the path for pkg_foobar.
assert_eq!(path.unwrap(), "/.dash/tools/pkg_foobar");
}
#[fuchsia::test]
async fn make_trampoline_vfs_test() {
async fn contents_of(path: &str, dir: &fio::DirectoryProxy) -> String {
let file =
open_file(dir, path, OpenFlags::RIGHT_READABLE).await.expect("could not open file");
read_to_string(&file)
.await
.unwrap_or_else(|e| panic!("could not open file: {}: {:?}", path, e))
}
let mut pkg_trampolines = Trampolines::new();
pkg_trampolines
.insert(
"pkg_foobar".to_string(),
BTreeSet::from([
Trampoline {
contents: "#!resolve foo".to_string(),
binary_name: "foo".to_string(),
},
Trampoline {
contents: "#!resolve bar".to_string(),
binary_name: "bar".to_string(),
},
]),
)
.unwrap();
pkg_trampolines
.insert(
"pkg_foobar2".to_string(),
BTreeSet::from([
Trampoline {
contents: "#!resolve foo2".to_string(),
binary_name: "foo2".to_string(),
},
Trampoline {
contents: "#!resolve bar2".to_string(),
binary_name: "bar2".to_string(),
},
]),
)
.unwrap();
let (dirs, path) = make_trampoline_vfs(pkg_trampolines).await.unwrap();
// Check the path. Order is insertion order.
assert_eq!(path, Some("/.dash/tools/pkg_foobar:/.dash/tools/pkg_foobar2".to_string()));
// Check the directory.
let dir = dirs.unwrap();
assert_eq!(
fuchsia_fs::directory::readdir(&dir).await.unwrap(),
vec![
fuchsia_fs::directory::DirEntry {
name: "pkg_foobar".to_string(),
kind: fuchsia_fs::directory::DirentKind::Directory
},
fuchsia_fs::directory::DirEntry {
name: "pkg_foobar2".to_string(),
kind: fuchsia_fs::directory::DirentKind::Directory
},
]
);
assert_eq!(&contents_of("pkg_foobar/foo", &dir).await, "#!resolve foo");
assert_eq!(&contents_of("pkg_foobar/bar", &dir).await, "#!resolve bar");
assert_eq!(&contents_of("pkg_foobar2/foo2", &dir).await, "#!resolve foo2");
assert_eq!(&contents_of("pkg_foobar2/bar2", &dir).await, "#!resolve bar2");
}
#[fuchsia::test]
async fn create_env_path_test() {
assert_eq!(create_env_path(Some("bar".to_string())), "PATH=bar");
assert_eq!(create_env_path(Some("".to_string())), "PATH=");
assert_eq!(create_env_path(Some(" ".to_string())), "PATH= ");
assert_eq!(create_env_path(None), "");
}
}