an http based roguelike rpg about jelqing
Blame: src/http/shaft.rs
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 1 | use std::convert::Infallible; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 2 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 3 | use axum::{ |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 4 | body::{Body, Bytes}, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 5 | http::{StatusCode, header}, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 6 | response::{IntoResponse, Response}, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 7 | }; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 8 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 9 | use super::ctx::Ctx; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 10 | use crate::{ |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 11 | game::{Run, balance::MM_PER_INCH, shaft}, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 12 | problem::Problem, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 13 | }; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 14 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 15 | const SHAFT_CHUNK: usize = 16; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 16 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 17 | pub(super) fn millimetres(inches: f64) -> usize { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 18 | (inches * MM_PER_INCH).round() as usize |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 19 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 20 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 21 | pub(super) enum InchRange { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 22 | Satisfiable(f64, f64), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 23 | Unsatisfiable, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 24 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 25 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 26 | /// `None` means the header is not a valid range, which RFC 9110 §14.2 says to ignore rather than refuse. |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 27 | pub(super) fn parse_inch_range(range: &str, total: f64) -> Option<InchRange> { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 28 | let (start, end) = range.strip_prefix("inches=")?.split_once('-')?; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 29 | let number = |text: &str| { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 30 | text.parse::<f64>() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 31 | .ok() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 32 | .filter(|n| n.is_finite() && *n >= 0.0) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 33 | }; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 34 | let (start, end) = match (start.trim(), end.trim()) { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 35 | ("", "") => return None, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 36 | ("", suffix) => ((total - number(suffix)?).max(0.0), total), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 37 | (start, "") => (number(start)?, total), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 38 | (start, end) => { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 39 | let (start, end) = (number(start)?, number(end)?); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 40 | if start > end { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 41 | return None; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 42 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 43 | (start, end) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 44 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 45 | }; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 46 | Some(if start < total { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 47 | InchRange::Satisfiable(start, end.min(total)) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 48 | } else { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 49 | InchRange::Unsatisfiable |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 50 | }) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 51 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 52 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 53 | pub(super) fn unsatisfied_range(inches: f64) -> Response { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 54 | Problem::new( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 55 | StatusCode::RANGE_NOT_SATISFIABLE, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 56 | "range-not-satisfiable", |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 57 | format!("The requested range is not available. {inches:.2} inches are."), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 58 | ) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 59 | .header(header::CONTENT_RANGE, format!("inches */{inches:.2}")) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 60 | .into_response() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 61 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 62 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 63 | pub(super) fn partial_inches( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 64 | body: &str, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 65 | offset: usize, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 66 | start: f64, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 67 | end: f64, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 68 | total: f64, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 69 | ) -> Response { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 70 | let from = (millimetres(start) + offset).min(body.len() - 1); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 71 | let to = (millimetres(end) + offset).clamp(from + 1, body.len()); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 72 | ( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 73 | StatusCode::PARTIAL_CONTENT, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 74 | [ |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 75 | ( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 76 | header::CONTENT_TYPE, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 77 | "text/plain; charset=us-ascii".to_owned(), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 78 | ), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 79 | (header::ACCEPT_RANGES, "inches".to_owned()), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 80 | ( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 81 | header::CONTENT_RANGE, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 82 | format!("inches {start:.2}-{end:.2}/{total:.2}"), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 83 | ), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 84 | ], |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 85 | body[from..to].to_owned(), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 86 | ) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 87 | .into_response() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 88 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 89 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 90 | pub(super) fn get_length(run: &mut Run, ctx: &Ctx) -> Response { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 91 | let inches = run.shown_length(); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 92 | let body = shaft(millimetres(inches)); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 93 | let content_type = ( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 94 | header::CONTENT_TYPE, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 95 | "text/plain; charset=us-ascii".to_owned(), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 96 | ); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 97 | let accept_ranges = (header::ACCEPT_RANGES, "inches".to_owned()); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 98 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 99 | if let Some(range) = ctx |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 100 | .header(header::RANGE) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 101 | .and_then(|range| parse_inch_range(range, inches)) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 102 | { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 103 | match range { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 104 | InchRange::Satisfiable(start, end) => { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 105 | return partial_inches(&body, 0, start, end, inches); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 106 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 107 | InchRange::Unsatisfiable => return unsatisfied_range(inches), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 108 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 109 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 110 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 111 | if run.measured() { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 112 | return (StatusCode::OK, [content_type, accept_ranges], body).into_response(); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 113 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 114 | let chunks: Vec<Result<Bytes, Infallible>> = body |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 115 | .into_bytes() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 116 | .chunks(SHAFT_CHUNK) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 117 | .map(|c| Ok(Bytes::copy_from_slice(c))) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 118 | .collect(); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 119 | let body = Body::from_stream(futures_util::stream::iter(chunks)); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 120 | (StatusCode::OK, [content_type, accept_ranges], body).into_response() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 121 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 122 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 123 | pub(super) fn get_foreskin(run: &mut Run, ctx: &Ctx) -> Response { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 124 | let Some(inches) = run.foreskin() else { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 125 | return Problem::new( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 126 | StatusCode::GONE, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 127 | "foreskin-gone", |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 128 | "The foreskin is gone. It will not be returning.", |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 129 | ) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 130 | .into_response(); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 131 | }; |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 132 | let body = format!("8({})D", "~".repeat(millimetres(inches))); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 133 | if let Some(range) = ctx |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 134 | .header(header::RANGE) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 135 | .and_then(|range| parse_inch_range(range, inches)) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 136 | { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 137 | match range { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 138 | InchRange::Satisfiable(start, end) => { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 139 | return partial_inches(&body, 2, start, end, inches); |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 140 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 141 | InchRange::Unsatisfiable => return unsatisfied_range(inches), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 142 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 143 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 144 | ( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 145 | StatusCode::OK, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 146 | [ |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 147 | (header::ETAG, run.etag()), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 148 | ( |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 149 | header::CONTENT_TYPE, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 150 | "text/plain; charset=us-ascii".to_owned(), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 151 | ), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 152 | (header::ACCEPT_RANGES, "inches".to_owned()), |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 153 | ], |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 154 | body, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 155 | ) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 156 | .into_response() |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 157 | } |