Files
ps5upload/payload/include/webui_codec.h
T

163 lines
10 KiB
C

/* Pure JSON <-> AVA1 adapters for the console web UI's API routes (webui_api.c).
*
* No I/O, no globals, no allocation: everything here is host-tested
* (payload/tests/webui_codec_selftest.c). Request bodies are NOT NUL-terminated;
* every reader takes (json, n). Only TOP-LEVEL keys of a JSON object match: a key
* inside a nested object or inside a string never does. */
#ifndef PS5UPLOAD_WEBUI_CODEC_H
#define PS5UPLOAD_WEBUI_CODEC_H
#include <stddef.h>
#include <stdint.h>
/* The string value of top-level `key`, unescaped into `out` as UTF-8 (\" \\ \/ \b \f \n
* \r \t and \uXXXX, surrogate pairs joined) and NUL-terminated.
* Returns 1 found, 0 absent (or the body is not a JSON object), -1 present but unusable:
* not a string, a bad escape, a lone surrogate, a \u0000 (it would cut a path short), a raw
* control character, or longer than cap - 1 bytes. */
int webui_json_str(const char *json, size_t n, const char *key, char *out, size_t cap);
/* An unsigned integer value: 1 found, 0 absent, -1 present but not a non-negative integer
* that fits 64 bits (a string, a fraction, an exponent, a sign). */
int webui_json_u64(const char *json, size_t n, const char *key, uint64_t *out);
/* true / false: 1 found (*out 1 or 0), 0 absent, -1 any other value. */
int webui_json_bool(const char *json, size_t n, const char *key, int *out);
/* 1 when top-level `key` is present with the value null, else 0. */
int webui_json_null(const char *json, size_t n, const char *key);
/* The top-level `key`'s array value as a span: *val at its '[', *len through its ']' (brackets
* balanced outside strings). 1 found, 0 absent, -1 present but not a complete array. */
int webui_json_array(const char *json, size_t n, const char *key, const char **val, size_t *len);
/* The next string of the array span `arr` (from webui_json_array): *pos starts at 0 and is moved
* past each element. 1 a string (unescaped as webui_json_str does), 0 the array ended, -1 an element
* that is not a usable string or the array is malformed (stop there). */
int webui_json_array_str(const char *arr, size_t len, size_t *pos, char *out, size_t cap);
/* Escapes n bytes of `s` as the inside of a JSON string (no quotes added): `"` and `\` are
* backslashed, control characters become \n \r \t \b \f or \u00XX, valid UTF-8 is copied as
* is, and every byte that is not part of a valid UTF-8 sequence becomes �, so the result
* is valid JSON whatever the input. Writes at most cap - 1 bytes plus a NUL (when cap > 0) and
* returns the full escaped length, like snprintf: a return >= cap means it did not fit. */
size_t webui_json_escape(char *out, size_t cap, const char *s, size_t n);
/* An encoded ava1_fs_list_result_t as
* {"entries":[{"name":..,"kind":"file"|"dir"|"link"|"other","size":N,"mtime":N|null},...],
* "more":bool,"next":N}
* Entries that do not fit `cap` are left out and "more" is set; "next" is the directory
* position after the last entry written (the next page's offset; not a count), so a cut page
* still pages correctly. Returns the number of entries written, or -1 when `res` does not decode or not even
* the empty list fits. `out` is always NUL-terminated when cap > 0. */
int webui_list_to_json(const uint8_t *res, size_t n, char *out, size_t cap);
/* An encoded ava1_job_list_result_t as
* {"jobs":[{"job":"<32 hex>","kind":"upload"|"download"|"copy"|"op"|"other",
* "state":"running"|"done"|"failed","files_done":N,"files_total":N,
* "bytes_done":N,"bytes_total":N,"mine":bool},...]}
* `mine` (may be NULL: never mine) tells whether a job is the caller's. Returns the number of
* jobs written or -1 (does not decode, or does not fit). */
int webui_jobs_to_json(const uint8_t *res, size_t n, int (*mine)(const uint8_t id[16], void *ctx), void *ctx,
char *out, size_t cap);
/* The longest operation result /api/jobs/status carries (a HASH result is ~2.2 KiB with a long
* path; CRC32 and FFPFSC_UNPACK far less). A longer one is answered as "result":null. */
#define WEBUI_JOB_RESULT_MAX 4096u
/* An encoded ava1_status_t (a JOB_STATUS reply) as
* {"job":"<32 hex>","state":"running"|"done"|"failed","files_done":N,"files_total":N,
* "bytes_done":N,"bytes_total":N,"message":".."|null,"result":".."|null,"code":N|null}
* `message` is the operation's current line (on a failure its cause token), `result` the
* operation's own result body (JSON text, carried as a string; null past WEBUI_JOB_RESULT_MAX),
* `code` the AVA1 status of a failed job. 0, or -1 (does not decode, or does not fit). */
int webui_job_status_to_json(const uint8_t *res, size_t n, char *out, size_t cap);
/* 1 when `s` can be one file name: 1-255 bytes, no '/', no control character, not "." or "..". */
int webui_name_ok(const char *s);
/* A decompress's output name: as webui_name_ok, but at most WEBUI_UNPACK_NAME_MAX bytes so
* "<name>.part" (written first, renamed at the end) still fits the 255-byte NAME_MAX. Used by
* both the page route and the FFPFSC_UNPACK op itself. */
#define WEBUI_UNPACK_NAME_MAX 250
int webui_unpack_name_ok(const char *s);
/* A rename whose target already names the source itself (same device + inode: "a.txt" ->
* "A.txt" on case-insensitive exFAT) goes through a temporary name in the same folder:
* `from` -> `tmp` -> `to`. Fills `tmp` ("<dir>/.ps5upload-rename-<salt hex>") and returns 0
* when `from` and `to` are two spellings in ONE folder (same directory part, byte for byte, so
* the detour never leaves the mount); -1 otherwise (different folders, the same spelling, an
* empty name, or `tmp` does not fit `cap`). */
int webui_rename_detour(const char *from, const char *to, unsigned salt, char *tmp, size_t cap);
/* A one-path job operation's args as the engine sends them: {"path":"<escaped>"}, then
* ,"name":"<escaped>" when `name` is not NULL, then `tail` (raw JSON members such as
* ,"mode":"0777"; may be NULL) and the closing brace. Returns the length, or -1 (no path, or
* it does not fit; out is then ""). */
int webui_path_job_args(char *out, size_t cap, const char *path, const char *name, const char *tail);
/* 16 bytes as 32 lowercase hex characters + NUL. */
void webui_hex16(const uint8_t id[16], char out[33]);
/* Exactly 32 hex characters (either case) to 16 bytes: 0, or -1. */
int webui_parse_hex16(const char *s, uint8_t id[16]);
/* The HTTP status for an AVA1 error status and its cause: 400 a malformed request, 403 a path
* the helper refuses, 404 something missing (unknown job, ENOENT), 409 busy / exists / crosses
* a mount, 500 anything else. */
int webui_http_status_for(int ava1_status, const char *cause);
/* The Content-Type /api/fs/read answers for `path`, by its extension (case-insensitive):
* png, jpg/jpeg, txt/log/ini (UTF-8 text), json; anything else (xml, svg, html included: they
* can render as active documents) application/octet-stream. */
const char *webui_mime_for(const char *path);
/* `key=value` lines (HW_INFO, HW_TEMPS): the value of `key` as a string (1 found, 0 absent;
* cut to cap - 1) or as a signed integer (1 found, 0 absent, -1 not an integer). */
int webui_kv_str(const char *text, size_t n, const char *key, char *out, size_t cap);
int webui_kv_int(const char *text, size_t n, const char *key, long *out);
/* 1 when `tid` is a game's title id: four letters of a game prefix (PPSA, CUSA, PCJS, PLJS, ...:
* Sony's disc and digital game ranges for every region) and five digits. Homebrew (FAKE..., PLDM...,
* WKAL...), system apps (NPXS...) and the page's own tile (WEBUI_TILE_TITLE_ID) are not games. */
int webui_is_game_title_id(const char *tid);
/* From a PROC_FOCUS reply ({"apps":[{"title_id":..,"app_id":..},...],"focus_app_id":N,
* "big_app_id":N,...}): what Home shows as running (and may close). First the app that owns the
* screen (focus_app_id, else big_app_id) when its title id is set, not a system app (NPXS...) and
* not the page's own tile; else the first app whose title id is a game's (webui_is_game_title_id),
* so the fallback never names the browser, the tile or a homebrew app. Returns 1 with title_id /
* *app_id set, 0 when no game runs, -1 when the reply does not parse. */
int webui_focus_running(const char *json, size_t n, char *title_id, size_t cap, uint32_t *app_id);
/* ---- cheats (CHEATS_STATUS / CHEATS_GET / CHEATS_TOGGLE, as the engine calls them) ---- */
/* 1 when `tid` can name a game to the cheat methods: 4 to 15 ASCII letters and digits. */
int webui_cheats_title_ok(const char *tid);
/* A CHEATS_STATUS reply ({"enabled":bool,"patches_last":N,"patches_total":N,"game_running":bool,
* "game_title_id":"..","game_pid":N}) as {"engine":bool,"game":{"title_id":".."}|null}. `game` is
* null unless a game runs under a usable title id (webui_cheats_title_ok) that is not the page's own
* tile. 0, or -1 (no "enabled", or does not fit; out is then ""). */
int webui_cheats_status_to_json(const char *json, size_t n, char *out, size_t cap);
/* A CHEATS_GET reply ({"title_id":..,"mods":[{"index":N,"name":..,"desc":..,"type":"checkbox"|"button",
* "on":bool},...]}, or {"mods":[],"error":"no cheat files found"}) as
* {"mods":[{"index":N,"name":"..","desc":"..","button":bool,"on":bool},...],"truncated":bool}
* Every string is re-escaped (valid JSON whatever the cheat file held). A mod without an index or a
* name is left out. The helper's reply can end mid-entry (its 64 KiB buffer): the entries before the
* cut are kept and "truncated" is set; so too when `out` is full. Returns the number of mods written,
* or -1: no mods array, or the reply names an error other than "no cheat files found" (copied to
* `err`, cut to err_cap - 1), or not even the empty list fits. */
int webui_cheats_mods_to_json(const char *json, size_t n, char *out, size_t cap, char *err, size_t err_cap);
/* A failed CHEATS_TOGGLE's cause ({"ok":false,"err":"<text>"}) as a token the page acts on:
* "game_not_running" (no game, or a different game than the one asked for), "no_cheats",
* "bad_index", "module_missing", else "toggle_failed". */
const char *webui_cheats_toggle_error(const char *cause, size_t n);
/* Why /api/volumes/unmount refuses `p` before asking the helper, or NULL: "system_volume" for the console's
* own volumes (/, /data, /user, /system*, /preinst*, /dev*, /mnt/usbN, /mnt/extN), else "game_running" when
* a game runs (`game_running` != 0 also when that could not be read). The helper's FS_UNMOUNT forces a busy
* unmount, so a running game is never asked about: it would lose its image (ruling R20). */
const char *webui_unmount_refusal(const char *p, int game_running);
#endif