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 void esm_url_fetch_body_cb(tlsuv_http_req_t *http_req, char *body, ssize_t len) {
105 esm_url_fetch_t *ctx = (esm_url_fetch_t *)http_req->data;
106
107 if (len == UV_EOF) {
108 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
109 return;
110 }
111
112 if (len < 0) {
113 ctx->failed = 1;
114 ctx->error_msg = strdup(uv_strerror((int)len));
115 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
116 return;
117 }
118
119 if (ctx->size + (size_t)len > ctx->capacity) {
120 size_t new_cap = ctx->capacity * 2;
121 while (new_cap < ctx->size + (size_t)len) new_cap *= 2;
122 char *new_data = realloc(ctx->data, new_cap);
123 if (!new_data) {
124 ctx->failed = 1;
125 ctx->error_msg = strdup("Out of memory");
126 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
127 return;
128 }
129 ctx->data = new_data;
130 ctx->capacity = new_cap;
131 }
132
133 memcpy(ctx->data + ctx->size, body, (size_t)len);
134 ctx->size += (size_t)len;
135}
136
137static void esm_url_fetch_resp_cb(tlsuv_http_resp_t *resp, void *data) {
138 (void)data;
139 esm_url_fetch_t *ctx = (esm_url_fetch_t *)resp->req->data;
140
141 if (resp->code < 0) {
142 ctx->failed = 1;
143 char err_buf[256];
144 snprintf(err_buf, sizeof(err_buf), "%s (code: %d)", uv_strerror(resp->code), resp->code);
145 ctx->error_msg = strdup(err_buf);
146 tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb);
147 return;
148 }
149
150 ctx->status_code = resp->code;
151 resp->body_cb = esm_url_fetch_body_cb;
152}
153
154static char *esm_get_cache_path(const char *url) {
155 uint64_t hash = hash_key(url, strlen(url));
156
157 char suffix[64];
158 snprintf(suffix, sizeof(suffix), "remote/%016llx", (unsigned long long)hash);
159
160 size_t len = 4096;
161 char *cache_path = malloc(len);
162 if (!cache_path) return NULL;
163
164 if (ant_xdg_cache_path(cache_path, len, suffix) != 0) {
165 free(cache_path);
166 return NULL;
167 }
168
169 return cache_path;
170}
171
172static char *esm_read_cache(const char *cache_path, size_t *out_len) {
173 FILE *fp = fopen(cache_path, "rb");
174 if (!fp) return NULL;
175
176 fseek(fp, 0, SEEK_END);
177 long size = ftell(fp);
178 fseek(fp, 0, SEEK_SET);
179
180 char *content = malloc(size + 1);
181 if (!content) {
182 fclose(fp);
183 return NULL;
184 }
185
186 fread(content, 1, size, fp);
187 fclose(fp);
188 content[size] = '\0';
189
190 if (out_len) *out_len = (size_t)size;
191 return content;
192}
193
194static void esm_mkdir_recursive(char *path) {
195 for (char *p = path + 1; *p; p++) {
196#ifdef _WIN32
197 if (p == path + 2 && path[1] == ':') continue;
198#endif
199 if (is_path_sep(*p)) {
200 *p = '\0';
201 mkdir_p(path);
202 *p = PATH_SEP;
203 }
204 }
205 mkdir_p(path);
206}
207
208static void esm_write_cache(const char *cache_path, const char *url, const char *content, size_t len) {
209 char *dir = strdup(cache_path);
210 if (!dir) return;
211
212 for (char *p = dir + strlen(dir) - 1; p > dir; p--) {
213 if (is_path_sep(*p)) { *p = '\0'; break; }
214 }
215
216 esm_mkdir_recursive(dir);
217
218 FILE *fp = fopen(cache_path, "wb");
219 if (!fp) { free(dir); return; }
220 fwrite(content, 1, len, fp);
221 fclose(fp);
222
223 size_t meta_len = strlen(dir) + 16;
224 char *meta_path = malloc(meta_len);
225 if (meta_path) {
226 snprintf(meta_path, meta_len, "%s/metadata.bin", dir);
227 FILE *mfp = fopen(meta_path, "ab");
228 if (mfp) {
229 uint64_t hash = hash_key(url, strlen(url));
230 uint16_t url_len = (uint16_t)strlen(url);
231 fwrite(&url_len, sizeof(url_len), 1, mfp);
232 fwrite(url, 1, url_len, mfp);
233 fwrite(&hash, sizeof(hash), 1, mfp);
234 fclose(mfp);
235 }
236 free(meta_path);
237 }
238
239 free(dir);
240}
241
242char *esm_fetch_url(const char *url, size_t *out_len, char **out_error) {
243 char *cache_path = esm_get_cache_path(url);
244 if (cache_path) {
245 char *cached = esm_read_cache(cache_path, out_len);
246 if (cached) { free(cache_path); return cached; }
247 }
248
249 uv_loop_t *loop = uv_default_loop();
250 esm_url_fetch_t ctx = {0};
251
252 ctx.capacity = 16384;
253 ctx.data = malloc(ctx.capacity);
254 if (!ctx.data) {
255 if (out_error) *out_error = strdup("Out of memory");
256 return NULL;
257 }
258
259 const char *scheme_end = strstr(url, "://");
260 if (!scheme_end) {
261 free(ctx.data);
262 if (out_error) *out_error = strdup("Invalid URL: no scheme");
263 return NULL;
264 }
265
266 const char *host_start = scheme_end + 3;
267 const char *path_start = strchr(host_start, '/');
268 const char *at_in_host = NULL;
269
270 for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) {
271 if (*p == '@') at_in_host = p;
272 }
273 if (at_in_host) host_start = at_in_host + 1;
274
275 size_t scheme_len = scheme_end - url;
276 size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start);
277 const char *path = path_start ? path_start : "/";
278
279 char *host_url = calloc(1, scheme_len + 3 + host_len + 1);
280 snprintf(host_url, scheme_len + 3 + host_len + 1, "%.*s://%.*s", (int)scheme_len, url, (int)host_len, host_start);
281 int rc = tlsuv_http_init(loop, &ctx.http_client, host_url);
282 free(host_url);
283
284 if (rc != 0) {
285 free(ctx.data);
286 if (out_error) *out_error = strdup("Failed to initialize HTTP client");
287 return NULL;
288 }
289
290 ctx.http_client.data = &ctx;
291 ctx.http_req = tlsuv_http_req(&ctx.http_client, "GET", path, esm_url_fetch_resp_cb, &ctx);
292
293 if (!ctx.http_req) {
294 free(ctx.data);
295 tlsuv_http_close(&ctx.http_client, NULL);
296 if (out_error) *out_error = strdup("Failed to create HTTP request");
297 return NULL;
298 }
299
300 ctx.http_req->data = &ctx;
301 while (!ctx.completed) uv_run(loop, UV_RUN_ONCE);
302
303 if (ctx.failed || ctx.status_code < 200 || ctx.status_code >= 400) {
304 if (out_error) {
305 if (ctx.error_msg) {
306 *out_error = ctx.error_msg;
307 ctx.error_msg = NULL;
308 } else {
309 char err_buf[64];
310 snprintf(err_buf, sizeof(err_buf), "HTTP error: %d", ctx.status_code);
311 *out_error = strdup(err_buf);
312 }
313 }
314 free(ctx.data);
315 if (ctx.error_msg) free(ctx.error_msg);
316 if (cache_path) free(cache_path);
317 return NULL;
318 }
319
320 ctx.data[ctx.size] = '\0';
321 if (out_len) *out_len = ctx.size;
322 if (ctx.error_msg) free(ctx.error_msg);
323
324 if (cache_path) {
325 esm_write_cache(cache_path, url, ctx.data, ctx.size);
326 free(cache_path);
327 }
328
329 return ctx.data;
330}
331
332char *esm_resolve_url(const char *specifier, const char *base_url) {
333 if (esm_is_url(specifier)) {
334 return strdup(specifier);
335 }
336
337 if (specifier[0] == '/') {
338 const char *scheme_end = strstr(base_url, "://");
339 if (!scheme_end) return NULL;
340
341 const char *host_start = scheme_end + 3;
342 const char *path_start = strchr(host_start, '/');
343
344 const char *at_in_host = NULL;
345 for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) {
346 if (*p == '@') at_in_host = p;
347 }
348 if (at_in_host) host_start = at_in_host + 1;
349
350 size_t scheme_len = scheme_end - base_url;
351 size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start);
352
353 size_t len = scheme_len + 3 + host_len + strlen(specifier) + 1;
354 char *result = malloc(len);
355 if (!result) return NULL;
356
357 snprintf(
358 result, len, "%.*s://%.*s%s",
359 (int)scheme_len, base_url,
360 (int)host_len, host_start,
361 specifier
362 );
363
364 return result;
365 }
366
367 if (specifier[0] == '.' && (specifier[1] == '/' || (specifier[1] == '.' && specifier[2] == '/'))) {
368 char *base_copy = strdup(base_url);
369 if (!base_copy) return NULL;
370
371 char *last_slash = strrchr(base_copy, '/');
372 char *scheme_end = strstr(base_copy, "://");
373 if (scheme_end && last_slash > scheme_end + 2) {
374 *last_slash = '\0';
375 }
376
377 const char *spec = specifier;
378 while (strncmp(spec, "../", 3) == 0) {
379 spec += 3;
380 char *prev_slash = strrchr(base_copy, '/');
381 if (prev_slash && prev_slash > scheme_end + 2) {
382 *prev_slash = '\0';
383 }
384 }
385
386 if (strncmp(spec, "./", 2) == 0) spec += 2;
387 size_t len = strlen(base_copy) + strlen(spec) + 2;
388 char *result = malloc(len);
389 if (!result) { free(base_copy); return NULL; }
390
391 snprintf(result, len, "%s/%s", base_copy, spec);
392 free(base_copy);
393
394 return result;
395 }
396
397 return strdup(specifier);
398}
399
400char *esm_resolve(const char *specifier, const char *base_path, char FILE_RESOLVER) {
401 if (esm_is_data_url(specifier)) return strdup(specifier);
402 if (esm_is_url(specifier) || esm_is_url(base_path)) return esm_resolve_url(specifier, base_path);
403 return file_resolver(specifier, base_path);
404}