#include "cell.h" #include #include #include #include // --- Playdate File API Definitions --- #ifndef pdext_file_h #define pdext_file_h #if TARGET_EXTENSION typedef void SDFile; typedef enum { kFileRead = (1<<0), kFileReadData = (1<<1), kFileWrite = (1<<2), kFileAppend = (2<<2) } FileOptions; typedef struct { int isdir; unsigned int size; int m_year; int m_month; int m_day; int m_hour; int m_minute; int m_second; } FileStat; #endif #if !defined(SEEK_SET) # define SEEK_SET 0 /* Seek from beginning of file. */ # define SEEK_CUR 1 /* Seek from current position. */ # define SEEK_END 2 /* Set file pointer to EOF plus "offset" */ #endif struct playdate_file { const char* (*geterr)(void); int (*listfiles)(const char* path, void (*callback)(const char* path, void* userdata), void* userdata, int showhidden); int (*stat)(const char* path, FileStat* stat); int (*mkdir)(const char* path); int (*unlink)(const char* name, int recursive); int (*rename)(const char* from, const char* to); SDFile* (*open)(const char* name, FileOptions mode); int (*close)(SDFile* file); int (*read)(SDFile* file, void* buf, unsigned int len); int (*write)(SDFile* file, const void* buf, unsigned int len); int (*flush)(SDFile* file); int (*tell)(SDFile* file); int (*seek)(SDFile* file, int pos, int whence); }; #endif /* pdext_file_h */ // Assumed global pointer to the file API extern struct playdate_file* pd_file; // --- Helper Functions --- // Convert JS value to SDFile* (casting via intptr_t/int) static SDFile* js2fd(JSContext *ctx, JSValueConst val) { int32_t fd_int; if (JS_ToInt32(ctx, &fd_int, val) < 0) { JS_ThrowTypeError(ctx, "Expected file descriptor number"); return NULL; } return (SDFile*)(intptr_t)fd_int; } static time_t pd_time_to_unix(FileStat *fs) { struct tm t = {0}; t.tm_year = fs->m_year - 1900; t.tm_mon = fs->m_month - 1; t.tm_mday = fs->m_day; t.tm_hour = fs->m_hour; t.tm_min = fs->m_minute; t.tm_sec = fs->m_second; return mktime(&t); } // --- Implementation --- JSC_SCALL(fd_open, FileOptions flags = kFileRead; // Parse optional flags argument if (argc > 1 && JS_IsString(argv[1])) { const char *flag_str = JS_ToCString(js, argv[1]); flags = 0; if (strchr(flag_str, 'r')) flags |= kFileRead; if (strchr(flag_str, 'w')) flags |= kFileWrite; if (strchr(flag_str, 'a')) flags |= kFileAppend; if (strchr(flag_str, '+')) { // Read+Write not directly mapped, assume Write covers it or combine? // Playdate docs say kFileRead|kFileWrite is valid flags |= kFileRead | kFileWrite; } JS_FreeCString(js, flag_str); } SDFile* file = pd_file->open(str, flags); if (!file) { const char* err = pd_file->geterr(); ret = JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error"); } else { ret = JS_NewInt32(js, (int32_t)(intptr_t)file); } ) JSC_CCALL(fd_write, SDFile* fd = js2fd(js, argv[0]); if (!fd) return JS_EXCEPTION; size_t len; int wrote; if (JS_IsString(argv[1])) { const char *data = JS_ToCStringLen(js, &len, argv[1]); if (!data) return JS_EXCEPTION; wrote = pd_file->write(fd, data, (unsigned int)len); JS_FreeCString(js, data); } else { void *data = js_get_blob_data(js, &len, argv[1]); if (data == (void*)-1) return JS_EXCEPTION; wrote = pd_file->write(fd, data, (unsigned int)len); } if (wrote < 0) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "write failed: %s", err ? err : "unknown error"); } return JS_NewInt64(js, wrote); ) JSC_CCALL(fd_read, SDFile* fd = js2fd(js, argv[0]); if (!fd) return JS_EXCEPTION; size_t size = 4096; if (argc > 1) size = js2number(js, argv[1]); void *buf = malloc(size); if (!buf) return JS_ThrowInternalError(js, "malloc failed"); int bytes_read = pd_file->read(fd, buf, (unsigned int)size); if (bytes_read < 0) { free(buf); const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "read failed: %s", err ? err : "unknown error"); } ret = js_new_blob_stoned_copy(js, buf, bytes_read); free(buf); return ret; ) JSC_SCALL(fd_slurp, FileStat st; if (pd_file->stat(str, &st) != 0) { // const char* err = pd_file->geterr(); // stat usually returns -1 on error return JS_ThrowInternalError(js, "stat failed for %s", str); } if (st.isdir) return JS_ThrowTypeError(js, "path is a directory"); size_t size = st.size; if (size == 0) return js_new_blob_stoned_copy(js, NULL, 0); SDFile* fd = pd_file->open(str, kFileRead); if (!fd) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error"); } void *data = malloc(size); if (!data) { pd_file->close(fd); return JS_ThrowInternalError(js, "malloc failed"); } int bytes_read = pd_file->read(fd, data, (unsigned int)size); pd_file->close(fd); if (bytes_read < 0) { free(data); return JS_ThrowInternalError(js, "read failed"); } ret = js_new_blob_stoned_copy(js, data, bytes_read); free(data); ) JSC_CCALL(fd_lseek, SDFile* fd = js2fd(js, argv[0]); if (!fd) return JS_EXCEPTION; int offset = (int)js2number(js, argv[1]); int whence = SEEK_SET; if (argc > 2) { const char *whence_str = JS_ToCString(js, argv[2]); if (strcmp(whence_str, "cur") == 0) whence = SEEK_CUR; else if (strcmp(whence_str, "end") == 0) whence = SEEK_END; JS_FreeCString(js, whence_str); } if (pd_file->seek(fd, offset, whence) != 0) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "seek failed: %s", err ? err : "unknown error"); } // tell to get new pos int new_pos = pd_file->tell(fd); return JS_NewInt64(js, new_pos); ) JSC_CCALL(fd_getcwd, // Playdate is always at root of its sandbox return JS_NewString(js, "/"); ) JSC_SCALL(fd_rmdir, int recursive = 0; if (argc > 1) recursive = JS_ToBool(js, argv[1]); if (pd_file->unlink(str, recursive) != 0) { const char* err = pd_file->geterr(); ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, err ? err : "unknown error"); } ) JSC_SCALL(fd_mkdir, if (pd_file->mkdir(str) != 0) { const char* err = pd_file->geterr(); ret = JS_ThrowInternalError(js, "could not make directory %s: %s", str, err ? err : "unknown error"); } ) JSC_SCALL(fd_mv, if (argc < 2) ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path"); else if (!JS_IsString(argv[1])) ret = JS_ThrowTypeError(js, "second argument must be a string (new path)"); else { const char *new_path = JS_ToCString(js, argv[1]); if (pd_file->rename(str, new_path) != 0) { const char* err = pd_file->geterr(); ret = JS_ThrowInternalError(js, "could not rename %s to %s: %s", str, new_path, err ? err : "unknown error"); } JS_FreeCString(js, new_path); } ) JSC_SCALL(fd_symlink, // Not supported if (argc >= 2 && JS_IsString(argv[1])) { // consume arg JS_FreeCString(js, JS_ToCString(js, argv[1])); } ret = JS_ThrowInternalError(js, "symlink not supported on Playdate"); ) JSC_SCALL(fd_unlink, if (pd_file->unlink(str, 0) != 0) { const char* err = pd_file->geterr(); ret = JS_ThrowInternalError(js, "could not remove file %s: %s", str, err ? err : "unknown error"); } ) JSC_SCALL(fd_rm, // Recursive remove if (pd_file->unlink(str, 1) != 0) { const char* err = pd_file->geterr(); ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, err ? err : "unknown error"); } ) JSC_CCALL(fd_fsync, SDFile* fd = js2fd(js, argv[0]); if (!fd) return JS_EXCEPTION; if (pd_file->flush(fd) != 0) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "fsync failed: %s", err ? err : "unknown error"); } return JS_NULL; ) JSC_CCALL(fd_close, SDFile* fd = js2fd(js, argv[0]); if (!fd) return JS_EXCEPTION; if (pd_file->close(fd) != 0) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "close failed: %s", err ? err : "unknown error"); } return JS_NULL; ) JSC_CCALL(fd_fstat, // fstat not supported by Playdate API SDFile* fd = js2fd(js, argv[0]); if (!fd) return JS_EXCEPTION; // We can't get stat from SDFile*. Return minimal object or error? // fd.c returns full object. // Return empty object with error property? Or just size 0? // Let's return empty object. JSValue obj = JS_NewObject(js); return obj; ) JSC_SCALL(fd_stat, FileStat st; if (pd_file->stat(str, &st) != 0) { return JS_NewObject(js); // Return empty on failure/not found } JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.size)); // Approximate mode JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.isdir ? 0040755 : 0100644)); JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, 0)); time_t ts = pd_time_to_unix(&st); JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, ts)); JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, ts)); JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, ts)); JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, 1)); JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, 0)); JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, 0)); JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 512)); JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, (st.size + 511) / 512)); JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, !st.isdir)); JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, st.isdir)); JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, 0)); JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, 0)); JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, 0)); JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, 0)); JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, 0)); return obj; ) struct listfiles_ctx { JSContext *js; JSValue array; int index; }; static void listfiles_cb(const char *path, void *userdata) { struct listfiles_ctx *ctx = (struct listfiles_ctx*)userdata; if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) return; // Playdate listfiles returns just the name, but sometimes with slash for dir? // Docs say "names of files". JS_SetPropertyUint32(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path)); } JSC_SCALL(fd_readdir, JSValue ret_arr = JS_NewArray(js); struct listfiles_ctx ctx = { js, ret_arr, 0 }; if (pd_file->listfiles(str, listfiles_cb, &ctx, 0) != 0) { const char* err = pd_file->geterr(); JS_FreeValue(js, ret_arr); return JS_ThrowInternalError(js, "listfiles failed: %s", err ? err : "unknown error"); } ret = ret_arr; ) JSC_CCALL(fd_is_file, const char *path = JS_ToCString(js, argv[0]); if (!path) return JS_EXCEPTION; FileStat st; int res = pd_file->stat(path, &st); JS_FreeCString(js, path); if (res != 0) return JS_NewBool(js, 0); return JS_NewBool(js, !st.isdir); ) JSC_CCALL(fd_is_dir, const char *path = JS_ToCString(js, argv[0]); if (!path) return JS_EXCEPTION; FileStat st; int res = pd_file->stat(path, &st); JS_FreeCString(js, path); if (res != 0) return JS_NewBool(js, 0); return JS_NewBool(js, st.isdir); ) JSC_CCALL(fd_slurpwrite, size_t len; const char *data = js_get_blob_data(js, &len, argv[1]); if (data == (const char *)-1) return JS_EXCEPTION; const char *str = JS_ToCString(js, argv[0]); if (!str) return JS_EXCEPTION; SDFile* fd = pd_file->open(str, kFileWrite | kFileRead); // Truncates by default? Playdate docs: kFileWrite truncates. JS_FreeCString(js, str); if (!fd) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error"); } int written = pd_file->write(fd, data, (unsigned int)len); pd_file->close(fd); if (written != (int)len) { const char* err = pd_file->geterr(); return JS_ThrowInternalError(js, "write failed: %s", err ? err : "unknown error"); } return JS_NULL; ) // Helper for recursive enumeration static void visit_directory_pd(JSContext *js, JSValue results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) { // We can't use listfiles with arbitrary userdata easily if we need to pass multiple args without struct // We'll reuse listfiles but we need a way to pass context. // The context struct can hold current path info. // Actually, listfiles takes a callback. // We need to implement recursion. // But listfiles doesn't give us full paths in callback, just names. // So we can reconstruct. // We need a nested struct or helper. } struct enum_ctx { JSContext *js; JSValue results; int *count; const char *curr_path; const char *rel_prefix; int recurse; }; static void enum_cb(const char *name, void *userdata) { struct enum_ctx *ctx = (struct enum_ctx*)userdata; if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) return; char item_rel[512]; // PATH_MAX if (ctx->rel_prefix && strlen(ctx->rel_prefix) > 0) { snprintf(item_rel, sizeof(item_rel), "%s/%s", ctx->rel_prefix, name); } else { strcpy(item_rel, name); } JS_SetPropertyUint32(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel)); if (ctx->recurse) { // Check if directory char child_path[512]; snprintf(child_path, sizeof(child_path), "%s/%s", ctx->curr_path, name); // Remove trailing slash if name has it? Playdate names sometimes have trailing slash for dirs // But stat should handle it. FileStat st; if (pd_file->stat(child_path, &st) == 0 && st.isdir) { struct enum_ctx subctx = *ctx; subctx.curr_path = child_path; subctx.rel_prefix = item_rel; pd_file->listfiles(child_path, enum_cb, &subctx, 0); } } } JSC_SCALL(fd_enumerate, const char *path = str; if (!path) path = "."; int recurse = 0; if (argc > 1) recurse = JS_ToBool(js, argv[1]); JSValue results = JS_NewArray(js); int result_count = 0; FileStat st; if (pd_file->stat(path, &st) == 0 && st.isdir) { struct enum_ctx ctx = { js, results, &result_count, path, "", recurse }; pd_file->listfiles(path, enum_cb, &ctx, 0); } ret = results; ) JSC_CCALL(fd_realpath, const char *path = JS_ToCString(js, argv[0]); if (!path) return JS_EXCEPTION; // Playdate has no realpath, just return path JSValue res = JS_NewString(js, path); JS_FreeCString(js, path); return res; ) JSC_CCALL(fd_is_link, const char *path = JS_ToCString(js, argv[0]); JS_FreeCString(js, path); return JS_NewBool(js, 0); ) JSC_CCALL(fd_readlink, const char *path = JS_ToCString(js, argv[0]); JS_FreeCString(js, path); return JS_ThrowInternalError(js, "readlink not supported"); ) static const JSCFunctionListEntry js_fd_funcs[] = { MIST_FUNC_DEF(fd, open, 2), MIST_FUNC_DEF(fd, write, 2), MIST_FUNC_DEF(fd, read, 2), MIST_FUNC_DEF(fd, slurp, 1), MIST_FUNC_DEF(fd, slurpwrite, 2), MIST_FUNC_DEF(fd, lseek, 3), MIST_FUNC_DEF(fd, getcwd, 0), MIST_FUNC_DEF(fd, rmdir, 2), MIST_FUNC_DEF(fd, unlink, 1), MIST_FUNC_DEF(fd, mkdir, 1), MIST_FUNC_DEF(fd, mv, 2), MIST_FUNC_DEF(fd, rm, 1), MIST_FUNC_DEF(fd, fsync, 1), MIST_FUNC_DEF(fd, close, 1), MIST_FUNC_DEF(fd, stat, 1), MIST_FUNC_DEF(fd, fstat, 1), MIST_FUNC_DEF(fd, readdir, 1), MIST_FUNC_DEF(fd, is_file, 1), MIST_FUNC_DEF(fd, is_dir, 1), MIST_FUNC_DEF(fd, is_link, 1), MIST_FUNC_DEF(fd, enumerate, 2), MIST_FUNC_DEF(fd, symlink, 2), MIST_FUNC_DEF(fd, realpath, 1), MIST_FUNC_DEF(fd, readlink, 1), }; JSValue js_fd_use(JSContext *js) { JSValue mod = JS_NewObject(js); JS_SetPropertyFunctionList(js, mod, js_fd_funcs, countof(js_fd_funcs)); return mod; }