Skip to content

Commit

Permalink
Issue 17912: Add function for creating a temporary file.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jmdavis committed Mar 22, 2018
1 parent d02b422 commit 8f17342
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 27 deletions.
15 changes: 15 additions & 0 deletions changelog/std-file-createTempFile.dd
Original file line number Diff line number Diff line change
@@ -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).
217 changes: 190 additions & 27 deletions std/file.d
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ $(TR $(TD Files) $(TD
$(LREF remove)
$(LREF slurp)
$(LREF write)
$(LREF createTempFile)
))
$(TR $(TD Symlinks) $(TD
$(LREF symlink)
Expand Down Expand Up @@ -801,8 +802,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
{
Expand All @@ -813,27 +812,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
{
Expand All @@ -860,20 +841,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;
}

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;
}
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.
Expand Down

0 comments on commit 8f17342

Please sign in to comment.