Files
cell/fd_playdate.c

523 lines
16 KiB
C

#include "cell.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "pd_api.h"
// Global Playdate API pointers - defined in main_playdate.c
extern const 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;
}