From a4b45bbc46f487e0e1a175fcbe2134e826f098b9 Mon Sep 17 00:00:00 2001 From: Jonathan M Davis Date: Tue, 17 Oct 2017 00:58:17 -0600 Subject: [PATCH] Issue 17912: Add function for creating a temporary file. This adds std.file.createTempFile, which creates a temporary file with a random name (optionally with a specified prefix and/or suffix). By default, the file is empty, but if data is passed to createTempFile, then the file is populated with that data just like it would have been with std.file.write. The name of the file that was created is returned. So, essentially, createTempFile is like write except that it generates the file name for you and returns it. Previously, we added scratchFile to std.stdio, which was like createTempFile except that it gave you an open File object, but it was scrapped, because the dependencies that it added to std.stdio made "hello world" bigger. See: https://issues.dlang.org/show_bug.cgi?id=14599 Just like scratchFile did, createTempFile ensures that a filename is selected which did not exist previously so that the file can't be hijacked with different permissions by someone creating a file with the same name beforehand (the randomness of the name should make that effectively impossible, but the way that createTempFile opens the file guarantees it). These changes also consolidate some of write's implementation across OSes, since the writing portion of writeImpl was esentially identical across OSes but needlessly duplicated. That consolidated functionality is then also used by createTempFile. --- changelog/std-file-createTempFile.dd | 15 ++ std/file.d | 217 +++++++++++++++++++++++---- 2 files changed, 205 insertions(+), 27 deletions(-) create mode 100644 changelog/std-file-createTempFile.dd diff --git a/changelog/std-file-createTempFile.dd b/changelog/std-file-createTempFile.dd new file mode 100644 index 00000000000..7b46645c671 --- /dev/null +++ b/changelog/std-file-createTempFile.dd @@ -0,0 +1,15 @@ +`createTempFile` added to std.file + +$(REF std, file, createTempFile) creates a file in $(REF tempDir, std, file) +with a random name (with an optionally provided prefix and suffix) and returns +the file name. It also optionally takes data to write to the file when creating +it (if no data is provided, then the newly created file will be empty). + +This is different from $(REF File.tmpfile, std, stdio), which creates a +temporary file with no name, returns a $(REF File, std, stdio) to that file, +and then deletes the file when the $(REF File, std, stdio) is closed. The file +created by $(REF std, file, createTempFile) is a normal file which can be +opened and closed as many times as desired and will only be deleted by the +program if the program explicitly does so (though it is sitting in a temp +directory and is subject to the normal things tha happen to such files - e.g. +some OSes delete those files on boot). diff --git a/std/file.d b/std/file.d index 429c1569b5e..d7bf5fbd4e1 100644 --- a/std/file.d +++ b/std/file.d @@ -35,6 +35,7 @@ $(TR $(TD Files) $(TD $(LREF remove) $(LREF slurp) $(LREF write) + $(LREF createTempFile) )) $(TR $(TD Symlinks) $(TD $(LREF symlink) @@ -809,8 +810,6 @@ if (isConvertibleToString!R) static assert(__traits(compiles, append(TestAliasedString("foo"), [0, 1, 2, 3]))); } -// Posix implementation helper for write and append - version(Posix) private void writeImpl(const(char)[] name, const(FSChar)* namez, in void[] buffer, bool append) @trusted { @@ -821,27 +820,9 @@ version(Posix) private void writeImpl(const(char)[] name, const(FSChar)* namez, : O_CREAT | O_WRONLY | O_TRUNC; immutable fd = core.sys.posix.fcntl.open(namez, mode, octal!666); - cenforce(fd != -1, name, namez); - { - scope(failure) core.sys.posix.unistd.close(fd); - - immutable size = buffer.length; - size_t sum, cnt = void; - while (sum != size) - { - cnt = (size - sum < 2^^30) ? (size - sum) : 2^^30; - const numwritten = core.sys.posix.unistd.write(fd, buffer.ptr + sum, cnt); - if (numwritten != cnt) - break; - sum += numwritten; - } - cenforce(sum == size, name, namez); - } - cenforce(core.sys.posix.unistd.close(fd) == 0, name, namez); + writeToOpenFile(fd, buffer, name, namez); } -// Windows implementation helper for write and append - version(Windows) private void writeImpl(const(char)[] name, const(FSChar)* namez, in void[] buffer, bool append) @trusted { @@ -868,20 +849,202 @@ version(Windows) private void writeImpl(const(char)[] name, const(FSChar)* namez h = CreateFileW(namez, defaults); cenforce(h != INVALID_HANDLE_VALUE, name, namez); } + writeToOpenFile(h, buffer, name, namez); +} + +private void writeToOpenFile(FD)(FD fd, const void[] buffer, const(char)[] name, + const(FSChar)* namez) @trusted +{ immutable size = buffer.length; size_t sum, cnt = void; - DWORD numwritten = void; + while (sum != size) { - cnt = (size - sum < 2^^30) ? (size - sum) : 2^^30; - WriteFile(h, buffer.ptr + sum, cast(uint) cnt, &numwritten, null); - if (numwritten != cnt) + cnt = size - sum < 2^^30 ? size - sum : 2^^30; + + version(Posix) + immutable numWritten = core.sys.posix.unistd.write(fd, buffer.ptr + sum, cnt); + else version(Windows) + { + DWORD numWritten = void; + WriteFile(fd, buffer.ptr + sum, cast(uint) cnt, &numWritten, null); + } + else + static assert(0, "Unsupported OS"); + + if (numWritten != cnt) break; - sum += numwritten; + sum += numWritten; } - cenforce(sum == size && CloseHandle(h), name, namez); + + version(Posix) + cenforce(sum == size && core.sys.posix.unistd.close(fd) == 0, name, namez); + else version(Windows) + cenforce(sum == size && CloseHandle(fd), name, namez); + else + static assert(0, "Unsupported OS"); } + +/++ + Creates a file with a random name in $(LREF tempDir) and returns the name + of the file. If data is passed in, then the file is written with that data; + otherwise, it's empty. + + The idea is that this creates a temporary file for testing or whatever + other purpose a temporary file might be needed for. However, it will only + be deleted if it's explicitly deleted or if the OS decides to delete it as + some OSes do with files in temp directories on startup or shutdown. So, + unlike $(REF File.tmpfile, std, stdio), the file has a name, and it can have + all of the things done to it that one might typically do with a file + (including reopening it after closing it). + + The file is created with R/W permissions. On POSIX systems, the permissions + are restricted to the current user, though the effective permissions are + modified by the process' umask in the usual way. + + $(REF rndGen, std, random) is used as the random number generator. + + Params: + prefix = Prefix to the random portion of the file name. + suffix = Suffix to the random portion of the file name (e.g. for a file + extension). + buffer = Data to populate the file with. + + Returns: + The name of the generated file. + + Throws: + $(LREF FileException) if it fails to create the file. + + See_Also: + $(REF File.tmpfile, std, stdio) + +/ +string createTempFile(const void[] buffer = null) @safe +{ + return createTempFile(null, null, buffer); +} + +/// Ditto +string createTempFile(const(char)[] prefix, const(char)[] suffix, const void[] buffer = null) @trusted +{ + import std.path : absolutePath, baseName, buildPath, dirName; + + static string genTempName(const(char)[] prefix, const(char)[] suffix) + { + import std.ascii : digits, letters; + import std.random : choice, rndGen; + import std.range : chain; + import std.string : representation; + + auto name = new char[](prefix.length + 15 + suffix.length); + name[0 .. prefix.length] = prefix; + name[$ - suffix.length .. $] = suffix; + + auto random = &rndGen(); + rndGen.popFront(); + + auto chars = chain(letters.representation, digits.representation); + foreach (ref c; name[prefix.length .. $ - suffix.length]) + { + c = choice(chars); + random.popFront(); + } + + return buildPath(tempDir, name); + } + + while (1) + { + auto filename = genTempName(prefix, suffix); + + version(Posix) + { + auto fd = open(tempCString!FSChar(filename), O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); + if (fd == -1) + { + immutable errno = .errno; + if (errno == EEXIST) + continue; + else + throw new FileException("Failed to create a temporary file", errno); + } + } + else version(Windows) + { + auto fd = CreateFileW(tempCString!FSChar(filename), GENERIC_WRITE, 0, null, CREATE_NEW, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, HANDLE.init); + if (fd == INVALID_HANDLE_VALUE) + { + immutable errno = .GetLastError(); + if (errno == ERROR_FILE_EXISTS) + continue; + else + throw new FileException("Failed to create a temporary file", errno); + } + } + else + static assert(0, "Unsupported OS"); + + writeToOpenFile(fd, buffer, filename, null); + + return filename; + } +} + +/// +@safe unittest +{ + import std.algorithm.searching : startsWith; + import std.range.primitives; + import std.path : baseName, buildNormalizedPath, dirName, extension; + + { + auto path = createTempFile(); + scope(exit) if (path.exists) remove(path); + assert(!path.empty); + assert(buildNormalizedPath(path.dirName) == buildNormalizedPath(tempDir)); + assert(!path.baseName.empty); + assert(path.exists); + assert(path.isFile); + assert(readText(path).empty); + } + { + auto path = createTempFile("hello world"); + scope(exit) if (path.exists) remove(path); + assert(!path.empty); + assert(buildNormalizedPath(path.dirName) == buildNormalizedPath(tempDir)); + assert(!path.baseName.empty); + assert(path.exists); + assert(path.isFile); + assert(readText(path) == "hello world"); + } + { + auto path = createTempFile("prefix_", ".txt"); + scope(exit) if (path.exists) remove(path); + auto name = path.baseName; + assert(name.startsWith("prefix_")); + assert(name.extension == ".txt"); + assert(name.length > "prefix_.txt".length); + assert(buildNormalizedPath(path.dirName) == buildNormalizedPath(tempDir)); + assert(path.exists); + assert(path.isFile); + assert(readText(path).empty); + } + { + auto path = createTempFile(null, ".txt", "D rocks"); + scope(exit) if (path.exists) remove(path); + auto name = path.baseName; + assert(name.extension == ".txt"); + assert(name.length > ".txt".length); + assert(buildNormalizedPath(path.dirName) == buildNormalizedPath(tempDir)); + assert(path.exists); + assert(path.isFile); + assert(readText(path) == "D rocks"); + } +} + + /*************************************************** * Rename file $(D from) _to $(D to). * If the target file exists, it is overwritten.