Files
SabreTools.IO/SabreTools.IO.Extensions.Test/IOExtensionsTests.cs

811 lines
27 KiB
C#
Raw Permalink Normal View History

2024-11-25 16:52:49 -05:00
using System;
using System.Collections.Generic;
2024-11-25 16:52:49 -05:00
using System.IO;
using System.Text;
2024-03-05 10:45:31 -05:00
using Xunit;
2026-03-18 15:47:44 -04:00
namespace SabreTools.IO.Extensions.Test
2024-03-05 10:45:31 -05:00
{
public class IOExtensionsTests
{
2026-03-18 13:44:43 -04:00
#region EnsureDirectory
2024-11-25 16:52:49 -05:00
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", " ")] // TODO: This is a bad result
[InlineData("dirname", "dirname")]
[InlineData("\"dirname\"", "dirname")]
2026-03-18 13:44:43 -04:00
public void EnsureDirectoryTest(string? dir, string? expected)
2024-11-25 16:52:49 -05:00
{
// Handle test setup
expected ??= PathTool.GetRuntimeDirectory();
2026-01-25 17:04:11 -05:00
if (expected is not null)
2024-11-25 16:52:49 -05:00
expected = Path.GetFullPath(expected);
2026-03-18 13:44:43 -04:00
string actual = dir.EnsureDirectory(create: false);
2024-11-25 16:52:49 -05:00
Assert.Equal(expected, actual);
}
#endregion
#region GetDirectoriesOnly
[Fact]
public void GetDirectoriesOnly_NoAppendParent()
{
2026-07-09 15:45:00 -04:00
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
string expectedCurrent = Path.Combine(expectedParent, "Subdirectory");
List<string> inputs =
[
string.Empty,
2026-07-09 15:45:00 -04:00
Path.Combine(Environment.CurrentDirectory, "TestData", "Path"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "Subdir*"),
];
var actual = IOExtensions.GetDirectoriesOnly(inputs, appendParent: true);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(expectedParent, first.ParentPath);
}
[Fact]
public void GetDirectoriesOnly_AppendParent()
{
2026-07-09 15:45:00 -04:00
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
string expectedCurrent = Path.Combine(expectedParent, "Subdirectory");
List<string> inputs =
[
string.Empty,
2026-07-09 15:45:00 -04:00
Path.Combine(Environment.CurrentDirectory, "TestData", "Path"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "Subdir*"),
];
var actual = IOExtensions.GetDirectoriesOnly(inputs, appendParent: false);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(string.Empty, first.ParentPath);
}
#endregion
#region ListEmpty
[Fact]
public void ListEmpty_NullDirectory()
{
string? dir = null;
var empty = dir.ListEmpty();
Assert.Null(empty);
}
[Fact]
public void ListEmpty_InvalidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "INVALID");
var empty = dir.ListEmpty();
Assert.Null(empty);
}
[Fact]
public void ListEmpty_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
var empty = dir.ListEmpty();
Assert.NotNull(empty);
Assert.Empty(empty);
}
#endregion
2026-03-24 09:57:40 -04:00
#region Concatenate
[Fact]
public void Concatenate_EmptyList_False()
{
List<string> paths = [];
string output = string.Empty;
bool actual = IOExtensions.Concatenate(paths, output);
Assert.False(actual);
}
[Fact]
public void Concatenate_InvalidOutput_False()
{
List<string> paths = ["a"];
string output = string.Empty;
bool actual = IOExtensions.Concatenate(paths, output);
Assert.False(actual);
}
[Fact]
public void Concatenate_FilledList_True()
{
List<string> paths = [
2026-07-09 15:58:03 -04:00
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "ascii.txt"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "file-to-compress.bin"),
2026-03-24 09:57:40 -04:00
];
string output = Guid.NewGuid().ToString();
bool actual = IOExtensions.Concatenate(paths, output);
Assert.True(actual);
string text = File.ReadAllText(output);
Assert.Equal("This doesn't match anythingThis is just a file that has a known set of hashes to make sure that everything with hashing is still working as anticipated.", text);
File.Delete(output);
}
#endregion
#region GetEncoding
2024-11-25 16:52:49 -05:00
[Fact]
public void GetEncoding_EmptyPath()
{
string path = "";
Encoding expected = Encoding.Default;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_InvalidPath()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "INVALID");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.Default;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
// Disable warning about UTF7 usage
#pragma warning disable SYSLIB0001
[Fact]
public void GetEncoding_UTF7()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf7bom.txt");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.UTF7;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
#pragma warning restore SYSLIB0001
[Fact]
public void GetEncoding_UTF8()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf8bom.txt");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.UTF8;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_Unicode()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf16lebom.txt");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.Unicode;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_BigEndianUnicode()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf16bebom.txt");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.BigEndianUnicode;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_UTF32()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf32bom.txt");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.UTF32;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
[Fact]
public void GetEncoding_ASCII()
{
2026-07-09 15:58:03 -04:00
string path = Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "ascii.txt");
2024-11-25 16:52:49 -05:00
Encoding expected = Encoding.Default;
var actual = path.GetEncoding();
Assert.Equal(expected, actual);
}
#endregion
#region GetFilesOnly
[Fact]
public void GetFilesOnly_NoAppendParent()
{
2026-07-09 15:45:00 -04:00
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
string expectedCurrent = Path.Combine(expectedParent, "ascii.txt");
List<string> inputs =
[
string.Empty,
2026-07-09 15:45:00 -04:00
Path.Combine(Environment.CurrentDirectory, "TestData", "Path"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "Subdir*"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf8bom.txt"),
];
var actual = IOExtensions.GetFilesOnly(inputs, appendParent: true);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(expectedParent, first.ParentPath);
}
[Fact]
public void GetFilesOnly_AppendParent()
{
2026-07-09 15:45:00 -04:00
string expectedParent = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
string expectedCurrent = Path.Combine(expectedParent, "ascii.txt");
List<string> inputs =
[
string.Empty,
2026-07-09 15:45:00 -04:00
Path.Combine(Environment.CurrentDirectory, "TestData", "Path"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "Subdir*"),
Path.Combine(Environment.CurrentDirectory, "TestData", "Path", "utf8bom.txt"),
];
var actual = IOExtensions.GetFilesOnly(inputs, appendParent: false);
Assert.NotEmpty(actual);
var first = actual[0];
Assert.Equal(expectedCurrent, first.CurrentPath);
Assert.Equal(string.Empty, first.ParentPath);
}
#endregion
2026-03-22 17:17:13 -04:00
#region FileSize
[Fact]
public void FileSize_Null_Invalid()
{
string? filename = null;
long actual = filename.GetFileSize();
Assert.Equal(-1, actual);
}
[Fact]
public void FileSize_Empty_Invalid()
{
string? filename = string.Empty;
long actual = filename.GetFileSize();
Assert.Equal(-1, actual);
}
[Fact]
public void FileSize_Invalid_Invalid()
{
string? filename = "INVALID";
long actual = filename.GetFileSize();
Assert.Equal(-1, actual);
}
#endregion
#region GetNormalizedExtension
2024-11-25 16:52:49 -05:00
2024-03-05 10:45:31 -05:00
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
[InlineData("no-extension", null)]
[InlineData("NO-EXTENSION", null)]
[InlineData("no-extension.", null)]
[InlineData("NO-EXTENSION.", null)]
[InlineData("filename.ext", "ext")]
[InlineData("FILENAME.EXT", "ext")]
2024-11-25 16:52:49 -05:00
public void GetNormalizedExtensionTest(string? path, string? expected)
2024-03-05 10:45:31 -05:00
{
string? actual = path.GetNormalizedExtension();
Assert.Equal(expected, actual);
}
2024-11-25 16:52:49 -05:00
#endregion
#region NormalizeFilePath
2026-03-18 13:44:43 -04:00
[Theory]
[InlineData(null, false, "")]
[InlineData(null, true, "")]
[InlineData("", false, "")]
[InlineData("", true, "")]
[InlineData("filename.bin", false, "filename.bin")]
[InlineData("filename.bin", true, "filename.bin")]
[InlineData("\"filename.bin\"", false, "filename.bin")]
[InlineData("\"filename.bin\"", true, "filename.bin")]
[InlineData("<filename.bin>", false, "filename.bin")]
[InlineData("<filename.bin>", true, "filename.bin")]
[InlineData("1.2.3.4..bin", false, "1.2.3.4..bin")]
[InlineData("1.2.3.4..bin", true, "1.2.3.4..bin")]
[InlineData("dir/filename.bin", false, "dir/filename.bin")]
[InlineData("dir/filename.bin", true, "dir/filename.bin")]
[InlineData(" dir / filename.bin", false, "dir/filename.bin")]
[InlineData(" dir / filename.bin", true, "dir/filename.bin")]
[InlineData("\0dir/\0filename.bin", false, "_dir/_filename.bin")]
[InlineData("\0dir/\0filename.bin", true, "_dir/_filename.bin")]
public void NormalizeOutputPathsTest(string? path, bool getFullPath, string expected)
{
// Modify expected to account for test data if necessary
if (getFullPath && !string.IsNullOrEmpty(expected))
expected = Path.GetFullPath(expected);
string actual = path.NormalizeFilePath(getFullPath);
Assert.Equal(expected, actual);
}
#endregion
2026-07-02 10:01:18 -04:00
#region ResolvePath
[Theory]
[InlineData(null)]
[InlineData("")]
public void ResolvePath_NullOrEmpty_ReturnsNull(string? path)
{
string? actual = path.ResolvePath();
Assert.Null(actual);
}
// TODO: Add tests for home path without modifying the runner's home path
[Fact]
public void ResolvePath_AbsolutePath_Exists_ReturnsFullPath()
2026-07-02 10:01:18 -04:00
{
string filename = Guid.NewGuid().ToString();
string path = Path.Combine(Environment.CurrentDirectory, filename);
2026-07-02 10:01:18 -04:00
File.WriteAllBytes(path, []);
try
{
string? actual = filename.ResolvePath();
Assert.Equal(path, actual);
}
finally
{
File.Delete(path);
}
2026-07-02 10:01:18 -04:00
}
[Fact]
public void ResolvePath_AbsolutePath_Missing_ReturnsNull()
{
string filename = Guid.NewGuid().ToString();
string path = Path.Combine(Environment.CurrentDirectory, filename);
2026-07-02 10:01:18 -04:00
string? actual = path.ResolvePath();
Assert.Null(actual);
}
[Fact]
public void ResolvePath_RelativePath_Exists_ReturnsFullPath()
{
string filename = Guid.NewGuid().ToString();
string subDir = "RELATIVE";
string path = Path.Combine(subDir, filename);
string expected = Path.Combine(Environment.CurrentDirectory, path);
Directory.CreateDirectory(subDir);
File.WriteAllBytes(path, []);
try
{
string? actual = path.ResolvePath();
Assert.Equal(expected, actual);
}
finally
{
Directory.Delete(subDir, recursive: true);
}
}
2026-07-02 10:01:18 -04:00
[Fact]
public void ResolvePath_RelativePath_Missing_ReturnsNull()
{
string filename = Guid.NewGuid().ToString();
string subDir = "INVALID";
string path = Path.Combine(subDir, filename);
2026-07-02 10:01:18 -04:00
string? actual = path.ResolvePath();
Assert.Null(actual);
}
[Fact]
public void ResolvePath_BareName_RuntimeDirectory_ReturnsFullPath()
{
string filename = Guid.NewGuid().ToString();
2026-07-02 10:01:18 -04:00
string path = Path.Combine(AppContext.BaseDirectory, filename);
File.WriteAllBytes(path, []);
try
{
string? actual = filename.ResolvePath();
Assert.Equal(path, actual);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ResolvePath_BareName_Path_ReturnsFullPath()
{
string filename = Guid.NewGuid().ToString();
string subDir = Path.GetFullPath("TEMP");
string path = Path.Combine(subDir, filename);
string expected = Path.Combine(Environment.CurrentDirectory, path);
2026-07-02 10:01:18 -04:00
Directory.CreateDirectory(subDir);
2026-07-02 10:01:18 -04:00
File.WriteAllBytes(path, []);
// Prepend the temp directory to PATH and check it exists
// TODO: See if there's a way around changing the actual environment variables
string? originalPath = Environment.GetEnvironmentVariable("PATH");
try
{
Environment.SetEnvironmentVariable("PATH", subDir + Path.PathSeparator + (originalPath ?? string.Empty));
2026-07-02 10:01:18 -04:00
string? actual = filename.ResolvePath();
Assert.Equal(expected, actual);
2026-07-02 10:01:18 -04:00
}
finally
{
Environment.SetEnvironmentVariable("PATH", originalPath);
Directory.Delete(subDir, recursive: true);
2026-07-02 10:01:18 -04:00
}
}
[Fact]
public void ResolvePath_BareName_Missing_ReturnsNull()
{
string filename = Guid.NewGuid().ToString();
2026-07-02 10:01:18 -04:00
string? actual = filename.ResolvePath();
Assert.Null(actual);
}
#endregion
#region SafeGetDirectories
2024-11-25 16:52:49 -05:00
[Fact]
public void SafeGetDirectories_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var dirs = dir.SafeGetDirectories();
Assert.Single(dirs);
}
[Fact]
public void SafeGetDirectories_ValidDirectory_Pattern()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var dirs = dir.SafeGetDirectories("*");
Assert.Single(dirs);
}
[Fact]
public void SafeGetDirectories_ValidDirectory_PatternOption()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var dirs = dir.SafeGetDirectories("*", SearchOption.AllDirectories);
Assert.Single(dirs);
}
Do not follow linked directories in SafeGetFiles outside of Windows (#8) * Do not follow linked directories in SafeGetFiles outside of Windows SafeGetFiles with SearchOption.AllDirectories follows directory symlinks. On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose target reads as empty, and each one resolves to its own folder, so the recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276). Outside of Windows, walk the tree one directory at a time and do not enter directories that carry FileAttributes.ReparsePoint. Files are matched with Directory.GetFiles per directory, so the pattern behaves as before, and the order is the same breadth-first order. Windows keeps Directory.GetFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not follow linked directories in the other AllDirectories overloads SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in the same way. On Linux, with one directory linked to itself, SafeEnumerateFiles returns 43 files instead of 2, and with three such links it is still running after 10 seconds. The walk from SafeGetFiles is now a lazy iterator that all of them share, so the Enumerate wrappers stay lazy. Each directory is listed with the matching overload without SearchOption (Directory.GetFiles, SafeEnumerateFiles and so on), so patterns and inaccessible directories behave as in the recursive search. If the attributes of a subdirectory cannot be read, such as below a directory without execute permission, it is entered anyway, so it fails in the same way as in the recursive search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for linked directories without catching exceptions DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00
[Fact]
public void SafeGetDirectories_LinkedDirectory_NotFollowed()
{
// Linked directories are only skipped outside of Windows
if (SystemEnvironment.IsWindows())
return;
string root = CreateLinkedDirectoryTree();
try
{
var dirs = root.SafeGetDirectories("*", SearchOption.AllDirectories);
Assert.Equal(2, dirs.Length);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
#endregion
#region SafeGetFiles
2024-11-25 16:52:49 -05:00
[Fact]
public void SafeGetFiles_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var files = dir.SafeGetFiles();
Assert.NotEmpty(files);
}
[Fact]
public void SafeGetFiles_ValidDirectory_Pattern()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var files = dir.SafeGetFiles("*");
Assert.NotEmpty(files);
}
[Fact]
public void SafeGetFiles_ValidDirectory_PatternOption()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var files = dir.SafeGetFiles("*", SearchOption.AllDirectories);
Assert.NotEmpty(files);
}
Do not follow linked directories in SafeGetFiles outside of Windows (#8) * Do not follow linked directories in SafeGetFiles outside of Windows SafeGetFiles with SearchOption.AllDirectories follows directory symlinks. On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose target reads as empty, and each one resolves to its own folder, so the recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276). Outside of Windows, walk the tree one directory at a time and do not enter directories that carry FileAttributes.ReparsePoint. Files are matched with Directory.GetFiles per directory, so the pattern behaves as before, and the order is the same breadth-first order. Windows keeps Directory.GetFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not follow linked directories in the other AllDirectories overloads SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in the same way. On Linux, with one directory linked to itself, SafeEnumerateFiles returns 43 files instead of 2, and with three such links it is still running after 10 seconds. The walk from SafeGetFiles is now a lazy iterator that all of them share, so the Enumerate wrappers stay lazy. Each directory is listed with the matching overload without SearchOption (Directory.GetFiles, SafeEnumerateFiles and so on), so patterns and inaccessible directories behave as in the recursive search. If the attributes of a subdirectory cannot be read, such as below a directory without execute permission, it is entered anyway, so it fails in the same way as in the recursive search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for linked directories without catching exceptions DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00
[Fact]
public void SafeGetFiles_ValidDirectory_PatternTopDirectoryOnly()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
var files = dir.SafeGetFiles("*", SearchOption.TopDirectoryOnly);
Assert.Equal(11, files.Length);
}
[Fact]
public void SafeGetFiles_ValidDirectory_PatternWithDirectory()
{
string dir = Path.Combine(Environment.CurrentDirectory, "TestData");
var files = dir.SafeGetFiles(Path.Combine("Path", "Subdirectory", "*"), SearchOption.AllDirectories);
Assert.Single(files);
}
[Fact]
public void SafeGetFiles_LinkedDirectory_NotFollowed()
{
// Linked directories are only skipped outside of Windows
if (SystemEnvironment.IsWindows())
return;
string root = CreateLinkedDirectoryTree();
try
{
var files = root.SafeGetFiles("*", SearchOption.AllDirectories);
Assert.Equal(3, files.Length);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
#endregion
#region SafeGetFileSystemEntries
2024-11-25 16:52:49 -05:00
[Fact]
public void SafeGetFileSystemEntries_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var entries = dir.SafeGetFileSystemEntries();
Assert.NotEmpty(entries);
}
[Fact]
public void SafeGetFileSystemEntries_ValidDirectory_Pattern()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var entries = dir.SafeGetFileSystemEntries("*");
Assert.NotEmpty(entries);
}
[Fact]
public void SafeGetFileSystemEntries_ValidDirectory_PatternOption()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var entries = dir.SafeGetFileSystemEntries("*", SearchOption.AllDirectories);
Assert.NotEmpty(entries);
}
#endregion
#region SafeEnumerateDirectories
2024-11-25 16:52:49 -05:00
[Fact]
public void SafeEnumerateDirectories_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var dirs = dir.SafeEnumerateDirectories();
Assert.Single(dirs);
}
[Fact]
public void SafeEnumerateDirectories_ValidDirectory_Pattern()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var dirs = dir.SafeEnumerateDirectories("*");
Assert.Single(dirs);
}
[Fact]
public void SafeEnumerateDirectories_ValidDirectory_PatternOption()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var dirs = dir.SafeEnumerateDirectories("*", SearchOption.AllDirectories);
Assert.Single(dirs);
}
Do not follow linked directories in SafeGetFiles outside of Windows (#8) * Do not follow linked directories in SafeGetFiles outside of Windows SafeGetFiles with SearchOption.AllDirectories follows directory symlinks. On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose target reads as empty, and each one resolves to its own folder, so the recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276). Outside of Windows, walk the tree one directory at a time and do not enter directories that carry FileAttributes.ReparsePoint. Files are matched with Directory.GetFiles per directory, so the pattern behaves as before, and the order is the same breadth-first order. Windows keeps Directory.GetFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not follow linked directories in the other AllDirectories overloads SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in the same way. On Linux, with one directory linked to itself, SafeEnumerateFiles returns 43 files instead of 2, and with three such links it is still running after 10 seconds. The walk from SafeGetFiles is now a lazy iterator that all of them share, so the Enumerate wrappers stay lazy. Each directory is listed with the matching overload without SearchOption (Directory.GetFiles, SafeEnumerateFiles and so on), so patterns and inaccessible directories behave as in the recursive search. If the attributes of a subdirectory cannot be read, such as below a directory without execute permission, it is entered anyway, so it fails in the same way as in the recursive search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for linked directories without catching exceptions DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00
[Fact]
public void SafeEnumerateDirectories_LinkedDirectory_NotFollowed()
{
// Linked directories are only skipped outside of Windows
if (SystemEnvironment.IsWindows())
return;
string root = CreateLinkedDirectoryTree();
try
{
List<string> dirs = [.. root.SafeEnumerateDirectories("*", SearchOption.AllDirectories)];
Assert.Equal(2, dirs.Count);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
#endregion
#region SafeEnumerateFiles
2024-11-25 16:52:49 -05:00
[Fact]
public void SafeEnumerateFiles_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var files = dir.SafeEnumerateFiles();
Assert.NotEmpty(files);
}
[Fact]
public void SafeEnumerateFiles_ValidDirectory_Pattern()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var files = dir.SafeEnumerateFiles("*");
Assert.NotEmpty(files);
}
[Fact]
public void SafeEnumerateFiles_ValidDirectory_PatternOption()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var files = dir.SafeEnumerateFiles("*", SearchOption.AllDirectories);
Assert.NotEmpty(files);
}
Do not follow linked directories in SafeGetFiles outside of Windows (#8) * Do not follow linked directories in SafeGetFiles outside of Windows SafeGetFiles with SearchOption.AllDirectories follows directory symlinks. On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose target reads as empty, and each one resolves to its own folder, so the recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276). Outside of Windows, walk the tree one directory at a time and do not enter directories that carry FileAttributes.ReparsePoint. Files are matched with Directory.GetFiles per directory, so the pattern behaves as before, and the order is the same breadth-first order. Windows keeps Directory.GetFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not follow linked directories in the other AllDirectories overloads SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in the same way. On Linux, with one directory linked to itself, SafeEnumerateFiles returns 43 files instead of 2, and with three such links it is still running after 10 seconds. The walk from SafeGetFiles is now a lazy iterator that all of them share, so the Enumerate wrappers stay lazy. Each directory is listed with the matching overload without SearchOption (Directory.GetFiles, SafeEnumerateFiles and so on), so patterns and inaccessible directories behave as in the recursive search. If the attributes of a subdirectory cannot be read, such as below a directory without execute permission, it is entered anyway, so it fails in the same way as in the recursive search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for linked directories without catching exceptions DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00
[Fact]
public void SafeEnumerateFiles_LinkedDirectory_NotFollowed()
{
// Linked directories are only skipped outside of Windows
if (SystemEnvironment.IsWindows())
return;
string root = CreateLinkedDirectoryTree();
try
{
List<string> files = [.. root.SafeEnumerateFiles("*", SearchOption.AllDirectories)];
Assert.Equal(3, files.Count);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void SafeEnumerateFiles_DirectoryWithoutExecutePermission_Skipped()
{
// Execute permission on directories only exists outside of Windows
if (OperatingSystem.IsWindows())
return;
string root = Path.Combine(Environment.CurrentDirectory, Guid.NewGuid().ToString());
string locked = Path.Combine(root, "Locked");
string other = Path.Combine(root, "Other", "Subdirectory");
Directory.CreateDirectory(Path.Combine(locked, "Subdirectory"));
Directory.CreateDirectory(other);
File.WriteAllBytes(Path.Combine(locked, "file.bin"), []);
File.WriteAllBytes(Path.Combine(other, "file.bin"), []);
File.SetUnixFileMode(locked, UnixFileMode.UserRead | UnixFileMode.UserWrite);
try
{
// Only the subdirectory of the locked directory is left out
List<string> files = [.. root.SafeEnumerateFiles("*", SearchOption.AllDirectories)];
Assert.Equal(2, files.Count);
}
finally
{
File.SetUnixFileMode(locked, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
Directory.Delete(root, recursive: true);
}
}
#endregion
#region SafeEnumerateFileSystemEntries
2024-11-25 16:52:49 -05:00
[Fact]
public void SafeEnumerateFileSystemEntries_ValidDirectory()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var entries = dir.SafeEnumerateFileSystemEntries();
Assert.NotEmpty(entries);
}
[Fact]
public void SafeEnumerateFileSystemEntries_ValidDirectory_Pattern()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var entries = dir.SafeEnumerateFileSystemEntries("*");
Assert.NotEmpty(entries);
}
[Fact]
public void SafeEnumerateFileSystemEntries_ValidDirectory_PatternOption()
{
2026-07-09 15:45:00 -04:00
string dir = Path.Combine(Environment.CurrentDirectory, "TestData", "Path");
2024-11-25 16:52:49 -05:00
var entries = dir.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories);
Assert.NotEmpty(entries);
}
Do not follow linked directories in SafeGetFiles outside of Windows (#8) * Do not follow linked directories in SafeGetFiles outside of Windows SafeGetFiles with SearchOption.AllDirectories follows directory symlinks. On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose target reads as empty, and each one resolves to its own folder, so the recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276). Outside of Windows, walk the tree one directory at a time and do not enter directories that carry FileAttributes.ReparsePoint. Files are matched with Directory.GetFiles per directory, so the pattern behaves as before, and the order is the same breadth-first order. Windows keeps Directory.GetFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not follow linked directories in the other AllDirectories overloads SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in the same way. On Linux, with one directory linked to itself, SafeEnumerateFiles returns 43 files instead of 2, and with three such links it is still running after 10 seconds. The walk from SafeGetFiles is now a lazy iterator that all of them share, so the Enumerate wrappers stay lazy. Each directory is listed with the matching overload without SearchOption (Directory.GetFiles, SafeEnumerateFiles and so on), so patterns and inaccessible directories behave as in the recursive search. If the attributes of a subdirectory cannot be read, such as below a directory without execute permission, it is entered anyway, so it fails in the same way as in the recursive search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for linked directories without catching exceptions DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00
[Fact]
public void SafeEnumerateFileSystemEntries_LinkedDirectory_NotFollowed()
{
// Linked directories are only skipped outside of Windows
if (SystemEnvironment.IsWindows())
return;
string root = CreateLinkedDirectoryTree();
try
{
List<string> entries = [.. root.SafeEnumerateFileSystemEntries("*", SearchOption.AllDirectories)];
Assert.Equal(5, entries.Count);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
2024-11-25 16:52:49 -05:00
#endregion
Do not follow linked directories in SafeGetFiles outside of Windows (#8) * Do not follow linked directories in SafeGetFiles outside of Windows SafeGetFiles with SearchOption.AllDirectories follows directory symlinks. On Linux, the UDF view of some PC/Mac hybrid discs has symlinks whose target reads as empty, and each one resolves to its own folder, so the recursion never ends (fixes #2, SabreTools/BinaryObjectScanner#276). Outside of Windows, walk the tree one directory at a time and do not enter directories that carry FileAttributes.ReparsePoint. Files are matched with Directory.GetFiles per directory, so the pattern behaves as before, and the order is the same breadth-first order. Windows keeps Directory.GetFiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not follow linked directories in the other AllDirectories overloads SafeGetDirectories and the SafeEnumerateDirectories, SafeEnumerateFiles and SafeEnumerateFileSystemEntries wrappers follow directory symlinks in the same way. On Linux, with one directory linked to itself, SafeEnumerateFiles returns 43 files instead of 2, and with three such links it is still running after 10 seconds. The walk from SafeGetFiles is now a lazy iterator that all of them share, so the Enumerate wrappers stay lazy. Each directory is listed with the matching overload without SearchOption (Directory.GetFiles, SafeEnumerateFiles and so on), so patterns and inaccessible directories behave as in the recursive search. If the attributes of a subdirectory cannot be read, such as below a directory without execute permission, it is entered anyway, so it fails in the same way as in the recursive search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Check for linked directories without catching exceptions DirectoryInfo.Exists returns false where the attributes cannot be read, and the criteria for the walk are checked one at a time with their reasons. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 13:52:11 +01:00
/// <summary>
/// Create a directory tree with a linked file and a directory linked to the directory it is in
/// </summary>
/// <returns>Root of the tree, to be deleted by the caller</returns>
private static string CreateLinkedDirectoryTree()
{
string root = Path.Combine(Environment.CurrentDirectory, Guid.NewGuid().ToString());
string subdir = Path.Combine(root, "Subdirectory");
Directory.CreateDirectory(subdir);
File.WriteAllBytes(Path.Combine(root, "file.bin"), []);
File.WriteAllBytes(Path.Combine(subdir, "file.bin"), []);
File.CreateSymbolicLink(Path.Combine(subdir, "link.bin"), "file.bin");
Directory.CreateSymbolicLink(Path.Combine(subdir, "self"), ".");
return root;
}
2024-03-05 10:45:31 -05:00
}
}