diff --git a/src/main.rs b/src/main.rs index 6a8f69e..cf5c2fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ -use std::{ - collections::HashMap, - fmt, - fs::{self, File}, - io::{ Cursor, Write }, - time::SystemTime -}; -use serde::{ Deserialize, Serialize }; use random_string::generate; -use tiny_http::{ Header, Server, StatusCode, Response, Request }; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + fmt, + fs::{self, File}, + io::{Cursor, Write}, + time::SystemTime, +}; +use tiny_http::{Header, Request, Response, Server, StatusCode}; use urlencoding::decode; const LETTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -25,7 +25,15 @@ struct Config { impl fmt::Display for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Port: {}\nURL path: {:?}, Links URL path: {:?}\n{} entries, {}llowing new entries", self.port, self.url_path, self.link_path, self.entries_len, if self.allow_new {"A"} else {"Not a"} ) + write!( + f, + "Port: {}\nURL path: {:?}, Links URL path: {:?}\n{} entries, {}llowing new entries", + self.port, + self.url_path, + self.link_path, + self.entries_len, + if self.allow_new { "A" } else { "Not a" } + ) } } @@ -37,18 +45,18 @@ struct Entry { delete: Option, } -fn main() { - // Default Config - let mut config: Config = Config { +fn main() { + // Default Config + let mut config: Config = Config { host: String::from("http://localhost:8000"), - url_path: String::from(""), + url_path: String::from(""), link_path: String::from("links"), port: 8000, entries: HashMap::new(), // parse in from toml or something, entries_len: 0, allow_new: true, }; - + // Read in toml file (if exists) println!("Reading config toml file..."); if let Ok(file_contents) = fs::read_to_string("ssurlss.toml") { @@ -64,12 +72,12 @@ fn main() { println!("Reading environment variables..."); for (key, value) in std::env::vars() { match key.as_str() { - "HOST" => { config.host = value }, - "URLPATH" => { config.url_path = value }, - "LINKPATH" => { config.link_path = value.trim_matches(['/']).to_string() }, - "PORT" => { config.port = value.parse().unwrap_or(config.port) }, - "ALLOWNEW" => { config.allow_new = true }, - "DISALLOWNEW" => { config.allow_new = false }, + "HOST" => config.host = value, + "URLPATH" => config.url_path = value, + "LINKPATH" => config.link_path = value.trim_matches(['/']).to_string(), + "PORT" => config.port = value.parse().unwrap_or(config.port), + "ALLOWNEW" => config.allow_new = true, + "DISALLOWNEW" => config.allow_new = false, _ => {} } } @@ -88,18 +96,19 @@ fn main() { } else { let server = server.unwrap(); println!("Started server on port {}", config.port); - + for request in server.incoming_requests() { - print!("{}: {:?}, {:?} // ", + print!( + "{}: {:?}, {:?} // ", get_now(), request.method(), request.url(), ); - + let full_url = request.url(); let prefix_no_trail = String::from("/") + &config.url_path; let prefix = String::from("/") + &config.url_path + "/"; - + if full_url == prefix || full_url == prefix_no_trail { println!("200 Index {full_url:?}"); let r = html_resp_incl(include_str!("index.html")); @@ -107,7 +116,7 @@ fn main() { } else if full_url.starts_with(&prefix) { let url = full_url.strip_prefix(&prefix).unwrap(); let link_prefix = config.link_path.clone() + "/"; - + if url.contains("favicon.ico") { file_resp(include_bytes!("../assets/favicon.ico"), request); } else if url.starts_with(&link_prefix) { @@ -130,7 +139,8 @@ fn main() { show_404(request); } } else if url.starts_with("add") && config.allow_new { - if url.contains('?') { // receiving data to add + if url.contains('?') { + // receiving data to add let data = url.strip_prefix("add?").unwrap(); let entry = process_entry(&config, data); @@ -140,22 +150,29 @@ fn main() { let _ = request.respond(r); continue; } - + let name = entry.id.clone(); let _ = config.entries.insert(name.clone(), entry); config.entries_len += 1; println!("200 Add (data) {full_url:?}"); - + write_config(&config); let template = include_str!("added.html"); let complete_url = if !config.url_path.is_empty() { - format!("{}/{}/{}/{}",config.host.clone(), &config.url_path, &config.link_path, &name) + format!( + "{}/{}/{}/{}", + config.host.clone(), + &config.url_path, + &config.link_path, + &name + ) } else { - format!("{}/{}/{}",config.host.clone(), &config.link_path, &name) + format!("{}/{}/{}", config.host.clone(), &config.link_path, &name) }; let r = html_resp_incl(&template.replace("{url}", &complete_url)); let _ = request.respond(r); - } else { // give the normal add page + } else { + // give the normal add page println!("200 Add (page) {full_url:?}"); let r = html_resp_incl(include_str!("add.html")); let _ = request.respond(r); @@ -170,30 +187,42 @@ fn main() { fn process_entry(config: &Config, data: &str) -> Entry { let mut entry = Entry { - url: String::new(), - id: generate(>::try_into((config.entries_len + 1).ilog(52)).unwrap() + 1, LETTERS), - created: get_now(), - delete: None + url: String::new(), + id: generate( + >::try_into((config.entries_len + 1).ilog(52)).unwrap() + 1, + LETTERS, + ), + created: get_now(), + delete: None, }; - let data = data.split("&").map(|x| { - let y = x.split("=").collect::>(); - if y.len() >= 2 { - (y[0], y[1]) - } else {("", "")} - }).collect::>(); + let data = data + .split("&") + .map(|x| { + let y = x.split("=").collect::>(); + if y.len() >= 2 { + (y[0], y[1]) + } else { + ("", "") + } + }) + .collect::>(); for (key, val) in &data { match *key { - "url" => { if !val.is_empty() { - if let Ok(url) = decode(val) { entry.url = url.into_owned() } - }}, - "id" => { + "url" => { + if !val.is_empty() { + if let Ok(url) = decode(val) { + entry.url = url.into_owned() + } + } + } + "id" => { let val = val.to_string(); if !config.entries.contains_key(&val) && !val.is_empty() { entry.id = val; } - }, + } "time" => { let time = val.to_string(); if !time.is_empty() { @@ -202,7 +231,7 @@ fn process_entry(config: &Config, data: &str) -> Entry { entry.delete = Some(timestamp); } } - }, + } _ => {} } } @@ -210,11 +239,11 @@ fn process_entry(config: &Config, data: &str) -> Entry { entry } -fn timestamp_from_str(string: String) -> u64 { +fn timestamp_from_str(string: String) -> u64 { // in an ideal world, the string should just be the timestamp itself. let year: u64 = (parse_timeslice(&string, 0, 4) - 1970u64) * 31_557_600u64; - let month: u64 = parse_timeslice(&string, 5, 7) * 2_629_800u64; - let day: u64 = parse_timeslice(&string, 8, 10) * 24u64 * 360u64; + let month: u64 = (parse_timeslice(&string, 5, 7) - 1) * 2_629_800u64; + let day: u64 = (parse_timeslice(&string, 8, 10) - 1) * 24u64 * 360u64; let hour: u64 = parse_timeslice(&string, 11, 13) * 360u64; let min: u64 = parse_timeslice(&string, 16, 18) * 60u64; year + month + day + min + hour @@ -222,24 +251,18 @@ fn timestamp_from_str(string: String) -> u64 { /// only works for png images fn file_resp(file: &[u8], request: Request) { - let response = Response::new( - StatusCode(200), - Vec::new(), - file, - Some(file.len()), - None - ); + let response = Response::new(StatusCode(200), Vec::new(), file, Some(file.len()), None); let _ = request.respond(response); } fn redir_resp(to: &str, request: Request) { let from = request.url(); let response = Response::new( - StatusCode(302), - vec![Header::from_bytes( - &b"Location"[..], to.as_bytes() - ).unwrap()], - "redirecting...".as_bytes(), None, None + StatusCode(302), + vec![Header::from_bytes(&b"Location"[..], to.as_bytes()).unwrap()], + "redirecting...".as_bytes(), + None, + None, ); println!("302 {from:?} -> {to:?}"); let _ = request.respond(response); @@ -249,19 +272,21 @@ fn html_resp_incl(content: &str) -> Response>> { let mut r = Response::from_string(content); if let Ok(header) = Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=UTF-8"[..]) { r = r.with_header(header); - }; + }; r } fn parse_timeslice(string: &str, start: usize, end: usize) -> u64 { if let Ok(timestamp) = string[start..end].parse() { timestamp - } else { 0u64 } + } else { + 0u64 + } } fn get_now() -> u64 { match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(n) => { n.as_secs() }, + Ok(n) => n.as_secs(), Err(e) => panic!("Err: this program doesn't work on time-travelling systems\n{e}"), } } @@ -278,6 +303,48 @@ fn write_config(config: &Config) { if let Err(e) = write!(file, "{}", toml::to_string(&config).unwrap()) { println!("Failed to write config!\n{e}"); }; - } else { println!("Failed to open toml config file!"); } + } else { + println!("Failed to open toml config file!"); + } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parsing_timeslice_valid() { + let str = "123456789"; + let e = parse_timeslice(str, 0, 4); + assert_eq!(1234, e); + } + + #[test] + fn test_parsing_timeslice_invalid() { + let str = "e123456789"; + let e = parse_timeslice(str, 0, 4); + assert_eq!(0, e); + } + + #[test] + fn test_parsing_timeslice_valid_zeroprefix() { + let str = "0123456789"; + let e = parse_timeslice(str, 0, 4); + assert_eq!(123, e); + } + + #[test] + fn test_timestamp_from_str_zero() { + let str = String::from("1970-01-01T00:00:00"); + let e = timestamp_from_str(str); + assert_eq!(0, e) + } + + /// Fails, due to the wacky way the timestamp is calculated (specifically months) + #[test] + fn test_timestamp_from_str_2000() { + let str = String::from("2000-05-23T09:15:44"); + let e = timestamp_from_str(str); + assert_eq!(959073344, e) + } +}