atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit ec6fd4bb5b60

tarball

atagen <boss@atagen.co>2026-10-11 04:36 UTC

4 files changed+88-28
http: keep the shafts uncompressed and give the foreskin a body

gzip was stripping Content-Length from every response, so the ruler joke about a real Content-Length never reached a browser. Only JSON and XML compress now. GET /foreskin used to be an empty 204, so the ETag needed for its DELETE was a cold fact, and now it draws itself with inch ranges of its own. The UI prefills If-Match from the last ETag and the Digest nonce from her challenge, and lubes no longer vanish from the form when the combo partner has no opinion.
Modified file: src/http/middleware.rs+12 additions
@@ -61,6 +61,18 @@ fn reject(request: &Request) -> Option<Problem> {
6161 }
6262
6363 async fn compress(response: Response) -> Response {
64+ let compressible = response
65+ .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());
Modified file: src/http/runs.rs+64 additions−24 deletions
@@ -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) = ctx
343+ .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 {
Modified file: src/tests.rs+2 additions−1 deletions
@@ -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",
Modified file: src/ui.html+10 additions−3 deletions
@@ -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;