6
\$\begingroup\$

WARNING: do not use this solution

See discussion in the accepted answer's comments. Basically, this solution would reproduce std::tmpnam's and boost::filesystem::unique_path()'s known weakness.

In fact, the situation is even worse, as shown in that discussion.

Original post

I was looking for a solution for that problem in the C++ standard library, but did not find a satisfactory solution. We have std::filesystem::temp_directory_path but no corresponding file_path. We have std::FILE* tmpfile() i.e. not quite modern C++... And we have std::tmpnam which is officially documented as unreliable:

it is possible that a file with that name is created by another process between the moment std::tmpnam returns and the moment this program attempts to use the returned name to create a file.

No probability is mentioned, but that does not sound too good.

As far as I understand, however, the whole IT-industry is using random UUIDs as unique names that do not collide in practice, and modern C++ has high quality pseudo-random generation, so it seems to me we are almost there:

#include <filesystem>
#include <format>
#include <limits>
#include <random>

auto temp_file_path() -> std::filesystem::path
{
    static std::mt19937 gen{std::random_device{}()};
    static std::uniform_int_distribution<> dist{0, std::numeric_limits<uint8_t>::max()};
    std::string name{};
    static constexpr auto num_bits = 128; // i.e. long enough to avoid collisions (see UUID)
    for (auto i = 0; i < (num_bits / std::numeric_limits<uint8_t>::digits); ++i) {
        name += std::format("{:02x}", dist(gen));
    }
    return std::filesystem::temp_directory_path() / name;
}

Am I missing anything or can the above be considered as a safe way to generate unique temporary file paths?

\$\endgroup\$
1
  • 2
    \$\begingroup\$ "No probability is mentioned, but that does not sound too good." ==> Indeed. The Linux man page uses [[deprecated]] for it and warns against using it, suggesting tmpfile() and mkstemp() instead. But mkstemp() is POSIX/BSD/GNU, not ISO C, and StackOverflow says that there is no C++ standard library equivalent of mkstemp(). \$\endgroup\$
    – Harith
    Commented May 24 at 21:40

1 Answer 1

7
\$\begingroup\$

It's less random than you think

While std::mt19937 is a very good pseudo-random number generator with a very large state, you are seeding it with the result of std::random_device{}(), which is just a 32-bit number on most platforms. So you will only get \$2^{32}\$ possible sequences of UUIDs.

To ensure the seed has enough bits, pass the constructor of std::mt19937 a std::seed_seq that itself is constructed with at least 4 random 32-bit numbers.

The filename is not in a valid UUID format

There is actually a standard for UUIDs, where parts of the 128 bits are used for encoding the version number and variant of the UUID, even for version 4 (which is the one that contains just a random number).

Consider securely opening and returning a file handle

Instead of just creating a name, which could not be as unique as you thought, it's much safer if your functions actually tries to open the file in exclusive mode (either using the POSIX open() function with the O_EXCL flag, or using std::fstream with C++23's std::ios::noreplace), and then return the opened file's handle instead of the filename. If opening fails because a file with that name already exists, you can just generate a new UUID and try again.

\$\endgroup\$
10
  • 1
    \$\begingroup\$ @OP If you're open to using fopen(), use C11's "x" mode. The call fails if the file already exists. cppreference.com tells me that it was added to C++ in C++17. \$\endgroup\$
    – Harith
    Commented May 24 at 23:19
  • 1
    \$\begingroup\$ While this answer has taught me about std::seed_seq which I am very thankful for, I do not agree with the conclusion "so you will only get 2³² possible UUIDs". I believe the correct conclusion would be "so you will only get 2³² possible sequences of UUIDs". I.e. a problem could occur when multiple (many) programs use that implementation on a single computer, not when a single program uses it multiple times. Of course, that is still a problem, which I definitely want to solve. \$\endgroup\$
    – nilo
    Commented May 25 at 8:13
  • 2
    \$\begingroup\$ How often you want to retry is up to you. However, making the function open the file securely or fail trying is better than just returning a filename that might not be unique, as it is easy for the caller to forget to handle this case. \$\endgroup\$
    – G. Sliepen
    Commented May 25 at 8:17
  • 1
    \$\begingroup\$ But going down that rabbit hole will of course force me to handle file deleting, i.e. the solution will no longer be a function, it will be a class. After reading a discussion about boot::filesystem::unique_path(), I understand that this is the way to go, since otherwise, I am basically recreating tmpnam's known weakness. I will now accept this answer. \$\endgroup\$
    – nilo
    Commented May 25 at 13:55
  • 2
    \$\begingroup\$ @Harith, my interpretation of what I have read is that opening the file in the constructor with std::ios::noreplace would be safe enough, because it would not let the invoker open the file without noticing it already exists. The name would never be exposed. \$\endgroup\$
    – nilo
    Commented May 26 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.