mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-23 14:00:14 +08:00
* (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
1221 lines
48 KiB
Swift
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
|