atagen

jelq

public

an http based roguelike rpg about jelqing

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