Commit 1a885caa6e7 for nodejs
commit 1a885caa6e7f5fb0372ba774c021335b7389dda9
Author: Marco <marco.piraccini@gmail.com>
Date: Mon Oct 19 04:34:09 2026 +0200
fs: add mkstemp()
Add fs.mkstemp(), fs.mkstempSync() and fsPromises.mkstemp(), which
create and open a unique temporary file in one operation. They expose
uv_fs_mkstemp(), the file counterpart of the function behind
fs.mkdtemp().
The callback and sync versions return the path and a file descriptor,
the promise version returns the path and a FileHandle.
libuv clears the path of the request when mkstemp() fails, so the
template is saved in the request and used for the path of the error.
The functions are also supported on mounted virtual file systems.
Refs: https://github.com/nodejs/node/pull/33549
Refs: https://github.com/nodejs/node/issues/33890
Refs: https://github.com/nodejs/node/issues/5332
Signed-off-by: marcopiraccini <marco.piraccini@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66484
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
diff --git a/doc/api/fs.md b/doc/api/fs.md
index e327a330a14..2c5690b5f34 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -1732,6 +1732,58 @@ For detailed information, see the documentation of [`fsPromises.mkdtemp()`][].
The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.
+### `fsPromises.mkstemp(prefix[, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `prefix` {string|Buffer|URL}
+* `options` {string|Object}
+ * `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
+* Returns: {Promise} Fulfills with an {Object}:
+ * `path` {string|Buffer} The path of the created file.
+ * `handle` {FileHandle} The created file, opened for reading and writing.
+
+Creates and opens a unique temporary file. A unique file name is generated by
+appending six random characters to the end of the provided `prefix`. Due to
+platform inconsistencies, avoid trailing `X` characters in `prefix`. Some
+platforms, notably the BSDs, can return more than six random characters, and
+replace trailing `X` characters in `prefix` with random characters.
+
+The file is created and opened in a single operation, so another process cannot
+create a file with the same name in between. On POSIX systems, the file is only
+readable and writable by its owner.
+
+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 returned `path`.
+
+```mjs
+import { mkstemp } from 'node:fs/promises';
+import { join } from 'node:path';
+import { tmpdir } from 'node:os';
+
+const { path, handle } = await mkstemp(join(tmpdir(), 'foo-'));
+try {
+ await handle.writeFile('some data');
+ console.log(path);
+ // Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
+} finally {
+ await handle.close();
+}
+```
+
+The file is not removed automatically. It is the caller's responsibility to
+close the {FileHandle} and to remove the file when it is no longer needed.
+
+As with [`fsPromises.mkdtemp()`][], the random characters are appended directly
+to `prefix`. To create a file _within_ a directory, `prefix` must end with a
+trailing platform-specific path separator (`require('node:path').sep`) or
+include the beginning of the file name.
+
### `fsPromises.open(path, flags[, mode])`
<!-- YAML
@@ -4099,6 +4151,66 @@ mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
});
```
+### `fs.mkstemp(prefix[, options], callback)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `prefix` {string|Buffer|URL}
+* `options` {string|Object}
+ * `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
+* `callback` {Function}
+ * `err` {Error}
+ * `file` {Object}
+ * `path` {string|Buffer} The path of the created file.
+ * `fd` {integer} A file descriptor for the created file, opened for
+ reading and writing.
+
+Creates and opens a unique temporary file.
+
+Generates six random characters to be appended behind a required `prefix` to
+create a unique temporary file. Due to platform inconsistencies, avoid trailing
+`X` characters in `prefix`. Some platforms, notably the BSDs, can return more
+than six random characters, and replace trailing `X` characters in `prefix`
+with random characters.
+
+The file is created and opened in a single operation, so another process cannot
+create a file with the same name in between. On POSIX systems, the file is only
+readable and writable by its owner.
+
+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 `path` passed to the callback.
+
+```mjs
+import { mkstemp, write, close } from 'node:fs';
+import { join } from 'node:path';
+import { tmpdir } from 'node:os';
+
+mkstemp(join(tmpdir(), 'foo-'), (err, file) => {
+ if (err) throw err;
+ console.log(file.path);
+ // Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2
+ write(file.fd, 'some data', (err) => {
+ if (err) throw err;
+ close(file.fd, (err) => {
+ if (err) throw err;
+ });
+ });
+});
+```
+
+The file is not removed automatically. It is the caller's responsibility to
+close the file descriptor and to remove the file when it is no longer needed.
+
+As with [`fs.mkdtemp()`][], the random characters are appended directly to
+`prefix`. To create a file _within_ a directory, `prefix` must end with a
+trailing platform-specific path separator (`require('node:path').sep`) or
+include the beginning of the file name.
+
### `fs.open(path[, flags[, mode]], callback)`
<!-- YAML
@@ -6583,6 +6695,31 @@ with the [`using`][] syntax.
The optional `options` argument can be a string specifying an encoding, or an
object with an `encoding` property specifying the character encoding to use.
+### `fs.mkstempSync(prefix[, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `prefix` {string|Buffer|URL}
+* `options` {string|Object}
+ * `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`)
+* Returns: {Object}
+ * `path` {string|Buffer} The path of the created file.
+ * `fd` {integer} A file descriptor for the created file, opened for reading
+ and writing.
+
+Synchronously creates and opens a unique temporary file.
+
+For detailed information, see the documentation of the asynchronous version of
+this API: [`fs.mkstemp()`][].
+
+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 returned `path`.
+
### `fs.openAsBlobSync(path[, options])`
<!-- YAML
@@ -9610,6 +9747,7 @@ the file contents.
[`fs.lutimes()`]: #fslutimespath-atime-mtime-callback
[`fs.mkdir()`]: #fsmkdirpath-options-callback
[`fs.mkdtemp()`]: #fsmkdtempprefix-options-callback
+[`fs.mkstemp()`]: #fsmkstempprefix-options-callback
[`fs.open()`]: #fsopenpath-flags-mode-callback
[`fs.openAsBlob()`]: #fsopenasblobpath-options
[`fs.opendir()`]: #fsopendirpath-options-callback
diff --git a/doc/api/vfs.md b/doc/api/vfs.md
index 90a4428d89b..726c12c33d9 100644
--- a/doc/api/vfs.md
+++ b/doc/api/vfs.md
@@ -376,6 +376,7 @@ signatures as their [`node:fs`][] counterparts:
* `utimesSync(path, atime, mtime)`
* `lutimesSync(path, atime, mtime)`
* `mkdtempSync(prefix)`
+* `mkstempSync(prefix)`
* `opendirSync(path[, options])`
* `openAsBlob(path[, options])`
* File-descriptor ops: `openSync`, `closeSync`, `readSync`, `writeSync`,
@@ -387,8 +388,8 @@ signatures as their [`node:fs`][] counterparts:
`readFile`, `writeFile`, `stat`, `lstat`, `readdir`, `realpath`, `readlink`,
`access`, `open`, `close`, `read`, `write`, `rm`, `fstat`, `truncate`,
-`ftruncate`, `link`, `mkdtemp`, `opendir`. Each takes a Node.js-style
-callback `(err, ...result) => {}`.
+`ftruncate`, `link`, `mkdtemp`, `mkstemp`, `opendir`. Each takes a
+Node.js-style callback `(err, ...result) => {}`.
#### Promise API
@@ -409,8 +410,8 @@ example();
The promise namespace mirrors `fs.promises` and includes `readFile`,
`writeFile`, `appendFile`, `stat`, `lstat`, `readdir`, `mkdir`, `rmdir`,
`unlink`, `rename`, `copyFile`, `realpath`, `readlink`, `symlink`,
-`access`, `rm`, `truncate`, `link`, `mkdtemp`, `chmod`, `chown`, `lchown`,
-`utimes`, `lutimes`, `open`, `lchmod`, and `watch`.
+`access`, `rm`, `truncate`, `link`, `mkdtemp`, `mkstemp`, `chmod`, `chown`,
+`lchown`, `utimes`, `lutimes`, `open`, `lchmod`, and `watch`.
## The reserved root directory
diff --git a/lib/fs.js b/lib/fs.js
index 4436fa2df6e..c0f7e6f0adf 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -3773,6 +3773,58 @@ function mkdtempDisposableSync(prefix, options) {
};
}
+/**
+ * Creates and opens a unique temporary file.
+ * @param {string | Buffer | URL} prefix
+ * @param {string | { encoding?: string; }} [options]
+ * @param {(err?: Error, file?: { path: string | Buffer, fd: number }) => any} callback
+ * @returns {void}
+ */
+function mkstemp(prefix, options, callback) {
+ callback = makeCallback(typeof options === 'function' ? options : callback);
+
+ options = getOptions(options);
+ if (BufferIsBuffer(prefix)) {
+ options = { ...options, encoding: 'buffer' };
+ }
+ prefix = getValidatedPath(prefix, 'prefix');
+ warnOnNonPortableTemplate(prefix);
+
+ const h = vfsState.handlers;
+ if (h !== null && vfsResult(h.mkstemp(prefix, options), callback)) return;
+
+ const req = new FSReqCallback();
+ req.oncomplete = (err, result) => {
+ if (err) return callback(err);
+ callback(null, { path: result[0], fd: result[1] });
+ };
+ binding.mkstemp(prefix, options.encoding, req);
+}
+
+/**
+ * Synchronously creates and opens a unique temporary file.
+ * @param {string | Buffer | URL} prefix
+ * @param {string | { encoding?: string; }} [options]
+ * @returns {{ path: string | Buffer, fd: number }}
+ */
+function mkstempSync(prefix, options) {
+ options = getOptions(options);
+ if (BufferIsBuffer(prefix)) {
+ options = { ...options, encoding: 'buffer' };
+ }
+ prefix = getValidatedPath(prefix, 'prefix');
+ warnOnNonPortableTemplate(prefix);
+
+ const h = vfsState.handlers;
+ if (h !== null) {
+ const result = h.mkstempSync(prefix, options);
+ if (result !== undefined) return result;
+ }
+
+ const result = binding.mkstemp(prefix, options.encoding);
+ return { path: result[0], fd: result[1] };
+}
+
/**
* Asynchronously copies `src` to `dest`. By
* default, `dest` is overwritten if it already exists.
@@ -3997,6 +4049,8 @@ module.exports = fs = {
mkdtemp,
mkdtempSync,
mkdtempDisposableSync,
+ mkstemp,
+ mkstempSync,
open,
openSync,
openAsBlob,
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index c21b1e5740f..47574a5a471 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -2136,6 +2136,28 @@ async function mkdtemp(prefix, options) {
);
}
+async function mkstemp(prefix, options) {
+ options = getOptions(options);
+ if (BufferIsBuffer(prefix)) {
+ options = { ...options, encoding: 'buffer' };
+ }
+ prefix = getValidatedPath(prefix, 'prefix');
+ warnOnNonPortableTemplate(prefix);
+
+ const h = vfsState.handlers;
+ if (h !== null) {
+ const promise = h.promisesMkstemp(prefix, options);
+ if (promise !== undefined) return await promise;
+ }
+
+ const result = await PromisePrototypeThen(
+ binding.mkstemp(prefix, options.encoding, kUsePromises),
+ undefined,
+ handleErrorFromBinding,
+ );
+ return { __proto__: null, path: result[0], handle: new FileHandle(result[1]) };
+}
+
async function mkdtempDisposable(prefix, options) {
options = getOptions(options);
if (BufferIsBuffer(prefix)) {
@@ -2389,6 +2411,7 @@ module.exports = {
realpath,
mkdtemp,
mkdtempDisposable,
+ mkstemp,
writeFile,
appendFile,
readFile,
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index 3aadc19c473..29f254d9250 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -965,12 +965,14 @@ const validateBufferArray = hideStackFrames((buffers, propName = 'buffers') => {
let nonPortableTemplateWarn = true;
function warnOnNonPortableTemplate(template) {
- // Template strings passed to the mkdtemp() family of functions should not
- // end with 'X' because they are handled inconsistently across platforms.
+ // Template strings passed to the mkdtemp() and mkstemp() families of
+ // functions should not end with 'X' because they are handled inconsistently
+ // across platforms.
if (nonPortableTemplateWarn &&
((typeof template === 'string' && StringPrototypeEndsWith(template, 'X')) ||
(typeof template !== 'string' && TypedArrayPrototypeAt(template, -1) === 0x58))) {
- process.emitWarning('mkdtemp() templates ending with X are not portable. ' +
+ process.emitWarning('mkdtemp() and mkstemp() templates ending with X are ' +
+ 'not portable. ' +
'For details see: https://nodejs.org/api/fs.html');
nonPortableTemplateWarn = false;
}
diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js
index 4f740579413..6964564c6ee 100644
--- a/lib/internal/vfs/file_system.js
+++ b/lib/internal/vfs/file_system.js
@@ -614,8 +614,19 @@ class VirtualFileSystem {
}
/**
- * Converts a mkdtemp prefix to a provider-relative one, keeping a
- * trailing separator.
+ * Creates and opens a unique temporary file synchronously.
+ * @param {string} prefix The prefix for the temp file
+ * @returns {{ path: string, fd: number }} The full path and a file descriptor
+ */
+ mkstempSync(prefix) {
+ const filePath = this.#toProviderPrefix(prefix) + randomSuffix();
+ const handle = this[kProvider].openSync(filePath, 'wx+', 0o600);
+ return { path: this.#toMountedPath(filePath), fd: openVirtualFd(handle) };
+ }
+
+ /**
+ * Converts a mkdtemp or mkstemp prefix to a provider-relative one, keeping
+ * a trailing separator.
* @param {string} prefix The mounted prefix
* @returns {string}
*/
@@ -1057,6 +1068,25 @@ class VirtualFileSystem {
}
}
+ /**
+ * Creates and opens a unique temporary file asynchronously.
+ * @param {string} prefix The prefix for the temp file
+ * @param {object|Function} [options] Options or callback
+ * @param {Function} [callback] Callback (err, { path, fd })
+ */
+ mkstemp(prefix, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ try {
+ const file = this.mkstempSync(prefix);
+ process.nextTick(callback, null, file);
+ } catch (err) {
+ process.nextTick(callback, err);
+ }
+ }
+
/**
* Opens a directory asynchronously.
* @param {string} dirPath The directory path
@@ -1308,6 +1338,16 @@ class VirtualFileSystem {
return toMountedPath(dirPath);
},
+ async mkstemp(prefix) {
+ const filePath = toProviderPrefix(prefix) + randomSuffix();
+ const handle = provider.openSync(filePath, 'wx+', 0o600);
+ return {
+ __proto__: null,
+ path: toMountedPath(filePath),
+ fd: openVirtualFd(handle),
+ };
+ },
+
async chmod(filePath, mode) {
const providerPath = toProviderPath(filePath);
provider.chmodSync(providerPath, mode);
diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js
index d6af8e566f8..6ad0824f85e 100644
--- a/lib/internal/vfs/setup.js
+++ b/lib/internal/vfs/setup.js
@@ -532,6 +532,13 @@ function createVfsHandlers() {
}
return result;
},
+ mkstempSync(prefix, options) {
+ const result = vfsOp(prefix, (vfs, n) => vfs.mkstempSync(n));
+ if (result !== undefined && options?.encoding === 'buffer') {
+ result.path = Buffer.from(result.path);
+ }
+ return result;
+ },
opendirSync: (path, options) => vfsOp(path, (vfs, n) => vfs.opendirSync(n, options)),
openAsBlob(path, options) {
const pathStr = toPathStr(path);
@@ -862,6 +869,27 @@ function createVfsHandlers() {
}
return promise;
},
+ // Wrapped in async functions so provider throws become rejections.
+ mkstemp(prefix, options) {
+ return vfsOp(prefix, async (vfs, n) => {
+ const { path, fd } = vfs.mkstempSync(n);
+ return {
+ __proto__: null,
+ path: options?.encoding === 'buffer' ? Buffer.from(path) : path,
+ fd,
+ };
+ });
+ },
+ promisesMkstemp(prefix, options) {
+ return vfsOp(prefix, async (vfs, n) => {
+ const { path, fd } = vfs.mkstempSync(n);
+ return {
+ __proto__: null,
+ path: options?.encoding === 'buffer' ? Buffer.from(path) : path,
+ handle: createVfsFileHandle(getVirtualFd(fd)),
+ };
+ });
+ },
chmod: (path, mode) =>
vfsOp(path, (vfs, n) => vfs.promises.chmod(n, mode).then(() => true)),
utimes: (path, atime, mtime) =>
diff --git a/src/node_file-inl.h b/src/node_file-inl.h
index 803446b1efb..9b402b57139 100644
--- a/src/node_file-inl.h
+++ b/src/node_file-inl.h
@@ -424,6 +424,70 @@ int SyncCallAndThrowOnError(Environment* env,
return SyncCallAndThrowIf(is_uv_error, env, req_wrap, fn, args...);
}
+// Closes a file created by mkstemp() whose descriptor cannot be handed to JS.
+inline void CloseMkstempFd(int fd) {
+ uv_fs_t close_req;
+ uv_fs_close(nullptr, &close_req, fd, nullptr);
+ uv_fs_req_cleanup(&close_req);
+}
+
+// The descriptor is handed to JS as a number, like the one of fs.open().
+inline v8::MaybeLocal<v8::Value> MkstempFd(Environment* env, int fd) {
+ env->AddUnmanagedFd(fd);
+ return v8::Integer::New(env->isolate(), fd);
+}
+
+// The descriptor is owned by the FileHandle that `new_handle` creates.
+template <typename NewHandle>
+v8::MaybeLocal<v8::Value> MkstempFileHandle(NewHandle&& new_handle) {
+ FileHandle* handle = new_handle();
+ if (handle == nullptr) return {};
+ return handle->object();
+}
+
+// Builds the [path, file] result for a file created by mkstemp(). `make_file`
+// takes ownership of the descriptor. If a step fails, the file is closed and
+// the exception is left pending.
+template <typename EncodePath, typename MakeFile>
+v8::MaybeLocal<v8::Value> MkstempResult(v8::Isolate* isolate,
+ int fd,
+ EncodePath&& encode_path,
+ MakeFile&& make_file) {
+ v8::Local<v8::Value> path;
+ v8::Local<v8::Value> file;
+ if (!encode_path().ToLocal(&path) || !make_file().ToLocal(&file)) {
+ CloseMkstempFd(fd);
+ return {};
+ }
+ v8::Local<v8::Value> result[] = {path, file};
+ return v8::Array::New(isolate, result, arraysize(result));
+}
+
+// Resolves `req_wrap` with the result of mkstemp(), or rejects it with the
+// exception thrown while building it. The TryCatch is left before calling
+// into JS.
+template <typename EncodePath, typename MakeFile>
+void ResolveMkstemp(FSReqBase* req_wrap,
+ int fd,
+ EncodePath&& encode_path,
+ MakeFile&& make_file) {
+ v8::Isolate* isolate = req_wrap->env()->isolate();
+ v8::Local<v8::Value> result;
+ v8::Local<v8::Value> error;
+ {
+ v8::TryCatch try_catch(isolate);
+ if (!MkstempResult(isolate, fd, encode_path, make_file).ToLocal(&result)) {
+ CHECK(try_catch.CanContinue());
+ error = try_catch.Exception();
+ }
+ }
+ if (error.IsEmpty()) {
+ req_wrap->Resolve(result);
+ } else {
+ req_wrap->Reject(error);
+ }
+}
+
} // namespace fs
} // namespace node
diff --git a/src/node_file.cc b/src/node_file.cc
index 0bc7511ccb5..f5e7cc80e72 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -800,12 +800,15 @@ void FSReqAfterScope::Clear() {
// in JS for more flexibility.
void FSReqAfterScope::Reject(uv_fs_t* req) {
BaseObjectPtr<FSReqBase> wrap{wrap_};
+ // libuv clears req->path when mkstemp() fails, so the template that was
+ // saved in the request is reported instead.
+ const bool is_mkstemp = req->fs_type == UV_FS_MKSTEMP;
Local<Value> exception = UVException(wrap_->env()->isolate(),
static_cast<int>(req->result),
wrap_->syscall(),
nullptr,
- req->path,
- wrap_->data());
+ is_mkstemp ? wrap_->data() : req->path,
+ is_mkstemp ? nullptr : wrap_->data());
Clear();
wrap->Reject(exception);
}
@@ -892,6 +895,41 @@ void AfterOpenFileHandle(uv_fs_t* req) {
}
}
+// Delivers the result of mkstemp(): [path, fd], or [path, FileHandle].
+static void AfterMkstempImpl(uv_fs_t* req, bool as_file_handle) {
+ FSReqBase* req_wrap = FSReqBase::from_req(req);
+ FSReqAfterScope after(req_wrap, req);
+ FS_ASYNC_TRACE_END1(
+ req->fs_type, req_wrap, "result", static_cast<int>(req->result))
+ const int fd = static_cast<int>(req->result);
+ if (after.Proceed()) {
+ Environment* env = req_wrap->env();
+ return ResolveMkstemp(
+ req_wrap,
+ fd,
+ [&]() {
+ return StringBytes::Encode(
+ env->isolate(), req->path, req_wrap->encoding());
+ },
+ [&]() {
+ if (!as_file_handle) return MkstempFd(env, fd);
+ return MkstempFileHandle([&]() {
+ return FileHandle::New(req_wrap->binding_data(), fd, {}, req->path);
+ });
+ });
+ }
+ // The environment is shutting down: nothing will receive the descriptor.
+ if (fd >= 0) CloseMkstempFd(fd);
+}
+
+static void AfterMkstemp(uv_fs_t* req) {
+ AfterMkstempImpl(req, false);
+}
+
+static void AfterMkstempFileHandle(uv_fs_t* req) {
+ AfterMkstempImpl(req, true);
+}
+
void AfterMkdirp(uv_fs_t* req) {
FSReqBase* req_wrap = FSReqBase::from_req(req);
FSReqAfterScope after(req_wrap, req);
@@ -4410,6 +4448,15 @@ static void LUTimes(const FunctionCallbackInfo<Value>& args) {
}
}
+// Appends the placeholder that mkdtemp() and mkstemp() replace.
+static void AppendTemplateSuffix(BufferValue* tmpl) {
+ static constexpr std::string_view suffix = "XXXXXX";
+ const auto prefix_length = tmpl->length();
+ tmpl->AllocateSufficientStorage(prefix_length + suffix.size() + 1);
+ memcpy(tmpl->out() + prefix_length, suffix.data(), suffix.size());
+ tmpl->SetLengthAndZeroTerminate(prefix_length + suffix.size());
+}
+
static void Mkdtemp(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
@@ -4418,11 +4465,7 @@ static void Mkdtemp(const FunctionCallbackInfo<Value>& args) {
CHECK_GE(argc, 2);
BufferValue tmpl(isolate, args[0]);
- const auto prefix_length = tmpl.length();
- static constexpr std::string_view suffix = "XXXXXX";
- tmpl.AllocateSufficientStorage(prefix_length + suffix.size() + 1);
- memcpy(tmpl.out() + prefix_length, suffix.data(), suffix.size());
- tmpl.SetLengthAndZeroTerminate(prefix_length + suffix.size());
+ AppendTemplateSuffix(&tmpl);
CHECK_NOT_NULL(*tmpl);
@@ -4467,6 +4510,67 @@ static void Mkdtemp(const FunctionCallbackInfo<Value>& args) {
}
}
+// mkstemp() creates the file and opens it for reading and writing, so it
+// needs the same permissions as the equivalent open().
+static constexpr int kMkstempFlags =
+ UV_FS_O_RDWR | UV_FS_O_CREAT | UV_FS_O_EXCL;
+
+static void Mkstemp(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Isolate* isolate = env->isolate();
+
+ const int argc = args.Length();
+ CHECK_GE(argc, 2);
+
+ BufferValue tmpl(isolate, args[0]);
+ AppendTemplateSuffix(&tmpl);
+ CHECK_NOT_NULL(*tmpl);
+
+ const enum encoding encoding = ParseEncoding(isolate, args[1], UTF8);
+
+ if (argc > 2) { // mkstemp(tmpl, encoding, req)
+ FSReqBase* req_wrap_async = GetReqWrap(args, 2);
+ CHECK_NOT_NULL(req_wrap_async);
+ if (AsyncCheckOpenPermissions(env, req_wrap_async, tmpl, kMkstempFlags)
+ .IsNothing()) {
+ return;
+ }
+ // The callback API gets a file descriptor, the promise API a FileHandle.
+ uv_fs_cb after =
+ args[2]->IsObject() ? AfterMkstemp : AfterMkstempFileHandle;
+ FS_ASYNC_TRACE_BEGIN1(
+ UV_FS_MKSTEMP, req_wrap_async, "path", TRACE_STR_COPY(*tmpl))
+ // The template is saved in the request to report it if mkstemp() fails.
+ AsyncDestCall(env,
+ req_wrap_async,
+ args,
+ "mkstemp",
+ *tmpl,
+ tmpl.length(),
+ encoding,
+ after,
+ uv_fs_mkstemp,
+ *tmpl);
+ } else { // mkstemp(tmpl, encoding)
+ if (CheckOpenPermissions(env, tmpl, kMkstempFlags).IsNothing()) return;
+ FSReqWrapSync req_wrap_sync("mkstemp", *tmpl);
+ FS_SYNC_TRACE_BEGIN(mkstemp);
+ int fd = SyncCallAndThrowOnError(env, &req_wrap_sync, uv_fs_mkstemp, *tmpl);
+ FS_SYNC_TRACE_END(mkstemp);
+ if (is_uv_error(fd)) {
+ return;
+ }
+ MaybeLocal<Value> result = MkstempResult(
+ isolate,
+ fd,
+ [&]() {
+ return StringBytes::Encode(isolate, req_wrap_sync.req.path, encoding);
+ },
+ [&]() { return MkstempFd(env, fd); });
+ args.GetReturnValue().Set(result.FromMaybe(Local<Value>()));
+ }
+}
+
static void GetFormatOfExtensionlessFile(
const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 1);
@@ -5676,6 +5780,7 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
SetMethod(isolate, target, "lutimes", LUTimes);
SetMethod(isolate, target, "mkdtemp", Mkdtemp);
+ SetMethod(isolate, target, "mkstemp", Mkstemp);
#ifdef _WIN32
SetMethod(isolate, target, "handleToFd", HandleToFd);
@@ -5837,6 +5942,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(LUTimes);
registry->Register(Mkdtemp);
+ registry->Register(Mkstemp);
#ifdef _WIN32
registry->Register(HandleToFd);
#endif
diff --git a/test/cctest/test_node_file_mkstemp.cc b/test/cctest/test_node_file_mkstemp.cc
new file mode 100644
index 00000000000..a747580b003
--- /dev/null
+++ b/test/cctest/test_node_file_mkstemp.cc
@@ -0,0 +1,298 @@
+#include "base_object-inl.h"
+#include "env-inl.h"
+#include "gtest/gtest.h"
+#include "node_file-inl.h"
+#include "node_test_fixture.h"
+
+#include <string>
+
+// The branches taken when the path returned by mkstemp() cannot be encoded
+// cannot be reached from JavaScript, so they are tested here.
+
+namespace {
+
+using node::fs::BindingData;
+using node::fs::FSReqBase;
+using node::fs::MkstempFd;
+using node::fs::MkstempFileHandle;
+using node::fs::MkstempResult;
+using node::fs::ResolveMkstemp;
+using v8::Array;
+using v8::Exception;
+using v8::FunctionCallbackInfo;
+using v8::Global;
+using v8::HandleScope;
+using v8::Isolate;
+using v8::Local;
+using v8::MaybeLocal;
+using v8::Object;
+using v8::ObjectTemplate;
+using v8::String;
+using v8::TryCatch;
+using v8::Value;
+
+class NodeFileMkstempTest : public EnvironmentTestFixture {};
+
+// Records how the request was settled. With `throw_on_settle`, settling
+// throws, like a JS callback that throws would.
+class MkstempTestReq final : public FSReqBase {
+ public:
+ MkstempTestReq(BindingData* binding_data, Local<Object> object)
+ : FSReqBase(binding_data,
+ object,
+ node::AsyncWrap::PROVIDER_FSREQCALLBACK,
+ false) {}
+
+ void Resolve(Local<Value> value) override {
+ resolved_.Reset(env()->isolate(), value);
+ MaybeThrow();
+ }
+
+ void Reject(Local<Value> error) override {
+ rejected_.Reset(env()->isolate(), error);
+ MaybeThrow();
+ }
+
+ void ResolveStat(const uv_stat_t*) override {}
+ void ResolveStatFs(const uv_statfs_t*) override {}
+ void SetReturnValue(const FunctionCallbackInfo<Value>&) override {}
+
+ SET_MEMORY_INFO_NAME(MkstempTestReq)
+ SET_SELF_SIZE(MkstempTestReq)
+
+ Local<Value> resolved() const { return resolved_.Get(env()->isolate()); }
+ Local<Value> rejected() const { return rejected_.Get(env()->isolate()); }
+
+ bool throw_on_settle = false;
+
+ private:
+ void MaybeThrow() {
+ if (!throw_on_settle) return;
+ Isolate* isolate = env()->isolate();
+ isolate->ThrowException(
+ Exception::Error(String::NewFromUtf8Literal(isolate, "settle")));
+ }
+
+ Global<Value> resolved_;
+ Global<Value> rejected_;
+};
+
+struct TempFile {
+ TempFile() {
+ char dir[4096];
+ size_t size = sizeof(dir);
+ CHECK_EQ(0, uv_os_tmpdir(dir, &size));
+ const std::string tmpl = std::string(dir, size) + "/node-cctest-XXXXXX";
+ uv_fs_t req;
+ fd = uv_fs_mkstemp(nullptr, &req, tmpl.c_str(), nullptr);
+ CHECK_GE(fd, 0);
+ path = req.path;
+ uv_fs_req_cleanup(&req);
+ }
+
+ ~TempFile() {
+ uv_fs_t req;
+ if (!IsClosed()) {
+ uv_fs_close(nullptr, &req, fd, nullptr);
+ uv_fs_req_cleanup(&req);
+ }
+ uv_fs_unlink(nullptr, &req, path.c_str(), nullptr);
+ uv_fs_req_cleanup(&req);
+ }
+
+ bool IsClosed() const {
+ uv_fs_t req;
+ const int err = uv_fs_fstat(nullptr, &req, fd, nullptr);
+ uv_fs_req_cleanup(&req);
+ return err == UV_EBADF;
+ }
+
+ int fd;
+ std::string path;
+};
+
+BindingData* GetBindingData(node::Environment* env) {
+ BindingData* binding_data =
+ env->principal_realm()->GetBindingData<BindingData>();
+ CHECK_NOT_NULL(binding_data);
+ return binding_data;
+}
+
+node::BaseObjectPtr<MkstempTestReq> NewReq(node::Environment* env) {
+ Local<ObjectTemplate> object_template = ObjectTemplate::New(env->isolate());
+ object_template->SetInternalFieldCount(FSReqBase::kInternalFieldCount);
+ Local<Object> object =
+ object_template->NewInstance(env->context()).ToLocalChecked();
+ return node::MakeDetachedBaseObject<MkstempTestReq>(GetBindingData(env),
+ object);
+}
+
+MaybeLocal<Value> Path(Isolate* isolate) {
+ return String::NewFromUtf8Literal(isolate, "path");
+}
+
+MaybeLocal<Value> Throw(Isolate* isolate, Local<Value> error) {
+ isolate->ThrowException(error);
+ return {};
+}
+
+Local<Value> NewError(Isolate* isolate) {
+ return Exception::Error(String::NewFromUtf8Literal(isolate, "expected"));
+}
+
+TEST_F(NodeFileMkstempTest, ResultHoldsPathAndFile) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ TempFile file;
+
+ Local<Value> result;
+ ASSERT_TRUE(MkstempResult(
+ isolate_,
+ file.fd,
+ [&]() { return Path(isolate_); },
+ [&]() { return MkstempFd(*env, file.fd); })
+ .ToLocal(&result));
+ ASSERT_TRUE(result->IsArray());
+ Local<Array> array = result.As<Array>();
+ EXPECT_EQ(array->Length(), 2U);
+ Local<Value> fd = array->Get(env.context(), 1).ToLocalChecked();
+ EXPECT_EQ(fd->Int32Value(env.context()).FromJust(), file.fd);
+ EXPECT_FALSE(file.IsClosed());
+}
+
+TEST_F(NodeFileMkstempTest, ResultClosesDescriptorIfEncodingThrows) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ TempFile file;
+ bool made_file = false;
+
+ TryCatch try_catch(isolate_);
+ EXPECT_TRUE(MkstempResult(
+ isolate_,
+ file.fd,
+ [&]() { return Throw(isolate_, NewError(isolate_)); },
+ [&]() {
+ made_file = true;
+ return MkstempFd(*env, file.fd);
+ })
+ .IsEmpty());
+ EXPECT_TRUE(try_catch.HasCaught());
+ EXPECT_FALSE(made_file);
+ EXPECT_TRUE(file.IsClosed());
+}
+
+TEST_F(NodeFileMkstempTest, ResultClosesDescriptorIfMakingFileThrows) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ TempFile file;
+
+ TryCatch try_catch(isolate_);
+ EXPECT_TRUE(MkstempResult(
+ isolate_,
+ file.fd,
+ [&]() { return Path(isolate_); },
+ [&]() { return Throw(isolate_, NewError(isolate_)); })
+ .IsEmpty());
+ EXPECT_TRUE(try_catch.HasCaught());
+ EXPECT_TRUE(file.IsClosed());
+}
+
+TEST_F(NodeFileMkstempTest, FileHandleFailureIsReported) {
+ const HandleScope handle_scope(isolate_);
+ EXPECT_TRUE(MkstempFileHandle([]() -> node::fs::FileHandle* {
+ return nullptr;
+ }).IsEmpty());
+}
+
+TEST_F(NodeFileMkstempTest, ResolveDeliversResult) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ node::LoadEnvironment(*env, "require('fs');");
+ TempFile file;
+ auto req = NewReq(*env);
+
+ ResolveMkstemp(
+ req.get(),
+ file.fd,
+ [&]() { return Path(isolate_); },
+ [&]() { return MkstempFd(*env, file.fd); });
+
+ EXPECT_TRUE(req->rejected().IsEmpty());
+ ASSERT_FALSE(req->resolved().IsEmpty());
+ EXPECT_TRUE(req->resolved()->IsArray());
+ EXPECT_FALSE(file.IsClosed());
+}
+
+TEST_F(NodeFileMkstempTest, ResolveRejectsAndClosesDescriptorOnException) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ node::LoadEnvironment(*env, "require('fs');");
+ TempFile file;
+ auto req = NewReq(*env);
+ Local<Value> expected = NewError(isolate_);
+
+ TryCatch try_catch(isolate_);
+ ResolveMkstemp(
+ req.get(),
+ file.fd,
+ [&]() { return Throw(isolate_, expected); },
+ [&]() { return MkstempFd(*env, file.fd); });
+
+ EXPECT_FALSE(try_catch.HasCaught());
+ EXPECT_TRUE(req->resolved().IsEmpty());
+ ASSERT_FALSE(req->rejected().IsEmpty());
+ EXPECT_TRUE(req->rejected()->StrictEquals(expected));
+ EXPECT_TRUE(file.IsClosed());
+}
+
+// An exception thrown while the request is settled, as a JS callback could,
+// must not be caught by the helper.
+TEST_F(NodeFileMkstempTest, ExceptionThrownByResolveEscapes) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ node::LoadEnvironment(*env, "require('fs');");
+ TempFile file;
+ auto req = NewReq(*env);
+ req->throw_on_settle = true;
+
+ TryCatch try_catch(isolate_);
+ ResolveMkstemp(
+ req.get(),
+ file.fd,
+ [&]() { return Path(isolate_); },
+ [&]() { return MkstempFd(*env, file.fd); });
+
+ EXPECT_FALSE(req->resolved().IsEmpty());
+ EXPECT_TRUE(try_catch.HasCaught());
+}
+
+TEST_F(NodeFileMkstempTest, ExceptionThrownByRejectEscapes) {
+ const HandleScope handle_scope(isolate_);
+ const Argv argv;
+ Env env{handle_scope, argv};
+ node::LoadEnvironment(*env, "require('fs');");
+ TempFile file;
+ auto req = NewReq(*env);
+ req->throw_on_settle = true;
+ Local<Value> expected = NewError(isolate_);
+
+ TryCatch try_catch(isolate_);
+ ResolveMkstemp(
+ req.get(),
+ file.fd,
+ [&]() { return Throw(isolate_, expected); },
+ [&]() { return MkstempFd(*env, file.fd); });
+
+ ASSERT_FALSE(req->rejected().IsEmpty());
+ EXPECT_TRUE(req->rejected()->StrictEquals(expected));
+ ASSERT_TRUE(try_catch.HasCaught());
+ EXPECT_FALSE(try_catch.Exception()->StrictEquals(expected));
+}
+
+} // namespace
diff --git a/test/fixtures/permission/fs-read.js b/test/fixtures/permission/fs-read.js
index 511c9bf027b..59900f8f03f 100644
--- a/test/fixtures/permission/fs-read.js
+++ b/test/fixtures/permission/fs-read.js
@@ -255,6 +255,20 @@ const regularFile = __filename;
}));
}
+// fs.mkstemp: the file is opened for reading and writing
+{
+ const expected = {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemRead',
+ };
+ assert.throws(() => {
+ fs.mkstempSync(path.join(blockedFolder, 'any-file'));
+ }, expected);
+ fs.mkstemp(path.join(blockedFolder, 'any-file'), common.expectsError(expected));
+ assert.rejects(fs.promises.mkstemp(path.join(blockedFolder, 'any-file')), expected)
+ .then(common.mustCall());
+}
+
// fs.opendir
{
fs.opendir(blockedFolder, common.expectsError({
diff --git a/test/fixtures/permission/fs-write.js b/test/fixtures/permission/fs-write.js
index 6771485679c..7a9963e17d9 100644
--- a/test/fixtures/permission/fs-write.js
+++ b/test/fixtures/permission/fs-write.js
@@ -230,6 +230,26 @@ const relativeProtectedFolder = process.env.RELATIVEBLOCKEDFOLDER;
});
}
+// fs.mkstemp
+{
+ assert.throws(() => {
+ fs.mkstempSync(path.join(blockedFolder, 'any-file'));
+ },{
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ });
+ fs.mkstemp(path.join(relativeProtectedFolder, 'any-file'), common.expectsError({
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ }));
+ assert.rejects(async () => {
+ await fsPromises.mkstemp(path.join(blockedFolder, 'any-file'));
+ }, {
+ code: 'ERR_ACCESS_DENIED',
+ permission: 'FileSystemWrite',
+ });
+}
+
// fs.rename
{
assert.throws(() => {
diff --git a/test/parallel/test-fs-assert-encoding-error.js b/test/parallel/test-fs-assert-encoding-error.js
index 9b22e042c52..2ac0bd0021b 100644
--- a/test/parallel/test-fs-assert-encoding-error.js
+++ b/test/parallel/test-fs-assert-encoding-error.js
@@ -71,6 +71,14 @@ assert.throws(() => {
fs.mkdtempSync(testPath, options);
}, expectedError);
+assert.throws(() => {
+ fs.mkstemp(testPath, options, common.mustNotCall());
+}, expectedError);
+
+assert.throws(() => {
+ fs.mkstempSync(testPath, options);
+}, expectedError);
+
assert.throws(() => {
fs.ReadStream(testPath, options);
}, expectedError);
diff --git a/test/parallel/test-fs-mkdtemp.js b/test/parallel/test-fs-mkdtemp.js
index 3c2323440e0..76555cd9cf6 100644
--- a/test/parallel/test-fs-mkdtemp.js
+++ b/test/parallel/test-fs-mkdtemp.js
@@ -32,7 +32,8 @@ function handler(err, folder) {
// affect the way the callback function is handled.
fs.mkdtemp(tmpdir.resolve('bar.'), {}, common.mustCall(handler));
- const warningMsg = 'mkdtemp() templates ending with X are not portable. ' +
+ const warningMsg = 'mkdtemp() and mkstemp() templates ending with X are ' +
+ 'not portable. ' +
'For details see: https://nodejs.org/api/fs.html';
common.expectWarning('Warning', warningMsg);
fs.mkdtemp(tmpdir.resolve('bar.X'), common.mustCall(handler));
diff --git a/test/parallel/test-fs-mkstemp.js b/test/parallel/test-fs-mkstemp.js
new file mode 100644
index 00000000000..bbca38b56db
--- /dev/null
+++ b/test/parallel/test-fs-mkstemp.js
@@ -0,0 +1,110 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { pathToFileURL } = require('url');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+function checkPath(file, prefix) {
+ const name = path.basename(file);
+ assert.strictEqual(path.dirname(file), tmpdir.path);
+ assert.strictEqual(name.length, prefix.length + 6);
+ assert.ok(name.startsWith(prefix));
+}
+
+function checkFile({ path: file, fd }, prefix) {
+ checkPath(file, prefix);
+ assert.strictEqual(fs.writeSync(fd, 'hello'), 5);
+ if (!common.isWindows) {
+ assert.strictEqual(fs.fstatSync(fd).mode & 0o777, 0o600);
+ }
+ fs.closeSync(fd);
+ assert.strictEqual(fs.readFileSync(file, 'utf8'), 'hello');
+}
+
+// Sync
+{
+ const first = fs.mkstempSync(tmpdir.resolve('sync-'));
+ const second = fs.mkstempSync(tmpdir.resolve('sync-'));
+ assert.notStrictEqual(first.path, second.path);
+ checkFile(first, 'sync-');
+ checkFile(second, 'sync-');
+
+ checkFile(fs.mkstempSync(pathToFileURL(tmpdir.resolve('url-'))), 'url-');
+
+ for (const result of [
+ fs.mkstempSync(tmpdir.resolve('buffer-'), 'buffer'),
+ fs.mkstempSync(tmpdir.resolve('buffer-'), { encoding: 'buffer' }),
+ fs.mkstempSync(Buffer.from(tmpdir.resolve('buffer-'))),
+ ]) {
+ assert.ok(Buffer.isBuffer(result.path));
+ checkFile({ path: result.path.toString(), fd: result.fd }, 'buffer-');
+ }
+}
+
+// Callback
+{
+ fs.mkstemp(tmpdir.resolve('callback-'), common.mustSucceed((file) => {
+ checkFile(file, 'callback-');
+ }));
+
+ fs.mkstemp(tmpdir.resolve('callback-'), 'buffer', common.mustSucceed((file) => {
+ assert.ok(Buffer.isBuffer(file.path));
+ checkFile({ path: file.path.toString(), fd: file.fd }, 'callback-');
+ }));
+}
+
+// Promises
+(async () => {
+ const { path: file, handle } = await fs.promises.mkstemp(tmpdir.resolve('promise-'));
+ checkPath(file, 'promise-');
+ await handle.writeFile('hello');
+ await handle.close();
+ assert.strictEqual(fs.readFileSync(file, 'utf8'), 'hello');
+
+ const result = await fs.promises.mkstemp(Buffer.from(tmpdir.resolve('promise-')));
+ assert.ok(Buffer.isBuffer(result.path));
+ await result.handle.close();
+})().then(common.mustCall());
+
+// Errors report the template.
+{
+ const prefix = tmpdir.resolve('missing', 'error-');
+ const expected = {
+ code: 'ENOENT',
+ syscall: 'mkstemp',
+ path: `${prefix}XXXXXX`,
+ message: `ENOENT: no such file or directory, mkstemp '${prefix}XXXXXX'`,
+ };
+
+ assert.throws(() => fs.mkstempSync(prefix), expected);
+ fs.mkstemp(prefix, common.mustCall((err, file) => {
+ assert.throws(() => { throw err; }, expected);
+ assert.strictEqual(file, undefined);
+ }));
+ assert.rejects(fs.promises.mkstemp(prefix), expected).then(common.mustCall());
+}
+
+// Invalid arguments
+{
+ for (const prefix of [undefined, null, 1, {}, true]) {
+ const expected = { code: 'ERR_INVALID_ARG_TYPE', name: 'TypeError' };
+ assert.throws(() => fs.mkstempSync(prefix), expected);
+ assert.throws(() => fs.mkstemp(prefix, common.mustNotCall()), expected);
+ assert.rejects(fs.promises.mkstemp(prefix), expected).then(common.mustCall());
+ }
+
+ assert.throws(() => fs.mkstemp(tmpdir.resolve('no-callback-')), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ name: 'TypeError',
+ });
+
+ const withNull = { code: 'ERR_INVALID_ARG_VALUE', name: 'TypeError' };
+ assert.throws(() => fs.mkstempSync('fhqwhgads\u0000'), withNull);
+ assert.throws(() => fs.mkstemp('fhqwhgads\u0000', common.mustNotCall()), withNull);
+ assert.rejects(fs.promises.mkstemp('fhqwhgads\u0000'), withNull).then(common.mustCall());
+}
diff --git a/test/parallel/test-fs-options-immutable.js b/test/parallel/test-fs-options-immutable.js
index fffdecdc87c..9c1290afdce 100644
--- a/test/parallel/test-fs-options-immutable.js
+++ b/test/parallel/test-fs-options-immutable.js
@@ -62,6 +62,14 @@ if (!common.isIBMi) { // IBMi does not support fs.watch()
fs.mkdtemp(tempFileName, options, common.mustSucceed());
}
+{
+ const tempFileName = tmpdir.resolve('mkstemp-');
+ fs.closeSync(fs.mkstempSync(tempFileName, options).fd);
+ fs.mkstemp(tempFileName, options, common.mustSucceed(({ fd }) => {
+ fs.closeSync(fd);
+ }));
+}
+
{
const fileName = tmpdir.resolve('streams');
fs.WriteStream(fileName, options).once('open', common.mustCall(() => {
diff --git a/test/parallel/test-permission-fs-supported.js b/test/parallel/test-permission-fs-supported.js
index 5da01d338c4..df1ef82de10 100644
--- a/test/parallel/test-permission-fs-supported.js
+++ b/test/parallel/test-permission-fs-supported.js
@@ -39,6 +39,7 @@ const supportedApis = [
...syncAndAsyncAPI('openAsBlob'),
...syncAndAsyncAPI('mkdtemp'),
'mkdtempDisposableSync',
+ ...syncAndAsyncAPI('mkstemp'),
...syncAndAsyncAPI('readdir'),
...syncAndAsyncAPI('readFile'),
...syncAndAsyncAPI('readlink'),
diff --git a/test/parallel/test-trace-events-fs-async.js b/test/parallel/test-trace-events-fs-async.js
index 30233aeb735..989e5ddc106 100644
--- a/test/parallel/test-trace-events-fs-async.js
+++ b/test/parallel/test-trace-events-fs-async.js
@@ -178,6 +178,14 @@ function mktmp() {
});
}
+function mkstemp() {
+ const fs = require('fs');
+ fs.mkstemp('fstemp2', (err, file) => {
+ fs.closeSync(file.fd);
+ fs.unlinkSync(file.path);
+ });
+}
+
function open() {
const fs = require('fs');
fs.open('fs16.txt', 'w', (err, fd) => {
@@ -296,6 +304,7 @@ tests.link = wrapper(link);
tests.lstat = wrapper(lstat);
tests.mkdir = wrapper(mkdir);
tests.mkdtemp = wrapper(mktmp);
+tests.mkstemp = wrapper(mkstemp);
tests.open = wrapper(open);
tests.read = wrapper(read);
tests.scandir = wrapper(readdir);
diff --git a/test/parallel/test-vfs-mkstemp.js b/test/parallel/test-vfs-mkstemp.js
new file mode 100644
index 00000000000..5dd805e3643
--- /dev/null
+++ b/test/parallel/test-vfs-mkstemp.js
@@ -0,0 +1,93 @@
+// Flags: --experimental-vfs
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vfs = require('node:vfs');
+
+// VirtualFileSystem API
+{
+ const myVfs = vfs.create();
+ const first = myVfs.mkstempSync('/tmp-');
+ const second = myVfs.mkstempSync('/tmp-');
+ assert.notStrictEqual(first.path, second.path);
+ assert.ok(first.path.startsWith('/tmp-'));
+ assert.strictEqual(first.path.length, '/tmp-'.length + 6);
+ assert.ok(myVfs.statSync(first.path).isFile());
+ myVfs.writeSync(first.fd, Buffer.from('hello'), 0, 5, 0);
+ myVfs.closeSync(first.fd);
+ myVfs.closeSync(second.fd);
+ assert.strictEqual(myVfs.readFileSync(first.path, 'utf8'), 'hello');
+
+ myVfs.mkstemp('/tmp-', common.mustSucceed(({ path, fd }) => {
+ assert.ok(path.startsWith('/tmp-'));
+ myVfs.closeSync(fd);
+ }));
+ myVfs.mkstemp('/tmp-', {}, common.mustSucceed(({ fd }) => myVfs.closeSync(fd)));
+ myVfs.mkstemp('/missing/prefix-', common.expectsError({ code: 'ENOENT' }));
+
+ myVfs.promises.mkstemp('/tmp-').then(common.mustCall(({ path, fd }) => {
+ assert.ok(path.startsWith('/tmp-'));
+ myVfs.closeSync(fd);
+ }));
+ assert.rejects(myVfs.promises.mkstemp('/missing/prefix-'), { code: 'ENOENT' })
+ .then(common.mustCall());
+}
+
+// node:fs on a mounted path
+{
+ const layer = vfs.create();
+ layer.mkdirSync('/d');
+ const dir = path.join(layer.mount(), 'd');
+ const prefix = path.join(dir, 'f-');
+
+ function check(file) {
+ assert.strictEqual(path.dirname(file), dir);
+ assert.strictEqual(path.basename(file).length, 'f-'.length + 6);
+ assert.strictEqual(fs.readFileSync(file, 'utf8'), 'hello');
+ }
+
+ {
+ const { path: file, fd } = fs.mkstempSync(prefix);
+ fs.writeSync(fd, 'hello');
+ fs.closeSync(fd);
+ check(file);
+ }
+
+ for (const args of [[prefix, 'buffer'], [prefix, { encoding: 'buffer' }], [Buffer.from(prefix)]]) {
+ const { path: file, fd } = fs.mkstempSync(...args);
+ assert.ok(Buffer.isBuffer(file));
+ fs.writeSync(fd, 'hello');
+ fs.closeSync(fd);
+ check(file.toString());
+ }
+
+ fs.mkstemp(prefix, common.mustSucceed(({ path: file, fd }) => {
+ fs.writeSync(fd, 'hello');
+ fs.closeSync(fd);
+ check(file);
+ }));
+
+ fs.mkstemp(prefix, 'buffer', common.mustSucceed(({ path: file, fd }) => {
+ assert.ok(Buffer.isBuffer(file));
+ fs.closeSync(fd);
+ }));
+
+ (async () => {
+ const { path: file, handle } = await fs.promises.mkstemp(prefix);
+ await handle.writeFile('hello');
+ await handle.close();
+ check(file);
+
+ const result = await fs.promises.mkstemp(Buffer.from(prefix));
+ assert.ok(Buffer.isBuffer(result.path));
+ await result.handle.close();
+ })().then(common.mustCall());
+
+ const missing = path.join(dir, 'missing', 'f-');
+ assert.throws(() => fs.mkstempSync(missing), { code: 'ENOENT' });
+ fs.mkstemp(missing, common.expectsError({ code: 'ENOENT' }));
+ assert.rejects(fs.promises.mkstemp(missing), { code: 'ENOENT' }).then(common.mustCall());
+}
diff --git a/test/parallel/test-worker-fs-mkstemp-terminate.js b/test/parallel/test-worker-fs-mkstemp-terminate.js
new file mode 100644
index 00000000000..01c761adb2e
--- /dev/null
+++ b/test/parallel/test-worker-fs-mkstemp-terminate.js
@@ -0,0 +1,35 @@
+'use strict';
+
+// A file created by mkstemp() must be closed if the worker terminates before
+// the result can be delivered.
+
+const common = require('../common');
+if (!common.isLinux) common.skip('counts descriptors through /proc/self/fd');
+
+const assert = require('assert');
+const fs = require('fs');
+const { once } = require('events');
+const { Worker } = require('worker_threads');
+const tmpdir = require('../common/tmpdir');
+
+tmpdir.refresh();
+
+const countFds = () => fs.readdirSync('/proc/self/fd').length;
+
+(async () => {
+ const before = countFds();
+ for (const method of ['fs.mkstemp', 'fs.promises.mkstemp']) {
+ const worker = new Worker(`
+ const fs = require('fs');
+ const { parentPort } = require('worker_threads');
+ for (let i = 0; i < 64; i++) {
+ const result = ${method}(${JSON.stringify(tmpdir.resolve('terminate-'))}, () => {});
+ result?.catch(() => {});
+ }
+ parentPort.postMessage('started');
+ `, { eval: true });
+ await once(worker, 'message');
+ await worker.terminate();
+ }
+ assert.strictEqual(countFds(), before);
+})().then(common.mustCall());
diff --git a/test/parallel/test-worker-track-unmanaged-fds.js b/test/parallel/test-worker-track-unmanaged-fds.js
index 05b54208191..8b7bd230ea4 100644
--- a/test/parallel/test-worker-track-unmanaged-fds.js
+++ b/test/parallel/test-worker-track-unmanaged-fds.js
@@ -50,6 +50,26 @@ process.on('warning', (warning) => parentPort.postMessage({ warning }));
assert.throws(() => fs.fstatSync(fd), { code: 'EBADF' });
}
+ // FDs created by mkstemp() are tracked as well.
+ {
+ const w = new Worker(`${preamble}
+ const tmpdir = require(${JSON.stringify(require.resolve('../common/tmpdir'))});
+ tmpdir.refresh();
+ const first = fs.mkstempSync(tmpdir.resolve('worker-'));
+ fs.closeSync(first.fd);
+ fs.mkstemp(tmpdir.resolve('worker-'), (err, second) => {
+ if (err) throw err;
+ fs.closeSync(second.fd);
+ const third = fs.mkstempSync(tmpdir.resolve('worker-'));
+ parentPort.postMessage({ fd: third.fd });
+ });
+ `, { eval: true, trackUnmanagedFds: true });
+ const [ [ message ] ] = await Promise.all([once(w, 'message'), once(w, 'exit')]);
+ assert.strictEqual(message.warning, undefined);
+ assert(message.fd > 2);
+ assert.throws(() => fs.fstatSync(message.fd), { code: 'EBADF' });
+ }
+
// There is a warning when an fd is unexpectedly opened twice.
{
const w = new Worker(`${preamble}
diff --git a/typings/internalBinding/fs.d.ts b/typings/internalBinding/fs.d.ts
index 6e1702996df..e2e04d4bc0c 100644
--- a/typings/internalBinding/fs.d.ts
+++ b/typings/internalBinding/fs.d.ts
@@ -146,6 +146,10 @@ declare namespace InternalFSBinding {
function mkdtemp(prefix: string, encoding: unknown, usePromises: typeof kUsePromises): Promise<string>;
function mkdtemp(prefix: string, encoding: unknown): string;
+ function mkstemp(prefix: StringOrBuffer, encoding: unknown, req: FSReqCallback<[StringOrBuffer, number]>): void;
+ function mkstemp(prefix: StringOrBuffer, encoding: unknown): [StringOrBuffer, number];
+ function mkstemp(prefix: StringOrBuffer, encoding: unknown, usePromises: typeof kUsePromises): Promise<[StringOrBuffer, FileHandle]>;
+
function mkdir(path: string, mode: number, recursive: boolean, req: FSReqCallback<void | string>): void;
function mkdir(path: string, mode: number, recursive: true, req: FSReqCallback<string>): void;
function mkdir(path: string, mode: number, recursive: false, req: FSReqCallback<void>): void;
@@ -277,6 +281,7 @@ export interface FsBinding {
lstat: typeof InternalFSBinding.lstat;
lutimes: typeof InternalFSBinding.lutimes;
mkdtemp: typeof InternalFSBinding.mkdtemp;
+ mkstemp: typeof InternalFSBinding.mkstemp;
mkdir: typeof InternalFSBinding.mkdir;
open: typeof InternalFSBinding.open;
openFileHandle: typeof InternalFSBinding.openFileHandle;