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")); + } +}