2024-11-25 16:52:49 -05:00
using System ;
2026-03-22 15:35:11 -04:00
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
2026-03-22 15:35:11 -04:00
#region GetDirectoriesOnly
[Fact]
public void GetDirectoriesOnly_NoAppendParent ( )
{
2026-07-09 15:45:00 -04:00
string expectedParent = Path . Combine ( Environment . CurrentDirectory , "TestData" , "Path" ) ;
2026-03-22 15:35:11 -04:00
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*" ) ,
2026-03-22 15:35:11 -04:00
] ;
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" ) ;
2026-03-22 15:35:11 -04:00
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*" ) ,
2026-03-22 15:35:11 -04:00
] ;
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" ) ;
2026-03-22 15:35:11 -04:00
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" ) ;
2026-03-22 15:35:11 -04:00
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
2026-03-22 15:35:11 -04:00
#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
2026-03-22 15:35:11 -04:00
#region GetFilesOnly
[Fact]
public void GetFilesOnly_NoAppendParent ( )
{
2026-07-09 15:45:00 -04:00
string expectedParent = Path . Combine ( Environment . CurrentDirectory , "TestData" , "Path" ) ;
2026-03-22 15:35:11 -04:00
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" ) ,
2026-03-22 15:35:11 -04:00
] ;
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" ) ;
2026-03-22 15:35:11 -04:00
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" ) ,
2026-03-22 15:35:11 -04:00
] ;
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
2026-03-22 15:35:11 -04:00
#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
2026-03-22 15:35:11 -04:00
#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]
2026-07-02 10:21:15 -04:00
public void ResolvePath_AbsolutePath_Exists_ReturnsFullPath ( )
2026-07-02 10:01:18 -04:00
{
2026-07-02 10:21:15 -04:00
string filename = Guid . NewGuid ( ) . ToString ( ) ;
string path = Path . Combine ( Environment . CurrentDirectory , filename ) ;
2026-07-02 10:01:18 -04:00
2026-07-02 10:21:15 -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 ( )
{
2026-07-02 10:21:15 -04:00
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 ) ;
}
2026-07-02 10:21:15 -04:00
[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 ( )
{
2026-07-02 10:21:15 -04:00
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 ( )
{
2026-07-02 10:21:15 -04:00
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 ( )
{
2026-07-02 10:21:15 -04:00
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
2026-07-02 10:21:15 -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
{
2026-07-02 10:21:15 -04:00
Environment . SetEnvironmentVariable ( "PATH" , subDir + Path . PathSeparator + ( originalPath ? ? string . Empty ) ) ;
2026-07-02 10:01:18 -04:00
string? actual = filename . ResolvePath ( ) ;
2026-07-02 10:21:15 -04:00
Assert . Equal ( expected , actual ) ;
2026-07-02 10:01:18 -04:00
}
finally
{
Environment . SetEnvironmentVariable ( "PATH" , originalPath ) ;
2026-07-02 10:21:15 -04:00
Directory . Delete ( subDir , recursive : true ) ;
2026-07-02 10:01:18 -04:00
}
}
[Fact]
public void ResolvePath_BareName_Missing_ReturnsNull ( )
{
2026-07-02 10:21:15 -04:00
string filename = Guid . NewGuid ( ) . ToString ( ) ;
2026-07-02 10:01:18 -04:00
string? actual = filename . ResolvePath ( ) ;
Assert . Null ( actual ) ;
}
#endregion
2026-03-22 15:35:11 -04:00
#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 ) ;
}
2025-07-23 10:25:19 -04:00
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 ) ;
}
}
2026-03-22 15:35:11 -04:00
#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 ) ;
}
}
2026-03-22 15:35:11 -04:00
#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 ) ;
}
2026-03-22 15:35:11 -04:00
#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 ) ;
}
2025-07-23 10:25:19 -04:00
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 ) ;
}
}
2026-03-22 15:35:11 -04:00
#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 ) ;
}
}
2026-03-22 15:35:11 -04:00
#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
}
2025-07-23 10:25:19 -04:00
}