Extract plugin to temp directory before copying to final location, some minor cleanup

This commit is contained in:
goaaats 2025-04-01 18:55:27 +02:00
parent f2c89bfc00
commit 34679d085b
8 changed files with 263 additions and 153 deletions

View file

@ -91,7 +91,7 @@ internal sealed partial class TextureManager
throw new NullReferenceException($"{nameof(path)} cannot be null.");
using var wrapAux = new WrapAux(wrap, true);
var pathTemp = Util.GetTempFileNameForFileReplacement(path);
var pathTemp = Util.GetReplaceableFileName(path);
var trashfire = new List<Exception>();
try
{

View file

@ -48,6 +48,8 @@ internal class PluginManager : IInternalDisposableService
/// </summary>
public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable
private const string BrokenMarkerFileName = ".broken";
private static readonly ModuleLog Log = ModuleLog.Create<PluginManager>();
private readonly object pluginListLock = new();
@ -874,7 +876,7 @@ internal class PluginManager : IInternalDisposableService
}
/// <summary>
/// Cleanup disabled plugins. Does not target devPlugins.
/// Cleanup disabled and broken plugins. Does not target devPlugins.
/// </summary>
public void CleanupPlugins()
{
@ -882,6 +884,13 @@ internal class PluginManager : IInternalDisposableService
{
try
{
if (File.Exists(Path.Combine(pluginDir.FullName, BrokenMarkerFileName)))
{
Log.Warning("Cleaning up broken plugin {Name}", pluginDir.Name);
pluginDir.Delete(true);
continue;
}
var versionDirs = pluginDir.GetDirectories();
versionDirs = versionDirs
@ -1423,7 +1432,8 @@ internal class PluginManager : IInternalDisposableService
else
{
// If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed
Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null");
if (inheritedWorkingPluginId != null)
throw new InvalidOperationException("Inherited WorkingPluginId must not be null");
}
// Ensure that we have a testing opt-in for this plugin if we are installing a testing version
@ -1434,31 +1444,28 @@ internal class PluginManager : IInternalDisposableService
this.configuration.QueueSave();
}
var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty));
var pluginVersionsDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName));
var tempOutputDir = new DirectoryInfo(FilesystemUtil.GetTempFileName());
var outputDir = new DirectoryInfo(Path.Combine(pluginVersionsDir.FullName, version?.ToString() ?? string.Empty));
FilesystemUtil.DeleteAndRecreateDirectory(tempOutputDir);
FilesystemUtil.DeleteAndRecreateDirectory(outputDir);
Log.Debug("Extracting plugin to {TempOutputDir}", tempOutputDir);
try
{
if (outputDir.Exists)
outputDir.Delete(true);
using var archive = new ZipArchive(zipStream);
outputDir.Create();
}
catch
{
// ignored, since the plugin may be loaded already
}
Log.Debug("Extracting to {OutputDir}", outputDir);
using (var archive = new ZipArchive(zipStream))
{
foreach (var zipFile in archive.Entries)
{
var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName)));
var outputFile = new FileInfo(
Path.GetFullPath(Path.Combine(tempOutputDir.FullName, zipFile.FullName)));
if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase))
if (!outputFile.FullName.StartsWith(tempOutputDir.FullName, StringComparison.OrdinalIgnoreCase))
{
throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability");
throw new IOException(
"Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability");
}
if (outputFile.Directory == null)
@ -1469,70 +1476,88 @@ internal class PluginManager : IInternalDisposableService
if (zipFile.Name.IsNullOrEmpty())
{
// Assuming Empty for Directory
Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}");
Log.Verbose(
"ZipFile name is null or empty, treating as a directory: {Path}", outputFile.Directory.FullName);
Directory.CreateDirectory(outputFile.Directory.FullName);
continue;
}
// Ensure directory is created
Directory.CreateDirectory(outputFile.Directory.FullName);
try
{
zipFile.ExtractToFile(outputFile.FullName, true);
}
catch (Exception ex)
{
if (outputFile.Extension.EndsWith("dll"))
{
throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}");
}
Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}");
}
zipFile.ExtractToFile(outputFile.FullName, true);
}
var tempDllFile = LocalPluginManifest.GetPluginFile(tempOutputDir, repoManifest);
var tempManifestFile = LocalPluginManifest.GetManifestFile(tempDllFile);
// We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use.
FilesystemUtil.WriteAllTextSafe(
tempManifestFile.FullName,
JsonConvert.SerializeObject(repoManifest, Formatting.Indented));
// Reload as a local manifest, add some attributes, and save again.
var tempManifest = LocalPluginManifest.Load(tempManifestFile);
if (tempManifest == null)
throw new Exception("Plugin had no valid manifest");
if (tempManifest.InternalName != repoManifest.InternalName)
{
throw new Exception(
$"Distributed internal name does not match repo internal name: {tempManifest.InternalName} - {repoManifest.InternalName}");
}
if (tempManifest.WorkingPluginId != Guid.Empty)
throw new Exception("Plugin shall not specify a WorkingPluginId");
tempManifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid();
if (useTesting)
{
tempManifest.Testing = true;
}
// Document the url the plugin was installed from
tempManifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty
? repoManifest.SourceRepo.PluginMasterUrl
: SpecialPluginSource.MainRepo;
tempManifest.Save(tempManifestFile, "installation");
// Copy the directory to the final location
Log.Debug("Copying plugin from {TempOutputDir} to {OutputDir}", tempOutputDir, outputDir);
FilesystemUtil.CopyFilesRecursively(tempOutputDir, outputDir);
var finalDllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest);
var finalManifestFile = LocalPluginManifest.GetManifestFile(finalDllFile);
var finalManifest = LocalPluginManifest.Load(finalManifestFile) ??
throw new Exception("Plugin had no valid manifest after copy");
Log.Information("Installed plugin {InternalName} (testing={UseTesting})", tempManifest.Name, useTesting);
var plugin = await this.LoadPluginAsync(finalDllFile, finalManifest, reason);
this.NotifyinstalledPluginsListChanged();
return plugin;
}
var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest);
var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
// We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use.
Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented));
// Reload as a local manifest, add some attributes, and save again.
var manifest = LocalPluginManifest.Load(manifestFile);
if (manifest == null)
throw new Exception("Plugin had no valid manifest");
if (manifest.InternalName != repoManifest.InternalName)
catch
{
Directory.Delete(outputDir.FullName, true);
throw new Exception(
$"Distributed internal name does not match repo internal name: {manifest.InternalName} - {repoManifest.InternalName}");
// Attempt to clean up if we can
try
{
outputDir.Delete(true);
}
catch
{
// Write marker file if we can't, we'll try to do it at the next start
File.WriteAllText(Path.Combine(pluginVersionsDir.FullName, BrokenMarkerFileName), string.Empty);
}
throw;
}
if (manifest.WorkingPluginId != Guid.Empty)
throw new Exception("Plugin shall not specify a WorkingPluginId");
manifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid();
if (useTesting)
finally
{
manifest.Testing = true;
tempOutputDir.Delete(true);
}
// Document the url the plugin was installed from
manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : SpecialPluginSource.MainRepo;
manifest.Save(manifestFile, "installation");
Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})");
var plugin = await this.LoadPluginAsync(dllFile, manifest, reason);
this.NotifyinstalledPluginsListChanged();
return plugin;
}
/// <summary>
@ -1547,7 +1572,6 @@ internal class PluginManager : IInternalDisposableService
/// <returns>The loaded plugin.</returns>
private async Task<LocalPlugin> LoadPluginAsync(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
{
var name = manifest?.Name ?? dllFile.Name;
var loadPlugin = !doNotLoad;
LocalPlugin? plugin;
@ -1560,7 +1584,7 @@ internal class PluginManager : IInternalDisposableService
if (isDev)
{
Log.Information($"Loading dev plugin {name}");
Log.Information("Loading dev plugin {Name}", manifest.InternalName);
plugin = new LocalDevPlugin(dllFile, manifest);
// This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet
@ -1569,7 +1593,7 @@ internal class PluginManager : IInternalDisposableService
}
else
{
Log.Information($"Loading plugin {name}");
Log.Information("Loading plugin {Name}", manifest.InternalName);
plugin = new LocalPlugin(dllFile, manifest);
}
@ -1663,7 +1687,7 @@ internal class PluginManager : IInternalDisposableService
}
else
{
Log.Verbose($"{name} not loaded, wantToLoad:{wantedByAnyProfile} orphaned:{plugin.IsOrphaned}");
Log.Verbose("{Name} not loaded, wantToLoad:{WantedByAnyProfile} orphaned:{IsOrphaned}", manifest.InternalName, wantedByAnyProfile, plugin.IsOrphaned);
}
}
catch (InvalidPluginException)

View file

@ -57,7 +57,7 @@ internal class ProfileManager : IServiceType
/// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins.
/// </summary>
public bool IsBusy => this.isBusy;
/// <summary>
/// Get a disposable that will lock the profile list while it is not disposed.
/// You must NEVER use this in async code.
@ -77,7 +77,7 @@ internal class ProfileManager : IServiceType
{
var want = false;
var wasInAnyProfile = false;
lock (this.profiles)
{
foreach (var profile in this.profiles)
@ -93,7 +93,7 @@ internal class ProfileManager : IServiceType
if (!wasInAnyProfile && addIfNotDeclared)
{
Log.Warning("'{Guid}'('{InternalName}') was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState);
Log.Warning("{Guid}({InternalName}) was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState);
await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, internalName, defaultState, false);
return defaultState;
@ -175,7 +175,7 @@ internal class ProfileManager : IServiceType
{
// Disable it
modelV1.IsEnabled = false;
// Try to find matching plugins for all plugins in the profile
var pm = Service<PluginManager>.Get();
foreach (var plugin in modelV1.Plugins)
@ -313,7 +313,7 @@ internal class ProfileManager : IServiceType
profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid);
}
}
/// <summary>
/// Validate profiles for errors.
/// </summary>
@ -328,7 +328,7 @@ internal class ProfileManager : IServiceType
{
if (seenIds.Contains(pluginEntry.WorkingPluginId))
throw new Exception($"Plugin '{pluginEntry.WorkingPluginId}'('{pluginEntry.InternalName}') is twice in profile '{profile.Guid}'('{profile.Name}')");
seenIds.Add(pluginEntry.WorkingPluginId);
}
}

View file

@ -605,6 +605,10 @@ internal class LocalPlugin : IAsyncDisposable
if (this.loader != null)
return;
this.DllFile.Refresh();
if (!this.DllFile.Exists)
throw new Exception($"Plugin DLL file at '{this.DllFile.FullName}' did not exist, cannot load.");
try
{
this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);

View file

@ -57,15 +57,15 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest
/// <param name="reason">The reason the manifest was saved.</param>
public void Save(FileInfo manifestFile, string reason)
{
Log.Verbose("Saving manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason);
Log.Verbose("Saving manifest for {PluginName} because {Reason}", this.InternalName, reason);
try
{
Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
FilesystemUtil.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
}
catch
{
Log.Error("Could not write out manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason);
Log.Error("Could not write out manifest for {PluginName} because {Reason}", this.InternalName, reason);
throw;
}
}
@ -78,7 +78,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest
public static LocalPluginManifest? Load(FileInfo manifestFile) => JsonConvert.DeserializeObject<LocalPluginManifest>(File.ReadAllText(manifestFile.FullName));
/// <summary>
/// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
/// A standardized way to get the plugin DLL name that should accompany a manifest file.
/// </summary>
/// <param name="dir">Manifest directory.</param>
/// <param name="manifest">The manifest.</param>
@ -86,7 +86,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest
public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
/// <summary>
/// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
/// A standardized way to get the manifest file that should accompany a plugin DLL.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> file.</returns>

View file

@ -29,7 +29,7 @@ internal class ReliableFileStorage : IInternalDisposableService
private readonly object syncRoot = new();
private SQLiteConnection? db;
/// <summary>
/// Initializes a new instance of the <see cref="ReliableFileStorage"/> class.
/// </summary>
@ -37,7 +37,7 @@ internal class ReliableFileStorage : IInternalDisposableService
public ReliableFileStorage(string vfsDbPath)
{
var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db");
Log.Verbose("Initializing VFS database at {Path}", databasePath);
try
@ -52,7 +52,7 @@ internal class ReliableFileStorage : IInternalDisposableService
{
if (File.Exists(databasePath))
File.Delete(databasePath);
this.SetupDb(databasePath);
}
catch (Exception)
@ -79,13 +79,13 @@ internal class ReliableFileStorage : IInternalDisposableService
if (this.db == null)
return false;
// If the file doesn't actually exist on the FS, but it does in the DB, we can say YES and read operations will read from the DB instead
var normalizedPath = NormalizePath(path);
var file = this.db.Table<DbFile>().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId);
return file != null;
}
/// <summary>
/// Write all text to a file.
/// </summary>
@ -94,7 +94,7 @@ internal class ReliableFileStorage : IInternalDisposableService
/// <param name="containerId">Container to write to.</param>
public void WriteAllText(string path, string? contents, Guid containerId = default)
=> this.WriteAllText(path, contents, Encoding.UTF8, containerId);
/// <summary>
/// Write all text to a file.
/// </summary>
@ -107,7 +107,7 @@ internal class ReliableFileStorage : IInternalDisposableService
var bytes = encoding.GetBytes(contents ?? string.Empty);
this.WriteAllBytes(path, bytes, containerId);
}
/// <summary>
/// Write all bytes to a file.
/// </summary>
@ -122,10 +122,10 @@ internal class ReliableFileStorage : IInternalDisposableService
{
if (this.db == null)
{
Util.WriteAllBytesSafe(path, bytes);
FilesystemUtil.WriteAllBytesSafe(path, bytes);
return;
}
this.db.RunInTransaction(() =>
{
var normalizedPath = NormalizePath(path);
@ -145,8 +145,8 @@ internal class ReliableFileStorage : IInternalDisposableService
file.Data = bytes;
this.db.Update(file);
}
Util.WriteAllBytesSafe(path, bytes);
FilesystemUtil.WriteAllBytesSafe(path, bytes);
});
}
}
@ -180,7 +180,7 @@ internal class ReliableFileStorage : IInternalDisposableService
var bytes = this.ReadAllBytes(path, forceBackup, containerId);
return encoding.GetString(bytes);
}
/// <summary>
/// Read all text from a file, and automatically try again with the backup if the file does not exist or
/// the <paramref name="reader"/> function throws an exception. If the backup read also throws an exception,
@ -208,11 +208,11 @@ internal class ReliableFileStorage : IInternalDisposableService
public void ReadAllText(string path, Encoding encoding, Action<string> reader, Guid containerId = default)
{
ArgumentException.ThrowIfNullOrEmpty(path);
// TODO: We are technically reading one time too many here, if the file does not exist on the FS, ReadAllText
// fails over to the backup, and then the backup fails to read in the lambda. We should do something about that,
// but it's not a big deal. Would be nice if ReadAllText could indicate if it did fail over.
// 1.) Try without using the backup
try
{
@ -229,7 +229,7 @@ internal class ReliableFileStorage : IInternalDisposableService
{
Log.Verbose(ex, "First chance read from {Path} failed, trying backup", path);
}
// 2.) Try using the backup
try
{
@ -256,13 +256,13 @@ internal class ReliableFileStorage : IInternalDisposableService
public byte[] ReadAllBytes(string path, bool forceBackup = false, Guid containerId = default)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (forceBackup)
{
// If the db failed to load, act as if the file does not exist
if (this.db == null)
throw new FileNotFoundException("Backup database was not available");
var normalizedPath = NormalizePath(path);
var file = this.db.Table<DbFile>().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId);
if (file == null)
@ -274,7 +274,7 @@ internal class ReliableFileStorage : IInternalDisposableService
// If the file doesn't exist, immediately check the backup db
if (!File.Exists(path))
return this.ReadAllBytes(path, true, containerId);
try
{
return File.ReadAllBytes(path);
@ -302,10 +302,10 @@ internal class ReliableFileStorage : IInternalDisposableService
// Replace users folder
var usersFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
path = path.Replace(usersFolder, "%USERPROFILE%");
return path;
}
private void SetupDb(string path)
{
this.db = new SQLiteConnection(path,
@ -320,9 +320,9 @@ internal class ReliableFileStorage : IInternalDisposableService
[PrimaryKey]
[AutoIncrement]
public int Id { get; set; }
public Guid ContainerId { get; set; }
public string Path { get; set; } = null!;
public byte[] Data { get; set; } = null!;

View file

@ -0,0 +1,116 @@
using System.ComponentModel;
using System.IO;
using System.Text;
using Windows.Win32.Storage.FileSystem;
namespace Dalamud.Utility;
/// <summary>
/// Helper functions for filesystem operations.
/// </summary>
public static class FilesystemUtil
{
/// <summary>
/// Overwrite text in a file by first writing it to a temporary file, and then
/// moving that file to the path specified.
/// </summary>
/// <param name="path">The path of the file to write to.</param>
/// <param name="text">The text to write.</param>
public static void WriteAllTextSafe(string path, string text)
{
WriteAllTextSafe(path, text, Encoding.UTF8);
}
/// <summary>
/// Overwrite text in a file by first writing it to a temporary file, and then
/// moving that file to the path specified.
/// </summary>
/// <param name="path">The path of the file to write to.</param>
/// <param name="text">The text to write.</param>
/// <param name="encoding">Encoding to use.</param>
public static void WriteAllTextSafe(string path, string text, Encoding encoding)
{
WriteAllBytesSafe(path, encoding.GetBytes(text));
}
/// <summary>
/// Overwrite data in a file by first writing it to a temporary file, and then
/// moving that file to the path specified.
/// </summary>
/// <param name="path">The path of the file to write to.</param>
/// <param name="bytes">The data to write.</param>
public static unsafe void WriteAllBytesSafe(string path, byte[] bytes)
{
ArgumentException.ThrowIfNullOrEmpty(path);
// Open the temp file
var tempPath = path + ".tmp";
using var tempFile = Windows.Win32.PInvoke.CreateFile(
tempPath,
(uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE),
FILE_SHARE_MODE.FILE_SHARE_NONE,
null,
FILE_CREATION_DISPOSITION.CREATE_ALWAYS,
FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL,
null);
if (tempFile.IsInvalid)
throw new Win32Exception();
// Write the data
uint bytesWritten = 0;
if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan<byte>(bytes), &bytesWritten, null))
throw new Win32Exception();
if (bytesWritten != bytes.Length)
throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})");
if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile))
throw new Win32Exception();
tempFile.Close();
if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH))
throw new Win32Exception();
}
/// <summary>
/// Generates a temporary file name.
/// </summary>
/// <returns>A temporary file name that is almost guaranteed to be unique.</returns>
internal static string GetTempFileName()
{
// https://stackoverflow.com/a/50413126
return Path.Combine(Path.GetTempPath(), "dalamud_" + Guid.NewGuid());
}
/// <summary>
/// Copy files recursively from one directory to another.
/// </summary>
/// <param name="source">The source directory.</param>
/// <param name="target">The target directory.</param>
internal static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target)
{
foreach (var dir in source.GetDirectories())
CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name));
foreach (var file in source.GetFiles())
file.CopyTo(Path.Combine(target.FullName, file.Name));
}
/// <summary>
/// Delete and recreate a directory.
/// </summary>
/// <param name="dir">The directory to delete and recreate.</param>
internal static void DeleteAndRecreateDirectory(DirectoryInfo dir)
{
if (dir.Exists)
{
dir.Delete(true);
}
dir.Create();
}
}

View file

@ -604,10 +604,9 @@ public static class Util
/// </summary>
/// <param name="path">The path of the file to write to.</param>
/// <param name="text">The text to write.</param>
public static void WriteAllTextSafe(string path, string text)
{
WriteAllTextSafe(path, text, Encoding.UTF8);
}
[Api13ToDo("Remove.")]
[Obsolete("Replaced with FilesystemUtil.WriteAllTextSafe()")]
public static void WriteAllTextSafe(string path, string text) => FilesystemUtil.WriteAllTextSafe(path, text);
/// <summary>
/// Overwrite text in a file by first writing it to a temporary file, and then
@ -616,10 +615,9 @@ public static class Util
/// <param name="path">The path of the file to write to.</param>
/// <param name="text">The text to write.</param>
/// <param name="encoding">Encoding to use.</param>
public static void WriteAllTextSafe(string path, string text, Encoding encoding)
{
WriteAllBytesSafe(path, encoding.GetBytes(text));
}
[Api13ToDo("Remove.")]
[Obsolete("Replaced with FilesystemUtil.WriteAllTextSafe()")]
public static void WriteAllTextSafe(string path, string text, Encoding encoding) => FilesystemUtil.WriteAllTextSafe(path, text, encoding);
/// <summary>
/// Overwrite data in a file by first writing it to a temporary file, and then
@ -627,41 +625,9 @@ public static class Util
/// </summary>
/// <param name="path">The path of the file to write to.</param>
/// <param name="bytes">The data to write.</param>
public static unsafe void WriteAllBytesSafe(string path, byte[] bytes)
{
ArgumentException.ThrowIfNullOrEmpty(path);
// Open the temp file
var tempPath = path + ".tmp";
using var tempFile = Windows.Win32.PInvoke.CreateFile(
tempPath,
(uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE),
FILE_SHARE_MODE.FILE_SHARE_NONE,
null,
FILE_CREATION_DISPOSITION.CREATE_ALWAYS,
FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL,
null);
if (tempFile.IsInvalid)
throw new Win32Exception();
// Write the data
uint bytesWritten = 0;
if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan<byte>(bytes), &bytesWritten, null))
throw new Win32Exception();
if (bytesWritten != bytes.Length)
throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})");
if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile))
throw new Win32Exception();
tempFile.Close();
if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH))
throw new Win32Exception();
}
[Api13ToDo("Remove.")]
[Obsolete("Replaced with FilesystemUtil.WriteAllBytesSafe()")]
public static void WriteAllBytesSafe(string path, byte[] bytes) => FilesystemUtil.WriteAllBytesSafe(path, bytes);
/// <summary>Gets a temporary file name, for use as the sourceFileName in
/// <see cref="File.Replace(string,string,string?)"/>.</summary>
@ -669,7 +635,7 @@ public static class Util
/// <returns>A temporary file name that should be usable with <see cref="File.Replace(string,string,string?)"/>.
/// </returns>
/// <remarks>No write operation is done on the filesystem.</remarks>
public static string GetTempFileNameForFileReplacement(string targetFile)
public static string GetReplaceableFileName(string targetFile)
{
Span<byte> buf = stackalloc byte[9];
Random.Shared.NextBytes(buf);