master
Edward Shen 2022-06-02 22:23:35 -07:00
parent ce68f4dd42
commit 531a7da636
Signed by: edward
GPG Key ID: 19182661E818369F
5 changed files with 57 additions and 53 deletions

View File

@ -92,7 +92,7 @@ impl From<&'static str> for Route {
/// web path. This incurs a disk check operation, but since users shouldn't be /// web path. This incurs a disk check operation, but since users shouldn't be
/// updating the config that frequently, it should be fine. /// updating the config that frequently, it should be fine.
impl<'de> Deserialize<'de> for Route { impl<'de> Deserialize<'de> for Route {
fn deserialize<D>(deserializer: D) -> Result<Route, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
@ -233,7 +233,7 @@ pub enum RouteType {
Internal, Internal,
} }
pub struct ConfigData { pub struct FileData {
pub path: PathBuf, pub path: PathBuf,
pub file: File, pub file: File,
} }
@ -242,19 +242,19 @@ pub struct ConfigData {
/// locations for a place to write a config file to. In order, it checks the /// locations for a place to write a config file to. In order, it checks the
/// system-wide config location (`/etc/`, in Linux), followed by the config /// system-wide config location (`/etc/`, in Linux), followed by the config
/// folder, followed by the user's home folder. /// folder, followed by the user's home folder.
pub fn get_config_data() -> Result<ConfigData, BunBunError> { pub fn get_config_data() -> Result<FileData, BunBunError> {
// Locations to check, with highest priority first // Locations to check, with highest priority first
let locations: Vec<_> = { let locations: Vec<_> = {
let mut folders = vec![PathBuf::from("/etc/")]; let mut folders = vec![PathBuf::from("/etc/")];
// Config folder // Config folder
if let Some(folder) = config_dir() { if let Some(folder) = config_dir() {
folders.push(folder) folders.push(folder);
} }
// Home folder // Home folder
if let Some(folder) = home_dir() { if let Some(folder) = home_dir() {
folders.push(folder) folders.push(folder);
} }
folders folders
@ -271,13 +271,13 @@ pub fn get_config_data() -> Result<ConfigData, BunBunError> {
match file { match file {
Ok(file) => { Ok(file) => {
debug!("Found file at {location:?}."); debug!("Found file at {location:?}.");
return Ok(ConfigData { return Ok(FileData {
path: location.clone(), path: location.clone(),
file, file,
}); });
} }
Err(e) => { Err(e) => {
debug!("Tried to read '{location:?}' but failed due to error: {e}",) debug!("Tried to read '{location:?}' but failed due to error: {e}");
} }
} }
} }
@ -298,7 +298,7 @@ pub fn get_config_data() -> Result<ConfigData, BunBunError> {
file.write_all(DEFAULT_CONFIG)?; file.write_all(DEFAULT_CONFIG)?;
let file = OpenOptions::new().read(true).open(location.clone())?; let file = OpenOptions::new().read(true).open(location.clone())?;
return Ok(ConfigData { return Ok(FileData {
path: location, path: location,
file, file,
}); });
@ -314,19 +314,19 @@ pub fn get_config_data() -> Result<ConfigData, BunBunError> {
/// Assumes that the user knows what they're talking about and will only try /// Assumes that the user knows what they're talking about and will only try
/// to load the config at the given path. /// to load the config at the given path.
pub fn load_custom_path_config( pub fn load_custom_file(
path: impl Into<PathBuf>, path: impl Into<PathBuf>,
) -> Result<ConfigData, BunBunError> { ) -> Result<FileData, BunBunError> {
let path = path.into(); let path = path.into();
let file = OpenOptions::new() let file = OpenOptions::new()
.read(true) .read(true)
.open(&path) .open(&path)
.map_err(|e| BunBunError::InvalidConfigPath(path.clone(), e))?; .map_err(|e| BunBunError::InvalidConfigPath(path.clone(), e))?;
Ok(ConfigData { file, path }) Ok(FileData { path, file })
} }
pub fn read_config( pub fn load_file(
mut config_file: File, mut config_file: File,
large_config: bool, large_config: bool,
) -> Result<Config, BunBunError> { ) -> Result<Config, BunBunError> {
@ -417,7 +417,7 @@ mod read_config {
fn empty_file() -> Result<()> { fn empty_file() -> Result<()> {
let config_file = tempfile::tempfile()?; let config_file = tempfile::tempfile()?;
assert!(matches!( assert!(matches!(
read_config(config_file, false), load_file(config_file, false),
Err(BunBunError::ZeroByteConfig) Err(BunBunError::ZeroByteConfig)
)); ));
Ok(()) Ok(())
@ -428,7 +428,7 @@ mod read_config {
let mut config_file = tempfile::tempfile()?; let mut config_file = tempfile::tempfile()?;
let size_to_write = (LARGE_FILE_SIZE_THRESHOLD + 1) as usize; let size_to_write = (LARGE_FILE_SIZE_THRESHOLD + 1) as usize;
config_file.write(&[0].repeat(size_to_write))?; config_file.write(&[0].repeat(size_to_write))?;
match read_config(config_file, false) { match load_file(config_file, false) {
Err(BunBunError::ConfigTooLarge(size)) Err(BunBunError::ConfigTooLarge(size))
if size as usize == size_to_write => {} if size as usize == size_to_write => {}
Err(BunBunError::ConfigTooLarge(size)) => { Err(BunBunError::ConfigTooLarge(size)) => {
@ -441,7 +441,7 @@ mod read_config {
#[test] #[test]
fn valid_config() -> Result<()> { fn valid_config() -> Result<()> {
assert!(read_config(File::open("bunbun.default.yaml")?, false).is_ok()); assert!(load_file(File::open("bunbun.default.yaml")?, false).is_ok());
Ok(()) Ok(())
} }
} }

View File

@ -2,6 +2,7 @@ use std::error::Error;
use std::fmt; use std::fmt;
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::module_name_repetitions)]
pub enum BunBunError { pub enum BunBunError {
Io(std::io::Error), Io(std::io::Error),
Parse(serde_yaml::Error), Parse(serde_yaml::Error),

View File

@ -1,13 +1,13 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(missing_docs)] #![deny(missing_docs)]
#![warn(clippy::nursery, clippy::pedantic)]
//! Bunbun is a pure-Rust implementation of bunny1 that provides a customizable //! Bunbun is a pure-Rust implementation of bunny1 that provides a customizable
//! search engine and quick-jump tool in one small binary. For information on //! search engine and quick-jump tool in one small binary. For information on
//! usage, please take a look at the readme. //! usage, please take a look at the readme.
use crate::config::{ use crate::config::{
get_config_data, load_custom_path_config, read_config, ConfigData, Route, get_config_data, load_custom_file, load_file, FileData, Route, RouteGroup,
RouteGroup,
}; };
use anyhow::Result; use anyhow::Result;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
@ -50,15 +50,15 @@ async fn main() -> Result<()> {
init_logger(opts.verbose, opts.quiet)?; init_logger(opts.verbose, opts.quiet)?;
let conf_data = match opts.config { let conf_data = match opts.config {
Some(file_name) => load_custom_path_config(file_name), Some(file_name) => load_custom_file(file_name),
None => get_config_data(), None => get_config_data(),
}?; }?;
let conf = read_config(conf_data.file.try_clone()?, opts.large_config)?; let conf = load_file(conf_data.file.try_clone()?, opts.large_config)?;
let state = Arc::from(ArcSwap::from_pointee(State { let state = Arc::from(ArcSwap::from_pointee(State {
public_address: conf.public_address, public_address: conf.public_address,
default_route: conf.default_route, default_route: conf.default_route,
routes: cache_routes(&conf.groups), routes: cache_routes(conf.groups.clone()),
groups: conf.groups, groups: conf.groups,
})); }));
@ -106,14 +106,15 @@ fn init_logger(num_verbose_flags: u8, num_quiet_flags: u8) -> Result<()> {
/// 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: Vec<RouteGroup>) -> HashMap<String, 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 {
// This function isn't called often enough to not be a performance issue.
match mapping.insert(kw.clone(), dest.clone()) { match mapping.insert(kw.clone(), dest.clone()) {
None => trace!("Inserting {} into mapping.", kw), None => trace!("Inserting {kw} into mapping."),
Some(old_value) => { Some(old_value) => {
trace!("Overriding {} route from {} to {}.", kw, old_value, dest) trace!("Overriding {kw} route from {old_value} to {dest}.");
} }
} }
} }
@ -158,16 +159,14 @@ fn compile_templates() -> Result<Handlebars<'static>> {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn start_watch( fn start_watch(
state: Arc<ArcSwap<State>>, state: Arc<ArcSwap<State>>,
config_data: ConfigData, config_data: FileData,
large_config: bool, large_config: bool,
) -> Result<Hotwatch> { ) -> Result<Hotwatch> {
let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?; let mut watch = Hotwatch::new_with_custom_delay(Duration::from_millis(500))?;
let ConfigData { path, mut file } = config_data; let FileData { path, mut file } = config_data;
let watch_result = watch.watch(&path, move |e: Event| { let watch_result = watch.watch(&path, move |e: Event| {
if let Event::Create(ref path) = e { if let Event::Create(ref path) = e {
file = load_custom_path_config(path) file = load_custom_file(path).expect("file to exist at path").file;
.expect("file to exist at path")
.file;
trace!("Getting new file handler as file was recreated."); trace!("Getting new file handler as file was recreated.");
} }
@ -175,7 +174,7 @@ fn start_watch(
Event::Write(_) | Event::Create(_) => { Event::Write(_) | Event::Create(_) => {
trace!("Grabbing writer lock on state..."); trace!("Grabbing writer lock on state...");
trace!("Obtained writer lock on state!"); trace!("Obtained writer lock on state!");
match read_config( match load_file(
file.try_clone().expect("Failed to clone file handle"), file.try_clone().expect("Failed to clone file handle"),
large_config, large_config,
) { ) {
@ -183,7 +182,7 @@ fn start_watch(
state.store(Arc::new(State { state.store(Arc::new(State {
public_address: conf.public_address, public_address: conf.public_address,
default_route: conf.default_route, default_route: conf.default_route,
routes: cache_routes(&conf.groups), routes: cache_routes(conf.groups.clone()),
groups: conf.groups, groups: conf.groups,
})); }));
info!("Successfully updated active state"); info!("Successfully updated active state");
@ -198,7 +197,9 @@ fn start_watch(
match watch_result { match watch_result {
Ok(_) => info!("Watcher is now watching {path:?}"), Ok(_) => info!("Watcher is now watching {path:?}"),
Err(e) => { Err(e) => {
warn!("Couldn't watch {path:?}: {e}. Changes to this file won't be seen!",) warn!(
"Couldn't watch {path:?}: {e}. Changes to this file won't be seen!"
);
} }
} }
@ -255,7 +256,7 @@ mod cache_routes {
#[test] #[test]
fn empty_groups_yield_empty_routes() { fn empty_groups_yield_empty_routes() {
assert_eq!(cache_routes(&[]), HashMap::new()); assert_eq!(cache_routes(Vec::new()), HashMap::new());
} }
#[test] #[test]
@ -275,7 +276,7 @@ mod cache_routes {
}; };
assert_eq!( assert_eq!(
cache_routes(&[group1, group2]), cache_routes(vec![group1, group2]),
generate_external_routes(&[ generate_external_routes(&[
("a", "b"), ("a", "b"),
("c", "d"), ("c", "d"),
@ -302,7 +303,7 @@ mod cache_routes {
}; };
assert_eq!( assert_eq!(
cache_routes(&[group1.clone(), group2]), cache_routes(vec![group1.clone(), group2]),
generate_external_routes(&[("a", "1"), ("c", "2")]) generate_external_routes(&[("a", "1"), ("c", "2")])
); );
@ -314,7 +315,7 @@ mod cache_routes {
}; };
assert_eq!( assert_eq!(
cache_routes(&[group1, group3]), cache_routes(vec![group1, group3]),
generate_external_routes(&[("a", "1"), ("b", "2"), ("c", "d")]) generate_external_routes(&[("a", "1"), ("b", "2"), ("c", "d")])
); );
} }

View File

@ -11,11 +11,11 @@ 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::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::Path;
use std::process::Command; use std::process::Command;
use std::sync::Arc; use std::sync::Arc;
/// https://url.spec.whatwg.org/#fragment-percent-encode-set // https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ') .add(b' ')
.add(b'"') .add(b'"')
@ -27,6 +27,7 @@ const FRAGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b'#') // Interpreted as a hyperlink section target .add(b'#') // Interpreted as a hyperlink section target
.add(b'\''); .add(b'\'');
#[allow(clippy::unused_async)]
pub async fn index( pub async fn index(
Extension(data): Extension<Arc<ArcSwap<State>>>, Extension(data): Extension<Arc<ArcSwap<State>>>,
Extension(handlebars): Extension<Handlebars<'static>>, Extension(handlebars): Extension<Handlebars<'static>>,
@ -40,6 +41,7 @@ pub async fn index(
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
} }
#[allow(clippy::unused_async)]
pub async fn opensearch( pub async fn opensearch(
Extension(data): Extension<Arc<ArcSwap<State>>>, Extension(data): Extension<Arc<ArcSwap<State>>>,
Extension(handlebars): Extension<Handlebars<'static>>, Extension(handlebars): Extension<Handlebars<'static>>,
@ -62,6 +64,7 @@ pub async fn opensearch(
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
} }
#[allow(clippy::unused_async)]
pub async fn list( pub async fn list(
Extension(data): Extension<Arc<ArcSwap<State>>>, Extension(data): Extension<Arc<ArcSwap<State>>>,
Extension(handlebars): Extension<Handlebars<'static>>, Extension(handlebars): Extension<Handlebars<'static>>,
@ -77,6 +80,7 @@ pub struct SearchQuery {
to: String, to: String,
} }
#[allow(clippy::unused_async)]
pub async fn hop( pub async fn hop(
Extension(data): Extension<Arc<ArcSwap<State>>>, Extension(data): Extension<Arc<ArcSwap<State>>>,
Extension(handlebars): Extension<Handlebars<'static>>, Extension(handlebars): Extension<Handlebars<'static>>,
@ -91,7 +95,7 @@ pub async fn hop(
route_type: RouteType::Internal, route_type: RouteType::Internal,
path, path,
.. ..
} => resolve_path(PathBuf::from(path), &args), } => resolve_path(Path::new(path), &args),
ConfigRoute { ConfigRoute {
route_type: RouteType::External, route_type: RouteType::External,
path, path,
@ -190,7 +194,7 @@ fn resolve_hop<'a>(
/// Checks if the user provided string has the correct properties required by /// Checks if the user provided string has the correct properties required by
/// the route to be successfully matched. /// the route to be successfully matched.
fn check_route(route: &Route, arg_count: usize) -> bool { const fn check_route(route: &Route, arg_count: usize) -> bool {
if let Some(min_args) = route.min_args { if let Some(min_args) = route.min_args {
if arg_count < min_args { if arg_count < min_args {
return false; return false;
@ -217,7 +221,7 @@ enum HopAction {
/// 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
/// file. /// file.
fn resolve_path(path: PathBuf, args: &str) -> Result<HopAction, BunBunError> { fn resolve_path(path: &Path, args: &str) -> Result<HopAction, BunBunError> {
let output = Command::new(path.canonicalize()?) let output = Command::new(path.canonicalize()?)
.args(args.split(' ')) .args(args.split(' '))
.output()?; .output()?;
@ -369,18 +373,16 @@ mod resolve_path {
use super::{resolve_path, HopAction}; use super::{resolve_path, HopAction};
use anyhow::Result; use anyhow::Result;
use std::env::current_dir; use std::env::current_dir;
use std::path::PathBuf; use std::path::{Path, PathBuf};
#[test] #[test]
fn invalid_path_returns_err() { fn invalid_path_returns_err() {
assert!(resolve_path(PathBuf::from("/bin/aaaa"), "aaaa").is_err()); assert!(resolve_path(&Path::new("/bin/aaaa"), "aaaa").is_err());
} }
#[test] #[test]
fn valid_path_returns_ok() { fn valid_path_returns_ok() {
assert!( assert!(resolve_path(&Path::new("/bin/echo"), r#"{"body": "a"}"#).is_ok());
resolve_path(PathBuf::from("/bin/echo"), r#"{"body": "a"}"#).is_ok()
);
} }
#[test] #[test]
@ -389,7 +391,7 @@ mod resolve_path {
let nest_level = current_dir()?.ancestors().count() - 1; let nest_level = current_dir()?.ancestors().count() - 1;
let mut rel_path = PathBuf::from("../".repeat(nest_level)); let mut rel_path = PathBuf::from("../".repeat(nest_level));
rel_path.push("./bin/echo"); rel_path.push("./bin/echo");
assert!(resolve_path(rel_path, r#"{"body": "a"}"#).is_ok()); assert!(resolve_path(&rel_path, r#"{"body": "a"}"#).is_ok());
Ok(()) Ok(())
} }
@ -399,7 +401,7 @@ mod resolve_path {
// Trying to run a command without permission // Trying to run a command without permission
format!( format!(
"{}", "{}",
resolve_path(PathBuf::from("/root/some_exec"), "").unwrap_err() resolve_path(&Path::new("/root/some_exec"), "").unwrap_err()
) )
.contains("Permission denied") .contains("Permission denied")
); );
@ -408,13 +410,13 @@ mod resolve_path {
#[test] #[test]
fn non_success_exit_code_yields_err() { fn non_success_exit_code_yields_err() {
// cat-ing a folder always returns exit code 1 // cat-ing a folder always returns exit code 1
assert!(resolve_path(PathBuf::from("/bin/cat"), "/").is_err()); assert!(resolve_path(&Path::new("/bin/cat"), "/").is_err());
} }
#[test] #[test]
fn return_body() -> Result<()> { fn return_body() -> Result<()> {
assert_eq!( assert_eq!(
resolve_path(PathBuf::from("/bin/echo"), r#"{"body": "a"}"#)?, resolve_path(&Path::new("/bin/echo"), r#"{"body": "a"}"#)?,
HopAction::Body("a".to_string()) HopAction::Body("a".to_string())
); );
@ -424,7 +426,7 @@ mod resolve_path {
#[test] #[test]
fn return_redirect() -> Result<()> { fn return_redirect() -> Result<()> {
assert_eq!( assert_eq!(
resolve_path(PathBuf::from("/bin/echo"), r#"{"redirect": "a"}"#)?, resolve_path(&Path::new("/bin/echo"), r#"{"redirect": "a"}"#)?,
HopAction::Redirect("a".to_string()) HopAction::Redirect("a".to_string())
); );
Ok(()) Ok(())

View File

@ -3,7 +3,7 @@ use std::borrow::Cow;
use percent_encoding::PercentEncode; use percent_encoding::PercentEncode;
use serde::Serialize; use serde::Serialize;
pub fn query<'a>(query: PercentEncode<'a>) -> impl Serialize + 'a { pub fn query(query: PercentEncode<'_>) -> impl Serialize + '_ {
#[derive(Serialize)] #[derive(Serialize)]
struct TemplateArgs<'a> { struct TemplateArgs<'a> {
query: Cow<'a, str>, query: Cow<'a, str>,
@ -13,7 +13,7 @@ pub fn query<'a>(query: PercentEncode<'a>) -> impl Serialize + 'a {
} }
} }
pub fn hostname<'a>(hostname: &'a str) -> impl Serialize + 'a { pub fn hostname(hostname: &'_ str) -> impl Serialize + '_ {
#[derive(Serialize)] #[derive(Serialize)]
pub struct TemplateArgs<'a> { pub struct TemplateArgs<'a> {
pub hostname: &'a str, pub hostname: &'a str,