diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..766bc4b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,179 @@ +use std::collections::HashSet; +use std::future::Future; +use std::net::SocketAddr; +use std::ops::RangeInclusive; +use std::pin::Pin; +use std::process::ExitCode; +use std::sync::Arc; + +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::server::conn::http1; +use hyper::service::Service; +use hyper::{Request, Response, StatusCode}; +use hyper_util::rt::TokioIo; +use rand::prelude::*; +use rand_pcg::Pcg64; +use rand_seeder::Seeder; +use tokio::net::TcpListener; + +#[tokio::main] +async fn main() -> Result<ExitCode, Box<dyn std::error::Error + Send + Sync>> { + let args: Vec<String> = std::env::args().collect(); + + if args.len() != 2 { + eprintln!("must supply exactly one arg for the seed"); + return Ok(ExitCode::FAILURE); + } + let seed = args.get(1).unwrap(); + eprintln!("seed: {}", seed); + + tracing_subscriber::fmt::init(); + + let svc = RandomPageService::new(seed.as_str()); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3200)); + + let listener = TcpListener::bind(addr).await?; + + loop { + let (stream, _) = listener.accept().await?; + + let io = TokioIo::new(stream); + + let svc = svc.clone(); + + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new().serve_connection(io, svc).await { + eprintln!("Error serving connection: {:?}", err); + } + }); + } +} + +#[derive(Debug, Clone)] +struct RandomPageService { + pub seed: String, + pub dict: Arc<Vec<&'static str>>, + pub dict_set: Arc<HashSet<&'static str>>, +} + +impl RandomPageService { + pub fn new(seed: &str) -> Self { + let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH")); + let dictionary_string: &'static str = + std::str::from_utf8(dictionary_data).unwrap(); + Self { + seed: seed.to_string(), + dict: Arc::new(dictionary_string.split_whitespace().collect()), + dict_set: Arc::new(dictionary_string.split_whitespace().collect()), + } + } +} + +impl Service<Request<hyper::body::Incoming>> for RandomPageService { + type Response = Response<Full<Bytes>>; + type Error = hyper::Error; + + type Future = + Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; + + fn call(&self, req: Request<hyper::body::Incoming>) -> Self::Future { + fn mk_response( + s: String, + status_code: StatusCode, + ) -> Result<Response<Full<Bytes>>, hyper::Error> { + Ok( + Response::builder() + .status(status_code) + .body(Full::new(Bytes::from(s))) + .unwrap(), + ) + } + + let path = req.uri().path(); + + tracing::info!(path, "path"); + + let path_segments = path.split('/').filter(|v| v.len() > 0); + + for segment in path_segments { + if !self.dict_set.contains(segment) { + return Box::pin(async { + mk_response("not found".to_string(), StatusCode::NOT_FOUND) + }); + } + } + + let res_body = build_page(&self.seed, path, &self.dict); + + let res = mk_response(res_body, StatusCode::OK); + + Box::pin(async { res }) + } +} + +const MAX_ROUTE_SEGMENTS: usize = 6; +const PARAGRAPH_WORDS: RangeInclusive<usize> = 10..=200; +const N_LINKS: RangeInclusive<usize> = 3..=10; + +pub fn build_page( + base_seed: &str, + route: &str, + dictionary: &[&'static str], +) -> String { + let mut rng: Pcg64 = + Seeder::from(format!("{}---{}", base_seed, route)).make_rng(); + let n_words = rng.gen_range(PARAGRAPH_WORDS); + let n_links = rng.gen_range(N_LINKS); + + let random_paragraph = (0..n_words) + .map(|_| random_word(&mut rng, dictionary)) + .collect::<Vec<&str>>() + .join(" "); + + let random_links = (0..n_links) + .map(|_| random_route_link(&mut rng, dictionary)) + .map(|link| format!("<p>{}</p>", link)) + .collect::<Vec<String>>() + .join("\n"); + + format!( + r#" + <html> + <head> + </head> + <body> + <p>{}</p> + {} + </body> + </html> + "#, + random_paragraph, random_links, + ) +} + +fn random_route_link(rng: &mut Pcg64, dictionary: &[&'static str]) -> String { + let n_segments = rng.gen_range(1..=MAX_ROUTE_SEGMENTS); + let random_route = (0..n_segments) + .map(|_| random_word(rng, dictionary)) + .collect::<Vec<&str>>() + .join("/"); + let label = random_word(rng, dictionary); + format!("<a href=\"/{}\">{}</a>", random_route, label) +} + +pub fn random_word( + rng: &mut Pcg64, + dictionary: &[&'static str], +) -> &'static str { + let i = rng.gen_range(0..dictionary.len()); + dictionary[i] +} + +pub fn build_dict() -> Vec<&'static str> { + let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH")); + let dictionary_string: &'static str = + std::str::from_utf8(dictionary_data).unwrap(); + dictionary_string.split_whitespace().collect() +} |