diff --git a/Mono.TextTemplating/Mono.TextTemplating/DirectoryHelper.cs b/Mono.TextTemplating/Mono.TextTemplating/DirectoryHelper.cs new file mode 100644 index 0000000..d3113d9 --- /dev/null +++ b/Mono.TextTemplating/Mono.TextTemplating/DirectoryHelper.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +// imported from dotnet/runtime +// see notes on individual methods + +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Mono.TextTemplating; + +#nullable enable + +static class TempSubdirectoryHelper +{ + /// + /// Create e temporary subdirectory in the system temp directory. + /// On .NET 7+, calls . Otherwise, it uses an implementation based on the one in from .NET 7. + /// + public static DirectoryInfo Create (string? prefix = default) +#if NETCOREAPP7_0_OR_GREATER + => Directory.CreateTempSubdirectory(prefix); +} +#else + { + if (ClassLibsImpl is not null) { + return ClassLibsImpl (prefix); + } + + if (prefix is string p && p.IndexOfAny (DirectorySeparatorChars) > -1) { + throw new ArgumentException ("Prefix may not contain directory separators", nameof (prefix)); + } + + string path = isWindows ? CreateTempSubdirectoryCoreWindows (prefix) : CreateTempSubdirectoryCoreUnix (prefix); + return new DirectoryInfo (path); + } + + static readonly Func? ClassLibsImpl; + static readonly bool isWindows = Path.DirectorySeparatorChar == '\\'; + static readonly char[] DirectorySeparatorChars; + + static TempSubdirectoryHelper () + { + if (typeof (Directory).GetMethod ("CreateTempSubdirectory", BindingFlags.Static | BindingFlags.Public) is MethodInfo fxMethod) { + ClassLibsImpl = (Func) fxMethod.CreateDelegate (typeof(Func)); + } + + DirectorySeparatorChars = Path.AltDirectorySeparatorChar == Path.DirectorySeparatorChar + ? new[] { Path.DirectorySeparatorChar } + : new[] { Path.DirectorySeparatorChar }; + } + + // copy of https://github.com/dotnet/runtime/blob/eb6f712d68f75add00f17a144838c1a64a3c3a47/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Unix.cs#L27 + // modified to remove deps on new/internal framework APIs + static unsafe string CreateTempSubdirectoryCoreUnix (string? prefix) + { + // mkdtemp takes a char* and overwrites the XXXXXX with six characters + // that'll result in a unique directory name. + string tempPath = Path.GetTempPath (); + int tempPathByteCount = Encoding.UTF8.GetByteCount (tempPath); + int prefixByteCount = prefix is not null ? Encoding.UTF8.GetByteCount (prefix) : 0; + int totalByteCount = tempPathByteCount + prefixByteCount + 6 + 1; + + byte[] path = new byte[totalByteCount]; + int pos = Encoding.UTF8.GetBytes (tempPath, 0, tempPath.Length, path, 0); + if (prefix is not null) { + pos += Encoding.UTF8.GetBytes (prefix, 0, prefix.Length, path, pos); + } + for(int i = 0; i < 6; i++) { + path[pos + i] = (byte)'X'; + } + path[pos + 6] = 0; + + // Create the temp directory. + fixed (byte* pPath = path) { + if (libc_mkdtemp (pPath) is null) { + Unix_ThrowIOExceptionForLastError (); + } + } + + // 'path' is now the name of the directory + Debug.Assert (path[path.Length-1] == 0); + return Encoding.UTF8.GetString (path, 0, path.Length - 1); // trim off the trailing '\0' + } + + [DllImport ("libc", SetLastError = true, EntryPoint = "mkdtemp")] + static unsafe extern byte* libc_mkdtemp (byte* path); + + static void Unix_ThrowIOExceptionForLastError () + { + var error = Marshal.GetLastWin32Error (); + throw new IOException ("error"); + } + + // copy of https://github.com/dotnet/runtime/blob/eb6f712d68f75add00f17a144838c1a64a3c3a47/src/libraries/System.Private.CoreLib/src/System/IO/Directory.Windows.cs#L16 + // modified to remove deps on new/internal framework APIs + static unsafe string CreateTempSubdirectoryCoreWindows (string? prefix) + { + StringBuilder builder = new StringBuilder (MaxShortPath); + var tempRoot = Path.GetTempPath (); + builder.Append (tempRoot); + + // ensure the base TEMP directory exists + Directory.CreateDirectory (tempRoot); + + builder.Append (prefix); + + const int RandomFileNameLength = 12; // 12 == 8 + 1 (for period) + 3 + int initialTempPathLength = builder.Length; + builder.EnsureCapacity (initialTempPathLength + RandomFileNameLength); + + // For generating random file names + // 8 random bytes provides 12 chars in our encoding for the 8.3 name. + const int RandomKeyLength = 8; + byte* pKey = stackalloc byte[RandomKeyLength]; + + // to avoid an infinite loop, only try as many as GetTempFileNameW will create + const int MaxAttempts = ushort.MaxValue; + int attempts = 0; + while (attempts < MaxAttempts) { + builder.Length = initialTempPathLength; + builder.Append (Path.GetRandomFileName ()); + var path = builder.ToString (); + + bool directoryCreated = Kernel32.CreateDirectory (path, null); + if (!directoryCreated) { + // in the off-chance that the directory already exists, try again + int error = Marshal.GetLastWin32Error (); + if (error == ERROR_ALREADY_EXISTS) { + builder.Length = initialTempPathLength; + attempts++; + continue; + } + + ThrowExceptionForWin32Error (error); + } + + return path; + } + + throw new IOException (IO_MaxAttemptsReached); + } + + static void ThrowExceptionForWin32Error(int error) + { + // this is not ideal but we don't have access to anything better + Marshal.ThrowExceptionForHR (MakeHRFromErrorCode (error)); + } + + const int MaxShortPath = 260; + const int ERROR_ALREADY_EXISTS = 0xB7; + const string IO_MaxAttemptsReached = "Reached maximum directory creation attempts"; + + // https://github.com/dotnet/runtime/blob/cbc8695ae0c8c2c2d1ac1fc4546d81e0967ef716/src/libraries/Common/src/System/IO/Win32Marshal.cs#L84 + static int MakeHRFromErrorCode (int errorCode) + { + // Don't convert it if it is already an HRESULT + if ((0xFFFF0000 & errorCode) != 0) + return errorCode; + + return unchecked(((int)0x80070000) | errorCode); + } + + // https://github.com/dotnet/runtime/blob/cbc8695ae0c8c2c2d1ac1fc4546d81e0967ef716/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateDirectory.cs#L15 + internal static class Kernel32 + { + [DllImport ("kernel32.dll", EntryPoint = "CreateDirectoryW", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs (UnmanagedType.Bool)] + static unsafe extern bool CreateDirectoryPrivate ( + string path, + SECURITY_ATTRIBUTES* lpSecurityAttributes); + + internal static unsafe bool CreateDirectory (string path, SECURITY_ATTRIBUTES* lpSecurityAttributes) + { + // We always want to add for CreateDirectory to get around the legacy 248 character limitation + path = PathInternal.EnsureExtendedPrefix (path); + return CreateDirectoryPrivate (path, lpSecurityAttributes); + } + + [StructLayout (LayoutKind.Sequential)] + internal struct SECURITY_ATTRIBUTES + { + internal uint nLength; + internal IntPtr lpSecurityDescriptor; + internal BOOL bInheritHandle; + } + + internal enum BOOL : int + { + FALSE = 0, + TRUE = 1, + } + } + + // https://github.com/dotnet/runtime/blob/34bec269ff0b02224f318792c02f0254025b43bf/src/libraries/Common/src/System/IO/PathInternal.Windows.cs#L94 + class PathInternal + { + internal static string EnsureExtendedPrefix (string path) + { + if (IsPartiallyQualified (path) || IsDevice (path)) + return path; + + // Given \\server\share in longpath becomes \\?\UNC\server\share + if (path.StartsWith (UncPathPrefix, StringComparison.OrdinalIgnoreCase)) + return path.Insert (2, UncExtendedPrefixToInsert); + + return ExtendedPathPrefix + path; + } + + internal static bool IsPartiallyQualified (string path) + { + if (path.Length < 2) { + // It isn't fixed, it must be relative. There is no way to specify a fixed + // path with one character (or less). + return true; + } + + if (IsDirectorySeparator (path[0])) { + // There is no valid way to specify a relative path with two initial slashes or + // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\ + return !(path[1] == '?' || IsDirectorySeparator (path[1])); + } + + // The only way to specify a fixed path that doesn't begin with two slashes + // is the drive, colon, slash format- i.e. C:\ + return !((path.Length >= 3) + && (path[1] == Path.VolumeSeparatorChar) + && IsDirectorySeparator (path[2]) + // To match old behavior we'll check the drive character for validity as the path is technically + // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream. + && IsValidDriveChar (path[0])); + } + + [MethodImpl (MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator (char c) + { + return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + } + + internal static bool IsValidDriveChar (char value) + { + return (uint)((value | 0x20) - 'a') <= (uint)('z' - 'a'); + } + + internal static bool IsDevice (string path) + { + return IsExtended (path) + || + ( + path.Length >= DevicePrefixLength + && IsDirectorySeparator (path[0]) + && IsDirectorySeparator (path[1]) + && (path[2] == '.' || path[2] == '?') + && IsDirectorySeparator (path[3]) + ); + } + + internal static bool IsExtended (string path) + { + // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. + // Skipping of normalization will *only* occur if back slashes ('\') are used. + return path.Length >= DevicePrefixLength + && path[0] == '\\' + && (path[1] == '\\' || path[1] == '?') + && path[2] == '?' + && path[3] == '\\'; + } + + internal const string ExtendedPathPrefix = @"\\?\"; + internal const string UncPathPrefix = @"\\"; + internal const string UncExtendedPrefixToInsert = @"?\UNC\"; + internal const string UncExtendedPathPrefix = @"\\?\UNC\"; + internal const int DevicePrefixLength = 4; + } +} +#endif \ No newline at end of file diff --git a/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs b/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs index 4288213..b3a0e84 100644 --- a/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs +++ b/Mono.TextTemplating/Mono.TextTemplating/TemplatingEngine.cs @@ -306,9 +306,7 @@ CancellationToken token // there are no equivalent for directories, so we create a directory // based on the file name, which *should* be unique as long as the file // exists. - var tempFile = Path.GetTempFileName (); - var tempFolder = tempFile + "dir"; - Directory.CreateDirectory (tempFolder); + var tempFolder = TempSubdirectoryHelper.Create ("t4-").FullName; if (settings.Log != null) { settings.Log.WriteLine ($"Generating code in '{tempFolder}'"); @@ -332,7 +330,7 @@ CancellationToken token var result = await compiler.CompileFile (args, settings.Log, token).ConfigureAwait (false); - var r = new CompilerResults (new TempFileCollection ()); + var r = new CompilerResults (new TempFileCollection (tempFolder)); r.TempFiles.AddFile (sourceFilename, false); if (result.ResponseFile != null) { @@ -370,7 +368,6 @@ CancellationToken token r.TempFiles.Delete (); // we can delete our temporary file after our temporary folder is deleted. Directory.Delete (tempFolder); - File.Delete (tempFile); } return (r, compiledAssembly);