Skip to content

Commit

Permalink
Add skeleton reimplementation of sandboxfs in Rust (#23)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jmmv authored Aug 3, 2018
1 parent 547f6e1 commit 0b78fae
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
/.gopath
/.gopath-tools
/bazel-*
Cargo.lock
target
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ env:
- DO=bazel
- DO=gotools
- DO=lint
- DO=rust

before_install: ./admin/travis-install.sh
script: ./admin/travis-build.sh
Expand All @@ -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
5 changes: 5 additions & 0 deletions .vscode/settings.json.in
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
"editor.quickSuggestions": false
},

"[rust]": {
"editor.rulers": [100],
"editor.wordWrapColumn": 100
},

"[troff]": {
"editor.quickSuggestions": false
}
Expand Down
21 changes: 21 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
authors = ["Julio Merino <[email protected]>"]
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"
11 changes: 10 additions & 1 deletion admin/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
122 changes: 121 additions & 1 deletion admin/travis-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
;;

Expand Down
12 changes: 12 additions & 0 deletions admin/travis-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,4 +77,9 @@ case "${DO}" in
lint)
install_bazel
;;

rust)
install_fuse
install_rust
;;
esac
46 changes: 46 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<&OsStr>>();
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)))
}
117 changes: 117 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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::<UsageError>() {
eprintln!("Usage error: {}", err);
eprintln!("Type {} --help for more information", program);
process::exit(2);
} else if let Some(err) = err.downcast_ref::<getopts::Fail>() {
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"));
}
}

0 comments on commit 0b78fae

Please sign in to comment.