523 lines
16 KiB
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;
|
|
}
|