├── .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 | {{__global.name}}
14 | {{__global.description}}
15 |
16 |
17 |
18 | {{/children.0}}
19 | {{#children}}
20 |
21 |
22 |
23 |
24 | {{{summary}}}
25 |
26 | {{/children}}
27 | {{#children.0}}
28 |
29 |
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 | {{__global.type}}
37 | {{__global.name}}
38 | {{__global.description}}
39 |
40 |
41 |
42 | {{/parameters.0}}
43 | {{#parameters}}
44 |
45 | {{{type.specName.0.value}}}
46 | {{{id}}}
47 | {{{description}}}
48 |
49 | {{/parameters}}
50 | {{#parameters.0}}
51 |
52 |
53 | {{/parameters.0}}
54 | {{#return}}
55 | {{__global.returns}}
56 |
57 |
58 |
59 | {{__global.type}}
60 | {{__global.description}}
61 |
62 |
63 |
64 |
65 | {{{type.specName.0.value}}}
66 | {{{description}}}
67 |
68 |
69 |
70 | {{/return}}
71 | {{#typeParameters.0}}
72 | {{__global.typeParameters}}
73 |
74 |
75 |
76 | {{__global.name}}
77 | {{__global.description}}
78 |
79 |
80 |
81 | {{/typeParameters.0}}
82 | {{#typeParameters}}
83 |
84 | {{{id}}}
85 | {{{description}}}
86 |
87 | {{/typeParameters}}
88 | {{#typeParameters.0}}
89 |
90 |
91 | {{/typeParameters.0}}
92 | {{#fieldValue}}
93 | {{__global.fieldValue}}
94 |
95 |
96 |
97 | {{__global.type}}
98 | {{__global.description}}
99 |
100 |
101 |
102 |
103 | {{{type.specName.0.value}}}
104 | {{{description}}}
105 |
106 |
107 |
108 | {{/fieldValue}}
109 | {{#propertyValue}}
110 | {{__global.propertyValue}}
111 |
112 |
113 |
114 | {{__global.type}}
115 | {{__global.description}}
116 |
117 |
118 |
119 |
120 | {{{type.specName.0.value}}}
121 | {{{description}}}
122 |
123 |
124 |
125 | {{/propertyValue}}
126 | {{#eventType}}
127 | {{__global.eventType}}
128 |
129 |
130 |
131 | {{__global.type}}
132 | {{__global.description}}
133 |
134 |
135 |
136 |
137 | {{{type.specName.0.value}}}
138 | {{{description}}}
139 |
140 |
141 |
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 |
161 |
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 | {{__global.type}}
175 | {{__global.condition}}
176 |
177 |
178 |
179 | {{/exceptions.0}}
180 | {{#exceptions}}
181 |
182 | {{{type.specName.0.value}}}
183 | {{{description}}}
184 |
185 | {{/exceptions}}
186 | {{#exceptions.0}}
187 |
188 |
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 |
38 |
39 |
40 | {{#_enableSearch}}
41 | {{>partials/searchResults}}
42 | {{/_enableSearch}}
43 |
44 |
45 |
46 |
47 | {{^_disableBreadcrumb}}
48 | {{>partials/breadcrumb}}
49 | {{/_disableBreadcrumb}}
50 |
51 | {{^_disableContribution}}
52 |
57 | {{/_disableContribution}}
58 |
59 |
60 | {{!body}}
61 |
62 |
63 |
64 | {{#_copyrightFooter}}
65 |
68 | {{/_copyrightFooter}}
69 |
70 |
71 |
72 | {{>partials/scripts}}
73 |
74 |
75 |
--------------------------------------------------------------------------------
/Docs/templates/singulinkfx/partials/footer.tmpl.partial:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
7 | {{#children}}
8 | {{>partials/namespaceSubtitle}}
9 | {{#children}}
10 |
11 |
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 |
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 |
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 |
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 |
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 | [](https://discord.gg/EkQhJFsBu6)
4 | [](https://www.nuget.org/packages/Singulink.IO.FileSystem/)
5 | [](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 |
--------------------------------------------------------------------------------