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