249 lines
7.2 KiB
C
249 lines
7.2 KiB
C
// http_playdate.c - HTTP module for Playdate using Playdate Network API
|
|
// Note: Playdate HTTP does not support SSL/HTTPS
|
|
|
|
#include "cell.h"
|
|
#include "pd_api.h"
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
// Global Playdate API pointers - defined in main_playdate.c
|
|
extern const struct playdate_network *pd_network;
|
|
extern const struct playdate_sys *pd_sys;
|
|
|
|
#if TARGET_EXTENSION
|
|
|
|
// Context for async HTTP fetch
|
|
typedef struct {
|
|
JSContext *js;
|
|
HTTPConnection *conn;
|
|
uint8_t *data;
|
|
size_t data_len;
|
|
size_t data_cap;
|
|
int complete;
|
|
int success;
|
|
int status_code;
|
|
} http_fetch_ctx;
|
|
|
|
static void http_response_callback(HTTPConnection *conn) {
|
|
http_fetch_ctx *ctx = (http_fetch_ctx *)pd_network->http->getUserdata(conn);
|
|
if (!ctx) return;
|
|
|
|
ctx->status_code = pd_network->http->getResponseStatus(conn);
|
|
|
|
// Read all available data
|
|
while (1) {
|
|
size_t avail = pd_network->http->getBytesAvailable(conn);
|
|
if (avail == 0) break;
|
|
|
|
// Grow buffer if needed
|
|
if (ctx->data_len + avail > ctx->data_cap) {
|
|
size_t new_cap = ctx->data_cap * 2;
|
|
if (new_cap < ctx->data_len + avail) new_cap = ctx->data_len + avail + 4096;
|
|
uint8_t *new_data = realloc(ctx->data, new_cap);
|
|
if (!new_data) {
|
|
ctx->success = 0;
|
|
ctx->complete = 1;
|
|
return;
|
|
}
|
|
ctx->data = new_data;
|
|
ctx->data_cap = new_cap;
|
|
}
|
|
|
|
int read = pd_network->http->read(conn, ctx->data + ctx->data_len, (unsigned int)avail);
|
|
if (read < 0) {
|
|
ctx->success = 0;
|
|
ctx->complete = 1;
|
|
return;
|
|
}
|
|
ctx->data_len += read;
|
|
}
|
|
}
|
|
|
|
static void http_complete_callback(HTTPConnection *conn) {
|
|
http_fetch_ctx *ctx = (http_fetch_ctx *)pd_network->http->getUserdata(conn);
|
|
if (!ctx) return;
|
|
|
|
// Read any remaining data
|
|
http_response_callback(conn);
|
|
|
|
ctx->success = (ctx->status_code >= 200 && ctx->status_code < 400);
|
|
ctx->complete = 1;
|
|
}
|
|
|
|
static void http_closed_callback(HTTPConnection *conn) {
|
|
http_fetch_ctx *ctx = (http_fetch_ctx *)pd_network->http->getUserdata(conn);
|
|
if (!ctx) return;
|
|
ctx->complete = 1;
|
|
}
|
|
|
|
// Parse URL into host, port, path, and check if HTTPS
|
|
static int parse_url(const char *url, char **host, int *port, char **path, int *is_https) {
|
|
*host = NULL;
|
|
*path = NULL;
|
|
*port = 80;
|
|
*is_https = 0;
|
|
|
|
const char *p = url;
|
|
|
|
// Check scheme
|
|
if (strncmp(p, "https://", 8) == 0) {
|
|
*is_https = 1;
|
|
*port = 443;
|
|
p += 8;
|
|
} else if (strncmp(p, "http://", 7) == 0) {
|
|
p += 7;
|
|
} else {
|
|
return -1; // Invalid scheme
|
|
}
|
|
|
|
// Find end of host (either :, /, or end of string)
|
|
const char *host_start = p;
|
|
const char *host_end = p;
|
|
while (*host_end && *host_end != ':' && *host_end != '/') host_end++;
|
|
|
|
size_t host_len = host_end - host_start;
|
|
*host = malloc(host_len + 1);
|
|
if (!*host) return -1;
|
|
memcpy(*host, host_start, host_len);
|
|
(*host)[host_len] = '\0';
|
|
|
|
p = host_end;
|
|
|
|
// Check for port
|
|
if (*p == ':') {
|
|
p++;
|
|
*port = atoi(p);
|
|
while (*p && *p != '/') p++;
|
|
}
|
|
|
|
// Get path (default to "/" if none)
|
|
if (*p == '/') {
|
|
*path = strdup(p);
|
|
} else {
|
|
*path = strdup("/");
|
|
}
|
|
|
|
if (!*path) {
|
|
free(*host);
|
|
*host = NULL;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Performs a blocking HTTP GET and returns a QuickJS Blob of the body
|
|
static JSValue js_fetch_playdate(JSContext *ctx, JSValueConst this_val,
|
|
int argc, JSValueConst *argv) {
|
|
if (argc < 1 || !JS_IsText(argv[0]))
|
|
return JS_ThrowTypeError(ctx, "fetch: URL string required");
|
|
|
|
if (!pd_network || !pd_network->http) {
|
|
return JS_ThrowInternalError(ctx, "fetch: Playdate network API not available");
|
|
}
|
|
|
|
const char *url = JS_ToCString(ctx, argv[0]);
|
|
if (!url) return JS_ThrowTypeError(ctx, "fetch: invalid URL");
|
|
|
|
char *host = NULL;
|
|
char *path = NULL;
|
|
int port = 80;
|
|
int is_https = 0;
|
|
|
|
if (parse_url(url, &host, &port, &path, &is_https) < 0) {
|
|
JS_FreeCString(ctx, url);
|
|
return JS_ThrowTypeError(ctx, "fetch: failed to parse URL");
|
|
}
|
|
|
|
JS_FreeCString(ctx, url);
|
|
|
|
// Playdate doesn't support HTTPS
|
|
if (is_https) {
|
|
free(host);
|
|
free(path);
|
|
return JS_ThrowTypeError(ctx, "fetch: HTTPS not supported on Playdate");
|
|
}
|
|
|
|
// Create HTTP connection
|
|
HTTPConnection *conn = pd_network->http->newConnection(host, port, 0);
|
|
free(host);
|
|
|
|
if (!conn) {
|
|
free(path);
|
|
return JS_ThrowInternalError(ctx, "fetch: failed to create connection");
|
|
}
|
|
|
|
// Set up context
|
|
http_fetch_ctx fetch_ctx = {0};
|
|
fetch_ctx.js = ctx;
|
|
fetch_ctx.conn = conn;
|
|
fetch_ctx.data = malloc(4096);
|
|
fetch_ctx.data_cap = 4096;
|
|
fetch_ctx.data_len = 0;
|
|
fetch_ctx.complete = 0;
|
|
fetch_ctx.success = 0;
|
|
|
|
if (!fetch_ctx.data) {
|
|
pd_network->http->release(conn);
|
|
free(path);
|
|
return JS_ThrowInternalError(ctx, "fetch: malloc failed");
|
|
}
|
|
|
|
pd_network->http->setUserdata(conn, &fetch_ctx);
|
|
pd_network->http->setResponseCallback(conn, http_response_callback);
|
|
pd_network->http->setRequestCompleteCallback(conn, http_complete_callback);
|
|
pd_network->http->setConnectionClosedCallback(conn, http_closed_callback);
|
|
pd_network->http->setConnectTimeout(conn, 30000); // 30 second timeout
|
|
pd_network->http->setReadTimeout(conn, 30000);
|
|
|
|
// Start the GET request
|
|
PDNetErr err = pd_network->http->get(conn, path, NULL, 0);
|
|
free(path);
|
|
|
|
if (err != NET_OK) {
|
|
free(fetch_ctx.data);
|
|
pd_network->http->release(conn);
|
|
return JS_ThrowInternalError(ctx, "fetch: request failed with error %d", err);
|
|
}
|
|
|
|
// Poll until complete (blocking)
|
|
// Note: This is a simple blocking implementation. In a real game,
|
|
// you'd want to use async callbacks instead.
|
|
while (!fetch_ctx.complete) {
|
|
// Small delay to avoid busy-waiting
|
|
pd_sys->delay(10);
|
|
}
|
|
|
|
pd_network->http->close(conn);
|
|
pd_network->http->release(conn);
|
|
|
|
if (!fetch_ctx.success) {
|
|
free(fetch_ctx.data);
|
|
return JS_ThrowTypeError(ctx, "fetch: request failed (status %d)", fetch_ctx.status_code);
|
|
}
|
|
|
|
// Return a Blob wrapping the data
|
|
JSValue blob = js_new_blob_stoned_copy(ctx, fetch_ctx.data, fetch_ctx.data_len);
|
|
free(fetch_ctx.data);
|
|
return blob;
|
|
}
|
|
|
|
#else
|
|
// Simulator/non-extension build - provide stub
|
|
static JSValue js_fetch_playdate(JSContext *ctx, JSValueConst this_val,
|
|
int argc, JSValueConst *argv) {
|
|
return JS_ThrowInternalError(ctx, "fetch: not available in simulator");
|
|
}
|
|
#endif
|
|
|
|
static const JSCFunctionListEntry js_http_funcs[] = {
|
|
JS_CFUNC_DEF("fetch", 2, js_fetch_playdate),
|
|
};
|
|
|
|
JSValue js_http_use(JSContext *js) {
|
|
JSValue obj = JS_NewObject(js);
|
|
JS_SetPropertyFunctionList(js, obj, js_http_funcs,
|
|
sizeof(js_http_funcs)/sizeof(js_http_funcs[0]));
|
|
return obj;
|
|
}
|