diff --git a/Cargo.toml b/Cargo.toml index b2ffe7e..a541cfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,9 @@ name = "crate_server" path = "rust/crate_server/main.rs" [dependencies] +arrayvec = "0.7.6" hyper = { version = "0.14.12", features = ["http1", "server", "tcp"] } lazy_static = "1.5.0" -phf = { version = "0.11.2", features = ["macros"] } -serde = { version = "1.0", features = ["derive"] } -serde_yaml = "0.9" tokio = { version = "1", features = ["full"] } [profile.release] diff --git a/rust/crate_server/crates.rs b/rust/crate_server/crates.rs index 91220dd..fc76cb8 100644 --- a/rust/crate_server/crates.rs +++ b/rust/crate_server/crates.rs @@ -1,113 +1,63 @@ +use arrayvec::ArrayString; use hyper::header::{ ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL, CDN_CACHE_CONTROL, CONTENT_ENCODING, CONTENT_TYPE, COOKIE, EXPIRES, LOCATION, VARY, }; use hyper::{Body, Request, Response, StatusCode}; use lazy_static::lazy_static; -use serde::Deserialize; use std::collections::HashMap; use std::convert::Infallible; use std::fs; -use std::fs::File; -#[derive(Debug, Deserialize)] -struct CrateRecipe { - dizin: String, - sayfalar: Vec, -} - -#[derive(Debug, Deserialize)] -struct Sayfa { - tr: String, - en: String, -} - -fn load(path: &str) -> HashMap { +fn load(path: &str) -> HashMap, &'static [u8]> { let mut files = HashMap::new(); for entry in fs::read_dir(path).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if path.is_file() { - let filename = path.file_name().unwrap().to_string_lossy().to_string(); - let content: &'static [u8] = Box::leak(fs::read(&path).unwrap().into_boxed_slice()); - files.insert(filename, content); - } - } - let file = File::open("./crate/crate.yaml").unwrap(); - let recipe: CrateRecipe = serde_yaml::from_reader(file).unwrap(); - - let mut insert_alias = |alias: &str, key_base: &str, lang: &str, ext: &str| { - if let Some(&content) = files.get(&format!("{}-{}.html{}", key_base, lang, ext)) { - files.insert(format!("{}{}", alias, ext), content); + let filename = path.file_name().unwrap().to_string_lossy(); + if let Ok(array_key) = ArrayString::<16>::from(&filename[..]) { + let content: &'static [u8] = Box::leak(fs::read(&path).unwrap().into_boxed_slice()); + files.insert(array_key, content); + } else { + eprintln!("Filename '{}' is too long for ArrayString<16>", filename); + } } - }; - - insert_alias("?en", &recipe.dizin, "en", ""); - insert_alias("?en", &recipe.dizin, "en", ".br"); - insert_alias("?en", &recipe.dizin, "en", ".gz"); - insert_alias("?tr", &recipe.dizin, "tr", ""); - insert_alias("?tr", &recipe.dizin, "tr", ".br"); - insert_alias("?tr", &recipe.dizin, "tr", ".gz"); - - for sayfa in &recipe.sayfalar { - insert_alias(&sayfa.en, &sayfa.tr, "en", ""); - insert_alias(&sayfa.tr, &sayfa.tr, "tr", ""); - insert_alias(&sayfa.en, &sayfa.tr, "en", ".br"); - insert_alias(&sayfa.tr, &sayfa.tr, "tr", ".br"); - insert_alias(&sayfa.en, &sayfa.tr, "en", ".gz"); - insert_alias(&sayfa.tr, &sayfa.tr, "tr", ".gz"); } - files } lazy_static! { - static ref FILES: HashMap = load("./crate"); + static ref FILES: HashMap, &'static [u8]> = load("./crate"); } static STATIC_CACHE_CONTROL: &'static str = "max-age=29030400,public,immutable"; -fn get_crate_key(req: &Request) -> String { - let trimmed_path = req.uri().path().trim_matches('/'); - - if !trimmed_path.is_empty() { - return trimmed_path.to_string(); +fn get_crate_key(req: &Request) -> &str { + let key = req.uri().path().trim_matches('/'); + if !key.is_empty() { + return key; } - let trimmed_path = req - .uri() - .path_and_query() - .unwrap() - .as_str() - .trim_matches('/'); - if trimmed_path.len() == 3 { - return trimmed_path.to_string(); + if let Some(cookie_header) = req.headers().get(COOKIE) + && let Ok(cookie_str) = cookie_header.to_str() + && let Some(leq) = cookie_str.find("l=") + { + return &cookie_str[leq + 2..leq + 4]; } - let cookies_str = req - .headers() - .get(COOKIE) - .and_then(|v| v.to_str().ok()) - .unwrap_or(""); - if cookies_str.contains("l=tr") { - "?tr".to_string() - } else if cookies_str.contains("l=en") { - "?en".to_string() - } else if req - .headers() - .get(ACCEPT_LANGUAGE) - .and_then(|v| v.to_str().ok()) - .unwrap_or("") - .contains("tr") + if let Some(lang) = req.headers().get(ACCEPT_LANGUAGE) + && let Ok(lang_str) = lang.to_str() + && lang_str.contains("tr") { - "?tr".to_string() - } else { - "?en".to_string() + return "tr"; } + "en" } pub async fn serve_file(req: Request) -> Result, Infallible> { - let mut key = get_crate_key(&req); + let mut key = ArrayString::<16>::new(); + key.push_str(get_crate_key(&req)); let accept_encoding = req .headers() .get(ACCEPT_ENCODING) @@ -129,7 +79,7 @@ pub async fn serve_file(req: Request) -> Result, Infallible None }; if let Some(compression) = maybe_compression { - key += &compression[..3]; + key.push_str(&compression[..3]); } match FILES.get(&key) { Some(&content) => { @@ -157,69 +107,62 @@ pub async fn serve_file(req: Request) -> Result, Infallible #[cfg(test)] mod tests { use super::*; - use hyper::header::HeaderValue; - use hyper::{Body, Request, Uri}; - - fn mock_request( - uri: &'static str, - cookies: Option<&str>, - accept_language: Option<&str>, - ) -> Request { - let mut request = Request::builder() - .uri(Uri::from_static(uri)) - .body(Body::empty()) - .unwrap(); - - let headers = request.headers_mut(); - if let Some(cookie) = cookies { - headers.insert("cookie", HeaderValue::from_str(cookie).unwrap()); - } - if let Some(lang) = accept_language { - headers.insert("accept-language", HeaderValue::from_str(lang).unwrap()); - } - - request - } - - #[test] - fn test_get_crate_key_with_non_empty_path() { - let req = mock_request("/test", None, None); - assert_eq!(get_crate_key(&req), "test".to_string()); - } + use hyper::{ + header::{ACCEPT_LANGUAGE, COOKIE}, + Body, Request, + }; #[test] - fn test_get_crate_key_with_empty_path_and_short_query() { - let req = mock_request("/", None, None); - assert_eq!(get_crate_key(&req), "?en".to_string()); + fn test_get_crate_key_with_path() { + // Case 1: Request with a non-empty path + let req = Request::builder() + .uri("/somepath") + .body(Body::empty()) + .unwrap(); + assert_eq!(get_crate_key(&req), "somepath"); } #[test] - fn test_get_crate_key_with_empty_path_and_ltr_cookie() { - let req = mock_request("/", Some("l=tr"), None); - assert_eq!(get_crate_key(&req), "?tr".to_string()); + fn test_get_crate_key_with_cookie_language() { + // Case 2: Request with a cookie containing 'l=' + let req = Request::builder() + .header(COOKIE, "session=abc; l=tr; other=data") + .body(Body::empty()) + .unwrap(); + assert_eq!(get_crate_key(&req), "tr"); } #[test] - fn test_get_crate_key_with_empty_path_and_len_cookie() { - let req = mock_request("/", Some("l=en"), None); - assert_eq!(get_crate_key(&req), "?en".to_string()); - } + fn test_get_crate_key_with_accept_language_header() { + // Case 3: Request with 'Accept-Language' header containing 'tr' + let req = Request::builder() + .header(ACCEPT_LANGUAGE, "tr, en;q=0.8") + .body(Body::empty()) + .unwrap(); + assert_eq!(get_crate_key(&req), "tr"); - #[test] - fn test_get_crate_key_with_empty_path_and_accept_language_tr() { - let req = mock_request("/", None, Some("tr")); - assert_eq!(get_crate_key(&req), "?tr".to_string()); + // Case 4: Request with 'Accept-Language' header not containing 'tr' + let req = Request::builder() + .header(ACCEPT_LANGUAGE, "en, fr;q=0.8") + .body(Body::empty()) + .unwrap(); + assert_eq!(get_crate_key(&req), "en"); } #[test] - fn test_get_crate_key_with_empty_path_and_accept_language_en() { - let req = mock_request("/", None, Some("en")); - assert_eq!(get_crate_key(&req), "?en".to_string()); + fn test_get_crate_key_with_no_headers() { + // Case 5: Request with no relevant headers and no path + let req = Request::builder().uri("/").body(Body::empty()).unwrap(); + assert_eq!(get_crate_key(&req), "en"); } #[test] - fn test_get_crate_key_with_empty_path_no_cookies_or_headers() { - let req = mock_request("/", None, None); - assert_eq!(get_crate_key(&req), "?en".to_string()); + fn test_get_crate_key_empty_cookie_header() { + // Case 6: Request with empty cookie header + let req = Request::builder() + .header(COOKIE, "") + .body(Body::empty()) + .unwrap(); + assert_eq!(get_crate_key(&req), "en"); } } diff --git a/rust/crate_server/main.rs b/rust/crate_server/main.rs index ff35fd2..1c55d59 100644 --- a/rust/crate_server/main.rs +++ b/rust/crate_server/main.rs @@ -1,3 +1,5 @@ +#![feature(let_chains)] + use hyper::server::Server; use hyper::service::{make_service_fn, service_fn}; use std::convert::Infallible; @@ -12,7 +14,7 @@ async fn main() { Ok::<_, Infallible>(service_fn(move |req| crates::serve_file(req))) }); - let addr = SocketAddr::from(([127, 0, 0, 1], 8787)); // Running server on localhost:3000 + let addr = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 80)); let server = Server::bind(&addr).serve(make_svc); println!("Listening on http://{}", addr);