From 0ac793d4f88daa62a8433dff7fbf95e34016334b Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Thu, 24 Dec 2015 10:12:12 -0800 Subject: [PATCH] Add Directory.list() to io. --- src/cli/modules.c | 6 +- src/module/io.c | 152 +++++++++++++-------- src/module/io.wren | 33 +++++ src/module/io.wren.inc | 33 +++++ test/io/directory/dir/a.txt | 1 + test/io/directory/dir/b.txt | 1 + test/io/directory/dir/c.txt | 1 + test/io/directory/list.wren | 8 ++ test/io/directory/list_file.wren | 3 + test/io/directory/list_nonexistent.wren | 3 + test/io/directory/list_wrong_arg_type.wren | 3 + 11 files changed, 188 insertions(+), 56 deletions(-) create mode 100644 test/io/directory/dir/a.txt create mode 100644 test/io/directory/dir/b.txt create mode 100644 test/io/directory/dir/c.txt create mode 100644 test/io/directory/list.wren create mode 100644 test/io/directory/list_file.wren create mode 100644 test/io/directory/list_nonexistent.wren create mode 100644 test/io/directory/list_wrong_arg_type.wren diff --git a/src/cli/modules.c b/src/cli/modules.c index 093844e6..27111e4c 100644 --- a/src/cli/modules.c +++ b/src/cli/modules.c @@ -7,6 +7,7 @@ #include "scheduler.wren.inc" #include "timer.wren.inc" +extern void directoryList(WrenVM* vm); extern void fileAllocate(WrenVM* vm); extern void fileFinalize(WrenVM* vm); extern void fileOpen(WrenVM* vm); @@ -35,7 +36,7 @@ extern void timerStartTimer(WrenVM* vm); // If you add a new class to the largest module below, make sure to bump this. // Note that it also includes an extra slot for the sentinel value indicating // the end of the list. -#define MAX_CLASSES_PER_MODULE 3 +#define MAX_CLASSES_PER_MODULE 4 // Describes one foreign method in a class. typedef struct @@ -87,6 +88,9 @@ typedef struct static ModuleRegistry modules[] = { MODULE(io) + CLASS(Directory) + STATIC_METHOD("list_(_,_)", directoryList) + END_CLASS CLASS(File) STATIC_METHOD("", fileAllocate) STATIC_METHOD("", fileFinalize) diff --git a/src/module/io.c b/src/module/io.c index 9f3a836b..ac1cc789 100644 --- a/src/module/io.c +++ b/src/module/io.c @@ -46,6 +46,96 @@ void ioShutdown() shutdownStdin(); } +// If [request] failed with an error, sends the runtime error to the VM and +// frees the request. +// +// Returns true if an error was reported. +static bool handleRequestError(uv_fs_t* request) +{ + if (request->result >= 0) return false; + + FileRequestData* data = (FileRequestData*)request->data; + WrenValue* fiber = (WrenValue*)data->fiber; + + schedulerResumeError(fiber, uv_strerror((int)request->result)); + + free(data); + uv_fs_req_cleanup(request); + free(request); + return true; +} + +// Allocates a new request that resumes [fiber] when it completes. +uv_fs_t* createRequest(WrenValue* fiber) +{ + uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t)); + + FileRequestData* data = (FileRequestData*)malloc(sizeof(FileRequestData)); + data->fiber = fiber; + + request->data = data; + return request; +} + +// Releases resources used by [request]. +// +// Returns the fiber that should be resumed after [request] completes. +WrenValue* freeRequest(uv_fs_t* request) +{ + FileRequestData* data = (FileRequestData*)request->data; + WrenValue* fiber = data->fiber; + + free(data); + uv_fs_req_cleanup(request); + free(request); + + return fiber; +} + +static void directoryListCallback(uv_fs_t* request) +{ + if (handleRequestError(request)) return; + + uv_dirent_t entry; + + // TODO: Right now, there's way to create a list using the C API so create a + // buffer containing all of the result paths terminated by '\0'. We'll split + // that back into a list in Wren. + size_t resultLength = 0; + size_t bufferSize = 1024; + char* result = (char*)malloc(bufferSize); + + while (uv_fs_scandir_next(request, &entry) != UV_EOF) + { + size_t length = strlen(entry.name); + + // Grow the buffer if needed. + while (resultLength + length + 1 > bufferSize) + { + bufferSize *= 2; + result = realloc(result, bufferSize); + } + + // Copy the path, including the null terminator. + memcpy(result + resultLength, entry.name, length + 1); + resultLength += length + 1; + } + + WrenValue* fiber = freeRequest(request); + schedulerResumeBytes(fiber, result, resultLength); + free(result); +} + +void directoryList(WrenVM* vm) +{ + const char* path = wrenGetArgumentString(vm, 1); + + uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 2)); + + // TODO: Check return. + uv_fs_scandir(getLoop(), request, path, 0, directoryListCallback); +} + void fileAllocate(WrenVM* vm) { // Store the file descriptor in the foreign data, so that we can get to it @@ -66,59 +156,12 @@ void fileFinalize(WrenVM* vm) uv_fs_req_cleanup(&request); } -// If [request] failed with an error, sends the runtime error to the VM and -// frees the request. -// -// Returns true if an error was reported. -static bool handleRequestError(uv_fs_t* request) -{ - if (request->result >= 0) return false; - - FileRequestData* data = (FileRequestData*)request->data; - WrenValue* fiber = (WrenValue*)data->fiber; - - schedulerResumeError(fiber, uv_strerror((int)request->result)); - - free(data); - uv_fs_req_cleanup(request); - free(request); - return true; -} - -// Allocates a new request that resumes [fiber] when it completes. -uv_fs_t* createRequest(WrenValue* fiber) -{ - uv_fs_t* request = (uv_fs_t*)malloc(sizeof(uv_fs_t)); - - FileRequestData* data = (FileRequestData*)malloc(sizeof(FileRequestData)); - data->fiber = fiber; - - request->data = data; - return request; -} - -// Releases resources used by [request]. -// -// Returns the fiber that should be resumed after [request] completes. -WrenValue* freeRequest(uv_fs_t* request) -{ - FileRequestData* data = (FileRequestData*)request->data; - WrenValue* fiber = data->fiber; - - free(data); - uv_fs_req_cleanup(request); - free(request); - - return fiber; -} - -static void openCallback(uv_fs_t* request) +static void fileOpenCallback(uv_fs_t* request) { if (handleRequestError(request)) return; double fd = (double)request->result; WrenValue* fiber = freeRequest(request); - schedulerResumeDouble(fiber, fd); } @@ -128,17 +171,16 @@ void fileOpen(WrenVM* vm) uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 2)); // TODO: Allow controlling flags and modes. - uv_fs_open(getLoop(), request, path, O_RDONLY, 0, openCallback); + uv_fs_open(getLoop(), request, path, O_RDONLY, 0, fileOpenCallback); } // Called by libuv when the stat call for size completes. -static void sizeCallback(uv_fs_t* request) +static void fileSizeCallback(uv_fs_t* request) { if (handleRequestError(request)) return; double size = (double)request->statbuf.st_size; WrenValue* fiber = freeRequest(request); - schedulerResumeDouble(fiber, size); } @@ -146,10 +188,10 @@ void fileSizePath(WrenVM* vm) { const char* path = wrenGetArgumentString(vm, 1); uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 2)); - uv_fs_stat(getLoop(), request, path, sizeCallback); + uv_fs_stat(getLoop(), request, path, fileSizeCallback); } -static void closeCallback(uv_fs_t* request) +static void fileCloseCallback(uv_fs_t* request) { if (handleRequestError(request)) return; @@ -173,7 +215,7 @@ void fileClose(WrenVM* vm) *foreign = -1; uv_fs_t* request = createRequest(wrenGetArgumentValue(vm, 1)); - uv_fs_close(getLoop(), request, fd, closeCallback); + uv_fs_close(getLoop(), request, fd, fileCloseCallback); wrenReturnBool(vm, false); } @@ -226,7 +268,7 @@ void fileSize(WrenVM* vm) int fd = *(int*)wrenGetArgumentForeign(vm, 0); // TODO: Assert fd != -1. - uv_fs_fstat(getLoop(), request, fd, sizeCallback); + uv_fs_fstat(getLoop(), request, fd, fileSizeCallback); } static void allocCallback(uv_handle_t* handle, size_t suggestedSize, diff --git a/src/module/io.wren b/src/module/io.wren index 03ca9ce0..d641743a 100644 --- a/src/module/io.wren +++ b/src/module/io.wren @@ -1,5 +1,37 @@ import "scheduler" for Scheduler +class Directory { + static list(path) { + if (!(path is String)) Fiber.abort("Path must be a string.") + + list_(path, Fiber.current) + + // We get back a byte array containing all of the paths, each terminated by + // a zero byte. + var entryBuffer = Scheduler.runNextScheduled_() + + // TODO: Add split() to String. + // Split it into an array of strings. + var entries = [] + var start = 0 + var i = 0 + var bytes = entryBuffer.bytes + while (i < bytes.count) { + if (bytes[i] == 0) { + var entry = entryBuffer[start...i] + start = i + 1 + entries.add(entry) + } + + i = i + 1 + } + + return entries + } + + foreign static list_(path, fiber) +} + foreign class File { static open(path) { if (!(path is String)) Fiber.abort("Path must be a string.") @@ -90,6 +122,7 @@ class Stdin { readStop_() if (__line != null) { + // Emit the last line. var line = __line __line = null if (__waitingFiber != null) __waitingFiber.transfer(line) diff --git a/src/module/io.wren.inc b/src/module/io.wren.inc index b9b5272f..32b62086 100644 --- a/src/module/io.wren.inc +++ b/src/module/io.wren.inc @@ -2,6 +2,38 @@ static const char* ioModuleSource = "import \"scheduler\" for Scheduler\n" "\n" +"class Directory {\n" +" static list(path) {\n" +" if (!(path is String)) Fiber.abort(\"Path must be a string.\")\n" +"\n" +" list_(path, Fiber.current)\n" +"\n" +" // We get back a byte array containing all of the paths, each terminated by\n" +" // a zero byte.\n" +" var entryBuffer = Scheduler.runNextScheduled_()\n" +"\n" +" // TODO: Add split() to String.\n" +" // Split it into an array of strings.\n" +" var entries = []\n" +" var start = 0\n" +" var i = 0\n" +" var bytes = entryBuffer.bytes\n" +" while (i < bytes.count) {\n" +" if (bytes[i] == 0) {\n" +" var entry = entryBuffer[start...i]\n" +" start = i + 1\n" +" entries.add(entry)\n" +" }\n" +"\n" +" i = i + 1\n" +" }\n" +"\n" +" return entries\n" +" }\n" +"\n" +" foreign static list_(path, fiber)\n" +"}\n" +"\n" "foreign class File {\n" " static open(path) {\n" " if (!(path is String)) Fiber.abort(\"Path must be a string.\")\n" @@ -92,6 +124,7 @@ static const char* ioModuleSource = " readStop_()\n" "\n" " if (__line != null) {\n" +" // Emit the last line.\n" " var line = __line\n" " __line = null\n" " if (__waitingFiber != null) __waitingFiber.transfer(line)\n" diff --git a/test/io/directory/dir/a.txt b/test/io/directory/dir/a.txt new file mode 100644 index 00000000..e068544d --- /dev/null +++ b/test/io/directory/dir/a.txt @@ -0,0 +1 @@ +this is a text file \ No newline at end of file diff --git a/test/io/directory/dir/b.txt b/test/io/directory/dir/b.txt new file mode 100644 index 00000000..e068544d --- /dev/null +++ b/test/io/directory/dir/b.txt @@ -0,0 +1 @@ +this is a text file \ No newline at end of file diff --git a/test/io/directory/dir/c.txt b/test/io/directory/dir/c.txt new file mode 100644 index 00000000..e068544d --- /dev/null +++ b/test/io/directory/dir/c.txt @@ -0,0 +1 @@ +this is a text file \ No newline at end of file diff --git a/test/io/directory/list.wren b/test/io/directory/list.wren new file mode 100644 index 00000000..bf61d759 --- /dev/null +++ b/test/io/directory/list.wren @@ -0,0 +1,8 @@ +import "io" for Directory + +var entries = Directory.list("test/io/directory/dir") + +// Ignore OS-specific dot files like ".DS_Store". +entries = entries.where {|entry| !entry.startsWith(".") }.toList + +System.print(entries) // expect: [a.txt, b.txt, c.txt] diff --git a/test/io/directory/list_file.wren b/test/io/directory/list_file.wren new file mode 100644 index 00000000..a411eeb0 --- /dev/null +++ b/test/io/directory/list_file.wren @@ -0,0 +1,3 @@ +import "io" for Directory + +var entries = Directory.list("test/io/directory/dir/a.txt") // expect runtime error: not a directory diff --git a/test/io/directory/list_nonexistent.wren b/test/io/directory/list_nonexistent.wren new file mode 100644 index 00000000..024e7de3 --- /dev/null +++ b/test/io/directory/list_nonexistent.wren @@ -0,0 +1,3 @@ +import "io" for Directory + +Directory.list("nonexistent") // expect runtime error: no such file or directory diff --git a/test/io/directory/list_wrong_arg_type.wren b/test/io/directory/list_wrong_arg_type.wren new file mode 100644 index 00000000..03bedcd5 --- /dev/null +++ b/test/io/directory/list_wrong_arg_type.wren @@ -0,0 +1,3 @@ +import "io" for Directory + +Directory.list(123) // expect runtime error: Path must be a string.