diff --git a/Sources/FoundationEssentials/URL/URL_Parsing.swift b/Sources/FoundationEssentials/URL/URL_Parsing.swift index 5ba8f7039..d5d8033e1 100644 --- a/Sources/FoundationEssentials/URL/URL_Parsing.swift +++ b/Sources/FoundationEssentials/URL/URL_Parsing.swift @@ -567,63 +567,57 @@ extension URL { shouldEncode = true } - if flags.contains(.hasHost) { - let hostRange = impl.pointee.hostRange + @inline(__always) + func checkHost() -> Bool { + guard flags.contains(.hasHost) else { return true } + let host = span.extracting(impl.pointee.hostRange) if flags.contains(.isIPLiteral) { // Ignore the leading and trailing brackets - var i = hostRange.startIndex + 1 - let endBracketIndex = hostRange.endIndex - 1 - while i < endBracketIndex && URLComponentAllowedSet.hostIPvFuture.contains(span[i]) { - i += 1 + assert(host.count >= 2) + let innerHost = host.extracting(1..<(host.count - 1)) + guard let isValid = validateIPLiteral( + innerHost: innerHost, + useModernParsing: useModernParsing + ) else { + return false } - if i < endBracketIndex { - // We found a character that's not allowed in .hostIPvFuture - // Only a zone ID (starting at "%") can be percent-encoded - guard span[i] == UInt8(ascii: "%") else { - // The IP portion contained an invalid character that was - // not the start of a zone ID, so return false. - return false - } - // "%25" is the correctly-encoded zone ID delimiter for a URL - let isValidZoneID = ( - i + 2 < endBracketIndex - && span[i + 1] == UInt8(ascii: "2") - && span[i + 2] == UInt8(ascii: "5") - && validate( - span: span.extracting((i + 3).. 9 { + if (port[i] &- 0x30) > 9 { isValid = false break } @@ -637,7 +631,7 @@ extension URL { return false } } - } else if !validate(span: span.extracting(impl.pointee.portRange), component: .anyValid) { + } else if !validate(span: port, component: .anyValid) { // Allow any valid URL character in the port for compatibility guard allowEncoding else { return false } shouldEncode = true @@ -645,16 +639,15 @@ extension URL { } // Path always exists - if !validate(span: span.extracting(impl.pointee.pathRange), component: .path) { + let path = span.extracting(impl.pointee.pathRange) + if !validate(span: path, component: .path) { guard allowEncoding else { return false } - if flags.isDisjoint(with: [.hasScheme, .hasHost]) { + if useModernParsing && flags.isDisjoint(with: [.hasScheme, .hasHost]) { // A relative-ref must not contain a ":" before the first "/" - let path = span.extracting(impl.pointee.pathRange) for i in path.indices { - let v = path[i] - if v == UInt8(ascii: "/") { + if path[i] == UInt8(ascii: "/") { break - } else if v == UInt8(ascii: ":") { + } else if path[i] == UInt8(ascii: ":") { return false } } @@ -1089,11 +1082,18 @@ extension URL { let newHostStart = hostRange.startIndex + extraBytesAdded if flags.contains(.shouldEncodeHost) { if flags.contains(.isIPLiteral) { - // We can only be encoding a zone ID, find the start - while copyEnd < hostRange.endIndex - 1 && span[copyEnd] != UInt8(ascii: "%") { - copyEnd += 1 + copyEnd += 1 // Include leading "[" + let endBracketIndex = hostRange.endIndex - 1 + if updateRanges { + // We can only be encoding a zone ID, find the start + while copyEnd < endBracketIndex && span[copyEnd] != UInt8(ascii: "%") { + copyEnd += 1 + } + guard encodeZoneID(range: copyEnd..( return false } +// Validates the IP literal host portion inside the "[" and "]" +// A return value of false can be encoded, nil means reject entirely +@inline(__always) +internal func validateIPLiteral( + innerHost: borrowing Span, + useModernParsing: Bool +) -> Bool? { + guard useModernParsing else { + // For CFURL, allow arbitrary percent-encoding + return validate(span: innerHost, component: .hostIPvFuture) + } + var i = 0 + while i < innerHost.count && URLComponentAllowedSet.hostIPvFuture.contains(innerHost[i]) { + i += 1 + } + if i == innerHost.count { + return true + } + // We found a character that's not allowed in .hostIPvFuture + // Only a zone ID (starting at "%") can be percent-encoded + guard innerHost[i] == UInt8(ascii: "%") else { + // The IP portion contained an invalid character that was + // not the start of a zone ID, so return nil. + return nil + } + // "%25" is the correctly-encoded zone ID delimiter for a URL + let isValidZoneID = ( + i + 2 < innerHost.count + && innerHost[i + 1] == UInt8(ascii: "2") + && innerHost[i + 2] == UInt8(ascii: "5") + && validate( + span: innerHost.extracting((i + 3)...), + component: .hostZoneID + ) + ) + return isValidZoneID +} + // Don't allow percent-escape sequences when validating certain paths @inline(__always) internal func strictValidate(path: borrowing Span) -> Bool {