Repository: microsoft/terminal
File: src/cascadia/TerminalSettingsModel/FileUtils.cpp (C++)

Code before:
    }

    void WriteUTF8File(const std::filesystem::path& path, const std::string_view content)
    {
        wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
        THROW_LAST_ERROR_IF(!file);

        const auto fileSize = gsl::narrow<DWORD>(content.size());
        DWORD bytesWritten = 0;
        THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr));

        if (bytesWritten != fileSize)
        {
            THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file");
        }
    }

    void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content)
    {
        // GH#10787: In the case of symbolic link, ensure that we modify the target rather than the link itself.
        // We append the paths manually rather than using "canonical" method to support scenario in which link target doesn't exist
        auto resolvedPath = path;
        while (std::filesystem::is_symlink(resolvedPath))
        {
            resolvedPath = resolvedPath.parent_path() / std::filesystem::read_symlink(resolvedPath);
        }

        auto tmpPath = resolvedPath;
        tmpPath += L".tmp";

        // Writing to a file isn't atomic, but...
        WriteUTF8File(tmpPath, content);

        // renaming one is (supposed to be) atomic.
        // Wait... "supposed to be"!? Well it's technically not always atomic,
        // but it's pretty darn close to it, so... better than nothing.
        std::filesystem::rename(tmpPath, resolvedPath);
    }
}


Review comment (bug):
One concern with manual symlink resolution is that it opens a can of worms regarding the symlink evaluation policy for L2L (local to local), L2R (local to remote), R2R, and R2L symlink types. (Check, for example, `fsutil.exe  behavior query symlinkEvaluation`.) I don't personally care about the first 3 types, but the last one is disturbing, and I think it should always be disabled. Symlinks are evaluated on the client side, so an R2L symlink is probably a mistake (e.g. "\\\\server\\share\\link" -> "C:\\local\\target\\path"). If a symlink type is disabled, then trying to follow it should fail with an `error_code` `value()` equal to `ERROR_SYMLINK_CLASS_DISABLED` (1463). This is the case for the `exists()` and `canonical()` methods, but `is_symlink()` and `read_symlink()` never ask the system to actually follow the link.

Code after:
    }

    void WriteUTF8File(const std::filesystem::path& path, const std::string_view content)
    {
        wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
        THROW_LAST_ERROR_IF(!file);

        const auto fileSize = gsl::narrow<DWORD>(content.size());
        DWORD bytesWritten = 0;
        THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), fileSize, &bytesWritten, nullptr));

        if (bytesWritten != fileSize)
        {
            THROW_WIN32_MSG(ERROR_WRITE_FAULT, "failed to write whole file");
        }
    }

    void WriteUTF8FileAtomic(const std::filesystem::path& path, const std::string_view content)
    {
        // GH#10787: rename() will replace symbolic links themselves and not the path they point at.
        // It's thus important that we first resolve them before generating temporary path.
        std::error_code ec;
        const auto resolvedPath = std::filesystem::is_symlink(path) ? std::filesystem::canonical(path, ec) : path;
        if (ec)
        {
            if (ec.value() != ERROR_FILE_NOT_FOUND)
            {
                THROW_WIN32_MSG(ec.value(), "failed to compute canonical path");
            }

            // The original file is a symbolic link, but the target doesn't exist.
            // Consider two fall-backs:
            //   * resolve the link manually, which might be less accurate and more prone to race conditions
            //   * write to the file directly, which lets the system resolve the symbolic link but leaves the write non-atomic
            // The latter is chosen, as this is an edge case and our 'atomic' writes are only best-effort.
            WriteUTF8File(path, content);
            return;
        }

        auto tmpPath = resolvedPath;
        tmpPath += L".tmp";

        // Writing to a file isn't atomic, but...
        WriteUTF8File(tmpPath, content);

        // renaming one is (supposed to be) atomic.
        // Wait... "supposed to be"!? Well it's technically not always atomic,
        // but it's pretty darn close to it, so... better than nothing.
        std::filesystem::rename(tmpPath, resolvedPath);
    }
}