OXYGENHUB

client+server: URL-encode key text so it round-trips any character 45a475bb3fa8

The client sent key(text=) raw, so '+' decoded to a space and '&'/'#'/'%'
truncated or corrupted the value. Percent-encode it on the client
(urllib.parse.quote, safe=''), and percent-decode query values (%XX, '+'->space)
in the server's query_str. Numeric params are read straight off the raw query
with query_long, so only string values (text=) are decoded — unaffected.

Git-Commit: 7b2fd15d34e4338bd06b7644e39c2a363a32d3c1
Git-Author: Andy McCall <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-07-25 21:35 UTC
Signature
◈ sealed by Andy McCall
Hash
45a475bb3fa8e59f7556e9dbf6f58c6625d6d6304ecf8aaabce9eb929b50d8e2
Depends on
689a9ccb0138 961b3c610c66 d44319b1e712
Depended on by
nothing yet
In bookmarks
main perf-6
Edits
6 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified client/emu_control.py +4 −1
…
19 unchanged lines
20 20
import json
21 21
import time
22 22
import urllib.error
23 +
import urllib.parse
23 24
import urllib.request
24 25
25 26
CONTRACT_MAJOR = 0
…
91 unchanged lines
117 118
        a program that samples input each frame."""
118 119
        parts = []
119 120
        if text is not None:
120 -
            parts.append(f"text={text}")
121 +
            # Percent-encode so a '+' isn't decoded to a space and '&'/'#'/'%'
122 +
            # don't truncate or corrupt the value. safe="" encodes '+' too.
123 +
            parts.append(f"text={urllib.parse.quote(text, safe='')}")
121 124
        if code is not None:
122 125
            parts.append(f"code={code}")
123 126
        if down is not None:
…
41 unchanged lines
modified core/retro_control.c +29 −6
…
230 unchanged lines
231 231
    return def;
232 232
}
233 233
234 -
/* copy the value of query key into out (NUL-terminated, truncated to cap).
235 -
 * Returns 1 if the key was present, 0 otherwise. */
234 +
/* one hex nibble, or -1. */
235 +
static int hex_nibble(int c)
236 +
{
237 +
    if (c >= '0' && c <= '9') return c - '0';
238 +
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
239 +
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
240 +
    return -1;
241 +
}
242 +
243 +
/* copy the value of query key into out (NUL-terminated, truncated to cap),
244 +
 * percent-decoding it (form-urlencoded: %XX -> byte, '+' -> space). Numeric
245 +
 * params are read with query_long straight off the raw query, so only string
246 +
 * values (e.g. text=) are decoded here. Returns 1 if the key was present. */
236 247
static int query_str(const char *q, const char *key, char *out, size_t cap)
237 248
{
238 249
    size_t kl = strlen(key);
…
2 unchanged lines
241 252
        if (!strncmp(p, key, kl) && p[kl] == '=') {
242 253
            const char *v = p + kl + 1;
243 254
            const char *e = strchr(v, '&');
244 -
            size_t n = e ? (size_t)(e - v) : strlen(v);
245 -
            if (n >= cap) n = cap - 1;
246 -
            memcpy(out, v, n);
247 -
            out[n] = 0;
255 +
            const char *end = e ? e : v + strlen(v);
256 +
            size_t w = 0;
257 +
            for (const char *r = v; r < end && w + 1 < cap; ) {
258 +
                int hi, lo;
259 +
                if (r[0] == '%' && r + 2 < end &&
260 +
                    (hi = hex_nibble(r[1])) >= 0 && (lo = hex_nibble(r[2])) >= 0) {
261 +
                    out[w++] = (char)((hi << 4) | lo);
262 +
                    r += 3;
263 +
                } else if (r[0] == '+') {
264 +
                    out[w++] = ' ';
265 +
                    r += 1;
266 +
                } else {
267 +
                    out[w++] = *r++;
268 +
                }
269 +
            }
270 +
            out[w] = 0;
248 271
            return 1;
249 272
        }
250 273
        p = strchr(p, '&');
…
189 unchanged lines