diff --git a/Cargo.lock b/Cargo.lock index e8afbc6..dd3c8c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,6 +363,8 @@ dependencies = [ "actix-web 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "handlebars 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "hotwatch 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -778,6 +780,14 @@ dependencies = [ "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "itoa" version = "0.4.4" @@ -1894,6 +1904,7 @@ dependencies = [ "checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" +"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" diff --git a/Cargo.toml b/Cargo.toml index a74b4f4..25a3fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ actix-web = "1.0" serde = "1.0" serde_yaml = "0.8" handlebars = "2.0" -hotwatch = "0.4.3" +hotwatch = "0.4" +percent-encoding = "2.1" +itertools = "0.8" [profile.release] lto = true diff --git a/src/main.rs b/src/main.rs index ca09e3e..638af95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ use actix_web::{ }; use handlebars::Handlebars; use hotwatch::{Event, Hotwatch}; +use itertools::Itertools; +use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use std::fmt; @@ -14,10 +16,14 @@ use std::io::Write; use std::sync::{Arc, RwLock}; use std::time::Duration; +/// https://url.spec.whatwg.org/#fragment-percent-encode-set +static FRAGMENT_ENCODE_SET: &AsciiSet = + &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); static DEFAULT_CONFIG: &[u8] = include_bytes!("../bunbun.default.toml"); static CONFIG_FILE: &str = "bunbun.toml"; #[derive(Debug)] +#[allow(clippy::enum_variant_names)] enum BunBunError { IoError(std::io::Error), ParseError(serde_yaml::Error), @@ -67,58 +73,60 @@ fn hop( query: Query, ) -> impl Responder { let data = data.read().unwrap(); - let mut raw_args = query.to.split_ascii_whitespace(); - let command = raw_args.next(); - if command.is_none() { - return HttpResponse::NotFound().body("not found"); - } + match resolve_hop(&query.to, &data.routes, &data.default_route) { + (Some(path), args) => { + let mut template_args = HashMap::new(); + template_args.insert( + "query", + utf8_percent_encode(&args, FRAGMENT_ENCODE_SET).to_string(), + ); - // Reform args into url-safe string (probably want to go thru an actual parser) - let mut args = String::new(); - if let Some(first_arg) = raw_args.next() { - args.push_str(first_arg); - for arg in raw_args { - args.push_str("+"); - args.push_str(arg); + HttpResponse::Found() + .header( + header::LOCATION, + data + .renderer + .render_template(&path, &template_args) + .unwrap(), + ) + .finish() } + (None, _) => HttpResponse::NotFound().body("not found"), } +} - let mut template_args = HashMap::new(); - template_args.insert("query", args); +/// Attempts to resolve the provided string into its route and its arguments. +/// If a default route was provided, then this will consider that route before +/// failing to resolve a route. +/// +/// The first element in the tuple describes the route, while the second element +/// returns the remaining arguments. If none remain, an empty string is given. +fn resolve_hop( + query: &str, + routes: &BTreeMap, + default_route: &Option, +) -> (Option, String) { + let mut split_args = query.split_ascii_whitespace().peekable(); + let command = match split_args.peek() { + Some(command) => command, + None => return (None, String::new()), + }; - match data.routes.get(command.unwrap()) { - Some(template) => HttpResponse::Found() - .header( - header::LOCATION, - data - .renderer - .render_template(template, &template_args) - .unwrap(), - ) - .finish(), - None => match &data.default_route { - Some(route) => { - template_args.insert( - "query", - format!( - "{}+{}", - command.unwrap(), - template_args.get("query").unwrap() - ), - ); - HttpResponse::Found() - .header( - header::LOCATION, - data - .renderer - .render_template(data.routes.get(route).unwrap(), &template_args) - .unwrap(), - ) - .finish() - } - None => HttpResponse::NotFound().body("not found"), - }, + match (routes.get(*command), default_route) { + // Found a route + (Some(resolved), _) => ( + Some(resolved.clone()), + match split_args.next() { + // Discard the first result, we found the route using the first arg + Some(_) => split_args.join(" "), + None => String::new(), + }, + ), + // Unable to find route, but had a default route + (None, Some(route)) => (routes.get(route).cloned(), split_args.join(" ")), + // No default route and no match + (None, None) => (None, String::new()), } }