atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
a043b8dc91b73012728c66725d8a23704ec419d1
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/shaft.rsRust150 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
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
}