Skip to content

Commit de76915

Browse files
committed
Add EMSCRIPTEN support for URL query argument parsing
1 parent 922f55f commit de76915

1 file changed

Lines changed: 113 additions & 11 deletions

File tree

src/args.c

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
#define BUGS_ADDRESS "https://github.com/X65/emu/issues"
1111
const char* app_bug_address = BUGS_ADDRESS;
1212
const char* app_releases_address = "https://github.com/X65/emu/releases";
13-
#define FULL_NAME "X65 microcomputer emulator"
13+
#define SHORT_NAME "emu"
14+
#define FULL_NAME "X65 microcomputer emulator"
1415
const char full_name[] = FULL_NAME;
1516

1617
struct arguments arguments = {
@@ -62,7 +63,7 @@ static const option_t options[] = {
6263
#define HELP_LINE_WIDTH 79 // total line width used for word-wrapping
6364

6465
static void print_usage(FILE* f) {
65-
fprintf(f, "Usage: emu [OPTION...] %s\n", args_doc);
66+
fprintf(f, "Usage: " SHORT_NAME " [OPTION...] %s\n", args_doc);
6667
}
6768

6869
static bool key_is_short(int key) {
@@ -168,9 +169,117 @@ static void print_help(void) {
168169
printf("\nReport bugs to: %s\n", app_bug_address);
169170
}
170171

172+
#ifdef __EMSCRIPTEN__
173+
#include <ctype.h>
174+
#include <emscripten/emscripten.h>
175+
176+
// keep the JS runtime helpers used by the EM_JS below alive through closure
177+
#if defined(EM_JS_DEPS)
178+
EM_JS_DEPS(x65args, "$withStackSave,$stringToUTF8OnStack");
179+
#endif
180+
181+
// The page URL query string, captured from JS into a C string.
182+
static char* web_query = NULL;
183+
184+
EMSCRIPTEN_KEEPALIVE void args_store_query(const char* q) {
185+
web_query = strdup(q ? q : "");
186+
}
187+
188+
// Read window.location.search and hand it to args_store_query() as a C string.
189+
// clang-format off
190+
EM_JS(void, args_js_read_query, (void), {
191+
var search = window.location.search || "";
192+
withStackSave(function() { _args_store_query(stringToUTF8OnStack(search)); });
193+
})
194+
// clang-format on
195+
196+
// Decode %XX escapes and '+' in place (application/x-www-form-urlencoded style).
197+
static void url_decode(char* s) {
198+
char* o = s;
199+
for (char* p = s; *p;) {
200+
if (*p == '%' && isxdigit((unsigned char)p[1]) && isxdigit((unsigned char)p[2])) {
201+
char hex[3] = { p[1], p[2], 0 };
202+
*o++ = (char)strtol(hex, NULL, 16);
203+
p += 3;
204+
}
205+
else if (*p == '+') {
206+
*o++ = ' ';
207+
p++;
208+
}
209+
else {
210+
*o++ = *p++;
211+
}
212+
}
213+
*o = '\0';
214+
}
215+
216+
// Convert the page URL query string into an argv array parseable by optparse:
217+
// ?fullscreen&crt=1,2,3&file=rom.xex -> { prog, "--fullscreen", "--crt=1,2,3", "rom.xex", NULL }
218+
// Rules: split on '&'; "file=VALUE" becomes a bare positional (may repeat, for
219+
// many-files support); every other token that isn't already "--..." is given a
220+
// "--" prefix (only the long "--option[=value]" form is supported).
221+
static char** web_build_argv(char* prog) {
222+
// window.location.search, e.g. "?file=rom.xex&crt=1,2,3&fullscreen"
223+
args_js_read_query();
224+
const char* search = web_query ? web_query : "";
225+
if (search[0] == '?') search++;
226+
char* buf = strdup(search);
227+
228+
int cap = 8, argc = 0;
229+
char** argv = malloc((size_t)cap * sizeof(char*));
230+
#define PUSH(S) \
231+
do { \
232+
if (argc + 1 >= cap) { \
233+
cap *= 2; \
234+
argv = realloc(argv, (size_t)cap * sizeof(char*)); \
235+
} \
236+
argv[argc++] = (S); \
237+
} while (0)
238+
239+
PUSH(prog ? prog : (char*)SHORT_NAME); // argv[0] is the program name
240+
241+
char* p = buf;
242+
while (*p) {
243+
char* tok = p;
244+
char* amp = strchr(p, '&');
245+
if (amp) {
246+
*amp = '\0';
247+
p = amp + 1;
248+
}
249+
else {
250+
p += strlen(p);
251+
}
252+
if (!tok[0]) continue;
253+
url_decode(tok);
254+
255+
if (strncmp(tok, "file=", 5) == 0) {
256+
if (tok[5]) PUSH(tok + 5); // positional file argument
257+
}
258+
else if (strncmp(tok, "--", 2) == 0) {
259+
PUSH(tok); // already in long form
260+
}
261+
else {
262+
size_t n = strlen(tok) + 3;
263+
char* opt = malloc(n);
264+
snprintf(opt, n, "--%s", tok);
265+
PUSH(opt);
266+
}
267+
}
268+
PUSH(NULL); // optparse expects a NULL-terminated argv
269+
#undef PUSH
270+
271+
return argv;
272+
}
273+
#endif // __EMSCRIPTEN__
274+
171275
void args_parse(int argc, char* argv[]) {
172276
(void)argc;
173277

278+
#ifdef __EMSCRIPTEN__
279+
// On the web there is no command line; arguments come from the page URL.
280+
argv = web_build_argv(argc > 0 ? argv[0] : NULL);
281+
#endif
282+
174283
// Build the optparse long-option table from our single source of truth,
175284
// plus the synthetic --help/--version entries argp-like.
176285
struct optparse_long longopts[sizeof(options) / sizeof(options[0]) + 2];
@@ -219,20 +328,13 @@ void args_parse(int argc, char* argv[]) {
219328

220329
case '?':
221330
fprintf(stderr, "%s: %s\n", app_name, opt.errmsg);
222-
fprintf(stderr, "Try 'emu --help' for more information.\n");
331+
fprintf(stderr, "Try '" SHORT_NAME " --help' for more information.\n");
223332
exit(EXIT_FAILURE);
224333
}
225334
}
226335

227336
const char* pos;
228-
bool rom_set = false;
229337
while ((pos = optparse_arg(&opt))) {
230-
if (rom_set) {
231-
fprintf(stderr, "%s: too many arguments\n", app_name);
232-
print_usage(stderr);
233-
exit(EXIT_FAILURE);
234-
}
235-
arguments.rom = pos;
236-
rom_set = true;
338+
if (!arguments.rom) arguments.rom = pos; // first positional file
237339
}
238340
}

0 commit comments

Comments
 (0)