App-Ariza.git | runner/include/ | ariza_runner.h
/* ariza runner — the portable core of the compiled Windows launcher.
*
* A bundle's Windows entry point is bin/<exec>.exe: a small program that
* sets the same environment the .cmd and .ps1 launchers set, then starts
* the bundled interpreter on the app's script with the user's arguments
* passed through BYTE FOR BYTE. Everything in this header is the half
* of that job which has nothing to do with Windows — parsing the sidecar
* config, building environment values out of a bundle root, and deciding
* where the caller's arguments begin in a raw command line — so it can be
* compiled and tested anywhere, which is where the test suite runs.
*
* One type parameter, two instantiations. The whole core is written in
* terms of `arz_char`, which is `wchar_t` under Windows (where every
* string in play is UTF-16: GetCommandLineW, GetModuleFileNameW,
* SetEnvironmentVariableW) and `char` everywhere else (where the tests
* feed it UTF-8). The code is identical in both: it only ever inspects
* ASCII structure characters — '#', '=', quotes, spaces, separators —
* and copies everything else through untouched, so a UTF-8 byte and a
* UTF-16 unit are equally opaque to it.
*
* Every function that returns `arz_char *` returns freshly allocated
* memory the caller frees, or NULL when the allocation failed. Nothing
* here writes to a stream, exits, or knows what an error message looks
* like; that is the platform layer's business.
*/
#ifndef ARIZA_RUNNER_H
#define ARIZA_RUNNER_H
#include <stddef.h>
#ifdef _WIN32
#include <wchar.h>
typedef wchar_t arz_char;
#define ARZ_T(s) L##s
#else
typedef char arz_char;
#define ARZ_T(s) s
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef enum arz_status {
ARZ_OK = 0,
ARZ_E_NOMEM,
ARZ_E_SYNTAX
} arz_status;
/* The required directives, as an enum, so the platform layer can name a
* missing one in its own character type rather than being handed a
* `char *` it would have to convert. */
typedef enum arz_key {
ARZ_KEY_NONE = 0,
ARZ_KEY_TARGET,
ARZ_KEY_APP_DISPLAY,
ARZ_KEY_APP_EXEC
} arz_key;
/* The environment directives, in the order the file lists them. */
typedef enum arz_op_kind {
ARZ_OP_SET = 1,
ARZ_OP_UNSET,
ARZ_OP_PREPEND_PATH
} arz_op_kind;
/* One environment directive. `name` is the variable for `set` and
* `unset` and NULL for `prepend-path`, which always means PATH; `value`
* is the unexpanded text for `set` and `prepend-path` and NULL for
* `unset`. */
typedef struct arz_env_op {
arz_op_kind kind;
arz_char *name;
arz_char *value;
} arz_env_op;
/* bin/<exec>.ariza, parsed.
*
* The three strings are what the runner itself needs to know; `ops` is
* everything it does NOT — a list of environment changes to apply in
* order, whose meaning belongs entirely to whatever built the bundle.
* The runner has no idea what RAKULIB is, that notcurses exists, or
* that some bundles carry a SQLCipher: it discovers its root, applies
* the directives it was given, and starts the interpreter. */
typedef struct arz_config {
arz_char *target;
arz_char *app_display;
arz_char *app_exec;
arz_env_op *ops;
size_t op_count;
} arz_config;
/* The deliberately tiny authenticated handoff record written after an
* updater transaction commits. It contains no path: the platform launcher
* derives the managed `current` entry point itself, so a writable state file
* can never choose an executable. */
typedef struct arz_handoff {
arz_char *nonce;
arz_char *candidate;
} arz_handoff;
/* ------------------------------------------------------------------ */
/* Strings */
/* ------------------------------------------------------------------ */
size_t arz_len(const arz_char *s);
/* Concatenate up to three parts, skipping NULL ones. The workhorse
* everything else here is built from. */
arz_char *arz_cat(const arz_char *a, const arz_char *b, const arz_char *c);
arz_char *arz_dup(const arz_char *s);
/* True when `s`, of `n` units, is exactly the ASCII `name`. */
int arz_eq_ascii(const arz_char *s, size_t n, const char *name);
/* ------------------------------------------------------------------ */
/* The sidecar config */
/* ------------------------------------------------------------------ */
/* Parse `text` into `cfg`, which is zeroed first. On ARZ_E_SYNTAX,
* `*error_line` (when not NULL) is the 1-based line that could not be
* read; it is 0 otherwise.
*
* The format, which resources/templates/launcher-windows.ariza.j2 emits
* a description of into every file it writes:
*
* * UTF-8, one directive per line, LF or CRLF, applied in order.
* * A '#' in the first non-blank column starts a comment line; blank
* lines are ignored. A '#' anywhere else is an ordinary character,
* which matters more than it sounds: `inst#{root}\site` is a real
* value and an inline-comment rule would truncate it.
* * A leading UTF-8 BOM is skipped — nothing ariza writes has one, but
* an editor that has been near the file may have added it.
*
* The directives:
*
* target PATH the script the interpreter is given
* app-exec NAME names the first-run marker directory
* app-display NAME the name messages are printed under
* set NAME=VALUE set an environment variable
* unset NAME remove one
* prepend-path VALUE put VALUE at the front of PATH
*
* In a VALUE, `{root}` is the bundle root and `{{` is a literal '{'.
* Any other '{' is a syntax error rather than literal text, so a
* mistyped token fails at launch instead of reaching an app as a path
* that does not exist.
*
* An unrecognised directive is ARZ_E_SYNTAX, deliberately: a bundle
* whose configuration mentions something this runner cannot do is a
* bundle whose environment would be silently incomplete, and an app
* that starts with half its libraries unfindable fails later and much
* worse than one that does not start.
*
* Missing directives are NOT a parse error — see arz_config_missing. */
arz_status arz_config_parse(const arz_char *text, arz_config *cfg,
size_t *error_line);
/* The first required directive missing from a parsed config, or
* ARZ_KEY_NONE. Environment directives are all optional: a bundle with
* none is a legitimate, if unusual, bundle. */
arz_key arz_config_missing(const arz_config *cfg);
/* The directive's name in the file, ASCII, for messages and tests. */
const char *arz_key_name(arz_key key);
void arz_config_free(arz_config *cfg);
/* Parse exactly:
*
* protocol=1\n
* nonce=<64 lowercase hexadecimal characters>\n
* candidate=<ASCII digits>.<ASCII digits>.<ASCII digits>\n
*
* with an optional final newline. No duplicate, reordered, unknown or
* trailing fields are accepted. */
arz_status arz_handoff_parse(const arz_char *text, arz_handoff *handoff);
/* Constant-time comparison for a parsed nonce and an expected 64-character
* lowercase hexadecimal nonce. */
int arz_handoff_nonce_matches(const arz_handoff *handoff,
const arz_char *expected);
void arz_handoff_free(arz_handoff *handoff);
/* ------------------------------------------------------------------ */
/* Paths and environment values */
/* ------------------------------------------------------------------ */
/* `root` and `rel` joined with a single backslash, whatever mixture of
* trailing and leading separators they arrive with. Either separator is
* accepted on input; a backslash is always what comes out, because that
* is what the sidecar records and what a Windows user sees in an error
* message. */
arz_char *arz_path_join(const arz_char *root, const arz_char *rel);
/* `path` with its last component removed, or NULL when it has no
* separator to remove one at. Any trailing separators are ignored
* first, so both `C:\a\b` and `C:\a\b\` give `C:\a`. */
arz_char *arz_parent_dir(const arz_char *path);
/* A directive's value with `{root}` replaced by `root` and `{{` by a
* literal '{'. The value has already been through arz_config_parse,
* which rejects any other token, so the only failure left here is an
* allocation one (NULL). */
arz_char *arz_expand(const arz_char *value, const arz_char *root);
/* <dir>;<current> — or just <dir> when `current` is NULL or empty, since
* a trailing separator on PATH is an empty entry, which Windows reads as
* the current directory. */
arz_char *arz_env_path_prepend(const arz_char *dir, const arz_char *current);
/* <root>\rakudo\bin\raku.exe */
arz_char *arz_interpreter_path(const arz_char *root);
/* ------------------------------------------------------------------ */
/* Command lines */
/* ------------------------------------------------------------------ */
/* Where the caller's arguments begin in a raw command line, as a pointer
* into `cmdline` — never NULL, and pointing at the terminating NUL when
* there are none.
*
* This is the whole reason the runner exists, so it follows the C
* runtime's argv[0] rule exactly rather than approximately:
*
* * leading whitespace is skipped;
* * a command line starting with '"' has an argv[0] that ends at the
* NEXT '"' — inside it a backslash is a literal backslash and never
* an escape, because argv[0] has to be a path and a path cannot
* contain a quote;
* * otherwise argv[0] ends at the first space or tab;
* * whitespace between argv[0] and the rest is skipped.
*
* Everything after that point is handed to the child untouched. No
* re-quoting, no expansion, no splitting: that is what stops '^', '%x%',
* '!x!', embedded quotes and trailing backslashes from being mangled on
* the way through, which is what a .cmd trampoline cannot avoid doing. */
const arz_char *arz_cmdline_tail(const arz_char *cmdline);
/* `arg` quoted for a Windows command line, by the rules the C runtime
* parses back: the whole argument is wrapped in quotes, a backslash run
* immediately before a quote (or before the closing quote) is doubled,
* and an embedded quote is escaped.
*
* Used for the two arguments the runner itself supplies — the
* interpreter and the script — and for nothing the user typed. */
arz_char *arz_quote_arg(const arz_char *arg);
/* The child's command line: the quoted interpreter, the quoted script,
* then `tail` verbatim if there is any. `tail` may be NULL or empty. */
arz_char *arz_child_cmdline(const arz_char *exe, const arz_char *script,
const arz_char *tail);
#ifdef __cplusplus
}
#endif
#endif /* ARIZA_RUNNER_H */