Jonathan Flat 914b9f7bbd
Support NS/CFURL re-core in Swift (#1238)
* (146349351) Support NS/CFURL re-core in Swift

* Fix .fileSystemPath() calls in Windows test

* Use encoded strings for .absoluteURL, fix NSURL bridging and CFURL lastPathComponent edge cases

* Add workaround for crash on Linux

* Fix typo
2025-04-11 22:37:51 -06:00

1221 lines
48 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if canImport(Darwin)
import Darwin
#elseif canImport(Android)
@preconcurrency import Android
#elseif canImport(Glibc)
@preconcurrency import Glibc
#elseif canImport(Musl)
@preconcurrency import Musl
#elseif os(Windows)
import WinSDK
#elseif os(WASI)
@preconcurrency import WASILibc
#endif
/// `_SwiftURL` provides the new Swift implementation for `URL`, using the same parser
/// and `URLParseInfo` as `URLComponents`, but with a few compatibility behaviors.
///
/// Outside of `FOUNDATION_FRAMEWORK`, `_SwiftURL` provides the sole implementation
/// for `URL`. In `FOUNDATION_FRAMEWORK`, there are additional classes to handle `NSURL`
/// subclassing and bridging from ObjC.
///
/// - Note: For functions returning `URL?`, a `nil` return value allows `struct URL` to return `self` without creating a new struct.
internal final class _SwiftURL: Sendable, Hashable, Equatable {
typealias Parser = RFC3986Parser
internal let _parseInfo: URLParseInfo
internal let _baseURL: URL?
internal let _encoding: String.Encoding
#if FOUNDATION_FRAMEWORK
// Used frequently for NS/CFURL behaviors
internal var isDecomposable: Bool {
return _parseInfo.scheme == nil || hasAuthority || _parseInfo.path.utf8.first == ._slash
}
// For use by CoreServicesInternal to cache property values.
internal final class ResourceInfo: @unchecked Sendable {
let ref = LockedState<CFTypeRef?>(initialState: nil)
}
internal let _resourceInfo = ResourceInfo()
// Only used if foundation_swift_nsurl_enabled() is false.
// Note: We use a lock instead of a lazy var to ensure that we always
// bridge to the same NSURL even if the URL was copied across threads.
private let _nsurlLock = LockedState<NSURL?>(initialState: nil)
private var _nsurl: NSURL {
return _nsurlLock.withLock {
if let nsurl = $0 { return nsurl }
let nsurl = Self._makeNSURL(from: _parseInfo, baseURL: _baseURL)
$0 = nsurl
return nsurl
}
}
#endif
internal var url: URL {
URL(self)
}
private static func parse(string: String, encodingInvalidCharacters: Bool = true) -> URLParseInfo? {
return Parser.parse(urlString: string, encodingInvalidCharacters: encodingInvalidCharacters, allowEmptyScheme: true)
}
private static func compatibilityParse(string: String, encodingInvalidCharacters: Bool = false) -> URLParseInfo? {
return Parser.compatibilityParse(urlString: string, encodingInvalidCharacters: encodingInvalidCharacters)
}
init?(stringOrEmpty: String, relativeTo base: URL? = nil, encodingInvalidCharacters: Bool = true, encoding: String.Encoding = .utf8, compatibility: Bool = false, forceBaseURL: Bool = false) {
let parseInfo = if compatibility {
Self.compatibilityParse(string: stringOrEmpty, encodingInvalidCharacters: encodingInvalidCharacters)
} else {
Self.parse(string: stringOrEmpty, encodingInvalidCharacters: encodingInvalidCharacters)
}
guard let parseInfo else { return nil }
_parseInfo = parseInfo
_baseURL = (forceBaseURL || parseInfo.scheme == nil) ? base?.absoluteURL : nil
_encoding = encoding
}
convenience init?(string: String) {
guard !string.isEmpty else { return nil }
self.init(stringOrEmpty: string)
}
convenience init?(string: String, relativeTo base: URL?) {
guard !string.isEmpty else { return nil }
self.init(stringOrEmpty: string, relativeTo: base)
}
convenience init?(string: String, encodingInvalidCharacters: Bool) {
guard !string.isEmpty else { return nil }
self.init(stringOrEmpty: string, encodingInvalidCharacters: encodingInvalidCharacters)
}
convenience init?(stringOrEmpty: String, relativeTo base: URL?) {
self.init(stringOrEmpty: stringOrEmpty, relativeTo: base, encoding: .utf8)
}
convenience init(fileURLWithPath path: String, isDirectory: Bool, relativeTo base: URL?) {
let directoryHint: URL.DirectoryHint = isDirectory ? .isDirectory : .notDirectory
self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint, relativeTo: base)
}
convenience init(fileURLWithPath path: String, relativeTo base: URL?) {
let directoryHint: URL.DirectoryHint = path.utf8.last == ._slash ? .isDirectory : .checkFileSystem
self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint, relativeTo: base)
}
convenience init(fileURLWithPath path: String, isDirectory: Bool) {
let directoryHint: URL.DirectoryHint = isDirectory ? .isDirectory : .notDirectory
self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint)
}
convenience init(fileURLWithPath path: String) {
let directoryHint: URL.DirectoryHint = path.utf8.last == ._slash ? .isDirectory : .checkFileSystem
self.init(filePath: path.isEmpty ? "." : path, directoryHint: directoryHint)
}
convenience init(filePath path: String, directoryHint: URL.DirectoryHint = .inferFromPath, relativeTo base: URL? = nil) {
self.init(filePath: path, pathStyle: URL.defaultPathStyle, directoryHint: directoryHint, relativeTo: base)
}
internal init(filePath path: String, pathStyle: URL.PathStyle, directoryHint: URL.DirectoryHint = .inferFromPath, relativeTo base: URL? = nil) {
var baseURL = base
guard !path.isEmpty else {
#if !NO_FILESYSTEM
baseURL = baseURL ?? Self.currentDirectoryOrNil()
#endif
_parseInfo = Parser.parse(filePath: "./", isAbsolute: false)
_baseURL = baseURL?.absoluteURL
_encoding = .utf8
return
}
var filePath = if pathStyle == .windows {
// Convert any "\" to "/" before storing the URL parse info
path.replacing(._backslash, with: ._slash)
} else {
path
}
#if FOUNDATION_FRAMEWORK
// Linked-on-or-after check for apps which incorrectly pass a full URL
// string with a scheme. In the old implementation, this could work
// rarely if the app immediately called .appendingPathComponent(_:),
// which used to accidentally interpret a relative path starting with
// "scheme:" as an absolute "scheme:" URL string.
if URL.compatibility1 {
if filePath.utf8.starts(with: "file:".utf8) {
#if canImport(os)
URL.logger.fault("API MISUSE: URL(filePath:) called with a \"file:\" scheme. Input must only contain a path. Dropping \"file:\" scheme.")
#endif
filePath = String(filePath.dropFirst(5))._compressingSlashes()
} else if filePath.utf8.starts(with: "http:".utf8) || filePath.utf8.starts(with: "https:".utf8) {
#if canImport(os)
URL.logger.fault("API MISUSE: URL(filePath:) called with an HTTP URL string. Using URL(string:) instead.")
#endif
guard let parseInfo = Self.parse(string: filePath, encodingInvalidCharacters: true) else {
fatalError("API MISUSE: URL(filePath:) called with an HTTP URL string. URL(string:) returned nil.")
}
_parseInfo = parseInfo
_baseURL = nil // Drop the base URL since we have an HTTP scheme
_encoding = .utf8
return
}
}
#endif
let isAbsolute = URL.isAbsolute(standardizing: &filePath, pathStyle: pathStyle)
#if !NO_FILESYSTEM
if !isAbsolute {
baseURL = baseURL ?? Self.currentDirectoryOrNil()
}
#endif
let isDirectory: Bool
switch directoryHint {
case .isDirectory:
isDirectory = true
case .notDirectory:
filePath = filePath._droppingTrailingSlashes
isDirectory = false
case .checkFileSystem:
#if !NO_FILESYSTEM
func absoluteFilePath() -> String {
guard !isAbsolute, let baseURL else {
return filePath
}
let absolutePath = baseURL.absolutePath(percentEncoded: true).merging(relativePath: filePath)
return Self.fileSystemPath(for: absolutePath)
}
isDirectory = Self.isDirectory(absoluteFilePath())
#else
isDirectory = filePath.utf8.last == ._slash
#endif
case .inferFromPath:
isDirectory = filePath.utf8.last == ._slash
}
if isDirectory && !filePath.isEmpty && filePath.utf8.last != ._slash {
filePath += "/"
}
if isAbsolute {
let encodedPath = Parser.percentEncode(filePath, component: .path) ?? "/"
_parseInfo = Parser.parse(filePath: encodedPath, isAbsolute: true)
_baseURL = nil // Drop the baseURL if the URL is absolute
} else {
let encodedPath = Parser.percentEncode(filePath, component: .path) ?? ""
_parseInfo = Parser.parse(filePath: encodedPath, isAbsolute: false)
_baseURL = baseURL?.absoluteURL
}
_encoding = .utf8
}
init(url: _SwiftURL) {
_parseInfo = url._parseInfo
_baseURL = url._baseURL?.absoluteURL
_encoding = url._encoding
}
convenience init?(dataRepresentation: Data, relativeTo base: URL?, isAbsolute: Bool) {
guard !dataRepresentation.isEmpty else { return nil }
var url: _SwiftURL?
if let string = String(data: dataRepresentation, encoding: .utf8) {
url = _SwiftURL(stringOrEmpty: string, relativeTo: base, encoding: .utf8, compatibility: true)
}
if url == nil, let string = String(data: dataRepresentation, encoding: .isoLatin1) {
url = _SwiftURL(stringOrEmpty: string, relativeTo: base, encoding: .isoLatin1, compatibility: true)
}
guard let url else {
return nil
}
if isAbsolute {
self.init(url: url.absoluteSwiftURL)
} else {
self.init(url: url)
}
}
convenience init(fileURLWithFileSystemRepresentation path: UnsafePointer<Int8>, isDirectory: Bool, relativeTo base: URL?) {
let pathString = String(cString: path)
let directoryHint: URL.DirectoryHint = isDirectory ? .isDirectory : .notDirectory
self.init(filePath: pathString, directoryHint: directoryHint, relativeTo: base)
}
internal var encodedComponents: URLParseInfo.EncodedComponentSet {
return _parseInfo.encodedComponents
}
// MARK: - Strings, Data, and URLs
internal var originalString: String {
guard !encodedComponents.isEmpty else {
return relativeString
}
return URLComponents(parseInfo: _parseInfo)._uncheckedString(original: true)
}
var dataRepresentation: Data {
guard let result = originalString.data(using: _encoding) else {
fatalError("Could not convert URL.relativeString to data using encoding: \(_encoding)")
}
return result
}
var relativeString: String {
return _parseInfo.urlString
}
internal func absoluteString(original: Bool) -> String {
guard let baseURL else {
return original ? originalString : relativeString
}
var builder = URLStringBuilder(parseInfo: _parseInfo, original: original)
if builder.scheme != nil {
builder.path = builder.path.removingDotSegments
return builder.string
}
if let baseScheme = baseURL.scheme {
builder.scheme = baseScheme
}
if hasAuthority {
return builder.string
}
let baseParseInfo = baseURL._swiftURL?._parseInfo
let baseEncodedComponents = baseParseInfo?.encodedComponents ?? []
if let baseUser = baseURL.user(percentEncoded: !baseEncodedComponents.contains(.user)) {
builder.user = baseUser
}
if let basePassword = baseURL.password(percentEncoded: !baseEncodedComponents.contains(.password)) {
builder.password = basePassword
}
if let baseHost = baseParseInfo?.host {
builder.host = baseEncodedComponents.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost)
} else if let baseHost = baseURL.host(percentEncoded: !baseEncodedComponents.contains(.host)) {
builder.host = baseHost
}
if let basePort = baseParseInfo?.portString {
builder.portString = String(basePort)
} else if let basePort = baseURL.port {
builder.portString = String(basePort)
}
if builder.path.isEmpty {
builder.path = baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path))
if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseEncodedComponents.contains(.query)) {
builder.query = baseQuery
}
} else {
let newPath = if builder.path.utf8.first == ._slash {
builder.path
} else if baseURL.hasAuthority && baseURL.path().isEmpty {
"/" + builder.path
} else {
baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path)).merging(relativePath: builder.path)
}
builder.path = newPath.removingDotSegments
}
return builder.string
}
var absoluteString: String {
return absoluteString(original: false)
}
var baseURL: URL? {
return _baseURL
}
private var absoluteSwiftURL: _SwiftURL {
guard baseURL != nil else { return self }
return _SwiftURL(stringOrEmpty: absoluteString(original: false), encoding: _encoding, compatibility: true) ?? self
}
var absoluteURL: URL? {
guard baseURL != nil else { return nil }
return absoluteSwiftURL.url
}
// Compatibility mode for CFURLCreateAbsoluteURLWithBytes
internal var compatibilityAbsoluteString: String {
guard let baseURL = baseURL?._swiftURL else {
return URLStringBuilder(parseInfo: _parseInfo, original: true).removingDotSegments.string
}
let first = originalString.utf8.first
if first == nil || first == UInt8(ascii: "?") || first == UInt8(ascii: "#") {
return URLStringBuilder(parseInfo: baseURL._parseInfo, original: true).removingDotSegments.string + originalString
}
var builder = URLStringBuilder(parseInfo: _parseInfo, original: true)
if let scheme {
guard scheme == baseURL.scheme else {
return URLStringBuilder(parseInfo: _parseInfo, original: true).removingDotSegments.string
}
builder.scheme = nil
}
guard let newURL = _SwiftURL(stringOrEmpty: builder.string, relativeTo: _baseURL, encodingInvalidCharacters: true, encoding: _encoding, compatibility: true) else {
return absoluteString(original: true)
}
return newURL.absoluteString(original: true)
}
internal var compatibilityAbsoluteURL: URL? {
return _SwiftURL(stringOrEmpty: compatibilityAbsoluteString, encodingInvalidCharacters: true, encoding: _encoding, compatibility: true)?.url
}
// MARK: - Components
var scheme: String? {
guard let scheme = _parseInfo.scheme else { return baseURL?.scheme }
return String(scheme)
}
private static let fileSchemeUTF8 = Array("file".utf8)
var isFileURL: Bool {
guard let scheme else { return false }
return scheme.lowercased().utf8.elementsEqual(Self.fileSchemeUTF8)
}
var hasAuthority: Bool {
return _parseInfo.hasAuthority
}
internal var netLocation: String? {
guard hasAuthority else {
return baseURL?._swiftURL?.netLocation
}
guard let netLocation = _parseInfo.netLocation else {
return nil
}
return String(netLocation)
}
var user: String? {
return user(percentEncoded: false)
}
func user(percentEncoded: Bool) -> String? {
if !hasAuthority { return baseURL?.user(percentEncoded: percentEncoded) }
guard let user = _parseInfo.user else { return nil }
if percentEncoded {
return String(user)
} else if encodedComponents.contains(.user) {
// If we encoded it using UTF-8, decode it using UTF-8
return Parser.percentDecode(user)
} else {
// Otherwise, use the encoding we were given
return Parser.percentDecode(user, encoding: _encoding)
}
}
var password: String? {
return password(percentEncoded: true)
}
func password(percentEncoded: Bool) -> String? {
if !hasAuthority { return baseURL?.password(percentEncoded: percentEncoded) }
guard let password = _parseInfo.password else { return nil }
if percentEncoded {
return String(password)
} else if encodedComponents.contains(.password) {
return Parser.percentDecode(password)
} else {
return Parser.percentDecode(password, encoding: _encoding)
}
}
var host: String? {
return host(percentEncoded: false)
}
func host(percentEncoded: Bool) -> String? {
if !hasAuthority { return baseURL?.host(percentEncoded: percentEncoded) }
guard let encodedHost = _parseInfo.host.map(String.init) else { return nil }
// According to RFC 3986, a host always exists if there is an authority
// component, it just might be empty. However, the old implementation
// of URL.host() returned nil for URLs like "https:///", and apps rely
// on this behavior, so keep it for bincompat.
if encodedHost.isEmpty && _parseInfo.user == nil && _parseInfo.password == nil && _parseInfo.portRange == nil {
return nil
}
func requestedHost() -> String? {
if percentEncoded {
if !encodedComponents.contains(.host) || _parseInfo.didPercentEncodeHost {
return encodedHost
}
// Now we need to IDNA-decode, then percent-encode
guard let decoded = Parser.IDNADecodeHost(encodedHost) else {
return encodedHost
}
return Parser.percentEncode(decoded, component: .host)
} else if encodedComponents.contains(.host) {
if _parseInfo.didPercentEncodeHost {
return Parser.percentDecode(encodedHost)
}
// Return IDNA-encoded host, which is technically not percent-encoded
return encodedHost
} else {
return Parser.percentDecode(encodedHost, encoding: _encoding)
}
}
guard let requestedHost = requestedHost() else {
return nil
}
if _parseInfo.isIPLiteral {
// Strip square brackets to be compatible with old URL.host behavior
return String(requestedHost.utf8.dropFirst().dropLast())
} else {
return requestedHost
}
}
var port: Int? {
return hasAuthority ? _parseInfo.port : baseURL?.port
}
var relativePath: String {
return Self.fileSystemPath(for: relativePath(percentEncoded: true))
}
func relativePath(percentEncoded: Bool) -> String {
if percentEncoded {
return String(_parseInfo.path)
} else if encodedComponents.contains(.path) {
return Parser.percentDecode(_parseInfo.path) ?? ""
} else {
return Parser.percentDecode(_parseInfo.path, encoding: _encoding) ?? ""
}
}
func absolutePath(percentEncoded: Bool) -> String {
if baseURL != nil {
return absoluteURL?.relativePath(percentEncoded: percentEncoded) ?? relativePath(percentEncoded: percentEncoded)
}
if percentEncoded {
return String(_parseInfo.path)
} else if encodedComponents.contains(.path) {
return Parser.percentDecode(_parseInfo.path) ?? ""
} else {
return Parser.percentDecode(_parseInfo.path, encoding: _encoding) ?? ""
}
}
var path: String {
if isFileURL { return fileSystemPath() }
let path = absolutePath(percentEncoded: true)
if encodedComponents.contains(.path) {
return Parser.percentDecode(path)?._droppingTrailingSlashes ?? ""
} else {
return Parser.percentDecode(path, encoding: _encoding)?._droppingTrailingSlashes ?? ""
}
}
func path(percentEncoded: Bool) -> String {
return absolutePath(percentEncoded: percentEncoded)
}
var query: String? {
return query(percentEncoded: true)
}
func query(percentEncoded: Bool) -> String? {
let query = _parseInfo.query
if query == nil && !hasAuthority && _parseInfo.path.isEmpty {
return baseURL?.query(percentEncoded: percentEncoded)
}
guard let query else { return nil }
if percentEncoded {
return String(query)
} else if encodedComponents.contains(.query) {
return Parser.percentDecode(query)
} else {
return Parser.percentDecode(query, encoding: _encoding)
}
}
var fragment: String? {
return fragment(percentEncoded: true)
}
func fragment(percentEncoded: Bool) -> String? {
guard let fragment = _parseInfo.fragment else { return nil }
if percentEncoded {
return String(fragment)
} else if encodedComponents.contains(.fragment) {
return Parser.percentDecode(fragment)
} else {
return Parser.percentDecode(fragment, encoding: _encoding)
}
}
// MARK: - File Paths
private static func decodeFilePath(_ path: some StringProtocol) -> String {
// Don't decode "%2F" or "%00"
let charsToLeaveEncoded: Set<UInt8> = [._slash, 0]
return Parser.percentDecode(path, excluding: charsToLeaveEncoded) ?? ""
}
private static func windowsPath(for urlPath: String, slashDropper: (String) -> String) -> String {
var iter = urlPath.utf8.makeIterator()
guard iter.next() == ._slash else {
return decodeFilePath(slashDropper(urlPath))
}
// "C:\" is standardized to "/C:/" on initialization.
if let driveLetter = iter.next(), driveLetter.isAlpha,
iter.next() == ._colon,
iter.next() == ._slash {
// Strip trailing slashes from the path, which preserves a root "/".
let path = slashDropper(String(Substring(urlPath.utf8.dropFirst(3))))
// Don't include a leading slash before the drive letter
return "\(Unicode.Scalar(driveLetter)):\(decodeFilePath(path))"
}
// There are many flavors of UNC paths, so use PathIsRootW to ensure
// we don't strip a trailing slash that represents a root.
let path = decodeFilePath(urlPath)
#if os(Windows)
return path.replacing(._slash, with: ._backslash).withCString(encodedAs: UTF16.self) { pwszPath in
guard !PathIsRootW(pwszPath) else {
return path
}
return slashDropper(path)
}
#else
return slashDropper(path)
#endif
}
internal static func fileSystemPath(for urlPath: String, style: URL.PathStyle = URL.defaultPathStyle, compatibility: Bool = false) -> String {
let slashDropper: (String) -> String = if compatibility {
{ $0._droppingTrailingSlash }
} else {
{ $0._droppingTrailingSlashes }
}
switch style {
case .posix: return decodeFilePath(slashDropper(urlPath))
case .windows: return windowsPath(for: urlPath, slashDropper: slashDropper)
}
}
internal func fileSystemPath(style: URL.PathStyle = URL.defaultPathStyle, resolveAgainstBase: Bool = true, compatibility: Bool = false) -> String {
let urlPath = resolveAgainstBase ? absolutePath(percentEncoded: true) : relativePath(percentEncoded: true)
return Self.fileSystemPath(for: urlPath, style: style, compatibility: compatibility)
}
func withUnsafeFileSystemRepresentation<ResultType>(_ block: (UnsafePointer<Int8>?) throws -> ResultType) rethrows -> ResultType {
return try fileSystemPath().withFileSystemRepresentation { try block($0) }
}
var hasDirectoryPath: Bool {
let path = String(_parseInfo.path)
if path.utf8.last == ._slash {
return true
}
if path.isEmpty {
return _parseInfo.scheme == nil && !hasAuthority && baseURL?.hasDirectoryPath == true
}
return path.lastPathComponent == "." || path.lastPathComponent == ".."
}
var pathComponents: [String] {
var result = absolutePath(percentEncoded: true).pathComponents.map { Parser.percentDecode($0) ?? "" }
if result.count > 1 && result.last == "/" {
_ = result.popLast()
}
return result
}
var lastPathComponent: String {
let component = absolutePath(percentEncoded: true).lastPathComponent
if isFileURL {
return Self.fileSystemPath(for: component)
} else {
return Parser.percentDecode(component, encoding: _encoding) ?? ""
}
}
var pathExtension: String {
return path.pathExtension
}
func appendingPathComponent(_ pathComponent: String, isDirectory: Bool) -> URL? {
let directoryHint: URL.DirectoryHint = isDirectory ? .isDirectory : .notDirectory
return appending(path: pathComponent, directoryHint: directoryHint)
}
func appendingPathComponent(_ pathComponent: String) -> URL? {
return appending(path: pathComponent, directoryHint: .checkFileSystem)
}
func appending<S>(path: S, directoryHint: URL.DirectoryHint) -> URL? where S : StringProtocol {
return appending(path: path, directoryHint: directoryHint, encodingSlashes: false)
}
func appending<S>(component: S, directoryHint: URL.DirectoryHint) -> URL? where S : StringProtocol {
// The old .appending(component:) implementation did not actually percent-encode
// "/" for file URLs as the documentation suggests. Many apps accidentally use
// .appending(component: "path/with/slashes") instead of using .appending(path:),
// so changing this behavior would cause breakage.
if isFileURL {
return appending(path: component, directoryHint: directoryHint, encodingSlashes: false)
}
return appending(path: component, directoryHint: directoryHint, encodingSlashes: true)
}
internal func appending<S: StringProtocol>(path: S, directoryHint: URL.DirectoryHint, encodingSlashes: Bool, compatibility: Bool = false) -> URL? {
#if os(Windows)
var pathToAppend = path.replacing(._backslash, with: ._slash)
#else
var pathToAppend = String(path)
#endif
if !encodingSlashes && !compatibility {
pathToAppend = Parser.percentEncode(pathComponent: pathToAppend)
} else {
var toEncode = Set<UInt8>()
if encodingSlashes {
toEncode.insert(._slash)
}
if compatibility {
toEncode.insert(._semicolon)
}
pathToAppend = Parser.percentEncode(pathComponent: pathToAppend, including: toEncode)
}
func appendedPath() -> String {
var currentPath = relativePath(percentEncoded: true)
if currentPath.isEmpty && !hasAuthority {
guard _parseInfo.scheme == nil else {
// Scheme only, append directly to the empty path, e.g.
// URL("scheme:").appending(path: "path") == scheme:path
return pathToAppend
}
// No scheme or authority, treat the empty path as "."
currentPath = "."
}
// If currentPath is empty, pathToAppend is relative, and we have an authority,
// we must append a slash to separate the path from authority, which happens below.
if currentPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash {
currentPath += "/"
} else if currentPath.utf8.last == ._slash && pathToAppend.utf8.first == ._slash {
_ = currentPath.popLast()
}
return currentPath + pathToAppend
}
func mergedPath(for relativePath: String) -> String {
precondition(relativePath.utf8.first != UInt8(ascii: "/"))
guard let baseURL else {
return relativePath
}
let basePath = baseURL.relativePath(percentEncoded: true)
if baseURL.hasAuthority && basePath.isEmpty {
return "/" + relativePath
}
return basePath.merging(relativePath: relativePath)
}
var newPath = appendedPath()
let hasTrailingSlash = newPath.utf8.last == ._slash
let isDirectory: Bool
switch directoryHint {
case .isDirectory:
isDirectory = true
case .notDirectory:
isDirectory = false
case .checkFileSystem:
#if !NO_FILESYSTEM
// We can only check file system if the URL is a file URL
if isFileURL {
let filePath: String
if newPath.utf8.first == ._slash {
filePath = Self.fileSystemPath(for: newPath)
} else {
filePath = Self.fileSystemPath(for: mergedPath(for: newPath))
}
isDirectory = Self.isDirectory(filePath)
} else {
// For web addresses, trust the trailing slash
isDirectory = hasTrailingSlash
}
#else // !NO_FILESYSTEM
isDirectory = hasTrailingSlash
#endif // !NO_FILESYSTEM
case .inferFromPath:
isDirectory = hasTrailingSlash
}
if isDirectory && newPath.utf8.last != ._slash {
newPath += "/"
}
var components = URLComponents(parseInfo: _parseInfo)
components.percentEncodedPath = newPath
let string = components._uncheckedString(original: false)
return _SwiftURL(stringOrEmpty: string, relativeTo: baseURL)?.url
}
#if !NO_FILESYSTEM
private static func isDirectory(_ path: String) -> Bool {
guard !path.isEmpty else { return false }
#if os(Windows)
let path = path.replacing(._slash, with: ._backslash)
return (try? path.withNTPathRepresentation { pwszPath in
// If path points to a symlink (reparse point), get a handle to
// the symlink itself using FILE_FLAG_OPEN_REPARSE_POINT.
let handle = CreateFileW(pwszPath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nil)
guard handle != INVALID_HANDLE_VALUE else { return false }
defer { CloseHandle(handle) }
var info: BY_HANDLE_FILE_INFORMATION = BY_HANDLE_FILE_INFORMATION()
guard GetFileInformationByHandle(handle, &info) else { return false }
if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT { return false }
return (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY
}) ?? false
#else
// FileManager uses stat() to check if the file exists.
// URL historically won't follow a symlink at the end
// of the path, so use lstat() here instead.
return path.withFileSystemRepresentation { fsRep in
guard let fsRep else { return false }
var fileInfo = stat()
guard lstat(fsRep, &fileInfo) == 0 else { return false }
return (mode_t(fileInfo.st_mode) & S_IFMT) == S_IFDIR
}
#endif
}
private static func currentDirectoryOrNil() -> URL? {
let path: String? = FileManager.default.currentDirectoryPath
guard var filePath = path else {
return nil
}
#if os(Windows)
filePath = filePath.replacing(._backslash, with: ._slash)
#endif
guard URL.isAbsolute(standardizing: &filePath) else {
return nil
}
return URL(filePath: filePath, directoryHint: .isDirectory)
}
#endif
/// True if the URL's relative path would resolve against a base URL path
private var pathResolvesAgainstBase: Bool {
return _parseInfo.scheme == nil && !hasAuthority && _parseInfo.path.utf8.first != ._slash
}
func deletingLastPathComponent() -> URL? {
let path = relativePath(percentEncoded: true)
let shouldAppendDotDot = (
pathResolvesAgainstBase && (
path.isEmpty
|| path.lastPathComponent == "."
|| path.lastPathComponent == ".."
)
)
var newPath = path
if newPath.lastPathComponent != ".." {
newPath = newPath.deletingLastPathComponent()
}
if shouldAppendDotDot {
newPath = newPath.appendingPathComponent("..")
}
if newPath.isEmpty && pathResolvesAgainstBase {
newPath = "."
}
// .deletingLastPathComponent() removes the trailing "/", but we know it's a directory
if !newPath.isEmpty && newPath.utf8.last != ._slash {
newPath += "/"
}
var components = URLComponents(parseInfo: _parseInfo)
/// Compatibility path for apps that loop on:
/// `url = url.deletingPathComponent().standardized` until `url.path.isEmpty`.
///
/// This used to work due to a combination of bugs where:
/// `URL("/").deletingLastPathComponent == URL("/../")`
/// `URL("/../").standardized == URL("")`
#if FOUNDATION_FRAMEWORK
if URL.compatibility4 && path == "/" {
components.percentEncodedPath = "/../"
} else {
components.percentEncodedPath = newPath
}
#else
components.percentEncodedPath = newPath
#endif
let string = components._uncheckedString(original: false)
return _SwiftURL(stringOrEmpty: string, relativeTo: baseURL)?.url
}
internal func appendingPathExtension(_ pathExtension: String, compatibility: Bool) -> URL? {
guard !pathExtension.isEmpty, !_parseInfo.path.isEmpty else {
return nil
}
var components = URLComponents(parseInfo: _parseInfo)
// pathExtension might need to be percent-encoded
let encodedExtension = if compatibility {
Parser.percentEncode(pathComponent: pathExtension, including: [._semicolon])
} else {
Parser.percentEncode(pathComponent: pathExtension)
}
let newPath = components.percentEncodedPath.appendingPathExtension(encodedExtension)
components.percentEncodedPath = newPath
let string = components._uncheckedString(original: false)
return _SwiftURL(string: string, relativeTo: baseURL)?.url
}
func appendingPathExtension(_ pathExtension: String) -> URL? {
return appendingPathExtension(pathExtension, compatibility: false)
}
func deletingPathExtension() -> URL? {
guard !_parseInfo.path.isEmpty else { return nil }
var components = URLComponents(parseInfo: _parseInfo)
let newPath = components.percentEncodedPath.deletingPathExtension()
components.percentEncodedPath = newPath
let string = components._uncheckedString(original: false)
return _SwiftURL(stringOrEmpty: string, relativeTo: baseURL)?.url
}
var standardized: URL? {
/// Compatibility path for apps that loop on:
/// `url = url.deletingPathComponent().standardized` until `url.path.isEmpty`.
///
/// This used to work due to a combination of bugs where:
/// `URL("/").deletingLastPathComponent == URL("/../")`
/// `URL("/../").standardized == URL("")`
#if FOUNDATION_FRAMEWORK
guard isDecomposable else { return nil }
let newPath = if URL.compatibility4 && _parseInfo.path == "/../" {
""
} else {
String(_parseInfo.path).removingDotSegments
}
#else
let newPath = String(_parseInfo.path).removingDotSegments
#endif
var components = URLComponents(parseInfo: _parseInfo)
components.percentEncodedPath = newPath.removingDotSegments
if components.scheme != nil {
// Standardize scheme:// to scheme:///
if newPath.isEmpty && _parseInfo.netLocationRange?.isEmpty ?? false {
components.percentEncodedPath = "/"
}
// Standardize scheme:/path to scheme:///path
if components.encodedHost == nil {
components.encodedHost = ""
}
}
let string = components._uncheckedString(original: false)
return _SwiftURL(stringOrEmpty: string, relativeTo: baseURL)?.url
}
#if !NO_FILESYSTEM
var standardizedFileURL: URL? {
guard isFileURL, !fileSystemPath().isEmpty else { return nil }
return URL(filePath: fileSystemPath().standardizingPath, directoryHint: hasDirectoryPath ? .isDirectory : .notDirectory)
}
func resolvingSymlinksInPath() -> URL? {
guard isFileURL, !fileSystemPath().isEmpty else { return nil }
return URL(filePath: fileSystemPath().resolvingSymlinksInPath, directoryHint: hasDirectoryPath ? .isDirectory : .notDirectory)
}
#endif
private static let dataSchemeUTF8 = Array("data".utf8)
var description: String {
var urlString = relativeString
if let scheme, scheme.lowercased().utf8.elementsEqual(Self.dataSchemeUTF8), urlString.utf8.count > 128 {
let prefix = urlString.utf8.prefix(120)
let suffix = urlString.utf8.suffix(8)
urlString = "\(prefix) ... \(suffix)"
}
if let baseURL {
return "\(urlString) -- \(baseURL.description)"
}
return urlString
}
var debugDescription: String {
return description
}
#if FOUNDATION_FRAMEWORK
func bridgeToNSURL() -> NSURL {
if foundation_swift_nsurl_enabled() {
return _NSSwiftURL(url: self)
}
return _nsurl
}
internal func isFileReferenceURL() -> Bool {
#if NO_FILESYSTEM
return false
#else
return isFileURL && _parseInfo.pathHasFileID
#endif
}
internal func convertingFileReference() -> any _URLProtocol & AnyObject {
#if NO_FILESYSTEM
return self
#else
guard isFileReferenceURL() else { return self }
guard let url = bridgeToNSURL().filePathURL else {
return _SwiftURL(string: "com-apple-unresolvable-file-reference-url:")!
}
return url._url
#endif
}
#else
internal func convertingFileReference() -> _SwiftURL {
return self
}
#endif // FOUNDATION_FRAMEWORK
static func == (lhs: _SwiftURL, rhs: _SwiftURL) -> Bool {
return lhs.relativeString == rhs.relativeString && lhs.baseURL == rhs.baseURL
}
func hash(into hasher: inout Hasher) {
// Historically, the CF/NSURL hash only includes the relative string
hasher.combine(relativeString)
}
/// Convenience for constructing a URL string from components without validation.
private struct URLStringBuilder {
typealias Parser = _SwiftURL.Parser
var scheme: String?
var user: String?
var password: String?
var host: String?
var portString: String?
var path: String
var query: String?
var fragment: String?
var hasAuthority: Bool {
return user != nil || password != nil || host != nil || portString != nil
}
init(parseInfo: URLParseInfo, original: Bool) {
let encodedComponents = original ? parseInfo.encodedComponents : []
if let scheme = parseInfo.scheme {
self.scheme = String(scheme)
}
if let user = parseInfo.user{
self.user = encodedComponents.contains(.user) ? Parser.percentDecode(user) : String(user)
}
if let password = parseInfo.password {
self.password = encodedComponents.contains(.password) ? Parser.percentDecode(password) : String(password)
}
if let host = parseInfo.host {
// We don't need to check for IDNA-encoding since only CFURL uses
// the original string, and CFURL does not support INDA-encoding.
self.host = encodedComponents.contains(.host) ? Parser.percentDecode(host) : String(host)
}
if let portString = parseInfo.portString {
self.portString = String(portString)
}
self.path = encodedComponents.contains(.path) ? Parser.percentDecode(parseInfo.path) ?? "" : String(parseInfo.path)
if let query = parseInfo.query {
self.query = encodedComponents.contains(.query) ? Parser.percentDecode(query) : String(query)
}
if let fragment = parseInfo.fragment {
self.fragment = encodedComponents.contains(.fragment) ? Parser.percentDecode(fragment) : String(fragment)
}
}
var string: String {
var result = ""
if let scheme {
result += "\(scheme):"
}
if hasAuthority {
result += "//"
}
if let user {
result += user
}
if let password {
result += ":\(password)"
}
if user != nil || password != nil {
result += "@"
}
if let host {
result += host
}
if let portString {
result += ":\(portString)"
}
result += path
if let query {
result += "?\(query)"
}
if let fragment {
result += "#\(fragment)"
}
return result
}
var removingDotSegments: URLStringBuilder {
var result = self
result.path = result.path.removingDotSegments
return result
}
}
}
#if FOUNDATION_FRAMEWORK
internal import CoreFoundation_Private.CFURL
/// This conformance is only needed in `FOUNDATION_FRAMEWORK`,
/// where `URL` can be implemented by a few different classes.
extension _SwiftURL: _URLProtocol {}
extension _SwiftURL {
private static func _makeNSURL(from parseInfo: URLParseInfo, baseURL: URL?) -> NSURL {
return _makeCFURL(from: parseInfo, baseURL: baseURL as CFURL?) as NSURL
}
struct _CFURLFlags: OptionSet {
let rawValue: UInt32
// These must match the CFURL flags defined in CFURL.m
static let hasScheme = _CFURLFlags(rawValue: 0x00000001)
static let hasUser = _CFURLFlags(rawValue: 0x00000002)
static let hasPassword = _CFURLFlags(rawValue: 0x00000004)
static let hasHost = _CFURLFlags(rawValue: 0x00000008)
static let hasPort = _CFURLFlags(rawValue: 0x00000010)
static let hasPath = _CFURLFlags(rawValue: 0x00000020)
static let hasParameters = _CFURLFlags(rawValue: 0x00000040) // Unused
static let hasQuery = _CFURLFlags(rawValue: 0x00000080)
static let hasFragment = _CFURLFlags(rawValue: 0x00000100)
static let isIPLiteral = _CFURLFlags(rawValue: 0x00000400)
static let isDirectory = _CFURLFlags(rawValue: 0x00000800)
static let isCanonicalFileURL = _CFURLFlags(rawValue: 0x00001000) // Unused
static let pathHasFileID = _CFURLFlags(rawValue: 0x00002000)
static let isDecomposable = _CFURLFlags(rawValue: 0x00004000)
static let posixAndURLPathsMatch = _CFURLFlags(rawValue: 0x00008000)
static let originalAndURLStringsMatch = _CFURLFlags(rawValue: 0x00010000)
static let originatedFromSwift = _CFURLFlags(rawValue: 0x00020000)
}
private static func _makeCFURL(from parseInfo: URLParseInfo, baseURL: CFURL?) -> CFURL {
let string = parseInfo.urlString
var ranges = [CFRange]()
var flags: _CFURLFlags = [
.originalAndURLStringsMatch,
.originatedFromSwift,
]
// CFURL considers a URL decomposable if it does not have a scheme
// or if there is a slash directly following the scheme.
if parseInfo.scheme == nil || parseInfo.hasAuthority || parseInfo.path.utf8.first == ._slash {
flags.insert(.isDecomposable)
}
if let schemeRange = parseInfo.schemeRange {
flags.insert(.hasScheme)
let nsRange = string._toRelativeNSRange(schemeRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
if let userRange = parseInfo.userRange {
flags.insert(.hasUser)
let nsRange = string._toRelativeNSRange(userRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
if let passwordRange = parseInfo.passwordRange {
flags.insert(.hasPassword)
let nsRange = string._toRelativeNSRange(passwordRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
if parseInfo.portRange != nil {
flags.insert(.hasPort)
}
// CFURL considers an empty host nil unless there's another authority component
if let hostRange = parseInfo.hostRange,
(!hostRange.isEmpty || !flags.isDisjoint(with: [.hasUser, .hasPassword, .hasPort])) {
flags.insert(.hasHost)
let nsRange = string._toRelativeNSRange(hostRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
if let portRange = parseInfo.portRange {
let nsRange = string._toRelativeNSRange(portRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
flags.insert(.hasPath)
if let pathRange = parseInfo.pathRange {
let nsRange = string._toRelativeNSRange(pathRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
} else {
ranges.append(CFRange(location: kCFNotFound, length: 0))
}
if let queryRange = parseInfo.queryRange {
flags.insert(.hasQuery)
let nsRange = string._toRelativeNSRange(queryRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
if let fragmentRange = parseInfo.fragmentRange {
flags.insert(.hasFragment)
let nsRange = string._toRelativeNSRange(fragmentRange)
ranges.append(CFRange(location: nsRange.location, length: nsRange.length))
}
let path = parseInfo.path.utf8
let isDirectory = path.last == UInt8(ascii: "/")
if parseInfo.isIPLiteral {
flags.insert(.isIPLiteral)
}
if isDirectory {
flags.insert(.isDirectory)
}
if parseInfo.pathHasFileID {
flags.insert(.pathHasFileID)
}
if !isDirectory && !parseInfo.path.utf8.contains(UInt8(ascii: "%")) {
flags.insert(.posixAndURLPathsMatch)
}
return ranges.withUnsafeBufferPointer {
_CFURLCreateWithRangesAndFlags(string as CFString, $0.baseAddress!, UInt8($0.count), flags.rawValue, baseURL)
}
}
}
#endif