Merge pull request #34 from mxcl/symlinks

Symlink funcs & support `NSURL` file-refs
This commit is contained in:
Max Howell
2019-02-11 15:06:56 -05:00
committed by GitHub
7 changed files with 525 additions and 49 deletions

View File

@@ -108,7 +108,7 @@ let ls = Path.root.usr.bin.ls // => /usr/bin/ls
``` ```
This is less commonly useful than you would think, hence our documentation This is less commonly useful than you would think, hence our documentation
does not use it. Usually you are joining variables or other `String` arguments does not use it. Usually you are joining variables or other `String` arguments
or trying to describe files (and files usually have extensions). However when or trying to describe files (and files usually have extensions). However when
you need it, its *lovely*. you need it, its *lovely*.
@@ -171,13 +171,18 @@ let swiftFiles = Path.home.ls().files(withExtension: "swift")
# `Path.swift` is robust # `Path.swift` is robust
Some parts of `FileManager` are not exactly idiomatic. For example Some parts of `FileManager` are not exactly idiomatic. For example
`isExecutableFile` returns `true` even if there is no file there, it is instead `isExecutableFile` returns `true` even if there is no file there, it is instead
telling you that *if* you made a file there it *could* be executable. Thus we telling you that *if* you made a file there it *could* be executable. Thus we
check the POSIX permissions of the file first, before returning the result of check the POSIX permissions of the file first, before returning the result of
`isExecutableFile`. `Path.swift` has done the leg-work for you so you can get on `isExecutableFile`. `Path.swift` has done the leg-work for you so you can get on
with your work without worries. with your work without worries.
There is also some magic going on in Foundations filesystem APIs, which we look
for and ensure our API is deterministic, eg. [this test].
[this test]: TODO
# `Path.swift` is properly cross-platform # `Path.swift` is properly cross-platform
`FileManager` on Linux is full of holes. We have found the holes and worked `FileManager` on Linux is full of holes. We have found the holes and worked
@@ -199,6 +204,10 @@ Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash // joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b Path.home/"/b" // => /Users/mxcl/b
// joining with .. or . works as expected
Path.home.foo.bar.join("..") // => /Users/mxcl/foo
Path.home.foo.bar.join(".") // => /Users/mxcl/foo/bar
// of course, feel free to join variables: // of course, feel free to join variables:
let b = "b" let b = "b"
let c = "c" let c = "c"
@@ -211,8 +220,24 @@ Path.root/"~/b" // => /~/b
// but is here // but is here
Path("~/foo")! // => /Users/mxcl/foo Path("~/foo")! // => /Users/mxcl/foo
// this does not work though // this works provided the user `Guest` exists
Path("~Guest") // => /Users/Guest
// but if the user does not exist
Path("~foo") // => nil Path("~foo") // => nil
// paths with .. or . are resolved
Path("/foo/bar/../baz") // => /foo/baz
// symlinks are not resolved
Path.root.bar.symlink(as: "foo")
Path("foo") // => /foo
Path.foo // => /foo
// unless you do it explicitly
try Path.foo.readlink() // => /bar
// `readlink` only resolves the *final* path component,
// thus use `realpath` if there are multiple symlinks
``` ```
*Path.swift* has the general policy that if the desired end result preexists, *Path.swift* has the general policy that if the desired end result preexists,
@@ -220,30 +245,71 @@ then its a noop:
* If you try to delete a file, but the file doesn't exist, we do nothing. * If you try to delete a file, but the file doesn't exist, we do nothing.
* If you try to make a directory and it already exists, we do nothing. * If you try to make a directory and it already exists, we do nothing.
* If you call `readlink` on a non-symlink, we return `self`
However notably if you try to copy or move a file with specifying `overwrite` However notably if you try to copy or move a file with specifying `overwrite`
and the file already exists at the destination and is identical, we dont check and the file already exists at the destination and is identical, we dont check
for that as the check was deemed too expensive to be worthwhile. for that as the check was deemed too expensive to be worthwhile.
## Symbolic links
* Two paths may represent the same *resolved* path yet not be equal due to
symlinks in such cases you should use `realpath` on both first if an
equality check is required.
* There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for
functions that you would expect it (notably `realpath`), but we do *not* for
`Path.init`, *nor* if you are joining a path that ends up being one of these
paths, (eg. `Path.root.join("var/private')`).
## We do not provide change directory functionality
Changing directory is dangerous, you should *always* try to avoid it and thus
we dont even provide the method. If you are executing a sub-process then
use `Process.currentDirectoryURL`.
If you must then use `FileManager.changeCurrentDirectory`.
# I thought I should only use `URL`s?
Apple recommend this because they provide a magic translation for
[file-references embodied by URLs][file-refs], which gives you URLs like so:
file:///.file/id=6571367.15106761
Therefore, if you are not using this feature you are fine. If you have URLs the correct
way to get a `Path` is:
```swift
if let path = Path(url) {
/**/
}
```
Our initializer calls `path` on the URL which resolves any reference to an
actual filesystem path, however we also check the URL has a `file` scheme first.
[file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
# Installation # Installation
SwiftPM: SwiftPM:
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.5.0")) package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0"))
``` ```
CocoaPods: CocoaPods:
```ruby ```ruby
pod 'Path.swift', '~> 0.5' pod 'Path.swift', '~> 0.13'
``` ```
Carthage: Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945). > Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Please note ## Pre1.0 status
We are pre 1.0, thus we can change the API as we like, and we will (to the We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible. pursuit of getting it *right*)! We will tag 1.0 as soon as possible.

View File

@@ -211,6 +211,32 @@ public extension Path {
try FileManager.default.moveItem(atPath: string, toPath: newpath.string) try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
return newpath return newpath
} }
/**
Creates a symlink of this file at `as`.
- Note: If `self` does not exist, is **not** an error.
*/
@discardableResult
func symlink(as: Path) throws -> Path {
try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string)
return `as`
}
/**
Creates a symlink of this file with the same filename in the `into` directory.
- Note: If into does not exist, creates the directory with intermediate directories if necessary.
*/
@discardableResult
func symlink(into dir: Path) throws -> Path {
if !dir.exists {
try dir.mkdir(.p)
} else if !dir.isDirectory {
throw CocoaError.error(.fileWriteFileExists)
}
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
}
} }
/// Options for `Path.mkdir(_:)` /// Options for `Path.mkdir(_:)`

View File

@@ -1,5 +1,3 @@
import class Foundation.NSString
extension Path: CustomStringConvertible { extension Path: CustomStringConvertible {
/// Returns `Path.string` /// Returns `Path.string`
public var description: String { public var description: String {

View File

@@ -2,7 +2,7 @@ import Foundation
#if os(Linux) #if os(Linux)
import func Glibc.access import func Glibc.access
#else #else
import func Darwin.access import Darwin
#endif #endif
public extension Path { public extension Path {
@@ -55,4 +55,11 @@ public extension Path {
return false return false
} }
} }
/// Returns `true` if the file is a symbolic-link (symlink).
var isSymlink: Bool {
var sbuf = stat()
lstat(string, &sbuf)
return (sbuf.st_mode & S_IFMT) == S_IFLNK
}
} }

View File

@@ -1,4 +1,11 @@
import Foundation import Foundation
#if !os(Linux)
import func Darwin.realpath
let _realpath = Darwin.realpath
#else
import func Glibc.realpath
let _realpath = Glibc.realpath
#endif
/** /**
A `Path` represents an absolute path on a filesystem. A `Path` represents an absolute path on a filesystem.
@@ -32,19 +39,92 @@ import Foundation
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
init(string: String) { init(string: String) {
assert(string.first == "/")
assert(string.last != "/" || string == "/")
assert(string.split(separator: "/").contains("..") == false)
self.string = string self.string = string
} }
/// Returns `nil` unless fed an absolute path. /**
Creates a new absolute, standardized path.
- Note: Resolves any .. or . components.
- Note: Removes multiple subsequent and trailing occurences of `/`.
- Note: Does *not* resolve any symlinks.
- Note: On macOS, removes an initial component of /private/var/automount, /var/automount, or /private from the path, if the result still indicates an existing file or directory (checked by consulting the file system).
- Returns: The path or `nil` if fed a relative path or a `~foo` string where there is no user `foo`.
*/
public init?(_ description: String) { public init?(_ description: String) {
guard description.starts(with: "/") || description.starts(with: "~/") else { return nil } var pathComponents = description.split(separator: "/")
self.init(string: (description as NSString).standardizingPath) switch description.first {
case "/":
break
case "~":
if description == "~" {
self = Path.home
return
}
let tilded: String
if description.hasPrefix("~/") {
tilded = Path.home.string
} else {
let username = String(pathComponents[0].dropFirst())
#if os(macOS) || os(Linux)
if #available(OSX 10.12, *) {
guard let url = FileManager.default.homeDirectory(forUser: username) else { return nil }
assert(url.scheme == "file")
tilded = url.path
} else {
guard let dir = NSHomeDirectoryForUser(username) else { return nil }
tilded = dir
}
#else
if username != NSUserName() {
return nil
} else {
tilded = NSHomeDirectory()
}
#endif
}
pathComponents.remove(at: 0)
pathComponents.insert(contentsOf: tilded.split(separator: "/"), at: 0)
default:
return nil
}
#if os(macOS)
func ifExists(withPrefix prefix: String, removeFirst n: Int) {
assert(prefix.split(separator: "/").count == n)
if description.hasPrefix(prefix), FileManager.default.fileExists(atPath: description) {
pathComponents.removeFirst(n)
}
}
ifExists(withPrefix: "/private/var/automount", removeFirst: 3)
ifExists(withPrefix: "/var/automount", removeFirst: 2)
ifExists(withPrefix: "/private", removeFirst: 1)
#endif
self.string = join_(prefix: "/", pathComponents: pathComponents)
}
public init?(_ url: URL) {
guard url.scheme == "file" else { return nil }
self.init(string: url.path)
//NOTE: URL cannot be a file-reference url, unlike NSURL, so this always works
}
public init?(_ url: NSURL) {
guard url.scheme == "file", let path = url.path else { return nil }
self.init(string: path)
// ^^ works even if the url is a file-reference url
} }
/// :nodoc: /// :nodoc:
public subscript(dynamicMember pathComponent: String) -> Path { public subscript(dynamicMember addendum: String) -> Path {
let str = (string as NSString).appendingPathComponent(pathComponent) //NOTE its possible for the string to be anything if we are invoked via
return Path(string: str) // explicit subscript thus we use our fully sanitized `join` function
return Path(string: join_(prefix: string, appending: addendum))
} }
//MARK: Properties //MARK: Properties
@@ -57,6 +137,21 @@ public struct Path: Equatable, Hashable, Comparable {
return URL(fileURLWithPath: string) return URL(fileURLWithPath: string)
} }
/**
Returns a file-reference URL.
- Note: Only NSURL can be a file-reference-URL, hence we return NSURL.
- SeeAlso: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
- Important: On Linux returns an file scheme NSURL for this path string.
*/
public var fileReferenceURL: NSURL? {
#if !os(Linux)
// https://bugs.swift.org/browse/SR-2728
return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL
#else
return NSURL(fileURLWithPath: string)
#endif
}
/** /**
Returns the parent directory for this path. Returns the parent directory for this path.
@@ -66,20 +161,37 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`. - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/ */
public var parent: Path { public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent) let index = string.lastIndex(of: "/")!
let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr))
} }
/** /**
Returns the filename extension of this path. Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`. - Remark: If there is no extension returns "".
- Remark: If the filename ends with any number of ".", returns "".
- Note: We special case eg. `foo.tar.gz`. - Note: We special case eg. `foo.tar.gz`.
*/ */
@inlinable @inlinable
public var `extension`: String { public var `extension`: String {
if string.hasSuffix(".tar.gz") { //FIXME efficiency
switch true {
case string.hasSuffix(".tar.gz"):
return "tar.gz" return "tar.gz"
} else { case string.hasSuffix(".tar.bz"):
return (string as NSString).pathExtension return "tar.bz"
case string.hasSuffix(".tar.bz2"):
return "tar.bz2"
case string.hasSuffix(".tar.xz"):
return "tar.xz"
default:
let slash = string.lastIndex(of: "/")!
if let dot = string.lastIndex(of: "."), slash < dot {
let foo = string.index(after: dot)
return String(string[foo...])
} else {
return ""
}
} }
} }
@@ -93,14 +205,15 @@ public struct Path: Equatable, Hashable, Comparable {
Path.root.join("a").join("b") // => /a/b Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b Path.root.join("a").join("/b") // => /a/b
- Note: `..` and `.` components are interpreted.
- Note: pathComponent *may* be multiple components.
- Note: symlinks are *not* resolved.
- Parameter pathComponent: The string to join with this path. - Parameter pathComponent: The string to join with this path.
- Returns: A new joined path. - Returns: A new joined path.
- SeeAlso: `Path./(_:_:)` - SeeAlso: `Path./(_:_:)`
*/ */
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol { public func join<S>(_ addendum: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion) return Path(string: join_(prefix: string, appending: addendum))
let str = (string as NSString).appendingPathComponent(String(pathComponent))
return Path(string: (str as NSString).standardizingPath)
} }
/** /**
@@ -111,6 +224,9 @@ public struct Path: Equatable, Hashable, Comparable {
Path.root/"a"/"b" // => /a/b Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b Path.root/"a"/"/b" // => /a/b
- Note: `..` and `.` components are interpreted.
- Note: pathComponent *may* be multiple components.
- Note: symlinks are *not* resolved.
- Parameter lhs: The base path to join with `rhs`. - Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`. - Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path. - Returns: A new joined path.
@@ -168,17 +284,71 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter dropExtension: If `true` returns the basename without its file extension. - Parameter dropExtension: If `true` returns the basename without its file extension.
*/ */
public func basename(dropExtension: Bool = false) -> String { public func basename(dropExtension: Bool = false) -> String {
let str = string as NSString var lastPathComponent: Substring {
if !dropExtension { let slash = string.lastIndex(of: "/")!
return str.lastPathComponent let index = string.index(after: slash)
} else { return string[index...]
let ext = str.pathExtension }
if !ext.isEmpty { var go: Substring {
return String(str.lastPathComponent.dropLast(ext.count + 1)) if !dropExtension {
return lastPathComponent
} else { } else {
return str.lastPathComponent let ext = self.extension
if !ext.isEmpty {
return lastPathComponent.dropLast(ext.count + 1)
} else {
return lastPathComponent
}
} }
} }
return String(go)
}
/**
If the path represents an actual entry that is a symlink, returns the symlinks
absolute destination.
- Important: This is not exhaustive, the resulting path may still contain
symlink.
- Important: The path will only be different if the last path component is a
symlink, any symlinks in prior components are not resolved.
- Note: If file exists but isnt a symlink, returns `self`.
- Note: If symlink destination does not exist, is **not** an error.
*/
public func readlink() throws -> Path {
do {
let rv = try FileManager.default.destinationOfSymbolicLink(atPath: string)
return Path(rv) ?? parent/rv
} catch CocoaError.fileReadUnknown {
// file is not symlink, return `self`
assert(exists)
return self
} catch {
#if os(Linux)
// ugh: Swift on Linux
let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain, nsError.code == CocoaError.fileReadUnknown.rawValue, exists {
return self
}
#endif
throw error
}
}
/// Recursively resolves symlinks in this path.
public func realpath() throws -> Path {
guard let rv = _realpath(string, nil) else { throw CocoaError.error(.fileNoSuchFile) }
defer { free(rv) }
guard let rvv = String(validatingUTF8: rv) else { throw CocoaError.error(.fileReadUnknownStringEncoding) }
// Removing an initial component of /private/var/automount, /var/automount,
// or /private from the path, if the result still indicates an existing file or
// directory (checked by consulting the file system).
// ^^ we do this to not conflict with the results that other Apple APIs give
// which is necessary if we are to have equality checks work reliably
let rvvv = (rvv as NSString).standardizingPath
return Path(string: rvvv)
} }
/// Returns the locale-aware sort order for the two paths. /// Returns the locale-aware sort order for the two paths.
@@ -188,3 +358,38 @@ public struct Path: Equatable, Hashable, Comparable {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
} }
} }
@inline(__always)
private func join_<S>(prefix: String, appending: S) -> String where S: StringProtocol {
return join_(prefix: prefix, pathComponents: appending.split(separator: "/"))
}
private func join_<S>(prefix: String, pathComponents: S) -> String where S: Sequence, S.Element: StringProtocol {
assert(prefix.first == "/")
var rv = prefix
for component in pathComponents {
assert(!component.contains("/"))
switch component {
case "..":
let start = rv.indices.startIndex
let index = rv.lastIndex(of: "/")!
if start == index {
rv = "/"
} else {
rv = String(rv[start..<index])
}
case ".":
break
default:
if rv == "/" {
rv = "/\(component)"
} else {
rv = "\(rv)/\(component)"
}
}
}
return rv
}

View File

@@ -9,6 +9,10 @@ class PathTests: XCTestCase {
XCTAssertEqual((Path.root/"///bar").string, "/bar") XCTAssertEqual((Path.root/"///bar").string, "/bar")
XCTAssertEqual((Path.root/"foo///bar////").string, "/foo/bar") XCTAssertEqual((Path.root/"foo///bar////").string, "/foo/bar")
XCTAssertEqual((Path.root/"foo"/"/bar").string, "/foo/bar") XCTAssertEqual((Path.root/"foo"/"/bar").string, "/foo/bar")
XCTAssertEqual(Path.root.foo.bar.join(".."), Path.root.foo)
XCTAssertEqual(Path.root.foo.bar.join("."), Path.root.foo.bar)
XCTAssertEqual(Path.root.foo.bar.join("../baz"), Path.root.foo.baz)
} }
func testEnumeration() throws { func testEnumeration() throws {
@@ -76,15 +80,19 @@ class PathTests: XCTestCase {
} }
func testExtension() { func testExtension() {
XCTAssertEqual(Path.root.join("a.swift").extension, "swift") for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(Path.root.join("a").extension, "") XCTAssertEqual(prefix.join("a.swift").extension, "swift")
XCTAssertEqual(Path.root.join("a.").extension, "") XCTAssertEqual(prefix.join("a").extension, "")
XCTAssertEqual(Path.root.join("a..").extension, "") XCTAssertEqual(prefix.join("a.").extension, "")
XCTAssertEqual(Path.root.join("a..swift").extension, "swift") XCTAssertEqual(prefix.join("a..").extension, "")
XCTAssertEqual(Path.root.join("a..swift.").extension, "") XCTAssertEqual(prefix.join("a..swift").extension, "swift")
XCTAssertEqual(Path.root.join("a.tar.gz").extension, "tar.gz") XCTAssertEqual(prefix.join("a..swift.").extension, "")
XCTAssertEqual(Path.root.join("a..tar.gz").extension, "tar.gz") XCTAssertEqual(prefix.join("a.tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar..gz").extension, "gz") XCTAssertEqual(prefix.join("a.tar.bz2").extension, "tar.bz2")
XCTAssertEqual(prefix.join("a.tar.xz").extension, "tar.xz")
XCTAssertEqual(prefix.join("a..tar.bz").extension, "tar.bz")
XCTAssertEqual(prefix.join("a..tar..xz").extension, "xz")
}
} }
func testMktemp() throws { func testMktemp() throws {
@@ -107,28 +115,32 @@ class PathTests: XCTestCase {
} }
func testBasename() { func testBasename() {
XCTAssertEqual(Path.root.join("foo.bar").basename(dropExtension: true), "foo") for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(Path.root.join("foo").basename(dropExtension: true), "foo") XCTAssertEqual(prefix.join("foo.bar").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo.").basename(dropExtension: true), "foo.") XCTAssertEqual(prefix.join("foo").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo.bar.baz").basename(dropExtension: true), "foo.bar") XCTAssertEqual(prefix.join("foo.").basename(dropExtension: true), "foo.")
XCTAssertEqual(prefix.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
}
} }
func testCodable() throws { func testCodable() throws {
let input = [Path.root/"bar"] let input = [Path.root.foo, Path.root.foo.bar, Path.root]
XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input) XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input)
} }
func testRelativePathCodable() throws { func testRelativePathCodable() throws {
let root = Path.root/"bar" let root = Path.root.foo
let input = [ let input = [
root/"foo" Path.root,
root,
root.bar
] ]
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root encoder.userInfo[.relativePath] = root
let data = try encoder.encode(input) let data = try encoder.encode(input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["foo"]) XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"])
let decoder = JSONDecoder() let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode([Path].self, from: data)) XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
@@ -148,8 +160,16 @@ class PathTests: XCTestCase {
XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c")) XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c"))
XCTAssertEqual(Path.root/"~b", Path("/~b")) XCTAssertEqual(Path.root/"~b", Path("/~b"))
XCTAssertEqual(Path.root/"~/b", Path("/~/b")) XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
XCTAssertEqual(Path("~/foo"), Path.home/"foo") XCTAssertEqual(Path("~/foo"), Path.home/"foo")
XCTAssertEqual(Path("~"), Path.home)
XCTAssertEqual(Path("~/"), Path.home)
XCTAssertEqual(Path("~///"), Path.home)
XCTAssertEqual(Path("/~///"), Path.root/"~")
XCTAssertNil(Path("~foo")) XCTAssertNil(Path("~foo"))
XCTAssertNil(Path("~foo/bar"))
XCTAssertEqual(Path("~\(NSUserName())"), Path.home)
XCTAssertEqual(Path.root/"a/foo"/"../bar", Path.root/"a/bar") XCTAssertEqual(Path.root/"a/foo"/"../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"/../bar", Path.root/"a/bar") XCTAssertEqual(Path.root/"a/foo"/"/../bar", Path.root/"a/bar")
@@ -162,6 +182,9 @@ class PathTests: XCTestCase {
let a = Path.home.foo let a = Path.home.foo
XCTAssertEqual(a.Documents, Path.home/"foo/Documents") XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
// verify use of the dynamic-member-subscript works according to our rules
XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, "\(Path.home.parent.string)/~foo")
} }
func testCopyTo() throws { func testCopyTo() throws {
@@ -172,6 +195,14 @@ class PathTests: XCTestCase {
XCTAssertThrowsError(try root.foo.copy(to: root.bar)) XCTAssertThrowsError(try root.foo.copy(to: root.bar))
try root.foo.copy(to: root.bar, overwrite: true) try root.foo.copy(to: root.bar, overwrite: true)
} }
// test copy errors if directory exists at destination, even with overwrite
try Path.mktemp { root in
try root.foo.touch()
XCTAssert(root.foo.isFile)
XCTAssertThrowsError(try root.foo.copy(to: root.bar.mkdir()))
XCTAssertThrowsError(try root.foo.copy(to: root.bar, overwrite: true))
}
} }
func testCopyInto() throws { func testCopyInto() throws {
@@ -204,6 +235,14 @@ class PathTests: XCTestCase {
XCTAssertThrowsError(try tmpdir.foo.touch().move(to: tmpdir.bar)) XCTAssertThrowsError(try tmpdir.foo.touch().move(to: tmpdir.bar))
try tmpdir.foo.move(to: tmpdir.bar, overwrite: true) try tmpdir.foo.move(to: tmpdir.bar, overwrite: true)
} }
// test move errors if directory exists at destination, even with overwrite
try Path.mktemp { root in
try root.foo.touch()
XCTAssert(root.foo.isFile)
XCTAssertThrowsError(try root.foo.move(to: root.bar.mkdir()))
XCTAssertThrowsError(try root.foo.move(to: root.bar, overwrite: true))
}
} }
func testMoveInto() throws { func testMoveInto() throws {
@@ -424,4 +463,129 @@ class PathTests: XCTestCase {
XCTAssertThrowsError(try tmpdir.bar.touch()) XCTAssertThrowsError(try tmpdir.bar.touch())
} }
} }
func testSymlinkFunctions() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssert(bar.isSymlink)
XCTAssertEqual(try bar.readlink(), foo)
}
try Path.mktemp { tmpdir in
let foo1 = try tmpdir.foo.touch()
let foo2 = try foo1.symlink(into: tmpdir.bar)
XCTAssert(foo2.isSymlink)
XCTAssert(tmpdir.bar.isDirectory)
XCTAssertEqual(try foo2.readlink(), foo1)
// cannot symlink into when `into` is an existing entry that is not a directory
let baz = try tmpdir.baz.touch()
XCTAssertThrowsError(try foo1.symlink(into: baz))
}
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try tmpdir.bar.mkdir()
XCTAssertThrowsError(try foo.symlink(as: bar))
XCTAssert(try foo.symlink(as: bar.foo).isSymlink)
}
}
func testReadlinkOnRelativeSymlink() throws {
//TODO how to test on iOS etc.?
#if os(macOS) || os(Linux)
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir()
let bar = try tmpdir.bar.touch()
let task = Process()
task.currentDirectoryPath = foo.string
task.launchPath = "/bin/ln"
task.arguments = ["-s", "../bar", "baz"]
task.launch()
task.waitUntilExit()
XCTAssertEqual(task.terminationStatus, 0)
XCTAssert(tmpdir.foo.baz.isSymlink)
XCTAssertEqual(try FileManager.default.destinationOfSymbolicLink(atPath: tmpdir.foo.baz.string), "../bar")
XCTAssertEqual(try tmpdir.foo.baz.readlink(), bar)
}
#endif
}
func testReadlinkOnFileReturnsSelf() throws {
try Path.mktemp { tmpdir in
XCTAssertEqual(try tmpdir.foo.touch(), tmpdir.foo)
XCTAssertEqual(try tmpdir.foo.readlink(), tmpdir.foo)
}
}
func testReadlinkOnNonExistantFileThrows() throws {
try Path.mktemp { tmpdir in
XCTAssertThrowsError(try tmpdir.bar.readlink())
}
}
func testReadlinkWhereLinkDestinationDoesNotExist() throws {
try Path.mktemp { tmpdir in
let bar = try tmpdir.foo.symlink(as: tmpdir.bar)
XCTAssertEqual(try bar.readlink(), tmpdir.foo)
}
}
func testNoUndesiredSymlinkResolution() throws {
// this test because NSString.standardizingPath will resolve symlinks
// if the path you give it contains .. and the result is an actual entry
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir()
try foo.bar.mkdir().fuz.touch()
let baz = try foo.symlink(as: tmpdir.baz)
XCTAssert(baz.isSymlink)
XCTAssert(baz.bar.isDirectory)
XCTAssertEqual(baz.bar.join("..").string, "\(tmpdir)/baz")
XCTAssertEqual(Path("\(tmpdir)/baz/bar/..")?.string, "\(tmpdir)/baz")
}
}
func testRealpath() throws {
try Path.mktemp { tmpdir in
let b = try tmpdir.a.b.mkdir(.p)
let c = try tmpdir.a.c.touch()
let e = try c.symlink(as: b.e)
let f = try e.symlink(as: tmpdir.f)
XCTAssertEqual(try f.readlink(), e)
XCTAssertEqual(try f.realpath(), c)
}
try Path.mktemp { tmpdir in
XCTAssertThrowsError(try tmpdir.foo.realpath())
}
}
func testFileReference() throws {
let ref = Path.home.fileReferenceURL
#if !os(Linux)
XCTAssertTrue(ref?.isFileReferenceURL() ?? false)
#endif
XCTAssertEqual(ref?.path, Path.home.string)
}
func testURLInitializer() throws {
XCTAssertEqual(Path(Path.home.url), Path.home)
XCTAssertEqual(Path.home.fileReferenceURL.flatMap(Path.init), Path.home)
XCTAssertNil(Path(URL(string: "https://foo.com")!))
XCTAssertNil(Path(NSURL(string: "https://foo.com")!))
}
func testInitializerForRelativePath() throws {
XCTAssertNil(Path("foo"))
XCTAssertNil(Path("../foo"))
XCTAssertNil(Path("./foo"))
}
} }

View File

@@ -17,7 +17,9 @@ extension PathTests {
("testExists", testExists), ("testExists", testExists),
("testExtension", testExtension), ("testExtension", testExtension),
("testFileHandleExtensions", testFileHandleExtensions), ("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes), ("testFilesystemAttributes", testFilesystemAttributes),
("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testLock", testLock), ("testLock", testLock),
@@ -25,6 +27,12 @@ extension PathTests {
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto), ("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo), ("testMoveTo", testMoveTo),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),
("testReadlinkOnRelativeSymlink", testReadlinkOnRelativeSymlink),
("testReadlinkWhereLinkDestinationDoesNotExist", testReadlinkWhereLinkDestinationDoesNotExist),
("testRealpath", testRealpath),
("testRelativeCodable", testRelativeCodable), ("testRelativeCodable", testRelativeCodable),
("testRelativePathCodable", testRelativePathCodable), ("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo), ("testRelativeTo", testRelativeTo),
@@ -32,8 +40,10 @@ extension PathTests {
("testSort", testSort), ("testSort", testSort),
("testStringConvertibles", testStringConvertibles), ("testStringConvertibles", testStringConvertibles),
("testStringExtensions", testStringExtensions), ("testStringExtensions", testStringExtensions),
("testSymlinkFunctions", testSymlinkFunctions),
("testTimes", testTimes), ("testTimes", testTimes),
("testTouchThrowsIfCannotWrite", testTouchThrowsIfCannotWrite), ("testTouchThrowsIfCannotWrite", testTouchThrowsIfCannotWrite),
("testURLInitializer", testURLInitializer),
] ]
} }