4 using System.Collections.Generic;
5 using System.Collections.Immutable;
15 static class Validation
17 private static readonly ImmutableArray<Identifier> unwritableDirs =
new[] {
"Content".ToIdentifier() }.ToImmutableArray();
18 private static readonly ImmutableArray<Identifier> unwritableExtensions =
new[]
20 ".exe",
".dll",
".json",
".pdb",
".com",
".scr",
".dylib",
".so",
".a",
".app",
22 }.ToIdentifiers().ToImmutableArray();
28 SkipValidationInDebugBuilds =
false;
35 public static Skipper SkipInDebugBuilds()
37 SkipValidationInDebugBuilds =
true;
44 public static bool SkipValidationInDebugBuilds;
46 public static bool CanWrite(
string path,
bool isDirectory)
48 string getFullPath(
string p)
49 => System.IO.Path.GetFullPath(p).CleanUpPath();
51 path = getFullPath(path);
55 string workshopStagingDir = getFullPath(SteamManager.Workshop.PublishStagingDir);
56 string tempDownloadDir = getFullPath(ModReceiver.DownloadFolder);
61 Identifier extension = System.IO.Path.GetExtension(path).Replace(
" ",
"").ToIdentifier();
63 bool pathStartsWith(
string prefix)
64 => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
66 if (!pathStartsWith(workshopModsDir)
67 && !pathStartsWith(localModsDir)
69 && !pathStartsWith(tempDownloadDir)
70 && !pathStartsWith(workshopStagingDir)
72 && unwritableExtensions.Any(e => e == extension))
78 foreach (var unwritableDir
in unwritableDirs)
80 string dir = System.IO.Path.GetFullPath(unwritableDir.Value).CleanUpPath();
82 if (path.StartsWith(dir, StringComparison.InvariantCultureIgnoreCase))
85 return SkipValidationInDebugBuilds;
96 public static class SafeXML
98 public static void SaveSafe(
99 this System.Xml.Linq.XDocument doc,
101 System.Xml.Linq.SaveOptions saveOptions = System.Xml.Linq.SaveOptions.None,
102 bool throwExceptions =
false)
104 if (!Validation.CanWrite(path,
false))
106 string errorMsg = $
"Cannot save XML document to \"{path}\": modifying the files in this folder/with this extension is not allowed.";
109 throw new InvalidOperationException(errorMsg);
113 DebugConsole.ThrowError(errorMsg);
117 doc.Save(path, saveOptions);
120 public static void SaveSafe(
this System.Xml.Linq.XElement element,
string path,
bool throwExceptions =
false)
122 if (!Validation.CanWrite(path,
false))
124 string errorMsg = $
"Cannot save XML element to \"{path}\": modifying the files in this folder/with this extension is not allowed.";
127 throw new InvalidOperationException(errorMsg);
131 DebugConsole.ThrowError(errorMsg);
138 public static void SaveSafe(
this System.Xml.Linq.XDocument doc, XmlWriter writer)
143 public static void WriteTo(
this System.Xml.Linq.XDocument doc, XmlWriter writer)
153 public XmlWriter(
string path, System.Xml.XmlWriterSettings settings)
155 if (!Validation.CanWrite(path,
false))
157 DebugConsole.ThrowError($
"Cannot write XML document to \"{path}\": modifying the files in this folder/with this extension is not allowed.");
169 public void Write(System.Xml.Linq.XDocument doc)
173 DebugConsole.ThrowError(
"Cannot write to invalid XmlWriter");
183 DebugConsole.ThrowError(
"Cannot flush invalid XmlWriter");
193 DebugConsole.ThrowError(
"Cannot dispose invalid XmlWriter");
200 public static class XmlWriterExtensions
202 public static void Save(
this System.Xml.Linq.XDocument doc, XmlWriter writer)
204 doc.Save(writer.Writer ??
throw new NullReferenceException(
"Unable to save XML document: XML writer is null."));
208 public static class Path
210 public static readonly
char DirectorySeparatorChar = System.IO.Path.DirectorySeparatorChar;
211 public static readonly
char AltDirectorySeparatorChar = System.IO.Path.AltDirectorySeparatorChar;
213 public static string GetExtension(
string path) => System.IO.Path.GetExtension(path);
215 public static string GetFileNameWithoutExtension(
string path) => System.IO.Path.GetFileNameWithoutExtension(path);
217 public static string? GetPathRoot(
string? path) => System.IO.Path.GetPathRoot(path);
219 public static string GetRelativePath(
string relativeTo,
string path) => System.IO.Path.GetRelativePath(relativeTo, path);
221 public static string GetDirectoryName(ContentPath path) => GetDirectoryName(path.Value)!;
223 public static string? GetDirectoryName(
string path) => System.IO.Path.GetDirectoryName(path);
225 public static string GetFileName(
string path) => System.IO.Path.GetFileName(path);
227 public static string GetFullPath(
string path) => System.IO.Path.GetFullPath(path);
229 public static string Combine(params
string[] s) => System.IO.Path.Combine(s);
231 public static string GetTempFileName() => System.IO.Path.GetTempFileName();
233 public static bool IsPathRooted(
string path) => System.IO.Path.IsPathRooted(path);
235 private static readonly ImmutableHashSet<char> invalidFileNameChars = ImmutableHashSet.Create
237 '\"',
'<',
'>',
'|',
'\0',
238 (
char)1, (
char)2, (
char)3, (
char)4, (
char)5, (
char)6, (
char)7, (
char)8, (
char)9, (
char)10,
239 (
char)11, (
char)12, (
char)13, (
char)14, (
char)15, (
char)16, (
char)17, (
char)18, (
char)19, (
char)20,
240 (
char)21, (
char)22, (
char)23, (
char)24, (
char)25, (
char)26, (
char)27, (
char)28, (
char)29, (
char)30,
241 (
char)31,
':',
'*',
'?',
'\\',
'/'
247 public static ImmutableHashSet<char> GetInvalidFileNameCharsCrossPlatform() => invalidFileNameChars;
250 public static class Directory
252 public static string GetCurrentDirectory()
255 return System.IO.Directory.GetCurrentDirectory();
258 public static void SetCurrentDirectory(
string path)
261 System.IO.Directory.SetCurrentDirectory(path);
264 private static readonly EnumerationOptions IgnoreInaccessibleSystemAndHidden =
new EnumerationOptions
266 MatchType = MatchType.Win32,
267 AttributesToSkip = FileAttributes.System | FileAttributes.Hidden,
268 IgnoreInaccessible =
true
271 private static EnumerationOptions GetEnumerationOptions(
bool ignoreInaccessible,
bool recursive)
273 return new EnumerationOptions
275 MatchType = MatchType.Win32,
276 AttributesToSkip = FileAttributes.System | FileAttributes.Hidden,
277 IgnoreInaccessible = ignoreInaccessible,
278 RecurseSubdirectories = recursive
282 public static string[] GetFiles(
string path)
284 return System.IO.Directory.GetFiles(path,
"*", IgnoreInaccessibleSystemAndHidden);
287 public static string[] GetFiles(
string path,
string pattern, SearchOption option = SearchOption.AllDirectories)
289 EnumerationOptions enumerationOptions = GetEnumerationOptions(ignoreInaccessible:
true, option == SearchOption.AllDirectories);
290 return System.IO.Directory.GetFiles(path, pattern, enumerationOptions);
293 public static string[] GetDirectories(
string path,
string searchPattern =
"*")
295 return System.IO.Directory.GetDirectories(path, searchPattern, IgnoreInaccessibleSystemAndHidden);
298 public static string[] GetFileSystemEntries(
string path)
300 return System.IO.Directory.GetFileSystemEntries(path,
"*", IgnoreInaccessibleSystemAndHidden);
303 public static IEnumerable<string> EnumerateDirectories(
string path,
string pattern)
305 return System.IO.Directory.EnumerateDirectories(path, pattern, IgnoreInaccessibleSystemAndHidden);
308 public static IEnumerable<string> EnumerateFiles(
string path,
string pattern)
310 return System.IO.Directory.EnumerateFiles(path, pattern, IgnoreInaccessibleSystemAndHidden);
313 public static bool Exists(
string path)
315 return System.IO.Directory.Exists(path);
318 public static System.IO.DirectoryInfo? CreateDirectory(
string path,
bool catchUnauthorizedAccessExceptions =
false)
320 if (!Validation.CanWrite(path,
true))
322 DebugConsole.ThrowError($
"Cannot create directory \"{path}\": modifying the contents of this folder/using this extension is not allowed.");
323 Validation.CanWrite(path,
true);
328 return System.IO.Directory.CreateDirectory(path);
330 catch (UnauthorizedAccessException e)
332 DebugConsole.ThrowError($
"Cannot create directory at \"{path}\": unauthorized access. The file/folder might be read-only!", e);
333 if (!catchUnauthorizedAccessExceptions) {
throw; }
338 public static void Delete(
string path,
bool recursive =
true,
bool catchUnauthorizedAccessExceptions =
true)
340 if (!Validation.CanWrite(path,
true))
342 DebugConsole.ThrowError($
"Cannot delete directory \"{path}\": modifying the contents of this folder/using this extension is not allowed.");
348 System.IO.Directory.Delete(path, recursive);
350 catch (UnauthorizedAccessException e)
352 DebugConsole.ThrowError($
"Cannot delete \"{path}\": unauthorized access. The file/folder might be read-only!", e);
353 if (!catchUnauthorizedAccessExceptions) {
throw; }
357 public static bool TryDelete(
string path,
bool recursive =
true)
361 Delete(path, recursive, catchUnauthorizedAccessExceptions:
false);
370 public static DateTime GetLastWriteTime(
string path,
bool catchUnauthorizedAccessExceptions =
true)
374 return System.IO.Directory.GetLastWriteTime(path);
376 catch (UnauthorizedAccessException e)
378 DebugConsole.ThrowError($
"Cannot get last write time at \"{path}\": unauthorized access. The file/folder might be read-only!", e);
379 if (!catchUnauthorizedAccessExceptions) {
throw; }
380 return new DateTime();
384 public static void Copy(
string src,
string dest,
bool overwrite =
false)
386 if (!Validation.CanWrite(dest,
true))
388 DebugConsole.ThrowError($
"Cannot copy \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed.");
392 CreateDirectory(dest);
394 foreach (
string path
in GetFiles(src))
396 File.Copy(path, Path.Combine(dest, Path.GetRelativePath(src, path)), overwrite);
399 foreach (
string path
in GetDirectories(src))
401 Copy(path, Path.Combine(dest, Path.GetRelativePath(src, path)), overwrite);
405 public static void Move(
string src,
string dest,
bool overwrite =
false)
407 if (!overwrite && Exists(dest))
409 DebugConsole.ThrowError($
"Cannot move \"{src}\" to \"{dest}\": destination folder already exists.");
413 if (!Validation.CanWrite(src,
true))
415 DebugConsole.ThrowError($
"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the source folder is not allowed.");
419 if (!Validation.CanWrite(dest,
true))
421 DebugConsole.ThrowError($
"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed.");
425 if (!overwrite || !Exists(dest) || TryDelete(dest))
427 System.IO.Directory.Move(src, dest);
432 public static class File
434 public static bool Exists(ContentPath path) => Exists(path.Value);
436 public static bool Exists(
string path) => System.IO.File.Exists(path);
438 public static void Copy(
string src,
string dest,
bool overwrite =
false,
bool catchUnauthorizedAccessExceptions =
true)
440 if (!Validation.CanWrite(dest,
false))
442 DebugConsole.ThrowError($
"Cannot copy \"{src}\" to \"{dest}\": modifying the contents of this folder/using this extension is not allowed.");
447 System.IO.File.Copy(src, dest, overwrite);
449 catch (UnauthorizedAccessException e)
451 DebugConsole.ThrowError($
"Cannot copy \"{src}\" to \"{dest}\": unauthorized access. The file/folder might be read-only!", e);
452 if (!catchUnauthorizedAccessExceptions) {
throw; }
456 public static void Move(
string src,
string dest,
bool catchUnauthorizedAccessExceptions =
true)
458 if (!Validation.CanWrite(src,
false))
460 DebugConsole.ThrowError($
"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the source folder is not allowed.");
463 if (!Validation.CanWrite(dest,
false))
465 DebugConsole.ThrowError($
"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed");
470 System.IO.File.Move(src, dest);
472 catch (UnauthorizedAccessException e)
474 DebugConsole.ThrowError($
"Cannot move \"{src}\" to \"{dest}\": unauthorized access. The file/folder might be read-only!", e);
475 if (!catchUnauthorizedAccessExceptions) {
throw; }
479 public static void Delete(ContentPath path,
bool catchUnauthorizedAccessExceptions =
true) => Delete(path.Value, catchUnauthorizedAccessExceptions);
481 public static void Delete(
string path,
bool catchUnauthorizedAccessExceptions =
true)
483 if (!Validation.CanWrite(path,
false))
485 DebugConsole.ThrowError($
"Cannot delete file \"{path}\": modifying the contents of this folder/using this extension is not allowed.");
490 System.IO.File.Delete(path);
492 catch (UnauthorizedAccessException e)
494 DebugConsole.ThrowError($
"Cannot delete {path}: unauthorized access. The file/folder might be read-only!", e);
495 if (!catchUnauthorizedAccessExceptions) {
throw; }
499 public static DateTime GetLastWriteTime(
string path)
501 return System.IO.File.GetLastWriteTime(path);
504 public static FileStream? Open(
506 System.IO.FileMode mode,
507 System.IO.FileAccess access = System.IO.FileAccess.ReadWrite,
508 System.IO.FileShare? share =
null,
509 bool catchUnauthorizedAccessExceptions =
true)
513 case System.IO.FileMode.Create:
514 case System.IO.FileMode.CreateNew:
515 case System.IO.FileMode.OpenOrCreate:
516 case System.IO.FileMode.Append:
517 case System.IO.FileMode.Truncate:
518 if (!Validation.CanWrite(path,
false))
520 DebugConsole.ThrowError($
"Cannot open \"{path}\" in {mode} mode: modifying the contents of this folder/using this extension is not allowed.");
526 !Validation.CanWrite(path,
false) ?
527 System.IO.FileAccess.Read :
529 var shareVal = share ?? (access == System.IO.FileAccess.Read ? System.IO.FileShare.Read : System.IO.FileShare.None);
532 return new FileStream(path, System.IO.File.Open(path, mode, access, shareVal));
534 catch (UnauthorizedAccessException e)
536 DebugConsole.ThrowError($
"Cannot open {path} (stream): unauthorized access. The file/folder might be read-only!", e);
537 if (!catchUnauthorizedAccessExceptions) {
throw; }
542 public static FileStream? OpenRead(
string path,
bool catchUnauthorizedAccessExceptions =
true)
544 return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, catchUnauthorizedAccessExceptions: catchUnauthorizedAccessExceptions);
547 public static FileStream? OpenWrite(
string path,
bool catchUnauthorizedAccessExceptions =
true)
549 return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write, catchUnauthorizedAccessExceptions: catchUnauthorizedAccessExceptions);
552 public static FileStream? Create(
string path,
bool catchUnauthorizedAccessExceptions =
true)
554 return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write, catchUnauthorizedAccessExceptions: catchUnauthorizedAccessExceptions);
557 public static void WriteAllBytes(
string path,
byte[] contents,
bool catchUnauthorizedAccessExceptions =
true)
559 if (!Validation.CanWrite(path,
false))
561 DebugConsole.ThrowError($
"Cannot write all bytes to \"{path}\": modifying the files in this folder/with this extension is not allowed.");
566 System.IO.File.WriteAllBytes(path, contents);
568 catch (UnauthorizedAccessException e)
570 DebugConsole.ThrowError($
"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e);
571 if (!catchUnauthorizedAccessExceptions) {
throw; }
575 public static void WriteAllText(
string path,
string contents, System.Text.Encoding? encoding =
null,
bool catchUnauthorizedAccessExceptions =
true)
577 if (!Validation.CanWrite(path,
false))
579 DebugConsole.ThrowError($
"Cannot write all text to \"{path}\": modifying the files in this folder/with this extension is not allowed.");
584 System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8);
586 catch (UnauthorizedAccessException e)
588 DebugConsole.ThrowError($
"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e);
589 if (!catchUnauthorizedAccessExceptions) {
throw; }
593 public static void WriteAllLines(
string path, IEnumerable<string> contents, System.Text.Encoding? encoding =
null,
bool catchUnauthorizedAccessExceptions =
true)
595 if (!Validation.CanWrite(path,
false))
597 DebugConsole.ThrowError($
"Cannot write all lines to \"{path}\": modifying the files in this folder/with this extension is not allowed.");
602 System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8);
604 catch (UnauthorizedAccessException e)
606 DebugConsole.ThrowError($
"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e);
607 if (!catchUnauthorizedAccessExceptions) {
throw; }
611 public static byte[] ReadAllBytes(
string path,
bool catchUnauthorizedAccessExceptions =
true)
615 return System.IO.File.ReadAllBytes(path);
617 catch (UnauthorizedAccessException e)
619 DebugConsole.ThrowError($
"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e);
620 if (!catchUnauthorizedAccessExceptions) {
throw; }
621 return Array.Empty<
byte>();
625 public static string ReadAllText(
string path, System.Text.Encoding? encoding =
null,
bool catchUnauthorizedAccessExceptions =
true)
629 return System.IO.File.ReadAllText(path, encoding ?? System.Text.Encoding.UTF8);
631 catch (UnauthorizedAccessException e)
633 DebugConsole.ThrowError($
"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e);
634 if (!catchUnauthorizedAccessExceptions) {
throw; }
639 public static string[] ReadAllLines(
string path, System.Text.Encoding? encoding =
null,
bool catchUnauthorizedAccessExceptions =
true)
643 return System.IO.File.ReadAllLines(path, encoding ?? System.Text.Encoding.UTF8);
645 catch (UnauthorizedAccessException e)
647 DebugConsole.ThrowError($
"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e);
648 if (!catchUnauthorizedAccessExceptions) {
throw; }
649 return Array.Empty<
string>();
652 public static string SanitizeName(
string str)
654 string sanitized =
"";
655 foreach (
char c
in str)
657 char newChar = Path.GetInvalidFileNameCharsCrossPlatform().Contains(c) ?
'-' : c;
658 sanitized += newChar;
666 private readonly System.IO.
FileStream innerStream;
667 private readonly
string fileName;
671 innerStream = stream;
675 public override bool CanRead => innerStream.CanRead;
676 public override bool CanSeek => innerStream.CanSeek;
682 if (!Validation.CanWrite(fileName,
false)) {
return false; }
687 public override long Length => innerStream.Length;
701 public override int Read(
byte[] buffer,
int offset,
int count)
703 return innerStream.
Read(buffer, offset, count);
706 public override void Write(
byte[] buffer,
int offset,
int count)
708 if (Validation.CanWrite(fileName,
false))
710 innerStream.
Write(buffer, offset, count);
714 DebugConsole.ThrowError($
"Cannot write to file \"{fileName}\": modifying the files in this folder/with this extension is not allowed.");
718 public override long Seek(
long offset, System.IO.SeekOrigin origin)
720 return innerStream.
Seek(offset, origin);
733 protected override void Dispose(
bool notCalledByFinalizer)
735 if (notCalledByFinalizer) { innerStream.
Dispose(); }
754 public string Name => innerInfo.Name;
757 public System.IO.FileAttributes
Attributes => innerInfo.Attributes;
762 foreach (var dir
in dirs)
771 foreach (var file
in files)
779 if (!Validation.CanWrite(innerInfo.
FullName,
false))
781 DebugConsole.ThrowError($
"Cannot delete directory \"{Name}\": modifying the contents of this folder/using this extension is not allowed.");
790 private System.IO.
FileInfo innerInfo;
794 innerInfo =
new System.IO.
FileInfo(path);
803 public string Name => innerInfo.Name;
815 if (!Validation.CanWrite(innerInfo.
FullName,
false))
817 DebugConsole.ThrowError($
"Cannot set read-only to {value} for \"{Name}\": modifying the files in this folder/with this extension is not allowed.");
824 public void CopyTo(
string dest,
bool overwriteExisting =
false)
826 if (!Validation.CanWrite(dest,
false))
828 DebugConsole.ThrowError($
"Cannot copy \"{Name}\" to \"{dest}\": modifying the contents of the destination folder is not allowed.");
831 innerInfo.
CopyTo(dest, overwriteExisting);
836 if (!Validation.CanWrite(innerInfo.
FullName,
false))
838 DebugConsole.ThrowError($
"Cannot delete file \"{Name}\": modifying the files in this folder/with this extension is not allowed.");
static readonly string WorkshopModsDir
const string LocalModsDir
DirectoryInfo(string path)
System.IO.FileAttributes Attributes
IEnumerable< FileInfo > GetFiles()
IEnumerable< DirectoryInfo > GetDirectories()
FileInfo(System.IO.FileInfo info)
void CopyTo(string dest, bool overwriteExisting=false)
FileStream(string fn, System.IO.FileStream stream)
override long Seek(long offset, System.IO.SeekOrigin origin)
override void Write(byte[] buffer, int offset, int count)
override void SetLength(long value)
override void Dispose(bool notCalledByFinalizer)
override int Read(byte[] buffer, int offset, int count)
XmlWriter(string path, System.Xml.XmlWriterSettings settings)
static XmlWriter Create(string path, System.Xml.XmlWriterSettings settings)
readonly System.Xml.? XmlWriter Writer
void Write(System.Xml.Linq.XDocument doc)