Compare commits
No commits in common. "fd941b014a7a1bbedb360c42543f8a459b7c11bb" and "b8be3d8d53e4b1bc314cd9f6d71a8ccfdb14e22e" have entirely different histories.
fd941b014a
...
b8be3d8d53
3 changed files with 166 additions and 224 deletions
129
src/config.rs
129
src/config.rs
|
@ -1,11 +1,7 @@
|
||||||
use crate::BunBunError;
|
use crate::{routes::Route, BunBunError};
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace};
|
||||||
use serde::{
|
use serde::{Deserialize, Serialize};
|
||||||
de::{Deserializer, Visitor},
|
|
||||||
Deserialize, Serialize, Serializer,
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
|
||||||
use std::fs::{read_to_string, OpenOptions};
|
use std::fs::{read_to_string, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
@ -26,73 +22,6 @@ pub struct RouteGroup {
|
||||||
pub routes: HashMap<String, Route>,
|
pub routes: HashMap<String, Route>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub enum Route {
|
|
||||||
External(String),
|
|
||||||
Path(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialization of the Route enum needs to be transparent, but since the
|
|
||||||
/// `#[serde(transparent)]` macro isn't available on enums, so we need to
|
|
||||||
/// implement it manually.
|
|
||||||
impl Serialize for Route {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::External(s) => serializer.serialize_str(s),
|
|
||||||
Self::Path(s) => serializer.serialize_str(s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deserialization of the route string into the enum requires us to figure out
|
|
||||||
/// whether or not the string is valid to run as an executable or not. To
|
|
||||||
/// determine this, we simply check if it exists on disk or assume that it's a
|
|
||||||
/// web path. This incurs a disk check operation, but since users shouldn't be
|
|
||||||
/// updating the config that frequently, it should be fine.
|
|
||||||
impl<'de> Deserialize<'de> for Route {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Route, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct RouteVisitor;
|
|
||||||
impl<'de> Visitor<'de> for RouteVisitor {
|
|
||||||
type Value = Route;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
formatter.write_str("string")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
// Return early if it's a path, don't go through URL parsing
|
|
||||||
if std::path::Path::new(value).exists() {
|
|
||||||
debug!("Parsed {} as a valid local path.", value);
|
|
||||||
Ok(Route::Path(value.into()))
|
|
||||||
} else {
|
|
||||||
debug!("{} does not exist on disk, assuming web path.", value);
|
|
||||||
Ok(Route::External(value.into()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_str(RouteVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Route {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::External(s) => write!(f, "raw ({})", s),
|
|
||||||
Self::Path(s) => write!(f, "file ({})", s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to read the config file. If it doesn't exist, generate one a
|
/// Attempts to read the config file. If it doesn't exist, generate one a
|
||||||
/// default config file before attempting to parse it.
|
/// default config file before attempting to parse it.
|
||||||
pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
|
pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
|
||||||
|
@ -129,60 +58,6 @@ pub fn read_config(config_file_path: &str) -> Result<Config, BunBunError> {
|
||||||
Ok(serde_yaml::from_str(&config_str)?)
|
Ok(serde_yaml::from_str(&config_str)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod route {
|
|
||||||
use super::*;
|
|
||||||
use serde_yaml::{from_str, to_string};
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_relative_path() {
|
|
||||||
let tmpfile = NamedTempFile::new_in(".").unwrap();
|
|
||||||
let path = format!("{}", tmpfile.path().display());
|
|
||||||
let path = path.get(path.rfind(".").unwrap()..).unwrap();
|
|
||||||
let path = std::path::Path::new(path);
|
|
||||||
assert!(path.is_relative());
|
|
||||||
let path = path.to_str().unwrap();
|
|
||||||
assert_eq!(from_str::<Route>(path).unwrap(), Route::Path(path.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_absolute_path() {
|
|
||||||
let tmpfile = NamedTempFile::new().unwrap();
|
|
||||||
let path = format!("{}", tmpfile.path().display());
|
|
||||||
assert!(tmpfile.path().is_absolute());
|
|
||||||
assert_eq!(from_str::<Route>(&path).unwrap(), Route::Path(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_http_path() {
|
|
||||||
assert_eq!(
|
|
||||||
from_str::<Route>("http://google.com").unwrap(),
|
|
||||||
Route::External("http://google.com".into())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize_https_path() {
|
|
||||||
assert_eq!(
|
|
||||||
from_str::<Route>("https://google.com").unwrap(),
|
|
||||||
Route::External("https://google.com".into())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize() {
|
|
||||||
assert_eq!(
|
|
||||||
&to_string(&Route::External("hello world".into())).unwrap(),
|
|
||||||
"---\nhello world"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
&to_string(&Route::Path("hello world".into())).unwrap(),
|
|
||||||
"---\nhello world"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod read_config {
|
mod read_config {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -1,6 +1,6 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use crate::config::{read_config, Route, RouteGroup};
|
use crate::config::{read_config, RouteGroup};
|
||||||
use actix_web::{middleware::Logger, App, HttpServer};
|
use actix_web::{middleware::Logger, App, HttpServer};
|
||||||
use clap::{crate_authors, crate_version, load_yaml, App as ClapApp};
|
use clap::{crate_authors, crate_version, load_yaml, App as ClapApp};
|
||||||
use error::BunBunError;
|
use error::BunBunError;
|
||||||
|
@ -24,7 +24,7 @@ pub struct State {
|
||||||
default_route: Option<String>,
|
default_route: Option<String>,
|
||||||
groups: Vec<RouteGroup>,
|
groups: Vec<RouteGroup>,
|
||||||
/// Cached, flattened mapping of all routes and their destinations.
|
/// Cached, flattened mapping of all routes and their destinations.
|
||||||
routes: HashMap<String, Route>,
|
routes: HashMap<String, routes::Route>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
|
@ -97,7 +97,7 @@ fn init_logger(
|
||||||
/// Generates a hashmap of routes from the data structure created by the config
|
/// Generates a hashmap of routes from the data structure created by the config
|
||||||
/// file. This should improve runtime performance and is a better solution than
|
/// file. This should improve runtime performance and is a better solution than
|
||||||
/// just iterating over the config object for every hop resolution.
|
/// just iterating over the config object for every hop resolution.
|
||||||
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, Route> {
|
fn cache_routes(groups: &[RouteGroup]) -> HashMap<String, routes::Route> {
|
||||||
let mut mapping = HashMap::new();
|
let mut mapping = HashMap::new();
|
||||||
for group in groups {
|
for group in groups {
|
||||||
for (kw, dest) in &group.routes {
|
for (kw, dest) in &group.routes {
|
||||||
|
@ -218,11 +218,11 @@ mod cache_routes {
|
||||||
|
|
||||||
fn generate_external_routes(
|
fn generate_external_routes(
|
||||||
routes: &[(&str, &str)],
|
routes: &[(&str, &str)],
|
||||||
) -> HashMap<String, Route> {
|
) -> HashMap<String, routes::Route> {
|
||||||
HashMap::from_iter(
|
HashMap::from_iter(
|
||||||
routes
|
routes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| ((*k).into(), Route::External((*v).into()))),
|
.map(|(k, v)| ((*k).into(), routes::Route::External((*v).into()))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,17 +287,3 @@ mod cache_routes {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod compile_templates {
|
|
||||||
use super::compile_templates;
|
|
||||||
|
|
||||||
/// Successful compilation of the binary guarantees that the templates will be
|
|
||||||
/// present to be registered to. Thus, we only really need to see that
|
|
||||||
/// compilation of the templates don't panic, which is just making sure that
|
|
||||||
/// the function can be successfully called.
|
|
||||||
#[test]
|
|
||||||
fn templates_compile() {
|
|
||||||
let _ = compile_templates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
237
src/routes.rs
237
src/routes.rs
|
@ -1,4 +1,4 @@
|
||||||
use crate::{template_args, BunBunError, Route, State};
|
use crate::{template_args, BunBunError, State};
|
||||||
use actix_web::web::{Data, Query};
|
use actix_web::web::{Data, Query};
|
||||||
use actix_web::{get, http::header};
|
use actix_web::{get, http::header};
|
||||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||||
|
@ -6,8 +6,9 @@ use handlebars::Handlebars;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
|
||||||
use serde::Deserialize;
|
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
@ -23,43 +24,78 @@ const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
|
||||||
|
|
||||||
type StateData = Data<Arc<RwLock<State>>>;
|
type StateData = Data<Arc<RwLock<State>>>;
|
||||||
|
|
||||||
#[get("/")]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub async fn index(data: StateData, req: HttpRequest) -> impl Responder {
|
pub enum Route {
|
||||||
let data = data.read().unwrap();
|
External(String),
|
||||||
HttpResponse::Ok().body(
|
Path(String),
|
||||||
req
|
|
||||||
.app_data::<Handlebars>()
|
|
||||||
.unwrap()
|
|
||||||
.render(
|
|
||||||
"index",
|
|
||||||
&template_args::hostname(data.public_address.clone()),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/bunbunsearch.xml")]
|
/// Serialization of the Route enum needs to be transparent, but since the
|
||||||
pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
|
/// `#[serde(transparent)]` macro isn't available on enums, so we need to
|
||||||
let data = data.read().unwrap();
|
/// implement it manually.
|
||||||
HttpResponse::Ok()
|
impl Serialize for Route {
|
||||||
.header(
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
header::CONTENT_TYPE,
|
where
|
||||||
"application/opensearchdescription+xml",
|
S: Serializer,
|
||||||
)
|
{
|
||||||
.body(
|
match self {
|
||||||
req
|
Self::External(s) => serializer.serialize_str(s),
|
||||||
.app_data::<Handlebars>()
|
Self::Path(s) => serializer.serialize_str(s),
|
||||||
.unwrap()
|
}
|
||||||
.render(
|
}
|
||||||
"opensearch",
|
}
|
||||||
&template_args::hostname(data.public_address.clone()),
|
|
||||||
)
|
/// Deserialization of the route string into the enum requires us to figure out
|
||||||
.unwrap(),
|
/// whether or not the string is valid to run as an executable or not. To
|
||||||
)
|
/// determine this, we simply check if it exists on disk or assume that it's a
|
||||||
|
/// web path. This incurs a disk check operation, but since users shouldn't be
|
||||||
|
/// updating the config that frequently, it should be fine.
|
||||||
|
impl<'de> Deserialize<'de> for Route {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Route, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct RouteVisitor;
|
||||||
|
impl<'de> Visitor<'de> for RouteVisitor {
|
||||||
|
type Value = Route;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
// Return early if it's a path, don't go through URL parsing
|
||||||
|
if std::path::Path::new(value).exists() {
|
||||||
|
debug!("Parsed {} as a valid local path.", value);
|
||||||
|
Ok(Route::Path(value.into()))
|
||||||
|
} else {
|
||||||
|
debug!("{} does not exist on disk, assuming web path.", value);
|
||||||
|
Ok(Route::External(value.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(RouteVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Route {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::External(s) => write!(f, "raw ({})", s),
|
||||||
|
Self::Path(s) => write!(f, "file ({})", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ls")]
|
#[get("/ls")]
|
||||||
pub async fn list(data: StateData, req: HttpRequest) -> impl Responder {
|
pub async fn list(
|
||||||
|
data: Data<Arc<RwLock<State>>>,
|
||||||
|
req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
let data = data.read().unwrap();
|
let data = data.read().unwrap();
|
||||||
HttpResponse::Ok().body(
|
HttpResponse::Ok().body(
|
||||||
req
|
req
|
||||||
|
@ -170,6 +206,21 @@ fn resolve_hop<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn index(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
|
let data = data.read().unwrap();
|
||||||
|
HttpResponse::Ok().body(
|
||||||
|
req
|
||||||
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
|
.render(
|
||||||
|
"index",
|
||||||
|
&template_args::hostname(data.public_address.clone()),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the executable with the user's input as a single argument. Returns Ok
|
/// Runs the executable with the user's input as a single argument. Returns Ok
|
||||||
/// so long as the executable was successfully executed. Returns an Error if the
|
/// so long as the executable was successfully executed. Returns an Error if the
|
||||||
/// file doesn't exist or bunbun did not have permission to read and execute the
|
/// file doesn't exist or bunbun did not have permission to read and execute the
|
||||||
|
@ -189,6 +240,80 @@ fn resolve_path(path: PathBuf, args: &str) -> Result<Vec<u8>, BunBunError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/bunbunsearch.xml")]
|
||||||
|
pub async fn opensearch(data: StateData, req: HttpRequest) -> impl Responder {
|
||||||
|
let data = data.read().unwrap();
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
"application/opensearchdescription+xml",
|
||||||
|
)
|
||||||
|
.body(
|
||||||
|
req
|
||||||
|
.app_data::<Handlebars>()
|
||||||
|
.unwrap()
|
||||||
|
.render(
|
||||||
|
"opensearch",
|
||||||
|
&template_args::hostname(data.public_address.clone()),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod route {
|
||||||
|
use super::*;
|
||||||
|
use serde_yaml::{from_str, to_string};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_relative_path() {
|
||||||
|
let tmpfile = NamedTempFile::new_in(".").unwrap();
|
||||||
|
let path = format!("{}", tmpfile.path().display());
|
||||||
|
let path = path.get(path.rfind(".").unwrap()..).unwrap();
|
||||||
|
let path = std::path::Path::new(path);
|
||||||
|
assert!(path.is_relative());
|
||||||
|
let path = path.to_str().unwrap();
|
||||||
|
assert_eq!(from_str::<Route>(path).unwrap(), Route::Path(path.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_absolute_path() {
|
||||||
|
let tmpfile = NamedTempFile::new().unwrap();
|
||||||
|
let path = format!("{}", tmpfile.path().display());
|
||||||
|
assert!(tmpfile.path().is_absolute());
|
||||||
|
assert_eq!(from_str::<Route>(&path).unwrap(), Route::Path(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_http_path() {
|
||||||
|
assert_eq!(
|
||||||
|
from_str::<Route>("http://google.com").unwrap(),
|
||||||
|
Route::External("http://google.com".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_https_path() {
|
||||||
|
assert_eq!(
|
||||||
|
from_str::<Route>("https://google.com").unwrap(),
|
||||||
|
Route::External("https://google.com".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize() {
|
||||||
|
assert_eq!(
|
||||||
|
&to_string(&Route::External("hello world".into())).unwrap(),
|
||||||
|
"---\nhello world"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&to_string(&Route::Path("hello world".into())).unwrap(),
|
||||||
|
"---\nhello world"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod resolve_hop {
|
mod resolve_hop {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -268,47 +393,3 @@ mod resolve_hop {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod resolve_path {
|
|
||||||
use super::resolve_path;
|
|
||||||
use std::env::current_dir;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_path_returns_err() {
|
|
||||||
assert!(resolve_path(PathBuf::from("/bin/aaaa"), "aaaa").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn valid_path_returns_ok() {
|
|
||||||
assert!(resolve_path(PathBuf::from("/bin/echo"), "hello").is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn relative_path_returns_ok() {
|
|
||||||
// How many ".." needed to get to /
|
|
||||||
let nest_level = current_dir().unwrap().ancestors().count() - 1;
|
|
||||||
let mut rel_path = PathBuf::from("../".repeat(nest_level));
|
|
||||||
rel_path.push("./bin/env");
|
|
||||||
assert!(resolve_path(rel_path, "echo").is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_permissions_returns_err() {
|
|
||||||
assert!(
|
|
||||||
// Trying to run a command without permission
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
resolve_path(PathBuf::from("/root/some_exec"), "").unwrap_err()
|
|
||||||
)
|
|
||||||
.contains("Permission denied")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn non_success_exit_code_yields_err() {
|
|
||||||
// cat-ing a folder always returns exit code 1
|
|
||||||
assert!(resolve_path(PathBuf::from("/bin/cat"), "/").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue