an http based roguelike rpg about jelqing
Commit ec6fd4bb5b60
tarball4 files changed+88-28
@@ -61,6 +61,18 @@ fn reject(request: &Request) -> Option<Problem> {
6161
}6262
6363
async fn compress(response: Response) -> Response {64
+ let compressible = response65
+ .headers()66
+ .get(header::CONTENT_TYPE)67
+ .and_then(|value| value.to_str().ok())68
+ .is_some_and(|type_| {69
+ type_.starts_with("application/json")70
+ || type_.starts_with("application/problem+json")71
+ || type_.starts_with("application/xml")72
+ });73
+ if !compressible {74
+ return response;75
+ }6476
let (mut parts, body) = response.into_parts();6577
let Ok(bytes) = axum::body::to_bytes(body, usize::MAX).await else {6678
return Response::from_parts(parts, Body::empty());@@ -261,6 +261,37 @@ fn parse_inch_range(range: &str, total: f64) -> Option<InchRange> {
261261
})262262
}263263
264
+fn unsatisfied_range(inches: f64) -> Response {265
+ Problem::new(266
+ StatusCode::RANGE_NOT_SATISFIABLE,267
+ "range-not-satisfiable",268
+ format!("The requested range is not available. {inches:.2} inches are."),269
+ )270
+ .header(header::CONTENT_RANGE, format!("inches */{inches:.2}"))271
+ .into_response()272
+}273
+274
+fn partial_inches(body: &str, offset: usize, start: f64, end: f64, total: f64) -> Response {275
+ let from = (millimetres(start) + offset).min(body.len() - 1);276
+ let to = (millimetres(end) + offset).clamp(from + 1, body.len());277
+ (278
+ StatusCode::PARTIAL_CONTENT,279
+ [280
+ (281
+ header::CONTENT_TYPE,282
+ "text/plain; charset=us-ascii".to_owned(),283
+ ),284
+ (header::ACCEPT_RANGES, "inches".to_owned()),285
+ (286
+ header::CONTENT_RANGE,287
+ format!("inches {start:.2}-{end:.2}/{total:.2}"),288
+ ),289
+ ],290
+ body[from..to].to_owned(),291
+ )292
+ .into_response()293
+}294
+264295
fn get_length(run: &mut Run, ctx: &Ctx) -> Response {265296
let inches = if run.items.ruler {266297
run.current_length()@@ -278,27 +309,12 @@ fn get_length(run: &mut Run, ctx: &Ctx) -> Response {
278309
.header(header::RANGE)279310
.and_then(|range| parse_inch_range(range, inches))280311
{281
- let InchRange::Satisfiable(start, end) = range else {282
- return Problem::new(283
- StatusCode::RANGE_NOT_SATISFIABLE,284
- "range-not-satisfiable",285
- format!("The requested range is not available. {inches:.2} inches are."),286
- )287
- .header(header::CONTENT_RANGE, format!("inches */{inches:.2}"))288
- .into_response();289
- };290
- let from = millimetres(start).min(body.len() - 1);291
- let to = millimetres(end).clamp(from + 1, body.len());292
- let content_range = (293
- header::CONTENT_RANGE,294
- format!("inches {start:.2}-{end:.2}/{inches:.2}"),295
- );296
- return (297
- StatusCode::PARTIAL_CONTENT,298
- [content_type, accept_ranges, content_range],299
- body[from..to].to_owned(),300
- )301
- .into_response();312
+ match range {313
+ InchRange::Satisfiable(start, end) => {314
+ return partial_inches(&body, 0, start, end, inches);315
+ }316
+ InchRange::Unsatisfiable => return unsatisfied_range(inches),317
+ }302318
}303319
304320
if run.items.ruler {@@ -313,16 +329,40 @@ fn get_length(run: &mut Run, ctx: &Ctx) -> Response {
313329
(StatusCode::OK, [content_type, accept_ranges], body).into_response()314330
}315331
316
-fn get_foreskin(run: &mut Run, _: &Ctx) -> Response {317
- if run.foreskin.is_none() {332
+fn get_foreskin(run: &mut Run, ctx: &Ctx) -> Response {333
+ let Some(inches) = run.foreskin else {318334
return Problem::new(319335
StatusCode::GONE,320336
"foreskin-gone",321337
"The foreskin is gone. It will not be returning.",322338
)323339
.into_response();340
+ };341
+ let body = format!("8({})D", "~".repeat(millimetres(inches)));342
+ if let Some(range) = ctx343
+ .header(header::RANGE)344
+ .and_then(|range| parse_inch_range(range, inches))345
+ {346
+ match range {347
+ InchRange::Satisfiable(start, end) => {348
+ return partial_inches(&body, 2, start, end, inches);349
+ }350
+ InchRange::Unsatisfiable => return unsatisfied_range(inches),351
+ }324352
}325
- (StatusCode::NO_CONTENT, [(header::ETAG, run.etag())]).into_response()353
+ (354
+ StatusCode::OK,355
+ [356
+ (header::ETAG, run.etag()),357
+ (358
+ header::CONTENT_TYPE,359
+ "text/plain; charset=us-ascii".to_owned(),360
+ ),361
+ (header::ACCEPT_RANGES, "inches".to_owned()),362
+ ],363
+ body,364
+ )365
+ .into_response()326366
}327367
328368
fn delete_foreskin(run: &mut Run, ctx: &Ctx) -> Response {@@ -197,8 +197,9 @@ async fn deleting_the_foreskin_requires_certainty() {
197197
let response = send(&app, "DELETE", "/runs/cafebabe/foreskin", &[], "").await;198198
assert_eq!(response.status(), StatusCode::PRECONDITION_REQUIRED);199199
let look = send(&app, "GET", "/runs/cafebabe/foreskin", &[], "").await;200
- assert_eq!(look.status(), StatusCode::NO_CONTENT);200
+ assert_eq!(look.status(), StatusCode::OK);201201
let etag = header(&look, "etag").to_owned();202
+ assert!(text(look).await.contains('~'));202203
let response = send(203204
&app,204205
"DELETE",@@ -112,6 +112,7 @@ let linked = [];
112112
let techniques = [];113113
let offered = [];114114
let challenged = false;115
+const etags = {};115116
let target = "/runs";116117
let method = "POST";117118
const values = {};@@ -167,7 +168,7 @@ function fieldValue(name) {
167168
168169
function lubeRule() {169170
const picked = [technique(fieldValue("technique")), technique(fieldValue("also"))].filter(Boolean);170
- if (!picked.length || !picked[0].lubricant) return "none";171
+ if (!picked.some(t => t.lubricant)) return "none";171172
for (const rule of ["forbidden", "required"]) {172173
if (picked.some(t => t.lubricant === rule)) return rule;173174
}@@ -275,7 +276,8 @@ function renderFields() {
275276
table.replaceChildren();276277
for (const name of fieldNames()) {277278
const f = FIELDS[name];278
- const value = fieldValue(name);279
+ let value = fieldValue(name);280
+ if (name === "ifMatch" && !value) value = values.ifMatch = etags[current().path] || "";279281
const choices = options(name);280282
let input;281283
if (choices) {@@ -351,13 +353,18 @@ function describe(r) {
351353
const range = (r.headers.find(h => h[0] === "content-range") || [])[1] || "";352354
const partial = r.status === 206 && range.match(/^bytes (\d+)-(\d+)\/(\d+)$/);353355
lines.push(" " + r.body);354
- if (partial) lines.push(" [bytes " + partial[1] + "-" + partial[2] + " of " + partial[3] + ". A 206 carries only part of the representation, as RFC 9110 requires.]");356
+ if (partial) lines.push(" [bytes " + partial[1] + "-" + partial[2] + " of " + partial[3] + ". Half-mast.]");355357
}356358
return lines.join("\n");357359
}358360
359361
function absorb(r, sentMethod, sentPath) {360362
const headers = Object.fromEntries(r.headers.map(([n, v]) => [n.toLowerCase(), v]));363
+ if (headers.etag) etags[sentPath] = headers.etag;364
+ const digest = (headers["www-authenticate"] || "").match(/nonce="([^"]*)"/);365
+ if (digest && !(values.authorization || "").includes(digest[1])) {366
+ values.authorization = 'Digest username="", nonce="' + digest[1] + '"';367
+ }361368
let body = null;362369
try { body = JSON.parse(r.body); } catch (e) {}363370
const problem = body && typeof body.type === "string" ? body.type.split("/").pop() : null;