From 99aec4e7810ef419b621cf13afa53cd9e67a5d67 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. --- std/file.d | 232 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 205 insertions(+), 27 deletions(-) diff --git a/std/file.d b/std/file.d index b8c5835375e..9c19be22acb 100644 --- a/std/file.d +++ b/std/file.d @@ -611,8 +611,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 { @@ -623,27 +621,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 { @@ -670,20 +650,218 @@ 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; + } + + 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 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. + + 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 reason. + + See_Also: + $(REF 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; + import std.random : isSeedable, Random, unpredictableSeed; + + // We do this rather than use rndGen to avoid any problems caused by rndGen + // being reseeded or reused after being copied (which could easily lead to + // the program being able to figure out what the supposedly random name + // would be). + static Random random; + static bool initialized; + if (!initialized) + { + import std.algorithm.iteration : map; + import std.range : repeat; + static if (isSeedable!(Random, typeof(map!((a) => unpredictableSeed)(repeat(0))))) + random.seed(map!((a) => unpredictableSeed)(repeat(0))); + else + random = Random(unpredictableSeed); + initialized = true; + } + + static string genTempName(const(char)[] prefix, const(char)[] suffix) + { + import std.ascii : digits, letters; + 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 chars = chain(letters.representation, digits.representation); + foreach (ref c; name[prefix.length .. $ - suffix.length]) + { + c = chars[random.front % chars.length]; + random.popFront(); + } + + return buildPath(tempDir, name); + } + + while(1) + { + auto filename = genTempName(prefix, suffix); + + version(Posix) + { + // O_EXCL does not support symbolic links. + if (filename.dirName.isSymlink) + filename = buildPath(filename.dirName.readLink(), filename.baseName); + + 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; } - cenforce(sum == size && CloseHandle(h), name, namez); } +/// +@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.