Support optional file extension in TmpFile

TmpFile can now be supplied an optional file extension. This change
was motivated by validation work using the XCode SDK Metal compiler,
which expects a source file to end with the ".metal" extension.

See
https://developer.apple.com/documentation/metal/libraries/understanding_the_metal_shading_language_filename_extension

Bug: tint:535
Change-Id: I5d44baa20ba350530ace46569e238c4627135e51
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/45720
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Arman Uguray <armansito@chromium.org>
diff --git a/src/utils/tmpfile.h b/src/utils/tmpfile.h
index fbb66ff..b24fa51 100644
--- a/src/utils/tmpfile.h
+++ b/src/utils/tmpfile.h
@@ -28,7 +28,9 @@
   /// Constructor.
   /// Creates a new temporary file which can be written to.
   /// The temporary file will be automatically deleted on destruction.
-  TmpFile();
+  /// @param extension optional file extension to use with the file. The file
+  /// have no extension by default.
+  explicit TmpFile(std::string extension = "");
 
   /// Destructor.
   /// Deletes the temporary file.
diff --git a/src/utils/tmpfile_other.cc b/src/utils/tmpfile_other.cc
index 8a90238..8ee60e6 100644
--- a/src/utils/tmpfile_other.cc
+++ b/src/utils/tmpfile_other.cc
@@ -17,7 +17,7 @@
 namespace tint {
 namespace utils {
 
-TmpFile::TmpFile() = default;
+TmpFile::TmpFile(std::string) = default;
 
 TmpFile::~TmpFile() = default;
 
diff --git a/src/utils/tmpfile_posix.cc b/src/utils/tmpfile_posix.cc
index 5bf7804..fac1847 100644
--- a/src/utils/tmpfile_posix.cc
+++ b/src/utils/tmpfile_posix.cc
@@ -15,15 +15,25 @@
 #include "src/utils/tmpfile.h"
 
 #include <unistd.h>
+#include <limits>
+
+#include "src/debug.h"
 
 namespace tint {
 namespace utils {
 
 namespace {
 
-std::string TmpFilePath() {
-  char name[] = "tint_XXXXXX";
-  int file = mkstemp(name);
+std::string TmpFilePath(std::string ext) {
+  // mkstemps requires an `int` for the file extension name but STL represents
+  // size_t. Pre-C++20 there the behavior for unsigned-to-signed conversion
+  // (when the source value exceeds the representable range) is implementation
+  // defined. While such a large file extension is unlikely in practice, we
+  // enforce this here at runtime.
+  TINT_ASSERT(ext.length() <=
+              static_cast<size_t>(std::numeric_limits<int>::max()));
+  std::string name = "tint_XXXXXX" + ext;
+  int file = mkstemps(&name[0], static_cast<int>(ext.length()));
   if (file != -1) {
     close(file);
     return name;
@@ -33,7 +43,8 @@
 
 }  // namespace
 
-TmpFile::TmpFile() : path_(TmpFilePath()) {}
+TmpFile::TmpFile(std::string extension)
+    : path_(TmpFilePath(std::move(extension))) {}
 
 TmpFile::~TmpFile() {
   if (!path_.empty()) {
diff --git a/src/utils/tmpfile_test.cc b/src/utils/tmpfile_test.cc
index 44b2f72..5416da5 100644
--- a/src/utils/tmpfile_test.cc
+++ b/src/utils/tmpfile_test.cc
@@ -66,6 +66,25 @@
   ASSERT_FALSE(file);
 }
 
+TEST(TmpFileTest, FileExtension) {
+  const std::string kExt = ".foo";
+  std::string path;
+  {
+    TmpFile tmp(kExt);
+    if (!tmp) {
+      GTEST_SKIP() << "Unable create a temporary file";
+    }
+    path = tmp.Path();
+  }
+
+  ASSERT_GT(path.length(), kExt.length());
+  EXPECT_EQ(kExt, path.substr(path.length() - kExt.length()));
+
+  // Check the file has been deleted when it fell out of scope
+  std::ifstream file(path);
+  ASSERT_FALSE(file);
+}
+
 }  // namespace
 }  // namespace utils
 }  // namespace tint
diff --git a/src/utils/tmpfile_windows.cc b/src/utils/tmpfile_windows.cc
index 23fb4b3..764f2ad 100644
--- a/src/utils/tmpfile_windows.cc
+++ b/src/utils/tmpfile_windows.cc
@@ -32,7 +32,7 @@
 
 }  // namespace
 
-TmpFile::TmpFile() : path_(TmpFilePath()) {}
+TmpFile::TmpFile(std::string ext) : path_(TmpFilePath() + ext) {}
 
 TmpFile::~TmpFile() {
   if (!path_.empty()) {