├── .github ├── FUNDING.yml ├── docker.sh └── workflows │ ├── amazon-linux-2.yml │ ├── macos-11.yml │ ├── ubuntu-bionic.yml │ ├── ubuntu-focal.yml │ └── windows.yml ├── .gitignore ├── CHANGELOG.md ├── Examples ├── cwd │ └── main.swift ├── ls │ └── main.swift └── mkdir │ └── main.swift ├── LICENSE.txt ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── CSystemExtras │ ├── include │ │ ├── CSystemExtras.h │ │ └── module.modulemap │ └── shims.c └── SystemExtras │ ├── CRUD.swift │ ├── DirectoryContent.swift │ ├── FileMetadata.swift │ ├── FileType.swift │ ├── Internals.swift │ ├── Permissions.swift │ ├── ReadWrite.swift │ ├── Symlink.swift │ ├── Temprary.swift │ ├── Windows.swift │ ├── WindowsAttributes.swift │ └── WorkingDirectory.swift └── Tests └── SystemExtrasTests ├── CopyFileTests.swift ├── DirectoryContentTests.swift ├── ExistsTests.swift ├── MetadataTests.swift ├── MoveTests.swift ├── Permissions.swift ├── ReadWriteTests.swift ├── SymlinkTests.swift ├── TemporaryTests.swift └── WorkingDirectoryTests.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dduan] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v docker > /dev/null; then 4 | echo "Install docker https://docker.com" >&2 5 | exit 1 6 | fi 7 | project=$1 8 | action=$2 9 | swift=$3 10 | distro=$4 11 | dockerfile=$(mktemp) 12 | echo "FROM swift:$swift-$distro" > $dockerfile 13 | echo "ADD . $project" >> $dockerfile 14 | echo "WORKDIR $project" >> $dockerfile 15 | echo "RUN $action" >> $dockerfile 16 | image=$(echo $project | tr '[:upper:]' '[:lower:]') 17 | docker image rm -f $image &> /dev/null 18 | docker build -t $image -f $dockerfile . 19 | docker run --rm $image 20 | -------------------------------------------------------------------------------- /.github/workflows/amazon-linux-2.yml: -------------------------------------------------------------------------------- 1 | name: Amazon Linux 2 2 | 3 | on: [push] 4 | 5 | jobs: 6 | linux: 7 | name: Amazon Linux 2 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Test 12 | run: .github/docker.sh SystemExtras 'swift test -Xswiftc -warnings-as-errors' 5.6.1 amazonlinux2 13 | -------------------------------------------------------------------------------- /.github/workflows/macos-11.yml: -------------------------------------------------------------------------------- 1 | name: macOS 11 2 | 3 | on: [push] 4 | 5 | jobs: 6 | macos: 7 | name: macOS 8 | runs-on: macos-11 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Xcode version 12 | run: sudo xcode-select -s /Applications/Xcode_13.2.1.app 13 | - name: Test 14 | run: swift test 15 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu-bionic.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu Bionic 2 | 3 | on: [push] 4 | 5 | jobs: 6 | linux: 7 | name: Ubuntu Bionic 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Test 12 | run: .github/docker.sh SystemExtras 'swift test -Xswiftc -warnings-as-errors' 5.6.1 bionic 13 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu-focal.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu Focal 2 | 3 | on: [push] 4 | 5 | jobs: 6 | linux: 7 | name: Ubuntu Focal 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Test 12 | run: .github/docker.sh SystemExtras 'swift test -Xswiftc -warnings-as-errors' 5.6.1 focal 13 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2019 2 | 3 | on: [push] 4 | 5 | jobs: 6 | Windows: 7 | name: Windows 8 | runs-on: windows-2019 9 | steps: 10 | - name: Check out 11 | uses: actions/checkout@v2 12 | - name: Install Swift 13 | uses: compnerd/gha-setup-swift@main 14 | with: 15 | branch: swift-5.6.1-release 16 | tag: 5.6.1-RELEASE 17 | - name: Test 18 | shell: cmd 19 | run: | 20 | echo on 21 | C:\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin\swift-test.exe 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # main 2 | 3 | - Add `FileMetadata` type to reperesent file metadata 4 | - Add `FilePath.metadata()` method to retrieve file metadata 5 | - Add `FilePath.workingDirectory()` static method for returning current working directory. 6 | - Add `FilePath.setWorkingDirectory()` static method for setting current working directory. 7 | - Add `FilePath.exists()` method that checks whether a path exists. 8 | - Add `FilePath.makeDirectory(withParants:permissions:)` method that creates a directory and its parents. 9 | - Add `FilePath.searchForTemporaryDirectory()` static method that finds default temporary directory in an OS. 10 | - Add `FilePath.defaultTemporaryDirectory` static variable that dictates root of all temporary directories. 11 | - Add `FilePath.makeTemporaryDirectory()` static method that makes temporary directory with writable access. 12 | - Add `FilePath.asWorkingDirectory()` method which execute a closure with `self` as the working directory, and 13 | then restores the original working directory. 14 | - Add `FilePath.directoryContent(recursive:)` method. This method returns a `Sequence` `DirectoryContent`, 15 | whose element is a 2-tuple of file path and type in `self`, if `self` is a directory. 16 | - Add `FilePath.delete(recursive:)` which deletes the content at `self`. 17 | - Add `FilePath.withTemproraryDirectory(run:)` which runs a closure with a newly created temporary directory 18 | as the working directory, and removes the temporary directory, restores original working directories 19 | afterwards. 20 | - Add the following convenient method for reading and writing on a file path: 21 | * `FilePath.readBytes(fromAbsoluteOffset:)`: read bytes from `self` 22 | * `FilePath.readString(as:)`: read bytes and encode to string with a specified encoding. 23 | * `FilePath.readUTF8String()`: read bytes and encode to UTF-8 string. 24 | * `FilePath.write(:options:permissions:)`: write sequence of bytes to `self`. 25 | * `FilePath.write(utf8:options:permissions:)`: write a string decoded with UTF-8 to `self`. 26 | 27 | Add `FilePath.set(_:)` method, which sets new `FilePermission` at `self`. 28 | -------------------------------------------------------------------------------- /Examples/cwd/main.swift: -------------------------------------------------------------------------------- 1 | import SystemExtras 2 | import SystemPackage 3 | 4 | try print(FilePath.workingDirectory()) 5 | -------------------------------------------------------------------------------- /Examples/ls/main.swift: -------------------------------------------------------------------------------- 1 | import SystemExtras 2 | import SystemPackage 3 | 4 | extension String { 5 | func withLeftPad(_ n: Int) -> String { 6 | String(repeating: " ", count: max(0, n - count)) + self 7 | } 8 | } 9 | 10 | do { 11 | for child in FilePath(CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : ".").directoryContent() { 12 | var typeString: String = "" 13 | let meta = try child.0.metadata() 14 | 15 | typeString += meta.fileType.isDirectory ? "d" : "-" 16 | typeString += meta.fileType.isFile ? "f" : "-" 17 | typeString += meta.fileType.isSymlink ? "l" : "-" 18 | typeString += meta.permissions.isReadOnly ? "-" : "w" 19 | let sizeString = "\(meta.size)".withLeftPad(10) 20 | print("\(typeString) \(sizeString) \(child.0)") 21 | } 22 | } catch { 23 | print(error) 24 | } 25 | -------------------------------------------------------------------------------- /Examples/mkdir/main.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import SystemExtras 3 | 4 | func mkdir(_ args: [String]) { 5 | var args = Set(args) 6 | let makeParents: Bool 7 | if args.contains("-p") { 8 | makeParents = true 9 | args.remove("-p") 10 | } else { 11 | makeParents = false 12 | } 13 | 14 | do { 15 | try FilePath(args.first!).makeDirectory(withParents: makeParents) 16 | } catch { 17 | print(error) 18 | } 19 | } 20 | 21 | mkdir(Array(CommandLine.arguments.dropFirst())) 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | 205 | ## Runtime Library Exception to the Apache 2.0 License: ## 206 | 207 | 208 | As an exception, if you use this Software to compile your source code and 209 | portions of this Software are embedded into the binary product as a result, 210 | you may redistribute such product without providing attribution as would 211 | otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-system", 6 | "repositoryURL": "https://github.com/apple/swift-system", 7 | "state": { 8 | "branch": null, 9 | "revision": "836bc4557b74fe6d2660218d56e3ce96aff76574", 10 | "version": "1.1.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-system-extras", 7 | platforms: [.macOS(.v12)], 8 | products: [ 9 | .library( 10 | name: "SystemExtras", 11 | targets: ["SystemExtras"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-system", from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .target(name: "CSystemExtras"), 18 | .target( 19 | name: "SystemExtras", 20 | dependencies: [ 21 | .product(name: "SystemPackage", package: "swift-system"), 22 | .target(name: "CSystemExtras"), 23 | ], 24 | cSettings: [ 25 | .define("_CRT_SECURE_NO_WARNINGS") 26 | ] 27 | ), 28 | .testTarget(name: "SystemExtrasTests", dependencies: ["SystemExtras"]), 29 | 30 | ] + [ 31 | // Examples 32 | "cwd", 33 | "mkdir", 34 | "ls", 35 | ].map { name in 36 | .executableTarget( 37 | name: name, 38 | dependencies: [.target(name: "SystemExtras")], 39 | path: "Examples/\(name)" 40 | ) 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # System Extras 2 | 3 | As a complimentary set of extensions to Swift System, this library lets you create/read/update/delete files, directories, symlinks, and permissions, all the while leveraging the familiar types from Swift Systems. 4 | And yes, it all works on Windows! 5 | 6 | ## API Design Goals 7 | 1. Feel natural to use in conjunction to existing System APIs 8 | 2. Provide missing batteries from System 9 | 3. (Unlike System) prefer ergonomics over loyalty to native APIs 10 | 2. (Unlike System) strive for cross-platform APIs (as opposed to [multi-platform][]) 11 | 12 | [multi-platform]: https://github.com/apple/swift-system#multi-platform-not-cross-platform 13 | 14 | ## LICENSE 15 | 16 | See [LICENSE](LICENSE.txt) for license information. 17 | -------------------------------------------------------------------------------- /Sources/CSystemExtras/include/CSystemExtras.h: -------------------------------------------------------------------------------- 1 | #if defined(_WIN32) 2 | #ifndef CSYSTEMEXTRAS_H 3 | #define CSYSTEMEXTRAS_H 4 | 5 | #if defined(__MINGW32__) 6 | # define WINDOWSHELPERS_DECLARE(type) type 7 | #elif defined(WIN32) 8 | # if defined(WINDOWSHELPERS_DECLARE_STATIC) 9 | # define WINDOWSHELPERS_DECLARE(type) type 10 | # elif defined(WINDOWSHELPERS_DECLARE_EXPORT) 11 | # define WINDOWSHELPERS_DECLARE(type) __declspec(dllexport) type 12 | # else 13 | # define WINDOWSHELPERS_DECLARE(type) __declspec(dllimport) type 14 | # endif 15 | #else 16 | # define WINDOWSHELPERS_DECLARE(type) type 17 | #endif 18 | 19 | #if defined (_WIN64) 20 | #include 21 | #include 22 | 23 | #ifndef IO_REPARSE_TAG_SYMLINK 24 | #define IO_REPARSE_TAG_SYMLINK (0xA000000CL) 25 | #endif 26 | 27 | #ifndef IO_REPARSE_TAG_MOUNT_POINT 28 | #define IO_REPARSE_TAG_MOUNT_POINT (0xA0000003L) 29 | #endif 30 | 31 | typedef struct { 32 | unsigned long reparseTag; 33 | unsigned short reparseDataLength; 34 | unsigned short reserved; 35 | unsigned short substituteNameOffset; 36 | unsigned short substituteNameLength; 37 | unsigned short printNameOffset; 38 | unsigned short printNameLength; 39 | unsigned long flags; 40 | wchar_t pathBuffer[1]; 41 | } ReparseDataBuffer; 42 | 43 | typedef ReparseDataBuffer SymbolicLinkReparseBuffer; 44 | 45 | typedef struct { 46 | unsigned long reparseTag; 47 | unsigned short reparseDataLength; 48 | unsigned short reserved; 49 | unsigned short substituteNameOffset; 50 | unsigned short substituteNameLength; 51 | unsigned short printNameOffset; 52 | unsigned short printNameLength; 53 | wchar_t pathBuffer[1]; 54 | } MountPointReparseBuffer; 55 | #endif 56 | #endif 57 | #endif // defined(_WIN32) 58 | -------------------------------------------------------------------------------- /Sources/CSystemExtras/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CSystemExtras { 2 | header "CSystemExtras.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Sources/CSystemExtras/shims.c: -------------------------------------------------------------------------------- 1 | #if defined(_WIN32) 2 | #include 3 | #endif 4 | -------------------------------------------------------------------------------- /Sources/SystemExtras/CRUD.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | #if os(Windows) 3 | import WinSDK 4 | #endif 5 | 6 | let kCopyChunkSize = 16 * 1024 7 | 8 | extension FilePath { 9 | /// Create a directory, and, optionally, any intermediate directories that leads to it, if they don't 10 | /// exist yet. 11 | /// 12 | /// - Parameters: 13 | /// - withParents: Create intermediate directories as required. If this option is not specified, 14 | /// the full path prefix of each operand must already exist. 15 | /// On the other hand, with this option specified, no error will be reported if a 16 | /// directory given as an operand already exists. 17 | /// - permissions: Permissions for the new directories. Has no effect on Windows. 18 | public func makeDirectory( 19 | withParents: Bool = false, 20 | permissions: FilePermissions = FilePermissions(rawValue: 0o755) 21 | ) throws { 22 | func _makeDirectory() throws { 23 | let outcome = self.withPlatformString { cString in 24 | system_mkdir(cString, permissions.rawValue) 25 | } 26 | 27 | if outcome != 0 { 28 | let error = Errno(rawValue: system_errno) 29 | if !exists() || error == .fileExists && !withParents { 30 | throw error 31 | } 32 | } 33 | } 34 | 35 | if withParents && !self.exists() { 36 | try self.removingLastComponent().makeDirectory(withParents: true, permissions: permissions) 37 | } 38 | 39 | try _makeDirectory() 40 | } 41 | 42 | /// Delete content at `self`. 43 | /// Content of directory is deleted alongside the directory itself, unless specified otherwise. 44 | /// 45 | /// - Parameter recursive: `true` means content of non-empty directory will be deleted along 46 | /// with the directory itself. 47 | public func delete(recursive: Bool = false) throws { 48 | guard let meta = try? self.metadata() else { 49 | return // file doesn't exist 50 | } 51 | 52 | if meta.fileType.isDirectory { 53 | if recursive { 54 | for child in self.directoryContent() { 55 | try child.0.delete(recursive: true) 56 | } 57 | } 58 | 59 | // On Windows, we can't rely on the OS to think all children have been deleted at this point. 60 | // The only way to convince the os is to move the directory to another location first. 61 | let randomName = "\(UInt64.random(in: 0 ... .max))" 62 | let tempLocation = FilePath.defaultTemporaryDirectory.appending(randomName) 63 | try self.move(to: tempLocation) 64 | 65 | try tempLocation.withPlatformString { tempCString in 66 | if system_rmdir(tempCString) != 0 { 67 | throw Errno(rawValue: system_errno) 68 | } 69 | } 70 | } else { 71 | try self.withPlatformString { cString in 72 | if system_unlink(cString) != 0 { 73 | throw Errno(rawValue: system_errno) 74 | } 75 | } 76 | } 77 | } 78 | 79 | /// Move a file or direcotry to a new path. If the destination already exist, write over it. 80 | /// 81 | /// - Parameter newPath: New path for the content at the current path. 82 | public func move(to newPath: FilePath) throws { 83 | try self.withPlatformString { sourceCString in 84 | try newPath.withPlatformString { targetCString in 85 | if system_rename(sourceCString, targetCString) != 0 { 86 | throw Errno(rawValue: system_errno) 87 | } 88 | } 89 | } 90 | } 91 | 92 | /// Copy a file or symlink from `self` to `destination`. 93 | /// 94 | /// - Parameters 95 | /// - destination: The path to destination. 96 | /// - followSymlink: If `self` is a symlink, `true` to copy the content it links to, `false` to copy the 97 | /// link itself. 98 | public func copyFile(to destination: FilePath, followSymlink: Bool = true) throws { 99 | let sourceMeta = try metadata() 100 | 101 | if !sourceMeta.fileType.isFile && !sourceMeta.fileType.isSymlink { 102 | throw Errno(rawValue: -1) 103 | } 104 | 105 | let isLink = sourceMeta.fileType.isSymlink 106 | if !followSymlink && isLink { 107 | try self.readSymlink().makeSymlink(at: destination) 108 | return 109 | } 110 | 111 | let source = isLink ? try self.readSymlink() : self 112 | #if os(Windows) 113 | let attributes = try source.metadata().permissions as! WindowsAttributes 114 | 115 | try source.withPlatformString { sourcePath in 116 | try destination.withPlatformString { destinationPath in 117 | if !CopyFileW( 118 | sourcePath, 119 | destinationPath, 120 | false 121 | ) { 122 | throw Errno(rawValue: -1) 123 | } 124 | 125 | if !SetFileAttributesW( 126 | destinationPath, 127 | attributes.rawValue 128 | ) { 129 | throw Errno(rawValue: -1) 130 | } 131 | 132 | } 133 | } 134 | #else //os(Windows) 135 | let permissions = try source.metadata().permissions as! FilePermissions 136 | let sourceFD = try FileDescriptor.open(source, .readOnly) 137 | defer { try? sourceFD.close() } 138 | let destinationFD = try FileDescriptor.open(destination, .writeOnly, options: .create, permissions: permissions) 139 | defer { try? destinationFD.close() } 140 | let buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: kCopyChunkSize, alignment: MemoryLayout.alignment) 141 | defer { buffer.deallocate() } 142 | defer { 143 | try? destination.set(permissions) 144 | } 145 | 146 | var position: Int64 = 0 147 | while true { 148 | let length = try sourceFD.read(fromAbsoluteOffset: position, into: buffer) 149 | if length == 0 { 150 | break 151 | } 152 | _ = try destinationFD.write(toAbsoluteOffset: position, .init(start: buffer.baseAddress, count: length)) 153 | position = position + Int64(length) 154 | } 155 | #endif //os(Windows) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/SystemExtras/DirectoryContent.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 2 | import Darwin 3 | #elseif os(Linux) || os(FreeBSD) || os(Android) 4 | import Glibc 5 | #elseif os(Windows) 6 | import ucrt 7 | import WinSDK 8 | #else 9 | #error("Unsupported Platform") 10 | #endif 11 | 12 | import SystemPackage 13 | 14 | extension FilePath { 15 | public func directoryContent(recursive: Bool = false, followSymlink: Bool = false) -> DirectoryContent { 16 | DirectoryContent(of: self, recursive: recursive, followSymlink: followSymlink) 17 | } 18 | } 19 | 20 | public struct DirectoryContent: Sequence { 21 | let directory: FilePath 22 | let recursive: Bool 23 | let followSymlink: Bool 24 | 25 | public init(of directory: FilePath, recursive: Bool = false, followSymlink: Bool = false) { 26 | self.directory = directory 27 | self.recursive = recursive 28 | self.followSymlink = followSymlink 29 | } 30 | 31 | public func makeIterator() -> DirectoryContentIterator { 32 | DirectoryContentIterator(queue: [(directory, directory)], recursive: recursive, followSymlink: followSymlink) 33 | } 34 | } 35 | 36 | public final class DirectoryContentIterator: IteratorProtocol { 37 | var queue: [(FilePath, FilePath)] // (real, link) 38 | let recursive: Bool 39 | let followSymlink: Bool 40 | 41 | init(queue: [(FilePath, FilePath)], recursive: Bool, followSymlink: Bool) { 42 | self.queue = queue 43 | self.recursive = recursive 44 | self.followSymlink = followSymlink 45 | } 46 | 47 | #if os(Windows) 48 | deinit { 49 | if let handle = self.current?.handle { 50 | CloseHandle(handle) 51 | } 52 | } 53 | 54 | var current: (handle: UnsafeMutableRawPointer, parent: FilePath, logicalParent: FilePath)? 55 | public func next() -> (FilePath, FileType)? { 56 | func process(_ data: WIN32_FIND_DATAW, _ currentParent: FilePath, _ logicalCurrentParent: FilePath) -> (FilePath, FileType)? { 57 | guard let name = withUnsafeBytes(of: data.cFileName, { 58 | $0.bindMemory(to: CInterop.PlatformChar.self) 59 | .baseAddress 60 | .map(String.init(platformString:)) 61 | }), 62 | name != "..", 63 | name != ".", 64 | !name.isEmpty 65 | else { 66 | return nil 67 | } 68 | 69 | let path = currentParent.appending(name) 70 | let fileType = FileType(data) 71 | let logicalPath = logicalCurrentParent.appending(name) 72 | if self.recursive { 73 | if fileType.isSymlink, 74 | fileType.isDirectory, 75 | let link = try? path.readSymlink(), 76 | case let realPath = currentParent.pushing(link), 77 | (try? realPath.metadata().fileType.isDirectory) == true 78 | { 79 | queue.append((realPath, path)) 80 | } else if fileType.isDirectory { 81 | queue.append((path, logicalPath)) 82 | } 83 | } 84 | 85 | return (logicalPath, fileType) 86 | } 87 | 88 | var data = WIN32_FIND_DATAW() 89 | if let (handle, currentParent, logicalCurrentParent) = self.current { 90 | if FindNextFileW(handle, &data) { 91 | if let result = process(data, currentParent, logicalCurrentParent) { 92 | return result 93 | } 94 | } else { 95 | CloseHandle(handle) 96 | self.current = nil 97 | } 98 | } else { 99 | guard let nextPath = queue.first else { 100 | return nil 101 | } 102 | 103 | let handle = (nextPath.0.appending("*")).withPlatformString { FindFirstFileW($0, &data) } 104 | let currentParent = queue.removeFirst() 105 | if let handle = handle, handle != INVALID_HANDLE_VALUE { 106 | self.current = (handle, currentParent.0, currentParent.1) 107 | if let result = process(data, currentParent.0, currentParent.1) { 108 | return result 109 | } 110 | } else { 111 | self.current = nil 112 | } 113 | 114 | } 115 | return next() 116 | } 117 | #else 118 | deinit { 119 | if let handle = self.current?.handle { 120 | closedir(handle) 121 | } 122 | } 123 | 124 | #if os(macOS) 125 | var current: (handle: UnsafeMutablePointer, parent: FilePath, logicalParent: FilePath)? 126 | #else 127 | var current: (handle: OpaquePointer, parent: FilePath, logicalParent: FilePath)? 128 | #endif 129 | public func next() -> (FilePath, FileType)? { 130 | if let (handle, currentParent, logicalCurrentParent) = self.current { 131 | guard let entry = readdir(handle)?.pointee else { 132 | closedir(handle) 133 | self.current = nil 134 | return next() 135 | } 136 | 137 | guard let name = withUnsafeBytes(of: entry.d_name, { 138 | $0.bindMemory(to: CInterop.PlatformChar.self) 139 | .baseAddress 140 | .map(String.init(platformString:)) 141 | }), 142 | name != "..", 143 | name != "." 144 | else { 145 | return next() 146 | } 147 | 148 | let fileType = FileType(entry) 149 | let path = currentParent.appending(name) 150 | 151 | let logicalPath = logicalCurrentParent.appending(name) 152 | if recursive { 153 | if fileType.isDirectory { 154 | queue.append((path, logicalPath)) 155 | } else if 156 | fileType.isSymlink, 157 | self.followSymlink, 158 | let link = try? path.readSymlink(), 159 | case let realPath = currentParent.pushing(link), 160 | (try? realPath.metadata().fileType.isDirectory) == true 161 | { 162 | queue.append((realPath, path)) 163 | } 164 | } 165 | 166 | return (logicalPath, fileType) 167 | } else { 168 | guard let nextPath = queue.first else { 169 | return nil 170 | } 171 | 172 | let handle = nextPath.0.withPlatformString { opendir($0) } 173 | let currentParent = queue.removeFirst() 174 | if let handle = handle { 175 | self.current = (handle, currentParent.0, currentParent.1) 176 | } else { 177 | self.current = nil 178 | } 179 | 180 | return next() 181 | } 182 | } 183 | #endif 184 | } 185 | -------------------------------------------------------------------------------- /Sources/SystemExtras/FileMetadata.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | #if os(Windows) 3 | import WinSDK 4 | #endif 5 | 6 | public struct FileMetadata { 7 | public let permissions: Permissions 8 | public let fileType: FileType 9 | public let size: Int64 10 | 11 | #if os(Windows) 12 | init(_ data: WIN32_FIND_DATAW) { 13 | self.permissions = WindowsAttributes(rawValue: data.dwFileAttributes) 14 | self.fileType = FileType(data) 15 | self.size = Int64(UInt64(data.nFileSizeHigh) << 32 | UInt64(data.nFileSizeLow)) 16 | } 17 | #else 18 | init(_ status: system_stat_struct) { 19 | let mode = CInterop.Mode(status.st_mode) 20 | self.permissions = FilePermissions(rawValue: mode & 0o7777) 21 | self.fileType = FileType(mode) 22 | #if os(Linux) 23 | self.size = Int64(status.st_size) 24 | #else 25 | self.size = Int64(status.st_size) 26 | #endif 27 | } 28 | #endif 29 | } 30 | 31 | extension FilePath { 32 | #if os(Windows) 33 | public func metadata(followSymlink: Bool = false) throws -> FileMetadata { 34 | let path = try followSymlink ? self.windowsFinalName() : self 35 | var data = WIN32_FIND_DATAW() 36 | try path.withPlatformString { cString in 37 | let handle = FindFirstFileW(cString, &data) 38 | if handle == INVALID_HANDLE_VALUE { 39 | // TODO: Map windows error to errno 40 | throw Errno(rawValue: -1) 41 | } 42 | 43 | CloseHandle(handle) 44 | } 45 | 46 | return FileMetadata(data) 47 | } 48 | #else 49 | public func metadata(followSymlink: Bool = false) throws -> FileMetadata { 50 | var status = system_stat_struct() 51 | let anyStat = followSymlink ? system_stat : system_lstat 52 | try self.withPlatformString { cString in 53 | if anyStat(cString, &status) != 0 { 54 | throw Errno(rawValue: system_errno) 55 | } 56 | } 57 | 58 | return FileMetadata(status) 59 | } 60 | #endif // os(Windows) 61 | 62 | /// Return `true` if path refers to an existing path. 63 | /// On some platforms, this function may return `false` if permission is not 64 | /// granted to retrieve metadata on the requested file, even if the path physically exists. 65 | /// 66 | /// - Parameter followSymlink: whether to follow symbolic links. If `true`, return `false` for 67 | /// broken symbolic links. 68 | /// 69 | /// - Returns: whether path refers to an existing path or an open file descriptor. 70 | public func exists(followSymlink: Bool = false) -> Bool { 71 | (try? self.metadata(followSymlink: followSymlink)) != nil 72 | } 73 | 74 | #if os(Windows) 75 | /// Set new permissions for a file path. 76 | /// 77 | /// - Parameter permissions: The new file permission. 78 | public func set(_ permissions: Permissions) throws { 79 | guard let permissions = permissions as? WindowsAttributes else { 80 | fatalError("Attempting to set unix FilePermissions on Windows") 81 | } 82 | 83 | try self.withPlatformString { cString in 84 | if !SetFileAttributesW(cString, permissions.rawValue) { 85 | // TODO: Map windows error to errno 86 | throw Errno(rawValue: -1) 87 | } 88 | } 89 | } 90 | #else 91 | /// Set new permissions for a file path. 92 | /// 93 | /// - Parameter permissions: The new file permission. 94 | public func set(_ permissions: Permissions) throws { 95 | guard let permissions = permissions as? FilePermissions else { 96 | fatalError("Attempting to set Windows Attributes on Unix") 97 | } 98 | 99 | try self.withPlatformString { cString in 100 | if system_chmod(cString, permissions.rawValue) != 0 { 101 | throw Errno(rawValue: system_errno) 102 | } 103 | } 104 | } 105 | #endif // os(Windows) 106 | } 107 | -------------------------------------------------------------------------------- /Sources/SystemExtras/FileType.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 2 | import Darwin 3 | #elseif os(Linux) || os(FreeBSD) || os(Android) 4 | import Glibc 5 | #elseif os(Windows) 6 | import ucrt 7 | import WinSDK 8 | #else 9 | #error("Unsupported Platform") 10 | #endif 11 | 12 | import SystemPackage 13 | 14 | public struct FileType { 15 | public let isFile: Bool 16 | public let isDirectory: Bool 17 | public let isSymlink: Bool 18 | 19 | init(_ rawMode: CInterop.Mode) { 20 | let masked = rawMode & S_IFMT 21 | self.isFile = masked == S_IFREG 22 | self.isDirectory = masked == S_IFDIR 23 | #if os(Windows) 24 | self.isSymlink = false 25 | #else 26 | self.isSymlink = masked == S_IFLNK 27 | #endif 28 | } 29 | 30 | #if os(Windows) 31 | init(_ data: WIN32_FIND_DATAW) { 32 | self.isDirectory = data.dwFileAttributes & UInt32(bitPattern: FILE_ATTRIBUTE_DIRECTORY) != 0 33 | self.isSymlink = data.dwFileAttributes & UInt32(bitPattern: FILE_ATTRIBUTE_REPARSE_POINT) != 0 && data.dwReserved0 & 0x2000_0000 != 0 34 | self.isFile = !isDirectory 35 | } 36 | #else 37 | init(_ dirEntry: dirent) { 38 | let dType = dirEntry.d_type 39 | self.isFile = dType == DT_REG 40 | self.isDirectory = dType == DT_DIR 41 | self.isSymlink = dType == DT_LNK 42 | } 43 | #endif 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SystemExtras/Internals.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 2 | import Darwin 3 | #elseif os(Linux) || os(FreeBSD) || os(Android) 4 | import Glibc 5 | #elseif os(Windows) 6 | import ucrt 7 | import WinSDK 8 | #else 9 | #error("Unsupported Platform") 10 | #endif 11 | 12 | import SystemPackage 13 | 14 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 15 | internal var system_errno: CInt { 16 | get { Darwin.errno } 17 | set { Darwin.errno = newValue } 18 | } 19 | #elseif os(Windows) 20 | internal var system_errno: CInt { 21 | get { 22 | var value: CInt = 0 23 | _ = ucrt._get_errno(&value) 24 | return value 25 | } 26 | set { 27 | _ = ucrt._set_errno(newValue) 28 | } 29 | } 30 | #else 31 | internal var system_errno: CInt { 32 | get { Glibc.errno } 33 | set { Glibc.errno = newValue } 34 | } 35 | #endif 36 | 37 | #if os(Windows) 38 | #else 39 | typealias system_stat_struct = stat 40 | func system_stat(_ path: UnsafePointer, _ result: inout stat) -> CInt { 41 | stat(path, &result) 42 | } 43 | func system_lstat(_ path: UnsafePointer, _ result: inout stat) -> CInt { 44 | lstat(path, &result) 45 | } 46 | #endif 47 | 48 | #if os(Windows) 49 | func system_getcwd() -> UnsafeMutablePointer? { 50 | _wgetcwd(nil, 0) 51 | } 52 | 53 | func system_chdir(_ path: UnsafePointer) -> CInt { 54 | _wchdir(path) 55 | } 56 | #else 57 | func system_getcwd() -> UnsafeMutablePointer? { 58 | getcwd(nil, 0) 59 | } 60 | func system_chdir(_ path: UnsafePointer) -> CInt { 61 | chdir(path) 62 | } 63 | #endif 64 | 65 | #if os(Windows) 66 | func system_mkdir(_ path: UnsafePointer, _ mode: CInterop.Mode) -> CInt { 67 | _wmkdir(path) 68 | } 69 | #else 70 | func system_mkdir(_ path: UnsafePointer, _ mode: CInterop.Mode) -> CInt { 71 | mkdir(path, mode) 72 | } 73 | #endif 74 | 75 | #if os(Windows) 76 | func system_getenv(_ name: UnsafePointer) -> UnsafeMutablePointer? { 77 | _wgetenv(name) 78 | } 79 | #else 80 | func system_getenv(_ name: UnsafePointer) -> UnsafeMutablePointer? { 81 | getenv(name) 82 | } 83 | #endif 84 | 85 | #if os(Windows) 86 | func system_rmdir(_ path: UnsafePointer) -> CInt { 87 | _wrmdir(path) 88 | } 89 | #else 90 | func system_rmdir(_ path: UnsafePointer) -> CInt { 91 | rmdir(path) 92 | } 93 | #endif 94 | 95 | #if os(Windows) 96 | func system_unlink(_ path: UnsafePointer) -> CInt { 97 | _wunlink(path) 98 | } 99 | #else 100 | func system_unlink(_ path: UnsafePointer) -> CInt { 101 | unlink(path) 102 | } 103 | #endif 104 | 105 | #if os(Windows) 106 | #else 107 | func system_chmod(_ path: UnsafePointer, _ mode: CInterop.Mode) -> CInt { 108 | chmod(path, mode) 109 | } 110 | #endif 111 | 112 | #if os(Windows) 113 | #else 114 | enum Constants { 115 | static let maxPathLength = Int(PATH_MAX) 116 | } 117 | 118 | func system_readlink(_ path: UnsafePointer, _ buffer: UnsafeMutablePointer, _ size: Int) -> Int { 119 | readlink(path, buffer, size) 120 | } 121 | 122 | func system_symlink(_ source: UnsafePointer, _ target: UnsafePointer) -> CInt { 123 | symlink(source, target) 124 | } 125 | #endif 126 | 127 | #if os(Windows) 128 | func system_rename(_ source: UnsafePointer, _ target: UnsafePointer) -> CInt { 129 | if !MoveFileW(source, target) { 130 | let _ = GetLastError() 131 | // TODO: map WinAPI error to errno when Swift Systems does 132 | return -1 133 | } 134 | 135 | return 0 136 | } 137 | #else 138 | func system_rename(_ source: UnsafePointer, _ target: UnsafePointer) -> CInt { 139 | rename(source, target) 140 | } 141 | #endif 142 | -------------------------------------------------------------------------------- /Sources/SystemExtras/Permissions.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | 3 | /// OS agnostic permissions for a file path. 4 | public protocol Permissions { 5 | /// Whether the file is read only. NOTE: setting this value does not change the permission of 6 | /// the path on file system. Use `Path.set(_:)` with the updated value to achieve that. 7 | var isReadOnly: Bool { get set } 8 | } 9 | 10 | extension FilePermissions: Permissions { 11 | public var isReadOnly: Bool { 12 | get { 13 | !contains(.ownerWrite) 14 | } 15 | 16 | set { 17 | if newValue { 18 | remove(.ownerWrite) 19 | } else { 20 | insert(.ownerWrite) 21 | } 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Sources/SystemExtras/ReadWrite.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | 3 | extension FilePath { 4 | @discardableResult 5 | func whileOpen( 6 | _ mode: FileDescriptor.AccessMode, 7 | options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), 8 | permissions: FilePermissions? = nil, 9 | retryOnInterrupt: Bool = true, 10 | run action: (FileDescriptor) throws -> Result) throws -> Result 11 | { 12 | let fd = try FileDescriptor.open( 13 | self, 14 | mode, 15 | options: options, 16 | permissions: permissions, 17 | retryOnInterrupt: retryOnInterrupt 18 | ) 19 | let result = try action(fd) 20 | try fd.close() 21 | return result 22 | } 23 | } 24 | 25 | extension FilePath { 26 | /// Read from a normal file. 27 | /// 28 | /// - Returns: binary content of the normal file. 29 | public func readBytes( 30 | fromAbsoluteOffset offset: Int64 = 0 31 | ) throws -> [UInt8] 32 | { 33 | let meta = try self.metadata() 34 | guard meta.fileType.isFile else { // TODO: Deal with symlink? 35 | return [] 36 | } 37 | 38 | let requestSize = meta.size - Int64(offset) 39 | return try self.whileOpen(.readOnly) { fd in 40 | try Array(unsafeUninitializedCapacity: Int(requestSize)) { buffer, count in 41 | count = try fd.read(fromAbsoluteOffset: offset, into: UnsafeMutableRawBufferPointer(buffer)) 42 | } 43 | } 44 | } 45 | 46 | /// Read string from a normal file. 47 | /// 48 | /// - Parameter encoding: The encoding to use to decode the file's content. 49 | /// 50 | /// - Returns: The file's content decoded using `encoding`. 51 | public func readString(as encoding: Encoding.Type) throws -> String 52 | where Encoding: _UnicodeEncoding 53 | { 54 | try readBytes().withUnsafeBytes { rawBytes in 55 | String(decoding: rawBytes.bindMemory(to: Encoding.CodeUnit.self), as: Encoding.self) 56 | } 57 | } 58 | 59 | /// Read string from a normal file decoded using UTF-8. 60 | /// 61 | /// - Returns: The file's content as a String. 62 | public func readUTF8String() throws -> String { 63 | try self.readString(as: UTF8.self) 64 | } 65 | 66 | 67 | public func write( 68 | _ bytes: Bytes, 69 | options: FileDescriptor.OpenOptions = [.create, .truncate], 70 | permissions: FilePermissions = [.ownerWrite, .ownerRead, .groupRead, .groupWrite, .otherRead] 71 | ) throws where Bytes: Sequence, Bytes.Element == UInt8 { 72 | try self.whileOpen(.writeOnly, options: options, permissions: permissions) { fd in 73 | try fd.writeAll(bytes) 74 | } 75 | } 76 | 77 | public func write( 78 | utf8 string: String, 79 | options: FileDescriptor.OpenOptions = [.create, .truncate], 80 | permissions: FilePermissions = [.ownerWrite, .ownerRead, .groupRead, .groupWrite, .otherRead] 81 | ) throws 82 | { 83 | try string.withCString { signedPtr in 84 | try signedPtr.withMemoryRebound(to: UInt8.self, capacity: 1) { unsignedPtr in 85 | let buffer: UnsafeBufferPointer = UnsafeBufferPointer(start: unsignedPtr, count: string.utf8.count) 86 | try self.write(buffer, options: options, permissions: permissions) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/SystemExtras/Symlink.swift: -------------------------------------------------------------------------------- 1 | import CSystemExtras 2 | import SystemPackage 3 | 4 | #if os(Windows) 5 | import WinSDK 6 | #endif // os(Windows) 7 | 8 | extension FilePath { 9 | /// Create a symbolic link to `self`. 10 | /// 11 | /// - Parameter path: The path at which to create the symlink. 12 | public func readSymlink() throws -> FilePath { 13 | #if os(Windows) 14 | // Warning: intense Windows/C wacky-ness ahead. 15 | // 16 | // DeviceIoControl returns multiple type of structs, depending on which one you ask for via one of 17 | // its parameters. Here, we ask for a FSCTL_GET_REPARSE_POINT. This returns an REPARSE_DATA_BUFFER: 18 | // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer 19 | // 20 | // Because this guy is from a exotic place (NT kernel), we include a copy* of it as part of this 21 | // porject. 22 | // 23 | // REPARSE_DATA_BUFFER is a C struct that uses a few C features that Swift does not directly support. 24 | // 25 | // First of all, it has a union member with each option being structs with different content. We 26 | // include a copy of a struct with the union member flattened as direct members for each. 27 | // 28 | // Two of these options each has a "flexible array member". The last member of these structs is a 29 | // storage for the first element in an array. 30 | // 31 | // `DeviceIoControl` tells us the size of REPARSE_DATA_BUFFER, including the flexible array member. 32 | // To read data from this flexible array, we first skip the fixed potion of `REPARSE_DATA_BUFFER` from 33 | // the raw buffer returned by `DeviceIoControl`. Then, we cast the raw buffer to array whos element is 34 | // the same as the flexible array member's element (wchar_t). Since we know the first element is in 35 | // the fixed potion of `REPARSE_DATA_BUFFER`, we substract 1 step from the newly casted buffer, and 36 | // read its content from there. 37 | // 38 | // Each of the union member in `REPARSE_DATA_BUFFER` also contains content size of the flexible array. 39 | // We use that information as well when reading from it. 40 | try withHandle( 41 | access: 0, 42 | disposition: OPEN_EXISTING, 43 | attributes: FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS 44 | ) { handle in 45 | let data = try ContiguousArray(unsafeUninitializedCapacity: 16 * 1024) { buffer, count in 46 | var size: DWORD = 0 47 | if !DeviceIoControl( 48 | handle, 49 | FSCTL_GET_REPARSE_POINT, 50 | nil, 51 | 0, 52 | buffer.baseAddress, 53 | DWORD(buffer.count), 54 | &size, 55 | nil 56 | ) { 57 | throw Errno(rawValue: -1) 58 | } 59 | count = Int(size) 60 | } 61 | 62 | return try withUnsafePointer(to: data) { 63 | try $0.withMemoryRebound(to: [ReparseDataBuffer].self, capacity: 1) { reparseDataBuffer -> FilePath in 64 | guard let reparseData = reparseDataBuffer.pointee.first else { 65 | throw Errno(rawValue: -1) 66 | } 67 | 68 | let nameStartingPoint: Int 69 | let nameLength = Int(reparseData.printNameLength) / MemoryLayout.stride 70 | let nameOffset = Int(reparseData.printNameOffset) 71 | if reparseData.reparseTag == IO_REPARSE_TAG_SYMLINK { 72 | nameStartingPoint = (MemoryLayout.stride - 4 + nameOffset) / MemoryLayout.stride 73 | } else if reparseData.reparseTag == IO_REPARSE_TAG_MOUNT_POINT { 74 | nameStartingPoint = (MemoryLayout.stride - 4 + nameOffset) / MemoryLayout.stride 75 | } else { 76 | throw Errno(rawValue: -1) 77 | } 78 | 79 | return withUnsafePointer(to: data) { 80 | $0.withMemoryRebound(to: [UInt16].self, capacity: data.count / 2) { wideData in 81 | let platformString = Array(wideData.pointee[nameStartingPoint ..< (nameStartingPoint + nameLength)]) 82 | let result = FilePath(platformString: platformString + [0]) 83 | return result 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | #else // os(Windows) 91 | guard try self.metadata().fileType.isSymlink else { 92 | return self 93 | } 94 | 95 | let nullTerminated = try Array( 96 | unsafeUninitializedCapacity: Constants.maxPathLength + 1 97 | ) { buffer, count in 98 | try self.withPlatformString { cString in 99 | let length = Int( 100 | system_readlink(cString, buffer.baseAddress!, Constants.maxPathLength) 101 | ) 102 | if length == -1 { 103 | throw Errno(rawValue: system_errno) 104 | } 105 | 106 | buffer[length] = 0 107 | count = length + 1 108 | } 109 | } 110 | 111 | return FilePath(platformString: nullTerminated) 112 | #endif // os(Windows) 113 | } 114 | 115 | /// Create a symbolic link to self at `path`. 116 | /// 117 | /// - Parameter path: The path at which to create the symlink. 118 | public func makeSymlink(at path: FilePath) throws { 119 | #if os(Windows) 120 | let flag: DWORD = try metadata().fileType.isDirectory 121 | ? DWORD(SYMBOLIC_LINK_FLAG_DIRECTORY) 122 | : 0 123 | try self.withPlatformString { selfCString in 124 | try path.withPlatformString { pathCString in 125 | if CreateSymbolicLinkW(pathCString, selfCString, flag) == 0 { 126 | throw Errno(rawValue: -1) 127 | } 128 | } 129 | } 130 | #else // os(Windows) 131 | try self.withPlatformString { selfCString in 132 | try path.withPlatformString { pathCString in 133 | if system_symlink(selfCString, pathCString) != 0 { 134 | throw Errno(rawValue: system_errno) 135 | } 136 | } 137 | } 138 | #endif // os(Windows) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/SystemExtras/Temprary.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | 3 | extension FilePath { 4 | /// Search for a temporary directory suitable as the default temporary directory. 5 | /// 6 | /// A suitable location is in one of the candidate list and allows write permission for this process. 7 | /// 8 | /// The list of candidate locations to consider are the following: 9 | /// * Location defined as the TMPDIR environment variable. 10 | /// * Location defined as the TMP environment variable. 11 | /// * Location defined as the TMPDIR environment variable. 12 | /// * /tmp 13 | /// * /var/tmp 14 | /// * /usr/tmp 15 | /// Location defined as the HOME or USERPROFILE environment variable. 16 | /// * Current working directory. 17 | /// 18 | /// - Returns: A suitable default temporary directory. 19 | public static func searchForDefaultTemporaryDirectory() -> Self { 20 | for envName in ["TMPDIR", "TMP", "TEMP"] { 21 | guard let envvar = envName.withPlatformString({ system_getenv($0) }), 22 | case let path = FilePath(platformString: envvar), 23 | !path.isEmpty 24 | else { 25 | continue 26 | } 27 | 28 | if let meta = try? path.metadata(), !meta.permissions.isReadOnly { 29 | return path 30 | } 31 | } 32 | 33 | for tmpPath in ["/tmp", "/var/tmp", "/usr/tmp"] { 34 | let path = tmpPath.withPlatformString(FilePath.init(platformString:)) 35 | if let meta = try? path.metadata(), !meta.permissions.isReadOnly { 36 | return path 37 | } 38 | } 39 | 40 | for envName in ["HOME", "USERPROFILE"] { 41 | guard let envvar = envName.withPlatformString({ system_getenv($0) }), 42 | case let path = FilePath(platformString: envvar), 43 | path.isEmpty 44 | else { 45 | continue 46 | } 47 | 48 | if let meta = try? path.metadata(), !meta.permissions.isReadOnly { 49 | return path 50 | } 51 | } 52 | 53 | return (try? .workingDirectory()) ?? FilePath(".") 54 | } 55 | 56 | /// The default temporary used Pathos uses. Its default value is computed by 57 | /// `.searchForDefaultTemporaryDirectory`. If this value is set to `/x/y/z`, then functions such as 58 | /// `FilePath.makeTemporaryDirectory()` will create its result in `/x/y/z`. 59 | public static var defaultTemporaryDirectory = Self.searchForDefaultTemporaryDirectory() 60 | 61 | private static func constructTemporaryPath(prefix: String = "", suffix: String = "") -> FilePath { 62 | defaultTemporaryDirectory.appending("\(prefix)\(UInt64.random(in: 0 ... .max))\(suffix)") 63 | } 64 | 65 | /// Make a temporary directory with write access. 66 | /// 67 | /// The parent of the return value is the current value of `Path.defaultTemporaryDirectory`. It will have 68 | /// a randomized name. A prefix and a suffix can be optionally specified as options. 69 | /// 70 | /// - Parameters: 71 | /// - prefix: A prefix for the temporary directories's name. 72 | /// - suffix: A suffix for the temporary directories's name. 73 | /// 74 | /// - Returns: A temporary directory. 75 | public static func makeTemporaryDirectory(prefix: String = "", suffix: String = "") throws -> FilePath { 76 | let path = constructTemporaryPath(prefix: prefix, suffix: suffix) 77 | try path.makeDirectory() 78 | return path 79 | } 80 | 81 | /// Execute some code with a temporarily created directory as the current working directory. 82 | /// 83 | /// This method does the following: 84 | /// 85 | /// 1. make a temporary directory with write access (`Path.makeTemporaryDirectory`) 86 | /// 2. set the directory from previous step as the current working directory. 87 | /// 3. execute a closure as supplied as argument. 88 | /// 4. reset the current working directory. 89 | /// 5. delete the temporary directory along with its content. 90 | /// 91 | /// - Parameter action: The closure to execute in the temporary environment. The temporary directory is 92 | /// sent as a parameter for the action closure. 93 | public static func withTemporaryDirectory(run action: @escaping (FilePath) throws -> Void) throws { 94 | let temporaryDirectory = try makeTemporaryDirectory() 95 | try temporaryDirectory.asWorkingDirectory { 96 | try action(temporaryDirectory) 97 | } 98 | 99 | try temporaryDirectory.delete(recursive: true) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/SystemExtras/Windows.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | import SystemPackage 3 | import WinSDK 4 | extension FilePath { 5 | func withHandle(access: Int32, disposition: Int32, attributes: Int32, run action: (HANDLE?) throws -> Output) throws -> Output { 6 | try self.withPlatformString { cString in 7 | let handle = CreateFileW( 8 | cString, 9 | DWORD(access), 10 | 0, 11 | nil, 12 | DWORD(disposition), 13 | DWORD(attributes), 14 | nil 15 | ) 16 | 17 | if handle == INVALID_HANDLE_VALUE { 18 | throw Errno(rawValue: -1) 19 | } 20 | 21 | defer { 22 | CloseHandle(handle) 23 | } 24 | 25 | return try action(handle) 26 | } 27 | } 28 | 29 | func windowsFinalName() throws -> Self { 30 | try withHandle( 31 | access: 0, 32 | disposition: OPEN_EXISTING, 33 | attributes: FILE_FLAG_BACKUP_SEMANTICS 34 | ) { handle in 35 | let platformString = try Array( 36 | unsafeUninitializedCapacity: Int(MAX_PATH) 37 | ) { buffer, count in 38 | let length = Int( 39 | GetFinalPathNameByHandleW( 40 | handle, 41 | buffer.baseAddress, 42 | DWORD(MAX_PATH), 43 | DWORD(FILE_NAME_OPENED) 44 | ) 45 | ) 46 | 47 | if length == 0 { 48 | throw Errno(rawValue: -1) 49 | } 50 | 51 | buffer[length] = 0 52 | count = length + 1 53 | } 54 | 55 | return FilePath(platformString: platformString) 56 | } 57 | } 58 | } 59 | #endif 60 | -------------------------------------------------------------------------------- /Sources/SystemExtras/WindowsAttributes.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | 3 | import WinSDK 4 | 5 | public struct WindowsAttributes: OptionSet { 6 | // MARK: - OptionSet 7 | 8 | /// Attributes from Windows API. E.g. `WIN32_FIND_DATAW.dwFileAttributes'. 9 | public var rawValue: DWORD 10 | 11 | public init(rawValue: DWORD) { 12 | self.rawValue = rawValue 13 | } 14 | 15 | // MARK: - 16 | 17 | /// A file or directory that is an archive file or directory. Applications typically use this 18 | /// attribute to mark files for backup or removal . 19 | public static let archive = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_ARCHIVE)) 20 | /// A file or directory that is compressed. For a file, all of the data in the file is 21 | /// compressed. For a directory, compression is the default for newly created files and 22 | /// subdirectories. 23 | public static let compressed = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_COMPRESSED)) 24 | /// This value is reserved for system use. 25 | public static let device = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_DEVICE)) 26 | /// The handle that identifies a directory. 27 | public static let directory = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_DIRECTORY)) 28 | /// A file or directory that is encrypted. For a file, all data streams in the file are 29 | /// encrypted. For a directory, encryption is the default for newly created files and 30 | /// subdirectories. 31 | public static let encrypted = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_ENCRYPTED)) 32 | /// The file or directory is hidden. It is not included in an ordinary directory listing. 33 | public static let hidden = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_HIDDEN)) 34 | /// The directory or user data stream is configured with integrity (only supported on ReFS 35 | /// volumes). It is not included in an ordinary directory listing. The integrity setting persists 36 | /// with the file if it's renamed. If a file is copied the destination file will have integrity 37 | /// set if either the source file or destination directory have integrity set. 38 | public static let integrityStream = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_INTEGRITY_STREAM)) 39 | /// A file that does not have other attributes set. This attribute is valid only when used alone. 40 | public static let normal = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_NORMAL)) 41 | /// The file or directory is not to be indexed by the content indexing service. 42 | public static let notContentIndexed = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)) 43 | /// The user data stream not to be read by the background data integrity scanner (AKA scrubber). 44 | /// When set on a directory it only provides inheritance. This flag is only supported on Storage 45 | /// Spaces and ReFS volumes. It is not included in an ordinary directory listing. 46 | public static let noScrubData = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_NO_SCRUB_DATA)) 47 | /// The data of a file is not available immediately. This attribute indicates that the file data is physically moved to offline storage. This attribute is used by Remote Storage, which is the hierarchical storage management software. Applications should not arbitrarily change this attribute. 48 | public static let offline = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_OFFLINE)) 49 | /// A file that is read-only. Applications can read the file, but cannot write to it or delete 50 | /// it. This attribute is not honored on directories. For more information, see You cannot view 51 | /// or change the Read-only or the System attributes of folders in Windows Server 2003, in 52 | /// Windows XP, in Windows Vista or in Windows 7. 53 | public static let readOnly = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_READONLY)) 54 | /// When this attribute is set, it means that the file or directory is not fully present 55 | /// locally. For a file that means that not all of its data is on local storage (e.g. it may be 56 | /// sparse with some data still in remote storage). For a directory it means that some of the 57 | /// directory contents are being virtualized from another location. Reading the file / enumerating 58 | /// the directory will be more expensive than normal, e.g. it will cause at least some of the 59 | /// file/directory content to be fetched from a remote store. Only kernel-mode callers can set 60 | /// this bit. 61 | public static let recallOnDataAccess = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS)) 62 | /// This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, 63 | /// FILE_BOTH_DIR_INFORMATION, etc.). When this attribute is set, it means that the file or 64 | /// directory has no physical representation on the local system; the item is virtual. Opening the 65 | /// item will be more expensive than normal, e.g. it will cause at least some of it to be fetched 66 | /// from a remote store. 67 | public static let recallOnOpen = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_RECALL_ON_OPEN)) 68 | /// A file or directory that has an associated reparse point, or a file that is a symbolic link. 69 | public static let reparsePoint = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_REPARSE_POINT)) 70 | /// A file that is a sparse file. 71 | public static let sparseFile = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_SPARSE_FILE)) 72 | /// A file or directory that the operating system uses a part of, or uses exclusively. 73 | public static let system = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_SYSTEM)) 74 | /// A file that is being used for temporary storage. File systems avoid writing data back to 75 | /// mass storage if sufficient cache memory is available, because typically, an application 76 | /// deletes a temporary file after the handle is closed. In that scenario, the system can entirely 77 | /// avoid writing the data. Otherwise, the data is written after the handle is closed. 78 | public static let temporary = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_TEMPORARY)) 79 | /// This value is reserved for system use. 80 | public static let virtual = WindowsAttributes(rawValue: DWORD(FILE_ATTRIBUTE_VIRTUAL)) 81 | } 82 | 83 | extension WindowsAttributes: Permissions { 84 | public var isReadOnly: Bool { 85 | get { 86 | contains(.readOnly) 87 | } 88 | 89 | set { 90 | if newValue { 91 | insert(.readOnly) 92 | } else { 93 | remove(.readOnly) 94 | } 95 | } 96 | } 97 | } 98 | #endif // os(Windows) 99 | -------------------------------------------------------------------------------- /Sources/SystemExtras/WorkingDirectory.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | 3 | extension FilePath { 4 | public static func workingDirectory() throws -> Self { 5 | if let buffer = system_getcwd() { 6 | return FilePath(platformString: buffer) 7 | } 8 | 9 | throw Errno(rawValue: system_errno) 10 | } 11 | 12 | public static func setWorkingDirectory(_ path: FilePath) throws { 13 | try path.withPlatformString { cString in 14 | if system_chdir(cString) != 0 { 15 | throw Errno(rawValue: system_errno) 16 | } 17 | } 18 | } 19 | 20 | /// Set `self` as the current working directory (cwd), run `action`, and 21 | /// restore the original current working directory after `action` finishes. 22 | /// 23 | /// - Parameter action: A closure that runs with `self` being the current 24 | /// working directory. 25 | public func asWorkingDirectory(running action: @escaping () throws -> Void) throws { 26 | let currentDirectory = try FilePath.workingDirectory() 27 | try FilePath.setWorkingDirectory(self) 28 | try action() 29 | try FilePath.setWorkingDirectory(currentDirectory) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/CopyFileTests.swift: -------------------------------------------------------------------------------- 1 | import SystemExtras 2 | import SystemPackage 3 | import XCTest 4 | 5 | final class CopyFileTests: XCTestCase { 6 | func testCopyingNormalFileToNewLocation() throws { 7 | try FilePath.withTemporaryDirectory { _ in 8 | let content = "xyyyzz" 9 | let sourceFile = FilePath("a") 10 | try sourceFile.write(utf8: content) 11 | let destinationFile = FilePath("b") 12 | try sourceFile.copyFile(to: destinationFile) 13 | XCTAssertEqual(try destinationFile.readUTF8String(), content) 14 | } 15 | } 16 | 17 | func testCopyingNormalFileToNewLocationFollowingSymlink() throws { 18 | try FilePath.withTemporaryDirectory { _ in 19 | let content = "aoesubaoesurcaoheu" 20 | let sourceFile = FilePath("a") 21 | try sourceFile.write(utf8: content) 22 | let link = FilePath("b") 23 | try sourceFile.makeSymlink(at: link) 24 | let destinationFile = FilePath("c") 25 | try link.copyFile(to: destinationFile) 26 | XCTAssertEqual(try destinationFile.readUTF8String(), content) 27 | } 28 | } 29 | 30 | func testCopyingSymlinkToNewLocation() throws { 31 | try FilePath.withTemporaryDirectory { _ in 32 | let content = "aoesubaoesurcaoheu" 33 | let sourceFile = FilePath("a") 34 | try sourceFile.write(utf8: content) 35 | let link = FilePath("b") 36 | try sourceFile.makeSymlink(at: link) 37 | let destinationFile = FilePath("c") 38 | try link.copyFile(to: destinationFile, followSymlink: false) 39 | XCTAssertEqual(try destinationFile.readSymlink(), sourceFile) 40 | } 41 | } 42 | 43 | func testCopyingToExistingLocationNotFailingExisting() throws { 44 | try FilePath.withTemporaryDirectory { _ in 45 | let sourceContent = "aaa" 46 | let sourceFile = FilePath("a") 47 | let destinationFile = FilePath("b") 48 | 49 | try sourceFile.write(utf8: sourceContent) 50 | try destinationFile.write(utf8: "bbb") 51 | 52 | try sourceFile.copyFile(to: destinationFile) 53 | XCTAssertEqual(try destinationFile.readUTF8String(), sourceContent) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/DirectoryContentTests.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import SystemExtras 3 | import XCTest 4 | 5 | final class DirectoryContentTests: XCTestCase { 6 | func testDirectoryContentSequence() throws { 7 | let thisFile: FilePath = #file 8 | XCTAssert( 9 | Set( 10 | DirectoryContent(of: thisFile.removingLastComponent(), recursive: true) 11 | .map { $0.0 } 12 | ).contains(thisFile) 13 | ) 14 | } 15 | 16 | func testDirectoryContentLooping() throws { 17 | let thisFile: FilePath = #file 18 | var foundThis = false 19 | 20 | for (path, type) in thisFile.removingLastComponent().directoryContent() { 21 | if path == thisFile, type.isFile { 22 | foundThis = true 23 | } 24 | } 25 | 26 | XCTAssertTrue(foundThis) 27 | } 28 | 29 | func testRecursiveFollowingSymlink() throws { 30 | try FilePath.withTemporaryDirectory { _ in 31 | try FilePath("a").write(utf8: "") 32 | let b = FilePath("b") 33 | try b.makeDirectory() 34 | let be = b.appending("e") 35 | try be.makeDirectory() 36 | try be.appending("f").write(utf8: "") 37 | try b.appending("c").write(utf8: "") 38 | try b.makeSymlink(at: "d") 39 | let names = Set(FilePath(".").directoryContent(recursive: true, followSymlink: true).map(\.0)) 40 | XCTAssertEqual( 41 | names, 42 | [ 43 | FilePath(".").appending("a"), 44 | FilePath(".").appending("b"), 45 | FilePath(".").appending("b").appending("c"), 46 | FilePath(".").appending("b").appending("e"), 47 | FilePath(".").appending("b").appending("e").appending("f"), 48 | FilePath(".").appending("d"), 49 | FilePath(".").appending("d").appending("c"), 50 | FilePath(".").appending("d").appending("e"), 51 | FilePath(".").appending("d").appending("e").appending("f"), 52 | ] 53 | ) 54 | 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/ExistsTests.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import SystemExtras 3 | import XCTest 4 | 5 | final class ExistsTests: XCTestCase { 6 | func testFileAndDirectoryExists() throws { 7 | let file: FilePath = #file 8 | XCTAssertTrue(file.exists()) 9 | XCTAssertTrue(file.removingLastComponent().exists()) 10 | XCTAssertFalse(file.appending("IDontExist").exists()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/MetadataTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SystemExtras 2 | import SystemPackage 3 | import XCTest 4 | 5 | final class MetadataTests: XCTestCase { 6 | func testPermissions() throws { 7 | let path: FilePath = #file 8 | XCTAssert(try !path.metadata().permissions.isReadOnly) 9 | } 10 | 11 | func testType() throws { 12 | let path: FilePath = #file 13 | XCTAssertTrue(try path.metadata().fileType.isFile) 14 | XCTAssertTrue(try path.removingLastComponent().metadata().fileType.isDirectory) 15 | } 16 | 17 | func testSize() throws { 18 | let path: FilePath = #file 19 | XCTAssert(try path.metadata().size > 0) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/MoveTests.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import XCTest 3 | 4 | final class MoveTests: XCTestCase { 5 | func testMoveFile() throws { 6 | try FilePath.withTemporaryDirectory { temp in 7 | let expectedContent = "Hello" 8 | let aPath = temp.appending("a") 9 | let bPath = temp.appending("b") 10 | try aPath.write(utf8: expectedContent) 11 | 12 | XCTAssert(aPath.exists()) 13 | XCTAssertFalse(bPath.exists()) 14 | 15 | try aPath.move(to: bPath) 16 | 17 | XCTAssertFalse(aPath.exists()) 18 | XCTAssert(bPath.exists()) 19 | 20 | let content = try bPath.readUTF8String() 21 | XCTAssertEqual(content, expectedContent) 22 | } 23 | } 24 | 25 | func testMoveEmptyDirectory() throws { 26 | try FilePath.withTemporaryDirectory { temp in 27 | let aPath = temp.appending("a") 28 | let bPath = temp.appending("b") 29 | try aPath.makeDirectory() 30 | 31 | XCTAssert(aPath.exists()) 32 | XCTAssertFalse(bPath.exists()) 33 | 34 | try aPath.move(to: bPath) 35 | 36 | XCTAssertFalse(aPath.exists()) 37 | XCTAssert(bPath.exists()) 38 | 39 | XCTAssert(try bPath.metadata().fileType.isDirectory) 40 | } 41 | } 42 | 43 | func testMoveDirectory() throws { 44 | let expectedContent = "Hello" 45 | try FilePath.withTemporaryDirectory { temp in 46 | let aDirectoryPath = temp.appending("a") 47 | let bDirectoryPath = temp.appending("b") 48 | let cInAPath = aDirectoryPath.appending("c") 49 | let cInBPath = bDirectoryPath.appending("c") 50 | 51 | try aDirectoryPath.makeDirectory() 52 | try cInAPath.write(utf8: expectedContent) 53 | 54 | XCTAssert(aDirectoryPath.exists()) 55 | XCTAssert(cInAPath.exists()) 56 | XCTAssertFalse(bDirectoryPath.exists()) 57 | 58 | try aDirectoryPath.move(to: bDirectoryPath) 59 | 60 | XCTAssertFalse(aDirectoryPath.exists()) 61 | XCTAssertFalse(cInAPath.exists()) 62 | XCTAssert(bDirectoryPath.exists()) 63 | XCTAssert(cInBPath.exists()) 64 | 65 | XCTAssert(try bDirectoryPath.metadata().fileType.isDirectory) 66 | let content = try cInBPath.readUTF8String() 67 | XCTAssertEqual(content, expectedContent) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/Permissions.swift: -------------------------------------------------------------------------------- 1 | import SystemExtras 2 | import SystemPackage 3 | import XCTest 4 | 5 | final class PermissionsTests: XCTestCase { 6 | #if os(Windows) 7 | func testSettingPermissions() throws { 8 | try FilePath.withTemporaryDirectory { temp in 9 | let file = temp.pushing("a") 10 | try file.write(utf8: "hello") 11 | let oldPermissions = try file.metadata().permissions as! WindowsAttributes 12 | XCTAssertFalse(oldPermissions.contains(.readOnly)) 13 | try file.set(oldPermissions.union([.readOnly])) 14 | let newPermissions = try file.metadata().permissions as! WindowsAttributes 15 | XCTAssert(newPermissions.contains(.readOnly)) 16 | // Restore the write permission so we can clean it up 17 | try file.set(oldPermissions) 18 | } 19 | } 20 | #else 21 | func testSettingPermissions() throws { 22 | try FilePath.withTemporaryDirectory { temp in 23 | let file = temp.pushing("a") 24 | try file.write(utf8: "hello") 25 | let oldPermissions = try file.metadata().permissions as! FilePermissions 26 | XCTAssert(oldPermissions.contains(.ownerWrite)) 27 | try file.set(oldPermissions.subtracting([.ownerWrite])) 28 | let newPermissions = try file.metadata().permissions as! FilePermissions 29 | XCTAssertFalse(newPermissions.contains(.ownerWrite)) 30 | } 31 | } 32 | #endif // !os(Windows) 33 | } 34 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/ReadWriteTests.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import SystemExtras 3 | import XCTest 4 | 5 | final class ReadWriteTests: XCTestCase { 6 | func testSimpleReadWriteBytes() throws { 7 | let bytes: [UInt8] = [1, 2, 3, 4, 1] 8 | try FilePath.withTemporaryDirectory { temp in 9 | let target = temp.appending("a") 10 | XCTAssertFalse(target.exists()) 11 | try target.write(bytes) 12 | XCTAssert(target.exists()) 13 | 14 | let readBytes = try target.readBytes() 15 | XCTAssertEqual(readBytes, bytes) 16 | } 17 | } 18 | 19 | func testSimpleReadWriteUTF8String() throws { 20 | let s = "Hello, System!" 21 | try FilePath.withTemporaryDirectory { temp in 22 | let target = temp.appending("a") 23 | XCTAssertFalse(target.exists()) 24 | try target.write(utf8: s) 25 | XCTAssert(target.exists()) 26 | 27 | let readString = try target.readUTF8String() 28 | XCTAssertEqual(readString, s) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/SymlinkTests.swift: -------------------------------------------------------------------------------- 1 | import SystemExtras 2 | import SystemPackage 3 | import XCTest 4 | 5 | final class SymlinkTests: XCTestCase { 6 | func testMakingAndReadingSymlinksOfFile() throws { 7 | try FilePath.withTemporaryDirectory { temp in 8 | let aPath = temp.appending("a") 9 | let bPath = temp.appending("b") 10 | try aPath.write(utf8: "hello") 11 | 12 | XCTAssert(aPath.exists()) 13 | XCTAssertFalse(bPath.exists()) 14 | 15 | try aPath.makeSymlink(at: bPath) 16 | 17 | XCTAssert(try bPath.metadata().fileType.isSymlink) 18 | 19 | let bLink = try bPath.readSymlink() 20 | XCTAssertEqual(bLink, aPath) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/TemporaryTests.swift: -------------------------------------------------------------------------------- 1 | import SystemPackage 2 | import SystemExtras 3 | import XCTest 4 | 5 | final class DirectoryTests: XCTestCase { 6 | func testAsTemporaryDirectory() throws { 7 | try FilePath.withTemporaryDirectory { temp in 8 | XCTAssert(temp.exists()) 9 | XCTAssert(try temp.metadata().fileType.isDirectory) 10 | } 11 | } 12 | 13 | func testTemporaryIsWritable() throws { 14 | try FilePath.withTemporaryDirectory { temp in 15 | let target = temp.appending("a") 16 | let fd = try FileDescriptor.open(target, .writeOnly, options: [.create], permissions: [.ownerWrite, .ownerRead]) 17 | _ = try fd.closeAfter { 18 | try withUnsafeBytes(of: "hello") { content in 19 | try fd.write(content) 20 | } 21 | } 22 | try XCTAssert(target.metadata().fileType.isFile) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/SystemExtrasTests/WorkingDirectoryTests.swift: -------------------------------------------------------------------------------- 1 | import SystemExtras 2 | import SystemPackage 3 | import XCTest 4 | 5 | final class WorkingDirectoryTests: XCTestCase { 6 | func testWorkingDirectory() throws { 7 | XCTAssert(!(try FilePath.workingDirectory()).isEmpty) 8 | } 9 | } 10 | --------------------------------------------------------------------------------