From b78df8d2c4f6e5000cb0df50ae430a58db80ceed Mon Sep 17 00:00:00 2001 From: Julien Gilli Date: Mon, 6 Oct 2014 20:39:07 -0700 Subject: [PATCH] fs: uv_fs_{open,read}_dir for unix Tested on Linux, MacOS X, SmartOS and Windows. Fixes #1430. --- Makefile.am | 1 + include/uv-unix.h | 7 + include/uv-win.h | 23 ++ include/uv.h | 27 +++ src/unix/fs.c | 258 ++++++++++++++++++++- src/uv-common.c | 41 +++- src/uv-common.h | 2 + src/win/fs.c | 229 ++++++++++++++++++ test/test-fs-readdir.c | 514 +++++++++++++++++++++++++++++++++++++++++ test/test-list.h | 8 + uv.gyp | 1 + 11 files changed, 1090 insertions(+), 21 deletions(-) create mode 100644 test/test-fs-readdir.c diff --git a/Makefile.am b/Makefile.am index 07d628d3e4..ffe5efc645 100644 --- a/Makefile.am +++ b/Makefile.am @@ -140,6 +140,7 @@ test_run_tests_SOURCES = test/blackhole-server.c \ test/test-fs-event.c \ test/test-fs-poll.c \ test/test-fs.c \ + test/test-fs-readdir.c \ test/test-get-currentexe.c \ test/test-get-loadavg.c \ test/test-get-memory.c \ diff --git a/include/uv-unix.h b/include/uv-unix.h index e72492564d..e5865bb0a4 100644 --- a/include/uv-unix.h +++ b/include/uv-unix.h @@ -158,6 +158,13 @@ typedef gid_t uv_gid_t; typedef uid_t uv_uid_t; typedef struct dirent uv__dirent_t; +/* + * "dirent" is used to hold a buffer large enough for any dirent in the + * directory being read. Avoids allocating for each directory entry. + */ +#define UV_DIR_PRIVATE_FIELDS \ + uv__dirent_t* dirent; \ + DIR* dir; #if defined(DT_UNKNOWN) # define HAVE_DIRENT_TYPES diff --git a/include/uv-win.h b/include/uv-win.h index 4abb294c05..7962f4ad4e 100644 --- a/include/uv-win.h +++ b/include/uv-win.h @@ -294,6 +294,29 @@ typedef struct uv__dirent_s { char d_name[1]; } uv__dirent_t; +/* + * "handle" is the actual directory handle that is used to perform calls + * to FindFirstFile/FindNextFile. + * + * "find_data" is needed to store the result of the FindFirstFile call that + * happened in uv_fs_opendir so that it can be used in the subsequent + * uv_fs_readdir call. + * + * "need_find_call" is a boolean determining if the next call to uv_fs_readdir + * must call FindNextFile. In uv_fs_opendir, FindFirstFile reads the first + * entry of the directory, and thus the subsequent call to uv_fs_readdir must + * not call FindNextFile, or otherwise it would miss the first directory entry. + * The next uv_fs_readdir calls after the first one will use FindNextFile. + * + * "dirent" is used to hold a buffer large enough for any dirent in the + * directory being read. It avoids allocating for each directory entry. + */ +#define UV_DIR_PRIVATE_FIELDS \ + HANDLE dir_handle; \ + WIN32_FIND_DATAW find_data; \ + BOOL need_find_call; \ + uv__dirent_t* dirent; + #define UV__DT_DIR UV_DIRENT_DIR #define UV__DT_FILE UV_DIRENT_FILE #define UV__DT_LINK UV_DIRENT_LINK diff --git a/include/uv.h b/include/uv.h index d17f977b44..97132774e3 100644 --- a/include/uv.h +++ b/include/uv.h @@ -197,6 +197,7 @@ typedef enum { /* Handle types. */ typedef struct uv_loop_s uv_loop_t; typedef struct uv_handle_s uv_handle_t; +typedef struct uv_dir_s uv_dir_t; typedef struct uv_stream_s uv_stream_t; typedef struct uv_tcp_s uv_tcp_t; typedef struct uv_udp_s uv_udp_t; @@ -1033,6 +1034,9 @@ typedef enum { UV_FS_MKDTEMP, UV_FS_RENAME, UV_FS_SCANDIR, + UV_FS_OPENDIR, + UV_FS_READDIR, + UV_FS_CLOSEDIR, UV_FS_LINK, UV_FS_SYMLINK, UV_FS_READLINK, @@ -1040,6 +1044,10 @@ typedef enum { UV_FS_FCHOWN } uv_fs_type; +struct uv_dir_s { + UV_DIR_PRIVATE_FIELDS +}; + /* uv_fs_t is a subclass of uv_req_t. */ struct uv_fs_s { UV_REQ_FIELDS @@ -1050,6 +1058,9 @@ struct uv_fs_s { void* ptr; const char* path; uv_stat_t statbuf; /* Stores the result of uv_fs_stat() and uv_fs_fstat(). */ + const uv_dir_t* dir; /* Stores the result of uv_fs_opendir() */ + uv_dirent_t* dirents; + size_t nentries; UV_FS_PRIVATE_FIELDS }; @@ -1102,6 +1113,22 @@ UV_EXTERN int uv_fs_scandir(uv_loop_t* loop, uv_fs_cb cb); UV_EXTERN int uv_fs_scandir_next(uv_fs_t* req, uv_dirent_t* ent); + +UV_EXTERN int uv_fs_opendir(uv_loop_t* loop, + uv_fs_t* req, + const char* path, + uv_fs_cb cb); +UV_EXTERN int uv_fs_readdir(uv_loop_t* loop, + uv_fs_t* req, + const uv_dir_t* dir, + uv_dirent_t dirents[], + size_t nentries, + uv_fs_cb cb); +UV_EXTERN int uv_fs_closedir(uv_loop_t* loop, + uv_fs_t* req, + const uv_dir_t* dir, + uv_fs_cb cb); + UV_EXTERN int uv_fs_stat(uv_loop_t* loop, uv_fs_t* req, const char* path, diff --git a/src/unix/fs.c b/src/unix/fs.c index 65fd01230b..c586cf1abe 100644 --- a/src/unix/fs.c +++ b/src/unix/fs.c @@ -136,6 +136,95 @@ static ssize_t uv__fs_fdatasync(uv_fs_t* req) { } +static ssize_t uv__fs_clamp_max_path_len(ssize_t len) { + if (len == -1) { + #if defined(PATH_MAX) + len = PATH_MAX; + #else + len = 4096; + #endif + } + + return len; +} + +/* + * Computes the required size in bytes for directory entries read from the + * directory identified by the handle "dir". + * + * This code does not trust values of NAME_MAX that are less than + * 255, since some systems (including at least HP-UX) incorrectly + * define it to be a smaller value. + * + * This code was copied from + * http://womble.decadent.org.uk/readdir_r-advisory.html + * and slightly adapted. + */ +static size_t uv__fs_direntry_size(const DIR* dir) { + long name_max; + size_t name_end; + + name_max = -1; + name_end = -1; + +#if defined(HAVE_FPATHCONF) && defined(HAVE_DIRFD) \ + && defined(_PC_NAME_MAX) + name_max = fpathconf(dirfd(dirp), _PC_NAME_MAX); + if (name_max == -1) +# if defined(NAME_MAX) + name_max = (NAME_MAX > 255) ? NAME_MAX : 255; +# else + return (size_t)(-1); +# endif /* defined(NAME_MAX) +#else +# if defined(NAME_MAX) + name_max = (NAME_MAX > 255) ? NAME_MAX : 255; +# else +# error "buffer size for readdir_r cannot be determined" +# endif /* defined(NAME_MAX) */ +#endif + + /* + * Most of the time, struct dirent is defined by operating systems' header + * files as either one of the following possible implementations: + * 1) + * + * struct dirent { + * char d_name[ NAME_MAX + 1 ]; + * other stuff; + * }; + * + * 2) + * + * struct dirent { + * char d_name[1]; + * other stuff; + * }; + * + * In case 1, using sizeof(struct dirent) to compute the size for the buffer + * would account for the path name. In case 2), it would not. + * + * The following test ensures that this function always allocate enough + * memory to hold the longest path names that can exist for a given + * directory, regardless of how system headers implement struct dirent. + */ + name_end = (size_t)offsetof(struct dirent, d_name) + name_max + 1; + if (name_end > sizeof(struct dirent)) + return name_end; + return sizeof(struct dirent); +} + +static ssize_t uv__fs_max_path_len(const char* path) { + ssize_t len; + + assert(path); + + len = pathconf(path, _PC_PATH_MAX); + len = uv__fs_clamp_max_path_len(len); + + return len; +} + static ssize_t uv__fs_futime(uv_fs_t* req) { #if defined(__linux__) /* utimesat() has nanosecond resolution but we stick to microseconds @@ -339,21 +428,121 @@ static ssize_t uv__fs_scandir(uv_fs_t* req) { return n; } +static int uv__fs_opendir(uv_fs_t* req) { + DIR *dir_stream; + uv__dirent_t *dirent; + uv_dir_t *dir; + size_t len; -static ssize_t uv__fs_readlink(uv_fs_t* req) { - ssize_t len; - char* buf; + assert(req); - len = pathconf(req->path, _PC_PATH_MAX); + dir_stream = opendir(req->path); + if (dir_stream == NULL) + return -1; - if (len == -1) { -#if defined(PATH_MAX) - len = PATH_MAX; -#else - len = 4096; -#endif + len = uv__fs_direntry_size(dir_stream); + if (len == (size_t)-1) + return -1; + + /* + * Allocate the space for each directory entry just once instead of + * once per directory entry. + */ + dirent = malloc(len + 1); + if (dirent == NULL) { + errno = ENOMEM; + goto failed; } + dir = malloc(sizeof(*dir)); + if (dir == NULL) { + errno = ENOMEM; + goto failed; + } + + dir->dir = dir_stream; + dir->dirent = dirent; + + req->dir = dir; + req->ptr = (void*)req->dir; + + return 0; + +failed: + if (errno == ENOMEM) { + if (dirent != NULL) + free(dirent); + if (dir != NULL) + free(dir); + } + + return -1; +} + +static int uv__fs_readdir(uv_fs_t* req) { + uv_dirent_t *dirents; + struct dirent *res; + int r; + unsigned int dirent_idx; + + assert(req); + + assert(req->dir); + assert(req->dir->dir); + assert(req->dir->dirent); + + assert(req->dirents); + assert(req->nentries > 0); + + dirents = req->dirents; + req->ptr = req->dirents; + + for (dirent_idx = 0; dirent_idx < req->nentries; ++dirent_idx) { + r = readdir_r(req->dir->dir, req->dir->dirent, &res); + + if (r == 0) { + if (res != NULL) { + dirents[dirent_idx].name = strdup(req->dir->dirent->d_name); + if (dirents[dirent_idx].name != NULL) { + dirents[dirent_idx].type = uv__fs_get_dirent_type(req->dir->dirent); + } else { + errno = ENOMEM; + return -1; + } + } else { + return dirent_idx; + } + } + } + + return dirent_idx; +} + +static int uv__fs_closedir(uv_fs_t* req) { + assert(req); + assert(req->dir); + + if (req->dir) { + if (req->dir->dir) { + closedir(req->dir->dir); + ((uv_dir_t*)req->dir)->dir = NULL; + } + + if (req->dir->dirent) { + free(req->dir->dirent); + ((uv_dir_t*)req->dir)->dirent = NULL; + } + } + + req->ptr = NULL; + return 0; +} + +static ssize_t uv__fs_readlink(uv_fs_t* req) { + ssize_t len; + char* buf; + + len = uv__fs_max_path_len(req->path); buf = malloc(len + 1); if (buf == NULL) { @@ -780,6 +969,9 @@ static void uv__fs_work(struct uv__work* w) { X(MKDTEMP, uv__fs_mkdtemp(req)); X(READ, uv__fs_read(req)); X(SCANDIR, uv__fs_scandir(req)); + X(OPENDIR, uv__fs_opendir(req)); + X(READDIR, uv__fs_readdir(req)); + X(CLOSEDIR, uv__fs_closedir(req)); X(READLINK, uv__fs_readlink(req)); X(RENAME, rename(req->path, req->new_path)); X(RMDIR, rmdir(req->path)); @@ -1063,6 +1255,40 @@ int uv_fs_scandir(uv_loop_t* loop, POST; } +int uv_fs_opendir(uv_loop_t* loop, + uv_fs_t* req, + const char* path, + uv_fs_cb cb) { + + INIT(OPENDIR); + PATH; + POST; +} + +int uv_fs_readdir(uv_loop_t* loop, + uv_fs_t* req, + const uv_dir_t* dir, + uv_dirent_t dirents[], + size_t nentries, + uv_fs_cb cb) { + INIT(READDIR); + + req->dir = dir; + req->dirents = dirents; + req->nentries = nentries; + + POST; +} + +int uv_fs_closedir(uv_loop_t* loop, + uv_fs_t* req, + const uv_dir_t* dir, + uv_fs_cb cb) { + INIT(CLOSEDIR); + req->dir = dir; + + POST; +} int uv_fs_readlink(uv_loop_t* loop, uv_fs_t* req, @@ -1182,7 +1408,17 @@ void uv_fs_req_cleanup(uv_fs_t* req) { if (req->fs_type == UV_FS_SCANDIR && req->ptr != NULL) uv__fs_scandir_cleanup(req); - if (req->ptr != &req->statbuf) + if (req->fs_type == UV_FS_READDIR) + uv__fs_readdir_cleanup(req); + + /* + * Do not free the results of OPENDIR or READDIR requests, + * as they point to data that needs to be reused for subsequent + * calls. + */ + if (req->fs_type != UV_FS_READDIR && + req->fs_type != UV_FS_OPENDIR && + req->ptr != &req->statbuf) free(req->ptr); req->ptr = NULL; } diff --git a/src/uv-common.c b/src/uv-common.c index 69fc53ae2c..f232657a3c 100644 --- a/src/uv-common.c +++ b/src/uv-common.c @@ -468,35 +468,56 @@ int uv_fs_scandir_next(uv_fs_t* req, uv_dirent_t* ent) { dent = dents[req->nbufs++]; ent->name = dent->d_name; + ent->type = uv__fs_get_dirent_type(dent); + + return 0; +} + +uv_dirent_type_t uv__fs_get_dirent_type(uv__dirent_t* dent) { + uv_dirent_type_t type; + #ifdef HAVE_DIRENT_TYPES switch (dent->d_type) { case UV__DT_DIR: - ent->type = UV_DIRENT_DIR; + type = UV_DIRENT_DIR; break; case UV__DT_FILE: - ent->type = UV_DIRENT_FILE; + type = UV_DIRENT_FILE; break; case UV__DT_LINK: - ent->type = UV_DIRENT_LINK; + type = UV_DIRENT_LINK; break; case UV__DT_FIFO: - ent->type = UV_DIRENT_FIFO; + type = UV_DIRENT_FIFO; break; case UV__DT_SOCKET: - ent->type = UV_DIRENT_SOCKET; + type = UV_DIRENT_SOCKET; break; case UV__DT_CHAR: - ent->type = UV_DIRENT_CHAR; + type = UV_DIRENT_CHAR; break; case UV__DT_BLOCK: - ent->type = UV_DIRENT_BLOCK; + type = UV_DIRENT_BLOCK; break; default: - ent->type = UV_DIRENT_UNKNOWN; + type = UV_DIRENT_UNKNOWN; } #else - ent->type = UV_DIRENT_UNKNOWN; + type = UV_DIRENT_UNKNOWN; #endif - return 0; + return type; +} + +void uv__fs_readdir_cleanup(uv_fs_t* req) { + unsigned int dirent_idx; + + if (req && req->ptr) { + uv_dirent_t* dirents = req->ptr; + for (dirent_idx = 0; dirent_idx < req->nentries; ++dirent_idx) { + if (dirents[dirent_idx].name) + free((char*)dirents[dirent_idx].name); + dirents[dirent_idx].name = NULL; + } + } } diff --git a/src/uv-common.h b/src/uv-common.h index e06606c19b..18f6da44d0 100644 --- a/src/uv-common.h +++ b/src/uv-common.h @@ -110,6 +110,8 @@ size_t uv__count_bufs(const uv_buf_t bufs[], unsigned int nbufs); int uv__socket_sockopt(uv_handle_t* handle, int optname, int* value); void uv__fs_scandir_cleanup(uv_fs_t* req); +void uv__fs_readdir_cleanup(uv_fs_t* req); +uv_dirent_type_t uv__fs_get_dirent_type(uv__dirent_t* dent); #define uv__has_active_reqs(loop) \ (QUEUE_EMPTY(&(loop)->active_reqs) == 0) diff --git a/src/win/fs.c b/src/win/fs.c index 13af7c41a3..5a1d655c59 100644 --- a/src/win/fs.c +++ b/src/win/fs.c @@ -907,6 +907,169 @@ void fs__scandir(uv_fs_t* req) { free(dents); } +void fs__opendir(uv_fs_t* req) { + WCHAR* pathw = req->pathw; + size_t len = wcslen(pathw); + HANDLE dir_handle; + WIN32_FIND_DATAW ent = { 0 }; + WCHAR* path2; + const WCHAR* fmt; + uv_dir_t* dir = NULL; + + /* Figure out whether path is a file or a directory. */ + if (!(GetFileAttributesW(pathw) & FILE_ATTRIBUTE_DIRECTORY)) { + req->result = UV_ENOTDIR; + req->sys_errno_ = ERROR_SUCCESS; + return; + } + + dir = malloc(sizeof(*dir)); + if (dir == NULL) { + uv_fatal_error(ERROR_OUTOFMEMORY, "malloc"); + } + memset(dir, 0, sizeof(*dir)); + + if (len == 0) { + fmt = L"./*"; + } else if (pathw[len - 1] == L'/' || pathw[len - 1] == L'\\') { + fmt = L"%s*"; + } else { + fmt = L"%s\\*"; + } + + path2 = (WCHAR*)malloc(sizeof(WCHAR) * (len + 4)); + if (!path2) { + SET_REQ_UV_ERROR(req, UV_ENOMEM, ERROR_OUTOFMEMORY); + return; + } + + _snwprintf(path2, len + 3, fmt, pathw); + dir_handle = FindFirstFileW(path2, &dir->find_data); + free(path2); + + if(dir_handle == INVALID_HANDLE_VALUE) { + SET_REQ_WIN32_ERROR(req, GetLastError()); + return; + } + + dir->dir_handle = dir_handle; + dir->need_find_call = FALSE; + + req->dir = dir; + req->ptr = (void*)req->dir; + + req->result = 0; +} + +void fs__readdir(uv_fs_t* req) { + uv__dirent_t* dent = NULL; + uv_dirent_t* dirents = NULL; + const uv_dir_t* dir = NULL; + size_t nb_entries_to_read = 0; + unsigned int dirent_idx = 0; + HANDLE dir_handle; + int utf8_len; + size_t len; + WCHAR* name = NULL; + PWIN32_FIND_DATAW find_data = NULL; + + assert(req); + if (req) + dir = req->dir; + + assert(req->nentries > 0); + nb_entries_to_read = req->nentries; + + assert(req->dirents); + dirents = req->dirents; + memset(dirents, 0, nb_entries_to_read * sizeof(uv_dirent_t)); + + assert(dir); + if (dir) { + dir_handle = dir->dir_handle; + find_data = &((uv_dir_t*)dir)->find_data; + } + + while (dirent_idx < nb_entries_to_read) { + if (dir->need_find_call) { + if (!FindNextFileW(dir_handle, find_data)) { + break; + } + } + + name = find_data->cFileName; + + /* Allocate enough space to fit utf8 encoding of file name */ + len = wcslen(name); + utf8_len = uv_utf16_to_utf8(name, len, NULL, 0); + if (!utf8_len) { + SET_REQ_WIN32_ERROR(req, GetLastError()); + goto fatal; + } + + dent = malloc(sizeof(*dent) + utf8_len + 1); + if (dent == NULL) { + SET_REQ_UV_ERROR(req, UV_ENOMEM, ERROR_OUTOFMEMORY); + goto fatal; + } + + /* Copy file name */ + utf8_len = uv_utf16_to_utf8(name, len, dent->d_name, utf8_len); + if (!utf8_len) { + free(dent); + SET_REQ_WIN32_ERROR(req, GetLastError()); + goto fatal; + } + dent->d_name[utf8_len] = '\0'; + + /* Copy file type */ + if ((find_data->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + dent->d_type = UV__DT_DIR; + else if ((find_data->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) + dent->d_type = UV__DT_LINK; + else + dent->d_type = UV__DT_FILE; + + dirents[dirent_idx].name = strdup(dent->d_name); + if (!dirents[dirent_idx].name) { + SET_REQ_UV_ERROR(req, UV_ENOMEM, ERROR_OUTOFMEMORY); + goto fatal; + } + + dirents[dirent_idx].type = uv__fs_get_dirent_type(dent); + + ((uv_dir_t*)dir)->need_find_call = TRUE; + if (dent) { + free(dent); + dent = NULL; + } + + ++dirent_idx; + } + + req->result = dirent_idx; + req->ptr = req->dirents; + +fatal: + if (dent) { + free(dent); + dent = NULL; + } + dent = NULL; +} + +void fs__closedir(uv_fs_t* req) { + assert(req); + assert(req->dir); + + if (req && req->dir) { + if (FindClose(req->dir->dir_handle)) { + req->result = 0; + } else { + SET_REQ_WIN32_ERROR(req, GetLastError()); + } + } +} INLINE static int fs__stat_handle(HANDLE handle, uv_stat_t* statbuf) { FILE_ALL_INFORMATION file_info; @@ -1629,6 +1792,9 @@ static void uv__fs_work(struct uv__work* w) { XX(MKDTEMP, mkdtemp) XX(RENAME, rename) XX(SCANDIR, scandir) + XX(READDIR, readdir) + XX(OPENDIR, opendir) + XX(CLOSEDIR, closedir) XX(LINK, link) XX(SYMLINK, symlink) XX(READLINK, readlink) @@ -1666,6 +1832,9 @@ void uv_fs_req_cleanup(uv_fs_t* req) { if (req->flags & UV_FS_FREE_PTR) free(req->ptr); + if (req->fs_type == UV_FS_READDIR) + uv__fs_readdir_cleanup(req); + req->path = NULL; req->pathw = NULL; req->new_pathw = NULL; @@ -1885,6 +2054,66 @@ int uv_fs_scandir(uv_loop_t* loop, uv_fs_t* req, const char* path, int flags, } } +int uv_fs_opendir(uv_loop_t* loop, + uv_fs_t* req, + const char* path, + uv_fs_cb cb) { + int err; + + uv_fs_req_init(loop, req, UV_FS_OPENDIR, cb); + + err = fs__capture_path(loop, req, path, NULL, cb != NULL); + if (err) { + return uv_translate_sys_error(err); + } + + if (cb) { + QUEUE_FS_TP_JOB(loop, req); + return 0; + } else { + fs__opendir(req); + return req->result; + } +} + +UV_EXTERN int uv_fs_readdir(uv_loop_t* loop, + uv_fs_t* req, + const uv_dir_t* dir, + uv_dirent_t dirents[], + size_t nentries, + uv_fs_cb cb) { + uv_fs_req_init(loop, req, UV_FS_READDIR, cb); + + req->dir = dir; + req->dirents = dirents; + req->nentries = nentries; + + if (cb) { + QUEUE_FS_TP_JOB(loop, req); + return 0; + } else { + fs__readdir(req); + return req->result; + } +} + +int uv_fs_closedir(uv_loop_t* loop, + uv_fs_t* req, + const uv_dir_t* dir, + uv_fs_cb cb) { + + uv_fs_req_init(loop, req, UV_FS_CLOSEDIR, cb); + + req->dir = dir; + + if (cb) { + QUEUE_FS_TP_JOB(loop, req); + return 0; + } else { + fs__closedir(req); + return req->result; + } +} int uv_fs_link(uv_loop_t* loop, uv_fs_t* req, const char* path, const char* new_path, uv_fs_cb cb) { diff --git a/test/test-fs-readdir.c b/test/test-fs-readdir.c new file mode 100644 index 0000000000..a24f95d260 --- /dev/null +++ b/test/test-fs-readdir.c @@ -0,0 +1,514 @@ +/* Copyright Joyent, Inc. and other Node contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "uv.h" +#include "task.h" + +/* FIXME we shouldn't need to branch in this file */ +#if defined(__unix__) || defined(__POSIX__) || \ + defined(__APPLE__) || defined(_AIX) +#include /* unlink, rmdir, memset */ +#else +# include +# include +# define unlink _unlink +# define rmdir _rmdir +#endif + +#if defined(__sun) || defined(__linux__) +#include /* memset */ +#endif + +#include +#include + +uv_loop_t* loop; + +uv_fs_t opendir_req; +uv_fs_t readdir_req; +uv_fs_t closedir_req; + +uv_dirent_t dirents[1]; + +static int empty_opendir_cb_count; +static int empty_readdir_cb_count; +static int empty_closedir_cb_count; + +static void empty_closedir_cb(uv_fs_t* req) { + ASSERT(req == &closedir_req); + ++empty_closedir_cb_count; +} + +static void empty_readdir_cb(uv_fs_t* req) { + ASSERT(req == &readdir_req); + ASSERT(req->fs_type == UV_FS_READDIR); + + // TODO jgilli: by default, uv_fs_readdir doesn't return 0 when reading + // an emptry dir. Instead, it returns "." and ".." entries in sequence. + // Should this be changed to mimic uv_fs_scandir's behavior? + if (req->result != 0) { + ASSERT(req->result == 1); + ASSERT(req->ptr == &dirents); + +#ifdef HAVE_DIRENT_TYPES + // In an empty directory, all entries are directories ("." and "..") + ASSERT(dirents[0].type == UV_DIRENT_DIR); +#else + ASSERT(dirents[0].type == UV_DIRENT_UNKNOWN); +#endif /* HAVE_DIRENT_TYPES */ + + ++empty_readdir_cb_count; + + uv_fs_readdir(uv_default_loop(), + req, + req->dir, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + empty_readdir_cb); + } else { + ASSERT(empty_readdir_cb_count == 2); + uv_fs_closedir(loop, &closedir_req, req->dir, empty_closedir_cb); + + uv_fs_req_cleanup(req); + } +} + +static void empty_opendir_cb(uv_fs_t* req) { + ASSERT(req == &opendir_req); + ASSERT(req->fs_type == UV_FS_OPENDIR); + ASSERT(req->result == 0); + ASSERT(req->dir != NULL); + ASSERT(0 == uv_fs_readdir(uv_default_loop(), + &readdir_req, + req->dir, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + empty_readdir_cb)); + uv_fs_req_cleanup(req); + ++empty_opendir_cb_count; +} + +/* + * This test makes sure that both synchronous and asynchronous flavors + * of the uv_fs_opendir -> uv_fs_readdir -> uv_fs_closedir sequence work + * as expected when processing an empty directory. + */ +TEST_IMPL(fs_readdir_empty_dir) { + const char* path; + uv_fs_t mkdir_req; + uv_fs_t rmdir_req; + int r; + int nb_entries_read; + int entry_idx; + size_t entries_count; + const uv_dir_t* dir; + + path = "./empty_dir/"; + loop = uv_default_loop(); + + uv_fs_mkdir(loop, &mkdir_req, path, 0777, NULL); + uv_fs_req_cleanup(&mkdir_req); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + /* Testing the synchronous flavor */ + r = uv_fs_opendir(loop, + &opendir_req, + path, + NULL); + ASSERT(r == 0); + ASSERT(opendir_req.fs_type == UV_FS_OPENDIR); + ASSERT(opendir_req.result == 0); + ASSERT(opendir_req.ptr != NULL); + + dir = opendir_req.ptr; + uv_fs_req_cleanup(&opendir_req); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&readdir_req, 0xdb, sizeof(readdir_req)); + entries_count = 0; + nb_entries_read = uv_fs_readdir(loop, + &readdir_req, + dir, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + NULL); + while (0 != nb_entries_read) { + entry_idx = 0; + while (entry_idx < nb_entries_read) { +#ifdef HAVE_DIRENT_TYPES + // In an empty directory, all entries are directories ("." and "..") + ASSERT(dirents[entry_idx].type == UV_DIRENT_DIR); +#else + ASSERT(dirents[entry_idx].type == UV_DIRENT_UNKNOWN); +#endif /* HAVE_DIRENT_TYPES */ + ++entry_idx; + ++entries_count; + } + + nb_entries_read = uv_fs_readdir(loop, + &readdir_req, + dir, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + NULL); + } + + /* + * TODO jgilli: by default, uv_fs_readdir doesn't return UV_EOF when reading + * an emptry dir. Instead, it returns "." and ".." entries in sequence. + * Should this be changed to mimic uv_fs_scandir's behavior? + */ + ASSERT(entries_count == 2); + + uv_fs_req_cleanup(&readdir_req); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&closedir_req, 0xdb, sizeof(closedir_req)); + uv_fs_closedir(loop, &closedir_req, dir, NULL); + ASSERT(closedir_req.result == 0); + + /* Testing the asynchronous flavor */ + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + memset(&readdir_req, 0xdb, sizeof(readdir_req)); + memset(&closedir_req, 0xdb, sizeof(closedir_req)); + + r = uv_fs_opendir(loop, + &opendir_req, + path, + empty_opendir_cb); + ASSERT(r == 0); + + ASSERT(empty_opendir_cb_count == 0); + ASSERT(empty_closedir_cb_count == 0); + + uv_run(loop, UV_RUN_DEFAULT); + + ASSERT(empty_opendir_cb_count == 1); + ASSERT(empty_closedir_cb_count == 1); + + uv_fs_rmdir(loop, &rmdir_req, path, NULL); + uv_fs_req_cleanup(&rmdir_req); + + MAKE_VALGRIND_HAPPY(); + return 0; +} + +/* + * This test makes sure that reading a non-existing directory with + * uv_fs_{open,read}_dir returns proper error codes. + */ + +static int non_existing_opendir_cb_count; + +static void non_existing_opendir_cb(uv_fs_t* req) { + ASSERT(req == &opendir_req); + ASSERT(req->fs_type == UV_FS_OPENDIR); + ASSERT(req->result == UV_ENOENT); + ASSERT(req->ptr == NULL); + + uv_fs_req_cleanup(req); + ++non_existing_opendir_cb_count; +} + +TEST_IMPL(fs_readdir_non_existing_dir) { + const char* path; + int r; + + path = "./non-existing-dir/"; + loop = uv_default_loop(); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + /* Testing the synchronous flavor */ + r = uv_fs_opendir(loop, &opendir_req, path, NULL); + + ASSERT(r == UV_ENOENT); + ASSERT(opendir_req.fs_type == UV_FS_OPENDIR); + ASSERT(opendir_req.result == UV_ENOENT); + ASSERT(opendir_req.ptr == NULL); + + uv_fs_req_cleanup(&opendir_req); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + /* Testing the async flavor */ + r = uv_fs_opendir(loop, &opendir_req, path, non_existing_opendir_cb); + ASSERT(r == 0); + + ASSERT(non_existing_opendir_cb_count == 0); + + uv_run(loop, UV_RUN_DEFAULT); + + ASSERT(non_existing_opendir_cb_count == 1); + + uv_fs_req_cleanup(&opendir_req); + + MAKE_VALGRIND_HAPPY(); + return 0; +} + +/* + * This test makes sure that reading a file as a directory reports correct + * error codes. + */ + +static int file_opendir_cb_count; + +static void file_opendir_cb(uv_fs_t* req) { + ASSERT(req == &opendir_req); + ASSERT(req->fs_type == UV_FS_OPENDIR); + ASSERT(req->result == UV_ENOTDIR); + ASSERT(req->ptr == NULL); + + uv_fs_req_cleanup(req); + ++file_opendir_cb_count; +} + +TEST_IMPL(fs_readdir_file) { + const char* path; + int r; + + path = "test/fixtures/empty_file"; + loop = uv_default_loop(); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + /* Testing the synchronous flavor */ + r = uv_fs_opendir(loop, &opendir_req, path, NULL); + + ASSERT(r == UV_ENOTDIR); + ASSERT(opendir_req.fs_type == UV_FS_OPENDIR); + ASSERT(opendir_req.result == UV_ENOTDIR); + ASSERT(opendir_req.ptr == NULL); + + uv_fs_req_cleanup(&opendir_req); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + /* Testing the async flavor */ + r = uv_fs_opendir(loop, &opendir_req, path, file_opendir_cb); + ASSERT(r == 0); + + ASSERT(file_opendir_cb_count == 0); + + uv_run(loop, UV_RUN_DEFAULT); + + ASSERT(file_opendir_cb_count == 1); + + uv_fs_req_cleanup(&opendir_req); + + MAKE_VALGRIND_HAPPY(); + return 0; +} + +/* + * This test makes sure that reading a non-empty directory with + * uv_fs_{open,read}_dir returns proper directory entries, including the + * correct entry types. + */ + +static int non_empty_opendir_cb_count; +static int non_empty_readdir_cb_count; +static int non_empty_closedir_cb_count; + +static void non_empty_closedir_cb(uv_fs_t* req) { + ASSERT(req == &closedir_req); + ASSERT(req->result == 0); + + ++non_empty_closedir_cb_count; +} + +static void non_empty_readdir_cb(uv_fs_t* req) { + ASSERT(req == &readdir_req); + ASSERT(req->fs_type == UV_FS_READDIR); + + // TODO jgilli: by default, uv_fs_readdir doesn't return UV_EOF when reading + // an emptry dir. Instead, it returns "." and ".." entries in sequence. + // Should this be fixed to mimic uv_fs_scandir's behavior? + if (req->result == 0) { + ASSERT(non_empty_readdir_cb_count == 5); + uv_fs_closedir(loop, &closedir_req, req->dir, non_empty_closedir_cb); + } else { + ASSERT(req->result == 1); + ASSERT(req->ptr == dirents); + +#ifdef HAVE_DIRENT_TYPES + if (!strcmp(dirents[0].name, "test_subdir") || + !strcmp(dirents[0].name, ".") || + !strcmp(dirents[0].name, "..")) { + ASSERT(dirents[0].type == UV_DIRENT_DIR); + } else { + ASSERT(dirents[0].type == UV_DIRENT_FILE); + } +#else + ASSERT(dirents[0].type == UV_DIRENT_UNKNOWN); +#endif /* HAVE_DIRENT_TYPES */ + + ++non_empty_readdir_cb_count; + + uv_fs_readdir(uv_default_loop(), + &readdir_req, + req->dir, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + non_empty_readdir_cb); + } + + uv_fs_req_cleanup(req); +} + +static void non_empty_opendir_cb(uv_fs_t* req) { + ASSERT(req == &opendir_req); + ASSERT(req->fs_type == UV_FS_OPENDIR); + ASSERT(req->result == 0); + ASSERT(req->ptr == req->dir); + ASSERT(req->dir != NULL); + ASSERT(0 == uv_fs_readdir(uv_default_loop(), + &readdir_req, + req->ptr, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + non_empty_readdir_cb)); + uv_fs_req_cleanup(req); + ++non_empty_opendir_cb_count; +} + +TEST_IMPL(fs_readdir_non_empty_dir) { + int r; + size_t entries_count; + + uv_fs_t mkdir_req; + uv_fs_t rmdir_req; + uv_fs_t create_req; + uv_fs_t close_req; + + const uv_dir_t* dir; + + loop = uv_default_loop(); + + /* Setup */ + unlink("test_dir/file1"); + unlink("test_dir/file2"); + rmdir("test_dir/test_subdir"); + rmdir("test_dir"); + + r = uv_fs_mkdir(loop, &mkdir_req, "test_dir", 0755, NULL); + ASSERT(r == 0); + + /* Create 2 files synchronously. */ + r = uv_fs_open(loop, &create_req, "test_dir/file1", O_WRONLY | O_CREAT, + S_IWUSR | S_IRUSR, NULL); + ASSERT(r >= 0); + uv_fs_req_cleanup(&create_req); + r = uv_fs_close(loop, &close_req, create_req.result, NULL); + ASSERT(r == 0); + uv_fs_req_cleanup(&close_req); + + r = uv_fs_open(loop, &create_req, "test_dir/file2", O_WRONLY | O_CREAT, + S_IWUSR | S_IRUSR, NULL); + ASSERT(r >= 0); + uv_fs_req_cleanup(&create_req); + r = uv_fs_close(loop, &close_req, create_req.result, NULL); + ASSERT(r == 0); + uv_fs_req_cleanup(&close_req); + + r = uv_fs_mkdir(loop, &mkdir_req, "test_dir/test_subdir", 0755, NULL); + ASSERT(r == 0); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + /* Testing the synchronous flavor */ + r = uv_fs_opendir(loop, &opendir_req, "test_dir", NULL); + + ASSERT(r == 0); + ASSERT(opendir_req.fs_type == UV_FS_OPENDIR); + ASSERT(opendir_req.result == 0); + ASSERT(opendir_req.ptr != NULL); + // TODO jgilli: by default, uv_fs_readdir doesn't return UV_EOF when reading + // an emptry dir. Instead, it returns "." and ".." entries in sequence. + // Should this be changed to mimic uv_fs_scandir's behavior? + entries_count = 0; + dir = opendir_req.ptr; + while (uv_fs_readdir(loop, + &readdir_req, + dir, + dirents, + sizeof(dirents) / sizeof(dirents[0]), + NULL) != 0) { +#ifdef HAVE_DIRENT_TYPES + if (!strcmp(dirents[0].name, "test_subdir") || + !strcmp(dirents[0].name, ".") || + !strcmp(dirents[0].name, "..")) { + ASSERT(dirents[0].type == UV_DIRENT_DIR); + } else { + ASSERT(dirents[0].type == UV_DIRENT_FILE); + } +#else + ASSERT(dirents[0].type == UV_DIRENT_UNKNOWN); +#endif /* HAVE_DIRENT_TYPES */ + ++entries_count; + } + + ASSERT(entries_count == 5); + uv_fs_req_cleanup(&readdir_req); + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&closedir_req, 0xdb, sizeof(closedir_req)); + uv_fs_closedir(loop, &closedir_req, dir, NULL); + ASSERT(closedir_req.result == 0); + + /* Testing the asynchronous flavor */ + + /* Fill the req to ensure that required fields are cleaned up */ + memset(&opendir_req, 0xdb, sizeof(opendir_req)); + + r = uv_fs_opendir(loop, &opendir_req, "test_dir", non_empty_opendir_cb); + ASSERT(r == 0); + + ASSERT(non_empty_opendir_cb_count == 0); + ASSERT(non_empty_closedir_cb_count == 0); + + uv_run(loop, UV_RUN_DEFAULT); + + ASSERT(non_empty_opendir_cb_count == 1); + ASSERT(non_empty_closedir_cb_count == 1); + + uv_fs_rmdir(loop, &rmdir_req, "test_subdir", NULL); + uv_fs_req_cleanup(&rmdir_req); + + /* Cleanup */ + unlink("test_dir/file1"); + unlink("test_dir/file2"); + rmdir("test_dir/test_subdir"); + rmdir("test_dir"); + + MAKE_VALGRIND_HAPPY(); + return 0; + } diff --git a/test/test-list.h b/test/test-list.h index 85ddac82ae..63e8d7fdd5 100644 --- a/test/test-list.h +++ b/test/test-list.h @@ -245,6 +245,10 @@ TEST_DECLARE (fs_event_getpath) TEST_DECLARE (fs_scandir_empty_dir) TEST_DECLARE (fs_scandir_file) TEST_DECLARE (fs_open_dir) +TEST_DECLARE (fs_readdir_empty_dir) +TEST_DECLARE (fs_readdir_non_existing_dir) +TEST_DECLARE (fs_readdir_file) +TEST_DECLARE (fs_readdir_non_empty_dir) TEST_DECLARE (fs_rename_to_existing_file) TEST_DECLARE (fs_write_multiple_bufs) TEST_DECLARE (threadpool_queue_work_simple) @@ -619,6 +623,10 @@ TASK_LIST_START TEST_ENTRY (fs_scandir_empty_dir) TEST_ENTRY (fs_scandir_file) TEST_ENTRY (fs_open_dir) + TEST_ENTRY (fs_readdir_empty_dir) + TEST_ENTRY (fs_readdir_non_existing_dir) + TEST_ENTRY (fs_readdir_file) + TEST_ENTRY (fs_readdir_non_empty_dir) TEST_ENTRY (fs_rename_to_existing_file) TEST_ENTRY (fs_write_multiple_bufs) TEST_ENTRY (threadpool_queue_work_simple) diff --git a/uv.gyp b/uv.gyp index 27885d23f9..6b4ab59461 100644 --- a/uv.gyp +++ b/uv.gyp @@ -342,6 +342,7 @@ 'test/test-fail-always.c', 'test/test-fs.c', 'test/test-fs-event.c', + 'test/test-fs-readdir.c', 'test/test-get-currentexe.c', 'test/test-get-memory.c', 'test/test-getaddrinfo.c',