diff --git a/Cargo.lock b/Cargo.lock index f5bee1d..bd5cbe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,9 +431,11 @@ dependencies = [ "base64", "chrono", "image", + "regex", "reqwest", "serde", "serde_json", + "sha256", "shuttle-axum", "shuttle-runtime", "shuttle-shared-db", @@ -2541,6 +2543,19 @@ dependencies = [ "digest", ] +[[package]] +name = "sha256" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7895c8ae88588ccead14ff438b939b0c569cd619116f14b4d13fdff7b8333386" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index f4bbdc9..22c718f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ axum-extra = { version = "0.9.0", features = ["typed-header"] } base64 = "0.21.5" chrono = { version = "0.4.31", default-features = false, features = ["std"] } image = "0.24.7" +regex = "1.10.2" reqwest = { version = "0.11.22", features = ["json"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" +sha256 = "1.4.0" shuttle-axum = { version = "0.35.0", default-features = false, features = ["axum-0-7"] } shuttle-runtime = "0.35.0" shuttle-shared-db = { version = "0.35.1", features = ["postgres"] } diff --git a/src/cal/day15.rs b/src/cal/day15.rs new file mode 100644 index 0000000..049a165 --- /dev/null +++ b/src/cal/day15.rs @@ -0,0 +1,117 @@ +use axum::{http::StatusCode, response::IntoResponse, routing::post, Json, Router}; + +pub(crate) fn router() -> Router { + Router::new() + .route("/15/nice", post(nice)) + .route("/15/game", post(game)) +} + +#[derive(serde::Deserialize)] +struct Nice { + input: String, +} + +async fn nice(Json(Nice { input }): Json) -> impl IntoResponse { + let vowels = input + .chars() + .filter(|&c| c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' || c == 'y') + .count(); + let repeat = input + .as_bytes() + .windows(2) + .any(|c| c[0].is_ascii_alphabetic() && c[0] == c[1]); + let substrs = input.contains("ab") + || input.contains("cd") + || input.contains("pq") + || input.contains("xy"); + if vowels > 2 && repeat && !substrs { + (StatusCode::OK, "{\"result\":\"nice\"}".to_string()) + } else { + ( + StatusCode::BAD_REQUEST, + "{\"result\":\"naughty\"}".to_string(), + ) + } +} + +#[derive(serde::Serialize)] +struct Game { + result: String, + reason: String, +} + +async fn game(Json(Nice { input }): Json) -> (StatusCode, Json) { + let code; + let result; + let reason; + let r1 = input.len() >= 8; + let r2 = input.chars().any(|c| c.is_ascii_uppercase()) + && input.chars().any(|c| c.is_ascii_lowercase()) + && input.chars().any(|c| c.is_ascii_digit()); + let r3 = input.chars().filter(char::is_ascii_digit).count() >= 5; + let r4 = regex::Regex::new(r"\d+") + .expect("Regex should be valid") + .captures_iter(&input) + .map(|c| c.extract::<0>().0) + .map(|s| s.parse::().expect("All matches are only digits")) + .sum::() + == 2023; + let r5 = regex::Regex::new(r".*j.*o.*y.*") + .expect("Regex should be valid") + .is_match(&input) + && input.chars().filter(|&c| c == 'j').count() == 1 + && input.chars().filter(|&c| c == 'o').count() == 1 + && input.chars().filter(|&c| c == 'y').count() == 1; + let r6 = input.as_bytes().windows(3).any(|c| { + c[0] != c[1] && c[0] == c[2] && c[0].is_ascii_alphabetic() && c[1].is_ascii_alphabetic() + }); + let r7 = input + .chars() + .any(|c| ('\u{2980}'..='\u{2BFF}').contains(&c)); + let r8 = regex::Regex::new(r"[\p{Emoji}--\p{Ascii}]") + .expect("Regex should be valid") + .is_match(&input); + let r9 = sha256::digest(input.clone()).ends_with('a'); + if !r1 { + code = StatusCode::BAD_REQUEST; + result = "naughty".to_string(); + reason = "8 chars".to_string(); + } else if !r2 { + code = StatusCode::BAD_REQUEST; + result = "naughty".to_string(); + reason = "more types of chars".to_string(); + } else if !r3 { + code = StatusCode::BAD_REQUEST; + result = "naughty".to_string(); + reason = "55555".to_string(); + } else if !r4 { + code = StatusCode::BAD_REQUEST; + result = "naughty".to_string(); + reason = "math is hard".to_string(); + } else if !r5 { + code = StatusCode::NOT_ACCEPTABLE; + result = "naughty".to_string(); + reason = "not joyful enough".to_string(); + } else if !r6 { + code = StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS; + result = "naughty".to_string(); + reason = "illegal: no sandwich".to_string(); + } else if !r7 { + code = StatusCode::RANGE_NOT_SATISFIABLE; + result = "naughty".to_string(); + reason = "outranged".to_string(); + } else if !r8 { + code = StatusCode::UPGRADE_REQUIRED; + result = "naughty".to_string(); + reason = "😳".to_string(); + } else if !r9 { + code = StatusCode::IM_A_TEAPOT; + result = "naughty".to_string(); + reason = "not a coffee brewer".to_string(); + } else { + code = StatusCode::OK; + result = "nice".to_string(); + reason = "that's a nice password".to_string(); + } + (code, Json(Game { result, reason })) +} diff --git a/src/cal/mod.rs b/src/cal/mod.rs index 8edecbe..469e2b4 100644 --- a/src/cal/mod.rs +++ b/src/cal/mod.rs @@ -8,6 +8,7 @@ mod day11; mod day12; mod day13; mod day14; +mod day15; pub(crate) fn router(pool: sqlx::PgPool) -> axum::Router { axum::Router::new() @@ -21,4 +22,5 @@ pub(crate) fn router(pool: sqlx::PgPool) -> axum::Router { .nest("/", day12::router()) .nest("/", day13::router(pool)) .nest("/", day14::router()) + .nest("/", day15::router()) }