From f4cfa601046046cbaebbc76c8b8741ea5e0ee02d Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 8 Mar 2016 20:58:45 -0800 Subject: [PATCH] fs: Buffer and encoding enhancements to fs API This makes several changes: 1. Allow path/filename to be passed in as a Buffer on fs methods 2. Add `options.encoding` to fs.readdir, fs.readdirSync, fs.readlink, fs.readlinkSync and fs.watch. 3. Documentation updates For 1... it's now possible to do: ```js fs.open(Buffer('/fs/foo/bar'), 'w+', (err, fd) => { }); ``` For 2... ```js fs.readdir('/fs/foo/bar', {encoding:'hex'}, (err,list) => { }); fs.readdir('/fs/foo/bar', {encoding:'buffer'}, (err, list) => { }); ``` encoding can also be passed as a string ```js fs.readdir('/fs/foo/bar', 'hex', (err,list) => { }); ``` The default encoding is set to UTF8 so this addresses the discrepency that existed previously between fs.readdir and fs.watch handling filenames differently. Fixes: https://github.com/nodejs/node/issues/2088 Refs: https://github.com/nodejs/node/issues/3519 Alternate: https://github.com/nodejs/node/pull/3401 --- doc/api/fs.markdown | 189 ++++++++++----- lib/fs.js | 106 ++++++--- src/fs_event_wrap.cc | 27 ++- src/node_file.cc | 295 +++++++++++++++--------- src/string_bytes.cc | 31 ++- src/string_bytes.h | 4 + src/util.cc | 69 ++++-- src/util.h | 19 ++ test/common.js | 10 +- test/parallel/test-fs-access.js | 2 +- test/parallel/test-fs-buffer.js | 41 ++++ test/parallel/test-fs-link.js | 4 +- test/parallel/test-fs-readdir-ucs2.js | 31 +++ test/parallel/test-fs-watch-encoding.js | 52 +++++ test/sequential/test-module-loading.js | 2 +- 15 files changed, 654 insertions(+), 228 deletions(-) create mode 100644 test/parallel/test-fs-buffer.js create mode 100644 test/parallel/test-fs-readdir-ucs2.js create mode 100644 test/parallel/test-fs-watch-encoding.js diff --git a/doc/api/fs.markdown b/doc/api/fs.markdown index 11aeb9d5b9f71b..e44db918aa1ccd 100644 --- a/doc/api/fs.markdown +++ b/doc/api/fs.markdown @@ -101,11 +101,24 @@ Objects returned from `fs.watch()` are of this type. ### Event: 'change' * `event` {String} The type of fs change -* `filename` {String} The filename that changed (if relevant/available) +* `filename` {String | Buffer} The filename that changed (if relevant/available) Emitted when something changes in a watched directory or file. See more details in [`fs.watch()`][]. +The `filename` argument may not be provided depending on operating system +support. If `filename` is provided, it will be provided as a `Buffer` if +`fs.watch()` is called with it's `encoding` option set to `'buffer'`, otherwise +`filename` will be a string. + +```js +fs.watch('./tmp', {encoding: 'buffer'}, (event, filename) => { + if (filename) + console.log(filename); + // Prints: +}); +``` + ### Event: 'error' * `error` {Error} @@ -128,7 +141,10 @@ Emitted when the ReadStream's file is opened. ### readStream.path -The path to the file the stream is reading from. +The path to the file the stream is reading from as specified in the first +argument to `fs.createReadStream()`. If `path` is passed as a string, then +`readStream.path` will be a string. If `path` is passed as a `Buffer`, then +`readStream.path` will be a `Buffer`. ## Class: fs.Stats @@ -217,11 +233,14 @@ for writing. ### writeStream.path -The path to the file the stream is writing to. +The path to the file the stream is writing to as specified in the first +argument to `fs.createWriteStream()`. If `path` is passed as a string, then +`writeStream.path` will be a string. If `path` is passed as a `Buffer`, then +`writeStream.path` will be a `Buffer`. ## fs.access(path[, mode], callback) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} * `callback` {Function} @@ -251,7 +270,7 @@ fs.access('/etc/passwd', fs.R_OK | fs.W_OK, (err) => { ## fs.accessSync(path[, mode]) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} Synchronous version of [`fs.access()`][]. This throws if any accessibility checks @@ -259,7 +278,7 @@ fail, and does nothing otherwise. ## fs.appendFile(file, data[, options], callback) -* `file` {String | Number} filename or file descriptor +* `file` {String | Buffer | Number} filename or file descriptor * `data` {String | Buffer} * `options` {Object | String} * `encoding` {String | Null} default = `'utf8'` @@ -291,11 +310,18 @@ _Note: Specified file descriptors will not be closed automatically._ ## fs.appendFileSync(file, data[, options]) +* `file` {String | Buffer} +* `data` {String | Buffer} +* `options` {Object | String} + * `encoding` {String | Null} default = `'utf8'` + * `mode` {Integer} default = `0o666` + * `flag` {String} default = `'a'` + The synchronous version of [`fs.appendFile()`][]. Returns `undefined`. ## fs.chmod(path, mode, callback) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} * `callback` {Function} @@ -304,14 +330,14 @@ to the completion callback. ## fs.chmodSync(path, mode) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} Synchronous chmod(2). Returns `undefined`. ## fs.chown(path, uid, gid, callback) -* `path` {String} +* `path` {String | Buffer} * `uid` {Integer} * `gid` {Integer} * `callback` {Function} @@ -321,7 +347,7 @@ to the completion callback. ## fs.chownSync(path, uid, gid) -* `path` {String} +* `path` {String | Buffer} * `uid` {Integer} * `gid` {Integer} @@ -343,7 +369,7 @@ Synchronous close(2). Returns `undefined`. ## fs.createReadStream(path[, options]) -* `path` {String} +* `path` {String | Buffer} * `options` {String | Object} * `flags` {String} * `encoding` {String} @@ -399,7 +425,7 @@ If `options` is a string, then it specifies the encoding. ## fs.createWriteStream(path[, options]) -* `path` {String} +* `path` {String | Buffer} * `options` {String | Object} * `flags` {String} * `defaultEncoding` {String} @@ -444,7 +470,7 @@ If `options` is a string, then it specifies the encoding. Stability: 0 - Deprecated: Use [`fs.stat()`][] or [`fs.access()`][] instead. -* `path` {String} +* `path` {String | Buffer} * `callback` {Function} Test whether or not the given path exists by checking with the file system. @@ -466,7 +492,7 @@ non-existent. Stability: 0 - Deprecated: Use [`fs.statSync()`][] or [`fs.accessSync()`][] instead. -* `path` {String} +* `path` {String | Buffer} Synchronous version of [`fs.exists()`][]. Returns `true` if the file exists, `false` otherwise. @@ -584,7 +610,7 @@ Synchronous version of [`fs.futimes()`][]. Returns `undefined`. ## fs.lchmod(path, mode, callback) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} * `callback` {Function} @@ -595,14 +621,14 @@ Only available on Mac OS X. ## fs.lchmodSync(path, mode) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} Synchronous lchmod(2). Returns `undefined`. ## fs.lchown(path, uid, gid, callback) -* `path` {String} +* `path` {String | Buffer} * `uid` {Integer} * `gid` {Integer} * `callback` {Function} @@ -612,7 +638,7 @@ to the completion callback. ## fs.lchownSync(path, uid, gid) -* `path` {String} +* `path` {String | Buffer} * `uid` {Integer} * `gid` {Integer} @@ -620,8 +646,8 @@ Synchronous lchown(2). Returns `undefined`. ## fs.link(srcpath, dstpath, callback) -* `srcpath` {String} -* `dstpath` {String} +* `srcpath` {String | Buffer} +* `dstpath` {String | Buffer} * `callback` {Function} Asynchronous link(2). No arguments other than a possible exception are given to @@ -629,14 +655,14 @@ the completion callback. ## fs.linkSync(srcpath, dstpath) -* `srcpath` {String} -* `dstpath` {String} +* `srcpath` {String | Buffer} +* `dstpath` {String | Buffer} Synchronous link(2). Returns `undefined`. ## fs.lstat(path, callback) -* `path` {String} +* `path` {String | Buffer} * `callback` {Function} Asynchronous lstat(2). The callback gets two arguments `(err, stats)` where @@ -646,13 +672,13 @@ refers to. ## fs.lstatSync(path) -* `path` {String} +* `path` {String | Buffer} Synchronous lstat(2). Returns an instance of `fs.Stats`. ## fs.mkdir(path[, mode], callback) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} * `callback` {Function} @@ -661,7 +687,7 @@ to the completion callback. `mode` defaults to `0o777`. ## fs.mkdirSync(path[, mode]) -* `path` {String} +* `path` {String | Buffer} * `mode` {Integer} Synchronous mkdir(2). Returns `undefined`. @@ -692,7 +718,7 @@ folder path. ## fs.open(path, flags[, mode], callback) -* `path` {String} +* `path` {String | Buffer} * `flags` {String | Number} * `mode` {Integer} * `callback` {Function} @@ -759,7 +785,7 @@ the end of the file. ## fs.openSync(path, flags[, mode]) -* `path` {String} +* `path` {String | Buffer} * `flags` {String | Number} * `mode` {Integer} @@ -788,7 +814,12 @@ If `position` is `null`, data will be read from the current file position. The callback is given the three arguments, `(err, bytesRead, buffer)`. -## fs.readdir(path, callback) +## fs.readdir(path[, options], callback) + +* `path` {String | Buffer} +* `options` {String | Object} + * `encoding` {String} default = `'utf8'` +* `callback` {Function} * `path` {String} * `callback` {Function} @@ -797,16 +828,30 @@ Asynchronous readdir(3). Reads the contents of a directory. The callback gets two arguments `(err, files)` where `files` is an array of the names of the files in the directory excluding `'.'` and `'..'`. -## fs.readdirSync(path) +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the filenames passed to the callback. If the `encoding` is set to `'buffer'`, +the filenames returned will be passed as `Buffer` objects. + +## fs.readdirSync(path[, options]) + +* `path` {String | Buffer} +* `options` {String | Object} + * `encoding` {String} default = `'utf8'` * `path` {String} Synchronous readdir(3). Returns an array of filenames excluding `'.'` and `'..'`. +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the filenames passed to the callback. If the `encoding` is set to `'buffer'`, +the filenames returned will be passed as `Buffer` objects. + ## fs.readFile(file[, options], callback) -* `file` {String | Integer} filename or file descriptor +* `file` {String | Buffer | Integer} filename or file descriptor * `options` {Object | String} * `encoding` {String | Null} default = `null` * `flag` {String} default = `'r'` @@ -838,7 +883,7 @@ _Note: Specified file descriptors will not be closed automatically._ ## fs.readFileSync(file[, options]) -* `file` {String | Integer} filename or file descriptor +* `file` {String | Buffer | Integer} filename or file descriptor * `options` {Object | String} * `encoding` {String | Null} default = `null` * `flag` {String} default = `'r'` @@ -848,7 +893,12 @@ Synchronous version of [`fs.readFile`][]. Returns the contents of the `file`. If the `encoding` option is specified then this function returns a string. Otherwise it returns a buffer. -## fs.readlink(path, callback) +## fs.readlink(path[, options], callback) + +* `path` {String | Buffer} +* `options` {String | Object} + * `encoding` {String} default = `'utf8'` +* `callback` {Function} * `path` {String} * `callback` {Function} @@ -856,15 +906,29 @@ string. Otherwise it returns a buffer. Asynchronous readlink(2). The callback gets two arguments `(err, linkString)`. -## fs.readlinkSync(path) +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the link path passed to the callback. If the `encoding` is set to `'buffer'`, +the link path returned will be passed as a `Buffer` object. + +## fs.readlinkSync(path[, options]) + +* `path` {String | Buffer} +* `options` {String | Object} + * `encoding` {String} default = `'utf8'` * `path` {String} Synchronous readlink(2). Returns the symbolic link's string value. +The optional `options` argument can be a string specifying an encoding, or an +object with an `encoding` property specifying the character encoding to use for +the link path passed to the callback. If the `encoding` is set to `'buffer'`, +the link path returned will be passed as a `Buffer` object. + ## fs.realpath(path[, cache], callback) -* `path` {String} +* `path` {String | Buffer} * `cache` {Object} * `callback` {Function} @@ -895,7 +959,7 @@ Synchronous version of [`fs.read()`][]. Returns the number of `bytesRead`. ## fs.realpathSync(path[, cache]) -* `path` {String} +* `path` {String | Buffer}; * `cache` {Object} Synchronous realpath(2). Returns the resolved path. `cache` is an @@ -904,8 +968,8 @@ resolution or avoid additional `fs.stat` calls for known real paths. ## fs.rename(oldPath, newPath, callback) -* `oldPath` {String} -* `newPath` {String} +* `oldPath` {String | Buffer} +* `newPath` {String | Buffer} * `callback` {Function} Asynchronous rename(2). No arguments other than a possible exception are given @@ -913,14 +977,14 @@ to the completion callback. ## fs.renameSync(oldPath, newPath) -* `oldPath` {String} -* `newPath` {String} +* `oldPath` {String | Buffer} +* `newPath` {String | Buffer} Synchronous rename(2). Returns `undefined`. ## fs.rmdir(path, callback) -* `path` {String} +* `path` {String | Buffer} * `callback` {Function} Asynchronous rmdir(2). No arguments other than a possible exception are given @@ -928,13 +992,13 @@ to the completion callback. ## fs.rmdirSync(path) -* `path` {String} +* `path` {String | Buffer} Synchronous rmdir(2). Returns `undefined`. ## fs.stat(path, callback) -* `path` {String} +* `path` {String | Buffer} * `callback` {Function} Asynchronous stat(2). The callback gets two arguments `(err, stats)` where @@ -943,14 +1007,14 @@ information. ## fs.statSync(path) -* `path` {String} +* `path` {String | Buffer} Synchronous stat(2). Returns an instance of [`fs.Stats`][]. ## fs.symlink(target, path[, type], callback) -* `target` {String} -* `path` {String} +* `target` {String | Buffer} +* `path` {String | Buffer} * `type` {String} * `callback` {Function} @@ -971,15 +1035,15 @@ It creates a symbolic link named "new-port" that points to "foo". ## fs.symlinkSync(target, path[, type]) -* `target` {String} -* `path` {String} +* `target` {String | Buffer} +* `path` {String | Buffer} * `type` {String} Synchronous symlink(2). Returns `undefined`. ## fs.truncate(path, len, callback) -* `path` {String} +* `path` {String | Buffer} * `len` {Integer} * `callback` {Function} @@ -989,14 +1053,14 @@ first argument. In this case, `fs.ftruncate()` is called. ## fs.truncateSync(path, len) -* `path` {String} +* `path` {String | Buffer} * `len` {Integer} Synchronous truncate(2). Returns `undefined`. ## fs.unlink(path, callback) -* `path` {String} +* `path` {String | Buffer} * `callback` {Function} Asynchronous unlink(2). No arguments other than a possible exception are given @@ -1004,13 +1068,13 @@ to the completion callback. ## fs.unlinkSync(path) -* `path` {String} +* `path` {String | Buffer} Synchronous unlink(2). Returns `undefined`. ## fs.unwatchFile(filename[, listener]) -* `filename` {String} +* `filename` {String | Buffer} * `listener` {Function} Stop watching for changes on `filename`. If `listener` is specified, only that @@ -1026,7 +1090,7 @@ when possible._ ## fs.utimes(path, atime, mtime, callback) -* `path` {String} +* `path` {String | Buffer} * `atime` {Integer} * `mtime` {Integer} * `callback` {Function} @@ -1043,7 +1107,7 @@ follow the below rules: ## fs.utimesSync(path, atime, mtime) -* `path` {String} +* `path` {String | Buffer} * `atime` {Integer} * `mtime` {Integer} @@ -1051,19 +1115,24 @@ Synchronous version of [`fs.utimes()`][]. Returns `undefined`. ## fs.watch(filename[, options][, listener]) -* `filename` {String} -* `options` {Object} +* `filename` {String | Buffer} +* `options` {String | Object} * `persistent` {Boolean} Indicates whether the process should continue to run as long as files are being watched. default = `true` * `recursive` {Boolean} Indicates whether all subdirectories should be watched, or only the current directory. The applies when a directory is specified, and only on supported platforms (See [Caveats][]). default = `false` + * `encoding` {String} Specifies the character encoding to be used for the + filename passed to the listener. default = `'utf8'` * `listener` {Function} Watch for changes on `filename`, where `filename` is either a file or a directory. The returned object is a [`fs.FSWatcher`][]. +The second argument is optional. If `options` is provided as a string, it +specifies the `encoding`. Otherwise `options` should be passed as an object. + The listener callback gets two arguments `(event, filename)`. `event` is either `'rename'` or `'change'`, and `filename` is the name of the file which triggered the event. @@ -1120,7 +1189,7 @@ fs.watch('somedir', (event, filename) => { ## fs.watchFile(filename[, options], listener) -* `filename` {String} +* `filename` {String | Buffer} * `options` {Object} * `persistent` {Boolean} * `interval` {Integer} @@ -1224,7 +1293,7 @@ the end of the file. ## fs.writeFile(file, data[, options], callback) -* `file` {String | Integer} filename or file descriptor +* `file` {String | Buffer | Integer} filename or file descriptor * `data` {String | Buffer} * `options` {Object | String} * `encoding` {String | Null} default = `'utf8'` @@ -1263,7 +1332,7 @@ _Note: Specified file descriptors will not be closed automatically._ ## fs.writeFileSync(file, data[, options]) -* `file` {String | Integer} filename or file descriptor +* `file` {String | Buffer | Integer} filename or file descriptor * `data` {String | Buffer} * `options` {Object | String} * `encoding` {String | Null} default = `'utf8'` diff --git a/lib/fs.js b/lib/fs.js index de7af70e4b1401..f4a31a6f4d44c2 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -903,17 +903,32 @@ fs.mkdirSync = function(path, mode) { modeNum(mode, 0o777)); }; -fs.readdir = function(path, callback) { +fs.readdir = function(path, options, callback) { + options = options || {}; + if (typeof options === 'function') { + callback = options; + options = {}; + } else if (typeof options === 'string') { + options = {encoding: options}; + } + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); + callback = makeCallback(callback); if (!nullCheck(path, callback)) return; var req = new FSReqWrap(); req.oncomplete = callback; - binding.readdir(pathModule._makeLong(path), req); + binding.readdir(pathModule._makeLong(path), options.encoding, req); }; -fs.readdirSync = function(path) { +fs.readdirSync = function(path, options) { + options = options || {}; + if (typeof options === 'string') + options = {encoding: options}; + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); nullCheck(path); - return binding.readdir(pathModule._makeLong(path)); + return binding.readdir(pathModule._makeLong(path), options.encoding); }; fs.fstat = function(fd, callback) { @@ -952,17 +967,31 @@ fs.statSync = function(path) { return binding.stat(pathModule._makeLong(path)); }; -fs.readlink = function(path, callback) { +fs.readlink = function(path, options, callback) { + options = options || {}; + if (typeof options === 'function') { + callback = options; + options = {}; + } else if (typeof options === 'string') { + options = {encoding: options}; + } + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); callback = makeCallback(callback); if (!nullCheck(path, callback)) return; var req = new FSReqWrap(); req.oncomplete = callback; - binding.readlink(pathModule._makeLong(path), req); + binding.readlink(pathModule._makeLong(path), options.encoding, req); }; -fs.readlinkSync = function(path) { +fs.readlinkSync = function(path, options) { + options = options || {}; + if (typeof options === 'string') + options = {encoding: options}; + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); nullCheck(path); - return binding.readlink(pathModule._makeLong(path)); + return binding.readlink(pathModule._makeLong(path), options.encoding); }; function preprocessSymlinkDestination(path, type, linkPath) { @@ -1363,11 +1392,15 @@ function FSWatcher() { } util.inherits(FSWatcher, EventEmitter); -FSWatcher.prototype.start = function(filename, persistent, recursive) { +FSWatcher.prototype.start = function(filename, + persistent, + recursive, + encoding) { nullCheck(filename); var err = this._handle.start(pathModule._makeLong(filename), persistent, - recursive); + recursive, + encoding); if (err) { this._handle.close(); const error = errnoException(err, `watch ${filename}`); @@ -1380,25 +1413,27 @@ FSWatcher.prototype.close = function() { this._handle.close(); }; -fs.watch = function(filename) { +fs.watch = function(filename, options, listener) { nullCheck(filename); - var watcher; - var options; - var listener; - if (arguments[1] !== null && typeof arguments[1] === 'object') { - options = arguments[1]; - listener = arguments[2]; - } else { + options = options || {}; + if (typeof options === 'function') { + listener = options; options = {}; - listener = arguments[1]; + } else if (typeof options === 'string') { + options = {encoding: options}; } + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); if (options.persistent === undefined) options.persistent = true; if (options.recursive === undefined) options.recursive = false; - watcher = new FSWatcher(); - watcher.start(filename, options.persistent, options.recursive); + const watcher = new FSWatcher(); + watcher.start(filename, + options.persistent, + options.recursive, + options.encoding); if (listener) { watcher.addListener('change', listener); @@ -2139,10 +2174,19 @@ SyncWriteStream.prototype.destroy = function() { SyncWriteStream.prototype.destroySoon = SyncWriteStream.prototype.destroy; -fs.mkdtemp = function(prefix, callback) { - if (typeof callback !== 'function') { - throw new TypeError('"callback" argument must be a function'); +fs.mkdtemp = function(prefix, options, callback) { + if (!prefix || typeof prefix !== 'string') + throw new TypeError('filename prefix is required'); + + options = options || {}; + if (typeof options === 'function') { + callback = options; + options = {}; + } else if (typeof options === 'string') { + options = {encoding: options}; } + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); if (!nullCheck(prefix, callback)) { return; @@ -2151,11 +2195,19 @@ fs.mkdtemp = function(prefix, callback) { var req = new FSReqWrap(); req.oncomplete = callback; - binding.mkdtemp(prefix + 'XXXXXX', req); + binding.mkdtemp(prefix + 'XXXXXX', options.encoding, req); }; -fs.mkdtempSync = function(prefix) { +fs.mkdtempSync = function(prefix, options) { + if (!prefix || typeof prefix !== 'string') + throw new TypeError('filename prefix is required'); + + options = options || {}; + if (typeof options === 'string') + options = {encoding: options}; + if (typeof options !== 'object') + throw new TypeError('"options" must be a string or an object'); nullCheck(prefix); - return binding.mkdtemp(prefix + 'XXXXXX'); + return binding.mkdtemp(prefix + 'XXXXXX', options.encoding); }; diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index 7768f94459c16a..58f2716a6c0be8 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -6,6 +6,7 @@ #include "util-inl.h" #include "node.h" #include "handle_wrap.h" +#include "string_bytes.h" #include @@ -41,6 +42,7 @@ class FSEventWrap: public HandleWrap { uv_fs_event_t handle_; bool initialized_; + enum encoding encoding_; }; @@ -86,16 +88,20 @@ void FSEventWrap::Start(const FunctionCallbackInfo& args) { FSEventWrap* wrap = Unwrap(args.Holder()); - if (args.Length() < 1 || !args[0]->IsString()) { - return env->ThrowTypeError("filename must be a valid string"); - } + static const char kErrMsg[] = "filename must be a string or Buffer"; + if (args.Length() < 1) + return env->ThrowTypeError(kErrMsg); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + if (*path == nullptr) + return env->ThrowTypeError(kErrMsg); unsigned int flags = 0; if (args[2]->IsTrue()) flags |= UV_FS_EVENT_RECURSIVE; + wrap->encoding_ = ParseEncoding(env->isolate(), args[3], UTF8); + int err = uv_fs_event_init(wrap->env()->event_loop(), &wrap->handle_); if (err == 0) { wrap->initialized_ = true; @@ -156,7 +162,18 @@ void FSEventWrap::OnEvent(uv_fs_event_t* handle, const char* filename, }; if (filename != nullptr) { - argv[2] = OneByteString(env->isolate(), filename); + Local fn = StringBytes::Encode(env->isolate(), + filename, + wrap->encoding_); + if (fn.IsEmpty()) { + argv[0] = Integer::New(env->isolate(), UV_EINVAL); + argv[2] = StringBytes::Encode(env->isolate(), + filename, + strlen(filename), + BUFFER); + } else { + argv[2] = fn; + } } wrap->MakeCallback(env->onchange_string(), ARRAY_SIZE(argv), argv); diff --git a/src/node_file.cc b/src/node_file.cc index 9c2bea48975cd3..a669c0855fd3a5 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -34,6 +34,7 @@ using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; using v8::Integer; +using v8::Isolate; using v8::Local; using v8::Number; using v8::Object; @@ -56,6 +57,7 @@ class FSReqWrap: public ReqWrap { Local req, const char* syscall, const char* data = nullptr, + enum encoding encoding = UTF8, Ownership ownership = COPY); inline void Dispose(); @@ -69,6 +71,7 @@ class FSReqWrap: public ReqWrap { const char* syscall() const { return syscall_; } const char* data() const { return data_; } + const enum encoding encoding_; size_t self_size() const override { return sizeof(*this); } @@ -76,8 +79,10 @@ class FSReqWrap: public ReqWrap { FSReqWrap(Environment* env, Local req, const char* syscall, - const char* data) + const char* data, + enum encoding encoding) : ReqWrap(env, req, AsyncWrap::PROVIDER_FSREQWRAP), + encoding_(encoding), syscall_(syscall), data_(data) { Wrap(object(), this); @@ -95,17 +100,21 @@ class FSReqWrap: public ReqWrap { DISALLOW_COPY_AND_ASSIGN(FSReqWrap); }; +#define ASSERT_PATH(path) \ + if (*path == nullptr) \ + return TYPE_ERROR( #path " must be a string or Buffer"); FSReqWrap* FSReqWrap::New(Environment* env, Local req, const char* syscall, const char* data, + enum encoding encoding, Ownership ownership) { const bool copy = (data != nullptr && ownership == COPY); const size_t size = copy ? 1 + strlen(data) : 0; FSReqWrap* that; char* const storage = new char[sizeof(*that) + size]; - that = new(storage) FSReqWrap(env, req, syscall, data); + that = new(storage) FSReqWrap(env, req, syscall, data, encoding); if (copy) that->data_ = static_cast(memcpy(that->inline_data(), data, size)); return that; @@ -127,7 +136,6 @@ static inline bool IsInt64(double x) { return x == static_cast(static_cast(x)); } - static void After(uv_fs_t *req) { FSReqWrap* req_wrap = static_cast(req->data); CHECK_EQ(&req_wrap->req_, req); @@ -143,6 +151,7 @@ static void After(uv_fs_t *req) { // Allocate space for two args. We may only use one depending on the case. // (Feel free to increase this if you need more) Local argv[2]; + Local link; if (req->result < 0) { // An error happened. @@ -201,13 +210,35 @@ static void After(uv_fs_t *req) { break; case UV_FS_MKDTEMP: - argv[1] = String::NewFromUtf8(env->isolate(), - static_cast(req->path)); + link = StringBytes::Encode(env->isolate(), + static_cast(req->path), + req_wrap->encoding_); + if (link.IsEmpty()) { + argv[0] = UVException(env->isolate(), + UV_EINVAL, + req_wrap->syscall(), + "Invalid character encoding for filename", + req->path, + req_wrap->data()); + } else { + argv[1] = link; + } break; case UV_FS_READLINK: - argv[1] = String::NewFromUtf8(env->isolate(), - static_cast(req->ptr)); + link = StringBytes::Encode(env->isolate(), + static_cast(req->ptr), + req_wrap->encoding_); + if (link.IsEmpty()) { + argv[0] = UVException(env->isolate(), + UV_EINVAL, + req_wrap->syscall(), + "Invalid character encoding for link", + req->path, + req_wrap->data()); + } else { + argv[1] = link; + } break; case UV_FS_READ: @@ -237,8 +268,19 @@ static void After(uv_fs_t *req) { break; } - name_argv[name_idx++] = - String::NewFromUtf8(env->isolate(), ent.name); + Local filename = StringBytes::Encode(env->isolate(), + ent.name, + req_wrap->encoding_); + if (filename.IsEmpty()) { + argv[0] = UVException(env->isolate(), + UV_EINVAL, + req_wrap->syscall(), + "Invalid character encoding for filename", + req->path, + req_wrap->data()); + break; + } + name_argv[name_idx++] = filename; if (name_idx >= ARRAY_SIZE(name_argv)) { fn->Call(env->context(), names, name_idx, name_argv) @@ -277,10 +319,11 @@ struct fs_req_wrap { }; -#define ASYNC_DEST_CALL(func, req, dest, ...) \ +#define ASYNC_DEST_CALL(func, req, dest, encoding, ...) \ Environment* env = Environment::GetCurrent(args); \ CHECK(req->IsObject()); \ - FSReqWrap* req_wrap = FSReqWrap::New(env, req.As(), #func, dest); \ + FSReqWrap* req_wrap = FSReqWrap::New(env, req.As(), \ + #func, dest, encoding); \ int err = uv_fs_ ## func(env->event_loop(), \ &req_wrap->req_, \ __VA_ARGS__, \ @@ -296,8 +339,8 @@ struct fs_req_wrap { args.GetReturnValue().Set(req_wrap->persistent()); \ } -#define ASYNC_CALL(func, req, ...) \ - ASYNC_DEST_CALL(func, req, nullptr, __VA_ARGS__) \ +#define ASYNC_CALL(func, req, encoding, ...) \ + ASYNC_DEST_CALL(func, req, nullptr, encoding, __VA_ARGS__) \ #define SYNC_DEST_CALL(func, path, dest, ...) \ fs_req_wrap req_wrap; \ @@ -317,23 +360,22 @@ struct fs_req_wrap { #define SYNC_RESULT err - static void Access(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args.GetIsolate()); HandleScope scope(env->isolate()); if (args.Length() < 2) return TYPE_ERROR("path and mode are required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); if (!args[1]->IsInt32()) return TYPE_ERROR("mode must be an integer"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + int mode = static_cast(args[1]->Int32Value()); if (args[2]->IsObject()) { - ASYNC_CALL(access, args[2], *path, mode); + ASYNC_CALL(access, args[2], UTF8, *path, mode); } else { SYNC_CALL(access, *path, *path, mode); } @@ -351,7 +393,7 @@ static void Close(const FunctionCallbackInfo& args) { int fd = args[0]->Int32Value(); if (args[1]->IsObject()) { - ASYNC_CALL(close, args[1], fd) + ASYNC_CALL(close, args[1], UTF8, fd) } else { SYNC_CALL(close, 0, fd) } @@ -544,13 +586,12 @@ static void Stat(const FunctionCallbackInfo& args) { if (args.Length() < 1) return TYPE_ERROR("path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) if (args[1]->IsObject()) { - ASYNC_CALL(stat, args[1], *path) + ASYNC_CALL(stat, args[1], UTF8, *path) } else { SYNC_CALL(stat, *path, *path) args.GetReturnValue().Set( @@ -563,13 +604,12 @@ static void LStat(const FunctionCallbackInfo& args) { if (args.Length() < 1) return TYPE_ERROR("path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) if (args[1]->IsObject()) { - ASYNC_CALL(lstat, args[1], *path) + ASYNC_CALL(lstat, args[1], UTF8, *path) } else { SYNC_CALL(lstat, *path, *path) args.GetReturnValue().Set( @@ -588,7 +628,7 @@ static void FStat(const FunctionCallbackInfo& args) { int fd = args[0]->Int32Value(); if (args[1]->IsObject()) { - ASYNC_CALL(fstat, args[1], fd) + ASYNC_CALL(fstat, args[1], UTF8, fd) } else { SYNC_CALL(fstat, 0, fd) args.GetReturnValue().Set( @@ -604,13 +644,12 @@ static void Symlink(const FunctionCallbackInfo& args) { return TYPE_ERROR("target path required"); if (len < 2) return TYPE_ERROR("src path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("target path must be a string"); - if (!args[1]->IsString()) - return TYPE_ERROR("src path must be a string"); - node::Utf8Value target(env->isolate(), args[0]); - node::Utf8Value path(env->isolate(), args[1]); + BufferValue target(env->isolate(), args[0]); + ASSERT_PATH(target) + BufferValue path(env->isolate(), args[1]); + ASSERT_PATH(path) + int flags = 0; if (args[2]->IsString()) { @@ -625,7 +664,7 @@ static void Symlink(const FunctionCallbackInfo& args) { } if (args[3]->IsObject()) { - ASYNC_DEST_CALL(symlink, args[3], *path, *target, *path, flags) + ASYNC_DEST_CALL(symlink, args[3], *path, UTF8, *target, *path, flags) } else { SYNC_DEST_CALL(symlink, *target, *path, *target, *path, flags) } @@ -639,37 +678,51 @@ static void Link(const FunctionCallbackInfo& args) { return TYPE_ERROR("src path required"); if (len < 2) return TYPE_ERROR("dest path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("src path must be a string"); - if (!args[1]->IsString()) - return TYPE_ERROR("dest path must be a string"); - node::Utf8Value orig_path(env->isolate(), args[0]); - node::Utf8Value new_path(env->isolate(), args[1]); + BufferValue src(env->isolate(), args[0]); + ASSERT_PATH(src) + + BufferValue dest(env->isolate(), args[1]); + ASSERT_PATH(dest) if (args[2]->IsObject()) { - ASYNC_DEST_CALL(link, args[2], *new_path, *orig_path, *new_path) + ASYNC_DEST_CALL(link, args[2], *dest, UTF8, *src, *dest) } else { - SYNC_DEST_CALL(link, *orig_path, *new_path, *orig_path, *new_path) + SYNC_DEST_CALL(link, *src, *dest, *src, *dest) } } static void ReadLink(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - if (args.Length() < 1) + const int argc = args.Length(); + + if (argc < 1) return TYPE_ERROR("path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) - if (args[1]->IsObject()) { - ASYNC_CALL(readlink, args[1], *path) + const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); + + Local callback = Null(env->isolate()); + if (argc == 3) + callback = args[2]; + + if (callback->IsObject()) { + ASYNC_CALL(readlink, callback, encoding, *path) } else { SYNC_CALL(readlink, *path, *path) const char* link_path = static_cast(SYNC_REQ.ptr); - Local rc = String::NewFromUtf8(env->isolate(), link_path); + Local rc = StringBytes::Encode(env->isolate(), + link_path, + encoding); + if (rc.IsEmpty()) { + return env->ThrowUVException(UV_EINVAL, + "readlink", + "Invalid character encoding for link", + *path); + } args.GetReturnValue().Set(rc); } } @@ -682,16 +735,14 @@ static void Rename(const FunctionCallbackInfo& args) { return TYPE_ERROR("old path required"); if (len < 2) return TYPE_ERROR("new path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("old path must be a string"); - if (!args[1]->IsString()) - return TYPE_ERROR("new path must be a string"); - node::Utf8Value old_path(env->isolate(), args[0]); - node::Utf8Value new_path(env->isolate(), args[1]); + BufferValue old_path(env->isolate(), args[0]); + ASSERT_PATH(old_path) + BufferValue new_path(env->isolate(), args[1]); + ASSERT_PATH(new_path) if (args[2]->IsObject()) { - ASYNC_DEST_CALL(rename, args[2], *new_path, *old_path, *new_path) + ASYNC_DEST_CALL(rename, args[2], *new_path, UTF8, *old_path, *new_path) } else { SYNC_DEST_CALL(rename, *old_path, *new_path, *old_path, *new_path) } @@ -720,7 +771,7 @@ static void FTruncate(const FunctionCallbackInfo& args) { const int64_t len = len_v->IntegerValue(); if (args[2]->IsObject()) { - ASYNC_CALL(ftruncate, args[2], fd, len) + ASYNC_CALL(ftruncate, args[2], UTF8, fd, len) } else { SYNC_CALL(ftruncate, 0, fd, len) } @@ -737,7 +788,7 @@ static void Fdatasync(const FunctionCallbackInfo& args) { int fd = args[0]->Int32Value(); if (args[1]->IsObject()) { - ASYNC_CALL(fdatasync, args[1], fd) + ASYNC_CALL(fdatasync, args[1], UTF8, fd) } else { SYNC_CALL(fdatasync, 0, fd) } @@ -754,7 +805,7 @@ static void Fsync(const FunctionCallbackInfo& args) { int fd = args[0]->Int32Value(); if (args[1]->IsObject()) { - ASYNC_CALL(fsync, args[1], fd) + ASYNC_CALL(fsync, args[1], UTF8, fd) } else { SYNC_CALL(fsync, 0, fd) } @@ -765,13 +816,12 @@ static void Unlink(const FunctionCallbackInfo& args) { if (args.Length() < 1) return TYPE_ERROR("path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) if (args[1]->IsObject()) { - ASYNC_CALL(unlink, args[1], *path) + ASYNC_CALL(unlink, args[1], UTF8, *path) } else { SYNC_CALL(unlink, *path, *path) } @@ -782,13 +832,12 @@ static void RMDir(const FunctionCallbackInfo& args) { if (args.Length() < 1) return TYPE_ERROR("path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) if (args[1]->IsObject()) { - ASYNC_CALL(rmdir, args[1], *path) + ASYNC_CALL(rmdir, args[1], UTF8, *path) } else { SYNC_CALL(rmdir, *path, *path) } @@ -799,16 +848,16 @@ static void MKDir(const FunctionCallbackInfo& args) { if (args.Length() < 2) return TYPE_ERROR("path and mode are required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); if (!args[1]->IsInt32()) return TYPE_ERROR("mode must be an integer"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + int mode = static_cast(args[1]->Int32Value()); if (args[2]->IsObject()) { - ASYNC_CALL(mkdir, args[2], *path, mode) + ASYNC_CALL(mkdir, args[2], UTF8, *path, mode) } else { SYNC_CALL(mkdir, *path, *path, mode) } @@ -817,15 +866,22 @@ static void MKDir(const FunctionCallbackInfo& args) { static void ReadDir(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - if (args.Length() < 1) + const int argc = args.Length(); + + if (argc < 1) return TYPE_ERROR("path required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) - if (args[1]->IsObject()) { - ASYNC_CALL(scandir, args[1], *path, 0 /*flags*/) + const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); + + Local callback = Null(env->isolate()); + if (argc == 3) + callback = args[2]; + + if (callback->IsObject()) { + ASYNC_CALL(scandir, callback, encoding, *path, 0 /*flags*/) } else { SYNC_CALL(scandir, *path, *path, 0 /*flags*/) @@ -845,8 +901,17 @@ static void ReadDir(const FunctionCallbackInfo& args) { if (r != 0) return env->ThrowUVException(r, "readdir", "", *path); + Local filename = StringBytes::Encode(env->isolate(), + ent.name, + encoding); + if (filename.IsEmpty()) { + return env->ThrowUVException(UV_EINVAL, + "readdir", + "Invalid character encoding for filename", + *path); + } - name_v[name_idx++] = String::NewFromUtf8(env->isolate(), ent.name); + name_v[name_idx++] = filename; if (name_idx >= ARRAY_SIZE(name_v)) { fn->Call(env->context(), names, name_idx, name_v) @@ -873,19 +938,19 @@ static void Open(const FunctionCallbackInfo& args) { return TYPE_ERROR("flags required"); if (len < 3) return TYPE_ERROR("mode required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); if (!args[1]->IsInt32()) return TYPE_ERROR("flags must be an int"); if (!args[2]->IsInt32()) return TYPE_ERROR("mode must be an int"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + int flags = args[1]->Int32Value(); int mode = static_cast(args[2]->Int32Value()); if (args[3]->IsObject()) { - ASYNC_CALL(open, args[3], *path, flags, mode) + ASYNC_CALL(open, args[3], UTF8, *path, flags, mode) } else { SYNC_CALL(open, *path, *path, flags, mode) args.GetReturnValue().Set(SYNC_RESULT); @@ -933,7 +998,7 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { uv_buf_t uvbuf = uv_buf_init(const_cast(buf), len); if (req->IsObject()) { - ASYNC_CALL(write, req, fd, &uvbuf, 1, pos) + ASYNC_CALL(write, req, UTF8, fd, &uvbuf, 1, pos) return; } @@ -983,7 +1048,7 @@ static void WriteBuffers(const FunctionCallbackInfo& args) { } if (req->IsObject()) { - ASYNC_CALL(write, req, fd, iovs, chunkCount, pos) + ASYNC_CALL(write, req, UTF8, fd, iovs, chunkCount, pos) if (iovs != s_iovs) delete[] iovs; return; @@ -1049,7 +1114,7 @@ static void WriteString(const FunctionCallbackInfo& args) { } FSReqWrap* req_wrap = - FSReqWrap::New(env, req.As(), "write", buf, ownership); + FSReqWrap::New(env, req.As(), "write", buf, UTF8, ownership); int err = uv_fs_write(env->event_loop(), &req_wrap->req_, fd, @@ -1123,7 +1188,7 @@ static void Read(const FunctionCallbackInfo& args) { req = args[5]; if (req->IsObject()) { - ASYNC_CALL(read, req, fd, &uvbuf, 1, pos); + ASYNC_CALL(read, req, UTF8, fd, &uvbuf, 1, pos); } else { SYNC_CALL(read, 0, fd, &uvbuf, 1, pos) args.GetReturnValue().Set(SYNC_RESULT); @@ -1139,16 +1204,16 @@ static void Chmod(const FunctionCallbackInfo& args) { if (args.Length() < 2) return TYPE_ERROR("path and mode are required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); if (!args[1]->IsInt32()) return TYPE_ERROR("mode must be an integer"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + int mode = static_cast(args[1]->Int32Value()); if (args[2]->IsObject()) { - ASYNC_CALL(chmod, args[2], *path, mode); + ASYNC_CALL(chmod, args[2], UTF8, *path, mode); } else { SYNC_CALL(chmod, *path, *path, mode); } @@ -1172,7 +1237,7 @@ static void FChmod(const FunctionCallbackInfo& args) { int mode = static_cast(args[1]->Int32Value()); if (args[2]->IsObject()) { - ASYNC_CALL(fchmod, args[2], fd, mode); + ASYNC_CALL(fchmod, args[2], UTF8, fd, mode); } else { SYNC_CALL(fchmod, 0, fd, mode); } @@ -1192,19 +1257,19 @@ static void Chown(const FunctionCallbackInfo& args) { return TYPE_ERROR("uid required"); if (len < 3) return TYPE_ERROR("gid required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); if (!args[1]->IsUint32()) return TYPE_ERROR("uid must be an unsigned int"); if (!args[2]->IsUint32()) return TYPE_ERROR("gid must be an unsigned int"); - node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + uv_uid_t uid = static_cast(args[1]->Uint32Value()); uv_gid_t gid = static_cast(args[2]->Uint32Value()); if (args[3]->IsObject()) { - ASYNC_CALL(chown, args[3], *path, uid, gid); + ASYNC_CALL(chown, args[3], UTF8, *path, uid, gid); } else { SYNC_CALL(chown, *path, *path, uid, gid); } @@ -1236,7 +1301,7 @@ static void FChown(const FunctionCallbackInfo& args) { uv_gid_t gid = static_cast(args[2]->Uint32Value()); if (args[3]->IsObject()) { - ASYNC_CALL(fchown, args[3], fd, uid, gid); + ASYNC_CALL(fchown, args[3], UTF8, fd, uid, gid); } else { SYNC_CALL(fchown, 0, fd, uid, gid); } @@ -1253,19 +1318,19 @@ static void UTimes(const FunctionCallbackInfo& args) { return TYPE_ERROR("atime required"); if (len < 3) return TYPE_ERROR("mtime required"); - if (!args[0]->IsString()) - return TYPE_ERROR("path must be a string"); if (!args[1]->IsNumber()) return TYPE_ERROR("atime must be a number"); if (!args[2]->IsNumber()) return TYPE_ERROR("mtime must be a number"); - const node::Utf8Value path(env->isolate(), args[0]); + BufferValue path(env->isolate(), args[0]); + ASSERT_PATH(path) + const double atime = static_cast(args[1]->NumberValue()); const double mtime = static_cast(args[2]->NumberValue()); if (args[3]->IsObject()) { - ASYNC_CALL(utime, args[3], *path, atime, mtime); + ASYNC_CALL(utime, args[3], UTF8, *path, atime, mtime); } else { SYNC_CALL(utime, *path, *path, atime, mtime); } @@ -1293,7 +1358,7 @@ static void FUTimes(const FunctionCallbackInfo& args) { const double mtime = static_cast(args[2]->NumberValue()); if (args[3]->IsObject()) { - ASYNC_CALL(futime, args[3], fd, atime, mtime); + ASYNC_CALL(futime, args[3], UTF8, fd, atime, mtime); } else { SYNC_CALL(futime, 0, fd, atime, mtime); } @@ -1302,19 +1367,27 @@ static void FUTimes(const FunctionCallbackInfo& args) { static void Mkdtemp(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); - if (args.Length() < 1) - return TYPE_ERROR("template is required"); - if (!args[0]->IsString()) - return TYPE_ERROR("template must be a string"); + CHECK_GE(args.Length(), 2); - node::Utf8Value tmpl(env->isolate(), args[0]); + BufferValue tmpl(env->isolate(), args[0]); + if (*tmpl == nullptr) + return TYPE_ERROR("template must be a string or Buffer"); - if (args[1]->IsObject()) { - ASYNC_CALL(mkdtemp, args[1], *tmpl); + const enum encoding encoding = ParseEncoding(env->isolate(), args[1], UTF8); + + if (args[2]->IsObject()) { + ASYNC_CALL(mkdtemp, args[2], encoding, *tmpl); } else { SYNC_CALL(mkdtemp, *tmpl, *tmpl); - args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(), - SYNC_REQ.path)); + const char* path = static_cast(SYNC_REQ.path); + Local rc = StringBytes::Encode(env->isolate(), path, encoding); + if (rc.IsEmpty()) { + return env->ThrowUVException(UV_EINVAL, + "mkdtemp", + "Invalid character encoding for filename", + *tmpl); + } + args.GetReturnValue().Set(rc); } } diff --git a/src/string_bytes.cc b/src/string_bytes.cc index a2e4fe388a5b72..b8d7d3b42f11c8 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -24,7 +24,6 @@ using v8::String; using v8::Value; using v8::MaybeLocal; - template class ExternString: public ResourceType { public: @@ -895,4 +894,34 @@ Local StringBytes::Encode(Isolate* isolate, return val; } +Local StringBytes::Encode(Isolate* isolate, + const char* buf, + enum encoding encoding) { + const size_t len = strlen(buf); + Local ret; + if (encoding == UCS2) { + // In Node, UCS2 means utf16le. The data must be in little-endian + // order and must be aligned on 2-bytes. This returns an empty + // value if it's not aligned and ensures the appropriate byte order + // on big endian architectures. + const bool be = IsBigEndian(); + if (len % 2 != 0) + return ret; + std::vector vec(len / 2); + for (size_t i = 0, k = 0; i < len; i += 2, k += 1) { + const uint8_t hi = static_cast(buf[i + 0]); + const uint8_t lo = static_cast(buf[i + 1]); + vec[k] = be ? + static_cast(hi) << 8 | lo + : static_cast(lo) << 8 | hi; + } + ret = vec.empty() ? + static_cast< Local >(String::Empty(isolate)) + : StringBytes::Encode(isolate, &vec[0], vec.size()); + } else { + ret = StringBytes::Encode(isolate, buf, len, encoding); + } + return ret; +} + } // namespace node diff --git a/src/string_bytes.h b/src/string_bytes.h index 79520d24705ac0..710886213932e6 100644 --- a/src/string_bytes.h +++ b/src/string_bytes.h @@ -106,6 +106,10 @@ class StringBytes { const uint16_t* buf, size_t buflen); + static v8::Local Encode(v8::Isolate* isolate, + const char* buf, + enum encoding encoding); + // Deprecated legacy interface NODE_DEPRECATED("Use IsValidString(isolate, ...)", diff --git a/src/util.cc b/src/util.cc index 095e5582db0dfa..3b0278cedaff67 100644 --- a/src/util.cc +++ b/src/util.cc @@ -1,37 +1,48 @@ #include "util.h" #include "string_bytes.h" +#include "node_buffer.h" +#include namespace node { -Utf8Value::Utf8Value(v8::Isolate* isolate, v8::Local value) - : length_(0), str_(str_st_) { - if (value.IsEmpty()) - return; +using v8::Isolate; +using v8::String; +using v8::Local; +using v8::Value; - v8::Local string = value->ToString(isolate); +static int MakeUtf8String(Isolate* isolate, + Local value, + char** dst, + const size_t size) { + Local string = value->ToString(isolate); if (string.IsEmpty()) - return; - - // Allocate enough space to include the null terminator + return 0; size_t len = StringBytes::StorageSize(isolate, string, UTF8) + 1; - if (len > sizeof(str_st_)) { - str_ = static_cast(malloc(len)); - CHECK_NE(str_, nullptr); + if (len > size) { + *dst = static_cast(malloc(len)); + CHECK_NE(*dst, nullptr); } - const int flags = - v8::String::NO_NULL_TERMINATION | v8::String::REPLACE_INVALID_UTF8; - length_ = string->WriteUtf8(str_, len, 0, flags); - str_[length_] = '\0'; + String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8; + const int length = string->WriteUtf8(*dst, len, 0, flags); + (*dst)[length] = '\0'; + return length; +} + +Utf8Value::Utf8Value(Isolate* isolate, Local value) + : length_(0), str_(str_st_) { + if (value.IsEmpty()) + return; + length_ = MakeUtf8String(isolate, value, &str_, sizeof(str_st_)); } -TwoByteValue::TwoByteValue(v8::Isolate* isolate, v8::Local value) +TwoByteValue::TwoByteValue(Isolate* isolate, Local value) : length_(0), str_(str_st_) { if (value.IsEmpty()) return; - v8::Local string = value->ToString(isolate); + Local string = value->ToString(isolate); if (string.IsEmpty()) return; @@ -43,9 +54,31 @@ TwoByteValue::TwoByteValue(v8::Isolate* isolate, v8::Local value) } const int flags = - v8::String::NO_NULL_TERMINATION | v8::String::REPLACE_INVALID_UTF8; + String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8; length_ = string->Write(str_, 0, len, flags); str_[length_] = '\0'; } +BufferValue::BufferValue(Isolate* isolate, Local value) + : str_(str_st_), fail_(true) { + // Slightly different take on Utf8Value. If value is a String, + // it will return a Utf8 encoded string. If value is a Buffer, + // it will copy the data out of the Buffer as is. + if (value.IsEmpty()) + return; + if (value->IsString()) { + MakeUtf8String(isolate, value, &str_, sizeof(str_st_)); + fail_ = false; + } else if (Buffer::HasInstance(value)) { + size_t len = Buffer::Length(value) + 1; + if (len > sizeof(str_st_)) { + str_ = static_cast(malloc(len)); + CHECK_NE(str_, nullptr); + } + memcpy(str_, Buffer::Data(value), len); + str_[len - 1] = '\0'; + fail_ = false; + } +} + } // namespace node diff --git a/src/util.h b/src/util.h index 84d0b5a170d94c..2c4d8c6286be8b 100644 --- a/src/util.h +++ b/src/util.h @@ -232,6 +232,25 @@ class TwoByteValue { uint16_t str_st_[1024]; }; +class BufferValue { + public: + explicit BufferValue(v8::Isolate* isolate, v8::Local value); + + ~BufferValue() { + if (str_ != str_st_) + free(str_); + } + + const char* operator*() const { + return fail_ ? nullptr : str_; + }; + + private: + char* str_; + char str_st_[1024]; + bool fail_; +}; + } // namespace node #endif // SRC_UTIL_H_ diff --git a/test/common.js b/test/common.js index 28e7972e223602..6fb357ba2586a2 100644 --- a/test/common.js +++ b/test/common.js @@ -57,8 +57,14 @@ function rmdirSync(p, originalEr) { if (e.code === 'ENOTDIR') throw originalEr; if (e.code === 'ENOTEMPTY' || e.code === 'EEXIST' || e.code === 'EPERM') { - fs.readdirSync(p).forEach(function(f) { - rimrafSync(path.join(p, f)); + const enc = process.platform === 'linux' ? 'buffer' : 'utf8'; + fs.readdirSync(p, enc).forEach((f) => { + if (f instanceof Buffer) { + const buf = Buffer.concat([Buffer.from(p), Buffer.from(path.sep), f]); + rimrafSync(buf); + } else { + rimrafSync(path.join(p, f)); + } }); fs.rmdirSync(p); } diff --git a/test/parallel/test-fs-access.js b/test/parallel/test-fs-access.js index 9a1b6b8a5db8ed..6b82b4bbf312c3 100644 --- a/test/parallel/test-fs-access.js +++ b/test/parallel/test-fs-access.js @@ -92,7 +92,7 @@ fs.access(readOnlyFile, fs.W_OK, function(err) { assert.throws(function() { fs.access(100, fs.F_OK, function(err) {}); -}, /path must be a string/); +}, /path must be a string or Buffer/); assert.throws(function() { fs.access(__filename, fs.F_OK); diff --git a/test/parallel/test-fs-buffer.js b/test/parallel/test-fs-buffer.js new file mode 100644 index 00000000000000..6f142310f5e418 --- /dev/null +++ b/test/parallel/test-fs-buffer.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +common.refreshTmpDir(); + +assert.doesNotThrow(() => { + fs.access(Buffer.from(common.tmpDir), common.mustCall((err) => { + if (err) throw err; + })); +}); + +assert.doesNotThrow(() => { + const buf = Buffer.from(path.join(common.tmpDir, 'a.txt')); + fs.open(buf, 'w+', common.mustCall((err, fd) => { + if (err) throw err; + assert(fd); + fs.close(fd, common.mustCall(() => { + fs.unlinkSync(buf); + })); + })); +}); + +assert.throws(() => { + fs.accessSync(true); +}, /path must be a string or Buffer/); + +const dir = Buffer.from(common.fixturesDir); +fs.readdir(dir, 'hex', common.mustCall((err, list) => { + if (err) throw err; + list = list.map((i) => { + return Buffer.from(i, 'hex').toString(); + }); + fs.readdir(dir, common.mustCall((err, list2) => { + if (err) throw err; + assert.deepStrictEqual(list, list2); + })); +})); diff --git a/test/parallel/test-fs-link.js b/test/parallel/test-fs-link.js index 292d48fb53e5c6..2cba47bfec83df 100644 --- a/test/parallel/test-fs-link.js +++ b/test/parallel/test-fs-link.js @@ -25,12 +25,12 @@ assert.throws( function() { fs.link(); }, - /src path/ + /src must be a string or Buffer/ ); assert.throws( function() { fs.link('abc'); }, - /dest path/ + /dest must be a string or Buffer/ ); diff --git a/test/parallel/test-fs-readdir-ucs2.js b/test/parallel/test-fs-readdir-ucs2.js new file mode 100644 index 00000000000000..578723117a038a --- /dev/null +++ b/test/parallel/test-fs-readdir-ucs2.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); + +if (process.platform !== 'linux') { + console.log('1..0 # Skipped: Test is linux specific.'); + return; +} + +common.refreshTmpDir(); +const filename = '\uD83D\uDC04'; +const root = Buffer.from(`${common.tmpDir}${path.sep}`); +const filebuff = Buffer.from(filename, 'ucs2'); +const fullpath = Buffer.concat([root, filebuff]); + +fs.closeSync(fs.openSync(fullpath, 'w+')); + +fs.readdir(common.tmpDir, 'ucs2', (err, list) => { + if (err) throw err; + assert.equal(1, list.length); + const fn = list[0]; + assert.deepStrictEqual(filebuff, Buffer.from(fn, 'ucs2')); + assert.strictEqual(fn, filename); +}); + +process.on('exit', () => { + fs.unlinkSync(fullpath); +}); diff --git a/test/parallel/test-fs-watch-encoding.js b/test/parallel/test-fs-watch-encoding.js new file mode 100644 index 00000000000000..cb7af00f6615f8 --- /dev/null +++ b/test/parallel/test-fs-watch-encoding.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +if (common.isFreeBSD) { + console.log('1..0 # Skipped: Test currently not working on FreeBSD'); + return; +} + +const fn = '新建文夹件.txt'; +const a = path.join(common.tmpDir, fn); + +const watcher1 = fs.watch( + common.tmpDir, + {encoding: 'hex'}, + (event, filename) => { + if (filename) + assert.equal(filename, 'e696b0e5bbbae69687e5a4b9e4bbb62e747874'); + watcher1.close(); + } +); + +const watcher2 = fs.watch( + common.tmpDir, + (event, filename) => { + if (filename) + assert.equal(filename, fn); + watcher2.close(); + } +); + +const watcher3 = fs.watch( + common.tmpDir, + {encoding: 'buffer'}, + (event, filename) => { + if (filename) { + assert(filename instanceof Buffer); + assert.equal(filename.toString('utf8'), fn); + } + watcher3.close(); + } +); + +const fd = fs.openSync(a, 'w+'); +fs.closeSync(fd); + +process.on('exit', () => { + fs.unlink(a); +}); diff --git a/test/sequential/test-module-loading.js b/test/sequential/test-module-loading.js index a6f9488f5f8092..7370f8290d484f 100644 --- a/test/sequential/test-module-loading.js +++ b/test/sequential/test-module-loading.js @@ -247,7 +247,7 @@ assert.deepEqual(children, { assert.throws(function() { console.error('require non-string'); require({ foo: 'bar' }); -}, 'path must be a string'); +}, 'path must be a string or Buffer'); assert.throws(function() { console.error('require empty string');