├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── Docs ├── .gitignore ├── api │ ├── .gitignore │ └── index.md ├── articles │ ├── advanced │ │ ├── pathoptions-handling.md │ │ └── toc.yml │ ├── guides │ │ ├── getting-started.md │ │ └── toc.yml │ ├── system.io │ │ ├── exception-handling-differences.md │ │ ├── problems-with-system-io.md │ │ └── toc.yml │ └── toc.yml ├── docfx.json ├── images │ ├── favicon.png │ └── logo.png ├── index.md ├── plugins │ ├── memberpage-extras │ │ ├── ManagedReference.extension.js │ │ └── toc.html.js │ └── memberpage.2.59.4 │ │ ├── ManagedReference.extension.js │ │ ├── ManagedReference.overwrite.js │ │ ├── partials │ │ ├── class.tmpl.partial │ │ ├── collection.tmpl.partial │ │ ├── customMREFContent.tmpl.partial │ │ └── item.tmpl.partial │ │ ├── plugins │ │ ├── HtmlAgilityPack.dll │ │ ├── Microsoft.DocAsCode.Build.Common.dll │ │ ├── Microsoft.DocAsCode.Build.MemberLevelManagedReference.dll │ │ ├── Microsoft.DocAsCode.Common.dll │ │ ├── Microsoft.DocAsCode.DataContracts.Common.dll │ │ ├── Microsoft.DocAsCode.DataContracts.ManagedReference.dll │ │ ├── Microsoft.DocAsCode.MarkdownLite.dll │ │ ├── Microsoft.DocAsCode.Plugins.dll │ │ ├── Microsoft.DocAsCode.YamlSerialization.dll │ │ ├── Newtonsoft.Json.dll │ │ ├── System.Buffers.dll │ │ ├── System.Collections.Immutable.dll │ │ ├── System.Composition.AttributedModel.dll │ │ ├── System.Composition.Convention.dll │ │ ├── System.Composition.Hosting.dll │ │ ├── System.Composition.Runtime.dll │ │ ├── System.Composition.TypedParts.dll │ │ ├── System.Memory.dll │ │ ├── System.Numerics.Vectors.dll │ │ ├── System.Runtime.CompilerServices.Unsafe.dll │ │ ├── YamlDotNet.dll │ │ └── docfx.plugins.config │ │ └── toc.html.js ├── templates │ └── singulinkfx │ │ ├── layout │ │ └── _master.tmpl │ │ ├── partials │ │ ├── footer.tmpl.partial │ │ ├── head.tmpl.partial │ │ ├── li.tmpl.partial │ │ ├── logo.tmpl.partial │ │ ├── namespace.tmpl.partial │ │ ├── navbar.tmpl.partial │ │ ├── scripts.tmpl.partial │ │ ├── searchResults.tmpl.partial │ │ └── toc.tmpl.partial │ │ ├── styles │ │ ├── config.css │ │ ├── down-arrow.svg │ │ ├── jquery.twbsPagination.js │ │ ├── jquery.twbsPagination.min.js │ │ ├── main.css │ │ ├── main.js │ │ ├── singulink.css │ │ ├── singulink.js │ │ └── url.min.js │ │ └── toc.html.primary.tmpl └── toc.yml ├── LICENSE ├── README.md ├── Resources └── Singulink Icon 128x128.png └── Source ├── .editorconfig ├── Directory.Build.props ├── Singulink.IO.FileSystem.Tests ├── AbsoluteDirectoryCombineTests.cs ├── AbsoluteDirectoryParentTests.cs ├── AbsoluteDirectoryParseTests.cs ├── AbsoluteDirectoryPropertyTests.cs ├── AbsoluteFileParentTests.cs ├── AbsoluteFileParseTests.cs ├── EnumeratingDirectoryTests.cs ├── EqualsTests.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── PlatformConsistencyTests.cs ├── Properties │ └── Assembly.cs ├── RelativeDirectoryCombineTests.cs ├── RelativeDirectoryParentTests.cs ├── RelativeDirectoryParseTests.cs ├── RelativeFileParentTests.cs ├── RelativeFileParseTests.cs ├── Singulink.IO.FileSystem.Tests.csproj └── stylecop.json ├── Singulink.IO.FileSystem.sln └── Singulink.IO.FileSystem ├── DirectoryPath.cs ├── FilePath.cs ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── IAbsoluteDirectoryPath.Impl.cs ├── IAbsoluteDirectoryPath.cs ├── IAbsoluteFilePath.Impl.cs ├── IAbsoluteFilePath.cs ├── IAbsolutePath.Impl.cs ├── IAbsolutePath.cs ├── IDirectoryPath.cs ├── IFilePath.cs ├── IPath.Impl.cs ├── IPath.cs ├── IRelativeDirectoryPath.Impl.cs ├── IRelativeDirectoryPath.cs ├── IRelativeFilePath.Impl.cs ├── IRelativeFilePath.cs ├── IRelativePath.Impl.cs ├── IRelativePath.cs ├── Interop+Windows.DiskSpace.cs ├── Interop+Windows.DriveType.cs ├── Interop+Windows.FileSystem.cs ├── Interop+Windows.LastError.cs ├── Interop+Windows.MediaInsertionPromptGuard.cs ├── Interop+WindowsNative.cs ├── PathFormat.Universal.cs ├── PathFormat.Unix.cs ├── PathFormat.Windows.cs ├── PathFormat.cs ├── PathKind.cs ├── PathOptions.cs ├── SearchOptions.cs ├── Singulink.IO.FileSystem.csproj ├── SystemExtensions.cs ├── UnauthorizedIOAccessException.cs ├── Utilities ├── Ex.cs ├── StringHelper.cs └── StringOrSpan.cs ├── key.snk └── stylecop.json /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build and test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | debug-windows: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: | 20 | 8.0.x 21 | - name: Clean 22 | run: dotnet clean --configuration Debug && dotnet nuget locals all --clear 23 | working-directory: Source 24 | - name: Install dependencies 25 | run: dotnet restore 26 | working-directory: Source 27 | - name: Build 28 | run: dotnet build --configuration Debug --no-restore 29 | working-directory: Source 30 | - name: Test 31 | run: dotnet test --configuration Debug --no-build --verbosity normal 32 | working-directory: Source 33 | 34 | release-windows: 35 | 36 | runs-on: windows-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | - name: Setup .NET 41 | uses: actions/setup-dotnet@v4 42 | with: 43 | dotnet-version: | 44 | 8.0.x 45 | - name: Clean 46 | run: dotnet clean --configuration Release && dotnet nuget locals all --clear 47 | working-directory: Source 48 | - name: Install dependencies 49 | run: dotnet restore 50 | working-directory: Source 51 | - name: Build 52 | run: dotnet build --configuration Release --no-restore 53 | working-directory: Source 54 | - name: Test 55 | run: dotnet test --configuration Release --no-build --verbosity normal 56 | working-directory: Source 57 | 58 | debug-linux: 59 | 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | - uses: actions/checkout@v4 64 | - name: Setup .NET 65 | uses: actions/setup-dotnet@v4 66 | with: 67 | dotnet-version: | 68 | 8.0.x 69 | - name: Clean 70 | run: dotnet clean --configuration Debug && dotnet nuget locals all --clear 71 | working-directory: Source 72 | - name: Install dependencies 73 | run: dotnet restore 74 | working-directory: Source 75 | - name: Build 76 | run: dotnet build --configuration Debug --no-restore 77 | working-directory: Source 78 | - name: Test 79 | run: dotnet test --configuration Debug --no-build --verbosity normal 80 | working-directory: Source 81 | 82 | release-linux: 83 | 84 | runs-on: ubuntu-latest 85 | 86 | steps: 87 | - uses: actions/checkout@v4 88 | - name: Setup .NET 89 | uses: actions/setup-dotnet@v4 90 | with: 91 | dotnet-version: | 92 | 8.0.x 93 | - name: Clean 94 | run: dotnet clean --configuration Release && dotnet nuget locals all --clear 95 | working-directory: Source 96 | - name: Install dependencies 97 | run: dotnet restore 98 | working-directory: Source 99 | - name: Build 100 | run: dotnet build --configuration Release --no-restore 101 | working-directory: Source 102 | - name: Test 103 | run: dotnet test --configuration Release --no-build --verbosity normal 104 | working-directory: Source 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /Docs/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | -------------------------------------------------------------------------------- /Docs/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /Docs/api/index.md: -------------------------------------------------------------------------------- 1 | # Singulink.IO.FileSystem 2 | 3 | Use the table of contents to browse API documentation for the `Singulink.IO.FileSystem` library. -------------------------------------------------------------------------------- /Docs/articles/advanced/pathoptions-handling.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # PathOptions Handling 4 | 5 | ## Summary 6 | 7 | More detailed article coming soon. See the [PathOptions API documentation](../../api/Singulink.IO.PathOptions.yml) for descriptions of the possible options that can be used when parsing paths. 8 | 9 | ### Unfriendly Names 10 | 11 | The two most important things to consider when using path options other than `PathOptions.NoUnfriendlyNames` are: 12 | 1) The paths may not be usable from Windows Explorer or other Windows applications, i.e. if they contain trailing spaces, reserved device names or end with a dot. For example, users may be stuck not being able to delete the files/directories without resorting to advanced command line operations. 13 | 2) Serializing/deserializing the paths must be handled with a high degree of care to ensure that leading and trailing spaces are preseved, otherwise round tripping the value will result in a path that points to a different file or directory. 14 | 15 | If you are receiving the path from something like an `OpenFileWindow` and simply opening the existing file without storing the path for later use then it is safe to use `PathOptions.None` to allow access to all existing files in the file system, even if the path is "unfriendly." 16 | 17 | ### Empty Directory Names 18 | 19 | By default, it is an error to parse a path that contains empty directory names such as `path/to//some/dir` (notice the double slash resulting in an empty path segment between them) as this indicates a malformed path. If you would like the parser to normalize out empty directories instead then you can use the `PathOptions.AllowEmptyDirectories` option, which would cause the above path to be parsed as `path/to/some/dir`. 20 | 21 |
-------------------------------------------------------------------------------- /Docs/articles/advanced/toc.yml: -------------------------------------------------------------------------------- 1 | - name: PathOptions Handling 2 | href: pathoptions-handling.md -------------------------------------------------------------------------------- /Docs/articles/guides/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Getting Started 2 | href: getting-started.md -------------------------------------------------------------------------------- /Docs/articles/system.io/exception-handling-differences.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Exception Handling 4 | 5 | ## Improvements 6 | 7 | There are a few important improvements to exception handling that `Singulink.IO.FileSystem` provides over the `System.IO` APIs: 8 | 9 | ### Cross-Platform Consistency 10 | 11 | The types of exceptions thrown on Unix and Windows are not consistent in `System.IO` in many instances, which this library attempts to remedy. If a `FileNotFoundException` is thrown on Windows then you can expect the same to happen on Unix as well. 12 | 13 | ### UnauthorizedIOAccessException 14 | 15 | This library eliminates any instances of `System.UnauthorizedAccessException` being thrown, instead replacing it with a new `UnauthorizedIOAccessException` that inherits from `System.IOException`, greatly improving the way exceptions can be handled by your code. 16 | 17 | ### Separation of Concerns 18 | 19 | With `System.IO`, exception handling is clunky because I/O operations could throw any number of exceptions with no common base type other than the overly general `Exception` type: `ArgumentException`, `IOException` (and subtypes), `UnauthorizedAccessException`, etc. You either have to put `Exception` handling blocks everywhere but that could hide issues in your code, or you have to add multiple exception handling blocks around everything which is tedious. 20 | 21 | With this library, parsing is separated from I/O, so you wrap path parsing operations with `ArgumentException` handling blocks, and you wrap I/O operations with `IOException` handling blocks since all the exceptions that can be thrown inherit from that type. Simple, tidy, concise, and easy to follow exception handling best practices. 22 | 23 |
-------------------------------------------------------------------------------- /Docs/articles/system.io/problems-with-system-io.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Problems with System.IO 4 | 5 | ## Endless Pitfalls 6 | 7 | The following is just a small sampling of the numerous pitfalls present in `System.IO` which make it virtually impossible to use for building reliable applications: 8 | 9 | #### Strange Behavior/Bugs 10 | 11 | If you get `DirectoryInfo.Attributes` for a path that points to a file or `FileInfo.Attributes` for a path that points to a directory it will happily give you the attributes even though `Exists` is false in both cases. `File.GetAttributes()` will happily return attributes for both files and directories. 12 | 13 | On Windows, calling `File.Delete()` on a directory path throws `UnauthorizedAccessException` instead of `FileNotFoundException`. Calling `Directory.Delete()` on a file path throws `IOException: directory name is invalid` instead of `DirectoryNotFoundException`. This behavior is not consistent across other platforms. 14 | 15 | Calling `Directory.GetParent(@"C:\temp\")` just returns `"C:\temp"` due to its naive algorithm so you have to be very careful about trailing slashes when manipulating paths. 16 | 17 | #### Problematic Handling of Spaces 18 | 19 | Silently modifying paths during file operations that contain leading/trailing spaces and dots is a major source of bugs. Here are some examples: 20 | 21 | ```c# 22 | var userProvidedDir = @"C:\directory \file.txt"; // Oops, user put a space after the directory. 23 | var fileInfo = new FileInfo(userProvidedPath); 24 | fileInfo.ParentDirectory.Create(); 25 | 26 | using (var stream = fileInfo.Create()) 27 | { 28 | // write some file contents 29 | } 30 | 31 | bool exists = File.Exists(userProvidedPath); // FALSE! No file for you! 32 | File.Open(userProvidedPath); // Exception! 33 | 34 | // Similar problem: 35 | 36 | var dirInfo = new DirectoryInfo(@"C:\directory \subdirectory"); 37 | dirInfo.Create(); 38 | 39 | bool exists = dirInfo.Parent.Exists; // FALSE! No directory for you! 40 | ``` 41 | 42 | If you parsed the path in `Singulink.IO.FileSystem` with `PathOptions.NoUnfriendlyNames` then the user would be notified of this problematic path. If you are opening an existing file then `PathOptions.None` will ensure you can seamlessly deal with any existing path the user throw at you. 43 | 44 | #### Unable to Open Existing Files 45 | 46 | `System.IO` will have difficulty if the path is "unfriendly" even though the user directly selected an existing file using an open file dialog, i.e. if it contains trailing/leading spaces, trailing dots, reserved device names, etc: 47 | 48 | ```c# 49 | string filePathString = new OpenFileWindow().FilePath; 50 | File.Open(filePathString); // Possible FileNotFoundException 51 | ``` 52 | 53 | Meanwhile, this "Just Works" (TM) in `Singulink.IO.FileSystem`! 54 | 55 | ```c# 56 | string filePathString = new OpenFileWindow().FilePath; 57 | var filePath = FilePath.Parse(filePathString, PathOptions.None); 58 | filePath.OpenStream(); 59 | ``` 60 | 61 | #### DriveInfo 62 | 63 | The only way to obtain available/used space information in `System.IO` is via `DriveInfo`. This limits you to only getting information for root directories in Windows and it does not work with UNC paths. The concept of "drives" is not cross-platform applicable and thus is not present in this library. Instead, the functionality of `DriveInfo` is now present in a much more versatile and reliable manner on all `IAbsoluteDirectoryPath` instances. 64 | 65 | #### Cross-Platform Concerns 66 | 67 | Searching for files/directories with a wildcard pattern using `System.IO` has different case-sensitivity settings by default on Unix and Windows. `Singulink.IO.FileSystem` does case-insensitive searches by default so you get consistent behavior across your app platforms unless you opt into platform-specific behavior. 68 | 69 | There is no way to determine whether a path will be cross-platform friendly using `System.IO`, nor is there a way to process and manipulate paths from the platform you aren't currently running on. `Singulink.IO.FileSystem` gives you `PathFormat.Universal` to validate that paths will work everywhere and convert them into a common format. You can also explicitly specify that a path is in Unix or Windows format during parsing and convert relative paths between the two formats as needed. 70 | 71 | #### UNC Path Handling 72 | 73 | There are numerous methods and operations that throw exceptions or do not work correctly with UNC paths. 74 | 75 | ### And more... 76 | 77 | This short list of issues is far from exhaustive, it's just what I could think of off the top of my head. There are countless pitfalls when using `System.IO` but hopefully by this point I have convinced you that it is a bloody minefield that should be avoided for any serious development. It takes immense effort and deep knowledge of all the nuances to use `System.IO` in a reliable manner, especially in cross-platform development. **It's simply too hard to get right**. `Singulink.IO.FileSystem` makes it easy and intuitive to write correct and reliable code every time. 78 | 79 |
-------------------------------------------------------------------------------- /Docs/articles/system.io/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Problems with System.IO 2 | href: problems-with-system-io.md 3 | - name: Exception Handling Differences 4 | href: exception-handling-differences.md -------------------------------------------------------------------------------- /Docs/articles/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Guides 2 | href: guides/toc.yml 3 | # items: 4 | # - name: Getting Started 5 | # href: getting-started.md 6 | 7 | - name: System.IO 8 | href: system.io/toc.yml 9 | # items: 10 | # - name: Problems with System.IO 11 | # href: problems-with-system-io.md 12 | # - name: Exception Handling Differences 13 | # href: exception-handling-differences.md 14 | 15 | - name: Advanced 16 | href: advanced/toc.yml 17 | # items: 18 | # - name: PathOptions Handling 19 | # href: pathoptions-handling.md -------------------------------------------------------------------------------- /Docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "Singulink.IO.FileSystem/Singulink.IO.FileSystem.csproj" 8 | ], 9 | "src": "../Source" 10 | } 11 | ], 12 | "dest": "api", 13 | "memberLayout": "separatePages", 14 | "includeExplicitInterfaceImplementations": true, 15 | "disableGitFeatures": true, 16 | "disableDefaultFilter": false 17 | } 18 | ], 19 | "build": { 20 | "globalMetadata": { 21 | "_appTitle": "Singulink.IO.FileSystem", 22 | "_appName": "File System", 23 | "_appLogoPath": "images/logo.png", 24 | "_appFaviconPath": "images/favicon.png", 25 | "_copyrightFooter": "© Singulink. All rights reserved.", 26 | "_enableSearch": true, 27 | "_enableNewTab": true 28 | }, 29 | "template": [ "default", "templates/singulinkfx" ], 30 | "xref": [ "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" ], 31 | "content": [ 32 | { 33 | "files": [ 34 | "api/**.yml", 35 | "api/index.md" 36 | ] 37 | }, 38 | { 39 | "files": [ 40 | "articles/**.md", 41 | "articles/**/toc.yml", 42 | "toc.yml", 43 | "*.md" 44 | ] 45 | } 46 | ], 47 | "resource": [ 48 | { 49 | "files": [ 50 | "images/**" 51 | ] 52 | } 53 | ], 54 | "overwrite": [ 55 | { 56 | "files": [ 57 | "apidoc/**.md" 58 | ], 59 | "exclude": [ 60 | "obj/**", 61 | "_site/**" 62 | ] 63 | } 64 | ], 65 | "dest": "_site", 66 | "disableGitFeatures": true, 67 | "globalMetadataFiles": [], 68 | "fileMetadataFiles": [], 69 | "postProcessors": [], 70 | "markdownEngineName": "markdig", 71 | "noLangKeyword": false, 72 | "keepFileLink": false 73 | } 74 | } -------------------------------------------------------------------------------- /Docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/images/favicon.png -------------------------------------------------------------------------------- /Docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/images/logo.png -------------------------------------------------------------------------------- /Docs/index.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Singulink.IO.FileSystem 4 | 5 | ## Summary 6 | 7 | **Singulink.IO.FileSystem** is a reliable cross-platform library that provides strongly-typed file/directory path manipulation and file system access in .NET. It has been designed to encourage developers to code with explicit intent in such a way that their applications can work seamlessly and bug free across both Unix and Windows file systems under all conditions. `System.IO.*` has numerous pitfalls that make 99% of file system code out in the wild fragile and problematic in edge cases. It also contains behavioral inconsistencies between Unix and Windows file systems that are abstracted and handled by this library so you don't have to worry about them. 8 | 9 | You can visit the [Problems with System.IO](articles/system.io/problems-with-system-io.md) article for a primer on some of the issues with `System.IO`. 10 | 11 | **Singulink.IO.FileSystem** is part of the **Singulink Libraries** collection. Visit https://github.com/Singulink/ to see the full list of libraries available. 12 | 13 | ## Installation 14 | 15 | The package is available on NuGet - simply install the `Singulink.IO.FileSystem` package. 16 | 17 | **Supported Runtimes**: Anywhere .NET Standard 2.1+ is supported, including: 18 | - .NET 19 | - Mono 20 | - Xamarin 21 | 22 | ## Information and Links 23 | 24 | Here are some additonal links to get you started: 25 | 26 | - [Getting Started](articles/guides/getting-started.md) - Visit here first if you want to read articles on how to use the library. 27 | - [API Documentation](api/index.md) - Browse the fully documented API here. 28 | - [Chat on Discord](https://discord.gg/EkQhJFsBu6) - Have questions or want to discuss the library? This is the place for all Singulink project discussions. 29 | - [Github Repo](https://github.com/Singulink/Singulink.IO.FileSystem) - File issues, contribute pull requests or check out the code for yourself! 30 | 31 |
-------------------------------------------------------------------------------- /Docs/plugins/memberpage-extras/ManagedReference.extension.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | var common = require('./ManagedReference.common.js'); 3 | 4 | exports.preTransform = function (model) { 5 | transform(model); 6 | 7 | function transform(item) { 8 | if (item.children) item.children.forEach(function(i) { 9 | transform(i); 10 | }); 11 | } 12 | 13 | return model; 14 | } 15 | 16 | exports.postTransform = function (model) { 17 | var type = model.type.toLowerCase(); 18 | var category = common.getCategory(type); 19 | if (category == 'class') { 20 | var typePropertyName = common.getTypePropertyName(type); 21 | if (typePropertyName) { 22 | model[typePropertyName] = true; 23 | } 24 | if (model.children && model.children.length > 0) { 25 | model.isCollection = true; 26 | common.groupChildren(model, 'class'); 27 | } else { 28 | model.isItem = true; 29 | } 30 | } 31 | 32 | return model; 33 | } -------------------------------------------------------------------------------- /Docs/plugins/memberpage-extras/toc.html.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | exports.transform = function (model) { 3 | var groupNames = { 4 | "constructor": { key: "constructorsInSubtitle" }, 5 | "field": { key: "fieldsInSubtitle" }, 6 | "property": { key: "propertiesInSubtitle" }, 7 | "method": { key: "methodsInSubtitle" }, 8 | "event": { key: "eventsInSubtitle" }, 9 | "operator": { key: "operatorsInSubtitle" }, 10 | "eii": { key: "eiisInSubtitle" }, 11 | }; 12 | 13 | groupChildren(model); 14 | transformItem(model, 1); 15 | return model; 16 | 17 | function groupChildren(item) { 18 | if (!item || !item.items || item.items.length == 0) { 19 | return; 20 | } 21 | var grouped = {}; 22 | var items = []; 23 | item.items.forEach(function (element) { 24 | groupChildren(element); 25 | if (element.type) { 26 | var type = element.isEii ? "eii" : element.type.toLowerCase(); 27 | if (!grouped.hasOwnProperty(type)) { 28 | if (!groupNames.hasOwnProperty(type)) { 29 | groupNames[type] = { 30 | name: element.type 31 | }; 32 | console.log(type + " is not predefined type, use its type name as display name.") 33 | } 34 | grouped[type] = []; 35 | } 36 | grouped[type].push(element); 37 | } else { 38 | items.push(element); 39 | } 40 | }, this); 41 | 42 | // With order defined in groupNames 43 | for (var key in groupNames) { 44 | if (groupNames.hasOwnProperty(key) && grouped.hasOwnProperty(key)) { 45 | items.push({ 46 | name: model.__global[groupNames[key].key] || groupNames[key].name, 47 | items: grouped[key] 48 | }) 49 | } 50 | } 51 | 52 | item.items = items; 53 | } 54 | 55 | function transformItem(item, level) { 56 | // set to null in case mustache looks up 57 | item.topicHref = item.topicHref || null; 58 | item.tocHref = item.tocHref || null; 59 | item.name = item.name || null; 60 | 61 | item.level = level; 62 | 63 | // Add word break opportunities before dots 64 | 65 | if (item.name) 66 | item.name = item.name.replace(/\./g, "\u200B."); 67 | 68 | if (item.items && item.items.length > 0) { 69 | item.leaf = false; 70 | var length = item.items.length; 71 | for (var i = 0; i < length; i++) { 72 | transformItem(item.items[i], level + 1); 73 | }; 74 | } else { 75 | item.items = []; 76 | item.leaf = true; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/ManagedReference.extension.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | var common = require('./ManagedReference.common.js'); 3 | 4 | exports.postTransform = function (model) { 5 | var type = model.type.toLowerCase(); 6 | var category = common.getCategory(type); 7 | if (category == 'class') { 8 | var typePropertyName = common.getTypePropertyName(type); 9 | if (typePropertyName) { 10 | model[typePropertyName] = true; 11 | } 12 | if (model.children && model.children.length > 0) { 13 | model.isCollection = true; 14 | common.groupChildren(model, 'class'); 15 | } else { 16 | model.isItem = true; 17 | } 18 | } 19 | return model; 20 | } -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/ManagedReference.overwrite.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | var common = require('./ManagedReference.common.js'); 3 | 4 | exports.getOptions = function (model) { 5 | var ignoreChildrenBookmarks = model._splitReference && model.type && common.getCategory(model.type) === 'ns'; 6 | 7 | return { 8 | "bookmarks": common.getBookmarks(model, ignoreChildrenBookmarks) 9 | }; 10 | } -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/partials/class.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | {{>partials/class.header}} 4 | {{#children}} 5 | {{#overload}} 6 | 7 | {{/overload}} 8 |

{{>partials/classSubtitle}}

9 | {{#children.0}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{/children.0}} 19 | {{#children}} 20 | 21 | 24 | 25 | 26 | {{/children}} 27 | {{#children.0}} 28 | 29 |
{{__global.name}}{{__global.description}}
22 | 23 | {{{summary}}}
30 | {{/children.0}} 31 | {{/children}} 32 | {{#extensionMethods.0}} 33 |

{{__global.extensionMethods}}

34 | {{/extensionMethods.0}} 35 | {{#extensionMethods}} 36 |
37 | {{#definition}} 38 | 39 | {{/definition}} 40 | {{^definition}} 41 | 42 | {{/definition}} 43 |
44 | {{/extensionMethods}} 45 | {{#seealso.0}} 46 |

{{__global.seealso}}

47 |
48 | {{/seealso.0}} 49 | {{#seealso}} 50 | {{#isCref}} 51 |
{{{type.specName.0.value}}}
52 | {{/isCref}} 53 | {{^isCref}} 54 |
{{{url}}}
55 | {{/isCref}} 56 | {{/seealso}} 57 | {{#seealso.0}} 58 |
59 | {{/seealso.0}} 60 | -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/partials/collection.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |

{{>partials/title}}

4 |
{{{summary}}}
5 |
{{{conceptual}}}
6 | 7 | {{#children}} 8 | {{#children}} 9 | {{^_disableContribution}} 10 | {{#docurl}} 11 | 12 | | 13 | {{__global.improveThisDoc}} 14 | {{/docurl}} 15 | {{#sourceurl}} 16 | 17 | {{__global.viewSource}} 18 | {{/sourceurl}} 19 | {{/_disableContribution}} 20 | {{#overload}} 21 | 22 | {{/overload}} 23 |

{{name.0.value}}

24 |
{{{summary}}}
25 |
{{{conceptual}}}
26 |
{{__global.declaration}}
27 | {{#syntax}} 28 |
29 |
{{syntax.content.0.value}}
30 |
31 | {{#parameters.0}} 32 |
{{__global.parameters}}
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {{/parameters.0}} 43 | {{#parameters}} 44 | 45 | 46 | 47 | 48 | 49 | {{/parameters}} 50 | {{#parameters.0}} 51 | 52 |
{{__global.type}}{{__global.name}}{{__global.description}}
{{{type.specName.0.value}}}{{{id}}}{{{description}}}
53 | {{/parameters.0}} 54 | {{#return}} 55 |
{{__global.returns}}
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
70 | {{/return}} 71 | {{#typeParameters.0}} 72 |
{{__global.typeParameters}}
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {{/typeParameters.0}} 82 | {{#typeParameters}} 83 | 84 | 85 | 86 | 87 | {{/typeParameters}} 88 | {{#typeParameters.0}} 89 | 90 |
{{__global.name}}{{__global.description}}
{{{id}}}{{{description}}}
91 | {{/typeParameters.0}} 92 | {{#fieldValue}} 93 |
{{__global.fieldValue}}
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
108 | {{/fieldValue}} 109 | {{#propertyValue}} 110 |
{{__global.propertyValue}}
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
125 | {{/propertyValue}} 126 | {{#eventType}} 127 |
{{__global.eventType}}
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
142 | {{/eventType}} 143 | {{/syntax}} 144 | {{#overridden}} 145 |
{{__global.overrides}}
146 |
147 | {{/overridden}} 148 | {{#implements.0}} 149 |
{{__global.implements}}
150 | {{/implements.0}} 151 | {{#implements}} 152 | {{#definition}} 153 |
154 | {{/definition}} 155 | {{^definition}} 156 |
157 | {{/definition}} 158 | {{/implements}} 159 | {{#remarks}} 160 |
{{__global.remarks}}
161 |
{{{remarks}}}
162 | {{/remarks}} 163 | {{#example.0}} 164 |
{{__global.examples}}
165 | {{/example.0}} 166 | {{#example}} 167 | {{{.}}} 168 | {{/example}} 169 | {{#exceptions.0}} 170 |
{{__global.exceptions}}
171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | {{/exceptions.0}} 180 | {{#exceptions}} 181 | 182 | 183 | 184 | 185 | {{/exceptions}} 186 | {{#exceptions.0}} 187 | 188 |
{{__global.type}}{{__global.condition}}
{{{type.specName.0.value}}}{{{description}}}
189 | {{/exceptions.0}} 190 | {{#seealso.0}} 191 |
{{__global.seealso}}
192 |
193 | {{/seealso.0}} 194 | {{#seealso}} 195 | {{#isCref}} 196 |
{{{type.specName.0.value}}}
197 | {{/isCref}} 198 | {{^isCref}} 199 |
{{{url}}}
200 | {{/isCref}} 201 | {{/seealso}} 202 | {{#seealso.0}} 203 |
204 | {{/seealso.0}} 205 | {{/children}} 206 | {{/children}} 207 | {{#extensionMethods.0}} 208 |

{{__global.extensionMethods}}

209 | {{/extensionMethods.0}} 210 | {{#extensionMethods}} 211 |
212 | {{#definition}} 213 | 214 | {{/definition}} 215 | {{^definition}} 216 | 217 | {{/definition}} 218 |
219 | {{/extensionMethods}} 220 | {{#seealso.0}} 221 |

{{__global.seealso}}

222 |
223 | {{/seealso.0}} 224 | {{#seealso}} 225 | {{#isCref}} 226 |
{{{type.specName.0.value}}}
227 | {{/isCref}} 228 | {{^isCref}} 229 |
{{{url}}}
230 | {{/isCref}} 231 | {{/seealso}} 232 | {{#seealso.0}} 233 |
234 | {{/seealso.0}} 235 | -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/partials/customMREFContent.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | {{#isCollection}} 3 | {{>partials/collection}} 4 | {{/isCollection}} 5 | {{#isItem}} 6 | {{>partials/item}} 7 | {{/isItem}} 8 | -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/partials/item.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | {{>partials/class.header}} -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/HtmlAgilityPack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/HtmlAgilityPack.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Build.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Build.Common.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Build.MemberLevelManagedReference.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Build.MemberLevelManagedReference.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Common.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.DataContracts.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.DataContracts.Common.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.DataContracts.ManagedReference.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.DataContracts.ManagedReference.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.MarkdownLite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.MarkdownLite.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Plugins.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.Plugins.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.YamlSerialization.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Microsoft.DocAsCode.YamlSerialization.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Buffers.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Buffers.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Collections.Immutable.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Collections.Immutable.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Composition.AttributedModel.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Composition.AttributedModel.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Composition.Convention.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Composition.Convention.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Composition.Hosting.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Composition.Hosting.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Composition.Runtime.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Composition.Runtime.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Composition.TypedParts.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Composition.TypedParts.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Memory.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Memory.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Numerics.Vectors.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Numerics.Vectors.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/System.Runtime.CompilerServices.Unsafe.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/System.Runtime.CompilerServices.Unsafe.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/YamlDotNet.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/plugins/memberpage.2.59.4/plugins/YamlDotNet.dll -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/plugins/docfx.plugins.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Docs/plugins/memberpage.2.59.4/toc.html.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | exports.transform = function (model) { 3 | var groupNames = { 4 | "constructor": { key: "constructorsInSubtitle" }, 5 | "field": { key: "fieldsInSubtitle" }, 6 | "property": { key: "propertiesInSubtitle" }, 7 | "method": { key: "methodsInSubtitle" }, 8 | "event": { key: "eventsInSubtitle" }, 9 | "operator": { key: "operatorsInSubtitle" }, 10 | }; 11 | 12 | groupChildren(model); 13 | transformItem(model, 1); 14 | return model; 15 | 16 | function groupChildren(item) { 17 | if (!item || !item.items || item.items.length == 0) { 18 | return; 19 | } 20 | var grouped = {}; 21 | var items = []; 22 | item.items.forEach(function (element) { 23 | groupChildren(element); 24 | if (element.type) { 25 | var type = element.type.toLowerCase(); 26 | if (!grouped.hasOwnProperty(type)) { 27 | if (!groupNames.hasOwnProperty(type)) { 28 | groupNames[type] = { 29 | name: element.type 30 | }; 31 | console.log(type + " is not predefined type, use its type name as display name.") 32 | } 33 | grouped[type] = []; 34 | } 35 | grouped[type].push(element); 36 | } else { 37 | items.push(element); 38 | } 39 | }, this); 40 | 41 | // With order defined in groupNames 42 | for (var key in groupNames) { 43 | if (groupNames.hasOwnProperty(key) && grouped.hasOwnProperty(key)) { 44 | items.push({ 45 | name: model.__global[groupNames[key].key] || groupNames[key].name, 46 | items: grouped[key] 47 | }) 48 | } 49 | } 50 | 51 | item.items = items; 52 | } 53 | 54 | function transformItem(item, level) { 55 | // set to null in case mustache looks up 56 | item.topicHref = item.topicHref || null; 57 | item.tocHref = item.tocHref || null; 58 | item.name = item.name || null; 59 | 60 | item.level = level; 61 | 62 | if (item.items && item.items.length > 0) { 63 | item.leaf = false; 64 | var length = item.items.length; 65 | for (var i = 0; i < length; i++) { 66 | transformItem(item.items[i], level + 1); 67 | }; 68 | } else { 69 | item.items = []; 70 | item.leaf = true; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/layout/_master.tmpl: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | {{!include(/^styles/.*/)}} 3 | {{!include(/^fonts/.*/)}} 4 | {{!include(favicon.ico)}} 5 | {{!include(logo.svg)}} 6 | {{!include(search-stopwords.json)}} 7 | 8 | 9 | 10 | {{>partials/head}} 11 | 12 | 13 | 14 |
15 |
16 | 17 | 20 | 21 | 22 | {{>partials/logo}} 23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | {{>partials/navbar}} 31 |
32 | {{^_disableToc}} 33 | {{>partials/toc}} 34 | {{/_disableToc}} 35 |
36 | {{>partials/footer}} 37 |
38 | 39 |
40 | {{#_enableSearch}} 41 | {{>partials/searchResults}} 42 | {{/_enableSearch}} 43 | 44 | 45 | 46 |
47 | {{^_disableBreadcrumb}} 48 | {{>partials/breadcrumb}} 49 | {{/_disableBreadcrumb}} 50 | 51 | {{^_disableContribution}} 52 |
53 | {{#docurl}} 54 | {{__global.improveThisDoc}} 55 | {{/docurl}} 56 |
57 | {{/_disableContribution}} 58 | 59 |
60 | {{!body}} 61 |
62 |
63 | 64 | {{#_copyrightFooter}} 65 |
66 | {{_copyrightFooter}} 67 |
68 | {{/_copyrightFooter}} 69 |
70 |
71 | 72 | {{>partials/scripts}} 73 | 74 | 75 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/footer.tmpl.partial: -------------------------------------------------------------------------------- 1 |
2 | {{{_appFooter}}} 3 | {{^_appFooter}}DocFX + Singulink = ♥{{/_appFooter}} 4 |
-------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/head.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | 4 | 5 | 6 | {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} 7 | 8 | 9 | 10 | {{#_description}}{{/_description}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{#_noindex}}{{/_noindex}} 21 | {{#_enableSearch}}{{/_enableSearch}} 22 | {{#_enableNewTab}}{{/_enableNewTab}} 23 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/li.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |
    4 | {{#items}} 5 | {{^dropdown}} 6 |
  • 7 | {{^leaf}} 8 | 9 | {{/leaf}} 10 | {{#topicHref}} 11 | {{name}} 12 | {{/topicHref}} 13 | {{^topicHref}} 14 | {{{name}}} 15 | {{/topicHref}} 16 | 17 | {{^leaf}} 18 | {{>partials/li}} 19 | {{/leaf}} 20 |
  • 21 | {{/dropdown}} 22 | {{#dropdown}} 23 |
  • 24 | {{name}} 25 |
      26 | {{>partials/dd-li}} 27 |
    28 |
  • 29 | {{/dropdown}} 30 | {{/items}} 31 |
32 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/logo.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | 4 | {{_appName}} 5 | {{_appName}} 6 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/namespace.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |

{{>partials/title}}

4 |
{{{summary}}}
5 |
{{{conceptual}}}
6 |
{{{remarks}}}
7 | {{#children}} 8 |

{{>partials/namespaceSubtitle}}

9 | {{#children}} 10 |
11 |
{{{summary}}}
12 | {{/children}} 13 | {{/children}} 14 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/navbar.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |
4 |
5 | {{>partials/logo}} 6 |
7 | 8 | {{#_enableSearch}} 9 |
10 |
11 | 12 | 13 |
14 |
15 | {{/_enableSearch}} 16 | 17 |
18 |
19 |
-------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/scripts.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/searchResults.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |
4 |

{{__global.searchResults}}

5 |
6 |

7 |
8 |
    9 |
    -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/partials/toc.tmpl.partial: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |
    4 |
    5 |
    6 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/config.css: -------------------------------------------------------------------------------- 1 | /* Theme Configuration Options */ 2 | 3 | :root 4 | { 5 | /* General */ 6 | 7 | --base-font-size: 16px; 8 | --smalldevice-base-font-size: 14px; /* Base font size for devices < 1024px */ 9 | 10 | --main-bg-color: #1f1f23; 11 | --footer-bg-color: rgba(0,0,0,.4); 12 | --separator-color: #42474f; 13 | 14 | --table-strip-bg-color: #151515; 15 | --table-header-bg-color: black; 16 | --table-header-color: hsla(0,0%,100%,.8); 17 | --table-header-border-color: #040405; 18 | 19 | /* Text */ 20 | 21 | --appname-color: white; 22 | 23 | --h1-color: white; 24 | --h2-color: #f2f2f2; 25 | --h3-color: #e3e3e3; 26 | --h4-color: #ffffff; 27 | --h5-color: #e0e0e0; 28 | 29 | --text-color: #e1e1e1; 30 | --link-color: #00b0f4; 31 | --link-hover-color: #2ec4ff; 32 | 33 | /* Mobile Topbar */ 34 | 35 | --topbar-bg-color: #18191c; 36 | 37 | /* Button */ 38 | 39 | --button-color: #747f8d; 40 | 41 | /* Sidebar */ 42 | 43 | --sidebar-width: 400px; 44 | --sidebar-bg-color: #292B30; 45 | 46 | --search-color: #bdbdbd; 47 | --search-bg-color: #1b1e21; 48 | --search-searchicon-color: #e3e3e3; 49 | --search-border-color: black; 50 | 51 | --sidebar-item-color: white; 52 | --sidebar-active-item-color: #00b0f4; 53 | --sidebar-level1-item-bg-color: #222429; 54 | --sidebar-level1-item-hover-bg-color: #1D1F22; 55 | 56 | --toc-filter-color: #bdbdbd; 57 | --toc-filter-bg-color: #1b1e21; 58 | --toc-filter-filtericon-color: #e3e3e3; 59 | --toc-filter-clearicon-color: #e68585; 60 | --toc-filter-border-color: black; 61 | 62 | /* Scrollbars */ 63 | 64 | --scrollbar-bg-color: transparent; 65 | --scrollbar-thumb-bg-color: rgba(0,0,0,.4); 66 | --scrollbar-thumb-border-color: transparent; 67 | 68 | /* Alerts and Blocks */ 69 | 70 | --alert-info-border-color: rgba(114,137,218,.5); 71 | --alert-info-bg-color: rgba(114,137,218,.1); 72 | 73 | --alert-warning-border-color: rgba(250,166,26,.5); 74 | --alert-warning-bg-color: rgba(250,166,26,.1); 75 | 76 | --alert-danger-border-color: rgba(240,71,71,.5); 77 | --alert-danger-bg-color: rgba(240,71,71,.1); 78 | 79 | --alert-tip-border-color: rgba(255,255,255,.5); 80 | --alert-tip-bg-color: rgba(255,255,255,.1); 81 | 82 | --blockquote-border-color: rgba(255,255,255,.5); 83 | --blockquote-bg-color: rgba(255,255,255,.1); 84 | 85 | --breadcrumb-bg-color: #2f3136; 86 | 87 | /* Tabs */ 88 | 89 | --nav-tabs-border-width: 1px; 90 | --nav-tabs-border-color: #495057; 91 | --nav-tabs-border-radius: .375rem; 92 | --nav-tabs-link-hover-border-color: #303336 #303336 transparent; 93 | --nav-tabs-link-active-color: white; 94 | --nav-tabs-link-active-bg: var(--main-bg-color); 95 | --nav-tabs-link-active-border-color: var(--nav-tabs-border-color) var(--nav-tabs-border-color) var(--main-bg-color); 96 | 97 | /* Inline Code */ 98 | 99 | --ref-bg-color: black; 100 | --ref-color: #89d4f1; 101 | 102 | /* Code Blocks */ 103 | 104 | --code-bg-color: #151515; 105 | --code-color: #d6deeb; 106 | --code-keyword-color: #569cd6; 107 | --code-comment-color: #57a64a; 108 | --code-macro-color: #beb7ff; 109 | --code-string-color: #d69d85; 110 | --code-string-escape-color: #ffd68f; 111 | --code-field-color: #c8c8c8; 112 | --code-function-color: #dcdcaa; 113 | --code-control-color: #d8a0df; 114 | --code-class-color: #4ec9b0; 115 | --code-number-color: #b5cea8; 116 | --code-params-color: #9a9a9a; 117 | --code-breakpoint-color: #8c2f2f; 118 | } 119 | 120 | /* Code Block Overrides */ 121 | 122 | pre, legend { 123 | --scrollbar-thumb-bg-color: #333; 124 | } -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/jquery.twbsPagination.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery pagination plugin v1.4.1 3 | * http://esimakin.github.io/twbs-pagination/ 4 | * 5 | * Copyright 2014-2016, Eugene Simakin 6 | * Released under Apache 2.0 license 7 | * http://apache.org/licenses/LICENSE-2.0.html 8 | */ !function(t,s,i,e){"use strict";var a=t.fn.twbsPagination,o=function(s,i){if(this.$element=t(s),this.options=t.extend({},t.fn.twbsPagination.defaults,i),this.options.startPage<1||this.options.startPage>this.options.totalPages)throw Error("Start page option is incorrect");if(this.options.totalPages=parseInt(this.options.totalPages),isNaN(this.options.totalPages))throw Error("Total pages option is not correct!");if(this.options.visiblePages=parseInt(this.options.visiblePages),isNaN(this.options.visiblePages))throw Error("Visible pages option is not correct!");if(this.options.onPageClick instanceof Function&&this.$element.first().on("page",this.options.onPageClick),this.options.hideOnlyOnePage&&1==this.options.totalPages)return this.$element.trigger("page",1),this;this.options.totalPages"),this.$listContainer.addClass(this.options.paginationClass),"UL"!==e&&this.$element.append(this.$listContainer),this.options.initiateStartPageClick?this.show(this.options.startPage):(this.render(this.getPages(this.options.startPage)),this.setupEvents()),this};o.prototype={constructor:o,destroy:function(){return this.$element.empty(),this.$element.removeData("twbs-pagination"),this.$element.off("page"),this},show:function(t){if(t<1||t>this.options.totalPages)throw Error("Page is incorrect.");return this.currentPage=t,this.render(this.getPages(t)),this.setupEvents(),this.$element.trigger("page",t),this},buildListItems:function(t){var s=[];if(this.options.first&&s.push(this.buildItem("first",1)),this.options.prev){var i=t.currentPage>1?t.currentPage-1:this.options.loop?this.options.totalPages:1;s.push(this.buildItem("prev",i))}for(var e=0;e"),a=t(""),o=this.options[s]?this.makeText(this.options[s],i):i;return e.addClass(this.options[s+"Class"]),e.data("page",i),e.data("page-type",s),e.append(a.attr("href",this.makeHref(i)).addClass(this.options.anchorClass).html(o)),e},getPages:function(t){var s=[],i=Math.floor(this.options.visiblePages/2),e=t-i+1-this.options.visiblePages%2,a=t+i;e<=0&&(e=1,a=this.options.visiblePages),a>this.options.totalPages&&(e=this.options.totalPages-this.options.visiblePages+1,a=this.options.totalPages);for(var o=e;o<=a;)s.push(o),o++;return{currentPage:t,numeric:s}},render:function(s){var i=this;this.$listContainer.children().remove();var e=this.buildListItems(s);jQuery.each(e,function(t,s){i.$listContainer.append(s)}),this.$listContainer.children().each(function(){var e=t(this),a=e.data("page-type");switch(a){case"page":e.data("page")===s.currentPage&&e.addClass(i.options.activeClass);break;case"first":e.toggleClass(i.options.disabledClass,1===s.currentPage);break;case"last":e.toggleClass(i.options.disabledClass,s.currentPage===i.options.totalPages);break;case"prev":e.toggleClass(i.options.disabledClass,!i.options.loop&&1===s.currentPage);break;case"next":e.toggleClass(i.options.disabledClass,!i.options.loop&&s.currentPage===i.options.totalPages)}})},setupEvents:function(){var s=this;this.$listContainer.off("click").on("click","li",function(i){var e=t(this);if(e.hasClass(s.options.disabledClass)||e.hasClass(s.options.activeClass))return!1;s.options.href||i.preventDefault(),s.show(parseInt(e.data("page")))})},makeHref:function(t){return this.options.href?this.generateQueryString(t):"#"},makeText:function(t,s){return t.replace(this.options.pageVariable,s).replace(this.options.totalPagesVariable,this.options.totalPages)},getPageFromQueryString:function(t){var s=this.getSearchString(t),i=RegExp(this.options.pageVariable+"(=([^&#]*)|&|#|$)").exec(s);return i&&i[2]?(i=parseInt(i=decodeURIComponent(i[2])),isNaN(i))?null:i:null},generateQueryString:function(t,s){var i=this.getSearchString(s),e=RegExp(this.options.pageVariable+"=*[^&#]*");return i?"?"+i.replace(e,this.options.pageVariable+"="+t):""},getSearchString:function(t){var i=t||s.location.search;return""===i?null:(0===i.indexOf("?")&&(i=i.substr(1)),i)}},t.fn.twbsPagination=function(s){var i,e=Array.prototype.slice.call(arguments,1),a=t(this),n=a.data("twbs-pagination");return n||a.data("twbs-pagination",n=new o(this,"object"==typeof s?s:{})),"string"==typeof s&&(i=n[s].apply(n,e)),void 0===i?a:i},t.fn.twbsPagination.defaults={totalPages:1,startPage:1,visiblePages:5,initiateStartPageClick:!0,hideOnlyOnePage:!1,href:!1,pageVariable:"{{page}}",totalPagesVariable:"{{total_pages}}",page:null,first:"First",prev:"Previous",next:"Next",last:"Last",loop:!1,onPageClick:null,paginationClass:"pagination",nextClass:"page-item next",prevClass:"page-item prev",lastClass:"page-item last",firstClass:"page-item first",pageClass:"page-item",activeClass:"active",disabledClass:"disabled",anchorClass:"page-link"},t.fn.twbsPagination.Constructor=o,t.fn.twbsPagination.noConflict=function(){return t.fn.twbsPagination=a,this},t.fn.twbsPagination.version="1.4.1"}(window.jQuery,window,document); -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/templates/singulinkfx/styles/main.css -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Docs/templates/singulinkfx/styles/main.js -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/singulink.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | function toggleMenu() { 4 | 5 | var sidebar = document.getElementById("sidebar"); 6 | var blackout = document.getElementById("blackout"); 7 | 8 | if (sidebar.style.left === "0px") 9 | { 10 | sidebar.style.left = "-" + sidebar.getBoundingClientRect().width + "px"; 11 | blackout.classList.remove("showThat"); 12 | blackout.classList.add("hideThat"); 13 | } 14 | else 15 | { 16 | sidebar.style.left = "0px"; 17 | blackout.classList.remove("hideThat"); 18 | blackout.classList.add("showThat"); 19 | } 20 | } 21 | 22 | // jQuery .deepest(): https://gist.github.com/geraldfullam/3a151078b55599277da4 23 | 24 | (function ($) { 25 | $.fn.deepest = function (selector) { 26 | var deepestLevel = 0, 27 | $deepestChild, 28 | $deepestChildSet; 29 | 30 | this.each(function () { 31 | $parent = $(this); 32 | $parent 33 | .find((selector || '*')) 34 | .each(function () { 35 | if (!this.firstChild || this.firstChild.nodeType !== 1) { 36 | var levelsToParent = $(this).parentsUntil($parent).length; 37 | if (levelsToParent > deepestLevel) { 38 | deepestLevel = levelsToParent; 39 | $deepestChild = $(this); 40 | } else if (levelsToParent === deepestLevel) { 41 | $deepestChild = !$deepestChild ? $(this) : $deepestChild.add(this); 42 | } 43 | } 44 | }); 45 | $deepestChildSet = !$deepestChildSet ? $deepestChild : $deepestChildSet.add($deepestChild); 46 | }); 47 | 48 | return this.pushStack($deepestChildSet || [], 'deepest', selector || ''); 49 | }; 50 | }(jQuery)); 51 | 52 | $(function() { 53 | $('table').each(function(a, tbl) { 54 | var currentTableRows = $(tbl).find('tbody tr').length; 55 | $(tbl).find('th').each(function(i) { 56 | var remove = 0; 57 | var currentTable = $(this).parents('table'); 58 | 59 | var tds = currentTable.find('tr td:nth-child(' + (i + 1) + ')'); 60 | tds.each(function(j) { if ($(this).text().trim() === '') remove++; }); 61 | 62 | if (remove == currentTableRows) { 63 | $(this).hide(); 64 | tds.hide(); 65 | } 66 | }); 67 | }); 68 | 69 | function scrollToc() { 70 | var activeTocItem = $('.sidebar').deepest('.sidebar-item.active')[0] 71 | 72 | if (activeTocItem) { 73 | activeTocItem.scrollIntoView({ block: "center" }); 74 | } 75 | else{ 76 | setTimeout(scrollToc, 500); 77 | } 78 | } 79 | 80 | setTimeout(scrollToc, 500); 81 | }); -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/styles/url.min.js: -------------------------------------------------------------------------------- 1 | /*! url - v1.8.6 - 2013-11-22 */window.url=function(){function a(a){return!isNaN(parseFloat(a))&&isFinite(a)}return function(b,c){var d=c||window.location.toString();if(!b)return d;b=b.toString(),"//"===d.substring(0,2)?d="http:"+d:1===d.split("://").length&&(d="http://"+d),c=d.split("/");var e={auth:""},f=c[2].split("@");1===f.length?f=f[0].split(":"):(e.auth=f[0],f=f[1].split(":")),e.protocol=c[0],e.hostname=f[0],e.port=f[1]||("https"===e.protocol.split(":")[0].toLowerCase()?"443":"80"),e.pathname=(c.length>3?"/":"")+c.slice(3,c.length).join("/").split("?")[0].split("#")[0];var g=e.pathname;"/"===g.charAt(g.length-1)&&(g=g.substring(0,g.length-1));var h=e.hostname,i=h.split("."),j=g.split("/");if("hostname"===b)return h;if("domain"===b)return/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(h)?h:i.slice(-2).join(".");if("sub"===b)return i.slice(0,i.length-2).join(".");if("port"===b)return e.port;if("protocol"===b)return e.protocol.split(":")[0];if("auth"===b)return e.auth;if("user"===b)return e.auth.split(":")[0];if("pass"===b)return e.auth.split(":")[1]||"";if("path"===b)return e.pathname;if("."===b.charAt(0)){if(b=b.substring(1),a(b))return b=parseInt(b,10),i[0>b?i.length+b:b-1]||""}else{if(a(b))return b=parseInt(b,10),j[0>b?j.length+b:b]||"";if("file"===b)return j.slice(-1)[0];if("filename"===b)return j.slice(-1)[0].split(".")[0];if("fileext"===b)return j.slice(-1)[0].split(".")[1]||"";if("?"===b.charAt(0)||"#"===b.charAt(0)){var k=d,l=null;if("?"===b.charAt(0)?k=(k.split("?")[1]||"").split("#")[0]:"#"===b.charAt(0)&&(k=k.split("#")[1]||""),!b.charAt(1))return k;b=b.substring(1),k=k.split("&");for(var m=0,n=k.length;n>m;m++)if(l=k[m].split("="),l[0]===b)return l[1]||"";return null}}return""}}(),"undefined"!=typeof jQuery&&jQuery.extend({url:function(a,b){return window.url(a,b)}}); -------------------------------------------------------------------------------- /Docs/templates/singulinkfx/toc.html.primary.tmpl: -------------------------------------------------------------------------------- 1 | {{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} 2 | 3 |
    4 |
    5 | {{^_disableSideFilter}} 6 |
    7 |
    8 | 9 | 10 | 11 |
    12 |
    13 | {{/_disableSideFilter}} 14 |
    15 |
    16 | {{^leaf}} 17 | {{>partials/li}} 18 | {{/leaf}} 19 |
    20 |
    21 |
    22 |
    23 | -------------------------------------------------------------------------------- /Docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Articles 2 | href: articles/ 3 | - name: API Documentation 4 | href: api/ 5 | - name: GitHub 6 | href: https://github.com/Singulink/Singulink.IO.FileSystem/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Singulink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Singulink.IO.FileSystem 2 | 3 | [![Chat on Discord](https://img.shields.io/discord/906246067773923490)](https://discord.gg/EkQhJFsBu6) 4 | [![View nuget packages](https://img.shields.io/nuget/v/Singulink.IO.FileSystem.svg)](https://www.nuget.org/packages/Singulink.IO.FileSystem/) 5 | [![Build and Test](https://github.com/Singulink/Singulink.IO.FileSystem/workflows/build%20and%20test/badge.svg)](https://github.com/Singulink/Singulink.IO.FileSystem/actions?query=workflow%3A%22build+and+test%22) 6 | 7 | **Singulink.IO.FileSystem** is a reliable cross-platform library that provides strongly-typed file/directory path manipulation and file system access in .NET. It has been designed to encourage developers to code with explicit intent in such a way that their applications can work seamlessly and bug free across both Unix and Windows file systems under all conditions. `System.IO.*` has numerous pitfalls that make 99% of file system code out in the wild fragile and problematic in edge cases. It also contains behavioral inconsistencies between Unix and Windows file systems that are abstracted and handled by this library so you don't have to worry about them. 8 | 9 | ### About Singulink 10 | 11 | We are a small team of engineers and designers dedicated to building beautiful, functional and well-engineered software solutions. We offer very competitive rates as well as fixed-price contracts and welcome inquiries to discuss any custom development / project support needs you may have. 12 | 13 | This package is part of our **Singulink Libraries** collection. Visit https://github.com/Singulink to see our full list of publicly available libraries and other open-source projects. 14 | 15 | ## Installation 16 | 17 | The package is available on NuGet - simply install the `Singulink.IO.FileSystem` package. 18 | 19 | **Supported Runtimes**: Anywhere .NET Standard 2.1+ is supported, including: 20 | - .NET 21 | - Mono 22 | - Xamarin 23 | 24 | ## Further Reading 25 | 26 | Please head over to the [project documentation site](http://www.singulink.com/Docs/Singulink.IO.FileSystem/) to view articles, examples and the fully documented API. 27 | -------------------------------------------------------------------------------- /Resources/Singulink Icon 128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Resources/Singulink Icon 128x128.png -------------------------------------------------------------------------------- /Source/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | enable 5 | true 6 | 7 | 8 | 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/AbsoluteDirectoryCombineTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace Singulink.IO.FileSystem.Tests; 5 | 6 | [TestClass] 7 | public class AbsoluteDirectoryCombineTests 8 | { 9 | [TestMethod] 10 | public void NavigateWindows() 11 | { 12 | var dir = DirectoryPath.ParseAbsolute(@"C:\dir1\dir2", PathFormat.Windows, PathOptions.None); 13 | var combined = dir.CombineDirectory("../", PathOptions.None); 14 | Assert.AreEqual(@"C:\dir1", combined.PathDisplay); 15 | Assert.IsFalse(combined.IsRoot); 16 | 17 | combined = dir.CombineDirectory("../../", PathOptions.None); 18 | Assert.AreEqual(@"C:\", combined.PathDisplay); 19 | Assert.IsTrue(combined.IsRoot); 20 | 21 | combined = dir.CombineDirectory(".", PathOptions.None); 22 | Assert.AreEqual(@"C:\dir1\dir2", combined.PathDisplay); 23 | } 24 | 25 | [TestMethod] 26 | public void NavigateRootedWindows() 27 | { 28 | var dir = DirectoryPath.ParseAbsolute(@"C:\dir1\dir2", PathFormat.Windows, PathOptions.None); 29 | 30 | var combined = dir.CombineDirectory("/", PathOptions.None); 31 | Assert.AreEqual(@"C:\", combined.PathDisplay); 32 | 33 | combined = dir.CombineDirectory("/test", PathOptions.None); 34 | Assert.AreEqual(@"C:\test", combined.PathDisplay); 35 | } 36 | 37 | [TestMethod] 38 | public void NavigateUnix() 39 | { 40 | var dir = DirectoryPath.ParseAbsolute("/dir1/dir2", PathFormat.Unix, PathOptions.None); 41 | var combined = dir.CombineDirectory("../", PathOptions.None); 42 | Assert.AreEqual("/dir1", combined.PathDisplay); 43 | Assert.IsFalse(combined.IsRoot); 44 | 45 | combined = dir.CombineDirectory("../../", PathOptions.None); 46 | Assert.AreEqual("/", combined.PathDisplay); 47 | Assert.IsTrue(combined.IsRoot); 48 | 49 | combined = dir.CombineDirectory(".", PathOptions.None); 50 | Assert.AreEqual("/dir1/dir2", combined.PathDisplay); 51 | } 52 | 53 | [TestMethod] 54 | public void NavigatePastRootWindows() 55 | { 56 | var dir = DirectoryPath.ParseAbsolute(@"C:\dir1\dir2", PathFormat.Windows, PathOptions.None); 57 | Assert.ThrowsException(() => dir.CombineDirectory("../../..", PathOptions.None)); 58 | } 59 | 60 | [TestMethod] 61 | public void NavigatePastRootUnix() 62 | { 63 | var dir = DirectoryPath.ParseAbsolute("/dir1/dir2", PathFormat.Unix, PathOptions.None); 64 | Assert.ThrowsException(() => dir.CombineDirectory("../../..", PathOptions.None)); 65 | } 66 | 67 | [TestMethod] 68 | public void CombineUniversalFile() 69 | { 70 | var dir = DirectoryPath.ParseAbsolute(@"C:\dir1\dir2", PathFormat.Windows, PathOptions.None); 71 | var file = dir.CombineFile("../file.txt", PathFormat.Universal, PathOptions.None); 72 | Assert.AreEqual(PathFormat.Windows, file.PathFormat); 73 | Assert.AreEqual(@"C:\dir1\file.txt", file.PathDisplay); 74 | } 75 | 76 | [TestMethod] 77 | public void CombineDirectory() 78 | { 79 | var dir = DirectoryPath.ParseAbsolute(@"C:\dir1\dir2", PathFormat.Windows, PathOptions.None); 80 | var combined = dir.CombineDirectory("..", PathFormat.Universal, PathOptions.None); 81 | Assert.AreEqual(PathFormat.Windows, combined.PathFormat); 82 | Assert.AreEqual(@"C:\dir1", combined.PathDisplay); 83 | 84 | dir = DirectoryPath.ParseAbsolute("/dir1/dir2", PathFormat.Unix, PathOptions.None); 85 | combined = dir.CombineDirectory(".", PathFormat.Universal, PathOptions.None); 86 | Assert.AreEqual(PathFormat.Unix, combined.PathFormat); 87 | Assert.AreEqual("/dir1/dir2", combined.PathDisplay); 88 | 89 | combined = dir.CombineDirectory("newdir/newdir2", PathFormat.Unix, PathOptions.None); 90 | Assert.AreEqual("/dir1/dir2/newdir/newdir2", combined.PathDisplay); 91 | } 92 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/AbsoluteDirectoryParentTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace Singulink.IO.FileSystem.Tests; 4 | 5 | [TestClass] 6 | public class AbsoluteDirectoryParentTests 7 | { 8 | [TestMethod] 9 | public void Windows() 10 | { 11 | var dir = DirectoryPath.ParseAbsolute(@"C:\test\test2\test3", PathFormat.Windows); 12 | Assert.IsTrue(dir.HasParentDirectory); 13 | Assert.AreEqual(@"C:\test\test2\test3", dir.PathDisplay); 14 | 15 | dir = dir.ParentDirectory!; 16 | Assert.IsTrue(dir.HasParentDirectory); 17 | Assert.AreEqual(@"C:\test\test2", dir.PathDisplay); 18 | 19 | dir = dir.ParentDirectory!; 20 | Assert.IsTrue(dir.HasParentDirectory); 21 | Assert.AreEqual(@"C:\test", dir.PathDisplay); 22 | 23 | dir = dir.ParentDirectory!; 24 | Assert.IsFalse(dir.HasParentDirectory); 25 | Assert.AreEqual(@"C:\", dir.PathDisplay); 26 | Assert.IsNull(dir.ParentDirectory); 27 | } 28 | 29 | [TestMethod] 30 | public void Unix() 31 | { 32 | var dir = DirectoryPath.ParseAbsolute("/test/test2/test3", PathFormat.Unix); 33 | Assert.IsTrue(dir.HasParentDirectory); 34 | Assert.AreEqual("/test/test2/test3", dir.PathDisplay); 35 | 36 | dir = dir.ParentDirectory!; 37 | Assert.IsTrue(dir.HasParentDirectory); 38 | Assert.AreEqual("/test/test2", dir.PathDisplay); 39 | 40 | dir = dir.ParentDirectory!; 41 | Assert.IsTrue(dir.HasParentDirectory); 42 | Assert.AreEqual("/test", dir.PathDisplay); 43 | 44 | dir = dir.ParentDirectory!; 45 | Assert.IsFalse(dir.HasParentDirectory); 46 | Assert.AreEqual("/", dir.PathDisplay); 47 | Assert.IsNull(dir.ParentDirectory); 48 | } 49 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/AbsoluteDirectoryParseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | #pragma warning disable SA1122 // Use string.Empty for empty strings 5 | 6 | namespace Singulink.IO.FileSystem.Tests; 7 | 8 | [TestClass] 9 | public class AbsoluteDirectoryParseTests 10 | { 11 | [DataTestMethod] 12 | [DataRow("c:/test")] 13 | [DataRow("c:")] 14 | [DataRow(@"c:\test")] 15 | [DataRow(@"c:\")] 16 | [DataRow(@"\\server\test")] 17 | [DataRow(@"\\server\test\")] 18 | [DataRow("//server/test")] 19 | [DataRow("//server/test/test")] 20 | public void ParseToCorrectWindowsType(string path) 21 | { 22 | var dir = DirectoryPath.Parse(path, PathFormat.Windows); 23 | Assert.IsTrue(dir.IsAbsolute); 24 | Assert.IsTrue(dir is IAbsoluteDirectoryPath); 25 | } 26 | 27 | [DataTestMethod] 28 | [DataRow("/")] 29 | [DataRow("/test")] 30 | public void ParseToCorrectUnixType(string path) 31 | { 32 | var dir = DirectoryPath.Parse(path, PathFormat.Unix); 33 | Assert.IsTrue(dir.IsAbsolute); 34 | Assert.IsTrue(dir is IAbsoluteDirectoryPath); 35 | } 36 | 37 | [TestMethod] 38 | public void RootUnix() 39 | { 40 | var dir = DirectoryPath.ParseAbsolute("/", PathFormat.Unix, PathOptions.None); 41 | Assert.AreEqual("/", dir.Name); 42 | Assert.AreEqual("/", dir.PathDisplay); 43 | Assert.AreEqual("/", dir.PathExport); 44 | Assert.IsTrue(dir.IsRooted); 45 | Assert.IsTrue(dir.IsRoot); 46 | } 47 | 48 | [TestMethod] 49 | public void RootWindowsDrive() 50 | { 51 | var dir = DirectoryPath.ParseAbsolute("c:", PathFormat.Windows, PathOptions.None); 52 | Assert.AreEqual("c:", dir.Name); 53 | Assert.AreEqual(@"c:\", dir.PathDisplay); 54 | Assert.AreEqual(@"\\?\c:\", dir.PathExport); 55 | Assert.IsTrue(dir.IsRooted); 56 | Assert.IsTrue(dir.IsRoot); 57 | 58 | dir = DirectoryPath.ParseAbsolute("x:/", PathFormat.Windows, PathOptions.None); 59 | Assert.AreEqual("x:", dir.Name); 60 | Assert.AreEqual(@"x:\", dir.PathDisplay); 61 | Assert.AreEqual(@"\\?\x:\", dir.PathExport); 62 | Assert.IsTrue(dir.IsRooted); 63 | Assert.IsTrue(dir.IsRoot); 64 | } 65 | 66 | [TestMethod] 67 | public void RootWindowsUnc() 68 | { 69 | var dir = DirectoryPath.ParseAbsolute(@"\\Server\Share", PathFormat.Windows, PathOptions.None); 70 | Assert.AreEqual(@"\\Server\Share", dir.Name); 71 | Assert.AreEqual(@"\\Server\Share\", dir.PathDisplay); 72 | Assert.AreEqual(@"\\?\UNC\Server\Share\", dir.PathExport); 73 | Assert.IsTrue(dir.IsRooted); 74 | Assert.IsTrue(dir.IsRoot); 75 | 76 | Assert.ThrowsException(() => DirectoryPath.ParseAbsolute("\\Server", PathFormat.Windows, PathOptions.None)); 77 | } 78 | 79 | [DataTestMethod] 80 | [DataRow("test")] 81 | [DataRow("")] 82 | [DataRow("xy:/ ")] 83 | [DataRow("1:/")] 84 | [DataRow(" :/")] 85 | public void BadWindowsPaths(string path) 86 | { 87 | Assert.ThrowsException(() => DirectoryPath.ParseAbsolute(path, PathFormat.Windows, PathOptions.None)); 88 | } 89 | 90 | [DataTestMethod] 91 | [DataRow("test")] 92 | [DataRow("")] 93 | [DataRow(" /")] 94 | public void BadUnixPaths(string path) 95 | { 96 | Assert.ThrowsException(() => DirectoryPath.ParseAbsolute(path, PathFormat.Unix, PathOptions.None)); 97 | } 98 | 99 | [TestMethod] 100 | public void Navigation() 101 | { 102 | var dir = DirectoryPath.ParseAbsolute(@"\\Server\Share\test1\test2\..\..", PathFormat.Windows, PathOptions.None); 103 | Assert.AreEqual(@"\\Server\Share\", dir.PathDisplay); 104 | 105 | var ex = Assert.ThrowsException(() => DirectoryPath.ParseAbsolute(@"\\Server\Share\test1\test2\..\..\..", PathFormat.Windows, PathOptions.None)); 106 | Assert.AreEqual("Attempt to navigate past root directory. (Parameter 'path')", ex.Message); 107 | 108 | dir = DirectoryPath.ParseAbsolute("c:/./test/.././", PathFormat.Windows, PathOptions.None); 109 | Assert.AreEqual(@"c:\", dir.PathDisplay); 110 | Assert.IsTrue(dir.IsRoot); 111 | 112 | dir = DirectoryPath.ParseAbsolute("/./test/.././", PathFormat.Unix, PathOptions.None); 113 | Assert.AreEqual("/", dir.PathDisplay); 114 | Assert.IsTrue(dir.IsRoot); 115 | } 116 | 117 | [TestMethod] 118 | public void PathFormatDependent() 119 | { 120 | var dir = DirectoryPath.ParseAbsolute("/ test.", PathFormat.Unix, PathOptions.PathFormatDependent); 121 | Assert.AreEqual("/ test.", dir.PathDisplay); 122 | 123 | dir = DirectoryPath.ParseAbsolute("c:/ test.", PathFormat.Windows, PathOptions.None); 124 | Assert.AreEqual(@"c:\ test.", dir.PathDisplay); 125 | Assert.ThrowsException(() => DirectoryPath.ParseRelative("/ test.", PathFormat.Windows, PathOptions.PathFormatDependent)); 126 | } 127 | 128 | [DataTestMethod] 129 | [DataRow("/test")] 130 | [DataRow("/")] 131 | public void NoUniversal(string path) 132 | { 133 | Assert.ThrowsException(() => DirectoryPath.Parse(path, PathFormat.Universal)); 134 | Assert.ThrowsException(() => DirectoryPath.ParseAbsolute(path, PathFormat.Universal)); 135 | } 136 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/AbsoluteDirectoryPropertyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Singulink.IO.FileSystem.Tests; 7 | 8 | [TestClass] 9 | public class AbsoluteDirectoryPropertyTests 10 | { 11 | public static readonly DateTime EarliestDateTime = new DateTime(2010, 1, 1); 12 | 13 | [TestMethod] 14 | public void Properties() 15 | { 16 | var dir = FilePath.ParseAbsolute(Assembly.GetExecutingAssembly().Location).ParentDirectory; 17 | 18 | Assert.IsTrue(dir.Exists); 19 | Assert.IsTrue(dir.TotalSize > 0); 20 | Assert.IsTrue(dir.TotalFreeSpace > 0); 21 | Assert.IsTrue(dir.AvailableFreeSpace > 0); 22 | Assert.IsFalse(string.IsNullOrWhiteSpace(dir.FileSystem)); 23 | Assert.AreNotEqual(DriveType.NoRootDirectory, dir.DriveType); 24 | Assert.IsTrue(dir.CreationTime > EarliestDateTime); 25 | Assert.IsTrue(dir.LastAccessTime > EarliestDateTime); 26 | Assert.IsTrue(dir.LastWriteTime > EarliestDateTime); 27 | } 28 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/AbsoluteFileParentTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace Singulink.IO.FileSystem.Tests; 4 | 5 | [TestClass] 6 | public class AbsoluteFileParentTests 7 | { 8 | [TestMethod] 9 | public void IsImplemented() 10 | { 11 | var file = FilePath.ParseAbsolute(@"C:\test.asdf", PathFormat.Windows); 12 | Assert.IsTrue(file.HasParentDirectory); 13 | Assert.IsNotNull(file.ParentDirectory); 14 | } 15 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/AbsoluteFileParseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace Singulink.IO.FileSystem.Tests; 5 | 6 | [TestClass] 7 | public class AbsoluteFileParseTests 8 | { 9 | [TestMethod] 10 | public void ParseToCorrectType() 11 | { 12 | var files = new[] { 13 | FilePath.Parse("/test.sdf", PathFormat.Unix), 14 | FilePath.Parse("c:/test.rga", PathFormat.Windows), 15 | FilePath.Parse(@"c:\test.agae", PathFormat.Windows), 16 | FilePath.Parse(@"\\server\test\test.sef", PathFormat.Windows), 17 | FilePath.Parse("//server/test/test.rae", PathFormat.Windows), 18 | }; 19 | 20 | foreach (var file in files) { 21 | Assert.IsTrue(file.IsAbsolute); 22 | Assert.IsTrue(file is IAbsoluteFilePath); 23 | } 24 | } 25 | 26 | [TestMethod] 27 | public void NoUniversal() 28 | { 29 | Assert.ThrowsException(() => FilePath.Parse("/test.asdf", PathFormat.Universal)); 30 | Assert.ThrowsException(() => FilePath.ParseAbsolute("/test.sdf", PathFormat.Universal)); 31 | } 32 | 33 | [TestMethod] 34 | public void NoMissingFilePaths() 35 | { 36 | Assert.ThrowsException(() => FilePath.ParseAbsolute("C:", PathFormat.Windows)); 37 | Assert.ThrowsException(() => FilePath.ParseAbsolute(@"C:\", PathFormat.Windows)); 38 | Assert.ThrowsException(() => FilePath.ParseAbsolute(@"C:\test\", PathFormat.Windows)); 39 | Assert.ThrowsException(() => FilePath.ParseAbsolute(@"C:\test\..", PathFormat.Windows)); 40 | Assert.ThrowsException(() => FilePath.ParseAbsolute(@"C:\test.txt\.", PathFormat.Windows)); 41 | Assert.ThrowsException(() => FilePath.ParseAbsolute(@"C:\test\test.txt\..", PathFormat.Windows)); 42 | 43 | Assert.ThrowsException(() => FilePath.ParseAbsolute("/", PathFormat.Unix)); 44 | Assert.ThrowsException(() => FilePath.ParseAbsolute("/test/", PathFormat.Unix)); 45 | Assert.ThrowsException(() => FilePath.ParseAbsolute("/test/..", PathFormat.Unix)); 46 | Assert.ThrowsException(() => FilePath.ParseAbsolute("/test.txt/.", PathFormat.Unix)); 47 | Assert.ThrowsException(() => FilePath.ParseAbsolute("/test/test.txt/..", PathFormat.Unix)); 48 | } 49 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/EnumeratingDirectoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | #pragma warning disable SA1122 // Use string.Empty for empty strings 7 | 8 | namespace Singulink.IO.FileSystem.Tests; 9 | 10 | [TestClass] 11 | public class EnumeratingDirectoryTests 12 | { 13 | private const int DirCount = 5; 14 | private const int SubDirCount = 6; 15 | private const int FileCount = 7; 16 | 17 | private const int TotalDirCount = DirCount + (DirCount * SubDirCount); 18 | private const int TotalFileCount = DirCount * SubDirCount * FileCount; 19 | 20 | private static IAbsoluteDirectoryPath SetupTestDirectory() 21 | { 22 | var testDir = DirectoryPath.GetCurrent() + DirectoryPath.ParseRelative("_test"); 23 | 24 | if (testDir.Exists) 25 | testDir.Delete(true); 26 | 27 | testDir.Create(); 28 | 29 | for (int i = 0; i < DirCount; i++) { 30 | var dirLevel1 = testDir.CombineDirectory($"{i}_dir"); 31 | 32 | for (int j = 0; j < SubDirCount; j++) { 33 | var dirLevel2 = dirLevel1.CombineDirectory($"{i}_{j}_subdir"); 34 | dirLevel2.Create(); 35 | 36 | for (int k = 0; k < FileCount; k++) 37 | dirLevel2.CombineFile($"{i}_{j}_{k}_file.txt").OpenStream(FileMode.CreateNew).Dispose(); 38 | } 39 | } 40 | 41 | return testDir; 42 | } 43 | 44 | [TestMethod] 45 | public void GetTotalChildEntries() 46 | { 47 | var dir = SetupTestDirectory(); 48 | var recursive = new SearchOptions { Recursive = true }; 49 | 50 | int entryCount = dir.GetChildEntries(recursive).Count(); 51 | Assert.AreEqual(TotalDirCount + TotalFileCount, entryCount); 52 | 53 | int fileCount = dir.GetChildFiles(recursive).Count(); 54 | Assert.AreEqual(TotalFileCount, fileCount); 55 | 56 | int dirCount = dir.GetChildDirectories(recursive).Count(); 57 | Assert.AreEqual(TotalDirCount, dirCount); 58 | } 59 | 60 | [TestMethod] 61 | public void GetFilteredChildEntries() 62 | { 63 | var dir = SetupTestDirectory(); 64 | var recursive = new SearchOptions { Recursive = true }; 65 | var nonRecursive = new SearchOptions(); 66 | 67 | var dirs = dir.GetChildDirectories("?_?_subdir", recursive).ToList(); 68 | Assert.AreEqual(DirCount * SubDirCount, dirs.Count()); 69 | Assert.AreEqual(dir.RootDirectory, dirs[0].RootDirectory); // Ensure RootLength is set correctly 70 | 71 | int fileCount = dir 72 | .GetChildDirectories("1_dir", nonRecursive).Single() 73 | .GetChildDirectories("1_1_subdir", nonRecursive).Single() 74 | .GetChildFiles("*file.txt", nonRecursive).Count(); 75 | 76 | Assert.AreEqual(FileCount, fileCount); 77 | } 78 | 79 | [TestMethod] 80 | public void GetRelativeChildEntries() 81 | { 82 | var dir = SetupTestDirectory(); 83 | var recursive = new SearchOptions { Recursive = true }; 84 | var nonRecursive = new SearchOptions(); 85 | 86 | var file = dir 87 | .GetChildDirectories("1_dir", nonRecursive).Single() 88 | .GetChildDirectories("1_1_subdir", nonRecursive).Single() 89 | .GetRelativeChildFiles("*_1_file.txt", nonRecursive).Single(); 90 | 91 | Assert.AreEqual("1_1_1_file.txt", file.PathDisplay); 92 | 93 | var files = dir.GetRelativeChildFiles("*_1_file.txt", recursive).ToArray(); 94 | Assert.AreEqual(DirCount * SubDirCount, files.Length); 95 | 96 | foreach (var f in files) { 97 | Assert.AreEqual("", f.ParentDirectory!.ParentDirectory!.ParentDirectory!.PathDisplay); 98 | Assert.AreEqual(false, f.IsRooted); 99 | } 100 | } 101 | 102 | [TestMethod] 103 | public void GetRelativeEntriesFromParentSearchLocation() 104 | { 105 | var dir = SetupTestDirectory(); 106 | var recursive = new SearchOptions { Recursive = true }; 107 | 108 | var parentDir = DirectoryPath.ParseRelative(".."); 109 | 110 | var files = dir.GetRelativeEntries(parentDir, "*_1_file.txt", recursive).ToArray(); 111 | Assert.AreEqual(DirCount * SubDirCount, files.Length); 112 | 113 | foreach (var f in files) { 114 | Assert.AreEqual("", f.ParentDirectory!.ParentDirectory!.ParentDirectory!.PathDisplay); 115 | Assert.AreEqual(false, f.IsRooted); 116 | } 117 | } 118 | 119 | [TestMethod] 120 | public void GetRelativeEntriesFromSubdirectorySearchLocation() 121 | { 122 | var dir = SetupTestDirectory(); 123 | var recursive = new SearchOptions { Recursive = true }; 124 | 125 | var dirLevel1 = DirectoryPath.ParseRelative("1_dir"); 126 | 127 | var files = dir.GetRelativeEntries(dirLevel1, "*_1_file.txt", recursive).ToArray(); 128 | Assert.AreEqual(SubDirCount, files.Length); 129 | 130 | foreach (var f in files) { 131 | Assert.AreEqual("", f.ParentDirectory!.ParentDirectory!.ParentDirectory!.PathDisplay); 132 | Assert.AreEqual(false, f.IsRooted); 133 | } 134 | } 135 | 136 | [DataTestMethod] 137 | [DataRow(".")] 138 | [DataRow("../_test")] 139 | public void GetRelativeEntriesFromCurrent(string currentDirPath) 140 | { 141 | var dir = SetupTestDirectory(); 142 | var recursive = new SearchOptions { Recursive = true }; 143 | 144 | var currentDir = DirectoryPath.ParseRelative(currentDirPath); 145 | var files = dir.GetRelativeEntries(currentDir, "*_1_file.txt", recursive).ToArray(); 146 | Assert.AreEqual(DirCount * SubDirCount, files.Length); 147 | 148 | foreach (var f in files) { 149 | Assert.AreEqual(currentDir.PathDisplay, f.ParentDirectory!.ParentDirectory!.ParentDirectory!.PathDisplay); 150 | Assert.AreEqual(false, f.IsRooted); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/EqualsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace Singulink.IO.FileSystem.Tests; 4 | 5 | [TestClass] 6 | public class EqualsTests 7 | { 8 | [TestMethod] 9 | public void EqualsMatchingFile() 10 | { 11 | var x = FilePath.Parse(@"c:\somepath", PathFormat.Windows); 12 | var y = FilePath.Parse(@"C:\somepath", PathFormat.Windows); 13 | 14 | Assert.IsTrue(x.Equals(y)); 15 | Assert.AreEqual(x, y); 16 | } 17 | 18 | [TestMethod] 19 | public void NotEqualsOtherFile() 20 | { 21 | var x = FilePath.Parse(@"c:\somepath", PathFormat.Windows); 22 | var y = FilePath.Parse(@"C:\someotherpath", PathFormat.Windows); 23 | 24 | Assert.IsFalse(x.Equals(y)); 25 | Assert.AreNotEqual(x, y); 26 | } 27 | 28 | [TestMethod] 29 | public void NotEqualsMatchingDir() 30 | { 31 | var x = FilePath.Parse(@"c:\somepath", PathFormat.Windows); 32 | var y = DirectoryPath.Parse(@"c:\somepath", PathFormat.Windows); 33 | 34 | Assert.IsFalse(x.Equals(y)); 35 | Assert.AreNotEqual((IPath)x, y); 36 | } 37 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Determines whether outputs (return values and out/ref parameters) have null checks injected. 12 | 13 | 14 | 15 | 16 | Determines whether non-public members have null checks injected or if only public entry points are checked. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 25 | 26 | 27 | 28 | 29 | A comma-separated list of error codes that can be safely ignored in assembly verification. 30 | 31 | 32 | 33 | 34 | 'false' to turn off automatic generation of the XML Schema file. 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/PlatformConsistencyTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace Singulink.IO.FileSystem.Tests; 6 | 7 | [TestClass] 8 | public class PlatformConsistencyTests 9 | { 10 | private const string FileName = "test.file"; 11 | 12 | private static IAbsoluteDirectoryPath SetupTestDirectory() 13 | { 14 | var testDir = DirectoryPath.GetCurrent() + DirectoryPath.ParseRelative("_test"); 15 | 16 | if (testDir.Exists) 17 | testDir.Delete(true); 18 | 19 | testDir.Create(); 20 | testDir.CombineFile(FileName).OpenStream(FileMode.CreateNew).Dispose(); 21 | 22 | return testDir; 23 | } 24 | 25 | [TestMethod] 26 | public void FileIsDirectory() 27 | { 28 | var file = FilePath.ParseAbsolute(SetupTestDirectory().PathExport); 29 | 30 | Assert.IsFalse(file.Exists); 31 | Assert.ThrowsException(() => _ = file.Attributes); 32 | Assert.ThrowsException(() => _ = file.CreationTime); 33 | Assert.ThrowsException(() => file.IsReadOnly = true); 34 | Assert.ThrowsException(() => file.Attributes |= FileAttributes.Hidden); 35 | Assert.ThrowsException(() => file.Length); 36 | 37 | // No exception should be thrown for files that don't exist 38 | file.Delete(); 39 | } 40 | 41 | [TestMethod] 42 | public void DirectoryIsFile() 43 | { 44 | var dir = SetupTestDirectory().CombineDirectory(FileName); 45 | 46 | Assert.IsFalse(dir.Exists); 47 | Assert.ThrowsException(() => _ = dir.IsEmpty); 48 | Assert.ThrowsException(() => _ = dir.Attributes); 49 | Assert.ThrowsException(() => _ = dir.CreationTime); 50 | Assert.ThrowsException(() => _ = dir.AvailableFreeSpace); 51 | Assert.ThrowsException(() => _ = dir.TotalFreeSpace); 52 | Assert.ThrowsException(() => _ = dir.TotalSize); 53 | Assert.ThrowsException(() => dir.Attributes |= FileAttributes.Hidden); 54 | Assert.ThrowsException(() => _ = dir.DriveType); 55 | Assert.ThrowsException(() => _ = dir.FileSystem); 56 | 57 | Assert.ThrowsException(() => dir.GetChildEntries().FirstOrDefault()); 58 | 59 | Assert.ThrowsException(() => dir.Delete(true)); 60 | } 61 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/Properties/Assembly.cs: -------------------------------------------------------------------------------- 1 | // [assembly: ] -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/RelativeDirectoryCombineTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | #pragma warning disable SA1122 // Use string.Empty for empty strings 4 | 5 | namespace Singulink.IO.FileSystem.Tests; 6 | 7 | [TestClass] 8 | public class RelativeDirectoryCombineTests 9 | { 10 | [TestMethod] 11 | public void NavigateRooted() 12 | { 13 | var dir = DirectoryPath.ParseRelative("test", PathFormat.Windows, PathOptions.None); 14 | 15 | var combined = dir.CombineDirectory("/rooted", PathOptions.None); 16 | Assert.AreEqual(@"\rooted", combined.PathDisplay); 17 | 18 | combined = dir.CombineDirectory("/", PathOptions.None); 19 | Assert.AreEqual(@"\", combined.PathDisplay); 20 | 21 | dir = DirectoryPath.ParseRelative("/dir1/dir2", PathFormat.Windows, PathOptions.None); 22 | 23 | combined = dir.CombineDirectory("/rooted", PathOptions.None); 24 | Assert.AreEqual(@"\rooted", combined.PathDisplay); 25 | 26 | combined = dir.CombineDirectory("/", PathOptions.None); 27 | Assert.AreEqual(@"\", combined.PathDisplay); 28 | 29 | dir = DirectoryPath.ParseRelative("", PathFormat.Windows, PathOptions.None); 30 | 31 | combined = dir.CombineDirectory("/rooted", PathOptions.None); 32 | Assert.AreEqual(@"\rooted", combined.PathDisplay); 33 | 34 | combined = dir.CombineDirectory("/", PathOptions.None); 35 | Assert.AreEqual(@"\", combined.PathDisplay); 36 | 37 | var combinedFile = dir.CombineFile("/dir/file.txt", PathFormat.Windows, PathOptions.None); 38 | Assert.AreEqual(@"\dir\file.txt", combinedFile.PathDisplay); 39 | } 40 | 41 | [TestMethod] 42 | public void CombineUniversalFile() 43 | { 44 | var dir = DirectoryPath.ParseRelative(@"dir1\dir2", PathFormat.Windows, PathOptions.None); 45 | var file = dir.CombineFile("../file.txt", PathFormat.Universal, PathOptions.None); 46 | Assert.AreEqual(PathFormat.Windows, file.PathFormat); 47 | Assert.AreEqual(@"dir1\file.txt", file.PathDisplay); 48 | } 49 | 50 | [TestMethod] 51 | public void CombineDirectory() 52 | { 53 | var dir = DirectoryPath.ParseRelative("dir1/dir2", PathFormat.Unix, PathOptions.None); 54 | var combined = dir.CombineDirectory("..", PathFormat.Universal, PathOptions.None); 55 | Assert.AreEqual(PathFormat.Unix, combined.PathFormat); 56 | Assert.AreEqual("dir1", combined.PathDisplay); 57 | 58 | dir = DirectoryPath.ParseRelative("dir1/dir2", PathFormat.Unix, PathOptions.None); 59 | combined = dir.CombineDirectory(".", PathFormat.Universal, PathOptions.None); 60 | Assert.AreEqual(PathFormat.Unix, combined.PathFormat); 61 | Assert.AreEqual("dir1/dir2", combined.PathDisplay); 62 | 63 | combined = dir.CombineDirectory("newdir/newdir2", PathFormat.Unix, PathOptions.None); 64 | Assert.AreEqual("dir1/dir2/newdir/newdir2", combined.PathDisplay); 65 | } 66 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/RelativeDirectoryParentTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | #pragma warning disable SA1122 // Use string.Empty for empty strings 4 | 5 | namespace Singulink.IO.FileSystem.Tests; 6 | 7 | [TestClass] 8 | public class RelativeDirectoryParentTests 9 | { 10 | [TestMethod] 11 | public void SpecialCurrent() 12 | { 13 | var dir = DirectoryPath.ParseRelative("", PathOptions.None); 14 | Assert.IsTrue(dir.HasParentDirectory); 15 | Assert.AreEqual("..", dir.ParentDirectory!.PathDisplay); 16 | 17 | dir = DirectoryPath.ParseRelative(".", PathOptions.None); 18 | Assert.IsTrue(dir.HasParentDirectory); 19 | Assert.AreEqual("..", dir.ParentDirectory!.PathDisplay); 20 | } 21 | 22 | [TestMethod] 23 | public void SpecialParent() 24 | { 25 | var dir = DirectoryPath.ParseRelative("..", PathFormat.Windows, PathOptions.None); 26 | Assert.IsTrue(dir.HasParentDirectory); 27 | Assert.AreEqual(@"..\..", dir.ParentDirectory!.PathDisplay); 28 | 29 | dir = DirectoryPath.ParseRelative("../..", PathFormat.Unix, PathOptions.None); 30 | Assert.IsTrue(dir.HasParentDirectory); 31 | Assert.AreEqual("../../..", dir.ParentDirectory!.PathDisplay); 32 | } 33 | 34 | [TestMethod] 35 | public void Rooted() 36 | { 37 | var dir = DirectoryPath.ParseRelative("/", PathFormat.Windows, PathOptions.None); 38 | Assert.IsFalse(dir.HasParentDirectory); 39 | Assert.IsNull(dir.ParentDirectory); 40 | 41 | dir = DirectoryPath.ParseRelative("/test", PathFormat.Windows, PathOptions.None); 42 | Assert.IsTrue(dir.HasParentDirectory); 43 | 44 | dir = dir.ParentDirectory!; 45 | Assert.AreEqual(@"\", dir.PathDisplay); 46 | Assert.IsFalse(dir.HasParentDirectory); 47 | } 48 | 49 | [TestMethod] 50 | public void NavigatingPastEmpty() 51 | { 52 | var dir = DirectoryPath.ParseRelative("dir1/dir2", PathFormat.Universal, PathOptions.None); 53 | Assert.AreEqual("dir1/dir2", dir.PathDisplay); 54 | 55 | var parent = dir.ParentDirectory!; 56 | Assert.AreEqual("dir1", parent.PathDisplay); 57 | 58 | parent = parent.ParentDirectory!; 59 | Assert.AreEqual("", parent.PathDisplay); 60 | 61 | parent = parent.ParentDirectory!; 62 | Assert.AreEqual("..", parent.PathDisplay); 63 | 64 | parent = parent.ParentDirectory!; 65 | Assert.AreEqual("../..", parent.PathDisplay); 66 | 67 | parent = parent.ParentDirectory!; 68 | Assert.AreEqual("../../..", parent.PathDisplay); 69 | } 70 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/RelativeFileParentTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | #pragma warning disable SA1122 // Use string.Empty for empty strings 4 | 5 | namespace Singulink.IO.FileSystem.Tests; 6 | 7 | [TestClass] 8 | public class RelativeFileParentTests 9 | { 10 | [TestMethod] 11 | public void IsImplemented() 12 | { 13 | var file = FilePath.ParseRelative("test.asdf", PathFormat.Windows); 14 | Assert.IsTrue(file.HasParentDirectory); 15 | 16 | var dir = file.ParentDirectory; 17 | Assert.AreEqual(PathFormat.Windows.RelativeCurrentDirectory, dir); 18 | } 19 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/RelativeFileParseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | #pragma warning disable SA1122 // Use string.Empty for empty strings 5 | 6 | namespace Singulink.IO.FileSystem.Tests; 7 | 8 | [TestClass] 9 | public class RelativeFileParseTests 10 | { 11 | [TestMethod] 12 | public void ParseToCorrectType() 13 | { 14 | var files = new[] { 15 | FilePath.Parse("test.sdf", PathFormat.Unix), 16 | FilePath.Parse("./test.sdf", PathFormat.Unix), 17 | FilePath.Parse("../test.sdf", PathFormat.Unix), 18 | 19 | FilePath.Parse("test.sdf", PathFormat.Universal), 20 | FilePath.Parse("./test.sdf", PathFormat.Universal), 21 | FilePath.Parse("../test.sdf", PathFormat.Universal), 22 | 23 | FilePath.Parse("test.rga", PathFormat.Windows), 24 | FilePath.Parse("/test.rga", PathFormat.Windows), 25 | FilePath.Parse("./test.sdf", PathFormat.Windows), 26 | FilePath.Parse("../test.sdf", PathFormat.Windows), 27 | FilePath.Parse(@"\test.agae", PathFormat.Windows), 28 | FilePath.Parse(@".\test.sdf", PathFormat.Windows), 29 | FilePath.Parse(@"..\test.sdf", PathFormat.Windows), 30 | }; 31 | 32 | foreach (var file in files) { 33 | Assert.IsFalse(file.IsAbsolute); 34 | Assert.IsTrue(file is IRelativeFilePath); 35 | } 36 | } 37 | 38 | [TestMethod] 39 | public void NoMissingFilePaths() 40 | { 41 | Assert.ThrowsException(() => FilePath.ParseRelative("", PathFormat.Windows)); 42 | Assert.ThrowsException(() => FilePath.ParseRelative(@"\", PathFormat.Windows)); 43 | Assert.ThrowsException(() => FilePath.ParseRelative(@"test\", PathFormat.Windows)); 44 | Assert.ThrowsException(() => FilePath.ParseRelative(@"test\..", PathFormat.Windows)); 45 | Assert.ThrowsException(() => FilePath.ParseRelative(@"test.txt\.", PathFormat.Windows)); 46 | Assert.ThrowsException(() => FilePath.ParseRelative(@"test\test.txt\..", PathFormat.Windows)); 47 | 48 | Assert.ThrowsException(() => FilePath.ParseRelative("", PathFormat.Unix)); 49 | Assert.ThrowsException(() => FilePath.ParseRelative("test/", PathFormat.Unix)); 50 | Assert.ThrowsException(() => FilePath.ParseRelative("test/..", PathFormat.Unix)); 51 | Assert.ThrowsException(() => FilePath.ParseRelative("test.txt/.", PathFormat.Unix)); 52 | Assert.ThrowsException(() => FilePath.ParseRelative("test/test.txt/..", PathFormat.Unix)); 53 | 54 | Assert.ThrowsException(() => FilePath.ParseRelative("", PathFormat.Universal)); 55 | Assert.ThrowsException(() => FilePath.ParseRelative("test/", PathFormat.Universal)); 56 | Assert.ThrowsException(() => FilePath.ParseRelative("test/..", PathFormat.Universal)); 57 | Assert.ThrowsException(() => FilePath.ParseRelative("test.txt/.", PathFormat.Universal)); 58 | Assert.ThrowsException(() => FilePath.ParseRelative("test/test.txt/..", PathFormat.Universal)); 59 | } 60 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/Singulink.IO.FileSystem.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | false 5 | 1591 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.Tests/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enabling configuration: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 3 | 4 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 5 | "settings": { 6 | "documentationRules": { 7 | "documentExposedElements": false, 8 | "documentInternalElements": false, 9 | "documentInterfaces": false 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.IO.FileSystem", "Singulink.IO.FileSystem\Singulink.IO.FileSystem.csproj", "{C4076254-20A2-4038-A216-789EC3B2CAE5}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5C70A00C-3119-4E0F-9F87-636636602B07}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | Directory.Build.props = Directory.Build.props 12 | EndProjectSection 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Singulink.IO.FileSystem.Tests", "Singulink.IO.FileSystem.Tests\Singulink.IO.FileSystem.Tests.csproj", "{ECD95980-56B0-4D4C-BB12-2822A2EB1A2C}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{DA4F4FCC-2874-4B22-A833-40875C4B5963}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {C4076254-20A2-4038-A216-789EC3B2CAE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {C4076254-20A2-4038-A216-789EC3B2CAE5}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {C4076254-20A2-4038-A216-789EC3B2CAE5}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {C4076254-20A2-4038-A216-789EC3B2CAE5}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {ECD95980-56B0-4D4C-BB12-2822A2EB1A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {ECD95980-56B0-4D4C-BB12-2822A2EB1A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {ECD95980-56B0-4D4C-BB12-2822A2EB1A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {ECD95980-56B0-4D4C-BB12-2822A2EB1A2C}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(SolutionProperties) = preSolution 34 | HideSolutionNode = FALSE 35 | EndGlobalSection 36 | GlobalSection(NestedProjects) = preSolution 37 | {ECD95980-56B0-4D4C-BB12-2822A2EB1A2C} = {DA4F4FCC-2874-4B22-A833-40875C4B5963} 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {E9737AFF-8BF4-4735-B24B-950C6FFE3BAE} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/DirectoryPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | namespace Singulink.IO; 7 | 8 | /// 9 | /// Contains methods for parsing directory paths and working with special directories. 10 | /// 11 | public static class DirectoryPath 12 | { 13 | #region Directory Parsing 14 | 15 | /// 16 | /// Parses an absolute or relative directory path using the specified options and the current platform's format. 17 | /// 18 | /// A directory path. 19 | /// Specifies the path parsing options. 20 | public static IDirectoryPath Parse(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 21 | { 22 | return Parse(path, PathFormat.Current, options); 23 | } 24 | 25 | /// 26 | /// Parses an absolute or relative directory path using the specified format and options. 27 | /// 28 | /// A directory path. 29 | /// The path's format. 30 | /// Specifies the path parsing options. 31 | public static IDirectoryPath Parse(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) 32 | { 33 | if (format.GetPathKind(path) == PathKind.Absolute) 34 | return ParseAbsolute(path, format, options); 35 | 36 | return ParseRelative(path, format, options); 37 | } 38 | 39 | #endregion 40 | 41 | #region Absolute Directory Parsing 42 | 43 | /// 44 | /// Parses an absolute directory path using the specified options and the current platform's format. 45 | /// 46 | /// An absolute directory path. 47 | /// Specifies the path parsing options. 48 | public static IAbsoluteDirectoryPath ParseAbsolute(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 49 | { 50 | return ParseAbsolute(path, PathFormat.Current, options); 51 | } 52 | 53 | /// 54 | /// Parses an absolute directory path using the specified format and options. 55 | /// 56 | /// An absolute directory path. 57 | /// The path's format. 58 | /// Specifies the path parsing options. 59 | public static IAbsoluteDirectoryPath ParseAbsolute(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) 60 | { 61 | path = format.NormalizeSeparators(path); 62 | string finalPath = format.NormalizeAbsolutePath(path, options, false, out int rootLength); 63 | return new IAbsoluteDirectoryPath.Impl(finalPath, rootLength, format); 64 | } 65 | 66 | #endregion 67 | 68 | #region Relative Directory Parsing 69 | 70 | /// 71 | /// Parses a relative directory path using the specified options and the current platform's format. 72 | /// 73 | /// A relative directory path. 74 | /// Specifies the path parsing options. 75 | public static IRelativeDirectoryPath ParseRelative(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 76 | { 77 | return ParseRelative(path, PathFormat.Current, options); 78 | } 79 | 80 | /// 81 | /// Parses a relative directory path using the specified format and options. 82 | /// 83 | /// A relative directory path. 84 | /// The path's format. 85 | /// Specifies the path parsing options. 86 | public static IRelativeDirectoryPath ParseRelative(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) 87 | { 88 | path = format.NormalizeSeparators(path); 89 | string finalPath = format.NormalizeRelativePath(path, options, false, out int rootLength); 90 | return new IRelativeDirectoryPath.Impl(finalPath, rootLength, format); 91 | } 92 | 93 | #endregion 94 | 95 | #region Special Directories 96 | 97 | /// 98 | /// Gets the directory path of the specified assembly. 99 | /// 100 | public static IAbsoluteDirectoryPath GetAssemblyLocation(Assembly assembly) => FilePath.GetAssemblyLocation(assembly).ParentDirectory; 101 | 102 | /// 103 | /// Gets the current working directory. 104 | /// 105 | public static IAbsoluteDirectoryPath GetCurrent() => ParseAbsolute(Directory.GetCurrentDirectory(), PathOptions.None); 106 | 107 | /// 108 | /// Sets the current working directory. 109 | /// 110 | public static void SetCurrent(IAbsoluteDirectoryPath dir) 111 | { 112 | dir.PathFormat.EnsureCurrent(nameof(dir)); 113 | Directory.SetCurrentDirectory(dir.PathExport); 114 | } 115 | 116 | /// 117 | /// Returns the current user's temporary directory. 118 | /// 119 | public static IAbsoluteDirectoryPath GetTemp() => ParseAbsolute(Path.GetTempPath(), PathOptions.None); 120 | 121 | /// 122 | /// Returns the special system folder directory path that is identified by the provided enumeration. 123 | /// 124 | /// The special system folder to get. 125 | public static IAbsoluteDirectoryPath GetSpecialFolder(Environment.SpecialFolder specialFolder) 126 | { 127 | return ParseAbsolute(Environment.GetFolderPath(specialFolder), PathOptions.None); 128 | } 129 | 130 | /// 131 | /// Gets the list of directory paths that represent mounting points (drives in Windows). 132 | /// 133 | public static IEnumerable GetMountingPoints() 134 | { 135 | foreach (string d in Environment.GetLogicalDrives()) 136 | yield return ParseAbsolute(d, PathOptions.None); 137 | } 138 | 139 | #endregion 140 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/FilePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace Singulink.IO; 6 | 7 | /// 8 | /// Contains methods for parsing file paths and working with special files. 9 | /// 10 | public static class FilePath 11 | { 12 | #region File Parsing 13 | 14 | /// 15 | /// Parses an absolute or relative file path using the specified options and the current platform's format. 16 | /// 17 | /// A file path. 18 | /// Specifies the path parsing options. 19 | public static IFilePath Parse(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 20 | { 21 | return Parse(path, PathFormat.Current, options); 22 | } 23 | 24 | /// 25 | /// Parses an absolute or relative file path using the specified format and options. 26 | /// 27 | /// A file path. 28 | /// The path's format. 29 | /// Specifies the path parsing options. 30 | public static IFilePath Parse(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) 31 | { 32 | if (format.GetPathKind(path) == PathKind.Absolute) 33 | return ParseAbsolute(path, format, options); 34 | 35 | return ParseRelative(path, format, options); 36 | } 37 | 38 | #endregion 39 | 40 | #region Absolute File Parsing 41 | 42 | /// 43 | /// Parses an absolute file path using the specified options and the current platform's format. 44 | /// 45 | /// An absolute file path. 46 | /// Specifies the path parsing options. 47 | public static IAbsoluteFilePath ParseAbsolute(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 48 | { 49 | return ParseAbsolute(path, PathFormat.Current, options); 50 | } 51 | 52 | /// 53 | /// Parses an absolute file path using the specified format and options. 54 | /// 55 | /// An absolute file path. 56 | /// The path's format. 57 | /// Specifies the path parsing options. 58 | public static IAbsoluteFilePath ParseAbsolute(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) 59 | { 60 | path = format.NormalizeSeparators(path); 61 | string finalPath = format.NormalizeAbsolutePath(path, options, true, out int rootLength); 62 | 63 | if (path.EndsWith(format.SeparatorString, StringComparison.Ordinal) || rootLength == finalPath.Length) 64 | throw new ArgumentException("No file name in path.", nameof(path)); 65 | 66 | return new IAbsoluteFilePath.Impl(finalPath, rootLength, format); 67 | } 68 | 69 | #endregion 70 | 71 | #region Relative Parsing 72 | 73 | /// 74 | /// Parses a relative file path using the specified options and the current platform's format. 75 | /// 76 | /// A relative file path. 77 | /// Specifies the path parsing options. 78 | public static IRelativeFilePath ParseRelative(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 79 | { 80 | return ParseRelative(path, PathFormat.Current, options); 81 | } 82 | 83 | /// 84 | /// Parses a relative file path using the specified format and options. 85 | /// 86 | /// A relative file path. 87 | /// The path's format. 88 | /// Specifies the path parsing options. 89 | public static IRelativeFilePath ParseRelative(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) 90 | { 91 | path = format.NormalizeSeparators(path); 92 | 93 | if (path.Length == 0 || path.EndsWith(format.SeparatorString, StringComparison.Ordinal)) 94 | throw new ArgumentException("No file name in path.", nameof(path)); 95 | 96 | string finalPath = format.NormalizeRelativePath(path, options, true, out int rootLength); 97 | return new IRelativeFilePath.Impl(finalPath, rootLength, format); 98 | } 99 | 100 | #endregion 101 | 102 | #region Special Files 103 | 104 | /// 105 | /// Gets the file path to the specified assembly. 106 | /// 107 | public static IAbsoluteFilePath GetAssemblyLocation(Assembly assembly) 108 | { 109 | string location = assembly.Location; 110 | 111 | if (string.IsNullOrEmpty(location)) 112 | throw new InvalidOperationException("Assembly does not have a location."); 113 | 114 | return ParseAbsolute(location, PathOptions.None); 115 | } 116 | 117 | /// 118 | /// Creates a new uniquely named zero-byte temporary file. 119 | /// 120 | /// The path to the newly created file. 121 | public static IAbsoluteFilePath CreateTempFile() => ParseAbsolute(Path.GetTempFileName(), PathOptions.None); 122 | 123 | #endregion 124 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Determines whether outputs (return values and out/ref parameters) have null checks injected. 12 | 13 | 14 | 15 | 16 | Determines whether non-public members have null checks injected or if only public entry points are checked. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 25 | 26 | 27 | 28 | 29 | A comma-separated list of error codes that can be safely ignored in assembly verification. 30 | 31 | 32 | 33 | 34 | 'false' to turn off automatic generation of the XML Schema file. 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IAbsoluteFilePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace Singulink.IO; 6 | 7 | /// 8 | /// Represents an absolute path to a file. 9 | /// 10 | public partial interface IAbsoluteFilePath : IAbsolutePath, IFilePath 11 | { 12 | /// 13 | /// Gets the file's parent directory. 14 | /// 15 | new IAbsoluteDirectoryPath ParentDirectory { get; } 16 | 17 | /// 18 | /// Gets or sets a value indicating whether the file is read-only. 19 | /// 20 | bool IsReadOnly { get; set; } 21 | 22 | /// 23 | /// Gets the size of the file in bytes. 24 | /// 25 | long Length { get; } 26 | 27 | #region Path Manipulation 28 | 29 | /// 30 | new IAbsoluteFilePath WithExtension(string? newExtension, PathOptions options = PathOptions.NoUnfriendlyNames); 31 | 32 | /// 33 | IFilePath IFilePath.WithExtension(string? newExtension, PathOptions options) => WithExtension(newExtension, options); 34 | 35 | #endregion 36 | 37 | #region File System Operations 38 | 39 | /// 40 | /// Opens a file stream to a new or existing file. 41 | /// 42 | /// A constant specifying the mode (for example, Open or Append) in which to open the file. 43 | /// A constant specifying whether to open the file with Read, Write, or ReadWrite 44 | /// file access. 45 | /// A constant specifying the type of access other FileStream objects have to this file. 46 | /// A positive value indicating the buffer size. 47 | /// Additional file options. 48 | /// A new to the opened file. 49 | FileStream OpenStream(FileMode mode = FileMode.Open, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.None, int bufferSize = 4096, FileOptions options = FileOptions.None); 50 | 51 | /// 52 | /// Opens an asynchronous file stream to a new or existing file (the option is always appended). 53 | /// 54 | /// A constant specifying the mode (for example, Open or Append) in which to open the file. 55 | /// A constant specifying whether to open the file with Read, Write, or ReadWrite 56 | /// file access. 57 | /// A constant specifying the type of access other FileStream objects have to this file. 58 | /// A positive value indicating the buffer size. 59 | /// Additional file options. 60 | /// A new to the opened file. 61 | /// 62 | /// Note that the underlying operating system might not support asynchronous I/O, so the handle might be opened synchronously depending on the 63 | /// platform. 64 | /// 65 | sealed FileStream OpenAsyncStream(FileMode mode = FileMode.Open, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.None, int bufferSize = 4096, FileOptions options = FileOptions.None) 66 | { 67 | return OpenStream(mode, access, share, bufferSize, options | FileOptions.Asynchronous); 68 | } 69 | 70 | /// 71 | /// Opens an asynchronous file stream to a new or existing file (the option is always appended). 72 | /// 73 | /// A constant specifying the mode (for example, Open or Append) in which to open the file. 74 | /// A constant specifying whether to open the file with Read, Write, or ReadWrite 75 | /// file access. 76 | /// A constant specifying the type of access other FileStream objects have to this file. 77 | /// A positive value indicating the buffer size. 78 | /// Additional file options. 79 | /// A task with a new to the opened file. 80 | /// 81 | /// Note that the underlying operating system might not support asynchronous I/O, so the handle might be opened synchronously depending on the 82 | /// platform. 83 | /// 84 | sealed Task OpenStreamAsync(FileMode mode = FileMode.Open, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.None, int bufferSize = 4096, FileOptions options = FileOptions.None) 85 | { 86 | return Task.Run(() => OpenAsyncStream(mode, access, share, bufferSize, options)); 87 | } 88 | 89 | /// 90 | /// Copies the file to a new file, optionally allowing the overwriting of an existing file. 91 | /// 92 | /// The new file to copy to. 93 | /// True to allow an existing file to be overwritten, otherwise false. 94 | void CopyTo(IAbsoluteFilePath destinationFile, bool overwrite = false); 95 | 96 | /// 97 | /// Moves the file to a new location, optionally allowing the overwriting of an existing file. Overwriting is only supported on .NET Core 3+ only, 98 | /// other runtimes (i.e. Mono/Xamarin) will throw ). 99 | /// 100 | /// The new location for the file. 101 | /// True to allow an existing file to be overwritten, otherwise false. 102 | void MoveTo(IAbsoluteFilePath destinationFile, bool overwrite = false); 103 | 104 | /// 105 | /// Replaces the contents of a file with the current file, deleting the original file and creating a backup of the replaced file. 106 | /// 107 | /// The file to replace. 108 | /// The location to backup the file described by the parameter. 109 | /// True to ignore merge errors (such as attributes and ACLs) from the replaced file to the replacement file; 110 | /// otherwise false. 111 | void Replace(IAbsoluteFilePath destinationFile, IAbsoluteFilePath backupFile, bool ignoreMetadataErrors = false); 112 | 113 | /// 114 | /// Deletes the file. 115 | /// 116 | void Delete(); 117 | 118 | #endregion 119 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IAbsolutePath.Impl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Singulink.IO; 5 | 6 | /// 7 | /// Contains an implementation of IAbsoluteEntryPath. 8 | /// 9 | public partial interface IAbsolutePath 10 | { 11 | internal new abstract class Impl : IPath.Impl, IAbsolutePath 12 | { 13 | protected Impl(string path, int rootLength, PathFormat pathFormat) : base(path, rootLength, pathFormat) 14 | { 15 | } 16 | 17 | public string PathExport => PathFormat.GetAbsolutePathExportString(PathDisplay); 18 | 19 | public bool IsUnc => PathFormat.IsUncPath(PathDisplay); 20 | 21 | public abstract bool Exists { get; } 22 | 23 | public abstract FileAttributes Attributes { get; set; } 24 | 25 | public IAbsoluteDirectoryPath RootDirectory { 26 | get { 27 | if (PathDisplay.Length == RootLength && this is IAbsoluteDirectoryPath dir) 28 | return dir; 29 | 30 | return new IAbsoluteDirectoryPath.Impl(PathDisplay[..RootLength], RootLength, PathFormat); 31 | } 32 | } 33 | 34 | public abstract IAbsoluteDirectoryPath? ParentDirectory { get; } 35 | 36 | public DateTime CreationTime { 37 | get { 38 | EnsureExists(); 39 | return File.GetCreationTime(PathExport); 40 | } 41 | set { 42 | EnsureExists(); 43 | File.SetCreationTime(PathExport, value); 44 | } 45 | } 46 | 47 | public DateTime LastAccessTime { 48 | get { 49 | EnsureExists(); 50 | return File.GetLastAccessTime(PathExport); 51 | } 52 | set { 53 | EnsureExists(); 54 | File.SetLastAccessTime(PathExport, value); 55 | } 56 | } 57 | 58 | public DateTime LastWriteTime { 59 | get { 60 | EnsureExists(); 61 | return File.GetLastWriteTime(PathExport); 62 | } 63 | set { 64 | EnsureExists(); 65 | File.SetLastWriteTime(PathExport, value); 66 | } 67 | } 68 | 69 | public abstract IAbsoluteDirectoryPath GetLastExistingDirectory(); 70 | 71 | /// 72 | /// This method is necessary before calling some operations because they work on both files and directories. 73 | /// 74 | /// 75 | /// Examples of problematic methods: File.GetLastWriteTime / Directory.GetLastWriteTime (plus all other timestamp methods). 76 | /// 77 | internal void EnsureExists() => _ = Attributes; 78 | } 79 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IAbsolutePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | 5 | namespace Singulink.IO; 6 | 7 | /// 8 | /// Represents an absolute path to a file or directory. 9 | /// 10 | [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Properties need to be overriden by implementing types")] 11 | public partial interface IAbsolutePath : IPath 12 | { 13 | /// 14 | /// Gets a path string that is specially formatted for reliably accessing this path through file system calls. 15 | /// 16 | /// 17 | /// This is the value that should always be used when a path string is needed for passing into file system calls (i.e. opening file streams). 18 | /// 19 | string PathExport { get; } 20 | 21 | /// 22 | /// Gets a value indicating whether this path is a UNC path. This can only ever return true for paths that use the 23 | /// path format. 24 | /// 25 | bool IsUnc { get; } 26 | 27 | /// 28 | /// Gets a value indicating whether the file/directory exists. 29 | /// 30 | bool Exists { get; } 31 | 32 | /// 33 | /// Gets or sets the file/directory attributes. 34 | /// 35 | FileAttributes Attributes { get; set; } 36 | 37 | /// 38 | /// Gets or sets the file/directory's creation time as a local time. 39 | /// 40 | DateTime CreationTime { get; set; } 41 | 42 | /// 43 | /// Gets or sets the file/directory's last access time as a local time. 44 | /// 45 | DateTime LastAccessTime { get; set; } 46 | 47 | /// 48 | /// Gets or sets the file/directory's last write time as a local time. 49 | /// 50 | DateTime LastWriteTime { get; set; } 51 | 52 | /// 53 | /// Gets the root directory of this file/directory. 54 | /// 55 | IAbsoluteDirectoryPath RootDirectory { get; } 56 | 57 | /// 58 | new IAbsoluteDirectoryPath? ParentDirectory { get; } 59 | 60 | /// 61 | IDirectoryPath? IPath.ParentDirectory => ParentDirectory; 62 | 63 | /// 64 | /// Gets the last directory in the path that exists. 65 | /// 66 | IAbsoluteDirectoryPath GetLastExistingDirectory(); 67 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IDirectoryPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO; 4 | 5 | /// 6 | /// Represents an absolute or relative path to a directory. 7 | /// 8 | public interface IDirectoryPath : IPath 9 | { 10 | /// 11 | /// Combines a directory with a relative directory. 12 | /// 13 | public static IDirectoryPath operator +(IDirectoryPath x, IRelativeDirectoryPath y) => x.Combine(y); 14 | 15 | /// 16 | /// Combines a directory with a relative file. 17 | /// 18 | public static IFilePath operator +(IDirectoryPath x, IRelativeFilePath y) => x.Combine(y); 19 | 20 | /// 21 | /// Combines a directory with a relative entry. 22 | /// 23 | public static IPath operator +(IDirectoryPath x, IRelativePath y) => x.Combine(y); 24 | 25 | #region Combining 26 | 27 | // Directory 28 | 29 | /// 30 | /// Combines this directory with a relative directory. 31 | /// 32 | /// The relative directory to apprend to this directory. 33 | IDirectoryPath Combine(IRelativeDirectoryPath path); 34 | 35 | /// 36 | /// Combines this directory with a relative directory path parsed using the specified options and this directory's path format. 37 | /// 38 | /// The relative directory path to append to this directory. 39 | /// The options to use for parsing the appended relative directory path. 40 | sealed IDirectoryPath CombineDirectory(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 41 | { 42 | return CombineDirectory(path, PathFormat, options); 43 | } 44 | 45 | /// 46 | /// Combines this directory with a relative directory path parsed using the specified format and options. 47 | /// 48 | /// The relative directory path to append to this directory. 49 | /// The appended relative directory path's format. 50 | /// The options to use for parsing the appended relative directory path. 51 | IDirectoryPath CombineDirectory(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames); 52 | 53 | // File 54 | 55 | /// 56 | /// Combines this directory with a relative file. 57 | /// 58 | /// The relative file to apprend to this directory. 59 | IFilePath Combine(IRelativeFilePath path); 60 | 61 | /// 62 | /// Combines this directory with a relative file path parsed using the specified options and this directory's path format. 63 | /// 64 | /// The relative file path to append to this directory. 65 | /// The options to use for parsing the appended relative file path. 66 | sealed IFilePath CombineFile(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) => CombineFile(path, PathFormat, options); 67 | 68 | /// 69 | /// Combines this directory with a relative file path parsed using the specified format and options. 70 | /// 71 | /// The relative file path to append to this directory. 72 | /// The appended relative file path's format. 73 | /// The options to use for parsing the appended relative file path. 74 | IFilePath CombineFile(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames); 75 | 76 | // Entry 77 | 78 | /// 79 | /// Combines this directory with a relative entry. 80 | /// 81 | /// The relative entry to apprend to this directory. 82 | IPath Combine(IRelativePath path); 83 | 84 | #endregion 85 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IFilePath.cs: -------------------------------------------------------------------------------- 1 | namespace Singulink.IO; 2 | 3 | /// 4 | /// Represents an absolute or relative path to a file. 5 | /// 6 | public interface IFilePath : IPath 7 | { 8 | /// 9 | /// Gets the file name without the extension. 10 | /// 11 | /// 12 | /// The dot in the extension is also removed from the name. File names with no extension are returned without changes. File names with trailing dots 13 | /// will have the dot removed. 14 | /// 15 | string NameWithoutExtension { get; } 16 | 17 | /// 18 | /// Gets the file extension including the leading dot, otherwise an empty string. 19 | /// 20 | /// Files names with trailing dots will return an extension which is just a dot. 21 | string Extension { get; } 22 | 23 | /// 24 | bool IPath.HasParentDirectory => true; // All files have parent directories. 25 | 26 | #region Path Manipulation 27 | 28 | /// 29 | /// Adds a new extension or changes the existing extension of the file. 30 | /// 31 | /// The new extension that should be applied to the file. 32 | /// The options to apply when parsing the new file name. The rest of the path is not reparsed. 33 | IFilePath WithExtension(string? newExtension, PathOptions options = PathOptions.NoUnfriendlyNames); 34 | 35 | #endregion 36 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IPath.Impl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO; 4 | 5 | /// 6 | /// Contains an implementation of IPath. 7 | /// 8 | public partial interface IPath 9 | { 10 | internal abstract class Impl : IPath 11 | { 12 | protected Impl(string pathDisplay, int rootLength, PathFormat pathFormat) 13 | { 14 | PathDisplay = pathDisplay; 15 | RootLength = rootLength; 16 | PathFormat = pathFormat; 17 | } 18 | 19 | public string PathDisplay { get; } 20 | 21 | public int RootLength { get; } 22 | 23 | public PathFormat PathFormat { get; } 24 | 25 | public string Name => PathFormat.GetEntryName(PathDisplay, RootLength); 26 | 27 | public bool IsRooted => PathFormat.GetPathKind(PathDisplay) != PathKind.Relative; 28 | 29 | int IPath.RootLength => RootLength; 30 | 31 | #region Equality 32 | 33 | public bool Equals(IPath? other) 34 | { 35 | if (other == null) 36 | return false; 37 | 38 | return (this is IFilePath) == (other is IFilePath) && 39 | PathFormat == other.PathFormat && 40 | PathDisplay.AsSpan(0, RootLength).Equals(other.PathDisplay.AsSpan(0, other.RootLength), StringComparison.OrdinalIgnoreCase) && 41 | PathDisplay.AsSpan(RootLength).Equals(other.PathDisplay.AsSpan(other.RootLength), StringComparison.Ordinal); 42 | } 43 | 44 | public override bool Equals(object? obj) => Equals(obj as IPath); 45 | 46 | // TODO: Combine case-insensitive root with case-sensitive remainder hash codes to avoid hash collisions with different case paths when new 47 | // ReadOnlySpan StringComparer APIs become available: https://github.com/dotnet/runtime/issues/27229 48 | public override int GetHashCode() => PathDisplay.GetHashCode(StringComparison.OrdinalIgnoreCase); 49 | 50 | #endregion 51 | 52 | #region String Formatting 53 | 54 | public override string ToString() 55 | { 56 | // Intentionally thwart users from using ToString() to get a usable path, rather force them to consider whether PathExport or PathDisplay is 57 | // more suitable. 58 | return (this is IFilePath ? "[File] " : "[Directory] ") + PathDisplay; 59 | } 60 | 61 | #endregion 62 | } 63 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Singulink.IO; 5 | 6 | /// 7 | /// Represents an absolute or relative path to a file or directory. 8 | /// 9 | [SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Properties need to be overriden by implementing types")] 10 | public partial interface IPath : IEquatable 11 | { 12 | /// 13 | /// Gets the name of the file or directory that this path refers to. 14 | /// 15 | string Name { get; } 16 | 17 | /// 18 | /// Gets a path string suitable for user friendly display or serialization. Do not use this value to access the file system. 19 | /// 20 | /// 21 | /// The value returned by this property is ideal for display to the user. Parsing this value with the appropriate parse method that matches the 22 | /// actual type of this path will recreate an identical path object. If you need a string path parameter in order to perform IO operations (i.e. 23 | /// opening a file stream) you should obtain an absolute path and use the property value instead as it is 24 | /// specifically formatted to ensure the path is correctly parsed by the underlying file system. 25 | /// 26 | string PathDisplay { get; } 27 | 28 | /// 29 | /// Gets the length of the path that comprises the root. 30 | /// 31 | internal int RootLength { get; } 32 | 33 | /// 34 | /// Gets the format of this path. 35 | /// 36 | PathFormat PathFormat { get; } 37 | 38 | /// 39 | /// Gets the parent directory of this file/directory. 40 | /// 41 | IDirectoryPath? ParentDirectory => throw new NotImplementedException(); 42 | 43 | /// 44 | /// Gets a value indicating whether this path has a parent directory. 45 | /// 46 | bool HasParentDirectory => throw new NotImplementedException(); 47 | 48 | /// 49 | /// Gets a value indicating whether this path is rooted. Relative paths can be rooted and absolute paths are always rooted. 50 | /// 51 | /// 52 | /// A rooted relative path starts with the path separator. 53 | /// 54 | bool IsRooted { get; } 55 | 56 | /// 57 | /// Gets a value indicating whether this is an absolute path. 58 | /// 59 | sealed bool IsAbsolute => this is IAbsolutePath; 60 | 61 | /// 62 | /// Gets a value indicating whether this is a relative path. 63 | /// 64 | sealed bool IsRelative => this is IRelativePath; 65 | 66 | /// 67 | /// Gets a value indicating whether this is a directory path. 68 | /// 69 | sealed bool IsDirectory => this is IDirectoryPath; 70 | 71 | /// 72 | /// Gets a value indicating whether this is a file path. 73 | /// 74 | sealed bool IsFile => this is IFilePath; 75 | 76 | /// 77 | /// Determines whether this file/directory is equal to another file/directory. 78 | /// 79 | /// 80 | /// The items being compared must be the same type and have matching path formats and character casing (aside from the drive letter or UNC name, 81 | /// if applicable) in order to be considered equal. 82 | /// 83 | new bool Equals(IPath? other); 84 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IRelativeDirectoryPath.Impl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Singulink.IO.Utilities; 3 | 4 | namespace Singulink.IO; 5 | 6 | /// 7 | /// Contains an implementation of IRelativeDirectoryPath. 8 | /// 9 | public partial interface IRelativeDirectoryPath 10 | { 11 | internal new sealed class Impl : IRelativePath.Impl, IRelativeDirectoryPath 12 | { 13 | internal Impl(string path, int rootLength, PathFormat pathFormat) : base(path, rootLength, pathFormat) 14 | { 15 | } 16 | 17 | public IRelativeDirectoryPath? ParentDirectory { 18 | get { 19 | if (!HasParentDirectory) 20 | return null; 21 | 22 | string parentPath; 23 | 24 | if (!IsRooted && PathFormat.GetEntryName(PathDisplay, 0).Length == 0) 25 | parentPath = PathDisplay.Length == 0 ? ".." : StringHelper.Concat(PathDisplay, PathFormat.SeparatorString, ".."); 26 | else 27 | parentPath = PathFormat.GetPreviousDirectory(PathDisplay, RootLength).ToString(); 28 | 29 | return new Impl(parentPath, RootLength, PathFormat); 30 | } 31 | } 32 | 33 | IRelativeDirectoryPath? IRelativePath.ParentDirectory => ParentDirectory; 34 | 35 | public bool HasParentDirectory => !IsRooted || PathDisplay.Length > RootLength; 36 | 37 | bool IPath.HasParentDirectory => HasParentDirectory; 38 | 39 | #region Combining 40 | 41 | public IRelativeDirectoryPath Combine(IRelativeDirectoryPath dir) => (IRelativeDirectoryPath)Combine(dir, nameof(dir), nameof(dir)); 42 | 43 | public IRelativeDirectoryPath CombineDirectory(ReadOnlySpan path, PathFormat format, PathOptions options) 44 | { 45 | return (IRelativeDirectoryPath)Combine(DirectoryPath.ParseRelative(path, format, options), nameof(path), nameof(format)); 46 | } 47 | 48 | public IRelativeFilePath Combine(IRelativeFilePath file) => (IRelativeFilePath)Combine((IRelativePath)file); 49 | 50 | public IRelativeFilePath CombineFile(ReadOnlySpan path, PathFormat format, PathOptions options) 51 | { 52 | return (IRelativeFilePath)Combine(FilePath.ParseRelative(path, format, options), nameof(path), nameof(format)); 53 | } 54 | 55 | public IRelativePath Combine(IRelativePath entry) => Combine(entry, nameof(entry), nameof(entry)); 56 | 57 | private IRelativePath Combine(IRelativePath entry, string? pathParamName, string? formatParamName) 58 | { 59 | var mutualFormat = PathFormat.GetMutualFormat(PathFormat, entry.PathFormat); 60 | 61 | if (mutualFormat == null) 62 | throw new ArgumentException("Cannot combine path formats that are not universal or do not match.", formatParamName); 63 | 64 | if (PathDisplay.Length == 0) 65 | return entry; 66 | 67 | if (entry.PathDisplay.Length == 0) 68 | return this; 69 | 70 | var appendPath = entry.PathFormat.SplitRelativeNavigation(entry.PathDisplay, out int parentDirs); 71 | appendPath = PathFormat.ConvertRelativePathToMutualFormat(appendPath, entry.PathFormat, mutualFormat); 72 | 73 | StringOrSpan basePath = GetBasePathForAppending(parentDirs) ?? 74 | throw new ArgumentException("Invalid path combination: Attempt to navigate past root directory.", pathParamName); 75 | 76 | basePath = PathFormat.ConvertRelativePathToMutualFormat(basePath, PathFormat, mutualFormat); 77 | 78 | string newPath = appendPath.Length > 0 || parentDirs == -1 ? 79 | StringHelper.Concat(basePath, PathFormat.SeparatorString, appendPath) : (string)basePath; 80 | 81 | if (entry.IsDirectory) 82 | return new Impl(newPath, RootLength, PathFormat); 83 | else 84 | return new IRelativeFilePath.Impl(newPath, RootLength, PathFormat); 85 | } 86 | 87 | private string? GetBasePathForAppending(int parentDirs) 88 | { 89 | // TODO: Can be optimized so that parent directory instances are not created. 90 | 91 | if (parentDirs == -1) 92 | return string.Empty; 93 | 94 | IRelativeDirectoryPath currentDir = this; 95 | 96 | for (int i = 0; i < parentDirs; i++) { 97 | currentDir = currentDir.ParentDirectory; 98 | 99 | if (currentDir == null) 100 | return null; 101 | } 102 | 103 | return currentDir.PathDisplay; 104 | } 105 | 106 | #endregion 107 | 108 | #region Path Format Conversion 109 | 110 | public IRelativeDirectoryPath ToPathFormat(PathFormat format, PathOptions options) 111 | { 112 | var path = PathFormat.ConvertRelativePathFormat(PathDisplay, PathFormat, format); 113 | return DirectoryPath.ParseRelative(path.Span, format, options); 114 | } 115 | 116 | #endregion 117 | } 118 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IRelativeDirectoryPath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO; 4 | 5 | /// 6 | /// Represents a relative path to a directory. 7 | /// 8 | public partial interface IRelativeDirectoryPath : IRelativePath, IDirectoryPath 9 | { 10 | /// 11 | /// Combines a relative directory with another relative directory. 12 | /// 13 | public static IRelativeDirectoryPath operator +(IRelativeDirectoryPath x, IRelativeDirectoryPath y) => x.Combine(y); 14 | 15 | /// 16 | /// Combines a relative directory with another relative file. 17 | /// 18 | public static IRelativeFilePath operator +(IRelativeDirectoryPath x, IRelativeFilePath y) => x.Combine(y); 19 | 20 | /// 21 | /// Combines a relative directory with another relative entry. 22 | /// 23 | public static IRelativePath operator +(IRelativeDirectoryPath x, IRelativePath y) => x.Combine(y); 24 | 25 | #region Combining 26 | 27 | // Directory 28 | 29 | /// 30 | new IRelativeDirectoryPath Combine(IRelativeDirectoryPath path); 31 | 32 | /// 33 | sealed new IRelativeDirectoryPath CombineDirectory(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 34 | { 35 | return CombineDirectory(path, PathFormat, options); 36 | } 37 | 38 | /// 39 | new IRelativeDirectoryPath CombineDirectory(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames); 40 | 41 | // File 42 | 43 | /// 44 | new IRelativeFilePath Combine(IRelativeFilePath path); 45 | 46 | /// 47 | sealed new IRelativeFilePath CombineFile(ReadOnlySpan path, PathOptions options = PathOptions.NoUnfriendlyNames) 48 | { 49 | return CombineFile(path, PathFormat, options); 50 | } 51 | 52 | /// 53 | new IRelativeFilePath CombineFile(ReadOnlySpan path, PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames); 54 | 55 | // Entry 56 | 57 | /// 58 | new IRelativePath Combine(IRelativePath path); 59 | 60 | // Explicit base implementations 61 | 62 | /// 63 | IDirectoryPath IDirectoryPath.Combine(IRelativeDirectoryPath path) => Combine(path); 64 | 65 | /// 66 | IDirectoryPath IDirectoryPath.CombineDirectory(ReadOnlySpan path, PathFormat format, PathOptions options) => CombineDirectory(path, format, options); 67 | 68 | /// 69 | IFilePath IDirectoryPath.Combine(IRelativeFilePath path) => Combine(path); 70 | 71 | /// 72 | IFilePath IDirectoryPath.CombineFile(ReadOnlySpan path, PathFormat format, PathOptions options) => CombineFile(path, format, options); 73 | 74 | /// 75 | IPath IDirectoryPath.Combine(IRelativePath path) => Combine(path); 76 | 77 | #endregion 78 | 79 | #region Path Format Conversion 80 | 81 | /// 82 | new IRelativeDirectoryPath ToPathFormat(PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames); 83 | 84 | /// 85 | IRelativePath IRelativePath.ToPathFormat(PathFormat format, PathOptions options) => ToPathFormat(format, options); 86 | 87 | #endregion 88 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IRelativeFilePath.Impl.cs: -------------------------------------------------------------------------------- 1 | namespace Singulink.IO; 2 | 3 | /// 4 | /// Contains an implementation of IRelativeFilePath. 5 | /// 6 | public partial interface IRelativeFilePath 7 | { 8 | internal new sealed class Impl : IRelativePath.Impl, IRelativeFilePath 9 | { 10 | internal Impl(string path, int rootLength, PathFormat pathFormat) : base(path, rootLength, pathFormat) 11 | { 12 | } 13 | 14 | public string NameWithoutExtension => PathFormat.GetFileNameWithoutExtension(PathDisplay); 15 | 16 | public string Extension => PathFormat.GetFileNameExtension(PathDisplay); 17 | 18 | public IRelativeDirectoryPath ParentDirectory { 19 | get { 20 | var parentPath = PathFormat.GetPreviousDirectory(PathDisplay, RootLength); 21 | return new IRelativeDirectoryPath.Impl(parentPath.ToString(), RootLength, PathFormat); 22 | } 23 | } 24 | 25 | public IRelativeFilePath WithExtension(string? newExtension, PathOptions options) 26 | { 27 | string newPath = PathFormat.ChangeFileNameExtension(PathDisplay, newExtension, options); 28 | return new Impl(newPath, RootLength, PathFormat); 29 | } 30 | 31 | #region Path Format Conversion 32 | 33 | public IRelativeFilePath ToPathFormat(PathFormat format, PathOptions options) 34 | { 35 | var path = PathFormat.ConvertRelativePathFormat(PathDisplay, PathFormat, format); 36 | return FilePath.ParseRelative(path.Span, format, options); 37 | } 38 | 39 | #endregion 40 | } 41 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IRelativeFilePath.cs: -------------------------------------------------------------------------------- 1 | namespace Singulink.IO; 2 | 3 | /// 4 | /// Represents a relative path to a file. 5 | /// 6 | public partial interface IRelativeFilePath : IRelativePath, IFilePath 7 | { 8 | /// 9 | /// Gets the file's parent directory. 10 | /// 11 | new IRelativeDirectoryPath ParentDirectory { get; } 12 | 13 | /// 14 | IRelativeDirectoryPath? IRelativePath.ParentDirectory => ParentDirectory; 15 | 16 | #region Path Manipulation 17 | 18 | /// 19 | new IRelativeFilePath WithExtension(string? newExtension, PathOptions options = PathOptions.NoUnfriendlyNames); 20 | 21 | /// 22 | IFilePath IFilePath.WithExtension(string? newExtension, PathOptions options) => WithExtension(newExtension, options); 23 | 24 | #endregion 25 | 26 | #region Path Format Conversion 27 | 28 | /// 29 | new IRelativeFilePath ToPathFormat(PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames); 30 | 31 | /// 32 | IRelativePath IRelativePath.ToPathFormat(PathFormat format, PathOptions options) => ToPathFormat(format, options); 33 | 34 | #endregion 35 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IRelativePath.Impl.cs: -------------------------------------------------------------------------------- 1 | namespace Singulink.IO; 2 | 3 | /// 4 | /// Contains an implementation of IRelativeEntryPath. 5 | /// 6 | public partial interface IRelativePath 7 | { 8 | internal new abstract class Impl : IPath.Impl, IRelativePath 9 | { 10 | protected Impl(string path, int rootLength, PathFormat pathFormat) : base(path, rootLength, pathFormat) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/IRelativePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO; 4 | 5 | /// 6 | /// Represents a relative path to a file or directory. 7 | /// 8 | public partial interface IRelativePath : IPath 9 | { 10 | /// 11 | new IRelativeDirectoryPath? ParentDirectory => null; // Override higher up. 12 | 13 | /// 14 | IDirectoryPath? IPath.ParentDirectory => ParentDirectory; 15 | 16 | #region Path Format Conversion 17 | 18 | /// 19 | /// Converts the path to use a different format. 20 | /// 21 | /// The format that the path should be converted to. 22 | /// The options to use when parsing the new path. 23 | IRelativePath ToPathFormat(PathFormat format, PathOptions options = PathOptions.NoUnfriendlyNames) => throw new NotImplementedException(); 24 | 25 | #endregion 26 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Interop+Windows.DiskSpace.cs: -------------------------------------------------------------------------------- 1 | namespace Singulink.IO; 2 | 3 | internal static partial class Interop 4 | { 5 | internal static partial class Windows 6 | { 7 | public static void GetSpace(IAbsoluteDirectoryPath.Impl path, out long availableBytes, out long totalBytes, out long freeBytes) 8 | { 9 | using (MediaInsertionPromptGuard.Enter()) { 10 | if (!WindowsNative.GetDiskFreeSpaceEx(path.PathExport, out availableBytes, out totalBytes, out freeBytes)) 11 | throw GetLastWin32ErrorException(path); 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Interop+Windows.DriveType.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Singulink.IO; 4 | 5 | internal static partial class Interop 6 | { 7 | internal static partial class Windows 8 | { 9 | public static DriveType GetDriveType(IAbsoluteDirectoryPath.Impl path) => WindowsNative.GetDriveType(path.PathExportWithTrailingSeparator); 10 | } 11 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Interop+Windows.FileSystem.cs: -------------------------------------------------------------------------------- 1 | namespace Singulink.IO; 2 | 3 | internal static partial class Interop 4 | { 5 | internal static partial class Windows 6 | { 7 | public static unsafe string GetFileSystem(IAbsoluteDirectoryPath.Impl rootDir) 8 | { 9 | // rootDir must be a symlink or root drive 10 | 11 | const int MAX_LENGTH = 261; // MAX_PATH + 1 12 | 13 | char* fileSystemName = stackalloc char[MAX_LENGTH]; 14 | 15 | using (MediaInsertionPromptGuard.Enter()) { 16 | if (!WindowsNative.GetVolumeInformation(rootDir.PathExportWithTrailingSeparator, null, 0, null, null, out int fileSystemFlags, fileSystemName, MAX_LENGTH)) { 17 | throw GetLastWin32ErrorException(rootDir); 18 | } 19 | } 20 | 21 | return new string(fileSystemName); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Interop+Windows.LastError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace Singulink.IO; 8 | 9 | internal static partial class Interop 10 | { 11 | internal static partial class Windows 12 | { 13 | public static Exception GetLastWin32ErrorException(IAbsoluteDirectoryPath.Impl path) 14 | { 15 | int error = Marshal.GetLastWin32Error(); 16 | 17 | Debug.Assert(error != 0, "no error"); 18 | 19 | var win32Ex = new Win32Exception(error); 20 | string message = $"{win32Ex.Message} Path: '{path.PathDisplay}'."; 21 | 22 | switch (error) 23 | { 24 | case WindowsNative.Errors.FILE_NOT_FOUND: 25 | case WindowsNative.Errors.PATH_NOT_FOUND: 26 | case WindowsNative.Errors.INVALID_DRIVE: 27 | return new DirectoryNotFoundException(message, win32Ex); 28 | case WindowsNative.Errors.ACCESS_DENIED: 29 | return new UnauthorizedIOAccessException(message, win32Ex); 30 | case WindowsNative.Errors.FILENAME_EXCED_RANGE: 31 | return new PathTooLongException(message, win32Ex); 32 | default: 33 | if (path.PathFormat == PathFormat.Windows) 34 | path.EnsureExists(); // Throw DirectoryNotFound exception instead of IOException if path is a file. 35 | 36 | return new IOException(message, win32Ex); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Interop+Windows.MediaInsertionPromptGuard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO; 4 | 5 | internal static partial class Interop 6 | { 7 | internal static partial class Windows 8 | { 9 | /// 10 | /// Disposable guard object that safely disables the normal media insertion prompt for removable media (floppies, cds, memory cards, etc.) 11 | /// 12 | /// 13 | /// Note that removable media file systems lazily load. After starting the OS they won't be loaded until you have media in the drive- and as 14 | /// such the prompt won't happen. You have to have had media in at least once to get the file system to load and then have removed it. 15 | /// 16 | internal struct MediaInsertionPromptGuard : IDisposable 17 | { 18 | private bool _disableSuccess; 19 | private uint _oldMode; 20 | 21 | public static MediaInsertionPromptGuard Enter() 22 | { 23 | MediaInsertionPromptGuard prompt = default; 24 | prompt._disableSuccess = WindowsNative.SetThreadErrorMode(WindowsNative.SEM_FAILCRITICALERRORS, out prompt._oldMode); 25 | return prompt; 26 | } 27 | 28 | public void Dispose() 29 | { 30 | if (_disableSuccess) 31 | WindowsNative.SetThreadErrorMode(_oldMode, out _); 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Interop+WindowsNative.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.InteropServices; 3 | 4 | #pragma warning disable SA1310 // Field names should not contain underscore 5 | 6 | namespace Singulink.IO; 7 | 8 | internal static partial class Interop 9 | { 10 | private static class WindowsNative 11 | { 12 | public const uint SEM_FAILCRITICALERRORS = 0x0001; 13 | 14 | [DllImport("kernel32.dll", SetLastError = true)] 15 | public static extern bool SetThreadErrorMode(uint dwNewMode, out uint lpOldMode); 16 | 17 | [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 18 | [return: MarshalAs(UnmanagedType.Bool)] 19 | public static extern bool GetDiskFreeSpaceEx(string lpDirectoryName, out long lpFreeBytesAvailable, out long lpTotalNumberOfBytes, out long lpTotalNumberOfFreeBytes); 20 | 21 | /// 22 | /// A trailing backslash is required. 23 | /// 24 | [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 25 | public static extern DriveType GetDriveType(string lpRootPathName); 26 | 27 | /// 28 | /// A trailing backslash is required. 29 | /// 30 | [DllImport("kernel32.dll", EntryPoint = "GetVolumeInformation", CharSet = CharSet.Unicode, SetLastError = true)] 31 | public static extern unsafe bool GetVolumeInformation(string drive, char* volumeName, int volumeNameBufLen, int* volSerialNumber, int* maxFileNameLen, out int fileSystemFlags, char* fileSystemName, int fileSystemNameBufLen); 32 | 33 | internal static class Errors 34 | { 35 | public const int FILE_NOT_FOUND = 0x2; 36 | public const int PATH_NOT_FOUND = 0x3; 37 | public const int ACCESS_DENIED = 0x5; 38 | public const int INVALID_DRIVE = 0xF; 39 | public const int BAD_NETPATH = 0x35; 40 | public const int BAD_NET_NAME = 0x43; 41 | public const int DIR_NOT_ROOT = 0x90; 42 | public const int FILENAME_EXCED_RANGE = 0xCE; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/PathFormat.Universal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Singulink.IO; 5 | 6 | /// 7 | /// Contains formatting for the universal path format. 8 | /// 9 | public abstract partial class PathFormat 10 | { 11 | private sealed class UniversalPathFormat : PathFormat 12 | { 13 | internal UniversalPathFormat() : base('/') { } 14 | 15 | public override bool SupportsRelativeRootedPaths => false; 16 | 17 | internal override PathKind GetPathKind(ReadOnlySpan path) => PathKind.Relative; 18 | 19 | internal override bool ValidateEntryName(ReadOnlySpan name, PathOptions options, bool allowWildcards, [NotNullWhen(false)] out string? error) 20 | { 21 | if (!Unix.ValidateEntryName(name, options, allowWildcards, out error)) 22 | return false; 23 | 24 | if (!Windows.ValidateEntryName(name, options, allowWildcards, out error)) 25 | return false; 26 | 27 | error = null; 28 | return true; 29 | } 30 | 31 | #region Not Supported 32 | 33 | internal override bool IsUncPath(string path) => throw new NotSupportedException(); 34 | 35 | private protected override ReadOnlySpan SplitAbsoluteRoot(ReadOnlySpan path, out ReadOnlySpan rest) => throw new NotSupportedException(); 36 | 37 | internal override string GetAbsolutePathExportString(string pathDisplay) => throw new NotSupportedException(); 38 | 39 | #endregion 40 | 41 | public override string ToString() => "Universal"; 42 | } 43 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/PathFormat.Unix.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Singulink.IO; 5 | 6 | /// 7 | /// Contains formatting for the unix path format. 8 | /// 9 | public abstract partial class PathFormat 10 | { 11 | private sealed class UnixPathFormat : PathFormat 12 | { 13 | internal UnixPathFormat() : base('/') { } 14 | 15 | public override bool SupportsRelativeRootedPaths => false; 16 | 17 | internal override PathKind GetPathKind(ReadOnlySpan path) 18 | { 19 | return path.Length > 0 && path[0] == SeparatorChar ? PathKind.Absolute : PathKind.Relative; 20 | } 21 | 22 | internal override bool IsUncPath(string path) => false; 23 | 24 | internal override bool ValidateEntryName(ReadOnlySpan name, PathOptions options, bool allowWildcards, [NotNullWhen(false)] out string? error) 25 | { 26 | if (!base.ValidateEntryName(name, options, allowWildcards, out error)) 27 | return false; 28 | 29 | if (name.IndexOfAny('/', (char)0) is int i && i >= 0) { 30 | error = $"Invalid character '{name[i]}' in entry name '{name.ToString()}'."; 31 | return false; 32 | } 33 | 34 | error = null; 35 | return true; 36 | } 37 | 38 | private protected override ReadOnlySpan SplitAbsoluteRoot(ReadOnlySpan path, out ReadOnlySpan rest) 39 | { 40 | var root = path[0..1]; 41 | rest = path[1..]; 42 | return root; 43 | } 44 | 45 | internal override string GetAbsolutePathExportString(string pathDisplay) => pathDisplay; 46 | 47 | public override string ToString() => "Unix"; 48 | } 49 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/PathFormat.Windows.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Globalization; 5 | using Singulink.IO.Utilities; 6 | 7 | namespace Singulink.IO; 8 | 9 | /// 10 | /// Contains formatting for the Windows path format. 11 | /// 12 | public abstract partial class PathFormat 13 | { 14 | private sealed class WindowsPathFormat : PathFormat 15 | { 16 | private static readonly HashSet InvalidNameChars = GetInvalidNameChars(true); 17 | private static readonly HashSet InvalidNameCharsWithoutWildcards = GetInvalidNameChars(false); 18 | 19 | internal WindowsPathFormat() : base('\\') { } 20 | 21 | public override bool SupportsRelativeRootedPaths => true; 22 | 23 | internal override ReadOnlySpan NormalizeSeparators(ReadOnlySpan path) 24 | { 25 | const char AltPathSeparatorChar = '/'; 26 | 27 | int altSeparatorIndex = path.IndexOf(AltPathSeparatorChar); 28 | 29 | if (altSeparatorIndex < 0) 30 | return path; 31 | 32 | char[] normalizedPath = path.ToArray(); 33 | 34 | for (int i = altSeparatorIndex; i < normalizedPath.Length; i++) { 35 | if (normalizedPath[i] == AltPathSeparatorChar) 36 | normalizedPath[i] = SeparatorChar; 37 | } 38 | 39 | return normalizedPath; 40 | } 41 | 42 | internal override PathKind GetPathKind(ReadOnlySpan path) 43 | { 44 | if (path.Length >= 2) { 45 | if (path[1] == ':' || path.StartsWith(@"\\") || path.StartsWith("//")) 46 | return PathKind.Absolute; 47 | } 48 | 49 | return path.Length > 0 && path[0] == SeparatorChar ? PathKind.RelativeRooted : PathKind.Relative; 50 | } 51 | 52 | internal override bool ValidateEntryName(ReadOnlySpan name, PathOptions options, bool allowWildcards, [NotNullWhen(false)] out string? error) 53 | { 54 | if (!base.ValidateEntryName(name, options, allowWildcards, out error)) 55 | return false; 56 | 57 | if (allowWildcards) { 58 | foreach (char c in name) { 59 | if (InvalidNameCharsWithoutWildcards.Contains(c)) { 60 | error = $"Invalid character '{c}' in entry name '{name.ToString()}'. Invalid characters include: < > : \" | / \\"; 61 | return false; 62 | } 63 | } 64 | } 65 | else { 66 | foreach (char c in name) { 67 | if (InvalidNameChars.Contains(c)) { 68 | error = $"Invalid character '{c}' in entry name '{name.ToString()}'. Invalid characters include: < > : \" | ? * / \\"; 69 | return false; 70 | } 71 | } 72 | } 73 | 74 | if (options.HasFlag(PathOptions.NoReservedDeviceNames)) { 75 | const StringComparison comp = StringComparison.OrdinalIgnoreCase; 76 | 77 | // File name without extension also cannot match a reserved device name. 78 | 79 | if (name.IndexOf('.') is int nameLength && nameLength >= 0) 80 | name = name[..nameLength]; 81 | 82 | // Reserved device names: 83 | // CON, PRN, AUX, NUL, COM1 to COM9, LPT1 to LPT9 84 | 85 | if ((name.Length == 3 && (name.Equals("CON", comp) || name.Equals("PRN", comp) || name.Equals("AUX", comp) || name.Equals("NUL", comp))) || 86 | (name.Length == 4 && char.IsDigit(name[3]) && (name.StartsWith("COM", comp) || name.StartsWith("LPT", comp)))) 87 | { 88 | error = $"Invalid reserved device name in entry name '{name.ToString()}'."; 89 | return false; 90 | } 91 | } 92 | 93 | error = null; 94 | return true; 95 | } 96 | 97 | internal override bool IsUncPath(string absoluteDisplayPath) => absoluteDisplayPath[1] != ':'; 98 | 99 | private protected override ReadOnlySpan SplitAbsoluteRoot(ReadOnlySpan path, out ReadOnlySpan rest) 100 | { 101 | if (path.StartsWith(@"\\?\", StringComparison.Ordinal) || path.StartsWith(@"\\.\", StringComparison.Ordinal)) { 102 | path = path.Slice(4); 103 | 104 | if (path.StartsWith(@"UNC\", StringComparison.Ordinal)) 105 | path = StringHelper.Concat(@"\\", path[4..]); 106 | } 107 | 108 | ReadOnlySpan root; 109 | int firstIndex = path.IndexOf(SeparatorChar); 110 | 111 | if (firstIndex == 0) { 112 | if (path.Length < 5 || path[1] != SeparatorChar) 113 | ThrowInvalidPathRoot(); 114 | 115 | int serverLength = path.Slice(2).IndexOf(SeparatorChar); 116 | int shareStart = 3 + serverLength; 117 | 118 | if (serverLength <= 0 || path.Length <= shareStart) 119 | ThrowInvalidPathRoot(); 120 | 121 | var server = path.Slice(2, serverLength); 122 | 123 | if (!IsValidServerName(server)) 124 | throw new ArgumentException("Invalid UNC server name.", nameof(path)); 125 | 126 | int shareLength = path.Slice(shareStart).IndexOf(SeparatorChar); 127 | 128 | ReadOnlySpan share; 129 | 130 | if (shareLength <= 0) { 131 | root = StringHelper.Concat(path, SeparatorString); 132 | rest = default; 133 | share = path.Slice(shareStart); 134 | } 135 | else { 136 | root = path.Slice(0, shareStart + shareLength + 1); 137 | rest = path.Slice(root.Length); 138 | share = path.Slice(shareStart, shareLength); 139 | } 140 | 141 | // Share names can contain trailing dots but no leading or trailing spaces. Reserved device names do not apply to the share name. 142 | 143 | if (!ValidateEntryName(share, PathOptions.NoLeadingSpaces | PathOptions.NoTrailingSpaces, false, out string error)) 144 | throw new ArgumentException($"Invalid UNC share name: {error}"); 145 | } 146 | else { 147 | if (path.Length < 2 || (path.Length >= 3 && firstIndex != 2) || !(char.ToUpper(path[0], CultureInfo.InvariantCulture) is char drive && drive >= 'A' && drive <= 'Z') || path[1] != ':') 148 | ThrowInvalidPathRoot(); 149 | 150 | if (path.Length == 2) { 151 | root = StringHelper.Concat(path, SeparatorString); 152 | rest = default; 153 | } 154 | else { 155 | root = path.Slice(0, 3); 156 | rest = path.Slice(3); 157 | } 158 | } 159 | 160 | return root; 161 | 162 | static bool IsValidServerName(ReadOnlySpan server) 163 | { 164 | // Server name can be any valid hostname 165 | 166 | if (server[0] == '.' || server[^1] == '.' || server.IndexOf("..", StringComparison.Ordinal) >= 0) 167 | return false; 168 | 169 | foreach (char c in server) { 170 | if (!char.IsLetter(c) && !char.IsDigit(c) && c != '.' && c != '-') 171 | return false; 172 | } 173 | 174 | return true; 175 | } 176 | 177 | static void ThrowInvalidPathRoot() => throw new ArgumentException("Invalid absolute path root.", nameof(path)); 178 | } 179 | 180 | internal override string GetAbsolutePathExportString(string pathDisplay) 181 | { 182 | if (pathDisplay[1] == ':') 183 | return @"\\?\" + pathDisplay; 184 | 185 | return StringHelper.Concat(@"\\?\UNC\", pathDisplay.AsSpan()[2..]); 186 | } 187 | 188 | private static HashSet GetInvalidNameChars(bool includeWildcardChars) 189 | { 190 | var invalidChars = new HashSet() { '<', '>', ':', '"', '|', '/', '\\' }; 191 | 192 | for (int i = 0; i <= 31; i++) 193 | invalidChars.Add((char)i); 194 | 195 | if (includeWildcardChars) { 196 | invalidChars.Add('?'); 197 | invalidChars.Add('*'); 198 | } 199 | 200 | return invalidChars; 201 | } 202 | 203 | public override string ToString() => "Windows"; 204 | } 205 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/PathKind.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SA1602 // Enumeration items should be documented 2 | 3 | namespace Singulink.IO; 4 | 5 | internal enum PathKind 6 | { 7 | Absolute, 8 | Relative, 9 | RelativeRooted, 10 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/PathOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable RCS1154 // Sort enum members. 4 | 5 | namespace Singulink.IO; 6 | 7 | /// 8 | /// Provides options to control how path parsing is handled. 9 | /// 10 | /// 11 | /// Most applications should not attempt to process unfriendly paths as the pitfalls and edge cases are numerous and difficult to predict. is generally the recommended option to use. Applications like file managers that must work with all possible paths including 13 | /// those that are likely to be buggy/problematic should use instead and take great care to ensure they are handled correctly. It is 14 | /// safe to use for paths that are obtained by directly querying the file system (as opposed to user input or file data) and never 15 | /// stored for later use. 16 | /// 17 | /// 18 | [Flags] 19 | public enum PathOptions 20 | { 21 | /// 22 | /// Default value with no options set which allows all possible valid file system paths without wildcard characters. 23 | /// 24 | None = 0, 25 | 26 | /// 27 | /// Allows paths with empty directories to be processed without throwing an exception by removing them from the path. 28 | /// If this flag is set then paths like some///path get parsed to some/path. 29 | /// 30 | AllowEmptyDirectories = 1, 31 | 32 | /// 33 | /// Disallows entry names that match reserved device names. This flag has no effect on the path format. 34 | /// Reserved device names in paths can cause problems for many Windows applications and are not supported by File Explorer. Reserved device 35 | /// names include CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9. 36 | /// 37 | NoReservedDeviceNames = 1 << 8, 38 | 39 | /// 40 | /// Disallows entry names with a leading space. 41 | /// Leading spaces can cause problems for many Windows applications and are not fully supported by File Explorer. They can be difficult to handle 42 | /// correctly in application code, i.e. trimming input from users/data needs to be handled with care and often doesn't play 43 | /// nice with them on Windows. 44 | /// 45 | NoLeadingSpaces = 1 << 9, 46 | 47 | /// 48 | /// Disallows entry names with a trailing space. 49 | /// Trailing spaces can cause problems for many Windows applications and are not supported by File Explorer. They can be difficult to handle 50 | /// correctly in application code, i.e. trimming input from users/data needs to be handled with care and often doesn't play 51 | /// nice with them on Windows. 52 | /// 53 | NoTrailingSpaces = 1 << 10, 54 | 55 | /// 56 | /// Disallows entry names with a trailing dot. This flag has no effect on the path format. 57 | /// Trailing dots can cause problems for many Windows applications, are not supported by File Explorer and often doesn't 58 | /// play nice with them on Windows. Trailing dots do not pose any problems in Unix-based file systems and they don't pose potential trimming bugs so 59 | /// this flag has no effect when the path format is used. 60 | /// 61 | NoTrailingDots = 1 << 11, 62 | 63 | /// 64 | /// Disallows navigational path segments (i.e. . or ..) and rooted relative paths (i.e. /Some/Path when using the path format). Regular non-rooted relative paths are permitted. 66 | /// 67 | NoNavigation = 1 << 12, 68 | 69 | /// 70 | /// A combination of the , , and flags. This is the default value used for all parsing operations if no value is specified. 72 | /// 73 | NoUnfriendlyNames = NoReservedDeviceNames | NoLeadingSpaces | NoTrailingSpaces | NoTrailingDots, 74 | 75 | /// 76 | /// Effectively causes the flags to be appended when using the and path formats. 78 | /// Unix-based file systems tend to handle "unfriendly" paths much better than Windows-based file systems, so you can use this flag if you only want 79 | /// to disallow unfriendly paths on Windows and universal paths. 80 | /// 81 | PathFormatDependent = 1 << 31, 82 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/SearchOptions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Singulink.IO; 4 | 5 | /// 6 | /// Provides options that control search behavior in directories. 7 | /// 8 | public class SearchOptions 9 | { 10 | internal static readonly EnumerationOptions DefaultEnumerationOptions = new EnumerationOptions() { 11 | AttributesToSkip = default, 12 | MatchCasing = MatchCasing.CaseInsensitive, 13 | BufferSize = 0, 14 | RecurseSubdirectories = false, 15 | }; 16 | 17 | /// 18 | /// Gets or sets the attributes that will cause entries to be skipped. Default is none. 19 | /// 20 | public FileAttributes AttributesToSkip { get; set; } 21 | 22 | /// 23 | /// Gets or sets the suggested buffer size, in bytes. Default value is 0 (no suggestion). 24 | /// 25 | public int BufferSize { get; set; } 26 | 27 | /// 28 | /// Gets or sets a value indicating whether the search is case sensitive. Default is case insensitive. 29 | /// 30 | public MatchCasing MatchCasing { get; set; } = MatchCasing.CaseInsensitive; 31 | 32 | /// 33 | /// Gets or sets a value indicating whether the search is recursive, i.e. continues into child directories. Default is false. 34 | /// 35 | public bool Recursive { get; set; } 36 | 37 | internal EnumerationOptions ToEnumerationOptions() => new() 38 | { 39 | AttributesToSkip = AttributesToSkip, 40 | MatchCasing = MatchCasing, 41 | BufferSize = BufferSize, 42 | RecurseSubdirectories = Recursive, 43 | 44 | // Inaccessible defaults: 45 | // MatchType = MatchType.Simple, 46 | // ReturnSpecialDirectories = false, 47 | // IgnoreInaccessible = true, 48 | }; 49 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Singulink.IO.FileSystem.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.1 4 | Singulink.IO 5 | true 6 | true 7 | 1.0.3 8 | Singulink 9 | Reliable cross-platform strongly-typed file/directory path manipulation and file system access. 10 | © Singulink. All rights reserved. 11 | MIT 12 | https://github.com/Singulink/Singulink.IO.FileSystem 13 | File, Directory, Path, Folder, FileSystem 14 | true 15 | key.snk 16 | Singulink Icon 128x128.png 17 | README.md 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | true 26 | true 27 | true 28 | true 29 | 30 | 31 | 32 | 33 | 34 | True 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/SystemExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Singulink.IO; 4 | 5 | /// 6 | /// Provides extension methods that convert System.IO types to Singulink.IO.FileSystem types. 7 | /// 8 | public static class SystemExtensions 9 | { 10 | /// 11 | /// Gets the absolute directory path represented by the using the specified options. 12 | /// 13 | /// 14 | /// 15 | /// This method disallows unfriendly names by default, but any silent path modifications performed by (i.e. trimming of 16 | /// trailing spaces and dots) will remain intact. 17 | /// 18 | /// The property is used to get the absolute path that is parsed. 19 | /// 20 | public static IAbsoluteDirectoryPath ToPath(this DirectoryInfo dirInfo, PathOptions options = PathOptions.NoUnfriendlyNames) 21 | { 22 | return DirectoryPath.ParseAbsolute(dirInfo.FullName, options); 23 | } 24 | 25 | /// 26 | /// Gets the absolute file path represented by the using the specified options. 27 | /// 28 | /// 29 | /// 30 | /// This method disallows unfriendly names by default, but any silent path modifications performed by (i.e. trimming of trailing 31 | /// spaces and dots) will remain intact. 32 | /// 33 | /// The property is used to get the absolute path that is parsed. 34 | /// 35 | public static IAbsoluteFilePath ToPath(this FileInfo fileInfo, PathOptions options = PathOptions.NoUnfriendlyNames) 36 | { 37 | return FilePath.ParseAbsolute(fileInfo.FullName, options); 38 | } 39 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/UnauthorizedIOAccessException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.Serialization; 4 | 5 | namespace Singulink.IO; 6 | 7 | /// 8 | /// The exception that is thrown when the operating system denies access because of an I/O error. 9 | /// 10 | [Serializable] 11 | public class UnauthorizedIOAccessException : IOException 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | public UnauthorizedIOAccessException() { } 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public UnauthorizedIOAccessException(string message) : base(message) { } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public UnauthorizedIOAccessException(string message, Exception innerException) : base(message, innerException) { } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | public UnauthorizedIOAccessException(string message, int hresult) : base(message, hresult) 32 | { 33 | } 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | protected UnauthorizedIOAccessException(SerializationInfo info, StreamingContext context) : base(info, context) { } 39 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Utilities/Ex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Singulink.IO.Utilities; 5 | 6 | internal static class Ex 7 | { 8 | public static DirectoryNotFoundException NotFound(IAbsoluteDirectoryPath path) 9 | { 10 | return new DirectoryNotFoundException($"Could not find a part of the path '{path.PathDisplay}'."); 11 | } 12 | 13 | public static FileNotFoundException NotFound(IAbsoluteFilePath path) 14 | { 15 | return new FileNotFoundException($"Could not find file '{path.PathDisplay}'.", path.PathDisplay); 16 | } 17 | 18 | public static IOException FileIsDir(IAbsoluteFilePath path) 19 | { 20 | return new IOException($"The path '{path.PathDisplay}' points to a directory."); 21 | } 22 | 23 | public static IOException DirIsFile(IAbsoluteDirectoryPath path) 24 | { 25 | return new IOException($"The path '{path.PathDisplay}' points to a file."); 26 | } 27 | 28 | public static UnauthorizedIOAccessException Convert(UnauthorizedAccessException ex) 29 | { 30 | return new UnauthorizedIOAccessException(ex.Message, ex); 31 | } 32 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Utilities/StringHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO.Utilities; 4 | 5 | #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type 6 | 7 | internal static class StringHelper 8 | { 9 | public static unsafe string Concat(ReadOnlySpan s1, ReadOnlySpan s2) 10 | { 11 | var p1 = &s1; 12 | var p2 = &s2; 13 | 14 | var data = (p1: (nint)p1, p2: (nint)p2); 15 | 16 | return string.Create(s1.Length + s2.Length, data, (span, data) => { 17 | var s1 = *(ReadOnlySpan*)data.p1; 18 | var s2 = *(ReadOnlySpan*)data.p2; 19 | 20 | s1.CopyTo(span); 21 | s2.CopyTo(span.Slice(s1.Length)); 22 | }); 23 | } 24 | 25 | public static unsafe string Concat(ReadOnlySpan s1, ReadOnlySpan s2, ReadOnlySpan s3) 26 | { 27 | var p1 = &s1; 28 | var p2 = &s2; 29 | var p3 = &s3; 30 | 31 | var data = (p1: (nint)p1, p2: (nint)p2, p3: (nint)p3); 32 | 33 | return string.Create(s1.Length + s2.Length + s3.Length, data, (span, data) => { 34 | var s1 = *(ReadOnlySpan*)data.p1; 35 | var s2 = *(ReadOnlySpan*)data.p2; 36 | var s3 = *(ReadOnlySpan*)data.p3; 37 | 38 | s1.CopyTo(span); 39 | s2.CopyTo(span = span.Slice(s1.Length)); 40 | s3.CopyTo(span.Slice(s2.Length)); 41 | }); 42 | } 43 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/Utilities/StringOrSpan.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Singulink.IO.Utilities; 4 | 5 | internal ref struct StringOrSpan 6 | { 7 | public static StringOrSpan Empty => new StringOrSpan(string.Empty); 8 | 9 | private readonly ReadOnlySpan _span; 10 | private string? _string; 11 | 12 | public ReadOnlySpan Span => _span; 13 | 14 | public string String => _string ??= _span.ToString(); 15 | 16 | public int Length => Span.Length; 17 | 18 | public StringOrSpan(string value) 19 | { 20 | _string = value; 21 | _span = value; 22 | } 23 | 24 | public StringOrSpan(ReadOnlySpan value) 25 | { 26 | _string = null; 27 | _span = value; 28 | } 29 | 30 | public static implicit operator string(StringOrSpan value) => value.String; 31 | 32 | public static implicit operator ReadOnlySpan(StringOrSpan value) => value.Span; 33 | 34 | public static implicit operator StringOrSpan(string value) => new StringOrSpan(value); 35 | 36 | public static implicit operator StringOrSpan(ReadOnlySpan value) => new StringOrSpan(value); 37 | 38 | public StringOrSpan Replace(char oldChar, char newChar) 39 | { 40 | if (_string is null && Span.IndexOf(oldChar) < 0) 41 | return this; 42 | 43 | return String.Replace(oldChar, newChar); 44 | } 45 | 46 | public override string ToString() => String; 47 | } -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singulink/Singulink.IO.FileSystem/0d6201ee7a08da3d5c7a4a5812cec809a772f767/Source/Singulink.IO.FileSystem/key.snk -------------------------------------------------------------------------------- /Source/Singulink.IO.FileSystem/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enabling configuration: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 3 | 4 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 5 | "settings": { 6 | "documentationRules": { 7 | "documentExposedElements": true, 8 | "documentInternalElements": false, 9 | "documentInterfaces": false 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------