diff --git a/CMakeLists.txt b/CMakeLists.txt index e4abb990..abbe230d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,10 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) option(BUILD_SHARED_LIBS "Build shared libraries by default" YES) +if(FIND_PM_DEPS) + find_package(SwiftSystem CONFIG REQUIRED) +endif() + find_package(dispatch QUIET) find_package(Foundation QUIET) find_package(Threads QUIET) diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..ebd3ef7f --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "swift-system", + "repositoryURL": "https://github.com/apple/swift-system.git", + "state": { + "branch": null, + "revision": "2bc160bfe34d843ae5ff47168080add24dfd7eac", + "version": "0.0.2" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 94b567be..63c59e33 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,9 @@ let package = Package( name: "TSCTestSupport", targets: ["TSCTestSupport"]), ], - dependencies: [], + dependencies: [ + .package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "0.0.1")) + ], targets: [ // MARK: Tools support core targets @@ -44,7 +46,8 @@ let package = Package( .target( /** TSCBasic support library */ name: "TSCBasic", - dependencies: ["TSCLibc", "TSCclibc"]), + dependencies: ["TSCLibc", "TSCclibc", + .product(name: "SystemPackage", package: "swift-system")]), .target( /** Abstractions for common operations, should migrate to TSCBasic */ name: "TSCUtility", @@ -82,8 +85,5 @@ let package = Package( TSCBasic.cxxSettings = [ .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), ] - TSCBasic.linkerSettings = [ - .linkedLibrary("Pathcch", .when(platforms: [.windows])), - ] } #endif diff --git a/Sources/TSCBasic/CMakeLists.txt b/Sources/TSCBasic/CMakeLists.txt index c5efd819..f853f868 100644 --- a/Sources/TSCBasic/CMakeLists.txt +++ b/Sources/TSCBasic/CMakeLists.txt @@ -57,17 +57,16 @@ target_compile_options(TSCBasic PUBLIC # Ignore secure function warnings on Windows. "$<$:SHELL:-Xcc -D_CRT_SECURE_NO_WARNINGS>") target_link_libraries(TSCBasic PUBLIC - TSCLibc) + TSCLibc + SwiftSystem) target_link_libraries(TSCBasic PRIVATE - TSCclibc) + TSCclibc) if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) if(Foundation_FOUND) target_link_libraries(TSCBasic PUBLIC Foundation) endif() endif() -target_link_libraries(TSCBasic PRIVATE - $<$:Pathcch>) # NOTE(compnerd) workaround for CMake not setting up include flags yet set_target_properties(TSCBasic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/TSCBasic/FileSystem.swift b/Sources/TSCBasic/FileSystem.swift index b15a2d6c..ce536db7 100644 --- a/Sources/TSCBasic/FileSystem.swift +++ b/Sources/TSCBasic/FileSystem.swift @@ -1,16 +1,16 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information See http://swift.org/CONTRIBUTORS.txt for Swift project authors */ -import TSCLibc import Foundation import Dispatch +import SystemPackage public struct FileSystemError: Error, Equatable { public enum Kind: Equatable { @@ -86,23 +86,34 @@ extension FileSystemError: CustomNSError { } } -public extension FileSystemError { - init(errno: Int32, _ path: AbsolutePath) { +extension FileSystemError.Kind { + public static func errno(_ errno: Errno) -> FileSystemError.Kind { switch errno { - case TSCLibc.EACCES: - self.init(.invalidAccess, path) - case TSCLibc.EISDIR: - self.init(.isDirectory, path) - case TSCLibc.ENOENT: - self.init(.noEntry, path) - case TSCLibc.ENOTDIR: - self.init(.notDirectory, path) - default: - self.init(.unknownOSError, path) + case .permissionDenied: return .invalidAccess + case .isDirectory: return.isDirectory + case .noSuchFileOrDirectory: return .noEntry + case .notDirectory: return .notDirectory + default: return .unknownOSError } } } +public extension FileSystemError { + init(errno: Errno, _ path: AbsolutePath) { + self.init(.errno(errno), path) + } + init(errno: Int32, _ path: AbsolutePath) { + self.init(errno: Errno(rawValue: errno), path) + } +} + +func withFileSystemError(path: AbsolutePath, _ body: () throws -> Result) throws -> Result { + do { return try body() } + catch let errno as Errno { + throw FileSystemError(errno: errno, path) + } +} + /// Defines the file modes. public enum FileMode { @@ -284,7 +295,7 @@ private class LocalFileSystem: FileSystem { func isExecutableFile(_ path: AbsolutePath) -> Bool { // Our semantics doesn't consider directories. - return (self.isFile(path) || self.isSymlink(path)) && FileManager.default.isExecutableFile(atPath: path.pathString) + return (self.isFile(path) || self.isSymlink(path)) && FileManager.default.isExecutableFile(atPath: path.pathString) } func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool { @@ -377,60 +388,39 @@ private class LocalFileSystem: FileSystem { } func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws { - let destString = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString + let destString = relative ? try destination.relative(to: path.parentDirectory).pathString : destination.pathString try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString) } func readFileContents(_ path: AbsolutePath) throws -> ByteString { - // Open the file. - let fp = fopen(path.pathString, "rb") - if fp == nil { - throw FileSystemError(errno: errno, path) - } - defer { fclose(fp) } - - // Read the data one block at a time. - let data = BufferedOutputByteStream() - var tmpBuffer = [UInt8](repeating: 0, count: 1 << 12) - while true { - let n = fread(&tmpBuffer, 1, tmpBuffer.count, fp) - if n < 0 { - if errno == EINTR { continue } - throw FileSystemError(.ioError(code: errno), path) - } - if n == 0 { - let errno = ferror(fp) - if errno != 0 { - throw FileSystemError(.ioError(code: errno), path) + try withFileSystemError(path: path) { + let data = BufferedOutputByteStream() + // Open the file. + let fd = try FileDescriptor.open(path.filepath, .readOnly) + var tmpBuffer = [UInt8](repeating: 0, count: 1 << 12) + try fd.closeAfter { + while true { + // Read the data one block at a time. + let n = try tmpBuffer.withUnsafeMutableBytes { + try fd.read(into: $0) + } + guard n > 0 else { break } + data <<< tmpBuffer[0.. AbsolutePath { - if path == AbsolutePath.root { + if path.isRoot { return root } else { // FIXME: Optimize? - return root.appending(RelativePath(String(path.pathString.dropFirst(1)))) + return root.appending(RelativePath(path.filepath.removingRoot())) } } diff --git a/Sources/TSCBasic/Path.swift b/Sources/TSCBasic/Path.swift index b13b0034..c4f60875 100644 --- a/Sources/TSCBasic/Path.swift +++ b/Sources/TSCBasic/Path.swift @@ -1,26 +1,178 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information See http://swift.org/CONTRIBUTORS.txt for Swift project authors */ -#if os(Windows) -import Foundation -import WinSDK -#endif - -#if os(Windows) -private typealias PathImpl = UNIXPath -#else -private typealias PathImpl = UNIXPath -#endif +import SystemPackage import protocol Foundation.CustomNSError import var Foundation.NSLocalizedDescriptionKey +public protocol Path: Hashable, Codable, CustomStringConvertible { + /// Underlying type, based on SwiftSystem. + var filepath: FilePath { get } + + /// Public initializer from FilePath. + init(_ filepath: FilePath) + + /// Public initializer from String. + init(_ string: String) + + /// Convenience initializer that verifies that the path lexically. + init(validating path: String) throws + + /// Normalized string representation (the normalization rules are described + /// in the documentation of the initializer). This string is never empty. + var pathString: String { get } + + /// The root of a path. + var root: String? { get } + + /// Directory component. An absolute path always has a non-empty directory + /// component (the directory component of the root path is the root itself). + var dirname: String { get } + + /// Last path component (including the suffix, if any). + var basename: String { get } + + /// Returns the basename without the extension. + var basenameWithoutExt: String { get } + + /// Extension of the give path's basename. This follow same rules as + /// suffix except that it doesn't include leading `.` character. + var `extension`: String? { get } + + /// Suffix (including leading `.` character) if any. Note that a basename + /// that starts with a `.` character is not considered a suffix, nor is a + /// trailing `.` character. + var suffix: String? { get } + + /// True if the path is a root directory. + var isRoot: Bool { get } + + /// Returns the path with an additional literal component appended. + /// + /// This method accepts pseudo-path like '.' or '..', but should not contain "/". + func appending(component: String) -> Self + + /// Returns the relative path with additional literal components appended. + /// + /// This method should only be used in cases where the input is guaranteed + /// to be a valid path component (i.e., it cannot be empty, contain a path + /// separator, or be a pseudo-path like '.' or '..'). + func appending(components names: [String]) -> Self + func appending(components names: String...) -> Self + + /// Returns an array of strings that make up the path components of the + /// path. This is the same sequence of strings as the basenames of each + /// successive path component. An empty path has a single path + /// component: the `.` string. + /// + /// NOTE: Path components no longer include the root. Use `root` instead. + var components: [String] { get } +} + +/// Default implementations of some protocol stubs. +extension Path { + public var pathString: String { + if filepath.string.isEmpty { + return "." + } + return filepath.string + } + + public var root: String? { + return filepath.root?.string + } + + public var dirname: String { + let dirname = filepath.removingLastComponent().string + if dirname.isEmpty { + return "." + } + return dirname + } + + public var basename: String { + return filepath.lastComponent?.string ?? root ?? "." + } + + public var basenameWithoutExt: String { + return filepath.lastComponent?.stem ?? root ?? "." + } + + public var `extension`: String? { + guard let ext = filepath.extension, + !ext.isEmpty else { + return nil + } + return filepath.extension + } + + public var suffix: String? { + if let ext = self.extension { + return "." + ext + } else { + return nil + } + } + + public var isRoot: Bool { + return filepath.isRoot + } + + public func appending(component: String) -> Self { + return Self(filepath.appending( + FilePath.Component(stringLiteral: component))) + } + + public func appending(components names: [String]) -> Self { + let components = names.map(FilePath.Component.init) + return Self(filepath.appending(components)) + } + + public func appending(components names: String...) -> Self { + appending(components: names) + } + + public var components: [String] { + var components = filepath.components.map(\.string) + if filepath.isRelative && components.isEmpty { + components.append(".") + } + return components + } +} + +/// Default implementation of `CustomStringConvertible`. +extension Path { + public var description: String { + return pathString + } + + public var debugDescription: String { + // FIXME: We should really be escaping backslashes and quotes here. + return "<\(Self.self):\"\(pathString)\">" + } +} + +/// Default implementation of `Codable`. +extension Path { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(pathString) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + try self.init(validating: container.decode(String.self)) + } +} + /// Represents an absolute file system path, independently of what (or whether /// anything at all) exists at that path in the file system at any given time. /// An absolute path always starts with a `/` character, and holds a normalized @@ -41,22 +193,14 @@ import var Foundation.NSLocalizedDescriptionKey /// normalization, because it is normally the responsibility of the shell and /// not the program being invoked (e.g. when invoking `cd ~`, it is the shell /// that evaluates the tilde; the `cd` command receives an absolute path). -public struct AbsolutePath: Hashable { - /// Check if the given name is a valid individual path component. - /// - /// This only checks with regard to the semantics enforced by `AbsolutePath` - /// and `RelativePath`; particular file systems may have their own - /// additional requirements. - static func isValidComponent(_ name: String) -> Bool { - return PathImpl.isValidComponent(name) - } +public struct AbsolutePath: Path { + /// Underlying type, based on SwiftSystem. + public let filepath: FilePath - /// Private implementation details, shared with the RelativePath struct. - private let _impl: PathImpl - - /// Private initializer when the backing storage is known. - private init(_ impl: PathImpl) { - _impl = impl + /// Public initializer with FilePath. + public init(_ filepath: FilePath) { + precondition(filepath.isAbsolute) + self.filepath = filepath.lexicallyNormalized() } /// Initializes the AbsolutePath from `absStr`, which must be an absolute @@ -65,82 +209,37 @@ public struct AbsolutePath: Hashable { /// The input string will be normalized if needed, as described in the /// documentation for AbsolutePath. public init(_ absStr: String) { - self.init(PathImpl(normalizingAbsolutePath: absStr)) + self.init(FilePath(absStr)) } /// Initializes an AbsolutePath from a string that may be either absolute /// or relative; if relative, `basePath` is used as the anchor; if absolute, /// it is used as is, and in this case `basePath` is ignored. public init(_ str: String, relativeTo basePath: AbsolutePath) { - if PathImpl(string: str).isAbsolute { - self.init(str) - } else { - self.init(basePath, RelativePath(str)) - } + self.init(basePath.filepath.pushing(FilePath(str))) } /// Initializes the AbsolutePath by concatenating a relative path to an /// existing absolute path, and renormalizing if necessary. public init(_ absPath: AbsolutePath, _ relPath: RelativePath) { - self.init(absPath._impl.appending(relativePath: relPath._impl)) + self.init(absPath.filepath.pushing(relPath.filepath)) } /// Convenience initializer that appends a string to a relative path. public init(_ absPath: AbsolutePath, _ relStr: String) { - self.init(absPath, RelativePath(relStr)) + self.init(absPath.filepath.pushing(FilePath(relStr))) } /// Convenience initializer that verifies that the path is absolute. public init(validating path: String) throws { - try self.init(PathImpl(validatingAbsolutePath: path)) - } - - /// Directory component. An absolute path always has a non-empty directory - /// component (the directory component of the root path is the root itself). - public var dirname: String { - return _impl.dirname - } - - /// Last path component (including the suffix, if any). it is never empty. - public var basename: String { - return _impl.basename - } - - /// Returns the basename without the extension. - public var basenameWithoutExt: String { - if let ext = self.extension { - return String(basename.dropLast(ext.count + 1)) - } - return basename - } - - /// Suffix (including leading `.` character) if any. Note that a basename - /// that starts with a `.` character is not considered a suffix, nor is a - /// trailing `.` character. - public var suffix: String? { - return _impl.suffix - } - - /// Extension of the give path's basename. This follow same rules as - /// suffix except that it doesn't include leading `.` character. - public var `extension`: String? { - return _impl.extension + try self.init(FilePath(validatingAbsolutePath: path)) } /// Absolute path of parent directory. This always returns a path, because /// every directory has a parent (the parent directory of the root directory /// is considered to be the root directory itself). public var parentDirectory: AbsolutePath { - return AbsolutePath(_impl.parentDirectory) - } - - /// True if the path is the root directory. - public var isRoot: Bool { -#if os(Windows) - return _impl.string.withCString(encodedAs: UTF16.self, PathCchIsRoot) -#else - return _impl == PathImpl.root -#endif + return AbsolutePath(filepath.removingLastComponent()) } /// Returns the absolute path with the relative path applied. @@ -148,29 +247,6 @@ public struct AbsolutePath: Hashable { return AbsolutePath(self, subpath) } - /// Returns the absolute path with an additional literal component appended. - /// - /// This method accepts pseudo-path like '.' or '..', but should not contain "/". - public func appending(component: String) -> AbsolutePath { - return AbsolutePath(_impl.appending(component: component)) - } - - /// Returns the absolute path with additional literal components appended. - /// - /// This method should only be used in cases where the input is guaranteed - /// to be a valid path component (i.e., it cannot be empty, contain a path - /// separator, or be a pseudo-path like '.' or '..'). - public func appending(components names: [String]) -> AbsolutePath { - // FIXME: This doesn't seem a particularly efficient way to do this. - return names.reduce(self, { path, name in - path.appending(component: name) - }) - } - - public func appending(components names: String...) -> AbsolutePath { - appending(components: names) - } - /// NOTE: We will most likely want to add other `appending()` methods, such /// as `appending(suffix:)`, and also perhaps `replacing()` methods, /// such as `replacing(suffix:)` or `replacing(basename:)` for some @@ -179,24 +255,29 @@ public struct AbsolutePath: Hashable { /// NOTE: We may want to consider adding operators such as `+` for appending /// a path component. - /// NOTE: We will want to add a method to return the lowest common ancestor - /// path. - - /// Root directory (whose string representation is just a path separator). - public static let root = AbsolutePath(PathImpl.root) - - /// Normalized string representation (the normalization rules are described - /// in the documentation of the initializer). This string is never empty. - public var pathString: String { - return _impl.string + /// Returns the lowest common ancestor path. + public func lowestCommonAncestor(with path: AbsolutePath) -> AbsolutePath? { + guard root == path.root else { + return nil + } + var filepath = path.filepath + while (!filepath.isRoot) { + if self.filepath.starts(with: filepath) { + break + } + filepath.removeLastComponent() + } + return AbsolutePath(filepath) } - /// Returns an array of strings that make up the path components of the - /// absolute path. This is the same sequence of strings as the basenames - /// of each successive path component, starting from the root. Therefore - /// the first path component of an absolute path is always `/`. - public var components: [String] { - return _impl.components + /// The root directory. It is always `/` on UNIX, but may vary on Windows. + @available(*, deprecated, message: "root is not a static value, use the instance property instead") + public static var root: AbsolutePath { + if let rootPath = localFileSystem.currentWorkingDirectory?.root { + return AbsolutePath(rootPath) + } else { + return AbsolutePath(FilePath._root) + } } } @@ -215,13 +296,14 @@ public struct AbsolutePath: Hashable { /// This string manipulation may change the meaning of a path if any of the /// path components are symbolic links on disk. However, the file system is /// never accessed in any way when initializing a RelativePath. -public struct RelativePath: Hashable { - /// Private implementation details, shared with the AbsolutePath struct. - fileprivate let _impl: PathImpl +public struct RelativePath: Path { + /// Underlying type, based on SwiftSystem. + public let filepath: FilePath - /// Private initializer when the backing storage is known. - private init(_ impl: PathImpl) { - _impl = impl + /// Public initializer with FilePath. + public init(_ filepath: FilePath) { + precondition(filepath.isRelative) + self.filepath = filepath.lexicallyNormalized() } /// Initializes the RelativePath from `str`, which must be a relative path @@ -230,112 +312,18 @@ public struct RelativePath: Hashable { /// character. The input string will be normalized if needed, as described /// in the documentation for RelativePath. public init(_ string: String) { - // Normalize the relative string and store it as our Path. - self.init(PathImpl(normalizingRelativePath: string)) + self.init(FilePath(string)) } /// Convenience initializer that verifies that the path is relative. public init(validating path: String) throws { - try self.init(PathImpl(validatingRelativePath: path)) - } - - /// Directory component. For a relative path without any path separators, - /// this is the `.` string instead of the empty string. - public var dirname: String { - return _impl.dirname - } - - /// Last path component (including the suffix, if any). It is never empty. - public var basename: String { - return _impl.basename - } - - /// Returns the basename without the extension. - public var basenameWithoutExt: String { - if let ext = self.extension { - return String(basename.dropLast(ext.count + 1)) - } - return basename - } - - /// Suffix (including leading `.` character) if any. Note that a basename - /// that starts with a `.` character is not considered a suffix, nor is a - /// trailing `.` character. - public var suffix: String? { - return _impl.suffix - } - - /// Extension of the give path's basename. This follow same rules as - /// suffix except that it doesn't include leading `.` character. - public var `extension`: String? { - return _impl.extension - } - - /// Normalized string representation (the normalization rules are described - /// in the documentation of the initializer). This string is never empty. - public var pathString: String { - return _impl.string - } - - /// Returns an array of strings that make up the path components of the - /// relative path. This is the same sequence of strings as the basenames - /// of each successive path component. Therefore the returned array of - /// path components is never empty; even an empty path has a single path - /// component: the `.` string. - public var components: [String] { - return _impl.components + try self.init(FilePath(validatingRelativePath: path)) } /// Returns the relative path with the given relative path applied. public func appending(_ subpath: RelativePath) -> RelativePath { - return RelativePath(_impl.appending(relativePath: subpath._impl)) - } - - /// Returns the relative path with an additional literal component appended. - /// - /// This method accepts pseudo-path like '.' or '..', but should not contain "/". - public func appending(component: String) -> RelativePath { - return RelativePath(_impl.appending(component: component)) - } - - /// Returns the relative path with additional literal components appended. - /// - /// This method should only be used in cases where the input is guaranteed - /// to be a valid path component (i.e., it cannot be empty, contain a path - /// separator, or be a pseudo-path like '.' or '..'). - public func appending(components names: [String]) -> RelativePath { - // FIXME: This doesn't seem a particularly efficient way to do this. - return names.reduce(self, { path, name in - path.appending(component: name) - }) - } - - public func appending(components names: String...) -> RelativePath { - appending(components: names) - } -} - -extension AbsolutePath: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(pathString) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - try self.init(validating: container.decode(String.self)) - } -} - -extension RelativePath: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(pathString) - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - try self.init(validating: container.decode(String.self)) + return + RelativePath(filepath.pushing(subpath.filepath)) } } @@ -346,509 +334,22 @@ extension AbsolutePath: Comparable { } } -/// Make absolute paths CustomStringConvertible and CustomDebugStringConvertible. -extension AbsolutePath: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return pathString - } - - public var debugDescription: String { - // FIXME: We should really be escaping backslashes and quotes here. - return "" - } -} - -/// Make relative paths CustomStringConvertible and CustomDebugStringConvertible. -extension RelativePath: CustomStringConvertible { - public var description: String { - return _impl.string - } - - public var debugDescription: String { - // FIXME: We should really be escaping backslashes and quotes here. - return "" - } -} - -/// Private implementation shared between AbsolutePath and RelativePath. -protocol Path: Hashable { - - /// Root directory. - static var root: Self { get } - - /// Checks if a string is a valid component. - static func isValidComponent(_ name: String) -> Bool - - /// Normalized string of the (absolute or relative) path. Never empty. - var string: String { get } - - /// Returns whether the path is an absolute path. - var isAbsolute: Bool { get } - - /// Returns the directory part of the stored path (relying on the fact that it has been normalized). Returns a - /// string consisting of just `.` if there is no directory part (which is the case if and only if there is no path - /// separator). - var dirname: String { get } - - /// Returns the last past component. - var basename: String { get } - - /// Returns the components of the path between each path separator. - var components: [String] { get } - - /// Path of parent directory. This always returns a path, because every directory has a parent (the parent - /// directory of the root directory is considered to be the root directory itself). - var parentDirectory: Self { get } - - /// Creates a path from its normalized string representation. - init(string: String) - - /// Creates a path from an absolute string representation and normalizes it. - init(normalizingAbsolutePath: String) - - /// Creates a path from an relative string representation and normalizes it. - init(normalizingRelativePath: String) - - /// Creates a path from a string representation, validates that it is a valid absolute path and normalizes it. - init(validatingAbsolutePath: String) throws - - /// Creates a path from a string representation, validates that it is a valid relative path and normalizes it. - init(validatingRelativePath: String) throws - - /// Returns suffix with leading `.` if withDot is true otherwise without it. - func suffix(withDot: Bool) -> String? - - /// Returns a new Path by appending the path component. - func appending(component: String) -> Self - - /// Returns a path by concatenating a relative path and renormalizing if necessary. - func appending(relativePath: Self) -> Self -} - -extension Path { - var suffix: String? { - return suffix(withDot: true) - } - - var `extension`: String? { - return suffix(withDot: false) - } -} - -private struct UNIXPath: Path { - let string: String - - static let root = UNIXPath(string: "/") - - static func isValidComponent(_ name: String) -> Bool { - return name != "" && name != "." && name != ".." && !name.contains("/") - } - -#if os(Windows) - static func isAbsolutePath(_ path: String) -> Bool { - return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) - } -#endif - - var dirname: String { -#if os(Windows) - let fsr: UnsafePointer = string.fileSystemRepresentation - defer { fsr.deallocate() } - - let path: String = String(cString: fsr) - return path.withCString(encodedAs: UTF16.self) { - let data = UnsafeMutablePointer(mutating: $0) - PathCchRemoveFileSpec(data, path.count) - return String(decodingCString: data, as: UTF16.self) - } -#else - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Find the last path separator. - guard let idx = string.lastIndex(of: "/") else { - // No path separators, so the directory name is `.`. - return "." - } - // Check if it's the only one in the string. - if idx == string.startIndex { - // Just one path separator, so the directory name is `/`. - return "/" - } - // Otherwise, it's the string up to (but not including) the last path - // separator. - return String(string.prefix(upTo: idx)) -#endif - } - - var isAbsolute: Bool { -#if os(Windows) - return UNIXPath.isAbsolutePath(string) -#else - return string.hasPrefix("/") -#endif - } - - var basename: String { -#if os(Windows) - let path: String = self.string - return path.withCString(encodedAs: UTF16.self) { - PathStripPathW(UnsafeMutablePointer(mutating: $0)) - return String(decodingCString: $0, as: UTF16.self) - } -#else - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Check for a special case of the root directory. - if string.spm_only == "/" { - // Root directory, so the basename is a single path separator (the - // root directory is special in this regard). - return "/" - } - // Find the last path separator. - guard let idx = string.lastIndex(of: "/") else { - // No path separators, so the basename is the whole string. - return string - } - // Otherwise, it's the string from (but not including) the last path - // separator. - return String(string.suffix(from: string.index(after: idx))) -#endif - } - - // FIXME: We should investigate if it would be more efficient to instead - // return a path component iterator that does all its work lazily, moving - // from one path separator to the next on-demand. - // - var components: [String] { -#if os(Windows) - return string.components(separatedBy: "\\").filter { !$0.isEmpty } -#else - // FIXME: This isn't particularly efficient; needs optimization, and - // in fact, it might well be best to return a custom iterator so we - // don't have to allocate everything up-front. It would be backed by - // the path string and just return a slice at a time. - let components = string.components(separatedBy: "/").filter({ !$0.isEmpty }) - - if string.hasPrefix("/") { - return ["/"] + components - } else { - return components - } -#endif - } - - var parentDirectory: UNIXPath { - return self == .root ? self : Self(string: dirname) - } - - init(string: String) { - self.string = string - } - - init(normalizingAbsolutePath path: String) { - #if os(Windows) - var buffer: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) - _ = path.withCString(encodedAs: UTF16.self) { - PathCanonicalizeW(&buffer, $0) - } - self.init(string: String(decodingCString: buffer, as: UTF16.self)) - #else - precondition(path.first == "/", "Failure normalizing \(path), absolute paths should start with '/'") - - // At this point we expect to have a path separator as first character. - assert(path.first == "/") - // Fast path. - if !mayNeedNormalization(absolute: path) { - self.init(string: path) - } - - // Split the character array into parts, folding components as we go. - // As we do so, we count the number of characters we'll end up with in - // the normalized string representation. - var parts: [String] = [] - var capacity = 0 - for part in path.split(separator: "/") { - switch part.count { - case 0: - // Ignore empty path components. - continue - case 1 where part.first == ".": - // Ignore `.` path components. - continue - case 2 where part.first == "." && part.last == ".": - // If there's a previous part, drop it; otherwise, do nothing. - if let prev = parts.last { - parts.removeLast() - capacity -= prev.count - } - default: - // Any other component gets appended. - parts.append(String(part)) - capacity += part.count - } - } - capacity += max(parts.count, 1) - - // Create an output buffer using the capacity we've calculated. - // FIXME: Determine the most efficient way to reassemble a string. - var result = "" - result.reserveCapacity(capacity) - - // Put the normalized parts back together again. - var iter = parts.makeIterator() - result.append("/") - if let first = iter.next() { - result.append(contentsOf: first) - while let next = iter.next() { - result.append("/") - result.append(contentsOf: next) - } - } - - // Sanity-check the result (including the capacity we reserved). - assert(!result.isEmpty, "unexpected empty string") - assert(result.count == capacity, "count: " + - "\(result.count), cap: \(capacity)") - - // Use the result as our stored string. - self.init(string: result) - #endif - } - - init(normalizingRelativePath path: String) { - #if os(Windows) - var buffer: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) - _ = path.replacingOccurrences(of: "/", with: "\\").withCString(encodedAs: UTF16.self) { - PathCanonicalizeW(&buffer, $0) - } - self.init(string: String(decodingCString: buffer, as: UTF16.self)) - #else - precondition(path.first != "/") - - // FIXME: Here we should also keep track of whether anything actually has - // to be changed in the string, and if not, just return the existing one. - - // Split the character array into parts, folding components as we go. - // As we do so, we count the number of characters we'll end up with in - // the normalized string representation. - var parts: [String] = [] - var capacity = 0 - for part in path.split(separator: "/") { - switch part.count { - case 0: - // Ignore empty path components. - continue - case 1 where part.first == ".": - // Ignore `.` path components. - continue - case 2 where part.first == "." && part.last == ".": - // If at beginning, fall through to treat the `..` literally. - guard let prev = parts.last else { - fallthrough - } - // If previous component is anything other than `..`, drop it. - if !(prev.count == 2 && prev.first == "." && prev.last == ".") { - parts.removeLast() - capacity -= prev.count - continue - } - // Otherwise, fall through to treat the `..` literally. - fallthrough - default: - // Any other component gets appended. - parts.append(String(part)) - capacity += part.count - } - } - capacity += max(parts.count - 1, 0) - - // Create an output buffer using the capacity we've calculated. - // FIXME: Determine the most efficient way to reassemble a string. - var result = "" - result.reserveCapacity(capacity) - - // Put the normalized parts back together again. - var iter = parts.makeIterator() - if let first = iter.next() { - result.append(contentsOf: first) - while let next = iter.next() { - result.append("/") - result.append(contentsOf: next) - } - } - - // Sanity-check the result (including the capacity we reserved). - assert(result.count == capacity, "count: " + - "\(result.count), cap: \(capacity)") - - // If the result is empty, return `.`, otherwise we return it as a string. - self.init(string: result.isEmpty ? "." : result) - #endif - } - - init(validatingAbsolutePath path: String) throws { - #if os(Windows) - let fsr: UnsafePointer = path.fileSystemRepresentation - defer { fsr.deallocate() } - - let realpath = String(cString: fsr) - if !UNIXPath.isAbsolutePath(realpath) { - throw PathValidationError.invalidAbsolutePath(path) - } - self.init(normalizingAbsolutePath: path) - #else - switch path.first { - case "/": - self.init(normalizingAbsolutePath: path) - case "~": - throw PathValidationError.startsWithTilde(path) - default: - throw PathValidationError.invalidAbsolutePath(path) - } - #endif - } - - init(validatingRelativePath path: String) throws { - #if os(Windows) - let fsr: UnsafePointer = path.fileSystemRepresentation - defer { fsr.deallocate() } - - let realpath: String = String(cString: fsr) - if UNIXPath.isAbsolutePath(realpath) { - throw PathValidationError.invalidRelativePath(path) - } - self.init(normalizingRelativePath: path) - #else - switch path.first { - case "/", "~": - throw PathValidationError.invalidRelativePath(path) - default: - self.init(normalizingRelativePath: path) - } - #endif - } - - func suffix(withDot: Bool) -> String? { -#if os(Windows) - let ext = self.string.withCString(encodedAs: UTF16.self) { - PathFindExtensionW($0) - } - var result = String(decodingCString: ext!, as: UTF16.self) - guard result.length > 0 else { return nil } - if !withDot { result.removeFirst(1) } - return result -#else - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Find the last path separator, if any. - let sIdx = string.lastIndex(of: "/") - // Find the start of the basename. - let bIdx = (sIdx != nil) ? string.index(after: sIdx!) : string.startIndex - // Find the last `.` (if any), starting from the second character of - // the basename (a leading `.` does not make the whole path component - // a suffix). - let fIdx = string.index(bIdx, offsetBy: 1, limitedBy: string.endIndex) ?? string.startIndex - if let idx = string[fIdx...].lastIndex(of: ".") { - // Unless it's just a `.` at the end, we have found a suffix. - if string.distance(from: idx, to: string.endIndex) > 1 { - let fromIndex = withDot ? idx : string.index(idx, offsetBy: 1) - return String(string.suffix(from: fromIndex)) - } else { - return nil - } - } - // If we get this far, there is no suffix. - return nil -#endif - } - - func appending(component name: String) -> UNIXPath { -#if os(Windows) - var result: PWSTR? - _ = string.withCString(encodedAs: UTF16.self) { root in - name.withCString(encodedAs: UTF16.self) { path in - PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) - } - } - defer { LocalFree(result) } - return PathImpl(string: String(decodingCString: result!, as: UTF16.self)) -#else - assert(!name.contains("/"), "\(name) is invalid path component") - - // Handle pseudo paths. - switch name { - case "", ".": - return self - case "..": - return self.parentDirectory - default: - break - } - - if self == Self.root { - return PathImpl(string: "/" + name) - } else { - return PathImpl(string: string + "/" + name) - } -#endif - } - - func appending(relativePath: UNIXPath) -> UNIXPath { -#if os(Windows) - var result: PWSTR? - _ = string.withCString(encodedAs: UTF16.self) { root in - relativePath.string.withCString(encodedAs: UTF16.self) { path in - PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) - } - } - defer { LocalFree(result) } - return PathImpl(string: String(decodingCString: result!, as: UTF16.self)) -#else - // Both paths are already normalized. The only case in which we have - // to renormalize their concatenation is if the relative path starts - // with a `..` path component. - var newPathString = string - if self != .root { - newPathString.append("/") - } - - let relativePathString = relativePath.string - newPathString.append(relativePathString) - - // If the relative string starts with `.` or `..`, we need to normalize - // the resulting string. - // FIXME: We can actually optimize that case, since we know that the - // normalization of a relative path can leave `..` path components at - // the beginning of the path only. - if relativePathString.hasPrefix(".") { - if newPathString.hasPrefix("/") { - return PathImpl(normalizingAbsolutePath: newPathString) - } else { - return PathImpl(normalizingRelativePath: newPathString) - } - } else { - return PathImpl(string: newPathString) - } -#endif - } -} - /// Describes the way in which a path is invalid. public enum PathValidationError: Error { - case startsWithTilde(String) case invalidAbsolutePath(String) case invalidRelativePath(String) + case differentRoot(String, String) } extension PathValidationError: CustomStringConvertible { public var description: String { switch self { - case .startsWithTilde(let path): - return "invalid absolute path '\(path)'; absolute path must begin with '/'" case .invalidAbsolutePath(let path): return "invalid absolute path '\(path)'" case .invalidRelativePath(let path): - return "invalid relative path '\(path)'; relative path should not begin with '/' or '~'" + return "invalid relative path '\(path)'" + case .differentRoot(let pathA, let pathB): + return "absolute paths '\(pathA)' and '\(pathB)' have different roots" } } } @@ -865,45 +366,29 @@ extension AbsolutePath { /// /// This method is strictly syntactic and does not access the file system /// in any way. Therefore, it does not take symbolic links into account. - public func relative(to base: AbsolutePath) -> RelativePath { - let result: RelativePath - // Split the two paths into their components. - // FIXME: The is needs to be optimized to avoid unncessary copying. - let pathComps = self.components - let baseComps = base.components - - // It's common for the base to be an ancestor, so try that first. - if pathComps.starts(with: baseComps) { - // Special case, which is a plain path without `..` components. It - // might be an empty path (when self and the base are equal). - let relComps = pathComps.dropFirst(baseComps.count) -#if os(Windows) - result = RelativePath(relComps.joined(separator: "\\")) -#else - result = RelativePath(relComps.joined(separator: "/")) -#endif - } else { - // General case, in which we might well need `..` components to go - // "up" before we can go "down" the directory tree. - var newPathComps = ArraySlice(pathComps) - var newBaseComps = ArraySlice(baseComps) - while newPathComps.prefix(1) == newBaseComps.prefix(1) { - // First component matches, so drop it. - newPathComps = newPathComps.dropFirst() - newBaseComps = newBaseComps.dropFirst() - } - // Now construct a path consisting of as many `..`s as are in the - // `newBaseComps` followed by what remains in `newPathComps`. - var relComps = Array(repeating: "..", count: newBaseComps.count) - relComps.append(contentsOf: newPathComps) + public func relative(to base: AbsolutePath) throws -> RelativePath { + var relFilePath = FilePath() + var filepath = filepath #if os(Windows) - result = RelativePath(relComps.joined(separator: "\\")) -#else - result = RelativePath(relComps.joined(separator: "/")) -#endif + /// TODO: DOS relative path may change the root. + if root != base.root { + throw PathValidationError.differentRoot(pathString, base.pathString) } - assert(base.appending(result) == self) - return result +#endif + filepath.root = base.filepath.root + + let commonAncestor = AbsolutePath(filepath).lowestCommonAncestor(with: base)! + let walkbackDepth: Int = { + var baseFilepath = base.filepath + precondition(baseFilepath.removePrefix(commonAncestor.filepath)) + return baseFilepath.components.count + }() + precondition(filepath.removePrefix(commonAncestor.filepath)) + + relFilePath.append(Array(repeating: FilePath.Component(".."), count: walkbackDepth)) + relFilePath.push(filepath) + + return RelativePath(relFilePath) } /// Returns true if the path contains the given path. @@ -911,7 +396,7 @@ extension AbsolutePath { /// This method is strictly syntactic and does not access the file system /// in any way. public func contains(_ other: AbsolutePath) -> Bool { - return self.components.starts(with: other.components) + return filepath.starts(with: other.filepath) } } @@ -922,31 +407,35 @@ extension PathValidationError: CustomNSError { } } -// FIXME: We should consider whether to merge the two `normalize()` functions. -// The argument for doing so is that some of the code is repeated; the argument -// against doing so is that some of the details are different, and since any -// given path is either absolute or relative, it's wasteful to keep checking -// for whether it's relative or absolute. Possibly we can do both by clever -// use of generics that abstract away the differences. +extension FilePath { + static var _root: FilePath { +#if os(Windows) + return FilePath("\\") +#else + return FilePath("/") +#endif + } + + init(validatingAbsolutePath path: String) throws { + self.init(path) + guard self.isAbsolute else { + throw PathValidationError.invalidAbsolutePath(path) + } + } -/// Fast check for if a string might need normalization. -/// -/// This assumes that paths containing dotfiles are rare: -private func mayNeedNormalization(absolute string: String) -> Bool { - var last = UInt8(ascii: "0") - for c in string.utf8 { - switch c { - case UInt8(ascii: "/") where last == UInt8(ascii: "/"): - return true - case UInt8(ascii: ".") where last == UInt8(ascii: "/"): - return true - default: - break + init(validatingRelativePath path: String) throws { + self.init(path) + guard self.isRelative else { + throw PathValidationError.invalidRelativePath(path) + } +#if !os(Windows) + guard self.components.first?.string != "~" else { + throw PathValidationError.invalidRelativePath(path) } - last = c +#endif } - if last == UInt8(ascii: "/") { - return true + + var isRoot: Bool { + root != nil && components.isEmpty } - return false } diff --git a/Sources/TSCBasic/PathShims.swift b/Sources/TSCBasic/PathShims.swift index bae0210d..e50c26f3 100644 --- a/Sources/TSCBasic/PathShims.swift +++ b/Sources/TSCBasic/PathShims.swift @@ -63,7 +63,7 @@ public func makeDirectories(_ path: AbsolutePath) throws { /// be a relative path, otherwise it will be absolute. @available(*, deprecated, renamed: "localFileSystem.createSymbolicLink") public func createSymlink(_ path: AbsolutePath, pointingAt dest: AbsolutePath, relative: Bool = true) throws { - let destString = relative ? dest.relative(to: path.parentDirectory).pathString : dest.pathString + let destString = relative ? try dest.relative(to: path.parentDirectory).pathString : dest.pathString try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString) } @@ -168,18 +168,16 @@ extension AbsolutePath { /// Returns a path suitable for display to the user (if possible, it is made /// to be relative to the current working directory). public func prettyPath(cwd: AbsolutePath? = localFileSystem.currentWorkingDirectory) -> String { - guard let dir = cwd else { - // No current directory, display as is. + guard let dir = cwd, + let rel = try? relative(to: dir) else { + // Cannot create relative path, display as is. return self.pathString } - // FIXME: Instead of string prefix comparison we should add a proper API - // to AbsolutePath to determine ancestry. - if self == dir { - return "." - } else if self.pathString.hasPrefix(dir.pathString + "/") { - return "./" + self.relative(to: dir).pathString + if let first = rel.components.first, + first != ".." { + return "./" + rel.pathString } else { - return self.pathString + return rel.pathString } } } diff --git a/Sources/TSCUtility/Platform.swift b/Sources/TSCUtility/Platform.swift index e62e6511..9adb2ac4 100644 --- a/Sources/TSCUtility/Platform.swift +++ b/Sources/TSCUtility/Platform.swift @@ -105,8 +105,8 @@ public enum Platform: Equatable { defer { tmp.deallocate() } guard confstr(name, tmp.baseAddress, len) == len else { return nil } let value = String(cString: tmp.baseAddress!) - guard value.hasSuffix(AbsolutePath.root.pathString) else { return nil } - return resolveSymlinks(AbsolutePath(value)) + guard let path = try? AbsolutePath(validating: value) else { return nil } + return resolveSymlinks(path) } #endif } diff --git a/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift b/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift index a68a5281..83c57a37 100644 --- a/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift +++ b/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift @@ -14,7 +14,7 @@ import TSCBasic import TSCTestSupport class SynchronizedQueuePerfTests: XCTestCasePerf { - + // Mock the UnitTest struct in SwiftPM/SwiftTestTool.swift struct Item { let productPath: AbsolutePath @@ -28,7 +28,6 @@ class SynchronizedQueuePerfTests: XCTestCasePerf { } } - func testEnqueueDequeue_10000() { let queue = SynchronizedQueue() let test = Item(productPath: AbsolutePath.root, name: "TestName", testCase: "TestCaseName") @@ -42,7 +41,7 @@ class SynchronizedQueuePerfTests: XCTestCasePerf { } } } - + func testEnqueueDequeue_1000() { let queue = SynchronizedQueue() let test = Item(productPath: AbsolutePath.root, name: "TestName", testCase: "TestCaseName") @@ -56,7 +55,7 @@ class SynchronizedQueuePerfTests: XCTestCasePerf { } } } - + func testEnqueueDequeue_100() { let queue = SynchronizedQueue() let test = Item(productPath: AbsolutePath.root, name: "TestName", testCase: "TestCaseName") diff --git a/Tests/TSCBasicTests/FileSystemTests.swift b/Tests/TSCBasicTests/FileSystemTests.swift index 0c548674..978bcf57 100644 --- a/Tests/TSCBasicTests/FileSystemTests.swift +++ b/Tests/TSCBasicTests/FileSystemTests.swift @@ -245,7 +245,7 @@ class FileSystemTests: XCTestCase { XCTAssertEqual(try! fs.readFileContents(filePath), "Hello, new world!") // Check read/write of a directory. - XCTAssertThrows(FileSystemError(.ioError(code: TSCLibc.EPERM), filePath.parentDirectory)) { + XCTAssertThrows(FileSystemError(.isDirectory, filePath.parentDirectory)) { _ = try fs.readFileContents(filePath.parentDirectory) } XCTAssertThrows(FileSystemError(.isDirectory, filePath.parentDirectory)) { @@ -259,9 +259,8 @@ class FileSystemTests: XCTestCase { #else let root = AbsolutePath("/") #endif - XCTAssertThrows(FileSystemError(.ioError(code: TSCLibc.EPERM), root)) { + XCTAssertThrows(FileSystemError(.isDirectory, root)) { _ = try fs.readFileContents(root) - } XCTAssertThrows(FileSystemError(.isDirectory, root)) { try fs.writeFileContents(root, bytes: []) diff --git a/Tests/TSCBasicTests/PathShimTests.swift b/Tests/TSCBasicTests/PathShimTests.swift index 9d79535a..f7a2dbf7 100644 --- a/Tests/TSCBasicTests/PathShimTests.swift +++ b/Tests/TSCBasicTests/PathShimTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -54,9 +54,9 @@ class WalkTests : XCTestCase { expected.remove(at: i) } #if os(Android) - XCTAssertEqual(3, x.components.count) - #else XCTAssertEqual(2, x.components.count) + #else + XCTAssertEqual(1, x.components.count) #endif } XCTAssertEqual(expected.count, 0) diff --git a/Tests/TSCBasicTests/PathTests.swift b/Tests/TSCBasicTests/PathTests.swift index ea8c191f..34f5d79d 100644 --- a/Tests/TSCBasicTests/PathTests.swift +++ b/Tests/TSCBasicTests/PathTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -229,8 +229,6 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath("/").appending(components: "a", "b").pathString, "/a/b") XCTAssertEqual(AbsolutePath("/a").appending(components: "b", "c").pathString, "/a/b/c") - XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "", "c").pathString, "/a/b/c/c") - XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "").pathString, "/a/b/c") XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: ".").pathString, "/a/b/c") XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "..").pathString, "/a/b") XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "..", "d").pathString, "/a/b/d") @@ -243,14 +241,14 @@ class PathTests: XCTestCase { } func testPathComponents() { - XCTAssertEqual(AbsolutePath("/").components, ["/"]) - XCTAssertEqual(AbsolutePath("/.").components, ["/"]) - XCTAssertEqual(AbsolutePath("/..").components, ["/"]) - XCTAssertEqual(AbsolutePath("/bar").components, ["/", "bar"]) - XCTAssertEqual(AbsolutePath("/foo/bar/..").components, ["/", "foo"]) - XCTAssertEqual(AbsolutePath("/bar/../foo").components, ["/", "foo"]) - XCTAssertEqual(AbsolutePath("/bar/../foo/..//").components, ["/"]) - XCTAssertEqual(AbsolutePath("/bar/../foo/..//yabba/a/b/").components, ["/", "yabba", "a", "b"]) + XCTAssertEqual(AbsolutePath("/").components, []) + XCTAssertEqual(AbsolutePath("/.").components, []) + XCTAssertEqual(AbsolutePath("/..").components, []) + XCTAssertEqual(AbsolutePath("/bar").components, ["bar"]) + XCTAssertEqual(AbsolutePath("/foo/bar/..").components, ["foo"]) + XCTAssertEqual(AbsolutePath("/bar/../foo").components, ["foo"]) + XCTAssertEqual(AbsolutePath("/bar/../foo/..//").components, []) + XCTAssertEqual(AbsolutePath("/bar/../foo/..//yabba/a/b/").components, ["yabba", "a", "b"]) XCTAssertEqual(RelativePath("").components, ["."]) XCTAssertEqual(RelativePath(".").components, ["."]) @@ -271,13 +269,13 @@ class PathTests: XCTestCase { } func testRelativePathFromAbsolutePaths() { - XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/")), RelativePath(".")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/")), RelativePath("a/b/c/d")); - XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/a/b/c")), RelativePath("../../..")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b")), RelativePath("c/d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b/c")), RelativePath("d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/c/d")), RelativePath("../../b/c/d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/b/c/d")), RelativePath("../../../a/b/c/d")); + XCTAssertEqual(try! AbsolutePath("/").relative(to: AbsolutePath("/")), RelativePath(".")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/")), RelativePath("a/b/c/d")); + XCTAssertEqual(try! AbsolutePath("/").relative(to: AbsolutePath("/a/b/c")), RelativePath("../../..")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b")), RelativePath("c/d")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b/c")), RelativePath("d")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/c/d")), RelativePath("../../b/c/d")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/b/c/d")), RelativePath("../../../a/b/c/d")); } func testComparison() { @@ -302,7 +300,7 @@ class PathTests: XCTestCase { XCTAssertNoThrow(try AbsolutePath(validating: "/a/b/c/d")) XCTAssertThrowsError(try AbsolutePath(validating: "~/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid absolute path '~/a/b/d'; absolute path must begin with '/'") + XCTAssertEqual("\(error)", "invalid absolute path '~/a/b/d'") } XCTAssertThrowsError(try AbsolutePath(validating: "a/b/d")) { error in @@ -314,11 +312,11 @@ class PathTests: XCTestCase { XCTAssertNoThrow(try RelativePath(validating: "a/b/c/d")) XCTAssertThrowsError(try RelativePath(validating: "/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid relative path '/a/b/d'; relative path should not begin with '/' or '~'") + XCTAssertEqual("\(error)", "invalid relative path '/a/b/d'") } XCTAssertThrowsError(try RelativePath(validating: "~/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid relative path '~/a/b/d'; relative path should not begin with '/' or '~'") + XCTAssertEqual("\(error)", "invalid relative path '~/a/b/d'") } }