├── .drstring.toml ├── .github ├── FUNDING.yml └── workflows │ ├── amazon-linux-2.yml │ ├── macos-11.yml │ ├── ubuntu-bionic.yml │ ├── ubuntu-focal.yml │ └── windows.yml ├── .gitignore ├── .swiftformat ├── CHANGELOG.md ├── CMakeLists.txt ├── Documentation ├── APIs │ ├── Constants.md │ ├── FileTime.md │ ├── FileType.md │ ├── Metadata.md │ ├── NativeEncodingUnit.md │ ├── POSIXConstants.md │ ├── POSIXFileType.md │ ├── POSIXPathConvertible.md │ ├── POSIXPermissions.md │ ├── Path.md │ ├── PathConvertible.md │ ├── Permissions.md │ ├── PurePOSIXPath.md │ ├── PurePath.md │ ├── PureWindowsPath.md │ ├── README.md │ ├── SystemError.md │ ├── WindowsAttributes.md │ ├── WindowsConstants.md │ ├── WindowsFileType.md │ └── WindowsPathConvertible.md ├── README.md ├── UserGuide.md └── design.md ├── Examples ├── CMakeLists.txt ├── lookup │ └── main.swift ├── ls │ └── main.swift ├── mk │ └── main.swift ├── readonly │ └── main.swift └── rm │ └── main.swift ├── LICENSE.md ├── Makefile ├── Package.swift ├── README.md ├── RELEASING.md ├── Resources └── Assets │ ├── Arts.sketch │ └── Banner.png ├── Scripts ├── Dockerfile-testing-linux ├── Package_windows.swift ├── cmake_root │ ├── Sources │ │ └── Pathos │ │ │ └── CMakeLists.txt │ └── Tests │ │ └── CMakeLists.txt ├── docker.sh ├── ensure-swiftformat.sh ├── gen-doc.py ├── githooks │ └── pre-commit ├── test_windows.swift └── update-cmake.py ├── Sources ├── CMakeLists.txt ├── LinuxHelpers │ ├── dummy.c │ └── include │ │ ├── linux_metadata.h │ │ └── module.modulemap ├── Pathos │ ├── Algorithms.swift │ ├── BinaryString.swift │ ├── Box.swift │ ├── CMakeLists.txt │ ├── Constants.swift │ ├── Darwin │ │ ├── Metadata+Darwin.swift │ │ └── Path+Darwin.swift │ ├── FileTime.swift │ ├── FileType.swift │ ├── LazyBoxed.swift │ ├── Linux │ │ ├── Metadata+Glibc.swift │ │ └── Path+Glibc.swift │ ├── Metadata.swift │ ├── POSIX │ │ ├── FileTime+POSIX.swift │ │ ├── POSIXConstants.swift │ │ ├── POSIXFileType.swift │ │ ├── POSIXPathConvertible.swift │ │ ├── POSIXPermissions.swift │ │ ├── Path+POSIX.swift │ │ ├── PathParts+POSIX.swift │ │ └── PurePOSIXPath.swift │ ├── Path+Joining.swift │ ├── Path+Temporary.swift │ ├── Path.swift │ ├── PathParts.swift │ ├── Permissions.swift │ ├── PurePath.swift │ ├── PurePathRepresentable.swift │ ├── SystemError.swift │ └── Windows │ │ ├── FileTime+Windows.swift │ │ ├── Metadata+Windows.swift │ │ ├── Path+Windows.swift │ │ ├── PathParts+Windows.swift │ │ ├── PureWindowsPath.swift │ │ ├── WindowsAttributes.swift │ │ ├── WindowsConstants.swift │ │ ├── WindowsFileType.swift │ │ └── WindowsPathConvertible.swift └── WindowsHelpers │ ├── CMakeLists.txt │ ├── dummy.c │ └── include │ ├── WindowsHelpers.h │ ├── module.modulemap │ └── reparsedata.h ├── Tests ├── CMakeLists.txt ├── LinuxMain.swift └── PathosTests │ ├── AbsoluteTests.swift │ ├── BaseTests.swift │ ├── ChildrenTests.swift │ ├── CopyTests.swift │ ├── GlobTests.swift │ ├── HomeTests.swift │ ├── MetadataTests.swift │ ├── POSIXBinaryStringTests.swift │ ├── POSIXPartsParsingTests.swift │ ├── POSIXPathInitializationTests.swift │ ├── POSIXPathJoiningTests.swift │ ├── PathDeletionTests.swift │ ├── PathExistsTests.swift │ ├── PathExtensionTests.swift │ ├── PathJoiningOperatorTests.swift │ ├── PathJoiningTests.swift │ ├── PathNormalTests.swift │ ├── PathParentsTests.swift │ ├── PermissionsTests.swift │ ├── PurePOSIXParentTests.swift │ ├── PurePOSIXPathBaseTests.swift │ ├── PurePOSIXPathExtensionTests.swift │ ├── PurePOSIXPathIsAbsoluteTests.swift │ ├── PurePOSIXPathJoiningOperatorTests.swift │ ├── PurePOSIXPathNormalTests.swift │ ├── PurePOSIXPathParentsTests.swift │ ├── PurePOSIXPathRelativeTests.swift │ ├── PurePOSIXPathTests.swift │ ├── PureWindowsExtensionTests.swift │ ├── PureWindowsPathBaseTests.swift │ ├── PureWindowsPathIsAbsoluteTests.swift │ ├── PureWindowsPathJoiningOperatorTests.swift │ ├── PureWindowsPathNormalTests.swift │ ├── PureWindowsPathParentTests.swift │ ├── PureWindowsPathParentsTests.swift │ ├── PureWindowsPathRelativeTests.swift │ ├── PureWindowsPathTests.swift │ ├── ReadStringTests.swift │ ├── ReadTests.swift │ ├── RealTests.swift │ ├── SymlinkTests.swift │ ├── TemporaryTests.swift │ ├── WindowsBinaryStringTests.swift │ ├── WindowsFileTimeTests.swift │ ├── WindowsPartsParsingTests.swift │ ├── WindowsPathJoiningTests.swift │ ├── WorkingDirectoryTests.swift │ ├── WriteTests.swift │ └── XCTestManifests.swift └── cmake └── modules ├── CMakeLists.txt ├── PathosConfig.cmake.in └── SwiftSupport.cmake /.drstring.toml: -------------------------------------------------------------------------------- 1 | parameter-style = "grouped" 2 | needs-separation = ["description"] 3 | column-limit = 110 4 | include = [ 5 | "Sources/**/*.swift" 6 | ] 7 | -------------------------------------------------------------------------------- /.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/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 | strategy: 10 | matrix: 11 | swift: 12 | - 5.6.1 13 | distro: 14 | - amazonlinux2 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Test 18 | run: Scripts/docker.sh Pathos 'swift test -Xswiftc -warnings-as-errors' ${{ matrix.swift }} ${{ matrix.distro }} 19 | -------------------------------------------------------------------------------- /.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 | strategy: 10 | matrix: 11 | xcode: 12 | - 13.2.1 13 | action: 14 | - SwiftPM 15 | - codegen 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Xcode version 19 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 20 | - name: Test 21 | run: make test-${{ matrix.action }} 22 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu-bionic.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu Bionic 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | linux: 7 | name: Ubuntu Bionic 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | swift: 12 | - 5.6.1 13 | distro: 14 | - bionic 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Test 18 | run: Scripts/docker.sh Pathos 'swift test -Xswiftc -warnings-as-errors' ${{ matrix.swift }} ${{ matrix.distro }} 19 | -------------------------------------------------------------------------------- /.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 | strategy: 10 | matrix: 11 | swift: 12 | - 5.6.1 13 | distro: 14 | - focal 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Test 18 | run: Scripts/docker.sh Pathos 'swift test -Xswiftc -warnings-as-errors' ${{ matrix.swift }} ${{ matrix.distro }} 19 | -------------------------------------------------------------------------------- /.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 -Xswiftc -warnings-as-errors 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | tmp 6 | Carthage/ 7 | Pathos.framework.zip 8 | .idea 9 | /build 10 | .swiftpm 11 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --exclude **/XCTestManifests.swift 2 | --exclude tmp 3 | --disable typeSugar 4 | --disable andOperator 5 | --ifdef no-indent 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master 2 | 3 | ## master 4 | 5 | ## 0.4.2 6 | 7 | * Fixed a problem where the `+` operator overload resolves in ambiguity when used on a `Path` and a string 8 | literal. This problem is due to Swift 5.4's behaviral change. 9 | 10 | ## 0.4.1 11 | 12 | Add support for Swift 5.4 13 | 14 | ## 0.4.0 15 | 16 | ### BREAKING CHANGES 17 | 18 | While the functionality of this library remain compatible with previous release, its public API has 19 | gone through a major re-design, making this release significantly source-breaking. 20 | 21 | The goals for the redesign are 22 | 23 | 1. Be more conventional in the Swift ecosystem by eliminating the "free functions". 24 | 2. Make the selection of APIs **cross-platform**, including macOS, Linux, Windows 10, and future 25 | platforms Swift runs on. 26 | 3. Use un-decoded, aka binary representation for the path value internally for efficiency and 27 | correctness reasons. 28 | 29 | ### Experimental Windows 10 Support 30 | 31 | This release includes implementation of all APIs for Windows 10. As Swift on Windows 10 is still 32 | at early stage, Pathos consider its current Windows support experimental. All unit tests pass on 33 | Windows. Pathos includes CMake builds as well as SwiftPM builds (as of this release, SwiftPM support 34 | is tested with WSL). 35 | 36 | ### New 37 | 38 | Documentation rewritten in Markdown format replaces the previously generated documentation. 39 | 40 | ## 0.3.2 41 | 42 | - Fix a bug where `children` always recursively visit its content when following symlinks. 43 | 44 | ## 0.3.1 45 | 46 | - children() can follow symlinks found in the content if `followSymlink` 47 | parameter is set to `True`. 48 | - expand Linux distro support to match that of Swift's. 49 | 50 | ## 0.3.0 51 | 52 | ### Fixes 53 | 54 | - Warnings in Swift 5.3 55 | 56 | ### New 57 | 58 | - Added a new type `Metadata`, which represents information from `stat` (Darwin, 59 | Linux). 60 | - Added API to retrieve `Metadata`. 61 | - `PathRepresentable.set(_:)` (permissions) 62 | 63 | 64 | ### Deprecations 65 | 66 | The following APIs are deprecated in favor of metadata access APIs. 67 | 68 | - `size(atPath)` 69 | - `modificationTime(atPath:)` 70 | - `accessTime(atPath:)` 71 | - `metadataChangeTime(atPath:)` 72 | - `PathRepresentable.size` 73 | - `PathRepresentable.modificationTime` 74 | - `PathRepresentable.accessTime` 75 | - `PathRepresentable.metadataChangeTime` 76 | - `permissions(forPath:)` 77 | - `PathRepresentable.permissions` 78 | 79 | Removed adding/removing permissions in favor of directly setting it: 80 | 81 | - `add(_:forPath:)` 82 | - `remove(_:forPath:)` 83 | - `PathRepresentable.add(_:)` 84 | - `PathRepresentable.remove(_:)` 85 | 86 | Previously symbolic links were referred to as "symbol" or "symbolic link" in 87 | APIs. From this version on, they'll be referred to as "symlink". This resulted 88 | in the following changes: 89 | 90 | - `createSymbolicLink(fromPath:toPath:)` -> `createSymlink(fromPath:toPath:)` 91 | - `PathRepresentable.createSymbolicLink(at:)` -> `PathRepresentable.createSymlink(at:)` 92 | - `readSymbolicLink(atPath:toPath)` -> `readSymlink(atPath:toPath)` 93 | - `PathRepresentable.readSymbolicLink()` -> `PathRepresentable.readSymlink()` 94 | - `FileType.symbolicLink` -> `FileType.symlink` 95 | 96 | Carthage is no longer supported. 97 | 98 | ### Breaking changes 99 | 100 | Previously symbolic links were referred to as "symbol" or "symbolic link" in 101 | APIs. From this version on, they'll be referred to as "symlink". This resulted 102 | in the following breaking changes: 103 | 104 | - `exists(atPath:followSymbol:)` -> `exists(atPath:followSymlink:)` 105 | - `PathRepresentable.exists(followSymbol:)` -> `PathRepresentable.exists(followSymlink:)` 106 | - `copyFile(fromPath:toPath:followSymbolicLink:checkSize:)` -> `copyFile(fromPath:toPath:followSymlink:checkSize:)` 107 | - `PathRepresentable.copy(to:followSymbolicLink:checkSize:)` -> `PathRepresentable.copy(to:followSymlink:checkSize:)` 108 | 109 | ## 0.2.3 110 | 111 | - Re-implemented `children(inPath:recursive)` to fix issue #122 112 | 113 | ## 0.2.2 114 | 115 | - Removed a build warning in Swift 5.2 116 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15.1) 2 | 3 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) 4 | 5 | project(Pathos 6 | LANGUAGES C Swift) 7 | 8 | option(BUILD_EXAMPLES "Build Example Programs" ON) 9 | option(BUILD_SHARED_LIBS "Build shared libraries by default" ON) 10 | if(CMAKE_SYSTEM_NAME STREQUAL Darwin) 11 | option(BUILD_TESTING "build tests by default" NO) 12 | else() 13 | option(BUILD_TESTING "build tests by default" YES) 14 | endif() 15 | 16 | if(CMAKE_VERSION VERSION_LESS 3.16 AND CMAKE_SYSTEM_NAME STREQUAL Windows) 17 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 18 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 19 | else() 20 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 21 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 22 | endif() 23 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 24 | set(CMAKE_Swift_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/swift) 25 | 26 | include(SwiftSupport) 27 | include(CTest) 28 | 29 | add_subdirectory(Sources) 30 | if(BUILD_EXAMPLES) 31 | add_subdirectory(Examples) 32 | endif() 33 | if(BUILD_TESTING) 34 | add_subdirectory(Tests) 35 | endif() 36 | add_subdirectory(cmake/modules) 37 | -------------------------------------------------------------------------------- /Documentation/APIs/Constants.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `Constants` 2 | 3 | ```swift 4 | public typealias Constants 5 | ``` 6 | 7 | This is some constants that users can use to extend Pathos in a consistent manner. On macOS and 8 | Linux, it is aliased to [POSIXConstants][]. On Windows, it is [WindowsConstants][]. 9 | 10 | [POSIXConstants]: POSIXConstants.md 11 | [WindowsConstants]: WindowsConstants.md 12 | -------------------------------------------------------------------------------- /Documentation/APIs/FileTime.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `FileTime` 2 | 3 | ```swift 4 | public struct FileTime 5 | ``` 6 | 7 | A time interval broken down into seconds and nanoseconds. This is used to represent a point in time 8 | since 1970-01-01 00:00:00 UTC 9 | 10 | ### Initializers 11 | 12 | ```swift 13 | public init(_ time: timespec) 14 | ``` 15 | 16 | ```swift 17 | public init(_ time: FILETIME) 18 | ``` 19 | 20 | ### Properties 21 | 22 | ```swift 23 | var seconds: Int 24 | ``` 25 | 26 | Number of seconds since 1970-01-01 00:00:00 UTC. 27 | 28 | *** 29 | 30 | ```swift 31 | var nanoseconds: Int 32 | ``` 33 | 34 | Number of nanoseconds since `seconds`. 35 | -------------------------------------------------------------------------------- /Documentation/APIs/FileType.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `FileType` 2 | 3 | ```swift 4 | public protocol FileType 5 | ``` 6 | 7 | OS agnostic information regarding type of the file. 8 | 9 | ### Types Conforming to `FileType` 10 | 11 | * [POSIXFileType][] 12 | * [WindowsFileType][] 13 | 14 | ### Properties 15 | 16 | ```swift 17 | var isFile: Bool 18 | ``` 19 | 20 | Whether the path points to a file. 21 | 22 | *** 23 | 24 | ```swift 25 | var isDirectory: Bool 26 | ``` 27 | 28 | Whether the path points to a directory. 29 | 30 | ```swift 31 | var isSymlink: Bool 32 | ``` 33 | 34 | Whether the path points to a symlink. 35 | 36 | [POSIXFileType]: POSIXFileType.md 37 | [WindowsFileType]: WindowsFileType.md 38 | -------------------------------------------------------------------------------- /Documentation/APIs/Metadata.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `Metadata` 2 | 3 | ```swift 4 | public struct Metadata 5 | ``` 6 | 7 | OS agnostic information about a file, besides the file itself. 8 | 9 | ### Initializers 10 | 11 | ```swift 12 | public init( 13 | mode: UInt16, 14 | size: UInt64, 15 | atime: timespec, 16 | mtime: timespec, 17 | btime: timespec 18 | ) 19 | ``` 20 | 21 | Creates a `Metadata` from each individual values. This is only available on Linux. 22 | 23 | *** 24 | 25 | ```swift 26 | public init(_ stat: stat) 27 | ``` 28 | 29 | Creates a `Metadata` from `stat`. This is only available on Darwin. 30 | 31 | *** 32 | 33 | ```swift 34 | public init(_ data: WIN32_FIND_DATAW) 35 | ``` 36 | 37 | Creates a `Metadata` from `WIN32_FIND_DATAW`. This is only available on Windows. 38 | 39 | ### Properties 40 | 41 | ```swift 42 | let fileType: FileType 43 | ``` 44 | 45 | What does a path leads to. 46 | 47 | *** 48 | 49 | ```swift 50 | let permissions: Permissions 51 | ``` 52 | 53 | What permission does current process has regarding the file. 54 | 55 | *** 56 | 57 | ```swift 58 | let size: Int64 59 | ``` 60 | Length of the file's content in bytes. 61 | 62 | *** 63 | 64 | ```swift 65 | let accessed: FileTime 66 | ``` 67 | 68 | Last time the file was accessed. 69 | 70 | *** 71 | 72 | ```swift 73 | let modified: FileTime 74 | ``` 75 | Last time the file was modified. 76 | 77 | *** 78 | 79 | ```swift 80 | let created: FileTime 81 | ``` 82 | 83 | Time of creation for the file. 84 | -------------------------------------------------------------------------------- /Documentation/APIs/NativeEncodingUnit.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `NativeEncodingUnit` 2 | 3 | ```swift 4 | public typealias NativeEncodingUnit 5 | ``` 6 | 7 | This is the String encoding unit on each OS. On macOS and Linux, it is aliased to `CChar`. On 8 | Windows, it is `UInt16`. 9 | -------------------------------------------------------------------------------- /Documentation/APIs/POSIXConstants.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `POSIXConstants` 2 | 3 | ```swift 4 | public enum POSIXConstants 5 | ``` 6 | 7 | ### Properties 8 | 9 | ```swift 10 | public static let binaryPathSeparator: CChar 11 | ``` 12 | 13 | Appropriate path separator native to the current operating system. 14 | 15 | *** 16 | 17 | ```swift 18 | public static let pathSeparator: Character 19 | ``` 20 | 21 | Appropriate path separator character. 22 | -------------------------------------------------------------------------------- /Documentation/APIs/POSIXFileType.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `POSIXFileType` 2 | 3 | ```swift 4 | public enum POSIXFileType 5 | ``` 6 | 7 | ### Conforms to 8 | 9 | * [FileType][] 10 | * Codable 11 | * Equatable 12 | 13 | ### Initializers 14 | 15 | ```swift 16 | public init(rawFileType: Int32) 17 | ``` 18 | 19 | Creates a `POSIXFileType` from a POSIX file type such as `DT_REG`. 20 | 21 | *** 22 | 23 | ```swift 24 | public init(rawMode: mode_t) 25 | ``` 26 | 27 | Creates a `POSIXFileType` from a POSIX inode protection mode (`stat.st_mode`) such as `S_IFREG`. 28 | 29 | ### Enumuration cases 30 | 31 | ```swift 32 | case unknown 33 | ``` 34 | 35 | Unknown type. 36 | *** 37 | 38 | ```swift 39 | case pipe 40 | ``` 41 | 42 | A FIFO pipe. 43 | *** 44 | 45 | ```swift 46 | case characterDevice 47 | ``` 48 | 49 | A character device, or character special file. 50 | 51 | *** 52 | 53 | ```swift 54 | case directory 55 | ``` 56 | A directory. 57 | 58 | *** 59 | 60 | ```swift 61 | case blockDevice 62 | ``` 63 | 64 | A block device/special file. 65 | 66 | *** 67 | 68 | ```swift 69 | case file 70 | ``` 71 | A regular file. 72 | 73 | *** 74 | 75 | ```swift 76 | case symlink 77 | ``` 78 | A symbolic link, or symlink. 79 | 80 | *** 81 | 82 | ```swift 83 | case socket 84 | ``` 85 | A socket. 86 | 87 | [FileType]: FileType.md 88 | -------------------------------------------------------------------------------- /Documentation/APIs/POSIXPathConvertible.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `POSIXPathConvertible` 2 | 3 | ```swift 4 | public protocol POSIXPathConvertible 5 | ``` 6 | 7 | Types that conform to this protocol can be joined with [PurePOSIXPath][]. 8 | 9 | ### Types conforming to `POSIXPathConvertible` 10 | 11 | * [Path][] 12 | * [PurePOSIXPath][] 13 | * String 14 | 15 | [PurePOSIXPath]: PurePOSIXPath.md 16 | [Path]: Path.md 17 | -------------------------------------------------------------------------------- /Documentation/APIs/POSIXPermissions.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `POSIXPermissions` 2 | 3 | ```swift 4 | public struct POSIXPermissions 5 | ``` 6 | 7 | Represents the POSIX file permission bits. These bits determines read/write/execution access to 8 | a file as well as some miscellaneous information. 9 | 10 | ### Conforms to 11 | 12 | * [Permissions][] 13 | * ExpressibleByIntegerLiteral 14 | * OptionSet 15 | 16 | ### Initializer 17 | 18 | ```swift 19 | public init(rawValue: mode_t) 20 | ``` 21 | 22 | Creates a `FilePermission` from native `mode_t`. 23 | 24 | ### Properties 25 | 26 | ```swift 27 | public var rawValue: mode_t 28 | 29 | ``` 30 | 31 | The file permission as the native `mode_t` type. A de-abstraction to help interact with POSIX APIs 32 | directly. 33 | 34 | *** 35 | 36 | ```swift 37 | public static let ownerAll: POSIXPermissions 38 | ``` 39 | 40 | This is equivalent to `[.ownerRead, .ownerWrite, .ownerExecute]` (`S_IRWXU`). 41 | 42 | *** 43 | 44 | ```swift 45 | public static let ownerRead: POSIXPermissions 46 | ``` 47 | 48 | Read permission bit for the owner of the file (`S_IRUSR`). 49 | 50 | *** 51 | 52 | ```swift 53 | public static let ownerWrite: POSIXPermissions 54 | ``` 55 | 56 | Write permission bit for the owner of the file (`S_IWUSR`). 57 | 58 | *** 59 | 60 | ```swift 61 | public static let ownerExecute: POSIXPermissions 62 | ``` 63 | 64 | Execute (for ordinary files) or search (for directories) permission bit for the owner of the file 65 | (`S_IXUSR`). 66 | 67 | *** 68 | 69 | ```swift 70 | public static let groupAll: POSIXPermissions 71 | ``` 72 | 73 | This is equivalent to `[.groupRead, .groupWrite, .groupExecute]` (`S_IRWXG`). 74 | 75 | *** 76 | 77 | ```swift 78 | public static let groupRead: POSIXPermissions 79 | ``` 80 | 81 | Read permission bit for the group owner of the file (`S_IRGRP`). 82 | 83 | *** 84 | 85 | ```swift 86 | public static let groupWrite: POSIXPermissions 87 | ``` 88 | 89 | Write permission bit for the group owner of the file (`S_IWGRP`). 90 | 91 | *** 92 | 93 | ```swift 94 | public static let groupExecute: POSIXPermissions 95 | ``` 96 | 97 | Execute or search permission bit for the group owner of the file (`S_IXGRP`). 98 | 99 | *** 100 | 101 | ```swift 102 | public static let otherAll: POSIXPermissions 103 | ``` 104 | 105 | This is equivalent to `[.otherRead, .otherWrite, .otherExecute]` (`S_IRWXO`). 106 | 107 | *** 108 | 109 | ```swift 110 | public static let otherRead: POSIXPermissions 111 | ``` 112 | 113 | Read permission bit for other users (`S_IROTH`). 114 | 115 | *** 116 | 117 | ```swift 118 | public static let otherWrite: POSIXPermissions 119 | ``` 120 | 121 | Read permission bit for other users (`S_IWOTH`). 122 | 123 | *** 124 | 125 | ```swift 126 | public static let otherExecute: POSIXPermissions 127 | ``` 128 | 129 | Read permission bit for other users (`S_IXOTH`). 130 | 131 | *** 132 | 133 | ```swift 134 | public static let setUserIDOnExecution: POSIXPermissions 135 | ``` 136 | 137 | This is the set-user-ID on execute bit. See [Process Persona][] to learm more. 138 | 139 | *** 140 | 141 | ```swift 142 | 143 | public static let setGroupIDOnExecution: POSIXPermissions 144 | ``` 145 | 146 | This is the set-group-ID on execute bit See [Process Persona][] to learm more. 147 | 148 | *** 149 | 150 | ```swift 151 | 152 | public static let saveSwappedTextAfterUser: POSIXPermissions 153 | ``` 154 | 155 | This is the sticky bit. 156 | 157 | For a directory it gives permission to delete a file in that directory only if you own that file. 158 | Ordinarily, a user can either delete all the files in a directory or cannot delete any of them 159 | (based on whether the user has write permission for the directory). The same restriction applies—you 160 | must have both write permission for the directory and own the file you want to delete. The one 161 | exception is that the owner of the directory can delete any file in the directory, no matter who 162 | owns it (provided the owner has given himself write permission for the directory). This is commonly 163 | used for the /tmp directory, where anyone may create files but not delete files created by other 164 | users. 165 | 166 | [Permissions]: Permissions.md 167 | [Process Persona]: https://www.gnu.org/software/libc/manual/html_node/Process-Persona.html 168 | -------------------------------------------------------------------------------- /Documentation/APIs/PathConvertible.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `PathConvertible` 2 | 3 | ```swift 4 | public typealias PathConvertible 5 | ``` 6 | 7 | Types that conform to this protocol can be joined with [Path][] and [PurePath][]. 8 | 9 | On macOS and Linux, this is aliased to [POSIXPathConvertible][]. On Windows, it is 10 | [WindowsPathConvertible][]. 11 | 12 | [POSIXPathConvertible]: POSIXPathConvertible.md 13 | [WindowsPathConvertible]: WindowsPathConvertible.md 14 | [PurePath]: PurePath.md 15 | [Path]: Path.md 16 | -------------------------------------------------------------------------------- /Documentation/APIs/Permissions.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `Permissions` 2 | 3 | ```swift 4 | public protocol Permissions 5 | ``` 6 | 7 | OS agnostic permissions for a file path. 8 | 9 | ### Types Conforming to Permissions 10 | 11 | * [POSIXPermissions][] 12 | * [WindowsAttributes][] 13 | 14 | ### Properties 15 | 16 | ```swift 17 | var isReadOnly: Bool 18 | ``` 19 | 20 | Whether the file is read only. NOTE: setting this value does not change the permission of the path 21 | on file system. Use Path.set(_:) with the updated value to achieve that. 22 | 23 | [POSIXPermissions]: POSIXPermissions.md 24 | [WindowsAttributes]: WindowsAttributes.md 25 | -------------------------------------------------------------------------------- /Documentation/APIs/PurePOSIXPath.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `PurePOSIXPath` 2 | 3 | ```swift 4 | public struct PurePOSIXPath 5 | ``` 6 | 7 | ### Conforms to 8 | 9 | * [POSIXPathConvertible][] 10 | * CustomStringConvertible 11 | * Equatable 12 | * ExpressibleByStringLiteral 13 | * Hashable 14 | 15 | ### Initializers 16 | 17 | ```swift 18 | public init(cString: UnsafePointer) 19 | ``` 20 | Create a `PurePOSIXPath` from an unsafe buffer of `CChar`. 21 | 22 | *** 23 | ```swift 24 | public init(_ string: String) 25 | ``` 26 | 27 | Create a `PurePOSIXPath` from a `String`. 28 | 29 | ### Properties 30 | 31 | ```swift 32 | var drive: String? 33 | ``` 34 | 35 | The drive for the path. For POSIX, this is always empty. 36 | 37 | *** 38 | 39 | ```swift 40 | var root: String? 41 | ``` 42 | 43 | Root component of the path. For example, on POSIX this is typically "/". 44 | 45 | *** 46 | 47 | ```swift 48 | var segments: [String] 49 | ``` 50 | 51 | The segments in the path separated by `Constants.binaryPathSeparator`. 52 | Root is not included. 53 | 54 | Example: `Path("/usr/bin/env").segments` is `["usr", "bin", "env"]`. 55 | 56 | *** 57 | 58 | ```swift 59 | var name: String? 60 | ``` 61 | 62 | The final path component, if any. 63 | 64 | Example: `Path("src/Pathos/README.md").name` is `"README.md"`. 65 | 66 | *** 67 | 68 | ```swift 69 | var extension: String? 70 | ``` 71 | 72 | Final suffix that begins with a `.` in the `name` the path. Leading `.` 73 | in the name does not count. 74 | 75 | Example: `Path("archive/Pathos.tar.gz").extension` is `"gz"`. 76 | 77 | *** 78 | 79 | ```swift 80 | var extensions: [String] 81 | ``` 82 | 83 | Final suffix that begins with a `.` in the `name` the path. Leading `.` 84 | in the name does not count. 85 | 86 | Example: `Path("archive/Pathos.tar.gz").extension` is `["tar", "gz"]`. 87 | 88 | *** 89 | 90 | ```swift 91 | var base: PurePOSIXPath 92 | ``` 93 | 94 | Path value without the `extension`. 95 | `path.base + path.extension` should be equal to `path`. 96 | 97 | *** 98 | 99 | ```swift 100 | var parent: PurePOSIXPath 101 | ``` 102 | 103 | The logical parent of the path. The parent of an anchor is the anchor itself. 104 | The parent of `.` is `.`. The parent of `/a/b/c` is `/a/b`. 105 | 106 | *** 107 | 108 | ```swift 109 | var parents: AnySequence 110 | ``` 111 | 112 | A sequence composed of the `self.parent`, `self.parent.parent`, etc. The 113 | final value is either the current context (`Path(".")`) or the root. 114 | 115 | Example: `Array(Path("a/b/c")` is `[Path("a/b"), Path("a"), Path(".")]`. 116 | 117 | *** 118 | 119 | ```swift 120 | var isEmpty: Bool 121 | ``` 122 | 123 | The path does not have a drive nor a root, and its `segments` is empty. 124 | 125 | *** 126 | 127 | ```swift 128 | var isAbsolute: Bool 129 | ``` 130 | 131 | Indicates whether this path is absolute. 132 | 133 | An absolute path is one that has a root and, if applicable, a drive. 134 | 135 | *** 136 | 137 | ```swift 138 | public var normal: Path 139 | ``` 140 | 141 | Normalize a path by removing redundant separators and up-level 142 | references (`..`). This is a pure computation. It does not access the 143 | file system. Therefore, it may change the meaning of a path that 144 | contains symbolic links. 145 | 146 | ### Methods 147 | 148 | ```swift 149 | public func joined(with paths: POSIXPathConvertible) -> Self 150 | public func joined(with paths: [POSIXPathConvertible]) -> Self 151 | ``` 152 | 153 | Joining one or more [POSIXPathConvertible][]s after this one. 154 | 155 | **Parameters** 156 | 157 | | | | | 158 | |-------|--------------------------|--------------------------------------| 159 | | paths | `[POSIXPathConvertible]` | Other values that represents a path. | 160 | 161 | **Returns** 162 | | | | 163 | | ------ | ------------------------ | 164 | | `Path` | Result of joining paths. | 165 | 166 | ```swift 167 | public static func +(lhs: PurePOSIXPath, rhs: PurePOSIXPath) -> PurePOSIXPath 168 | public static func +(lhs: PurePOSIXPath, rhs: POSIXPathConvertible) -> PurePOSIXPath 169 | public static func +(lhs: POSIXPathConvertible, rhs: PurePOSIXPath) -> PurePOSIXPath 170 | ``` 171 | 172 | `+` operators that enable path creation with code like `"/" + myPath + "file.md"`. 173 | 174 | _¹ an important reason these need to be addressed separately is we want to avoid overloading `+` 175 | when value on both sides are `String`s._ 176 | 177 | *** 178 | 179 | ```swift 180 | public func relative(to other: POSIXPathConvertible) -> PurePOSIXPath 181 | ``` 182 | 183 | Return a relative path to `self` from `other`. This is a pure computatian. 184 | File system is not accessed to confirm the existence or nature of `self` 185 | or `other`. 186 | 187 | For example, `Path("a/b/c").relative(to: Path("a/b"))` evaluates to 188 | `Path("c")`. That is to say, to get to "a/b/c" from "a/b", one go through 189 | "c". 190 | 191 | **Parameters** 192 | 193 | | | | | 194 | | ----- | ----------------- | --------------------------- | 195 | | other | `PathConvertible` | the path to start from.
| 196 | 197 | **Returns** 198 | | | | 199 | | ------ | ------------------------------------------- | 200 | | `Path` | the path that leads from `other` to `self`. | 201 | 202 | [POSIXPathConvertible]: POSIXPathConvertible.md 203 | -------------------------------------------------------------------------------- /Documentation/APIs/PurePath.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `PurePath` 2 | 3 | ```swift 4 | public typealias PurePath = … 5 | ``` 6 | 7 | `PurePath` is a typealias. It is [PurePOSIXPath][] on macOS and Linux, and [PureWindowsPath][] on 8 | Windows. 9 | 10 | [PurePOSIXPath]: PurePOSIXPath.md 11 | [PureWindowsPath]: PureWindowsPath.md 12 | -------------------------------------------------------------------------------- /Documentation/APIs/PureWindowsPath.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `PureWindowsPath` 2 | 3 | ```swift 4 | public struct PureWindowsPath 5 | ``` 6 | 7 | ### Conforms to 8 | 9 | * [WindowsPathConvertible][] 10 | * CustomStringConvertible 11 | * Equatable 12 | * ExpressibleByStringLiteral 13 | * Hashable 14 | 15 | ### Initializers 16 | 17 | ```swift 18 | public init(cString: UnsafePointer) 19 | ``` 20 | Create a `PureWindowsPath` from an unsafe buffer of `CChar`. 21 | 22 | *** 23 | ```swift 24 | public init(_ string: String) 25 | ``` 26 | 27 | Create a `PureWindowsPath` from a `String`. 28 | 29 | ### Properties 30 | 31 | ```swift 32 | var drive: String? 33 | ``` 34 | 35 | The drive for the path. For POSIX, this is always empty. 36 | 37 | *** 38 | 39 | ```swift 40 | var root: String? 41 | ``` 42 | 43 | Root component of the path. For example, on POSIX this is typically "/". 44 | 45 | *** 46 | 47 | ```swift 48 | var segments: [String] 49 | ``` 50 | 51 | The segments in the path separated by `Constants.binaryPathSeparator`. 52 | Root is not included. 53 | 54 | Example: `Path(#"\usr\bin\env"#).segments` is `["usr", "bin", "env"]`. 55 | 56 | *** 57 | 58 | ```swift 59 | var name: String? 60 | ``` 61 | 62 | The final path component, if any. 63 | 64 | Example: `Path(#"src\Pathos\README.md"#).name` is `"README.md"`. 65 | 66 | *** 67 | 68 | ```swift 69 | var extension: String? 70 | ``` 71 | 72 | Final suffix that begins with a `.` in the `name` the path. Leading `.` 73 | in the name does not count. 74 | 75 | Example: `Path(#"archive\Pathos.tar.gz"#).extension` is `"gz"`. 76 | 77 | *** 78 | 79 | ```swift 80 | var extensions: [String] 81 | ``` 82 | 83 | Final suffix that begins with a `.` in the `name` the path. Leading `.` 84 | in the name does not count. 85 | 86 | Example: `Path(#"archive\Pathos.tar.gz"#).extension` is `["tar", "gz"]`. 87 | 88 | *** 89 | 90 | ```swift 91 | var base: PureWindowsPath 92 | ``` 93 | 94 | Path value without the `extension`. 95 | `path.base + path.extension` should be equal to `path`. 96 | 97 | *** 98 | 99 | ```swift 100 | var parent: PureWindowsPath 101 | ``` 102 | 103 | The logical parent of the path. The parent of an anchor is the anchor itself. 104 | The parent of `.` is `.`. The parent of `\a\b\c` is `\a\b`. 105 | 106 | *** 107 | 108 | ```swift 109 | var parents: AnySequence 110 | ``` 111 | 112 | A sequence composed of the `self.parent`, `self.parent.parent`, etc. The 113 | final value is either the current context (`Path(".")`) or the root. 114 | 115 | Example: `Array(Path(#"a\b\c"#)` is `[Path(#"a\b"#), Path("a"), Path(".")]`. 116 | 117 | *** 118 | 119 | ```swift 120 | var isEmpty: Bool 121 | ``` 122 | 123 | The path does not have a drive nor a root, and its `segments` is empty. 124 | 125 | *** 126 | 127 | ```swift 128 | var isAbsolute: Bool 129 | ``` 130 | 131 | Indicates whether this path is absolute. 132 | 133 | An absolute path is one that has a root and, if applicable, a drive. 134 | 135 | *** 136 | 137 | ```swift 138 | public var normal: Path 139 | ``` 140 | 141 | Normalize a path by removing redundant separators and up-level 142 | references (`..`). This is a pure computation. It does not access the 143 | file system. Therefore, it may change the meaning of a path that 144 | contains symbolic links. 145 | 146 | ### Methods 147 | 148 | ```swift 149 | public func joined(with paths: WindowsPathConvertible) -> Self 150 | public func joined(with paths: [WindowsPathConvertible]) -> Self 151 | ``` 152 | 153 | Joining one or more [WindowsPathConvertible][]s after this one. 154 | 155 | **Parameters** 156 | 157 | | | | | 158 | |-------|--------------------------|--------------------------------------| 159 | | paths | `[WindowsPathConvertible]` | Other values that represents a path. | 160 | 161 | **Returns** 162 | | | | 163 | | ------ | ------------------------ | 164 | | `Path` | Result of joining paths. | 165 | 166 | *** 167 | 168 | ```swift 169 | public static func +(lhs: PureWindowsPath, rhs: PureWindowsPath) -> PureWindowsPath 170 | public static func +(lhs: PureWindowsPath, rhs: WindowsPathConvertible) -> PureWindowsPath 171 | public static func +(lhs: WindowsPathConvertible, rhs: PureWindowsPath) -> PureWindowsPath 172 | ``` 173 | 174 | `+` operators that enable path creation with code like `#"\"# + myPath + "file.md"`. 175 | 176 | _¹ an important reason these need to be addressed separately is we want to avoid overloading `+` 177 | when value on both sides are `String`s._ 178 | 179 | *** 180 | 181 | ```swift 182 | public func relative(to other: WindowsPathConvertible) -> PureWindowsPath 183 | ``` 184 | 185 | Return a relative path to `self` from `other`. This is a pure computatian. 186 | File system is not accessed to confirm the existence or nature of `self` 187 | or `other`. 188 | 189 | For example, `Path(#"a\b\c"#).relative(to: Path(#"a\b"#))` evaluates to 190 | `Path("c")`. That is to say, to get to "a/b/c" from "a/b", one go through 191 | "c". 192 | 193 | **Parameters** 194 | 195 | | | | | 196 | | ----- | ------------------------ | ----------------------- | 197 | | other | `WindowsPathConvertible` | the path to start from. | 198 | 199 | **Returns** 200 | | | | 201 | | ------ | ------------------------------------------- | 202 | | `Path` | the path that leads from `other` to `self`. | 203 | 204 | [WindowsPathConvertible]: WindowsPathConvertible.md 205 | -------------------------------------------------------------------------------- /Documentation/APIs/README.md: -------------------------------------------------------------------------------- 1 | # Pathos API Reference 2 | 3 | This document is a complete reference to Pathos' public API. 4 | 5 | Conventions used in this document: 6 | 7 | * `var`s, `func`s, `static func`s are members of a type in the enclosing documentation section. 8 | Types are defined in the global scope of the package regardless of where the documentation is 9 | listed. 10 | * Members of a type that satisfies a protocol is documented in the section of the protocol. It is 11 | omitted from the conforming type itself. 12 | 13 | ##### Table of content 14 | 15 | * [Path][] 16 | * [Components of a path][] 17 | * [Joining/concatenating paths][] 18 | * [Working with temporary directories][] 19 | * [Current working directory][] 20 | * [Discover related paths][] 21 | * [Symbolic link (symlink)][] 22 | * [Working with metadata][] 23 | * [Editing the file system][] 24 | * [Reading/Writing normal files][] 25 | * [PurePath][] 26 | * [PurePOSIXPath][] 27 | * [PureWindowsPath][] 28 | * [Metadata][] 29 | * [FileTime][] 30 | * [FileType][] 31 | * [POSIXFileType][] 32 | * [WindowsFileType][] 33 | * [Permissions][] 34 | * [POSIXPermissions][] 35 | * [WindowsAttributes][] 36 | * [PathConvertible][] 37 | * [POSIXPathConvertible][] 38 | * [WindowsPathConvertible][] 39 | * [SystemError][] 40 | * [NativeEncodingUnit][] 41 | * [Constants][] 42 | * [WindowsConstants][] 43 | * [POSIXConstants][] 44 | 45 | 46 | [Path]: Path.md 47 | [Components of a path]: Path.md#components-of-a-path 48 | [Joining/concatenating paths]: Path.md#joining-paths 49 | [Working with temporary directories]: Path.md#working-with-temporary-directories 50 | [Current working directory]: Path.md#current-working-directory 51 | [Discover related paths]: Path.md#discover-related-paths 52 | [Symbolic link (symlink)]: Path.md#symlink 53 | [Working with metadata]: Path.md#working-with-metadata 54 | [Editing the file system]: Path.md#editing-the-file-system 55 | [Reading/Writing normal files]: Path.md#reading-writing-normal-files 56 | [PathConvertible]: PathConvertible.md 57 | [POSIXPathConvertible]: POSIXPathConvertible.md 58 | [WindowsPathConvertible]: WindowsPathConvertible.md 59 | [SystemError]: SystemError.md 60 | [PurePath]: PurePath.md 61 | [PurePOSIXPath]: PurePOSIXPath.md 62 | [PureWindowsPath]: PureWindowsPath.md 63 | [Metadata]: Metadata.md 64 | [FileTime]: FileTime.md 65 | [FileType]: FileType.md 66 | [POSIXFileType]: POSIXFileType.md 67 | [WindowsFileType]: WindowsFileType.md 68 | [Permissions]: Permissions.md 69 | [POSIXPermissions]: POSIXPermissions.md 70 | [WindowsAttributes]: WindowsAttributes.md 71 | [NativeEncodingUnit]: NativeEncodingUnit.md 72 | [Constants]: Constants.md 73 | [POSIXConstants]: POSIXConstants.md 74 | [WindowsConstants]: WindowsConstants.md 75 | -------------------------------------------------------------------------------- /Documentation/APIs/SystemError.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `SystemError` 2 | 3 | ```swift 4 | public enum SystemError 5 | ``` 6 | 7 | An error returned by the OS. 8 | 9 | 10 | ### Conforms to 11 | 12 | * Equatable 13 | * Error 14 | 15 | ### Nested type aliases 16 | 17 | ```swift 18 | public typealias Code 19 | ``` 20 | 21 | On macOS and Linux, it is aliased to `Int32`. On Windows, it is `UInt32`. 22 | 23 | ### Initializers 24 | 25 | ```swift 26 | public init(code: Code) 27 | ``` 28 | 29 | ### Enumeration cases 30 | 31 | ```swift 32 | case unspecified(errorCode: Code) 33 | ``` 34 | 35 | Unspecified error returned by the OS. 36 | -------------------------------------------------------------------------------- /Documentation/APIs/WindowsAttributes.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `WindowsAttributes` 2 | 3 | ```swift 4 | public struct WindowsAttributes 5 | ``` 6 | 7 | ### Conforms to 8 | 9 | * [Permissions][] 10 | * OptionSet 11 | 12 | ### Initializers 13 | 14 | ```swift 15 | public init(rawValue: DWORD) 16 | ``` 17 | 18 | *** 19 | 20 | ### Properties 21 | 22 | ```swift 23 | var rawValue: DWORD 24 | ``` 25 | 26 | Attributes from Windows API. E.g. `WIN32_FIND_DATAW.dwFileAttributes`. 27 | 28 | *** 29 | 30 | ```swift 31 | public static let archive: WindowsAttributes 32 | ``` 33 | 34 | A file or directory that is an archive file or directory. Applications typically use this attribute 35 | to mark files for backup or removal . 36 | 37 | *** 38 | 39 | ```swift 40 | public static let compressed: WindowsAttributes 41 | ``` 42 | 43 | A file or directory that is compressed. For a file, all of the data in the file is compressed. For 44 | a directory, compression is the default for newly created files and subdirectories. to mark files 45 | for backup or removal . 46 | 47 | *** 48 | 49 | ```swift 50 | public static let device: WindowsAttributes 51 | ``` 52 | 53 | This value is reserved for system use. 54 | 55 | *** 56 | 57 | ```swift 58 | public static let directory: WindowsAttributes 59 | ``` 60 | 61 | The handle that identifies a directory. 62 | 63 | *** 64 | 65 | ```swift 66 | public static let encrypted: WindowsAttributes 67 | ``` 68 | 69 | A file or directory that is encrypted. For a file, all data streams in the file are encrypted. For 70 | a directory, encryption is the default for newly created files and subdirectories. 71 | 72 | *** 73 | 74 | ```swift 75 | public static let hidden: WindowsAttributes 76 | ``` 77 | 78 | The file or directory is hidden. It is not included in an ordinary directory listing. 79 | 80 | *** 81 | 82 | ```swift 83 | public static let integrityStream: WindowsAttributes 84 | ``` 85 | 86 | The directory or user data stream is configured with integrity (only supported on ReFS volumes). It 87 | is not included in an ordinary directory listing. The integrity setting persists with the file if 88 | it's renamed. If a file is copied the destination file will have integrity set if either the source 89 | file or destination directory have integrity set. 90 | 91 | *** 92 | 93 | ```swift 94 | public static let normal: WindowsAttributes 95 | ``` 96 | 97 | A file that does not have other attributes set. This attribute is valid only when used alone. 98 | 99 | *** 100 | 101 | ```swift 102 | public static let notContentIndexed: WindowsAttributes 103 | ``` 104 | 105 | The file or directory is not to be indexed by the content indexing service. 106 | 107 | *** 108 | 109 | ```swift 110 | public static let noScrubData: WindowsAttributes 111 | ``` 112 | 113 | The user data stream not to be read by the background data integrity scanner (AKA scrubber). When 114 | set on a directory it only provides inheritance. This flag is only supported on Storage Spaces and 115 | ReFS volumes. It is not included in an ordinary directory listing. 116 | 117 | *** 118 | 119 | ```swift 120 | public static let offline: WindowsAttributes 121 | ``` 122 | 123 | The data of a file is not available immediately. This attribute indicates that the file data is 124 | physically moved to offline storage. This attribute is used by Remote Storage, which is the 125 | hierarchical storage management software. Applications should not arbitrarily change this attribute. 126 | 127 | *** 128 | 129 | ```swift 130 | public static let readonly: WindowsAttributes 131 | ``` 132 | 133 | A file that is read-only. Applications can read the file, but cannot write to it or delete it. This 134 | attribute is not honored on directories. For more information, see You cannot view or change the 135 | Read-only or the System attributes of folders in Windows Server 2003, in Windows XP, in Windows 136 | Vista or in Windows 7. 137 | 138 | *** 139 | 140 | ```swift 141 | public static let recallOnDataAccess: WindowsAttributes 142 | ``` 143 | 144 | When this attribute is set, it means that the file or directory is not fully present locally. For 145 | a file that means that not all of its data is on local storage (e.g. it may be sparse with some data 146 | still in remote storage). For a directory it means that some of the directory contents are being 147 | virtualized from another location. Reading the file / enumerating the directory will be more 148 | expensive than normal, e.g. it will cause at least some of the file/directory content to be fetched 149 | from a remote store. Only kernel-mode callers can set this bit. 150 | 151 | *** 152 | ```swift 153 | public static let recallOnOpen: WindowsAttributes 154 | ``` 155 | 156 | This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, 157 | FILE_BOTH_DIR_INFORMATION, etc.). When this attribute is set, it means that the file or directory 158 | has no physical representation on the local system; the item is virtual. Opening the item will be 159 | more expensive than normal, e.g. it will cause at least some of it to be fetched from a remote 160 | store. 161 | 162 | *** 163 | ```swift 164 | public static let reparsePoint: WindowsAttributes 165 | ``` 166 | 167 | A file or directory that has an associated reparse point, or a file that is a symbolic link. 168 | 169 | *** 170 | ```swift 171 | public static let sparseFile: WindowsAttributes 172 | ``` 173 | 174 | A file that is a sparse file. 175 | 176 | *** 177 | ```swift 178 | public static let system: WindowsAttributes 179 | ``` 180 | 181 | A file or directory that the operating system uses a part of, or uses exclusively. 182 | 183 | *** 184 | ```swift 185 | public static let temporary: WindowsAttributes 186 | ``` 187 | 188 | A file that is being used for temporary storage. File systems avoid writing data back to mass 189 | storage if sufficient cache memory is available, because typically, an application deletes 190 | a temporary file after the handle is closed. In that scenario, the system can entirely avoid writing 191 | the data. Otherwise, the data is written after the handle is closed. 192 | 193 | *** 194 | ```swift 195 | public static let virtual: WindowsAttributes 196 | ``` 197 | 198 | This value is reserved for system use. 199 | 200 | [Permissions]: Permissions.md 201 | -------------------------------------------------------------------------------- /Documentation/APIs/WindowsConstants.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `WindowsConstants` 2 | 3 | ```swift 4 | public enum WindowsConstants 5 | ``` 6 | 7 | ### Properties 8 | 9 | ```swift 10 | public static let binaryPathSeparator: CChar 11 | ``` 12 | 13 | Appropriate path separator native to Windows. 14 | 15 | *** 16 | 17 | ```swift 18 | public static let pathSeparator: Character 19 | ``` 20 | 21 | Appropriate path separator character. 22 | -------------------------------------------------------------------------------- /Documentation/APIs/WindowsFileType.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `WindowsFileType` 2 | 3 | ```swift 4 | public struct WindowsFileType 5 | ``` 6 | 7 | ### Conforms to 8 | 9 | * [FileType][] 10 | * Equatable 11 | 12 | ### Properties 13 | 14 | ```swift 15 | public let isFile: Bool 16 | ``` 17 | 18 | *** 19 | 20 | ```swift 21 | public let isDirectory: Bool 22 | ``` 23 | 24 | *** 25 | 26 | ```swift 27 | public let isSymlink: Bool 28 | ``` 29 | -------------------------------------------------------------------------------- /Documentation/APIs/WindowsPathConvertible.md: -------------------------------------------------------------------------------- 1 | # Pathos API Documentation for `WindowsPathConvertible` 2 | 3 | ```swift 4 | public protocol WindowsPathConvertible 5 | ``` 6 | 7 | Types that conform to this protocol can be joined with [PureWindowsPath][]. 8 | 9 | ### Types conforming to `WindowsPathConvertible` 10 | 11 | * [Path][] 12 | * [PureWindowsPath][] 13 | * String 14 | 15 | [Path]: Path.md 16 | [PureWindowsPath]: PureWindowsPath.md 17 | -------------------------------------------------------------------------------- /Documentation/README.md: -------------------------------------------------------------------------------- 1 | # Pathos 1.0 Documentation 2 | 3 | 1. [User Guide][] - A tour of Pathos for introduction purposes. 4 | 2. [API Refererence][] - Complete reference to Pathos public APIs. 5 | 3. [Design][] - Answers for why an API is designed as such. 6 | 4. [Change Log][] - Change logs for all Pathos versions. 7 | 8 | [Design]: design.md 9 | [Change Log]: ../CHANGELOG.md 10 | [User Guide]: UserGuide.md 11 | [API Refererence]: APIs/README.md 12 | -------------------------------------------------------------------------------- /Documentation/UserGuide.md: -------------------------------------------------------------------------------- 1 | # Pathos User Guide 2 | 3 | ## Overview 4 | 5 | Pathos is a cross-platform library for the virtual file system. It lets you inspect and manipulate 6 | disk content on Windows and Unix without `#if os(…)`s. Pathos has no external dependencies (not even 7 | Foundation). 8 | 9 | Pathos includes enough APIs to for writing a typical file manager application. Or, if you prefer, 10 | "coreutils" on Unix systems. One can perform the basic [CRUD][] actions on files, directories, 11 | symlinks, etc. There's also conveniences such as `glob` with support for recursive patterns, 12 | analysis for the file extensions... Pathos should meet most applications' needs for interacting 13 | with the file system. Its API selection is comparable to Python's `os.path`. 14 | 15 | ## A Brief Tour 16 | 17 | Pathos organizes its APIs around the type `Path` (surprise!). A `Path` is a address to some content 18 | on the disk. This line creates a directory with all parent paths leading to it: 19 | 20 | ```swift 21 | let p = Path("path/to/victory") 22 | try p.makeDirectory(withParents: true) 23 | ``` 24 | 25 | By design, Pathos does not distinguish files and directories, nor does it promise the path points 26 | to some thing that actually exists. Users are responsible for checking what they are dealing with: 27 | 28 | ```swift 29 | p.exsits() // `true` means it points to something on the disk 30 | try p.metadata().fileType.isSymlink // is this a symlink? 31 | ``` 32 | 33 | When retrieving content of directories, file types are included: 34 | 35 | ```swift 36 | for (path, type) in try p.children() { // … } 37 | ``` 38 | 39 | A "pure path" has a subset of `Path`'s API, but they work on the "wrong" platforms. For example, one 40 | may create and use a `PureWindowsPath` on Linux. 41 | 42 | 43 | *The [design][] document explains why these choices were made.* 44 | 45 | `Path`s themselves are simple binary values. You can join them: 46 | 47 | ```swift 48 | p.joined(with: "yay") // evaluates to Path("path/to/victory/yay") 49 | p + "yay" // alternative syntax 50 | ``` 51 | 52 | One good reason to join them this way is to make your code work cross-platform. 53 | 54 | Sometimes it's useful to learn parts of the path: 55 | 56 | ```swift 57 | let doc = Path(#"C:\Users\Dan\Documents\journal.md"#) 58 | doc.drive // "C:" 59 | doc.root // "/" 60 | doc.segments // ["Users", "Dan", "Documents", "journal.md"] 61 | doc.isAbsolute // true 62 | doc.extension // ".md" 63 | doc.parent // Path(#"C:\Users\Dan\Documents"#) 64 | // ... 65 | ``` 66 | 67 | Earlier we used `p.metadata()`. It returns a `Metadata` type that describes information about the 68 | file/directory: 69 | 70 | ```swift 71 | let meta = doc.metadata() 72 | meta.fileType.isFile 73 | meta.permissions.isReadOnly 74 | meta.size // file size 75 | meta.accessed // last access time 76 | meta.created // file creation time 77 | // ... 78 | ``` 79 | 80 | A file's `Permission` is a protocol. To get more platform-specific information from it, cast it 81 | to the native type `POSIXPermission` or `WindowsAttributes`: 82 | 83 | ```swift 84 | #if !os(Windows) 85 | if let permissions = meta.permissions as? POSIXPermission { 86 | permissions.contains(.groupExecute) /// is the group execution bit on? 87 | } 88 | #endif 89 | ``` 90 | 91 | It's common to alter the current working directory to perform some work and restore the original: 92 | 93 | ```swift 94 | try Path("an/other/place").asWorkingDirectory { 95 | // Code here will run with "an/other/place" as the cwd 96 | } 97 | // the cwd here is the same as start of this segment 98 | ``` 99 | 100 | … you can manually set and reset the working directory as well. 101 | 102 | Working with temporary directory is similar: 103 | 104 | ```swift 105 | try Path.withTemporaryDirectory { temp in 106 | // temp is a directory that's guaranteed to be writable 107 | } 108 | // "temp" no longer exists here. 109 | ``` 110 | 111 | Finally, basic reading/writing operations are included for practicality. Although this is not the 112 | focus of Pathos. 113 | 114 | ```swift 115 | try doc.readBytes() // [CChar] 116 | try doc.readUTFString() // String decoded as UTF-8 117 | try doc.write("寿司", encoding: UTF16.self, truncate: true) // replace the file with UTF-16 encoded string. 118 | ``` 119 | 120 | That's a brief introduction to what Pathos has to offer. Many more APIs aren't covered here. You 121 | can find them in [API references][]. 122 | 123 | [CRUD]: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete 124 | [design]: design.md 125 | [API references]: APIs/ 126 | -------------------------------------------------------------------------------- /Documentation/design.md: -------------------------------------------------------------------------------- 1 | # Design of Pathos 2 | 3 | Pathos offers a common API for inspecting and manipulating file systems across different operating 4 | systems. 5 | 6 | 7 | ## Path 8 | 9 | Pathos operates on top of the virtual file system (referred to as "file system" in the rest of this 10 | document) abstraction. As the name implies, its APIs are organized around the central concept of 11 | a *path*. 12 | 13 | In Pathos, a path refers to a *potential* location in the file system. It does not represent what 14 | actually resides on the hard drive. Think of it as a pointer in C: it is merely an address. The 15 | location it represents 16 | 17 | * could be a file, a directory, or any other file types the operating system supports. 18 | * could be a file when the process of a program starts, and becomes a directory while the process is 19 | still alive, or be removed. 20 | * could turn out to be nothing at all. 21 | 22 | Pathos does offer functionality to manipulate actual content of the file system. But it is user's 23 | responsibility to ensure that the path points to the thing they expect, or handle errors originated 24 | from the OS otherwise. 25 | 26 | Paths themselves, when analyzed, are validated according to the OS convention (On Windows, UNC drive 27 | are recognized properly, path segments are connected by `\`, for example.) 28 | 29 | This separation of "address" and "memory" means operations such as creating a path value, or telling 30 | whether a path is absolute, does not require access to the hard drive at all. A subset of 31 | functionalities that belongs in this category are offered on all platforms, by "pure paths". For 32 | example, one can create a `PureWindowsPath` on macOS and use the entirety of its APIs. "Pure path" 33 | APIs are a strict subset of the more general "path". 34 | 35 | While reading/writing normal files is included for practical purposes, it is not the focus of the 36 | library. Sophisticated I/O operations such as seeking locations, streaming, buffering, etc, are not 37 | supported. 38 | 39 | ## Naming convention 40 | 41 | Pathos follows the API Design Guidelines on Swift.org 42 | 43 | In addition, it uses the following naming scheme to indicate some implementation details: 44 | 45 | 1. Pure in-memory operations that doesn't call system APIs are **getter**s. For example, 46 | `Path.isAboslute`. 47 | 2. Operations that accesses the file system, but generally aren't expected to make any changes to 48 | the file system, have a noun or an adjective as function names¹. For example, `Path.metadata()`. 49 | 3. Operations that make changes to the file system have a verb in their function names. For example, 50 | `Path.makeDirectory()`. 51 | 4. Functions that starts with "with" or "as" all accept a closure as its final argument. These 52 | functions set up a environment for the current process, executes the closure in the new 53 | environment, and restores the process to the previous environment afterwards. For example, 54 | `Path.withTemporaryDirectory` creates a temporary directory, sets it as the working directory, 55 | and undo all of that at the end. 56 | 57 | *¹ The act of reading something off the file system often updates the thing's latest access time. 58 | Its content, location, type, and most other metadata remain unchanged. The naming scheme disregard 59 | this special case and considers the file system unchanged.* 60 | 61 | ## Path is binary, if you care 62 | 63 | Internally, Path is stored in binary format (as opposed to `Swift.String`) for the following 64 | reasons: 65 | 66 | 1. The path value is agnostic to character encoding. Path values from system APIs are kept as-is. 67 | A potential failure in character encoding does not necessarily invalidates the path in this way, 68 | as the OS clearly thinks the bytes sequence is valid. 69 | 2. It is common to pass the path value in and out of C APIs. Storing the value directly means we 70 | don't spend time decoding and encoding between path and strings. Although one could argue whether 71 | the saving here is meaningful, since calling the C API often means hitting the hard drive. 72 | 73 | A users doesn't need to deal with binary values directly. They may use Swift strings to create 74 | a path. When they inspect parts of the path, they get string values (`var drive: String`). They may 75 | join paths and strings together. A path in string interpolation looks like a normal path… the 76 | encoding and decoding is handled transparently in the native format the operating system supports. 77 | The user doesn't have to care about this implementation detail unless they want to do. 78 | 79 | ## Beyond the common denominator 80 | 81 | Cross-platform doesn't mean only the common subset of system APIs can be included. Take the path 82 | `Metadata` for example, its interface is shared across all supported operating systems. But 83 | `Metadata.permissions` is an existential (has a protocol as its type). To get and set permissions 84 | unique to POSIX but not on Windows, the value can be casted to a `POSIXPermissions`; on Windows, it 85 | can be casted as a `WindowsAttributes`. This way, the full range of system functionalities is 86 | offered as an extension of the common, cross-platform API. Power users can harness it with some `#if 87 | os()` conditions. 88 | 89 | Of course, offering *all* native file system API unique to each platform is a non-goal for this 90 | library. The technique described above is applied only when it make sense for the library's existing 91 | APIs. 92 | -------------------------------------------------------------------------------- /Examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(ls 2 | ls/main.swift) 3 | target_link_libraries(ls PRIVATE 4 | Pathos) 5 | 6 | add_executable(readonly 7 | readonly/main.swift) 8 | target_link_libraries(readonly PRIVATE 9 | Pathos) 10 | 11 | add_executable(lookup 12 | lookup/main.swift) 13 | target_link_libraries(lookup PRIVATE 14 | Pathos) 15 | 16 | add_executable(mk 17 | mk/main.swift) 18 | target_link_libraries(mk PRIVATE 19 | Pathos) 20 | 21 | add_executable(rm 22 | rm/main.swift) 23 | target_link_libraries(rm PRIVATE 24 | Pathos) 25 | -------------------------------------------------------------------------------- /Examples/lookup/main.swift: -------------------------------------------------------------------------------- 1 | /// A file with a certain name may exist at any ancestor of the give path. For example, for the path 2 | /// `./Sources/Target/Nested/Awesome.swift`, a `.swiftlint.yml` may exist in `./Sources/Target`, or 3 | /// `./`. This utility finds the matching file closest to the input. 4 | 5 | #if canImport(Darwin) 6 | import Darwin 7 | #elseif canImport(WinSDK) 8 | import WinSDK 9 | #else 10 | import Glibc 11 | #endif 12 | 13 | import Pathos 14 | 15 | let fileToFind = CommandLine.arguments[1] 16 | 17 | let path = Path(CommandLine.arguments[2]) 18 | 19 | for parent in path.parents { 20 | let target = parent.joined(with: fileToFind) 21 | if target.exists() { 22 | print(target) 23 | exit(0) 24 | } 25 | } 26 | 27 | exit(1) 28 | -------------------------------------------------------------------------------- /Examples/ls/main.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | 3 | let recursive = CommandLine.arguments.count > 2 && CommandLine.arguments[2] == "-r" 4 | 5 | extension String { 6 | func withLeftPad(_ n: Int) -> String { 7 | String(repeating: " ", count: max(0, n - count)) + self 8 | } 9 | } 10 | 11 | do { 12 | for child in try Path(CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : ".").children(recursive: recursive) { 13 | let meta = try child.0.metadata() 14 | let permission = (meta.permissions.isReadOnly ? "ReadOnly" : "ReadWrite").withLeftPad("ReadWrite".count) 15 | let size = "\(meta.size)".withLeftPad(10) 16 | let modified = "\(meta.modified.seconds).\(meta.modified.nanoseconds)".withLeftPad(24) 17 | let fileType: String 18 | let fileTypeLength = "directory".count 19 | var name = "\(child)" 20 | if meta.fileType.isDirectory { 21 | fileType = "directory" 22 | } else if meta.fileType.isSymlink { 23 | fileType = "symlink".withLeftPad(fileTypeLength) 24 | name = "\(child) -> \(try child.0.readSymlink())" 25 | } else { 26 | fileType = "file".withLeftPad(fileTypeLength) 27 | } 28 | print("\(fileType) \(permission) \(size) \(modified) \(name)") 29 | } 30 | } catch { 31 | print(error) 32 | } 33 | -------------------------------------------------------------------------------- /Examples/mk/main.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Darwin) 2 | import Darwin 3 | #elseif canImport(WinSDK) 4 | import WinSDK 5 | #else 6 | import Glibc 7 | #endif 8 | 9 | import Pathos 10 | 11 | var args = Array(CommandLine.arguments.dropFirst()) 12 | 13 | func help() { 14 | print( 15 | """ 16 | Make stuff in the file system. 17 | 18 | SUBCOMMANDS: 19 | dir makes a directory 20 | link makes a symlink from a path to a path 21 | """) 22 | exit(0) 23 | } 24 | 25 | func mklink(_ args: [String]) { 26 | do { 27 | try Path(args[0]).makeSymlink(at: Path(args[1])) 28 | } catch { 29 | print(error) 30 | } 31 | } 32 | 33 | func mkdir(_ args: [String]) { 34 | var args = Set(args) 35 | let makeParents: Bool 36 | if args.contains("-p") { 37 | makeParents = true 38 | args.remove("-p") 39 | } else { 40 | makeParents = false 41 | } 42 | 43 | do { 44 | try Path(args.first!).makeDirectory(withParents: makeParents) 45 | } catch { 46 | print(error) 47 | } 48 | } 49 | 50 | if args.isEmpty { 51 | help() 52 | } 53 | 54 | switch args.first { 55 | case .some("dir") where args.count > 1: 56 | mkdir(Array(args.dropFirst())) 57 | case .some("link") where args.count > 2: 58 | mklink(Array(args.dropFirst())) 59 | default: 60 | help() 61 | } 62 | -------------------------------------------------------------------------------- /Examples/readonly/main.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | 3 | extension String { 4 | var asBool: Bool { 5 | !["f", "no", "false", "0"].contains(lowercased()) 6 | } 7 | } 8 | 9 | let readOnly = CommandLine.arguments[1].asBool 10 | let pathString = CommandLine.arguments[2] 11 | 12 | let path = Path(pathString) 13 | var meta = try path.metadata() 14 | var perms = meta.permissions 15 | 16 | print("\(path) was read-only: \(perms.isReadOnly)") 17 | 18 | perms.isReadOnly = readOnly 19 | 20 | try path.set(perms) 21 | 22 | meta = try path.metadata() 23 | 24 | print("\(path) is now read-only: \(meta.permissions.isReadOnly)") 25 | -------------------------------------------------------------------------------- /Examples/rm/main.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | 3 | var args = Set(CommandLine.arguments.dropFirst()) 4 | let recursive: Bool 5 | if args.contains("-r") { 6 | recursive = true 7 | args.remove("-r") 8 | } else { 9 | recursive = false 10 | } 11 | 12 | if args.isEmpty { 13 | print("Tell me what you want to delete") 14 | } 15 | 16 | do { 17 | try Path(args.first!).delete(recursive: recursive) 18 | } catch { 19 | print(error) 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Pathos contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | export LANG = en_US.UTF-8 3 | export LC_CTYPE = en_US.UTF-8 4 | 5 | .DEFAULT_GOAL := build 6 | 7 | .PHONY: test 8 | test: 9 | @swift test -Xswiftc -warnings-as-errors 10 | 11 | .PHONY: codegen 12 | codegen: update-cmake-lists update-test-manifest format 13 | 14 | update-test-manifest: 15 | @echo "Updating SwiftPM test manifests" 16 | ifeq ($(shell uname),Darwin) 17 | @rm Tests/PathosTests/XCTestManifests.swift 18 | @touch Tests/PathosTests/XCTestManifests.swift 19 | @swift test --generate-linuxmain 20 | else 21 | @echo "Skiping test manifest update: only works on macOS." 22 | endif 23 | 24 | update-cmake-lists: 25 | @echo "Updating cmake config files" 26 | @python3 Scripts/update-cmake.py Scripts/cmake_root 27 | 28 | test-docker: 29 | @Scripts/docker.sh Pathos 'swift test -Xswiftc -warnings-as-errors' 5.5.2 focal 30 | 31 | test-codegen: codegen 32 | @git diff --exit-code 33 | 34 | build: codegen 35 | @swift build -c release -Xswiftc -warnings-as-errors > /dev/null 36 | 37 | clean: 38 | @echo "Deleting build artifacts…" 39 | @rm -rf .build tmp build 40 | @echo "Done." 41 | 42 | test-SwiftPM: 43 | @swift test -Xswiftc -warnings-as-errors 44 | 45 | format: ensure-swiftformat 46 | ifeq ($(shell uname),Darwin) 47 | @./tmp/swiftformat --swiftversion 5.5 . 48 | else 49 | @echo "Skiping formatting: only works on macOS." 50 | endif 51 | 52 | ensure-swiftformat: 53 | @Scripts/ensure-swiftformat.sh 54 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let helpers: [Target.Dependency] 5 | #if os(Windows) 6 | helpers = ["WindowsHelpers"] 7 | #elseif os(macOS) 8 | helpers = [] 9 | #else 10 | helpers = ["LinuxHelpers"] 11 | #endif 12 | 13 | let package = Package( 14 | name: "Pathos", 15 | products: [ 16 | .library( 17 | name: "Pathos", 18 | targets: ["Pathos"] 19 | ), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "Pathos", 24 | dependencies: helpers 25 | ), 26 | .target(name: "LinuxHelpers"), 27 | .target(name: "WindowsHelpers"), 28 | .target( 29 | name: "ls", 30 | dependencies: ["Pathos"], 31 | path: "Examples/ls" 32 | ), 33 | .target( 34 | name: "readonly", 35 | dependencies: ["Pathos"], 36 | path: "Examples/readonly" 37 | ), 38 | .target( 39 | name: "lookup", 40 | dependencies: ["Pathos"], 41 | path: "Examples/lookup" 42 | ), 43 | .target( 44 | name: "mk", 45 | dependencies: ["Pathos"], 46 | path: "Examples/mk" 47 | ), 48 | .target( 49 | name: "rm", 50 | dependencies: ["Pathos"], 51 | path: "Examples/rm" 52 | ), 53 | .testTarget( 54 | name: "PathosTests", 55 | dependencies: ["Pathos"] 56 | ), 57 | ] 58 | ) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](Resources/Assets/Banner.png) 2 | 3 | Pathos offers cross-platform virtual file system APIs for Swift. 4 | 5 | Pathos is implement from ground-up with on each OS's native API. It has zero dependencies. 6 | 7 | Windows support is currently considered *experimental*. 8 | 9 | | Continuous Integration | 10 | |-| 11 | |[![Amazon Linux 2](https://github.com/dduan/Pathos/workflows/Amazon%20Linux%202/badge.svg)](https://github.com/dduan/Pathos/actions?query=workflow%3A%22Amazon+Linux+2%22)| 12 | |[![macOS 11.15](https://github.com/dduan/Pathos/workflows/macOS%2011.15/badge.svg)](https://github.com/dduan/Pathos/actions?query=workflow%3A%22macOS+11.15%22)| 13 | |[![Ubuntu Bionic](https://github.com/dduan/Pathos/workflows/Ubuntu%20Bionic/badge.svg)](https://github.com/dduan/Pathos/actions?query=workflow%3A%22Ubuntu+Bionic%22)| 14 | |[![Ubuntu Focal](https://github.com/dduan/Pathos/workflows/Ubuntu%20Focal/badge.svg)](https://github.com/dduan/Pathos/actions?query=workflow%3A%22Ubuntu+Focal%22)| 15 | |[![Windows 2019](https://github.com/dduan/Pathos/workflows/Windows%202019/badge.svg) (Experimental)](https://github.com/dduan/Pathos/actions?query=workflow%3A%22Windows+2019%22) 16 | 17 | For a taste of Pathos, let's generate a static site from Markdown! 18 | 19 | ```swift 20 | import Pathos 21 | 22 | // Set the CWD and execute a closure 23 | try Path("markdown-source-dir").asWorkingDirectory { 24 | // Build the site in a unique, temporary directory 25 | let temporaryRoot = try Path.makeTemporaryDirectory() 26 | 27 | // Joining path components that works across OSes. 28 | // E.g. `articles/**/*.md` on POSIX systems. 29 | let pattern = Path("articles") + "**" + "*.md" 30 | 31 | // Use glob to find files that matches the pattern 32 | for markdown in try pattern.glob() { 33 | // path/to/file.md => path/to/file 34 | let url = markdown.base 35 | 36 | // path that contains index.html 37 | let htmlDirectory = temporaryRoot + url 38 | 39 | // make a directory, including multiple levels 40 | try htmlDirectory.makeDirectory(withParents: true) 41 | 42 | // read content of a file 43 | let source = try markdown.readUTF8String() 44 | 45 | // write out the html, imagine `markdown2html` exists 46 | try (htmlDirectory + "index.html").write(utf8: markdown2html(source)) 47 | } 48 | 49 | // all done! move the built site to output directory 50 | try temporaryRoot.move(to: "output") 51 | } 52 | // CWD is restored after the closure is done 53 | ``` 54 | 55 | As you can see, Pathos offers a whole suite of APIs for inspecting and manipulating the file system. Programs built with Pathos compile and work on all supported OS without the need to use `#if OS()` in the source. 56 | 57 | There are more [Examples](./Examples) for the curious. 58 | 59 | ## Installation 60 | 61 | #### With [SwiftPM](https://swift.org/package-manager) 62 | 63 | ```swift 64 | .package(url: "http://github.com/dduan/Pathos", from: "0.4.2") 65 | ``` 66 | 67 | ## Documentation 68 | 69 | 1. [User Guide][] - A tour of Pathos for introduction purposes. 70 | 2. [API Refererence][] - Complete reference to Pathos public APIs. 71 | 3. [Design][] - Answers for why an API is designed as such. 72 | 4. [Change Log][] - Change logs for all Pathos versions. 73 | 74 | You may also checkout the [Example apps][]. 75 | 76 | ## License 77 | 78 | Pathos is released under the MIT license. See [LICENSE.md](./LICENSE.md) 79 | 80 | 81 | [Design]: Documentation/design.md 82 | [Change Log]: CHANGELOG.md 83 | [User Guide]: Documentation/UserGuide.md 84 | [API Refererence]: Documentation/APIs/README.md 85 | [Example apps]: ./Examples 86 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## Steps for a new release 2 | - Bump version in `README.md`. 3 | - In `CHANGELOG.md`, create a section for the new version and move changes from 4 | the master section there. 5 | - Check in all changes in git. 6 | - Make a new tag for the version number. 7 | - Push all changes and tag to GitHub and make a pull request. 8 | - Make sure CI is green. 9 | - Test the PR branch with a SwiftPM project using Pathos. 10 | - Merge the PR. 11 | - Create a GitHub release from the new version tag. Paste in content of 12 | corresponding change log. 13 | - Run `make docs` to generate and deploy the documentation website. 14 | -------------------------------------------------------------------------------- /Resources/Assets/Arts.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/Pathos/a12992fd569e7cbbf2e98d686dc662f3435ffcb9/Resources/Assets/Arts.sketch -------------------------------------------------------------------------------- /Resources/Assets/Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/Pathos/a12992fd569e7cbbf2e98d686dc662f3435ffcb9/Resources/Assets/Banner.png -------------------------------------------------------------------------------- /Scripts/Dockerfile-testing-linux: -------------------------------------------------------------------------------- 1 | FROM swift@sha256:6b85beec4ef69d2d520e84c053ae7c745193ee3bbf328f4692dd238dd63f924e 2 | ADD . Pathos 3 | RUN cd Pathos; make test 4 | -------------------------------------------------------------------------------- /Scripts/Package_windows.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Pathos", 7 | products: [ 8 | .library( 9 | name: "Pathos", 10 | targets: ["Pathos"] 11 | ), 12 | ], 13 | targets: [ 14 | .target(name: "Pathos", dependencies: ["WindowsHelpers"]), 15 | .target(name: "WindowsHelpers"), 16 | .target( 17 | name: "WindowsTests", 18 | dependencies: ["Pathos"], 19 | path: "./Tests/" 20 | ), 21 | .target( 22 | name: "ls", 23 | dependencies: ["Pathos"], 24 | path: "Examples/ls" 25 | ), 26 | .target( 27 | name: "readonly", 28 | dependencies: ["Pathos"], 29 | path: "Examples/readonly" 30 | ), 31 | .target( 32 | name: "lookup", 33 | dependencies: ["Pathos"], 34 | path: "Examples/lookup" 35 | ), 36 | .target( 37 | name: "mk", 38 | dependencies: ["Pathos"], 39 | path: "Examples/mk" 40 | ), 41 | .target( 42 | name: "rm", 43 | dependencies: ["Pathos"], 44 | path: "Examples/rm" 45 | ), 46 | ] 47 | ) 48 | -------------------------------------------------------------------------------- /Scripts/cmake_root/Sources/Pathos/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(Pathos 2 | $source_list 3 | ) 4 | target_compile_options(Pathos PRIVATE 5 | $<$:-enable-testing>) 6 | target_link_libraries(Pathos PRIVATE 7 | WindowsHelpers) 8 | set_target_properties(Pathos PROPERTIES 9 | INTERFACE_COMPILE_OPTIONS "SHELL:-Xcc -I$" 10 | INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) 11 | 12 | set_property(GLOBAL APPEND PROPERTY Pathos_EXPORTS Pathos) 13 | 14 | swift_install(TARGETS Pathos 15 | EXPORT PathosExports) 16 | -------------------------------------------------------------------------------- /Scripts/cmake_root/Tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library("PathosTests" 2 | $source_list 3 | ) 4 | 5 | set_target_properties("PathosTests" PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) 6 | target_compile_options("PathosTests" PRIVATE -enable-testing) 7 | target_link_libraries("PathosTests" PRIVATE 8 | "Pathos" 9 | ) 10 | 11 | add_executable(WindowsMain 12 | LinuxMain.swift 13 | ) 14 | 15 | target_link_libraries(WindowsMain PRIVATE 16 | "PathosTests" 17 | "WindowsHelpers" 18 | ) 19 | 20 | add_test(NAME WindowsMain COMMAND WindowsMain) 21 | set_property(TEST WindowsMain PROPERTY ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") 22 | -------------------------------------------------------------------------------- /Scripts/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 | ubuntu=$4 11 | dockerfile=$(mktemp) 12 | echo "FROM swift:$swift-$ubuntu" > $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 | -------------------------------------------------------------------------------- /Scripts/ensure-swiftformat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | version=0.45.6 6 | tmp=tmp 7 | bin=$tmp/swiftformat 8 | 9 | if [[ -f $bin ]] && [[ "$($bin --version)" =~ "$version" ]]; then 10 | exit 0 11 | fi 12 | 13 | tar=swiftformat.tar.gz 14 | mkdir -p $tmp 15 | curl -L https://github.com/nicklockwood/SwiftFormat/archive/$version.tar.gz -o $tmp/$tar 16 | pushd $tmp > /dev/null 17 | tar xzf $tar 18 | pushd SwiftFormat-$version /dev/null 19 | swift build -c release 20 | popd > /dev/null 21 | popd > /dev/null 22 | mv tmp/SwiftFormat-$version/.build/release/swiftformat $bin 23 | -------------------------------------------------------------------------------- /Scripts/gen-doc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import sys 5 | 6 | class Doc: 7 | def __init__(self, desc: str, params: list[tuple[str, str, list[str]]], ret: tuple[str, str]): 8 | self.desc = desc 9 | self.params = params 10 | self.ret = ret 11 | 12 | def formatted(self) -> str: 13 | maxName = max([0] + [len(n[0]) for n in self.params]) 14 | maxType = max([0] + [len(n[1]) for n in self.params]) 15 | maxText = max([0] + [sum(len(l) for l in p[2]) + 4 * (len(p[2]) - 1) for p in self.params]) 16 | result: [str] = [ 17 | '```swift', 18 | 'let a = 1', 19 | '```', 20 | '\n', 21 | self.desc, 22 | '\n', 23 | ] 24 | 25 | if self.params: 26 | result.append('**Parameters**\n') 27 | result.append('| ' + ' ' * maxName + ' | ' + ' ' * (maxType + 2) + ' | ' + ' ' * maxText + ' |') 28 | result.append('| ' + '-' * maxName + ' | ' + '-' * (maxType + 2) + ' | ' + '-' * maxText + ' |') 29 | for name, ptype, desc in self.params: 30 | result.append('| ' + name.ljust(maxName) + ' | `' + ptype.ljust(maxType) + '` | ' + ('
'.join(desc)).ljust(maxText) + ' |') 31 | 32 | if self.ret: 33 | result.append("") 34 | result.append("**Returns**") 35 | result.append('| ' + ' ' * (len(self.ret[0]) + 2) + ' | ' + ' ' * len(self.ret[1]) + ' |') 36 | result.append('| ' + '-' * (len(self.ret[0]) + 2) + ' | ' + '-' * len(self.ret[1]) + ' |') 37 | result.append('| `' + self.ret[0] + '` | ' + self.ret[1] + ' |') 38 | return '\n'.join(result) 39 | 40 | if __name__ == '__main__': 41 | for documented in json.loads(sys.stdin.read()): 42 | documentable = documented['documentable'] 43 | doc = documented['docstring'] 44 | params = [] 45 | for (param, paramDoc) in zip(documentable['details']['parameters'], doc['parameters']): 46 | params.append( 47 | ( 48 | param['name'], 49 | param['type'].strip(), 50 | [l['text'] for l in paramDoc['description']], 51 | ) 52 | ) 53 | returnType = documentable['details'].get('returnType', None) 54 | docReturn = doc.get('returns', None) 55 | if returnType and docReturn: 56 | returnDoc = (returnType, '\n'.join([l['text'] for l in docReturn['description']])) 57 | else: 58 | returnDoc = None 59 | doc = Doc( 60 | '\n'.join(l['text'] for l in doc['description']), 61 | params, 62 | returnDoc 63 | ) 64 | 65 | print('\n', documentable['path']) 66 | print(documentable['name']) 67 | print(doc.formatted()) 68 | -------------------------------------------------------------------------------- /Scripts/githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | make format 4 | git diff --exit-code 5 | -------------------------------------------------------------------------------- /Scripts/test_windows.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | var tests = [XCTestCaseEntry]() 4 | tests += __allTests() 5 | 6 | XCTMain(tests) 7 | -------------------------------------------------------------------------------- /Scripts/update-cmake.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | 3 | import sys 4 | import os 5 | from os import path 6 | from string import Template 7 | from glob import glob 8 | 9 | template_dir = sys.argv[1] 10 | original_dir = os.getcwd() 11 | os.chdir(template_dir) 12 | templates = glob("**/CMakeLists.txt", recursive=True) 13 | os.chdir(original_dir) 14 | exclusions = ["LinuxMain.swift"] 15 | for template_path in templates: 16 | template_path_dir = path.join(path.dirname(template_path)) 17 | glob_pattern = path.join(template_path_dir, "**", "*.swift") 18 | sources = [path.relpath(p, template_path_dir) for p in glob(glob_pattern, recursive=True)] 19 | sources = [p for p in sources if not p in exclusions] 20 | sources = [f'"{p}"' for p in sources if not p in exclusions] 21 | sources.sort() 22 | source_list_text = "\n ".join(sources) 23 | 24 | with open(path.join(template_dir, template_path)) as template_file: 25 | template_text = template_file.read() 26 | template_text = template_text.replace('$source_list', source_list_text) 27 | with open(template_path, 'w') as target_file: 28 | target_file.write(template_text) 29 | 30 | -------------------------------------------------------------------------------- /Sources/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(WindowsHelpers) 2 | add_subdirectory(Pathos) 3 | -------------------------------------------------------------------------------- /Sources/LinuxHelpers/dummy.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/Pathos/a12992fd569e7cbbf2e98d686dc662f3435ffcb9/Sources/LinuxHelpers/dummy.c -------------------------------------------------------------------------------- /Sources/LinuxHelpers/include/linux_metadata.h: -------------------------------------------------------------------------------- 1 | #if __linux__ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #define AT_STATX_SYNC_AS_STAT 0x0000 /* - Do whatever stat() does */ 9 | #ifdef __NR_statx 10 | 11 | 12 | static inline ssize_t 13 | _statx(int dfd, const char *filename, unsigned int flags, unsigned int mask, struct statx *buffer) { 14 | return syscall(__NR_statx, dfd, filename, flags, mask, buffer); 15 | } 16 | 17 | static inline ssize_t linux_metadata 18 | ( 19 | const char *filename, 20 | unsigned int flags, 21 | __u16 *mode, 22 | __u64 *size, 23 | struct timespec *atime, 24 | struct timespec *mtime, 25 | struct timespec *btime 26 | ) { 27 | struct statx statx_buffer = {0}; 28 | *btime = (struct timespec) {0}; 29 | 30 | ssize_t ret = _statx(AT_FDCWD, filename, flags | AT_STATX_SYNC_AS_STAT, STATX_ALL, &statx_buffer); 31 | if (ret == 0) { 32 | *mode = statx_buffer.stx_mode; 33 | *size = statx_buffer.stx_size; 34 | *atime = (struct timespec) { 35 | .tv_sec = statx_buffer.stx_atime.tv_sec, 36 | .tv_nsec = statx_buffer.stx_atime.tv_nsec 37 | }; 38 | *mtime = (struct timespec) { 39 | .tv_sec = statx_buffer.stx_mtime.tv_sec, 40 | .tv_nsec = statx_buffer.stx_mtime.tv_nsec 41 | }; 42 | // Check that stx_btime was set in the response, not all filesystems support it. 43 | if (statx_buffer.stx_mask & STATX_BTIME) { 44 | *btime = (struct timespec) { 45 | .tv_sec = statx_buffer.stx_btime.tv_sec, 46 | .tv_nsec = statx_buffer.stx_btime.tv_nsec 47 | }; 48 | } else { 49 | *btime = (struct timespec) { 50 | .tv_sec = 0, 51 | .tv_nsec = 0 52 | }; 53 | } 54 | } 55 | return ret; 56 | } 57 | #endif // __NR_statx 58 | #endif // __linux__ 59 | -------------------------------------------------------------------------------- /Sources/LinuxHelpers/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module LinuxHelpers { 2 | header "linux_metadata.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Sources/Pathos/Algorithms.swift: -------------------------------------------------------------------------------- 1 | func relativeSegments(from path: [String], to other: [String]) -> [String] { 2 | let commonCount = path.commonPrefix(with: other).count 3 | let parents = Array(repeating: "..", count: max(other.count - commonCount, 0)) 4 | let remainingSegments = path[commonCount...] 5 | return parents + remainingSegments 6 | } 7 | 8 | extension BidirectionalCollection where Element: Equatable { 9 | func firstIndex(of other: Self) -> Index? { 10 | guard 11 | let start = other.first.flatMap(firstIndex(of:)), 12 | self[start...].count >= other.count, 13 | case let end = index(start, offsetBy: other.count), 14 | zip(self[start ..< end], other).allSatisfy(==) 15 | else { 16 | return nil 17 | } 18 | 19 | return start 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Pathos/BinaryString.swift: -------------------------------------------------------------------------------- 1 | typealias WindowsEncodingUnit = UInt16 2 | typealias WindowsBinaryString = CString 3 | typealias POSIXEncodingUnit = CChar 4 | typealias POSIXBinaryString = CString 5 | 6 | #if os(Windows) 7 | typealias BinaryString = WindowsBinaryString 8 | typealias NativeEncodingUnit = UInt16 9 | #else 10 | typealias BinaryString = POSIXBinaryString 11 | typealias NativeEncodingUnit = CChar 12 | #endif 13 | 14 | struct CString: Equatable, Hashable, Comparable { 15 | private var storage: ContiguousArray 16 | 17 | static func < (lhs: CString, rhs: CString) -> Bool { 18 | zip(lhs.storage, rhs.storage).contains { $0 < $1 } 19 | } 20 | 21 | var content: ContiguousArray.SubSequence { 22 | storage[0 ..< storage.count - 1] 23 | } 24 | 25 | func c(action: (UnsafePointer) throws -> T) rethrows -> T { 26 | try content.withUnsafeBufferPointer { 27 | try action($0.baseAddress!) 28 | } 29 | } 30 | 31 | init(cString: UnsafePointer) { 32 | var length = 0 33 | while cString.advanced(by: length).pointee != 0 { 34 | length += 1 35 | } 36 | 37 | storage = ContiguousArray(unsafeUninitializedCapacity: length + 1) { buffer, count in 38 | for offset in 0 ..< length { 39 | buffer[offset] = cString.advanced(by: offset).pointee 40 | } 41 | 42 | buffer[length] = 0 43 | count = length + 1 44 | } 45 | } 46 | 47 | init(nulTerminatedStorage: ContiguousArray) { 48 | storage = nulTerminatedStorage 49 | } 50 | } 51 | 52 | extension CString where Unit == WindowsEncodingUnit { 53 | init(_ string: String) { 54 | storage = ContiguousArray(string.utf16) + [0] 55 | } 56 | 57 | var description: String { 58 | String(decoding: content, as: UTF16.self) 59 | } 60 | } 61 | 62 | extension CString where Unit == POSIXEncodingUnit { 63 | init(_ string: String) { 64 | storage = ContiguousArray(string.utf8CString) 65 | } 66 | 67 | var description: String { 68 | c { String(cString: $0) } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Pathos/Box.swift: -------------------------------------------------------------------------------- 1 | final class Box { 2 | var content: Content 3 | 4 | init(_ content: Content) { 5 | self.content = content 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Pathos/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(Pathos 2 | "Algorithms.swift" 3 | "BinaryString.swift" 4 | "Box.swift" 5 | "Constants.swift" 6 | "Darwin/Metadata+Darwin.swift" 7 | "Darwin/Path+Darwin.swift" 8 | "FileTime.swift" 9 | "FileType.swift" 10 | "LazyBoxed.swift" 11 | "Linux/Metadata+Glibc.swift" 12 | "Linux/Path+Glibc.swift" 13 | "Metadata.swift" 14 | "POSIX/FileTime+POSIX.swift" 15 | "POSIX/POSIXConstants.swift" 16 | "POSIX/POSIXFileType.swift" 17 | "POSIX/POSIXPathConvertible.swift" 18 | "POSIX/POSIXPermissions.swift" 19 | "POSIX/Path+POSIX.swift" 20 | "POSIX/PathParts+POSIX.swift" 21 | "POSIX/PurePOSIXPath.swift" 22 | "Path+Joining.swift" 23 | "Path+Temporary.swift" 24 | "Path.swift" 25 | "PathParts.swift" 26 | "Permissions.swift" 27 | "PurePath.swift" 28 | "PurePathRepresentable.swift" 29 | "SystemError.swift" 30 | "Windows/FileTime+Windows.swift" 31 | "Windows/Metadata+Windows.swift" 32 | "Windows/Path+Windows.swift" 33 | "Windows/PathParts+Windows.swift" 34 | "Windows/PureWindowsPath.swift" 35 | "Windows/WindowsAttributes.swift" 36 | "Windows/WindowsConstants.swift" 37 | "Windows/WindowsFileType.swift" 38 | "Windows/WindowsPathConvertible.swift" 39 | ) 40 | target_compile_options(Pathos PRIVATE 41 | $<$:-enable-testing>) 42 | target_link_libraries(Pathos PRIVATE 43 | WindowsHelpers) 44 | set_target_properties(Pathos PROPERTIES 45 | INTERFACE_COMPILE_OPTIONS "SHELL:-Xcc -I$" 46 | INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) 47 | 48 | set_property(GLOBAL APPEND PROPERTY Pathos_EXPORTS Pathos) 49 | 50 | swift_install(TARGETS Pathos 51 | EXPORT PathosExports) 52 | -------------------------------------------------------------------------------- /Sources/Pathos/Constants.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | public typealias Constants = WindowsConstants 3 | #else 4 | public typealias Constants = POSIXConstants 5 | #endif 6 | 7 | #if os(Windows) 8 | import WinSDK 9 | extension Constants { 10 | static let maxPathLength = Int(MAX_PATH) 11 | } 12 | #else 13 | #if canImport(Darwin) 14 | import Darwin 15 | #else 16 | import Glibc 17 | #endif // canImport(Darwin) 18 | extension Constants { 19 | static let maxPathLength = Int(PATH_MAX) 20 | } 21 | #endif // os(Windows) 22 | -------------------------------------------------------------------------------- /Sources/Pathos/Darwin/Metadata+Darwin.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Darwin) 2 | import Darwin 3 | 4 | extension Metadata { 5 | public init(_ stat: stat) { 6 | fileType = POSIXFileType(rawMode: stat.st_mode & S_IFMT) 7 | permissions = POSIXPermissions(rawValue: stat.st_mode) 8 | size = stat.st_size 9 | accessed = FileTime(stat.st_atimespec) 10 | modified = FileTime(stat.st_mtimespec) 11 | created = FileTime(stat.st_birthtimespec) 12 | } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/Pathos/Darwin/Path+Darwin.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Darwin) 2 | import Darwin 3 | 4 | extension Path { 5 | /// Return the metadata for the content pointed at by `self`. 6 | /// 7 | /// - Parameter followSymlink: Follow and resolve symlink and retrive metadata for its target. 8 | /// `false` means metadata for the symlink itself will be returned. 9 | /// 10 | /// - Returns: metadata for the path. 11 | public func metadata(followSymlink: Bool = false) throws -> Metadata { 12 | var status = stat() 13 | let correctStat = followSymlink ? stat : lstat 14 | return try binaryPath.c { cString in 15 | if correctStat(cString, &status) != 0 { 16 | throw SystemError(code: errno) 17 | } 18 | 19 | return Metadata(status) 20 | } 21 | } 22 | } 23 | #endif // canImport(Darwin) 24 | -------------------------------------------------------------------------------- /Sources/Pathos/FileTime.swift: -------------------------------------------------------------------------------- 1 | /// A time interval broken down into seconds and nanoseconds. This is used to represent a point in 2 | /// time since 1970-01-01 00:00:00 UTC 3 | public struct FileTime { 4 | /// Number of seconds since 1970-01-01 00:00:00 UTC 5 | public var seconds: Int 6 | 7 | /// Number of nanoseconds since `seconds`. 8 | public var nanoseconds: Int 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Pathos/FileType.swift: -------------------------------------------------------------------------------- 1 | /// OS agnostic information regarding type of the file. 2 | public protocol FileType { 3 | /// Whether the path is a file. 4 | var isFile: Bool { get } 5 | 6 | /// Whether the path is a directory. 7 | var isDirectory: Bool { get } 8 | 9 | /// Whether the path is a symlink. 10 | var isSymlink: Bool { get } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Pathos/LazyBoxed.swift: -------------------------------------------------------------------------------- 1 | @propertyWrapper 2 | struct LazyBoxed { 3 | let build: () -> Value 4 | private let storage = Box>(nil) 5 | 6 | init(build: @escaping () -> Value) { 7 | self.build = build 8 | } 9 | 10 | var wrappedValue: Value { 11 | get { 12 | if let cachedValue = storage.content { 13 | return cachedValue 14 | } 15 | 16 | let value = build() 17 | storage.content = value 18 | return value 19 | } 20 | 21 | set { 22 | storage.content = newValue 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Pathos/Linux/Metadata+Glibc.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Glibc) 2 | import Glibc 3 | 4 | extension Metadata { 5 | public init(mode: UInt16, size: UInt64, atime: timespec, mtime: timespec, btime: timespec) { 6 | let mode = mode_t(mode) 7 | fileType = POSIXFileType(rawMode: mode & S_IFMT) 8 | permissions = POSIXPermissions(rawValue: mode) 9 | self.size = Int64(size) 10 | accessed = FileTime(atime) 11 | modified = FileTime(mtime) 12 | created = FileTime(btime) 13 | } 14 | } 15 | #endif // canImport(Glibc) 16 | -------------------------------------------------------------------------------- /Sources/Pathos/Linux/Path+Glibc.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Glibc) 2 | import Glibc 3 | import LinuxHelpers 4 | 5 | extension Path { 6 | public func metadata(followSymlink: Bool = false) throws -> Metadata { 7 | try binaryPath.c { cString in 8 | let flags: UInt32 = UInt32(followSymlink ? 0 : AT_SYMLINK_NOFOLLOW) 9 | var mode: UInt16 = 0 10 | var size: UInt64 = 0 11 | var atime = timespec() 12 | var mtime = timespec() 13 | var btime = timespec() 14 | if linux_metadata( 15 | cString, 16 | flags, 17 | &mode, 18 | &size, 19 | &atime, 20 | &mtime, 21 | &btime 22 | ) != 0 { 23 | throw SystemError(code: errno) 24 | } 25 | 26 | return Metadata(mode: mode, size: size, atime: atime, mtime: mtime, btime: btime) 27 | } 28 | } 29 | } 30 | #endif // canImport(Glibc) 31 | -------------------------------------------------------------------------------- /Sources/Pathos/Metadata.swift: -------------------------------------------------------------------------------- 1 | /// OS agnostic information about a file, besides the file itself. 2 | public struct Metadata { 3 | /// What does a path leads to. 4 | public let fileType: FileType 5 | 6 | /// What permission does current process has regarding the file. 7 | public let permissions: Permissions 8 | 9 | /// Length of the file's content in bytes. 10 | public let size: Int64 11 | 12 | /// Last time the file was accessed. 13 | public let accessed: FileTime 14 | 15 | /// Last time the file was modified. 16 | public let modified: FileTime 17 | 18 | /// Time of creation for the file. 19 | public let created: FileTime 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/FileTime+POSIX.swift: -------------------------------------------------------------------------------- 1 | #if !os(Windows) 2 | 3 | #if canImport(Darwin) 4 | import Darwin 5 | #else 6 | import Glibc 7 | #endif // canImport(Darwin) 8 | extension FileTime { 9 | /// Create a FileTime from POSIX timespec. 10 | /// 11 | /// - Parameter time: a timespec struct from `stat`. 12 | public init(_ time: timespec) { 13 | seconds = time.tv_sec 14 | nanoseconds = time.tv_nsec 15 | } 16 | } 17 | 18 | #endif // !os(Windows) 19 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/POSIXConstants.swift: -------------------------------------------------------------------------------- 1 | public enum POSIXConstants { 2 | /// Appropriate path separator native to the current operating system. 3 | public static let binaryPathSeparator: CChar = "/".utf8CString[0] 4 | public static let pathSeparator: Character = "/" 5 | static let binaryCurrentContext: CChar = ".".utf8CString[0] 6 | static var currentContextCharacter: Character = "." 7 | static var currentContext = "." 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/POSIXFileType.swift: -------------------------------------------------------------------------------- 1 | #if !os(Windows) 2 | 3 | #if canImport(Darwin) 4 | import Darwin 5 | #else 6 | import Glibc 7 | #endif // canImport(Darwin) 8 | 9 | /// Type of files found in POSIX. 10 | public enum POSIXFileType: Int, Equatable, Codable { 11 | /// Unknown type. 12 | case unknown 13 | /// FIFO pipe. 14 | case pipe 15 | /// Character device, or character special file. 16 | case characterDevice 17 | /// Directory. 18 | case directory 19 | /// Block special files. 20 | case blockDevice 21 | /// A regular file. 22 | case file 23 | /// A symbolic link, or symlink. 24 | case symlink 25 | /// A socket. 26 | case socket 27 | 28 | /// Creates a `POSIXFileType` from a POSIX file type such as `DT_REG`. 29 | public init(rawFileType: Int32) { 30 | switch rawFileType { 31 | case Int32(DT_FIFO): 32 | self = .pipe 33 | case Int32(DT_CHR): 34 | self = .characterDevice 35 | case Int32(DT_DIR): 36 | self = .directory 37 | case Int32(DT_BLK): 38 | self = .blockDevice 39 | case Int32(DT_REG): 40 | self = .file 41 | case Int32(DT_LNK): 42 | self = .symlink 43 | case Int32(DT_SOCK): 44 | self = .socket 45 | default: 46 | self = .unknown 47 | } 48 | } 49 | 50 | /// Creates a `POSIXFileType` from a POSIX inode protection mode (`stat.st_mode`) such as 51 | /// `S_IFREG`. 52 | public init(rawMode: mode_t) { 53 | switch rawMode { 54 | case S_IFIFO: 55 | self = .pipe 56 | case S_IFCHR: 57 | self = .characterDevice 58 | case S_IFDIR: 59 | self = .directory 60 | case S_IFBLK: 61 | self = .blockDevice 62 | case S_IFREG: 63 | self = .file 64 | case S_IFLNK: 65 | self = .symlink 66 | case S_IFSOCK: 67 | self = .socket 68 | default: 69 | self = .unknown 70 | } 71 | } 72 | 73 | /// A corresponding POSIX file type such as `DT_REG`. 74 | public var rawFileType: Int32 { 75 | switch self { 76 | case .unknown: 77 | return Int32(DT_UNKNOWN) 78 | case .pipe: 79 | return Int32(DT_FIFO) 80 | case .characterDevice: 81 | return Int32(DT_CHR) 82 | case .directory: 83 | return Int32(DT_DIR) 84 | case .blockDevice: 85 | return Int32(DT_BLK) 86 | case .file: 87 | return Int32(DT_REG) 88 | case .symlink: 89 | return Int32(DT_LNK) 90 | case .socket: 91 | return Int32(DT_SOCK) 92 | } 93 | } 94 | 95 | /// A corresponding POSIX inode protection mode value (`stat.st_mode`) such as `S_IFREG`. 96 | public var rawMode: mode_t { 97 | switch self { 98 | case .unknown: 99 | return 0 100 | case .pipe: 101 | return S_IFIFO 102 | case .characterDevice: 103 | return S_IFCHR 104 | case .directory: 105 | return S_IFDIR 106 | case .blockDevice: 107 | return S_IFBLK 108 | case .file: 109 | return S_IFREG 110 | case .symlink: 111 | return S_IFLNK 112 | case .socket: 113 | return S_IFSOCK 114 | } 115 | } 116 | } 117 | 118 | extension POSIXFileType: FileType { 119 | public var isFile: Bool { 120 | self == .file 121 | } 122 | 123 | public var isDirectory: Bool { 124 | self == .directory 125 | } 126 | 127 | public var isSymlink: Bool { 128 | self == .symlink 129 | } 130 | } 131 | 132 | #endif // !os(Windows) 133 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/POSIXPathConvertible.swift: -------------------------------------------------------------------------------- 1 | public protocol POSIXPathConvertible { 2 | var asPOSIXPath: PurePOSIXPath { get } 3 | } 4 | 5 | extension PurePOSIXPath: POSIXPathConvertible { 6 | public var asPOSIXPath: PurePOSIXPath { self } 7 | } 8 | 9 | extension String: POSIXPathConvertible { 10 | public var asPOSIXPath: PurePOSIXPath { PurePOSIXPath(self) } 11 | } 12 | 13 | extension POSIXBinaryString: POSIXPathConvertible { 14 | public var asPOSIXPath: PurePOSIXPath { PurePOSIXPath(self) } 15 | } 16 | 17 | #if !os(Windows) 18 | extension Path: POSIXPathConvertible { 19 | public var asPOSIXPath: PurePOSIXPath { pure } 20 | } 21 | 22 | public typealias PathConvertible = POSIXPathConvertible 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/POSIXPermissions.swift: -------------------------------------------------------------------------------- 1 | #if !os(Windows) 2 | 3 | #if canImport(Darwin) 4 | import Darwin 5 | #else 6 | import Glibc 7 | #endif // canImport(Darwin) 8 | 9 | /// Represents the POSIX file permission bits. These bits determines read/write/execution access to a file as 10 | /// well as some miscellaneous information. 11 | public struct POSIXPermissions: OptionSet { 12 | /// The file permission as the native `mode_t` type. A de-abstraction to help interact with POSIX APIs directly. 13 | public var rawValue: mode_t 14 | 15 | /// Creates a `FilePermission` from native `mode_t`. 16 | public init(rawValue: mode_t) { 17 | self.rawValue = rawValue & 0o7777 18 | } 19 | 20 | /// This is equivalent to `[.ownerRead, .ownerWrite, .ownerExecute]` (`S_IRWXU`). 21 | public static let ownerAll = POSIXPermissions(rawValue: S_IRWXU) 22 | 23 | /// Read permission bit for the owner of the file (`S_IRUSR`). 24 | public static let ownerRead = POSIXPermissions(rawValue: S_IRUSR) 25 | 26 | /// Write permission bit for the owner of the file (`S_IWUSR`). 27 | public static let ownerWrite = POSIXPermissions(rawValue: S_IWUSR) 28 | 29 | /// Execute (for ordinary files) or search (for directories) permission bit for the owner of the file (`S_IXUSR`). 30 | public static let ownerExecute = POSIXPermissions(rawValue: S_IXUSR) 31 | 32 | /// This is equivalent to `[.groupRead, .groupWrite, .groupExecute]` (`S_IRWXG`). 33 | public static let groupAll = POSIXPermissions(rawValue: S_IRWXG) 34 | 35 | /// Read permission bit for the group owner of the file (`S_IRGRP`). 36 | public static let groupRead = POSIXPermissions(rawValue: S_IRGRP) 37 | 38 | /// Write permission bit for the group owner of the file (`S_IWGRP`). 39 | public static let groupWrite = POSIXPermissions(rawValue: S_IWGRP) 40 | 41 | /// Execute or search permission bit for the group owner of the file (`S_IXGRP`). 42 | public static let groupExecute = POSIXPermissions(rawValue: S_IXGRP) 43 | 44 | /// This is equivalent to `[.otherRead, .otherWrite, .otherExecute]` (`S_IRWXO`). 45 | public static let otherAll = POSIXPermissions(rawValue: S_IRWXO) 46 | 47 | /// Read permission bit for other users (`S_IROTH`). 48 | public static let otherRead = POSIXPermissions(rawValue: S_IROTH) 49 | 50 | /// Write permission bit for other users (`S_IWOTH`). 51 | public static let otherWrite = POSIXPermissions(rawValue: S_IWOTH) 52 | 53 | /// Execute or search permission bit for other users (`S_IXOTH`). 54 | public static let otherExecute = POSIXPermissions(rawValue: S_IXOTH) 55 | 56 | /// This is the set-user-ID on execute bit. 57 | /// See [Process Persona](http://www.gnu.org/software/libc/manual/html_node/Process-Persona.html#Process-Persona) 58 | /// to learm more. 59 | public static let setUserIDOnExecution = POSIXPermissions(rawValue: S_ISUID) 60 | 61 | /// This is the set-group-ID on execute bit 62 | /// See [Process Persona](http://www.gnu.org/software/libc/manual/html_node/Process-Persona.html#Process-Persona) 63 | /// to learm more. 64 | public static let setGroupIDOnExecution = POSIXPermissions(rawValue: S_ISGID) 65 | 66 | /// This is the sticky bit. 67 | /// 68 | /// For a directory it gives permission to delete a file in that directory only if you own that file. 69 | /// Ordinarily, a user can either delete all the files in a directory or cannot delete any of them (based on 70 | /// whether the user has write permission for the directory). The same restriction applies—you must have 71 | /// both write permission for the directory and own the file you want to delete. The one exception is that 72 | /// the owner of the directory can delete any file in the directory, no matter who owns it (provided the 73 | /// owner has given himself write permission for the directory). This is commonly used for the /tmp 74 | /// directory, where anyone may create files but not delete files created by other users. 75 | public static let saveSwappedTextAfterUser = POSIXPermissions(rawValue: S_ISVTX) 76 | } 77 | 78 | extension POSIXPermissions: ExpressibleByIntegerLiteral { 79 | public init(integerLiteral: UInt16) { 80 | rawValue = mode_t(integerLiteral) 81 | } 82 | } 83 | 84 | extension POSIXPermissions: Permissions { 85 | public var isReadOnly: Bool { 86 | get { 87 | !contains(.ownerWrite) 88 | } 89 | 90 | set { 91 | if newValue { 92 | remove(.ownerWrite) 93 | } else { 94 | insert(.ownerWrite) 95 | } 96 | } 97 | } 98 | } 99 | 100 | #endif // !os(Windows) 101 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/PathParts+POSIX.swift: -------------------------------------------------------------------------------- 1 | extension Path.Parts { 2 | init(forPOSIXWithBinary binary: POSIXBinaryString) { 3 | drive = nil 4 | 5 | (root, segments) = Self.parse( 6 | binary.content.withUnsafeBytes { $0 }, 7 | as: UTF8.self, 8 | separator: UTF8.CodeUnit(POSIXConstants.binaryPathSeparator), 9 | currentContext: UTF8.CodeUnit(POSIXConstants.binaryCurrentContext) 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Pathos/POSIX/PurePOSIXPath.swift: -------------------------------------------------------------------------------- 1 | public struct PurePOSIXPath { 2 | @LazyBoxed 3 | private var parts: Path.Parts 4 | 5 | let binaryPath: POSIXBinaryString 6 | 7 | init(_ binary: POSIXBinaryString) { 8 | binaryPath = binary 9 | _parts = LazyBoxed { Path.Parts(forPOSIXWithBinary: binary) } 10 | } 11 | 12 | init(root: String?, segments: [String]) { 13 | self.init((root ?? "") + segments.joined(separator: [POSIXConstants.pathSeparator])) 14 | } 15 | 16 | init(parts: Path.Parts) { 17 | self.init( 18 | root: parts.root, 19 | segments: parts.segments 20 | ) 21 | _parts.wrappedValue = parts 22 | } 23 | 24 | public init(cString: UnsafePointer) { 25 | self.init(POSIXBinaryString(cString: cString)) 26 | } 27 | 28 | public init(_ string: String) { 29 | self.init(POSIXBinaryString(string)) 30 | } 31 | 32 | public var drive: String? { 33 | parts.drive 34 | } 35 | 36 | public var root: String? { 37 | parts.root 38 | } 39 | 40 | public var segments: Array { 41 | parts.segments 42 | } 43 | 44 | public var name: String? { 45 | parts.segments.last 46 | } 47 | 48 | public var `extension`: String? { 49 | parts.extension 50 | } 51 | 52 | public var extensions: [String] { 53 | parts.extensions 54 | } 55 | 56 | public var base: Self { 57 | Self(parts: parts.base) 58 | } 59 | 60 | public func joined(with paths: POSIXPathConvertible...) -> Self { 61 | joined(with: paths) 62 | } 63 | 64 | public func joined(with paths: [POSIXPathConvertible]) -> Self { 65 | let paths = [self] + paths.map(\.asPOSIXPath) 66 | var resultString = ContiguousArray() 67 | for path in paths { 68 | if path.binaryPath.content.first == POSIXConstants.binaryPathSeparator { 69 | resultString = .init(path.binaryPath.content) 70 | } else if resultString.isEmpty || resultString.last == POSIXConstants.binaryPathSeparator { 71 | resultString += path.binaryPath.content 72 | } else { 73 | resultString += [POSIXConstants.binaryPathSeparator] + path.binaryPath.content 74 | } 75 | } 76 | 77 | return Self(CString(nulTerminatedStorage: resultString + [0])) 78 | } 79 | 80 | public var isAbsolute: Bool { 81 | root != nil 82 | } 83 | 84 | public static func + (lhs: Self, rhs: Self) -> Self { 85 | lhs.joined(with: rhs) 86 | } 87 | 88 | @_disfavoredOverload 89 | public static func + (lhs: Self, rhs: POSIXPathConvertible) -> Self { 90 | lhs.joined(with: rhs) 91 | } 92 | 93 | @_disfavoredOverload 94 | public static func + (lhs: POSIXPathConvertible, rhs: Self) -> Self { 95 | lhs.asPOSIXPath.joined(with: rhs) 96 | } 97 | 98 | public func relative(to other: POSIXPathConvertible) -> PurePOSIXPath { 99 | let other = other.asPOSIXPath 100 | let all = relativeSegments(from: segments, to: other.segments) 101 | 102 | if all.isEmpty { 103 | return PurePOSIXPath(".") 104 | } else { 105 | return PurePOSIXPath(all.joined(separator: "\(POSIXConstants.pathSeparator)")) 106 | } 107 | } 108 | 109 | public var parent: PurePOSIXPath { 110 | let newParts = parts.parentParts 111 | return PurePOSIXPath(root: newParts.root, segments: newParts.segments) 112 | } 113 | 114 | public var parents: AnySequence { 115 | var parents = Path.Parts.Parents(initialParts: parts) 116 | return AnySequence { 117 | AnyIterator { 118 | parents.next().map { PurePOSIXPath(root: $0.root, segments: $0.segments) } 119 | } 120 | } 121 | } 122 | 123 | public var normal: PurePOSIXPath { 124 | PurePOSIXPath(parts: parts.normalized) 125 | } 126 | 127 | var isEmpty: Bool { 128 | parts.isEmpty 129 | } 130 | } 131 | 132 | extension PurePOSIXPath: ExpressibleByStringLiteral { 133 | public init(stringLiteral value: StaticString) { 134 | self.init( 135 | value.withUTF8Buffer { 136 | String(decoding: $0, as: UTF8.self) 137 | } 138 | ) 139 | } 140 | } 141 | 142 | extension PurePOSIXPath: CustomStringConvertible { 143 | public var description: String { 144 | binaryPath.description 145 | } 146 | } 147 | 148 | extension PurePOSIXPath: Equatable { 149 | public static func == (lhs: Self, rhs: Self) -> Bool { 150 | lhs.binaryPath == rhs.binaryPath 151 | } 152 | } 153 | 154 | extension PurePOSIXPath: Hashable { 155 | public func hash(into hasher: inout Hasher) { 156 | hasher.combine(binaryPath) 157 | } 158 | } 159 | 160 | extension Collection where Element: Equatable { 161 | func commonPrefix(with other: Self) -> Self.SubSequence { 162 | let limit = Swift.min(endIndex, other.endIndex) 163 | var end = startIndex 164 | while end < limit && self[end] == other[end] { 165 | end = index(after: end) 166 | } 167 | 168 | return self[startIndex ..< end] 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/Pathos/Path+Joining.swift: -------------------------------------------------------------------------------- 1 | extension Path { 2 | public func joined(with others: PathConvertible...) -> Self { 3 | joined(with: others) 4 | } 5 | 6 | public func joined(with others: [PathConvertible]) -> Self { 7 | Path(pure.joined(with: others)) 8 | } 9 | 10 | public static func + (lhs: Self, rhs: Self) -> Self { 11 | lhs.joined(with: rhs) 12 | } 13 | 14 | @_disfavoredOverload 15 | public static func + (lhs: Self, rhs: PurePath) -> Self { 16 | lhs.joined(with: rhs) 17 | } 18 | 19 | @_disfavoredOverload 20 | public static func + (lhs: PurePath, rhs: Path) -> Self { 21 | Path(lhs.joined(with: rhs.pure)) 22 | } 23 | 24 | public static func + (lhs: Self, rhs: String) -> Self { 25 | lhs.joined(with: rhs) 26 | } 27 | 28 | #if os(Windows) 29 | public static func + (lhs: String, rhs: Self) -> Self { 30 | Path(lhs.asWindowsPath.joined(with: rhs.pure)) 31 | } 32 | 33 | #else 34 | public static func + (lhs: String, rhs: Self) -> Self { 35 | Path(lhs.asPOSIXPath.joined(with: rhs.pure)) 36 | } 37 | #endif // os(Windows) 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Pathos/Path+Temporary.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | import WinSDK 3 | #elseif canImport(Darwin) 4 | import Darwin 5 | #else 6 | import Glibc 7 | #endif 8 | 9 | private func getEnvVar(_ name: String) -> Path? { 10 | #if os(Windows) 11 | return name.withCString(encodedAs: UTF16.self) { namePtr -> ContiguousArray? in 12 | let storage = ContiguousArray( 13 | unsafeUninitializedCapacity: Int(MAX_PATH) 14 | ) { buffer, count in 15 | let length = Int(GetEnvironmentVariableW(namePtr, buffer.baseAddress, DWORD(MAX_PATH))) 16 | if length == 0 { 17 | count = 0 18 | } else { 19 | buffer[length] = 0 20 | count = length + 1 21 | } 22 | } 23 | 24 | return storage.isEmpty ? nil : storage 25 | } 26 | .map { Path(WindowsBinaryString(nulTerminatedStorage: $0)) } 27 | #else 28 | return getenv(name) 29 | .map { Path(BinaryString(cString: $0)) } 30 | #endif 31 | } 32 | 33 | extension Path { 34 | /// Search for a temporary directory suitable as the default temprorary directory. 35 | /// 36 | /// A suitable location is in one of the candidate list and allows write permission for this process. 37 | /// 38 | /// The list of candidate locations to consider are the following: 39 | /// * Location defined as the TMPDIR environment variable. 40 | /// * Location defined as the TMP environment variable. 41 | /// * Location defined as the TMPDIR environment variable. 42 | /// * /tmp 43 | /// * /var/tmp 44 | /// * /usr/tmp 45 | /// Location defined as the HOME or USERPROFILE environment variable. 46 | /// * Current working directory. 47 | /// 48 | /// - Returns: A suitable default temprary directory. 49 | public static func searchForDefaultTemporaryDirectory() -> Path { 50 | for envName in ["TMPDIR", "TMP", "TEMP"] { 51 | if let envvar = getEnvVar(envName), (try? envvar.metadata())?.permissions.isReadOnly == false { 52 | return envvar 53 | } 54 | } 55 | 56 | for tmpPath in ["/tmp", "/var/tmp", "/usr/tmp"] { 57 | let path = Path(tmpPath) 58 | if (try? path.metadata())?.permissions.isReadOnly == false { 59 | return path 60 | } 61 | } 62 | 63 | for envName in ["HOME", "USERPROFILE"] { 64 | if let envvar = getEnvVar(envName), (try? envvar.metadata())?.permissions.isReadOnly == false { 65 | return envvar 66 | } 67 | } 68 | 69 | return (try? .workingDirectory()) ?? Path(".") 70 | } 71 | 72 | /// The default temporary used Pathos uses. Its default value is computed by 73 | /// `.searchForDefaultTemporaryDirectory`. If this value is set to `/x/y/z`, then functions such as 74 | /// `Pathos.makeTemporaryDirectory()` will create its result in `/x/y/z`. 75 | public static var defaultTemporaryDirectory: Path = .searchForDefaultTemporaryDirectory() 76 | 77 | private static func constructTemporaryPath(prefix: String = "", suffix: String = "") -> Path { 78 | defaultTemporaryDirectory.joined(with: "\(prefix)\(UInt64.random(in: 0 ... .max))\(suffix)") 79 | } 80 | 81 | /// Make a temporary directory with write access. 82 | /// 83 | /// The parent of the return value is the current value of `Path.defaultTemporaryDirectory`. It will have 84 | /// a randomized name. A prefix and a suffix can be optionally specified as options. 85 | /// 86 | /// - Parameters: 87 | /// - prefix: A prefix for the temporary directories's name. 88 | /// - suffix: A suffix for the temporary directories's name. 89 | /// 90 | /// - Returns: A temporary directory. 91 | public static func makeTemporaryDirectory(prefix: String = "", suffix: String = "") throws -> Path { 92 | let path = constructTemporaryPath(prefix: prefix, suffix: suffix) 93 | try path.makeDirectory() 94 | return path 95 | } 96 | 97 | /// Execute some code with a temporarily created directory as the current working directory. 98 | /// 99 | /// This method does the following: 100 | /// 101 | /// 1. make a temporary directory with write access (`Path.makeTemporaryDirectory`) 102 | /// 2. set the directory from previous step as the current working directory. 103 | /// 3. execute a closure as supplied as argument. 104 | /// 4. reset the current working directory. 105 | /// 5. delete the temprary directory along with its content. 106 | /// 107 | /// - Parameter action: The closure to execute in the temporary environment. The temporary directory is 108 | /// sent as a parameter for the action closure. 109 | public static func withTemporaryDirectory(run action: @escaping (Path) throws -> Void) throws { 110 | let temporaryDirectory = try makeTemporaryDirectory() 111 | try temporaryDirectory.asWorkingDirectory { 112 | try action(temporaryDirectory) 113 | } 114 | 115 | try temporaryDirectory.delete(recursive: true) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/Pathos/PathParts.swift: -------------------------------------------------------------------------------- 1 | extension Path.Parts { 2 | static func parse( 3 | _ binary: C, 4 | as encoding: Encoding.Type, 5 | separator: Encoding.CodeUnit, 6 | currentContext: Encoding.CodeUnit 7 | ) -> (String?, [String]) 8 | where 9 | C: RandomAccessCollection, 10 | C.Index == Int, 11 | Encoding: _UnicodeEncoding, 12 | C.Element == Encoding.CodeUnit 13 | { 14 | let rest: C.SubSequence 15 | let root: String? 16 | if !binary.isEmpty && binary[0] == separator { 17 | let stop = binary.firstIndex(where: { $0 != separator }) ?? 0 18 | if stop == 2 || binary.count == 2 && stop == 0 && encoding == UTF8.self { 19 | root = String(decoding: [separator, separator], as: Encoding.self) 20 | } else { 21 | root = String(decoding: [separator], as: Encoding.self) 22 | } 23 | rest = binary[stop...] 24 | } else { 25 | root = nil 26 | rest = binary[...] 27 | } 28 | 29 | let segments = rest 30 | .split(separator: separator) 31 | .enumerated() 32 | .filter { $1.count != 1 || !($0 > 0 && $1.first == currentContext) } 33 | .map { String(decoding: $0.1, as: Encoding.self) } 34 | 35 | return (root, segments) 36 | } 37 | 38 | var `extension`: String? { 39 | segments.last.flatMap { Self.findExtension(s: $0[...])?.0 } 40 | } 41 | 42 | var extensions: [String] { 43 | guard let name = segments.last else { 44 | return [] 45 | } 46 | 47 | var result = [String]() 48 | var rest = name[...] 49 | while let (suffix, dotIndex) = Self.findExtension(s: rest) { 50 | rest = name[.. (String, String.Index)? { 95 | guard let dotIndex = s.lastIndex(of: Constants.currentContextCharacter), 96 | dotIndex != s.startIndex 97 | else { 98 | return nil 99 | } 100 | 101 | return (String(s[dotIndex...]), dotIndex) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Pathos/Permissions.swift: -------------------------------------------------------------------------------- 1 | /// OS agnostic permissions for a file path. 2 | public protocol Permissions { 3 | /// Whether the file is read only. NOTE: setting this value does not change the permission of 4 | /// the path on file system. Use `Path.set(_:)` with the updated value to achieve that. 5 | var isReadOnly: Bool { get set } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Pathos/PurePath.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | public typealias PurePath = PureWindowsPath 3 | #else 4 | public typealias PurePath = PurePOSIXPath 5 | #endif 6 | -------------------------------------------------------------------------------- /Sources/Pathos/PurePathRepresentable.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | protocol PurePathRepresentable: Hashable, CustomStringConvertible, ExpressibleByStringLiteral { 3 | associatedtype NativeEncodingUnit 4 | associatedtype BinaryStringLike 5 | associatedtype PathLike 6 | 7 | var binaryPath: BinaryStringLike { get } 8 | 9 | /// Creates a path from a C String. 10 | /// 11 | /// - Parameter cString: a nul-terminated C String. 12 | init(cString: UnsafePointer) 13 | 14 | /// Creates a path from a `String`. 15 | /// 16 | /// - Parameter string: The string that represents a path. 17 | init(_ string: String) 18 | 19 | /// Creates a path from a `BinaryString`. 20 | /// 21 | /// - Parameter string: The string that represents a path. 22 | init(_ string: BinaryStringLike) 23 | 24 | /// The drive for the path. For POSIX, this is always empty. 25 | var drive: String? { get } 26 | 27 | /// Root component of the path. For example, on POSIX this is typically "/". 28 | var root: String? { get } 29 | 30 | /// The segments in the path separated by `Constants.binaryPathSeparator`. 31 | /// Root is not included. 32 | /// 33 | /// Example: `Path("/usr/bin/env").segments` is `["usr", "bin", "env"]`. 34 | var segments: Array { get } 35 | 36 | /// The final path component, if any. 37 | /// 38 | /// Example: `Path("src/Pathos/README.md").name` is `"README.md"`. 39 | var name: String? { get } 40 | 41 | /// Join with other paths. Absolute path will override existing value. 42 | /// For example: 43 | /// Path("/a/b").joined(with: "c") => Path("/a/b/c") 44 | /// Path("/a/b").joined(with: "/c") => Path("/c") 45 | /// 46 | /// - Parameter paths: Other values that represents a path. 47 | /// 48 | /// - Returns: Result of joining paths. 49 | func joined(with paths: PathLike...) -> Self 50 | 51 | /// Join with other paths. Absolute path will override existing value. 52 | /// For example: 53 | /// Path("/a/b").joined(with: "c") => Path("/a/b/c") 54 | /// Path("/a/b").joined(with: "/c") => Path("/c") 55 | /// 56 | /// - Parameter paths: Other values that represents a path. 57 | /// 58 | /// - Returns: Result of joining paths. 59 | func joined(with paths: [PathLike]) -> Self 60 | 61 | /// Indicates whether this path is absolute. 62 | /// 63 | /// An absolute path is one that has a root and, if applicable, a drive. 64 | var isAbsolute: Bool { get } 65 | 66 | /// Final suffix that begins with a `.` in the `name` the path. Leading `.` 67 | /// in the name does not count. 68 | /// 69 | /// Example: `Path("archive/Pathos.tar.gz").extension` is `"gz"`. 70 | var `extension`: String? { get } 71 | 72 | /// Suffixes that begin with a `.` in the `name` the path, in the order they appear. 73 | /// Leading `.` in the name does not count. 74 | /// 75 | /// Example: `Path("archive/Pathos.tar.gz").extension` is `["tar", "gz"]`. 76 | var extensions: [String] { get } 77 | 78 | /// Path value without the `extension`. `path.base + path.extension` should be equal to `path`. 79 | var base: Self { get } 80 | 81 | /// Returns a path that connects this location and another. 82 | /// 83 | /// This is a pure computation: the file system is not accessed to confirm the existence or 84 | /// nature of this path or `other`. 85 | /// Example: the relative path of `/` to `/Users/dan` is `../..`. 86 | /// 87 | /// - Parameter other: The path, through the return value, can be reached from this path. 88 | /// - Returns: The path through which other can be reached from this branch. 89 | func relative(to other: PathLike) -> Self 90 | 91 | /// The logical parent of the path. The parent of an anchor is the anchor itself. 92 | /// The parent of `.` is `.`. The parent of `/a/b/c` is `/a/b`. 93 | var parent: Self { get } 94 | 95 | /// A sequence composed of the `self.parent`, `self.parent.parent`, etc. The 96 | /// final value is either the current context (`Path(".")`) or the root. 97 | /// 98 | /// Example: `Array(Path("a/b/c")` is `[Path("a/b"), Path("a"), Path(".")]`. 99 | var parents: AnySequence { get } 100 | 101 | /// The path does not have a drive nor a root, and its `segments` is empty. 102 | var isEmpty: Bool { get } 103 | } 104 | 105 | extension PureWindowsPath: PurePathRepresentable {} 106 | extension PurePOSIXPath: PurePathRepresentable {} 107 | extension Path: PurePathRepresentable {} 108 | #endif 109 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/FileTime+Windows.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | import WinSDK 3 | 4 | private let kSecondsFromWindowsEpoch: UInt64 = 11_644_473_600 5 | private let k100NanosIn1Second: UInt64 = 10_000_000 6 | extension FileTime { 7 | /// Converts to Unix epoch and in-library representation. 8 | /// 9 | /// Windows `FILETIME` is a 64-bit value representing the number of 100-nanosecond intervals since 1601-1-1 UTC. 10 | public init(_ time: FILETIME) { 11 | let hundredNanos = UInt64(time.dwHighDateTime) << 32 | UInt64(time.dwLowDateTime) 12 | let hundredNanosSinceUnixEpoch = hundredNanos - kSecondsFromWindowsEpoch * k100NanosIn1Second 13 | seconds = Int(hundredNanosSinceUnixEpoch / k100NanosIn1Second) 14 | nanoseconds = Int(hundredNanosSinceUnixEpoch % k100NanosIn1Second * 100) 15 | } 16 | } 17 | 18 | #endif // os(Windows) 19 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/Metadata+Windows.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | import WinSDK 3 | 4 | extension Metadata { 5 | public init(_ data: WIN32_FIND_DATAW) { 6 | accessed = FileTime(data.ftLastAccessTime) 7 | modified = FileTime(data.ftLastWriteTime) 8 | created = FileTime(data.ftCreationTime) 9 | size = Int64(UInt64(data.nFileSizeHigh << 32) | UInt64(data.nFileSizeLow)) 10 | permissions = WindowsAttributes(rawValue: data.dwFileAttributes) 11 | 12 | let isDirectory = data.dwFileAttributes & UInt32(bitPattern: FILE_ATTRIBUTE_DIRECTORY) != 0 13 | let isSymlink = data.dwFileAttributes & UInt32(bitPattern: FILE_ATTRIBUTE_REPARSE_POINT) != 0 && data.dwReserved0 & 0x2000_0000 != 0 14 | let isFile = !isDirectory 15 | 16 | fileType = WindowsFileType( 17 | isFile: isFile, 18 | isDirectory: isDirectory, 19 | isSymlink: isSymlink 20 | ) 21 | } 22 | } 23 | #endif // os(Windows) 24 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/PathParts+Windows.swift: -------------------------------------------------------------------------------- 1 | private let windowsSeparatorByte = "\\".utf16.first! 2 | private let alternativeSeparatorByte = "/".utf16.first! 3 | private let colonByte = ":".utf16.first! 4 | 5 | extension Path.Parts { 6 | init(forWindowsWithBinary binary: WindowsBinaryString) { 7 | let binary = ContiguousArray(binary.content.map { $0 == alternativeSeparatorByte ? windowsSeparatorByte : $0 }) 8 | let (drive, rest) = Self.splitDrive(path: binary) 9 | self.drive = drive.isEmpty ? nil : String(decoding: drive, as: UTF16.self) 10 | (root, segments) = Self.parse( 11 | rest, 12 | as: UTF16.self, 13 | separator: WindowsConstants.binaryPathSeparator, 14 | currentContext: WindowsConstants.binaryCurrentContext 15 | ) 16 | } 17 | 18 | /// Split the pathname path into a pair (drive, tail) where drive is either a 19 | /// mount point or the empty string. On systems which do not use drive 20 | /// specifications, drive will always be the empty string. In all cases, drive + 21 | /// tail will be the same as path. 22 | /// 23 | /// On Windows, splits a pathname into drive/UNC sharepoint and relative path. 24 | /// 25 | /// If the path contains a drive letter, drive will contain everything up to and 26 | /// including the colon. e.g. splitdrive("c:/dir") returns ("c:", "/dir") 27 | /// 28 | /// If the path contains a UNC path, drive will contain the host name and share, 29 | /// up to but not including the fourth separator. e.g. 30 | /// splitdrive("//host/computer/dir") returns ("//host/computer", "/dir") 31 | /// 32 | /// - Parameter path: The path to split. 33 | /// - Returns: A tuple with the first part being the drive or UNC host, the 34 | /// second part being the rest of the path. 35 | static func splitDrive(path: ContiguousArray) -> (ContiguousArray, ContiguousArray) { 36 | if path.count > 2 && path.starts(with: [windowsSeparatorByte, windowsSeparatorByte]) 37 | && path[2] != windowsSeparatorByte 38 | { 39 | // UNC path 40 | guard let nextSlashIndex = path[2...].firstIndex(of: windowsSeparatorByte) else { 41 | return ([], ContiguousArray(path)) 42 | } 43 | 44 | guard path.count > nextSlashIndex + 1, 45 | let nextNextSlashIndex = path[(nextSlashIndex + 1)...].firstIndex(of: windowsSeparatorByte) 46 | else { 47 | return (ContiguousArray(path), []) 48 | } 49 | 50 | if nextNextSlashIndex == nextSlashIndex + 1 { 51 | return ([], ContiguousArray(path)) 52 | } 53 | 54 | return ( 55 | ContiguousArray(path.prefix(nextNextSlashIndex)), 56 | ContiguousArray(path.dropFirst(nextNextSlashIndex)) 57 | ) 58 | } 59 | 60 | let colonIndex = path.index(after: path.startIndex) 61 | 62 | if path.count > 1 && path[colonIndex] == colonByte { 63 | return ( 64 | ContiguousArray(path[...colonIndex]), 65 | ContiguousArray(path.dropFirst(2)) 66 | ) 67 | } 68 | 69 | return ([], ContiguousArray(path)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/PureWindowsPath.swift: -------------------------------------------------------------------------------- 1 | public struct PureWindowsPath { 2 | @LazyBoxed 3 | private var parts: Path.Parts 4 | 5 | let binaryPath: WindowsBinaryString 6 | 7 | init(_ binary: WindowsBinaryString) { 8 | binaryPath = binary 9 | _parts = .init { Path.Parts(forWindowsWithBinary: binary) } 10 | } 11 | 12 | init(drive: String?, root: String?, segments: [String]) { 13 | self.init( 14 | (drive ?? "") + (root ?? "") + segments.joined(separator: [WindowsConstants.pathSeparator])) 15 | } 16 | 17 | init(parts: Path.Parts) { 18 | self.init( 19 | drive: parts.drive, 20 | root: parts.root, 21 | segments: parts.segments 22 | ) 23 | _parts.wrappedValue = parts 24 | } 25 | 26 | public init(cString: UnsafePointer) { 27 | self.init(WindowsBinaryString(cString: cString)) 28 | } 29 | 30 | public init(_ string: String) { 31 | self.init(WindowsBinaryString(string)) 32 | } 33 | 34 | public var drive: String? { 35 | parts.drive 36 | } 37 | 38 | public var root: String? { 39 | parts.root 40 | } 41 | 42 | public var segments: Array { 43 | parts.segments 44 | } 45 | 46 | public var name: String? { 47 | parts.segments.last 48 | } 49 | 50 | public var `extension`: String? { 51 | parts.extension 52 | } 53 | 54 | public var extensions: [String] { 55 | parts.extensions 56 | } 57 | 58 | public var base: Self { 59 | Self(parts: parts.base) 60 | } 61 | 62 | public func joined(with others: WindowsPathConvertible...) -> Self { 63 | joined(with: others) 64 | } 65 | 66 | public func joined(with paths: [WindowsPathConvertible]) -> Self { 67 | let paths = [self] + paths.map(\.asWindowsPath) 68 | var drive: String? 69 | var root: String? 70 | var segments = [String]() 71 | 72 | for path in paths { 73 | if let pathDrive = path.drive { 74 | drive = pathDrive 75 | root = path.root 76 | segments = path.segments 77 | } else if let pathRoot = path.root { 78 | root = pathRoot 79 | segments = path.segments 80 | } else { 81 | segments += path.segments 82 | } 83 | } 84 | 85 | return PureWindowsPath(drive: drive, root: root, segments: segments) 86 | } 87 | 88 | public var isAbsolute: Bool { 89 | root != nil && drive != nil 90 | } 91 | 92 | public static func + (lhs: Self, rhs: Self) -> Self { 93 | lhs.joined(with: rhs) 94 | } 95 | 96 | @_disfavoredOverload 97 | public static func + (lhs: Self, rhs: WindowsPathConvertible) -> Self { 98 | lhs.joined(with: rhs) 99 | } 100 | 101 | @_disfavoredOverload 102 | public static func + (lhs: WindowsPathConvertible, rhs: Self) -> Self { 103 | lhs.asWindowsPath.joined(with: rhs) 104 | } 105 | 106 | public func relative(to other: WindowsPathConvertible) -> PureWindowsPath { 107 | let other = other.asWindowsPath 108 | let all = relativeSegments(from: segments, to: other.segments) 109 | 110 | if all.isEmpty { 111 | return PureWindowsPath(".") 112 | } else { 113 | return PureWindowsPath(all.joined(separator: "\(WindowsConstants.pathSeparator)")) 114 | } 115 | } 116 | 117 | public var parent: PureWindowsPath { 118 | PureWindowsPath(parts: parts.parentParts) 119 | } 120 | 121 | public var parents: AnySequence { 122 | var parents = Path.Parts.Parents(initialParts: parts) 123 | return AnySequence { 124 | AnyIterator { 125 | parents.next().map { PureWindowsPath(drive: $0.drive, root: $0.root, segments: $0.segments) } 126 | } 127 | } 128 | } 129 | 130 | public var normal: PureWindowsPath { 131 | PureWindowsPath(parts: parts.normalized) 132 | } 133 | 134 | var isEmpty: Bool { 135 | parts.isEmpty 136 | } 137 | } 138 | 139 | extension PureWindowsPath: ExpressibleByStringLiteral { 140 | public init(stringLiteral value: StaticString) { 141 | self.init( 142 | value.withUTF8Buffer { 143 | String(decoding: $0, as: UTF8.self) 144 | } 145 | ) 146 | } 147 | } 148 | 149 | extension PureWindowsPath: CustomStringConvertible { 150 | public var description: String { 151 | binaryPath.description 152 | } 153 | } 154 | 155 | extension PureWindowsPath: Equatable { 156 | public static func == (lhs: Self, rhs: Self) -> Bool { 157 | lhs.binaryPath == rhs.binaryPath 158 | } 159 | } 160 | 161 | extension PureWindowsPath: Hashable { 162 | public func hash(into hasher: inout Hasher) { 163 | hasher.combine(binaryPath) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/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/Pathos/Windows/WindowsConstants.swift: -------------------------------------------------------------------------------- 1 | public enum WindowsConstants { 2 | /// Appropriate path separator native to the current operating system. 3 | public static let binaryPathSeparator: UInt16 = "\\".utf16.first! 4 | public static let pathSeparator: Character = "\\" 5 | static let binaryCurrentContext: UInt16 = ".".utf16.first! 6 | static var currentContextCharacter: Character = "." 7 | static var currentContext = "." 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/WindowsFileType.swift: -------------------------------------------------------------------------------- 1 | #if os(Windows) 2 | public struct WindowsFileType: Equatable, FileType { 3 | public let isFile: Bool 4 | public let isDirectory: Bool 5 | public let isSymlink: Bool 6 | } 7 | #endif 8 | -------------------------------------------------------------------------------- /Sources/Pathos/Windows/WindowsPathConvertible.swift: -------------------------------------------------------------------------------- 1 | public protocol WindowsPathConvertible { 2 | var asWindowsPath: PureWindowsPath { get } 3 | } 4 | 5 | extension PureWindowsPath: WindowsPathConvertible { 6 | public var asWindowsPath: PureWindowsPath { self } 7 | } 8 | 9 | extension String: WindowsPathConvertible { 10 | public var asWindowsPath: PureWindowsPath { PureWindowsPath(self) } 11 | } 12 | 13 | extension WindowsBinaryString: WindowsPathConvertible { 14 | public var asWindowsPath: PureWindowsPath { PureWindowsPath(self) } 15 | } 16 | 17 | #if os(Windows) 18 | extension Path: WindowsPathConvertible { 19 | public var asWindowsPath: PureWindowsPath { pure } 20 | } 21 | 22 | public typealias PathConvertible = WindowsPathConvertible 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/WindowsHelpers/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(WindowsHelpers STATIC 2 | dummy.c) 3 | if(NOT BUILD_SHARED_LIBS) 4 | target_compile_definitions(WindowsHelpers PRIVATE 5 | WINDOWSHELPERS_DECLARE_STATIC) 6 | endif() 7 | target_compile_definitions(WindowsHelpers PRIVATE 8 | WINDOWSHELPERS_DECLARE_EXPORT) 9 | target_include_directories(WindowsHelpers PUBLIC 10 | include) 11 | set_property(GLOBAL APPEND PROPERTY Pathos_EXPORTS WindowsHelpers) 12 | install(TARGETS WindowsHelpers 13 | EXPORT PathosExports 14 | RUNTIME DESTINATION bin 15 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 16 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) 17 | install(FILES 18 | include/module.modulemap 19 | include/reparsedata.h 20 | DESTINATION include/WindowsHelpers) 21 | -------------------------------------------------------------------------------- /Sources/WindowsHelpers/dummy.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dduan/Pathos/a12992fd569e7cbbf2e98d686dc662f3435ffcb9/Sources/WindowsHelpers/dummy.c -------------------------------------------------------------------------------- /Sources/WindowsHelpers/include/WindowsHelpers.h: -------------------------------------------------------------------------------- 1 | #include "reparsedata.h" 2 | -------------------------------------------------------------------------------- /Sources/WindowsHelpers/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module WindowsHelpers { 2 | header "reparsedata.h" 3 | header "WindowsHelpers.h" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/WindowsHelpers/include/reparsedata.h: -------------------------------------------------------------------------------- 1 | #ifndef REPARSEDATA_H 2 | #define REPARSEDATA_H 3 | 4 | #if defined(__MINGW32__) 5 | # define WINDOWSHELPERS_DECLARE(type) type 6 | #elif defined(WIN32) 7 | # if defined(WINDOWSHELPERS_DECLARE_STATIC) 8 | # define WINDOWSHELPERS_DECLARE(type) type 9 | # elif defined(WINDOWSHELPERS_DECLARE_EXPORT) 10 | # define WINDOWSHELPERS_DECLARE(type) __declspec(dllexport) type 11 | # else 12 | # define WINDOWSHELPERS_DECLARE(type) __declspec(dllimport) type 13 | # endif 14 | #else 15 | # define WINDOWSHELPERS_DECLARE(type) type 16 | #endif 17 | 18 | #if defined (_WIN64) 19 | #include 20 | #include 21 | 22 | #ifndef IO_REPARSE_TAG_SYMLINK 23 | #define IO_REPARSE_TAG_SYMLINK (0xA000000CL) 24 | #endif 25 | 26 | #ifndef IO_REPARSE_TAG_MOUNT_POINT 27 | #define IO_REPARSE_TAG_MOUNT_POINT (0xA0000003L) 28 | #endif 29 | 30 | typedef struct { 31 | unsigned long reparseTag; 32 | unsigned short reparseDataLength; 33 | unsigned short reserved; 34 | unsigned short substituteNameOffset; 35 | unsigned short substituteNameLength; 36 | unsigned short printNameOffset; 37 | unsigned short printNameLength; 38 | unsigned long flags; 39 | wchar_t pathBuffer[1]; 40 | } ReparseDataBuffer; 41 | 42 | typedef ReparseDataBuffer SymbolicLinkReparseBuffer; 43 | 44 | typedef struct { 45 | unsigned long reparseTag; 46 | unsigned short reparseDataLength; 47 | unsigned short reserved; 48 | unsigned short substituteNameOffset; 49 | unsigned short substituteNameLength; 50 | unsigned short printNameOffset; 51 | unsigned short printNameLength; 52 | wchar_t pathBuffer[1]; 53 | } MountPointReparseBuffer; 54 | #endif 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library("PathosTests" 2 | "PathosTests/AbsoluteTests.swift" 3 | "PathosTests/BaseTests.swift" 4 | "PathosTests/ChildrenTests.swift" 5 | "PathosTests/CopyTests.swift" 6 | "PathosTests/GlobTests.swift" 7 | "PathosTests/HomeTests.swift" 8 | "PathosTests/MetadataTests.swift" 9 | "PathosTests/POSIXBinaryStringTests.swift" 10 | "PathosTests/POSIXPartsParsingTests.swift" 11 | "PathosTests/POSIXPathInitializationTests.swift" 12 | "PathosTests/POSIXPathJoiningTests.swift" 13 | "PathosTests/PathDeletionTests.swift" 14 | "PathosTests/PathExistsTests.swift" 15 | "PathosTests/PathExtensionTests.swift" 16 | "PathosTests/PathJoiningOperatorTests.swift" 17 | "PathosTests/PathJoiningTests.swift" 18 | "PathosTests/PathNormalTests.swift" 19 | "PathosTests/PathParentsTests.swift" 20 | "PathosTests/PermissionsTests.swift" 21 | "PathosTests/PurePOSIXParentTests.swift" 22 | "PathosTests/PurePOSIXPathBaseTests.swift" 23 | "PathosTests/PurePOSIXPathExtensionTests.swift" 24 | "PathosTests/PurePOSIXPathIsAbsoluteTests.swift" 25 | "PathosTests/PurePOSIXPathJoiningOperatorTests.swift" 26 | "PathosTests/PurePOSIXPathNormalTests.swift" 27 | "PathosTests/PurePOSIXPathParentsTests.swift" 28 | "PathosTests/PurePOSIXPathRelativeTests.swift" 29 | "PathosTests/PurePOSIXPathTests.swift" 30 | "PathosTests/PureWindowsExtensionTests.swift" 31 | "PathosTests/PureWindowsPathBaseTests.swift" 32 | "PathosTests/PureWindowsPathIsAbsoluteTests.swift" 33 | "PathosTests/PureWindowsPathJoiningOperatorTests.swift" 34 | "PathosTests/PureWindowsPathNormalTests.swift" 35 | "PathosTests/PureWindowsPathParentTests.swift" 36 | "PathosTests/PureWindowsPathParentsTests.swift" 37 | "PathosTests/PureWindowsPathRelativeTests.swift" 38 | "PathosTests/PureWindowsPathTests.swift" 39 | "PathosTests/ReadStringTests.swift" 40 | "PathosTests/ReadTests.swift" 41 | "PathosTests/RealTests.swift" 42 | "PathosTests/SymlinkTests.swift" 43 | "PathosTests/TemporaryTests.swift" 44 | "PathosTests/WindowsBinaryStringTests.swift" 45 | "PathosTests/WindowsFileTimeTests.swift" 46 | "PathosTests/WindowsPartsParsingTests.swift" 47 | "PathosTests/WindowsPathJoiningTests.swift" 48 | "PathosTests/WorkingDirectoryTests.swift" 49 | "PathosTests/WriteTests.swift" 50 | "PathosTests/XCTestManifests.swift" 51 | ) 52 | 53 | set_target_properties("PathosTests" PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) 54 | target_compile_options("PathosTests" PRIVATE -enable-testing) 55 | target_link_libraries("PathosTests" PRIVATE 56 | "Pathos" 57 | ) 58 | 59 | add_executable(WindowsMain 60 | LinuxMain.swift 61 | ) 62 | 63 | target_link_libraries(WindowsMain PRIVATE 64 | "PathosTests" 65 | "WindowsHelpers" 66 | ) 67 | 68 | add_test(NAME WindowsMain COMMAND WindowsMain) 69 | set_property(TEST WindowsMain PROPERTY ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") 70 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PathosTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PathosTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /Tests/PathosTests/AbsoluteTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class AbsoluteTests: XCTestCase { 5 | func testMakingPathAbsolute() throws { 6 | print(">> \(#line)") 7 | XCTAssert(try Path("a").absolute().isAbsolute) 8 | print(">> \(#line)") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/PathosTests/BaseTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class BaseTests: XCTestCase { 5 | func testBaseDoesNotContainExtension() { 6 | let path = Path("hello") + "world.md" 7 | let base = Path("hello") + "world" 8 | XCTAssertEqual(path.base, base) 9 | } 10 | 11 | func testBaseOfPathWithoutExtensionIsItself() { 12 | let path = Path("hello") + "world" 13 | XCTAssertEqual(path.base, path) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/PathosTests/ChildrenTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class ChildrenTests: XCTestCase { 5 | func testListingChildrenNonRecursively() throws { 6 | try Path.withTemporaryDirectory { container in 7 | try Path("a").write(utf8: "") 8 | try Path("b").makeDirectory() 9 | try (Path("b") + "c").write(utf8: "") 10 | try Path("a").makeSymlink(at: Path("d")) 11 | var children = try container.children() 12 | .map { ($0.name ?? "", $1.isDirectory, $1.isSymlink, $1.isFile) } 13 | children.sort { $0.0 < $1.0 } 14 | XCTAssertEqual(children.count, 3) 15 | 16 | // a is a normal file 17 | XCTAssertEqual("a", children[0].0) 18 | XCTAssert(children[0].3) 19 | 20 | // b is a directory 21 | XCTAssertEqual("b", children[1].0) 22 | XCTAssert(children[1].1) 23 | 24 | // d is a symlink 25 | XCTAssertEqual("d", children[2].0) 26 | XCTAssert(children[2].2) 27 | } 28 | } 29 | 30 | func testListingChildrenRecursively() throws { 31 | try Path.withTemporaryDirectory { _ in 32 | try Path("a").write(utf8: "") 33 | try Path("b").makeDirectory() 34 | try (Path("b") + "c").write(utf8: "") 35 | try Path("a").makeSymlink(at: Path("d")) 36 | var children = try Path(".").children(recursive: true) 37 | .map { ($0.name ?? "", $1.isDirectory, $1.isSymlink, $1.isFile) } 38 | children.sort { $0.0 < $1.0 } 39 | XCTAssertEqual(children.count, 4) 40 | 41 | // a is a normal file 42 | XCTAssertEqual("a", children[0].0) 43 | XCTAssert(children[0].3) 44 | 45 | // b is a directory 46 | XCTAssertEqual("b", children[1].0) 47 | XCTAssert(children[1].1) 48 | 49 | // c is a file 50 | XCTAssertEqual("c", children[2].0) 51 | XCTAssert(children[2].3) 52 | 53 | // d is a symlink 54 | XCTAssertEqual("d", children[3].0) 55 | XCTAssert(children[3].2) 56 | } 57 | } 58 | 59 | func testListingChildrenRecursivelyFollowingSymlink() throws { 60 | try Path.withTemporaryDirectory { _ in 61 | try Path("a").write(utf8: "") 62 | try Path("b").makeDirectory() 63 | try (Path("b") + "e").makeDirectory() 64 | try (Path("b") + "e" + "f").write(utf8: "") 65 | try (Path("b") + "c").write(utf8: "") 66 | try Path("b").makeSymlink(at: Path("d")) 67 | let children = try Path(".").children(recursive: true, followSymlink: true) 68 | let names = Set(children.map(\.0)) 69 | 70 | XCTAssertEqual( 71 | names, 72 | [ 73 | Path(".") + "a", 74 | Path(".") + "b", 75 | Path(".") + "b" + "c", 76 | Path(".") + "b" + "e", 77 | Path(".") + "b" + "e" + "f", 78 | Path(".") + "d", 79 | Path(".") + "d" + "c", 80 | Path(".") + "d" + "e", 81 | Path(".") + "d" + "e" + "f", 82 | ] 83 | ) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/PathosTests/CopyTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class CopyTests: XCTestCase { 5 | func testCopyingNormalFileToNewLocation() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | let content = "xyyyzz" 8 | let sourceFile = Path("a") 9 | try sourceFile.write(utf8: content) 10 | let destinationFile = Path("b") 11 | try sourceFile.copy(to: destinationFile) 12 | XCTAssertEqual(try destinationFile.readUTF8String(), content) 13 | } 14 | } 15 | 16 | func testCopyingNormalFileToNewLocationFollowingSymlink() throws { 17 | try Path.withTemporaryDirectory { _ in 18 | let content = "aoesubaoesurcaoheu" 19 | let sourceFile = Path("a") 20 | try sourceFile.write(utf8: content) 21 | let link = Path("b") 22 | try sourceFile.makeSymlink(at: link) 23 | let destinationFile = Path("c") 24 | try link.copy(to: destinationFile) 25 | XCTAssertEqual(try destinationFile.readUTF8String(), content) 26 | } 27 | } 28 | 29 | func testCopyingSymlinkToNewLocation() throws { 30 | try Path.withTemporaryDirectory { _ in 31 | let content = "aoesubaoesurcaoheu" 32 | let sourceFile = Path("a") 33 | try sourceFile.write(utf8: content) 34 | let link = Path("b") 35 | try sourceFile.makeSymlink(at: link) 36 | let destinationFile = Path("c") 37 | try link.copy(to: destinationFile, followSymlink: false) 38 | XCTAssertEqual(try destinationFile.readSymlink(), sourceFile) 39 | } 40 | } 41 | 42 | func testCopyingToExistingLocationNotFailingExisting() throws { 43 | try Path.withTemporaryDirectory { _ in 44 | let sourceContent = "aaa" 45 | let sourceFile = Path("a") 46 | let destinationFile = Path("b") 47 | 48 | try sourceFile.write(utf8: sourceContent) 49 | try destinationFile.write(utf8: "bbb") 50 | 51 | try sourceFile.copy(to: destinationFile) 52 | XCTAssertEqual(try destinationFile.readUTF8String(), sourceContent) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PathosTests/GlobTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class GlobTests: XCTestCase { 5 | func testSimplePattern() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | let path0 = Path("1.swift") 8 | let path1 = Path("b.swift") 9 | let path2 = Path("3.py") 10 | try path0.write(utf8: "") 11 | try path1.write(utf8: "") 12 | try path2.write(utf8: "") 13 | 14 | XCTAssertEqual( 15 | Set(try Path("*.swift").glob()), 16 | [ 17 | path0, 18 | path1, 19 | ] 20 | ) 21 | } 22 | } 23 | 24 | func testGlobStarPatterns() throws { 25 | try Path.withTemporaryDirectory { _ in 26 | let dirs: [Path] = [ 27 | Path(".") + "x" + "a", 28 | Path(".") + "x" + "b", 29 | Path(".") + "y" + "a", 30 | Path(".") + "y" + "b", 31 | ] 32 | 33 | let files: [Path] = [ 34 | dirs[0] + "1.swift", 35 | dirs[0] + "2.swift", 36 | dirs[1] + "1.swift", 37 | dirs[1] + "2.py", 38 | dirs[2] + "1.swift", 39 | dirs[3] + "2.swift", 40 | ] 41 | 42 | for dir in dirs { 43 | try dir.makeDirectory(withParents: true) 44 | } 45 | 46 | for file in files { 47 | try file.write(utf8: "") 48 | } 49 | 50 | XCTAssertEqual( 51 | Set(try (Path(".") + "x" + "**" + "*.swift").glob()), 52 | [ 53 | files[0], 54 | files[1], 55 | files[2], 56 | ] 57 | ) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/PathosTests/HomeTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class HomeTests: XCTestCase { 5 | func testGettingHome() { 6 | XCTAssert(!Path.home().isEmpty) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/PathosTests/MetadataTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class MetadataTests: XCTestCase { 5 | func testRetrievingDirectoryMetadata() throws { 6 | try Path.withTemporaryDirectory { temp in 7 | let meta = try temp.metadata() 8 | XCTAssertTrue(meta.fileType.isDirectory) 9 | } 10 | } 11 | 12 | func testRetrievingFileMetadata() throws { 13 | try Path.withTemporaryDirectory { _ in 14 | let path = Path("a") 15 | try path.write(utf8: "") 16 | let meta = try path.metadata() 17 | XCTAssertTrue(meta.fileType.isFile) 18 | } 19 | } 20 | 21 | func testRetrievingSymlinkMetadata() throws { 22 | try Path.withTemporaryDirectory { _ in 23 | let path = Path("a") 24 | try path.write(utf8: "") 25 | let link = Path("b") 26 | try path.makeSymlink(at: link) 27 | let meta = try link.metadata() 28 | XCTAssertTrue(meta.fileType.isSymlink) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/PathosTests/POSIXBinaryStringTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class POSIXBinaryStringTests: XCTestCase { 5 | func testDecodingAndEncodingIsCommunitive() { 6 | let content = "A 🎭 二 production" 7 | XCTAssertEqual(POSIXBinaryString(content).description, content) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/PathosTests/POSIXPartsParsingTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class POSIXPartsParsingTests: XCTestCase { 5 | func testRootIsParsed() { 6 | let p = PurePOSIXPath(cString: "/") 7 | XCTAssertEqual(p.root, "/") 8 | } 9 | 10 | func testJust2SlashRoot() { 11 | let p = PurePOSIXPath("//") 12 | XCTAssertEqual(p.root, "//") 13 | } 14 | 15 | func test2SlashRootAndSegments() { 16 | let p = PurePOSIXPath("//a/b") 17 | XCTAssertEqual(p.root, "//") 18 | } 19 | 20 | func test3SlashRoot() { 21 | let p = PurePOSIXPath("///a/b") 22 | XCTAssertEqual(p.root, "/") 23 | } 24 | 25 | func testNoRootIsParsedCorrectly() { 26 | let p = PurePOSIXPath("a/b/c") 27 | XCTAssertNil(p.root) 28 | } 29 | 30 | func testParsingDriveOnPOSIX() { 31 | let p = PurePOSIXPath("C:/a/b") 32 | XCTAssertNil(p.drive) 33 | } 34 | 35 | func testParsingParts() { 36 | let p = PurePOSIXPath("/a/b/c.swift") 37 | XCTAssertEqual( 38 | p.segments, 39 | [ 40 | "a", 41 | "b", 42 | "c.swift", 43 | ] 44 | ) 45 | } 46 | 47 | func testIntermediateCurrentDirectoryIsRemoved() { 48 | let p = PurePOSIXPath("/a/b/./c.swift") 49 | XCTAssertEqual( 50 | p.segments, 51 | [ 52 | "a", 53 | "b", 54 | "c.swift", 55 | ] 56 | ) 57 | } 58 | 59 | func testInitialContextIsPreserved() { 60 | let p = PureWindowsPath(".") 61 | XCTAssertEqual(p.segments, ["."]) 62 | } 63 | 64 | func testInitialContextWithFollowUpIsPreserved() { 65 | let p = PureWindowsPath(#"./a"#) 66 | XCTAssertEqual(p.segments, [".", "a"]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/PathosTests/POSIXPathInitializationTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class POSIXPathInitializationTests: XCTestCase { 5 | func testInitializingFromCString() { 6 | let expectedPath = "/a/b/c" 7 | XCTAssertEqual( 8 | PurePOSIXPath(cString: expectedPath).binaryPath.content[...], 9 | expectedPath.utf8CString.dropLast() 10 | ) 11 | } 12 | 13 | func testInitializationFromString() { 14 | let expectedPath = "/a/b/c" 15 | XCTAssertEqual( 16 | PurePOSIXPath(expectedPath).binaryPath.content[...], 17 | expectedPath.utf8CString.dropLast() 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PathosTests/POSIXPathJoiningTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class POSIXPathJoiningTests: XCTestCase { 5 | func testSimpleJoining() { 6 | let a = PurePOSIXPath("a") 7 | let b = PurePOSIXPath("b") 8 | XCTAssertEqual(a.joined(with: b), PurePOSIXPath("a/b")) 9 | } 10 | 11 | func testSimpleJoiningWithMulilpePaths() { 12 | let a = PurePOSIXPath("a") 13 | let b = PurePOSIXPath("b") 14 | let c = PurePOSIXPath("c") 15 | let d = PurePOSIXPath("d") 16 | XCTAssertEqual(a.joined(with: b, c, d), PurePOSIXPath("a/b/c/d")) 17 | } 18 | 19 | func testJoiningWithExsitingSeparator() { 20 | let a = PurePOSIXPath("a/") 21 | let b = PurePOSIXPath("b") 22 | XCTAssertEqual(a.joined(with: b), PurePOSIXPath("a/b")) 23 | } 24 | 25 | func testSimpleJoiningStartingWithAbsolutePath() { 26 | let a = PurePOSIXPath("/a") 27 | let b = PurePOSIXPath("b") 28 | XCTAssertEqual(a.joined(with: b), PurePOSIXPath("/a/b")) 29 | } 30 | 31 | func testSimpleJoiningEndingWithAbsolutePath() { 32 | let a = PurePOSIXPath("a") 33 | let b = PurePOSIXPath("/b") 34 | XCTAssertEqual(a.joined(with: b), PurePOSIXPath("/b")) 35 | } 36 | 37 | func testSimpleJoiningStartingAndEndingWithAbsolutePath() { 38 | let a = PurePOSIXPath("/a") 39 | let b = PurePOSIXPath("/b") 40 | XCTAssertEqual(a.joined(with: b), PurePOSIXPath("/b")) 41 | } 42 | 43 | func testSimpleJoiningWithMultipleAbsolutePath() { 44 | let a = PurePOSIXPath("a") 45 | let b = PurePOSIXPath("/b/b") 46 | let c = PurePOSIXPath("/c") 47 | let d = PurePOSIXPath("d") 48 | XCTAssertEqual(a.joined(with: b, c, d), PurePOSIXPath("/c/d")) 49 | } 50 | 51 | func testJoiningMixedTypes() { 52 | let a = PurePOSIXPath("a") 53 | let b = "b" 54 | let c = PurePOSIXPath("c") 55 | let d = "d" 56 | XCTAssertEqual(a.joined(with: b, c, d), PurePOSIXPath("a/b/c/d")) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathDeletionTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PathDeletionTests: XCTestCase { 5 | func testNonExistingPathDoesNotThrow() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | try Path("X").delete() 8 | } 9 | } 10 | 11 | func testDeletingNormalFile() throws { 12 | try Path.withTemporaryDirectory { _ in 13 | let p = Path("X") 14 | try p.write(utf8: "x") 15 | try p.delete() 16 | XCTAssertFalse(p.exists()) 17 | } 18 | } 19 | 20 | func testDeletingNormalFileNonRecursive() throws { 21 | try Path.withTemporaryDirectory { _ in 22 | let p = Path("X") 23 | try p.write(utf8: "x") 24 | try p.delete(recursive: false) 25 | XCTAssertFalse(p.exists()) 26 | } 27 | } 28 | 29 | func testDeletingEmptyDirectory() throws { 30 | try Path.withTemporaryDirectory { _ in 31 | let p = Path("X") 32 | try p.makeDirectory() 33 | try p.delete() 34 | XCTAssertFalse(p.exists()) 35 | } 36 | } 37 | 38 | func testDeletingEmptyDirectoryNonRecursive() throws { 39 | try Path.withTemporaryDirectory { _ in 40 | let p = Path("X") 41 | try p.makeDirectory() 42 | try p.delete(recursive: false) 43 | XCTAssertFalse(p.exists()) 44 | } 45 | } 46 | 47 | func testDeletingSymlink() throws { 48 | try Path.withTemporaryDirectory { _ in 49 | let a = Path("a") 50 | try a.write(utf8: "") 51 | let b = Path("b") 52 | try a.makeSymlink(at: b) 53 | try b.delete() 54 | XCTAssertFalse(b.exists()) 55 | } 56 | } 57 | 58 | func testDeletingNonEmptyDirectory() throws { 59 | try Path.withTemporaryDirectory { _ in 60 | let p = Path("p") 61 | try p.makeDirectory() 62 | let a = p + "a" 63 | try a.write(utf8: "") 64 | try p.delete() 65 | XCTAssertFalse(p.exists()) 66 | } 67 | } 68 | 69 | func testDeletingNonEmptyDirectoryNonRecursive() throws { 70 | try Path.withTemporaryDirectory { _ in 71 | let p = Path("p") 72 | try p.makeDirectory() 73 | let a = p + "a" 74 | try a.write(utf8: "") 75 | XCTAssertThrowsError(try p.delete(recursive: false)) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathExistsTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PathExistsTests: XCTestCase { 5 | func testPathExists() { 6 | XCTAssert(Path(".").exists()) 7 | } 8 | 9 | func testPathDoesNotExists() { 10 | XCTAssertFalse(Path("/Path/Does/not/exist/ha/unless/question/mark").exists()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathExtensionTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PathExtensionTests: XCTestCase { 5 | func testSingleExtension() { 6 | XCTAssertEqual(Path("System/Pathos.dll").extension, ".dll") 7 | } 8 | 9 | func testNoExtension() { 10 | XCTAssertNil(Path("Open.Source.Software/Pathos").extension) 11 | } 12 | 13 | func testDotFileNoExtension() { 14 | XCTAssertNil(Path(".vimrc").extension) 15 | } 16 | 17 | func testDotFileWithExtension() { 18 | XCTAssertEqual(Path(".drstring.toml").extension, ".toml") 19 | } 20 | 21 | func testExtensionsForSingleExtension() { 22 | XCTAssertEqual(Path("Pathos/.drstring.toml").extensions, [".toml"]) 23 | } 24 | 25 | func testExtensionsForMultipleExtension() { 26 | XCTAssertEqual(Path("/Downloads/linux.tar.gz").extensions, [".tar", ".gz"]) 27 | } 28 | 29 | func testExtensionsForNoExtension() { 30 | XCTAssertEqual(Path("linux").extensions, []) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathJoiningOperatorTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class PathJoiningOperatorTests: XCTestCase { 5 | func testAddingPaths() { 6 | let result = Path("a") + Path("b") 7 | XCTAssertEqual(result, Path("a").joined(with: "b")) 8 | } 9 | 10 | func testAddingPathAndPurePath() { 11 | let result = Path("a") + PurePath("b") 12 | XCTAssertEqual(result, Path("a").joined(with: "b")) 13 | } 14 | 15 | func testAddingPurePathAndPath() { 16 | let result = PurePath("a") + Path("b") 17 | XCTAssertEqual(result, Path("a").joined(with: "b")) 18 | } 19 | 20 | func testAddingPathAndString() { 21 | let result = Path("a") + "b" 22 | XCTAssertEqual(result, Path("a").joined(with: "b")) 23 | } 24 | 25 | func testAddingStringAndPath() { 26 | let result = "a" + Path("b") 27 | XCTAssertEqual(result, Path("a").joined(with: "b")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathJoiningTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PathJoiningTests: XCTestCase { 5 | private let p = Path(".") 6 | func testJoiningWithLiterals() { 7 | _ = p + "b" + "c" // overload resolution supports this common use case 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathNormalTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PathNormalTests: XCTestCase { 5 | func testEmptyPathBecomesCurrent() { 6 | XCTAssertEqual(Path("").normal, Path(".")) 7 | } 8 | 9 | func testParentDirectoryGetsRemoved() { 10 | XCTAssertEqual((Path("a") + "b" + ".." + "c").normal, Path("a") + "c") 11 | } 12 | 13 | func testRedundantSeparatorsAreRemoved() { 14 | let path = Path("a\(Constants.pathSeparator)\(Constants.pathSeparator)b") 15 | XCTAssertEqual(path.normal, Path("a") + "b") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/PathosTests/PathParentsTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PathParentsTests: XCTestCase { 5 | func testRelativePathIteratingOverParents() { 6 | var results = [Path]() 7 | let seq = Path("a").joined(with: "b", "c").parents 8 | for path in seq { 9 | results.append(path) 10 | } 11 | 12 | XCTAssertEqual( 13 | results, 14 | [ 15 | Path("a").joined(with: "b"), 16 | Path("a"), 17 | Path("."), 18 | ] 19 | ) 20 | } 21 | 22 | func testRelativePathArrayParents() { 23 | XCTAssertEqual( 24 | Array(Path("a").joined(with: "b", "c").parents), 25 | [ 26 | Path("a").joined(with: "b"), 27 | Path("a"), 28 | Path("."), 29 | ] 30 | ) 31 | } 32 | 33 | func testAbsolutePathArrayParents() { 34 | let root = Path("\(Constants.pathSeparator)") 35 | XCTAssertEqual( 36 | Array((root + "a" + "b" + "c").parents), 37 | [ 38 | root + "a" + "b", 39 | root + "a", 40 | root, 41 | ] 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/PathosTests/PermissionsTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PermissionsTests: XCTestCase { 5 | func testMutatingReadOnly() { 6 | #if os(Windows) 7 | var p: Permissions = WindowsAttributes() 8 | #else 9 | var p: Permissions = POSIXPermissions() 10 | #endif 11 | let first = p.isReadOnly 12 | p.isReadOnly.toggle() 13 | XCTAssertNotEqual(first, p.isReadOnly) 14 | p.isReadOnly.toggle() 15 | XCTAssertEqual(first, p.isReadOnly) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXParentTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXParentTests: XCTestCase { 5 | func testNormalRelativeParent() { 6 | XCTAssertEqual(PurePOSIXPath("a/b/c").parent, PurePOSIXPath("a/b")) 7 | } 8 | 9 | func testNormalAbsoluteParent() { 10 | XCTAssertEqual(PurePOSIXPath("/a/b/c").parent, PurePOSIXPath("/a/b")) 11 | } 12 | 13 | func testParentBeingRoot() { 14 | XCTAssertEqual(PurePOSIXPath("/a").parent, PurePOSIXPath("/")) 15 | } 16 | 17 | func testRoot() { 18 | XCTAssertEqual(PurePOSIXPath("/").parent, PurePOSIXPath("/")) 19 | } 20 | 21 | func testNoMoreParent() { 22 | XCTAssertEqual(PurePOSIXPath("a").parent, PurePOSIXPath(".")) 23 | } 24 | 25 | func testCurrentContextParent() { 26 | XCTAssertEqual(PurePOSIXPath(".").parent, PurePOSIXPath(".")) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathBaseTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathBaseTests: XCTestCase { 5 | func testBaseDoesNotContainExtension() { 6 | let path = PurePOSIXPath("hello/world.md") 7 | let base = PurePOSIXPath("hello/world") 8 | XCTAssertEqual(path.base, base) 9 | } 10 | 11 | func testBaseOfPathWithoutExtensionIsItself() { 12 | let path = PurePOSIXPath("hello/world") 13 | XCTAssertEqual(path.base, path) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathExtensionTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathExtensionTests: XCTestCase { 5 | func testSingleExtension() { 6 | XCTAssertEqual(PurePOSIXPath("System/Pathos.dll").extension, ".dll") 7 | } 8 | 9 | func testNoExtension() { 10 | XCTAssertNil(PurePOSIXPath("Open.Source.Software/Pathos").extension) 11 | } 12 | 13 | func testDotFileNoExtension() { 14 | XCTAssertNil(PurePOSIXPath(".vimrc").extension) 15 | } 16 | 17 | func testDotFileWithExtension() { 18 | XCTAssertEqual(PurePOSIXPath(".drstring.toml").extension, ".toml") 19 | } 20 | 21 | func testExtensionsForSingleExtension() { 22 | XCTAssertEqual(PurePOSIXPath("Pathos/.drstring.toml").extensions, [".toml"]) 23 | } 24 | 25 | func testExtensionsForMultipleExtension() { 26 | XCTAssertEqual(PurePOSIXPath("/Downloads/linux.tar.gz").extensions, [".tar", ".gz"]) 27 | } 28 | 29 | func testExtensionsForNoExtension() { 30 | XCTAssertEqual(PurePOSIXPath("linux").extensions, []) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathIsAbsoluteTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathIsAbsoluteTests: XCTestCase { 5 | func testNoRootNoDriveIsNotAbsolute() { 6 | XCTAssertFalse(PurePOSIXPath(#"a/b"#).isAbsolute) 7 | } 8 | 9 | func testNoDriveIsAbsolute() { 10 | XCTAssert(PurePOSIXPath("/a/b").isAbsolute) 11 | } 12 | 13 | func testNoDriveTwoSlashesIsAbsolute() { 14 | XCTAssert(PurePOSIXPath("//a/b").isAbsolute) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathJoiningOperatorTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathJoiningOperatorTests: XCTestCase { 5 | func testAddingPurePOSIXPaths() { 6 | let result = PurePOSIXPath("/") + PurePOSIXPath("a") + PurePOSIXPath("b") 7 | XCTAssertEqual(result, PurePOSIXPath("/a/b")) 8 | } 9 | 10 | func testAddingPurePOSIXPathAndString() { 11 | let result = PurePOSIXPath("/a") + "b" 12 | XCTAssertEqual(result, PurePOSIXPath("/a/b")) 13 | } 14 | 15 | func testAddingStringAndPurePOSIXPath() { 16 | let result = "/a" + PurePOSIXPath("b") 17 | XCTAssertEqual(result, PurePOSIXPath("/a/b")) 18 | } 19 | 20 | func testAddingPurePOSIXPathAndPOSIXBinaryString() { 21 | let result = PurePOSIXPath("/a") + POSIXBinaryString("b") 22 | XCTAssertEqual(result, PurePOSIXPath("/a/b")) 23 | } 24 | 25 | func testAddingPOSIXBinaryStringAndPurePath() { 26 | let result = POSIXBinaryString("/") + PurePOSIXPath("a/b") 27 | XCTAssertEqual(result, PurePOSIXPath("/a/b")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathNormalTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathNormalTests: XCTestCase { 5 | func testEmptyPathBecomesCurrentContext() { 6 | XCTAssertEqual(PurePOSIXPath("").normal, PurePOSIXPath(".")) 7 | } 8 | 9 | func testRoots() { 10 | XCTAssertEqual(PurePOSIXPath("/").normal, PurePOSIXPath("/")) 11 | XCTAssertEqual(PurePOSIXPath("//").normal, PurePOSIXPath("//")) 12 | XCTAssertEqual(PurePOSIXPath("///").normal, PurePOSIXPath("/")) 13 | } 14 | 15 | func testNormalization() { 16 | XCTAssertEqual(PurePOSIXPath("///foo/.//bar//").normal, PurePOSIXPath("/foo/bar")) 17 | XCTAssertEqual(PurePOSIXPath("///foo/.//bar//.//..//.//baz").normal, PurePOSIXPath("/foo/baz")) 18 | XCTAssertEqual(PurePOSIXPath("///..//./foo/.//bar").normal, PurePOSIXPath("/foo/bar")) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathParentsTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathParentsTests: XCTestCase { 5 | func testAbsolutePathIteratingOverParents() { 6 | var results = [PurePOSIXPath]() 7 | for path in PurePOSIXPath("/a/b/c").parents { 8 | results.append(path) 9 | } 10 | 11 | XCTAssertEqual( 12 | results, 13 | [ 14 | PurePOSIXPath("/a/b"), 15 | PurePOSIXPath("/a"), 16 | PurePOSIXPath("/"), 17 | ] 18 | ) 19 | } 20 | 21 | func testAbsolutePathArrayParents() { 22 | XCTAssertEqual( 23 | Array(PurePOSIXPath("/a/b/c").parents), 24 | [ 25 | PurePOSIXPath("/a/b"), 26 | PurePOSIXPath("/a"), 27 | PurePOSIXPath("/"), 28 | ] 29 | ) 30 | } 31 | 32 | func testRelativePathIteratingOverParents() { 33 | var results = [PurePOSIXPath]() 34 | for path in PurePOSIXPath("a/b/c").parents { 35 | results.append(path) 36 | } 37 | 38 | XCTAssertEqual( 39 | results, 40 | [ 41 | PurePOSIXPath("a/b"), 42 | PurePOSIXPath("a"), 43 | PurePOSIXPath("."), 44 | ] 45 | ) 46 | } 47 | 48 | func testRelativePathArrayParents() { 49 | XCTAssertEqual( 50 | Array(PurePOSIXPath("a/b/c").parents), 51 | [ 52 | PurePOSIXPath("a/b"), 53 | PurePOSIXPath("a"), 54 | PurePOSIXPath("."), 55 | ] 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathRelativeTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PurePOSIXPathRelativeTests: XCTestCase { 5 | func testBasicRelativity() { 6 | XCTAssertEqual( 7 | PurePOSIXPath("/").relative(to: "/home/dan"), 8 | PurePOSIXPath("../..") 9 | ) 10 | } 11 | 12 | func testRelativeToSelf() { 13 | XCTAssertEqual( 14 | PurePOSIXPath("a").relative(to: "a"), 15 | PurePOSIXPath(".") 16 | ) 17 | } 18 | 19 | func testAbsoluteSibling() { 20 | XCTAssertEqual( 21 | PurePOSIXPath("/a/b").relative(to: "/x/y"), 22 | PurePOSIXPath("../../a/b") 23 | ) 24 | } 25 | 26 | func testAbsoluteChild() { 27 | XCTAssertEqual( 28 | PurePOSIXPath("/a/b/c").relative(to: "/a/b"), 29 | PurePOSIXPath("c") 30 | ) 31 | 32 | XCTAssertEqual( 33 | PurePOSIXPath("/a/b/c").relative(to: "/"), 34 | PurePOSIXPath("a/b/c") 35 | ) 36 | } 37 | 38 | func testAbsoluteParent() { 39 | XCTAssertEqual( 40 | PurePOSIXPath("/").relative(to: "/a/b/c"), 41 | PurePOSIXPath("../../..") 42 | ) 43 | } 44 | 45 | func testAbsoluteRoot() { 46 | XCTAssertEqual( 47 | PurePOSIXPath("/").relative(to: "/"), 48 | PurePOSIXPath(".") 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/PathosTests/PurePOSIXPathTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class POSIXPurePathTests: XCTestCase { 5 | func testNameWithASuffix() { 6 | XCTAssertEqual( 7 | PurePOSIXPath("/a/b/c.swift").name, 8 | "c.swift" 9 | ) 10 | } 11 | 12 | func testNameWithMultipleSuffixes() { 13 | XCTAssertEqual( 14 | PurePOSIXPath("/a/b/c.tar.gz").name, 15 | "c.tar.gz" 16 | ) 17 | } 18 | 19 | func testNameWithNoSuffix() { 20 | XCTAssertEqual( 21 | PurePOSIXPath("/a/b/c").name, 22 | "c" 23 | ) 24 | } 25 | 26 | func testNameFromPath() { 27 | XCTAssertEqual( 28 | PurePOSIXPath("/a/b/c/").name, 29 | "c" 30 | ) 31 | } 32 | 33 | func testNameWithOnlyRoot() { 34 | XCTAssertNil(PurePOSIXPath("/").name) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsExtensionTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathExtensionTests: XCTestCase { 5 | func testSingleExtension() { 6 | XCTAssertEqual(PureWindowsPath(#"System\Pathos.dll"#).extension, ".dll") 7 | } 8 | 9 | func testNoExtension() { 10 | XCTAssertNil(PureWindowsPath(#"Open.Source.Software\Pathos"#).extension) 11 | } 12 | 13 | func testDotFileNoExtension() { 14 | XCTAssertNil(PureWindowsPath(".vimrc").extension) 15 | } 16 | 17 | func testDotFileWithExtension() { 18 | XCTAssertEqual(PureWindowsPath(".drstring.toml").extension, ".toml") 19 | } 20 | 21 | func testExtensionsForSingleExtension() { 22 | XCTAssertEqual(PureWindowsPath(#"Pathos\.drstring.toml"#).extensions, [".toml"]) 23 | } 24 | 25 | func testExtensionsForMultipleExtension() { 26 | XCTAssertEqual(PureWindowsPath(#"C:\Downloads\linux.tar.gz"#).extensions, [".tar", ".gz"]) 27 | } 28 | 29 | func testExtensionsForNoExtension() { 30 | XCTAssertEqual(PureWindowsPath("linux").extensions, []) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathBaseTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathBaseTests: XCTestCase { 5 | func testBaseDoesNotContainExtension() { 6 | let path = PureWindowsPath(#"hello\world.md"#) 7 | let base = PureWindowsPath(#"hello\world"#) 8 | XCTAssertEqual(path.base, base) 9 | } 10 | 11 | func testBaseOfPathWithoutExtensionIsItself() { 12 | let path = PureWindowsPath(#"hello\world"#) 13 | XCTAssertEqual(path.base, path) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathIsAbsoluteTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathIsAbsoluteTests: XCTestCase { 5 | func testNoRootNoDriveIsNotAbsolute() { 6 | XCTAssertFalse(PureWindowsPath(#"a\b"#).isAbsolute) 7 | } 8 | 9 | func testNoDriveIsNotAbsolute() { 10 | XCTAssertFalse(PureWindowsPath(#"\a\b"#).isAbsolute) 11 | } 12 | 13 | func testNoRootIsNotAbsolute() { 14 | XCTAssertFalse(PureWindowsPath(#"C:a\b"#).isAbsolute) 15 | } 16 | 17 | func testIsAbsolute() { 18 | XCTAssert(PureWindowsPath(#"C:\a\b"#).isAbsolute) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathJoiningOperatorTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathJoiningOperatorTests: XCTestCase { 5 | func testAddingPureWindowsPath() { 6 | let result = PureWindowsPath(#"\"#) + PureWindowsPath("a") + PureWindowsPath("b") 7 | XCTAssertEqual(result, PureWindowsPath(#"\a\b"#)) 8 | } 9 | 10 | func testAddingPureWindowsPathAndString() { 11 | let result = PureWindowsPath(#"\a"#) + "b" 12 | XCTAssertEqual(result, PureWindowsPath(#"\a\b"#)) 13 | } 14 | 15 | func testAddingStringAndPureWindowsPath() { 16 | let result = #"\a"# + PureWindowsPath("b") 17 | XCTAssertEqual(result, PureWindowsPath(#"\a\b"#)) 18 | } 19 | 20 | func testAddingPureWindowsPathAndWindowsBinaryString() { 21 | let result = PureWindowsPath(#"\a"#) + WindowsBinaryString("b") 22 | XCTAssertEqual(result, PureWindowsPath(#"\a\b"#)) 23 | } 24 | 25 | func testAddingWindowsBinaryStringAndPurePath() { 26 | let result = WindowsBinaryString(#"\"#) + PureWindowsPath(#"a\b"#) 27 | XCTAssertEqual(result, PureWindowsPath(#"\a\b"#)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathNormalTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathNormalTests: XCTestCase { 5 | func testEmptyPathBecomesCurrentContext() { 6 | XCTAssertEqual(PureWindowsPath("").normal, PureWindowsPath(#"."#)) 7 | } 8 | 9 | func testRoots() { 10 | XCTAssertEqual(PureWindowsPath(#"\"#).normal, PureWindowsPath(#"\"#)) 11 | XCTAssertEqual(PureWindowsPath(#"\\"#).normal, PureWindowsPath(#"\"#)) 12 | XCTAssertEqual(PureWindowsPath(#"\\\"#).normal, PureWindowsPath(#"\"#)) 13 | } 14 | 15 | func testNormalization() { 16 | XCTAssertEqual(PureWindowsPath(#"\\\foo\.\\bar\\"#).normal, PureWindowsPath(#"\foo\bar"#)) 17 | XCTAssertEqual(PureWindowsPath(#"\\\foo\.\\bar\\.\\..\\.\\baz"#).normal, PureWindowsPath(#"\foo\baz"#)) 18 | XCTAssertEqual(PureWindowsPath(#"\\\..\\.\foo\.\\bar"#).normal, PureWindowsPath(#"\foo\bar"#)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathParentTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsParentTests: XCTestCase { 5 | func testNormalRelativeParent() { 6 | XCTAssertEqual(PureWindowsPath(#"a\b\c"#).parent, PureWindowsPath(#"a\b"#)) 7 | } 8 | 9 | func testNormalAbsoluteParent() { 10 | XCTAssertEqual(PureWindowsPath(#"C:\a\b\c"#).parent, PureWindowsPath(#"C:\a\b"#)) 11 | } 12 | 13 | func testNormalParentWithRoot() { 14 | XCTAssertEqual(PureWindowsPath(#"\a\b\c"#).parent, PureWindowsPath(#"\a\b"#)) 15 | } 16 | 17 | func testParentBeingRoot() { 18 | XCTAssertEqual(PureWindowsPath(#"\a"#).parent, PureWindowsPath(#"\"#)) 19 | } 20 | 21 | func testParentBeingDrive() { 22 | XCTAssertEqual(PureWindowsPath(#"C:a"#).parent, PureWindowsPath(#"C:"#)) 23 | } 24 | 25 | func testParentBeingAnchor() { 26 | XCTAssertEqual(PureWindowsPath(#"C:\a"#).parent, PureWindowsPath(#"C:\"#)) 27 | } 28 | 29 | func testRoot() { 30 | XCTAssertEqual(PureWindowsPath(#"\"#).parent, PureWindowsPath(#"\"#)) 31 | } 32 | 33 | func testDrive() { 34 | XCTAssertEqual(PureWindowsPath(#"C:"#).parent, PureWindowsPath(#"C:"#)) 35 | } 36 | 37 | func testAnchor() { 38 | XCTAssertEqual(PureWindowsPath(#"C:\"#).parent, PureWindowsPath(#"C:\"#)) 39 | } 40 | 41 | func testNoMoreParent() { 42 | XCTAssertEqual(PureWindowsPath(#"a"#).parent, PureWindowsPath(#"."#)) 43 | } 44 | 45 | func testCurrentContextParent() { 46 | XCTAssertEqual(PureWindowsPath(#"."#).parent, PureWindowsPath(#"."#)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathParentsTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathParentsTests: XCTestCase { 5 | func testAbsolutePathIteratingOverParents() { 6 | var results = [PureWindowsPath]() 7 | for path in PureWindowsPath(#"D:\a\b\c"#).parents { 8 | results.append(path) 9 | } 10 | 11 | XCTAssertEqual( 12 | results, 13 | [ 14 | PureWindowsPath(#"D:\a\b"#), 15 | PureWindowsPath(#"D:\a"#), 16 | PureWindowsPath(#"D:\"#), 17 | ] 18 | ) 19 | } 20 | 21 | func testAbsolutePathArrayParents() { 22 | XCTAssertEqual( 23 | Array(PureWindowsPath(#"D:\a\b\c"#).parents), 24 | [ 25 | PureWindowsPath(#"D:\a\b"#), 26 | PureWindowsPath(#"D:\a"#), 27 | PureWindowsPath(#"D:\"#), 28 | ] 29 | ) 30 | } 31 | 32 | func testPathWithDriveIteratingOverParents() { 33 | var results = [PureWindowsPath]() 34 | for path in PureWindowsPath(#"D:a\b\c"#).parents { 35 | results.append(path) 36 | } 37 | 38 | XCTAssertEqual( 39 | results, 40 | [ 41 | PureWindowsPath(#"D:a\b"#), 42 | PureWindowsPath(#"D:a"#), 43 | PureWindowsPath(#"D:"#), 44 | ] 45 | ) 46 | } 47 | 48 | func testPathWithDriveArrayParents() { 49 | XCTAssertEqual( 50 | Array(PureWindowsPath(#"D:a\b\c"#).parents), 51 | [ 52 | PureWindowsPath(#"D:a\b"#), 53 | PureWindowsPath(#"D:a"#), 54 | PureWindowsPath(#"D:"#), 55 | ] 56 | ) 57 | } 58 | 59 | func testPathWithRootIteratingOverParents() { 60 | var results = [PureWindowsPath]() 61 | for path in PureWindowsPath(#"\a\b\c"#).parents { 62 | results.append(path) 63 | } 64 | 65 | XCTAssertEqual( 66 | results, 67 | [ 68 | PureWindowsPath(#"\a\b"#), 69 | PureWindowsPath(#"\a"#), 70 | PureWindowsPath(#"\"#), 71 | ] 72 | ) 73 | } 74 | 75 | func testPathWithRootArrayParents() { 76 | XCTAssertEqual( 77 | Array(PureWindowsPath(#"\a\b\c"#).parents), 78 | [ 79 | PureWindowsPath(#"\a\b"#), 80 | PureWindowsPath(#"\a"#), 81 | PureWindowsPath(#"\"#), 82 | ] 83 | ) 84 | } 85 | 86 | func testRelativePathIteratingOverParents() { 87 | var results = [PureWindowsPath]() 88 | for path in PureWindowsPath(#"a\b\c"#).parents { 89 | results.append(path) 90 | } 91 | 92 | XCTAssertEqual( 93 | results, 94 | [ 95 | PureWindowsPath(#"a\b"#), 96 | PureWindowsPath(#"a"#), 97 | PureWindowsPath(#"."#), 98 | ] 99 | ) 100 | } 101 | 102 | func testRelativePathArrayParents() { 103 | XCTAssertEqual( 104 | Array(PureWindowsPath(#"a\b\c"#).parents), 105 | [ 106 | PureWindowsPath(#"a\b"#), 107 | PureWindowsPath(#"a"#), 108 | PureWindowsPath(#"."#), 109 | ] 110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathRelativeTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class PureWindowsPathRelativeTests: XCTestCase { 5 | func testBasicRelativity() { 6 | XCTAssertEqual( 7 | PureWindowsPath(#"\"#).relative(to: #"\home\dan"#), 8 | PureWindowsPath(#"..\.."#) 9 | ) 10 | } 11 | 12 | func testRelativeToSelf() { 13 | XCTAssertEqual( 14 | PureWindowsPath(#"a"#).relative(to: #"a"#), 15 | PureWindowsPath(#"."#) 16 | ) 17 | } 18 | 19 | func testAbsoluteSibling() { 20 | XCTAssertEqual( 21 | PureWindowsPath(#"C:\a\b"#).relative(to: #"C:\x\y"#), 22 | PureWindowsPath(#"..\..\a\b"#) 23 | ) 24 | } 25 | 26 | func testAbsoluteChild() { 27 | XCTAssertEqual( 28 | PureWindowsPath(#"C:\a\b\c"#).relative(to: #"C:\a\b"#), 29 | PureWindowsPath(#"c"#) 30 | ) 31 | 32 | XCTAssertEqual( 33 | PureWindowsPath(#"C:\a\b\c"#).relative(to: #"C:\"#), 34 | PureWindowsPath(#"a\b\c"#) 35 | ) 36 | } 37 | 38 | func testAbsoluteParent() { 39 | XCTAssertEqual( 40 | PureWindowsPath(#"C:\"#).relative(to: #"C:\a\b\c"#), 41 | PureWindowsPath(#"..\..\.."#) 42 | ) 43 | } 44 | 45 | func testAbsoluteRoot() { 46 | XCTAssertEqual( 47 | PureWindowsPath(#"D:\"#).relative(to: #"D:\"#), 48 | PureWindowsPath(#"."#) 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/PathosTests/PureWindowsPathTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class WindowsPurePathTests: XCTestCase { 5 | func testNameWithASuffix() { 6 | XCTAssertEqual( 7 | PureWindowsPath(#"C:\a\b\c.swift"#).name, 8 | "c.swift" 9 | ) 10 | 11 | XCTAssertEqual( 12 | PureWindowsPath(#"\a\b\c.swift"#).name, 13 | "c.swift" 14 | ) 15 | } 16 | 17 | func testNameWithMultipleSuffixes() { 18 | XCTAssertEqual( 19 | PureWindowsPath(#"C:\a\b\c.tar.gz"#).name, 20 | "c.tar.gz" 21 | ) 22 | 23 | XCTAssertEqual( 24 | PureWindowsPath(#"\a\b\c.tar.gz"#).name, 25 | "c.tar.gz" 26 | ) 27 | } 28 | 29 | func testNameWithNoSuffix() { 30 | XCTAssertEqual( 31 | PureWindowsPath(#"C:\a\b\c"#).name, 32 | "c" 33 | ) 34 | 35 | XCTAssertEqual( 36 | PureWindowsPath(#"\a\b\c"#).name, 37 | "c" 38 | ) 39 | } 40 | 41 | func testNameFromPath() { 42 | XCTAssertEqual( 43 | PureWindowsPath(#"C:\a\b\c\"#).name, 44 | "c" 45 | ) 46 | 47 | XCTAssertEqual( 48 | PureWindowsPath(#"\a\b\c\"#).name, 49 | "c" 50 | ) 51 | } 52 | 53 | func testNameWithOnlyDriveAndRoot() { 54 | XCTAssertNil(PureWindowsPath(#"C:\"#).name) 55 | } 56 | 57 | func testNameWithOnlyRoot() { 58 | XCTAssertNil(PureWindowsPath(#"\"#).name) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/PathosTests/ReadStringTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class ReadStringTests: XCTestCase { 5 | func testReadingWithEncoding() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | let filePath = Path("a") 8 | try filePath.write(bytes: [UInt8(97), 97]) 9 | XCTAssertEqual("慡", try filePath.readString(as: UTF16.self)) 10 | XCTAssertEqual("aa", try filePath.readString(as: UTF8.self)) 11 | } 12 | } 13 | 14 | func testReadingWithoutEncoding() throws { 15 | try Path.withTemporaryDirectory { _ in 16 | let filePath = Path("a") 17 | try filePath.write(bytes: [UInt8(97), 97]) 18 | try XCTAssertEqual(filePath.readString(as: UTF8.self), filePath.readUTF8String()) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PathosTests/ReadTests.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Tests/PathosTests/RealTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class RealTests: XCTestCase { 5 | func testResolvingSimpleSymlink() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | let origin = Path("a") 8 | try origin.write(utf8: "") 9 | let link = Path("b") 10 | try origin.makeSymlink(at: link) 11 | let real = try link.real() 12 | XCTAssert(real.isAbsolute) 13 | XCTAssertEqual(real.name, "a") 14 | } 15 | } 16 | 17 | func testResolving2LevelSymlink() throws { 18 | try Path.withTemporaryDirectory { _ in 19 | let origin = Path("a") 20 | try origin.write(utf8: "") 21 | let link1 = Path("b") 22 | let link2 = Path("c") 23 | try origin.makeSymlink(at: link1) 24 | try link1.makeSymlink(at: link2) 25 | let real = try link2.real() 26 | XCTAssert(real.isAbsolute) 27 | XCTAssertEqual(real.name, "a") 28 | } 29 | } 30 | 31 | func testLinkAsPathSegment() throws { 32 | try Path.withTemporaryDirectory { _ in 33 | let dir = Path("dir") 34 | try dir.makeDirectory() 35 | try (dir + "a").write(utf8: "") 36 | let dirLink = Path("dirlink") 37 | try dir.makeSymlink(at: dirLink) 38 | let file = dirLink + "a" 39 | let real = try file.real() 40 | XCTAssert(real.isAbsolute) 41 | XCTAssertEqual(real.segments.suffix(2), ["dir", "a"]) 42 | 43 | try dirLink.delete() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/PathosTests/SymlinkTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class SymlinkTests: XCTestCase { 5 | func testMakingAndReadingSymlinks() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | let source = Path("a") 8 | try source.write(utf8: "a") 9 | let link = Path("b") 10 | try source.makeSymlink(at: link) 11 | XCTAssertEqual(try link.readSymlink(), source) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/PathosTests/TemporaryTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class TemporaryTests: XCTestCase { 5 | func testDefaultTemporaryPathExists() { 6 | XCTAssert(Path.defaultTemporaryDirectory.exists()) 7 | } 8 | 9 | func testMakingTemporaryDirectory() throws { 10 | let path = try Path.makeTemporaryDirectory() 11 | defer { 12 | try? path.delete(recursive: true) 13 | } 14 | 15 | XCTAssert(path.exists()) 16 | } 17 | 18 | func testMakingTemporaryDirectoryWithPrefix() throws { 19 | let prefix = "test_prefix" 20 | let path = try Path.makeTemporaryDirectory(prefix: prefix) 21 | defer { 22 | try? path.delete(recursive: true) 23 | } 24 | 25 | XCTAssert(path.exists()) 26 | print(path.description) 27 | XCTAssert(try XCTUnwrap(path.name).hasPrefix(prefix)) 28 | } 29 | 30 | func testMakingTemporaryDirectoryWithSuffix() throws { 31 | let suffix = "suffix_test" 32 | let path = try Path.makeTemporaryDirectory(suffix: suffix) 33 | defer { 34 | try? path.delete(recursive: true) 35 | } 36 | 37 | XCTAssert(path.exists()) 38 | print(path.description) 39 | XCTAssert(try XCTUnwrap(path.name).hasSuffix(suffix)) 40 | } 41 | 42 | func testWithTemporaryDirectory() throws { 43 | var path: Path? 44 | try Path.withTemporaryDirectory { tempPath in 45 | path = tempPath 46 | XCTAssert(tempPath.exists()) 47 | XCTAssertEqual( 48 | try XCTUnwrap(Path.workingDirectory().name), 49 | try XCTUnwrap(tempPath.name) 50 | ) 51 | } 52 | 53 | XCTAssertFalse(try XCTUnwrap(path).exists()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PathosTests/WindowsBinaryStringTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class WindowsBinaryStringTests: XCTestCase { 5 | func testDecodingAndEncodingIsCommunitive() { 6 | let content = "A 🎭 二 production" 7 | XCTAssertEqual(WindowsBinaryString(content).description, content) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/PathosTests/WindowsFileTimeTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | #if os(Windows) 3 | import WinSDK 4 | #endif 5 | import XCTest 6 | 7 | final class WindowsFileTimeTests: XCTestCase { 8 | func testCovertingFILETIME() { 9 | #if os(Windows) 10 | let epochInHundredNanos: UInt64 = 11_644_473_600 * 10_000_000 11 | var filetime = FILETIME() 12 | filetime.dwHighDateTime = DWORD(epochInHundredNanos >> 32) 13 | filetime.dwLowDateTime = DWORD((epochInHundredNanos << 32) >> 32) 14 | let result = FileTime(filetime) 15 | 16 | XCTAssertEqual(result.seconds, 0) 17 | XCTAssertEqual(result.nanoseconds, 0) 18 | #endif 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PathosTests/WindowsPartsParsingTests.swift: -------------------------------------------------------------------------------- 1 | @testable import Pathos 2 | import XCTest 3 | 4 | final class WindowsPartsParsingTests: XCTestCase { 5 | func testRootIsParsed() { 6 | let p = PureWindowsPath(#"\"#) 7 | XCTAssertEqual(p.root, #"\"#) 8 | } 9 | 10 | func test2SlashRoot() { 11 | let p = PureWindowsPath(#"\\a\b"#) 12 | XCTAssertEqual(p.drive, #"\\a\b"#) 13 | XCTAssertNil(p.root) 14 | } 15 | 16 | func test3SlashRoot() { 17 | let p = PureWindowsPath(#"\\\a\b"#) 18 | XCTAssertEqual(p.root, #"\"#) 19 | } 20 | 21 | func testNoRootIsParsedCorrectly() { 22 | let p = PureWindowsPath(#"a\b\c"#) 23 | XCTAssertNil(p.root) 24 | } 25 | 26 | func testParsingDriveOnWindows() { 27 | let p = PureWindowsPath(#"C:\a\b"#) 28 | XCTAssertEqual(p.drive, "C:") 29 | } 30 | 31 | func testParsingDriveOnWindows2() { 32 | let p = PureWindowsPath(#"C:"#) 33 | XCTAssertEqual(p.drive, "C:") 34 | } 35 | 36 | func testParsingDriveOnWindows3() { 37 | let p = PureWindowsPath(#"\\?\C:\Users\dan\src"#) 38 | XCTAssertEqual(p.drive, #"\\?\C:"#) 39 | } 40 | 41 | func testParsingUNCDriveOnWindows() { 42 | let p = PureWindowsPath(#"\\drive\name\a\b"#) 43 | XCTAssertEqual(p.drive, #"\\drive\name"#) 44 | } 45 | 46 | func testParsingParts() { 47 | let p = PureWindowsPath(#"\a\b\c.swift"#) 48 | XCTAssertEqual( 49 | p.segments, 50 | [ 51 | "a", 52 | "b", 53 | "c.swift", 54 | ] 55 | ) 56 | } 57 | 58 | func testIntermediateCurrentDirectoryIsRemoved() { 59 | let p = PureWindowsPath(#"\a\b\.\c.swift"#) 60 | XCTAssertEqual( 61 | p.segments, 62 | [ 63 | "a", 64 | "b", 65 | "c.swift", 66 | ] 67 | ) 68 | } 69 | 70 | func testInitialContextIsPreserved() { 71 | let p = PureWindowsPath(#"."#) 72 | XCTAssertEqual(p.segments, ["."]) 73 | } 74 | 75 | func testInitialContextWithFollowUpIsPreserved() { 76 | let p = PureWindowsPath(#".\a"#) 77 | XCTAssertEqual(p.segments, [".", "a"]) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/PathosTests/WindowsPathJoiningTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class WindowsPathJoiningTests: XCTestCase { 5 | func testSimpleJoining() { 6 | let a = PureWindowsPath("a") 7 | let b = PureWindowsPath("b") 8 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"a\b"#)) 9 | } 10 | 11 | func testSimpleJoiningWithMulilpePaths() { 12 | let a = PureWindowsPath("a") 13 | let b = PureWindowsPath("b") 14 | let c = PureWindowsPath("c") 15 | let d = PureWindowsPath("d") 16 | XCTAssertEqual(a.joined(with: b, c, d), PureWindowsPath(#"a\b\c\d"#)) 17 | } 18 | 19 | func testJoiningWithExsitingSeparator() { 20 | let a = PureWindowsPath(#"a\"#) 21 | let b = PureWindowsPath("b") 22 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"a\b"#)) 23 | } 24 | 25 | func testSimpleJoiningStartingWithAbsolutePath() { 26 | let a = PureWindowsPath(#"\a"#) 27 | let b = PureWindowsPath("b") 28 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"\a\b"#)) 29 | } 30 | 31 | func testSimpleJoiningStartingWithDrive() { 32 | let a = PureWindowsPath(#"C:a"#) 33 | let b = PureWindowsPath("b") 34 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:a\b"#)) 35 | } 36 | 37 | func testSimpleJoiningStartingWithDriveAndRoot() { 38 | let a = PureWindowsPath(#"C:\a"#) 39 | let b = PureWindowsPath("b") 40 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:\a\b"#)) 41 | } 42 | 43 | func testSimpleJoiningEndingWithAbsolutePath() { 44 | let a = PureWindowsPath("a") 45 | let b = PureWindowsPath(#"\b"#) 46 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"\b"#)) 47 | } 48 | 49 | func testSimpleJoiningEndingWithDrive() { 50 | let a = PureWindowsPath("a") 51 | let b = PureWindowsPath(#"C:b"#) 52 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:b"#)) 53 | } 54 | 55 | func testSimpleJoiningEndingWithDriveAndRoot() { 56 | let a = PureWindowsPath("a") 57 | let b = PureWindowsPath(#"C:\b"#) 58 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:\b"#)) 59 | } 60 | 61 | func testSimpleJoiningStartingAndEndingWithRoot() { 62 | let a = PureWindowsPath(#"\a"#) 63 | let b = PureWindowsPath(#"\b"#) 64 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"\b"#)) 65 | } 66 | 67 | func testSimpleJoiningStartingAndEndingWithSameDrive() { 68 | let a = PureWindowsPath(#"C:a"#) 69 | let b = PureWindowsPath(#"C:b"#) 70 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:b"#)) 71 | } 72 | 73 | func testSimpleJoiningStartingAndEndingWithDifferentDrive() { 74 | let a = PureWindowsPath(#"\\unc\a\a"#) 75 | let b = PureWindowsPath(#"C:b"#) 76 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:b"#)) 77 | } 78 | 79 | func testJoiningStartingWithDriveAndEndingWithRoot() { 80 | let a = PureWindowsPath(#"C:a"#) 81 | let b = PureWindowsPath(#"\b"#) 82 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"C:\b"#)) 83 | } 84 | 85 | func testJoiningStartingWithRootAndEndingWithDrive() { 86 | let a = PureWindowsPath(#"\a"#) 87 | let b = PureWindowsPath(#"D:b"#) 88 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"D:b"#)) 89 | } 90 | 91 | func testJoiningDriveOnlyWithPath() { 92 | let a = PureWindowsPath(#"D:"#) 93 | let b = PureWindowsPath(#"b"#) 94 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"D:b"#)) 95 | } 96 | 97 | func testJoiningDriveOnlyWithRoot() { 98 | let a = PureWindowsPath(#"D:"#) 99 | let b = PureWindowsPath(#"\"#) 100 | XCTAssertEqual(a.joined(with: b), PureWindowsPath(#"D:\"#)) 101 | } 102 | 103 | func testSimpleJoiningWithMultipleAbsolutePath() { 104 | let a = PureWindowsPath("a") 105 | let b = PureWindowsPath(#"\b\b"#) 106 | let c = PureWindowsPath(#"\\unc\c\c"#) 107 | let d = PureWindowsPath("d") 108 | XCTAssertEqual(a.joined(with: b, c, d), PureWindowsPath(#"\\unc\c\c\d"#)) 109 | } 110 | 111 | func testJoiningMixedTypes() { 112 | let a = PureWindowsPath("a") 113 | let b = "b" 114 | let c = PureWindowsPath("c") 115 | let d = "d" 116 | XCTAssertEqual(a.joined(with: b, c, d), PureWindowsPath(#"a\b\c\d"#)) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Tests/PathosTests/WorkingDirectoryTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class WorkingDirectoryTests: XCTestCase { 5 | func testGettingWorkingDirecotry() throws { 6 | XCTAssertNoThrow(try Path.workingDirectory()) 7 | } 8 | 9 | func testSettingWorkingDirecotry() throws { 10 | let path = try Path.workingDirectory() 11 | XCTAssertNoThrow(try Path.setWorkingDirectory(path)) 12 | } 13 | 14 | func testAsWorkingDirectory() throws { 15 | let path = try Path.workingDirectory() 16 | var actionExecuted = false 17 | try path.asWorkingDirectory { 18 | actionExecuted = true 19 | } 20 | XCTAssert(actionExecuted) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/PathosTests/WriteTests.swift: -------------------------------------------------------------------------------- 1 | import Pathos 2 | import XCTest 3 | 4 | final class WriteTests: XCTestCase { 5 | func testWritingUnsignedBytes() throws { 6 | try Path.withTemporaryDirectory { _ in 7 | let path = Path("a") 8 | let data: [UInt8] = [65] 9 | try path.write(bytes: data, createIfNecessary: true) 10 | XCTAssert(path.exists()) 11 | XCTAssertEqual(try path.readBytes(), [65]) 12 | } 13 | } 14 | 15 | func testWritingSignedBytes() throws { 16 | try Path.withTemporaryDirectory { _ in 17 | let path = Path("a") 18 | let data: [Int8] = [65] 19 | try path.write(bytes: data, createIfNecessary: true) 20 | XCTAssert(path.exists()) 21 | XCTAssertEqual(try path.readBytes(), [65]) 22 | } 23 | } 24 | 25 | func testWritingString() throws { 26 | try Path.withTemporaryDirectory { _ in 27 | let path = Path("a.txt") 28 | try path.write(utf8: "axx", createIfNecessary: true) 29 | XCTAssert(path.exists()) 30 | XCTAssertEqual(try path.readBytes(), [97, 120, 120]) 31 | } 32 | } 33 | 34 | func testWritingStringWithEncoding() throws { 35 | try Path.withTemporaryDirectory { _ in 36 | let path = Path("a.txt") 37 | try path.write("a", encoding: UTF16.self, createIfNecessary: true) 38 | XCTAssert(path.exists()) 39 | XCTAssertEqual(try path.readBytes(), [97, 0]) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmake/modules/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(Pathos_EXPORTS_FILE ${CMAKE_CURRENT_BINARY_DIR}/PathosExports.cmake) 2 | 3 | configure_file(PathosConfig.cmake.in 4 | ${CMAKE_CURRENT_BINARY_DIR}/PathosConfig.cmake) 5 | 6 | get_property(Pathos_EXPORTS GLOBAL PROPERTY Pathos_EXPORTS) 7 | export(TARGETS ${Pathos_EXPORTS} 8 | FILE ${Pathos_EXPORTS_FILE}) 9 | -------------------------------------------------------------------------------- /cmake/modules/PathosConfig.cmake.in: -------------------------------------------------------------------------------- 1 | if(NOT TARGET Pathos) 2 | include(@Pathos_EXPORTS_FILE@) 3 | endif() -------------------------------------------------------------------------------- /cmake/modules/SwiftSupport.cmake: -------------------------------------------------------------------------------- 1 | include(CMakeParseArguments) 2 | 3 | # Returns the architecture name in a variable 4 | # 5 | # Usage: 6 | # swift_get_host_arch(result_var_name) 7 | # 8 | # Sets ${result_var_name} with the converted architecture name derived from 9 | # CMAKE_SYSTEM_PROCESSOR. 10 | function(swift_get_host_arch result_var_name) 11 | if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64") 12 | set("${result_var_name}" "x86_64" PARENT_SCOPE) 13 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64") 14 | set("${result_var_name}" "aarch64" PARENT_SCOPE) 15 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64") 16 | set("${result_var_name}" "powerpc64" PARENT_SCOPE) 17 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le") 18 | set("${result_var_name}" "powerpc64le" PARENT_SCOPE) 19 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "s390x") 20 | set("${result_var_name}" "s390x" PARENT_SCOPE) 21 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv6l") 22 | set("${result_var_name}" "armv6" PARENT_SCOPE) 23 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7l") 24 | set("${result_var_name}" "armv7" PARENT_SCOPE) 25 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7-a") 26 | set("${result_var_name}" "armv7" PARENT_SCOPE) 27 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64") 28 | set("${result_var_name}" "x86_64" PARENT_SCOPE) 29 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "IA64") 30 | set("${result_var_name}" "itanium" PARENT_SCOPE) 31 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86") 32 | set("${result_var_name}" "i686" PARENT_SCOPE) 33 | elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "i686") 34 | set("${result_var_name}" "i686" PARENT_SCOPE) 35 | else() 36 | message(FATAL_ERROR "Unrecognized architecture on host system: ${CMAKE_SYSTEM_PROCESSOR}") 37 | endif() 38 | endfunction() 39 | 40 | # Returns the os name in a variable 41 | # 42 | # Usage: 43 | # get_swift_host_os(result_var_name) 44 | # 45 | # 46 | # Sets ${result_var_name} with the converted OS name derived from 47 | # CMAKE_SYSTEM_NAME. 48 | function(get_swift_host_os result_var_name) 49 | if(CMAKE_SYSTEM_NAME STREQUAL Darwin) 50 | set(${result_var_name} macosx PARENT_SCOPE) 51 | else() 52 | string(TOLOWER ${CMAKE_SYSTEM_NAME} cmake_system_name_lc) 53 | set(${result_var_name} ${cmake_system_name_lc} PARENT_SCOPE) 54 | endif() 55 | endfunction() 56 | 57 | function(swift_install) 58 | set(options) 59 | set(single_parameter_options EXPORT) 60 | set(multiple_parameter_options TARGETS) 61 | 62 | cmake_parse_arguments(SI 63 | "${options}" 64 | "${single_parameter_options}" 65 | "${multiple_parameter_options}" 66 | ${ARGN}) 67 | 68 | list(LENGTH ${SI_TARGETS} si_num_targets) 69 | if(si_num_targets GREATER 1) 70 | message(SEND_ERROR "swift_install only supports a single target at a time") 71 | endif() 72 | 73 | get_swift_host_os(swift_os) 74 | get_target_property(type ${SI_TARGETS} TYPE) 75 | 76 | if(type STREQUAL STATIC_LIBRARY) 77 | set(swift_dir swift_static) 78 | else() 79 | set(swift_dir swift) 80 | endif() 81 | 82 | install(TARGETS ${SI_TARGETS} 83 | EXPORT ${SI_EXPORT} 84 | ARCHIVE DESTINATION lib/${swift_dir}/${swift_os} 85 | LIBRARY DESTINATION lib/${swift_dir}/${swift_os} 86 | RUNTIME DESTINATION bin) 87 | if(type STREQUAL EXECUTABLE) 88 | return() 89 | endif() 90 | 91 | swift_get_host_arch(swift_arch) 92 | get_target_property(module_name ${SI_TARGETS} Swift_MODULE_NAME) 93 | if(NOT module_name) 94 | set(module_name ${SI_TARGETS}) 95 | endif() 96 | 97 | if(CMAKE_SYSTEM_NAME STREQUAL Darwin) 98 | install(FILES 99 | $/${module_name}.swiftdoc 100 | DESTINATION lib/${swift_dir}/${swift_os}/${module_name}.swiftmodule 101 | RENAME ${swift_arch}.swiftdoc) 102 | install(FILES 103 | $/${module_name}.swiftmodule 104 | DESTINATION lib/${swift_dir}/${swift_os}/${module_name}.swiftmodule 105 | RENAME ${swift_arch}.swiftmodule) 106 | else() 107 | install(FILES 108 | $/${module_name}.swiftdoc 109 | $/${module_name}.swiftmodule 110 | DESTINATION lib/${swift_dir}/${swift_os}/${swift_arch}) 111 | endif() 112 | endfunction() 113 | --------------------------------------------------------------------------------