MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1#include "utils.h"
2#include "base64.h"
3#include "escape.h"
4#include "esm/remote.h"
5
6#include <stdio.h>
7#include <stdlib.h>
8#include <string.h>
9#include <stdint.h>
10#include <uv.h>
11#include <tlsuv/tlsuv.h>
12#include <tlsuv/http.h>
13
14#ifdef _WIN32
15#include <direct.h>
16#define WIN32_LEAN_AND_MEAN
17#include <windows.h>
18#define mkdir_p(path) _mkdir(path)
19#define PATH_SEP '\\'
20#else
21#include <sys/stat.h>
22#include <sys/types.h>
23#define mkdir_p(path) mkdir(path, 0755)
24#define PATH_SEP '/'
25#endif
26
27typedef struct {
28 char *data;
29 size_t size;
30 size_t capacity;
31 int status_code;
32 int completed;
33 int failed;
34 char *error_msg;
35 tlsuv_http_t http_client;
36 tlsuv_http_req_t *http_req;
37} esm_url_fetch_t;
38
39bool esm_is_url(const char *spec) {
40 return (strncmp(spec, "http://", 7) == 0 || strncmp(spec, "https://", 8) == 0);
41}
42
43bool esm_is_data_url(const char *spec) {
44 return strncmp(spec, "data:", 5) == 0;
45}
46
47static char *esm_percent_decode(const char *src, size_t src_len, size_t *out_len) {
48 char *out = malloc(src_len + 1);
49 if (!out) return NULL;
50
51 size_t j = 0;
52 for (size_t i = 0; i < src_len; i++) {
53 if (src[i] == '%' && i + 2 < src_len &&
54 is_xdigit(src[i + 1]) && is_xdigit(src[i + 2])) {
55 out[j++] = (char)((unhex(src[i + 1]) << 4) | unhex(src[i + 2]));
56 i += 2;
57 } else out[j++] = src[i];
58 }
59
60 out[j] = '\0';
61 if (out_len) *out_len = j;
62 return out;
63}
64
65char *esm_parse_data_url(const char *url, size_t *out_len) {
66 if (!esm_is_data_url(url)) return NULL;
67
68 const char *comma = strchr(url + 5, ',');
69 if (!comma) return NULL;
70
71 const char *header = url + 5;
72 size_t header_len = (size_t)(comma - header);
73
74 const char *body = comma + 1;
75 size_t body_len = strlen(body);
76
77 bool is_base64 = (header_len >= 7 && strncmp(comma - 7, ";base64", 7) == 0);
78 if (is_base64) {
79 uint8_t *decoded = ant_base64_decode(body, body_len, out_len);
80 if (!decoded) return NULL;
81
82 char *result = malloc(*out_len + 1);
83 if (!result) { free(decoded); return NULL; }
84
85 memcpy(result, decoded, *out_len);
86 result[*out_len] = '\0';
87 free(decoded);
88
89 return result;
90 }
91
92 return esm_percent_decode(body, body_len, out_len);
93}
94
95static int is_path_sep(char c) {
96 return c == '/' || c == '\\';
97}
98
99static void esm_url_fetch_close_cb(tlsuv_http_t *client) {
100 esm_url_fetch_t *ctx = (esm_url_fetch_t *)client->data;
101 ctx->completed = 1;
102}
103
104static const char *esm_get_home_dir(void) {
105#ifdef _WIN32
106 const char *home = getenv("USERPROFILE");
107 if (home) return home;
108
109 const char *drive = getenv("HOMEDRIVE");
110 const char *path = getenv("HOMEPATH");
111 if (drive && path) {
112 static char win_home[MAX_PATH];
113 snprintf(win_home, sizeof(win_home), "%s%s", drive, path);
114 return win_home;
115 }
116#else
117 const char *home = getenv("HOME");
118 if (home) return home;
119#endif
120 return NULL;
121}
122
123static void esm_url_fetch_body_cb(tlsuv_http_req_t *http_req, char *body, ssize_t len) {
124 esm_url_fetch_t *ctx = (esm_url_fetch_t *)http_req->data;
125
126 if (len == UV_EOF) {
127 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
128 return;
129 }
130
131 if (len < 0) {
132 ctx->failed = 1;
133 ctx->error_msg = strdup(uv_strerror((int)len));
134 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
135 return;
136 }
137
138 if (ctx->size + (size_t)len > ctx->capacity) {
139 size_t new_cap = ctx->capacity * 2;
140 while (new_cap < ctx->size + (size_t)len) new_cap *= 2;
141 char *new_data = realloc(ctx->data, new_cap);
142 if (!new_data) {
143 ctx->failed = 1;
144 ctx->error_msg = strdup("Out of memory");
145 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
146 return;
147 }
148 ctx->data = new_data;
149 ctx->capacity = new_cap;
150 }
151
152 memcpy(ctx->data + ctx->size, body, (size_t)len);
153 ctx->size += (size_t)len;
154}
155
156static void esm_url_fetch_resp_cb(tlsuv_http_resp_t *resp, void *data) {
157 (void)data;
158 esm_url_fetch_t *ctx = (esm_url_fetch_t *)resp->req->data;
159
160 if (resp->code < 0) {
161 ctx->failed = 1;
162 char err_buf[256];
163 snprintf(err_buf, sizeof(err_buf), "%s (code: %d)", uv_strerror(resp->code), resp->code);
164 ctx->error_msg = strdup(err_buf);
165 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
166 return;
167 }
168
169 ctx->status_code = resp->code;
170 resp->body_cb = esm_url_fetch_body_cb;
171}
172
173static char *esm_get_cache_path(const char *url) {
174 const char *home = esm_get_home_dir();
175 if (!home) home = ".";
176
177 uint64_t hash = hash_key(url, strlen(url));
178
179 size_t len = strlen(home) + 48;
180 char *cache_path = malloc(len);
181 if (!cache_path) return NULL;
182
183#ifdef _WIN32
184 snprintf(cache_path, len, "%s\\.ant\\remote\\%016llx", home, (unsigned long long)hash);
185#else
186 snprintf(cache_path, len, "%s/.ant/remote/%016llx", home, (unsigned long long)hash);
187#endif
188 return cache_path;
189}
190
191static char *esm_read_cache(const char *cache_path, size_t *out_len) {
192 FILE *fp = fopen(cache_path, "rb");
193 if (!fp) return NULL;
194
195 fseek(fp, 0, SEEK_END);
196 long size = ftell(fp);
197 fseek(fp, 0, SEEK_SET);
198
199 char *content = malloc(size + 1);
200 if (!content) {
201 fclose(fp);
202 return NULL;
203 }
204
205 fread(content, 1, size, fp);
206 fclose(fp);
207 content[size] = '\0';
208
209 if (out_len) *out_len = (size_t)size;
210 return content;
211}
212
213static void esm_mkdir_recursive(char *path) {
214 for (char *p = path + 1; *p; p++) {
215#ifdef _WIN32
216 if (p == path + 2 && path[1] == ':') continue;
217#endif
218 if (is_path_sep(*p)) {
219 *p = '\0';
220 mkdir_p(path);
221 *p = PATH_SEP;
222 }
223 }
224 mkdir_p(path);
225}
226
227static void esm_write_cache(const char *cache_path, const char *url, const char *content, size_t len) {
228 char *dir = strdup(cache_path);
229 if (!dir) return;
230
231 for (char *p = dir + strlen(dir) - 1; p > dir; p--) {
232 if (is_path_sep(*p)) { *p = '\0'; break; }
233 }
234
235 esm_mkdir_recursive(dir);
236
237 FILE *fp = fopen(cache_path, "wb");
238 if (!fp) { free(dir); return; }
239 fwrite(content, 1, len, fp);
240 fclose(fp);
241
242 size_t meta_len = strlen(dir) + 16;
243 char *meta_path = malloc(meta_len);
244 if (meta_path) {
245 snprintf(meta_path, meta_len, "%s/metadata.bin", dir);
246 FILE *mfp = fopen(meta_path, "ab");
247 if (mfp) {
248 uint64_t hash = hash_key(url, strlen(url));
249 uint16_t url_len = (uint16_t)strlen(url);
250 fwrite(&url_len, sizeof(url_len), 1, mfp);
251 fwrite(url, 1, url_len, mfp);
252 fwrite(&hash, sizeof(hash), 1, mfp);
253 fclose(mfp);
254 }
255 free(meta_path);
256 }
257
258 free(dir);
259}
260
261char *esm_fetch_url(const char *url, size_t *out_len, char **out_error) {
262 char *cache_path = esm_get_cache_path(url);
263 if (cache_path) {
264 char *cached = esm_read_cache(cache_path, out_len);
265 if (cached) { free(cache_path); return cached; }
266 }
267
268 uv_loop_t *loop = uv_default_loop();
269 esm_url_fetch_t ctx = {0};
270
271 ctx.capacity = 16384;
272 ctx.data = malloc(ctx.capacity);
273 if (!ctx.data) {
274 if (out_error) *out_error = strdup("Out of memory");
275 return NULL;
276 }
277
278 const char *scheme_end = strstr(url, "://");
279 if (!scheme_end) {
280 free(ctx.data);
281 if (out_error) *out_error = strdup("Invalid URL: no scheme");
282 return NULL;
283 }
284
285 const char *host_start = scheme_end + 3;
286 const char *path_start = strchr(host_start, '/');
287 const char *at_in_host = NULL;
288
289 for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) {
290 if (*p == '@') at_in_host = p;
291 }
292 if (at_in_host) host_start = at_in_host + 1;
293
294 size_t scheme_len = scheme_end - url;
295 size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start);
296 const char *path = path_start ? path_start : "/";
297
298 char *host_url = calloc(1, scheme_len + 3 + host_len + 1);
299 snprintf(host_url, scheme_len + 3 + host_len + 1, "%.*s://%.*s", (int)scheme_len, url, (int)host_len, host_start);
300 int rc = tlsuv_http_init(loop, &ctx.http_client, host_url);
301 free(host_url);
302
303 if (rc != 0) {
304 free(ctx.data);
305 if (out_error) *out_error = strdup("Failed to initialize HTTP client");
306 return NULL;
307 }
308
309 ctx.http_client.data = &ctx;
310 ctx.http_req = tlsuv_http_req(&ctx.http_client, "GET", path, esm_url_fetch_resp_cb, &ctx);
311
312 if (!ctx.http_req) {
313 free(ctx.data);
314 tlsuv_http_close(&ctx.http_client, NULL);
315 if (out_error) *out_error = strdup("Failed to create HTTP request");
316 return NULL;
317 }
318
319 ctx.http_req->data = &ctx;
320 while (!ctx.completed) uv_run(loop, UV_RUN_ONCE);
321
322 if (ctx.failed || ctx.status_code < 200 || ctx.status_code >= 400) {
323 if (out_error) {
324 if (ctx.error_msg) {
325 *out_error = ctx.error_msg;
326 ctx.error_msg = NULL;
327 } else {
328 char err_buf[64];
329 snprintf(err_buf, sizeof(err_buf), "HTTP error: %d", ctx.status_code);
330 *out_error = strdup(err_buf);
331 }
332 }
333 free(ctx.data);
334 if (ctx.error_msg) free(ctx.error_msg);
335 if (cache_path) free(cache_path);
336 return NULL;
337 }
338
339 ctx.data[ctx.size] = '\0';
340 if (out_len) *out_len = ctx.size;
341 if (ctx.error_msg) free(ctx.error_msg);
342
343 if (cache_path) {
344 esm_write_cache(cache_path, url, ctx.data, ctx.size);
345 free(cache_path);
346 }
347
348 return ctx.data;
349}
350
351char *esm_resolve_url(const char *specifier, const char *base_url) {
352 if (esm_is_url(specifier)) {
353 return strdup(specifier);
354 }
355
356 if (specifier[0] == '/') {
357 const char *scheme_end = strstr(base_url, "://");
358 if (!scheme_end) return NULL;
359
360 const char *host_start = scheme_end + 3;
361 const char *path_start = strchr(host_start, '/');
362
363 const char *at_in_host = NULL;
364 for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) {
365 if (*p == '@') at_in_host = p;
366 }
367 if (at_in_host) host_start = at_in_host + 1;
368
369 size_t scheme_len = scheme_end - base_url;
370 size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start);
371
372 size_t len = scheme_len + 3 + host_len + strlen(specifier) + 1;
373 char *result = malloc(len);
374 if (!result) return NULL;
375
376 snprintf(
377 result, len, "%.*s://%.*s%s",
378 (int)scheme_len, base_url,
379 (int)host_len, host_start,
380 specifier
381 );
382
383 return result;
384 }
385
386 if (specifier[0] == '.' && (specifier[1] == '/' || (specifier[1] == '.' && specifier[2] == '/'))) {
387 char *base_copy = strdup(base_url);
388 if (!base_copy) return NULL;
389
390 char *last_slash = strrchr(base_copy, '/');
391 char *scheme_end = strstr(base_copy, "://");
392 if (scheme_end && last_slash > scheme_end + 2) {
393 *last_slash = '\0';
394 }
395
396 const char *spec = specifier;
397 while (strncmp(spec, "../", 3) == 0) {
398 spec += 3;
399 char *prev_slash = strrchr(base_copy, '/');
400 if (prev_slash && prev_slash > scheme_end + 2) {
401 *prev_slash = '\0';
402 }
403 }
404
405 if (strncmp(spec, "./", 2) == 0) spec += 2;
406 size_t len = strlen(base_copy) + strlen(spec) + 2;
407 char *result = malloc(len);
408 if (!result) { free(base_copy); return NULL; }
409
410 snprintf(result, len, "%s/%s", base_copy, spec);
411 free(base_copy);
412
413 return result;
414 }
415
416 return strdup(specifier);
417}
418
419char *esm_resolve(const char *specifier, const char *base_path, char FILE_RESOLVER) {
420 if (esm_is_data_url(specifier)) return strdup(specifier);
421 if (esm_is_url(specifier) || esm_is_url(base_path)) return esm_resolve_url(specifier, base_path);
422 return file_resolver(specifier, base_path);
423}