From 0b78fae7b99082907b64e9f867ea7cfe10c22a64 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 3 Aug 2018 11:47:55 -0400 Subject: [PATCH] Add skeleton reimplementation of sandboxfs in Rust (#23) This change adds a barebones reimplementation of sandboxfs in Rust. As things are now, the new version does nothing: it just provides trivial command-line processing and mounts an empty FUSE file system. However, this change puts all pieces in place to continue development of this new version. In particular, this adjusts the Travis CI configuration to support running the exact same integration tests as we run for the Go implementation against the new code, and currently blacklists all of them because the new code passes none. --- .gitignore | 2 + .travis.yml | 6 ++ .vscode/settings.json.in | 5 ++ Cargo.toml | 21 +++++++ admin/lint/lint.go | 11 +++- admin/travis-build.sh | 122 ++++++++++++++++++++++++++++++++++++++- admin/travis-install.sh | 12 ++++ src/lib.rs | 46 +++++++++++++++ src/main.rs | 117 +++++++++++++++++++++++++++++++++++++ 9 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index a502f74..36efb92 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /.gopath /.gopath-tools /bazel-* +Cargo.lock +target diff --git a/.travis.yml b/.travis.yml index 64a72b9..f8d1e04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ env: - DO=bazel - DO=gotools - DO=lint + - DO=rust before_install: ./admin/travis-install.sh script: ./admin/travis-build.sh @@ -51,3 +52,8 @@ matrix: go: 1.8 - env: DO=lint os: osx + + # For testing the Rust implementation, we only need to build the tests using + # one Go version. + - env: DO=rust + go: 1.8 diff --git a/.vscode/settings.json.in b/.vscode/settings.json.in index 7c96be0..90a76aa 100644 --- a/.vscode/settings.json.in +++ b/.vscode/settings.json.in @@ -41,6 +41,11 @@ "editor.quickSuggestions": false }, + "[rust]": { + "editor.rulers": [100], + "editor.wordWrapColumn": 100 + }, + "[troff]": { "editor.quickSuggestions": false } diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2cd1e09 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = ["Julio Merino "] +categories = ["filesystems"] +description = "A virtual file system for sandboxing" +homepage = "https://github.com/bazelbuild/sandboxfs" +keywords = ["bazel", "filesystem", "fuse", "sandboxing"] +license = "Apache-2.0" +name = "sandboxfs" +readme = "README.md" +repository = "https://github.com/bazelbuild/sandboxfs" +version = "0.1.0" + +[badges] +travis-ci = { repository = "bazelbuild/sandboxfs", branch = "master" } + +[dependencies] +env_logger = "0.5" +failure = "0.1" +fuse = "0.3" +getopts = "0.2" +log = "0.4" diff --git a/admin/lint/lint.go b/admin/lint/lint.go index 6a78f07..2138181 100644 --- a/admin/lint/lint.go +++ b/admin/lint/lint.go @@ -76,7 +76,16 @@ func isBlacklisted(workspaceDir string, candidate string) (bool, error) { // Skip hidden files as we don't need to run checks on them. (This is not strictly // true, but it's simpler this way for now and the risk is low given that the hidden // files we have are trivial.) - return strings.HasPrefix(relative, "."), nil + if strings.HasPrefix(relative, ".") { + return true, nil + } + + // Skip the Rust build directory. + if strings.HasPrefix(candidate, "target/") { + return true, nil + } + + return false, nil } // collectFiles scans the given directory recursively and returns the paths to all regular files diff --git a/admin/travis-build.sh b/admin/travis-build.sh index 5b6c165..4e23354 100755 --- a/admin/travis-build.sh +++ b/admin/travis-build.sh @@ -69,8 +69,128 @@ do_lint() { bazel run //admin/lint -- --verbose } +do_rust() { + PATH="${HOME}/.cargo/bin:${PATH}" + + # The Rust implementation of sandboxfs is still experimental and is unable to + # pass all integration tests. This blacklist keeps track of which those are. + # Ideally, by the time this alternative implementation is ready, this list + # will be empty and we can then refactor this file to just test one version. + local blacklist=( + TestCli_Help + TestCli_Version + TestCli_VersionNotForRelease + TestCli_ExclusiveFlagsPriority + TestCli_Syntax + TestDebug_FuseOpsInLog + TestLayout_MountPointDoesNotExist + TestLayout_RootMustBeDirectory + TestLayout_TargetDoesNotExist + TestLayout_DuplicateMapping + TestLayout_TargetIsScaffoldDirectory + TestNesting_ScaffoldIntermediateComponents + TestNesting_ScaffoldIntermediateComponentsAreImmutable + TestNesting_ReadWriteWithinReadOnly + TestNesting_SameTarget + TestNesting_PreserveSymlinks + TestOptions_Allow + TestOptions_Syntax + TestProfiling_Http + TestProfiling_FileProfiles + TestProfiling_BadConfiguration + TestReadOnly_DirectoryStructure + TestReadOnly_FileContents + TestReadOnly_ReplaceUnderlyingFile + TestReadOnly_MoveUnderlyingDirectory + TestReadOnly_TargetDoesNotExist + TestReadOnly_RepeatedReadDirsWhileDirIsOpen + TestReadOnly_Attributes + TestReadOnly_Access + TestReadOnly_HardLinkCountsAreFixed + TestReadWrite_CreateFile + TestReadWrite_Remove + TestReadWrite_RewriteFile + TestReadWrite_RewriteFileWithShorterContent + TestReadWrite_InodeReassignedAfterRecreation + TestReadWrite_FstatOnDeletedNode + TestReadWrite_Truncate + TestReadWrite_FtruncateOnDeletedFile + TestReadWrite_NestedMappingsInheritDirectoryProperties + TestReadWrite_NestedMappingsClobberFiles + TestReadWrite_RenameFile + TestReadWrite_MoveFile + TestReadWrite_Mknod + TestReadWrite_Chmod + TestReadWrite_FchmodOnDeletedNode + TestReadWrite_Chown + TestReadWrite_FchownOnDeletedNode + TestReadWrite_Chtimes + TestReadWrite_FutimesOnDeletedNode + TestReadWrite_HardLinksNotSupported + TestReconfiguration_Streams + TestReconfiguration_Steps + TestReconfiguration_Unmap + TestReconfiguration_RemapInvalidatesCache + TestReconfiguration_Errors + TestReconfiguration_RaceSystemComponents + TestReconfiguration_DirectoryListings + TestReconfiguration_InodesAreStableForSameUnderlyingFiles + TestReconfiguration_WritableNodesAreDifferent + TestReconfiguration_FileSystemStillWorksAfterInputEOF + TestReconfiguration_StreamFileDoesNotExist + TestSignal_RaceBetweenSignalSetupAndMount + TestSignal_UnmountWhenCaught + TestSignal_QueuedWhileInUse + ) + + # TODO(https://github.com/bazelbuild/rules_rust/issues/2): Replace by a + # Bazel-based build once the Rust rules are capable of doing so. + cargo build + local bin="$(pwd)/target/debug/sandboxfs" + cargo test --verbose + + local all=( + $(go test -test.list=".*" github.com/bazelbuild/sandboxfs/integration \ + -sandboxfs_binary=irrelevant | grep -v "^ok") + ) + + # Compute the list of tests to run by comparing the full list of tests that + # we got from the test program and taking out all blacklisted tests. This is + # O(n^2), sure, but it doesn't matter: the list is small and this will go away + # at some point. + set +x + local valid=() + for t in "${all[@]}"; do + local blacklisted=no + for o in "${blacklist[@]}"; do + if [ "${t}" = "${o}" ]; then + blacklisted=yes + break + fi + done + if [ "${blacklisted}" = yes ]; then + echo "Skipping blacklisted test ${t}" 1>&2 + continue + fi + valid+="${t}" + done + set -x + + [ "${#valid[@]}" -gt 0 ] || return 0 # Only run tests if any are valid. + for t in "${valid[@]}"; do + go test -v -timeout=600s -test.run="^${t}$" \ + github.com/bazelbuild/sandboxfs/integration \ + -sandboxfs_binary="${bin}" -release_build=false + + sudo -H "${rootenv[@]}" -s \ + go test -v -timeout=600s -test.run="^${t}$" \ + github.com/bazelbuild/sandboxfs/integration \ + -sandboxfs_binary="$(pwd)/sandboxfs" -release_build=false + done +} + case "${DO}" in - bazel|gotools|lint) + bazel|gotools|lint|rust) "do_${DO}" ;; diff --git a/admin/travis-install.sh b/admin/travis-install.sh index 06b01c5..0888492 100755 --- a/admin/travis-install.sh +++ b/admin/travis-install.sh @@ -57,6 +57,13 @@ install_fuse() { esac } +install_rust() { + # We need to manually install Rust because we can only specify a single + # language in .travis.yml, and that language is Go for now. + curl https://sh.rustup.rs -sSf | sh -s -- -y + PATH="${HOME}/.cargo/bin:${PATH}" +} + case "${DO}" in bazel) install_bazel @@ -70,4 +77,9 @@ case "${DO}" in lint) install_bazel ;; + + rust) + install_fuse + install_rust + ;; esac diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..77f0b34 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,46 @@ +// Copyright 2018 Google Inc. +// +// 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. + +extern crate fuse; +#[macro_use] extern crate log; + +use std::ffi::OsStr; +use std::io; +use std::path::Path; + +/// FUSE file system implementation of sandboxfs. +struct SandboxFS { +} + +impl SandboxFS { + /// Creates a new `SandboxFS` instance. + fn new() -> SandboxFS { + SandboxFS {} + } +} + +impl fuse::Filesystem for SandboxFS { +} + +/// Mounts a new sandboxfs instance on the given `mount_point`. +pub fn mount(mount_point: &Path) -> io::Result<()> { + let options = ["-o", "ro", "-o", "fsname=sandboxfs"] + .iter() + .map(|o| o.as_ref()) + .collect::>(); + let fs = SandboxFS::new(); + info!("Mounting file system onto {:?}", mount_point); + fuse::mount(fs, &mount_point, &options) + .map_err(|e| io::Error::new(e.kind(), format!("mount on {:?} failed: {}", mount_point, e))) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c1230cb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,117 @@ +// Copyright 2018 Google Inc. +// +// 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. + +extern crate env_logger; +#[macro_use] extern crate failure; +extern crate getopts; +extern crate sandboxfs; + +use failure::Error; +use getopts::Options; +use std::env; +use std::path::Path; +use std::process; +use std::result::Result; + +/// Execution failure due to a user-triggered error. +#[derive(Debug, Fail)] +#[fail(display = "{}", message)] +struct UsageError { + message: String, +} + +/// Obtains the program name from the execution's first argument, or returns a default if the +/// program name cannot be determined for whatever reason. +fn program_name(args: &[String], default: &'static str) -> String { + let default = String::from(default); + match args.get(0) { + Some(arg0) => match Path::new(arg0).file_name() { + Some(basename) => match basename.to_str() { + Some(basename) => String::from(basename), + None => default, + }, + None => default, + }, + None => default, + } +} + +/// Prints program usage information to stdout. +fn usage(program: &str, opts: &Options) { + let brief = format!("Usage: {} [options] MOUNT_POINT", program); + print!("{}", opts.usage(&brief)); +} + +/// Program's entry point. This is a "safe" version of `main` in the sense that this doesn't +/// directly handle errors: all errors are returned to the caller for consistent reporter to the +/// user depending on their type. +fn safe_main(program: &str, args: &[String]) -> Result<(), Error> { + env_logger::init(); + + let mut opts = Options::new(); + opts.optflag("", "help", "prints usage information and exits"); + let matches = opts.parse(args)?; + + if matches.opt_present("help") { + usage(&program, &opts); + return Ok(()); + } + + let mount_point = if matches.free.len() == 1 { + &matches.free[0] + } else { + return Err(Error::from(UsageError { message: "invalid number of arguments".to_string() })); + }; + + sandboxfs::mount(Path::new(mount_point))?; + Ok(()) +} + +/// Program's entry point. This delegates to `safe_main` for all program logic and is just in +/// charge of consistently formatting and reporting all possible errors to the caller. +fn main() { + let args: Vec = env::args().collect(); + let program = program_name(&args, "sandboxfs"); + + if let Err(err) = safe_main(&program, &args[1..]) { + if let Some(err) = err.downcast_ref::() { + eprintln!("Usage error: {}", err); + eprintln!("Type {} --help for more information", program); + process::exit(2); + } else if let Some(err) = err.downcast_ref::() { + eprintln!("Usage error: {}", err); + eprintln!("Type {} --help for more information", program); + process::exit(2); + } else { + eprintln!("{}: {}", program, err); + process::exit(1); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_program_name_uses_default_on_errors() { + assert_eq!("default", program_name(&[], "default")); + } + + #[test] + fn test_program_name_uses_file_name_only() { + assert_eq!("b", program_name(&["a/b".to_string()], "unused")); + assert_eq!("foo", program_name(&["./x/y/foo".to_string()], "unused")); + } +}