mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-01 20:34:57 +01:00
Write settings, presets and favourites whole or not at all
A failed write leaves the old file in place. Settings that could not be read are not saved over. UI exceptions reach the log. Started from a shortcut set up elsewhere, the starter moves to its own folder when only that one holds a game.
This commit is contained in:
@@ -9,16 +9,61 @@ namespace StarterNG.Tests.Fakes;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Paths are normalised to forward slashes and compared case-insensitively,
|
||||
/// matching how the parsers treat installation paths on both platforms.
|
||||
/// matching how the parsers treat installation paths on both platforms - or,
|
||||
/// asked for, case-sensitively, as a Linux disk holds them.
|
||||
/// </remarks>
|
||||
public sealed class InMemoryFileSystem : IFileSystem
|
||||
{
|
||||
private readonly Dictionary<string, byte[]> _files = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _directories = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _executable = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly StringComparison _comparison;
|
||||
private readonly Dictionary<string, byte[]> _files;
|
||||
private readonly HashSet<string> _directories;
|
||||
private readonly HashSet<string> _executable;
|
||||
private readonly Dictionary<string, string> _links;
|
||||
private readonly HashSet<string> _unreadable;
|
||||
private readonly HashSet<string> _unlistable;
|
||||
private readonly Dictionary<string, DateTime> _written;
|
||||
|
||||
public InMemoryFileSystem(bool caseSensitive = false)
|
||||
{
|
||||
_comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
||||
var comparer = StringComparer.FromComparison(_comparison);
|
||||
_files = new Dictionary<string, byte[]>(comparer);
|
||||
_directories = new HashSet<string>(comparer);
|
||||
_executable = new HashSet<string>(comparer);
|
||||
_links = new Dictionary<string, string>(comparer);
|
||||
_unreadable = new HashSet<string>(comparer);
|
||||
_unlistable = new HashSet<string>(comparer);
|
||||
_written = new Dictionary<string, DateTime>(comparer);
|
||||
}
|
||||
|
||||
public DateTime LastWriteTimeUtc { get; set; } = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>Making a link throws, as on a disk that cannot hold one.</summary>
|
||||
public bool LinksFail { get; set; }
|
||||
|
||||
/// <summary>A folder moved to a destination this says yes to, its path with forward slashes, throws.</summary>
|
||||
public Func<string, bool>? MoveFails { get; set; }
|
||||
|
||||
/// <summary>The file stays, but reading or copying it throws, as a locked one does; writing it still goes through.</summary>
|
||||
public InMemoryFileSystem FailReading(string path)
|
||||
{
|
||||
_unreadable.Add(Normalize(path));
|
||||
return this;
|
||||
}
|
||||
|
||||
public InMemoryFileSystem AllowReading(string path)
|
||||
{
|
||||
_unreadable.Remove(Normalize(path));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>The folder is there, but listing it throws, as one without the permission does.</summary>
|
||||
public InMemoryFileSystem DenyListing(string path)
|
||||
{
|
||||
_unlistable.Add(Normalize(path));
|
||||
return this;
|
||||
}
|
||||
|
||||
static InMemoryFileSystem()
|
||||
{
|
||||
// The game's own files are code page 1250; a test that writes Polish text
|
||||
@@ -54,7 +99,10 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
|
||||
public bool FileExists(string path) => _files.ContainsKey(Normalize(path));
|
||||
|
||||
public bool DirectoryExists(string path) => _directories.Contains(Normalize(path));
|
||||
/// <summary>A link counts as the directory it points at, as the disk has it.</summary>
|
||||
public bool DirectoryExists(string path) =>
|
||||
_directories.Contains(Normalize(path)) ||
|
||||
(_links.TryGetValue(Normalize(path), out string? target) && _directories.Contains(Normalize(target)));
|
||||
|
||||
public string ReadAllText(string path) => Encoding.UTF8.GetString(Bytes(path));
|
||||
|
||||
@@ -76,6 +124,10 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
AddParents(key);
|
||||
}
|
||||
|
||||
public void WriteAllTextAtomic(string path, string contents) => WithFile(path, contents);
|
||||
|
||||
public void WriteAllBytesAtomic(string path, byte[] bytes) => WriteAllBytes(path, bytes);
|
||||
|
||||
public void WriteAllLines(string path, IEnumerable<string> lines) =>
|
||||
WithFile(path, string.Join("\n", lines));
|
||||
|
||||
@@ -103,7 +155,7 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
string prefix = Normalize(path) + "/";
|
||||
var pattern = ToRegex(searchPattern);
|
||||
return _files.Keys
|
||||
.Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(key => key.StartsWith(prefix, _comparison))
|
||||
.Where(key => !key[prefix.Length..].Contains('/'))
|
||||
.Where(key => pattern.IsMatch(key[prefix.Length..]))
|
||||
.OrderBy(key => key, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -115,7 +167,8 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
string prefix = Normalize(path) + "/";
|
||||
var pattern = ToRegex(searchPattern);
|
||||
return _files.Keys
|
||||
.Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(key => key.StartsWith(prefix, _comparison))
|
||||
.Where(key => !_unlistable.Any(folder => key.StartsWith(folder + "/", _comparison)))
|
||||
.Where(key => pattern.IsMatch(key[(key.LastIndexOf('/') + 1)..]))
|
||||
.OrderBy(key => key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
@@ -123,15 +176,26 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
|
||||
public IReadOnlyList<string> GetDirectories(string path)
|
||||
{
|
||||
if (_unlistable.Contains(Normalize(path)))
|
||||
throw new UnauthorizedAccessException($"Access to the path '{path}' is denied.");
|
||||
|
||||
string prefix = Normalize(path) + "/";
|
||||
return _directories
|
||||
.Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(key => key.StartsWith(prefix, _comparison))
|
||||
.Where(key => !key[prefix.Length..].Contains('/'))
|
||||
.OrderBy(key => key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public DateTime GetLastWriteTimeUtc(string path) => LastWriteTimeUtc;
|
||||
public DateTime GetLastWriteTimeUtc(string path) =>
|
||||
_written.TryGetValue(Normalize(path), out var written) ? written : LastWriteTimeUtc;
|
||||
|
||||
/// <summary>One file's own time, in place of <see cref="LastWriteTimeUtc"/>.</summary>
|
||||
public InMemoryFileSystem WrittenAt(string path, DateTime utc)
|
||||
{
|
||||
_written[Normalize(path)] = utc;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Files added through <see cref="WithExecutable"/> carry the execute bit.</summary>
|
||||
public long GetFileSize(string path) => Bytes(path).LongLength;
|
||||
@@ -145,10 +209,56 @@ public sealed class InMemoryFileSystem : IFileSystem
|
||||
return this;
|
||||
}
|
||||
|
||||
private byte[] Bytes(string path) =>
|
||||
_files.TryGetValue(Normalize(path), out var bytes)
|
||||
public string? LinkTarget(string path) => _links.GetValueOrDefault(Normalize(path));
|
||||
|
||||
public void CreateDirectoryLink(string path, string target)
|
||||
{
|
||||
if (LinksFail)
|
||||
throw new IOException($"cannot link {path}");
|
||||
|
||||
string key = Normalize(path);
|
||||
if (_links.ContainsKey(key) || _directories.Contains(key) || _files.ContainsKey(key))
|
||||
throw new IOException($"{path} already exists");
|
||||
_links[key] = target;
|
||||
AddParents(key);
|
||||
}
|
||||
|
||||
public void MoveDirectory(string sourcePath, string destinationPath)
|
||||
{
|
||||
if (MoveFails?.Invoke(Normalize(destinationPath)) == true)
|
||||
throw new IOException($"cannot move to {destinationPath}");
|
||||
|
||||
string from = Normalize(sourcePath), to = Normalize(destinationPath);
|
||||
if (!_directories.Contains(from) && !_links.ContainsKey(from))
|
||||
throw new DirectoryNotFoundException(sourcePath);
|
||||
if (_directories.Contains(to) || _links.ContainsKey(to) || _files.ContainsKey(to))
|
||||
throw new IOException($"{destinationPath} already exists");
|
||||
|
||||
string? Moved(string key) =>
|
||||
string.Equals(key, from, _comparison) ? to
|
||||
: key.StartsWith(from + "/", _comparison) ? to + key[from.Length..]
|
||||
: null;
|
||||
|
||||
foreach (var (key, bytes) in _files.ToList())
|
||||
if (Moved(key) is { } moved) { _files.Remove(key); _files[moved] = bytes; }
|
||||
foreach (string key in _executable.ToList())
|
||||
if (Moved(key) is { } moved) { _executable.Remove(key); _executable.Add(moved); }
|
||||
foreach (var (key, target) in _links.ToList())
|
||||
if (Moved(key) is { } moved) { _links.Remove(key); _links[moved] = target; }
|
||||
foreach (string key in _directories.ToList())
|
||||
if (Moved(key) is { } moved) { _directories.Remove(key); _directories.Add(moved); }
|
||||
AddParents(to);
|
||||
}
|
||||
|
||||
private byte[] Bytes(string path)
|
||||
{
|
||||
if (_unreadable.Contains(Normalize(path)))
|
||||
throw new IOException($"The process cannot access the file '{path}'.");
|
||||
|
||||
return _files.TryGetValue(Normalize(path), out var bytes)
|
||||
? bytes
|
||||
: throw new FileNotFoundException(path);
|
||||
}
|
||||
|
||||
private void AddParents(string key)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user