Skip to content

Commit

Permalink
Percent encode, URLEncodedForm using FoundationEssentials (#639)
Browse files Browse the repository at this point in the history
* Add percentEncode/Decode from Foundation Essentials

* Use percent encoding in URLEncodedForms

* Use new percent encode/decode functions

* Use Swift 6.0 ISO8601 format parser/style if available

* Add NOTICE.txt

* Remove URLEncoded DateDecodingStrategy.formatted

* import FoundationEssentials in URLEncodedForm code

* Copyright header

* Remove breaking change
  • Loading branch information
adam-fowler authored Jan 13, 2025
1 parent a4cc469 commit f7a769b
Show file tree
Hide file tree
Showing 12 changed files with 555 additions and 73 deletions.
30 changes: 30 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
The Hummingbird Project
====================

Please visit the Hummingbird web site for more information:

* https://hummingbird.codes

Copyright 2024 The Hummingbird Project

The Hummingbird Project licenses this file to you under the Apache License,
version 2.0 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at:

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.

-------------------------------------------------------------------------------

This product contains code from swift-foundation.

* LICENSE (MIT):
* https://github.com/swiftlang/swift-foundation/blob/main/LICENSE.md
* HOMEPAGE:
* https://github.com/swiftlang/swift-foundation/

11 changes: 6 additions & 5 deletions Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
//
//===----------------------------------------------------------------------===//

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

internal enum URLEncodedForm {
/// CodingKey used by URLEncodedFormEncoder and URLEncodedFormDecoder
Expand Down Expand Up @@ -43,15 +47,12 @@ internal enum URLEncodedForm {
fileprivate static let `super` = Key(stringValue: "super")!
}

/// ASCII characters that will not be percent encoded in URL encoded form data
static let unreservedCharacters = CharacterSet(
charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
)

#if compiler(<6.0)
/// ISO8601 data formatter used throughout URL encoded form code
static var iso8601Formatter: ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -634,15 +634,17 @@ extension _URLEncodedFormDecoder {
let seconds = try unbox(node, as: Double.self)
return Date(timeIntervalSince1970: seconds)
case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
let dateString = try unbox(node, as: String.self)
guard let date = URLEncodedForm.iso8601Formatter.date(from: dateString) else {
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format"))
}
return date
} else {
preconditionFailure("ISO8601DateFormatter is unavailable on this platform")
let dateString = try unbox(node, as: String.self)
#if compiler(>=6.0)
guard let date = try? Date(dateString, strategy: .iso8601) else {
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format"))
}
#else
guard let date = URLEncodedForm.iso8601Formatter.date(from: dateString) else {
throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format"))
}
#endif
return date
case .formatted(let formatter):
let dateString = try unbox(node, as: String.self)
guard let date = formatter.date(from: dateString) else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -330,11 +330,11 @@ extension _URLEncodedFormEncoder {
case .secondsSince1970:
try self.encode(Double(date.timeIntervalSince1970).description)
case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
try encode(URLEncodedForm.iso8601Formatter.string(from: date))
} else {
preconditionFailure("ISO8601DateFormatter is unavailable on this platform")
}
#if compiler(>=6.0)
try self.encode(date.formatted(.iso8601))
#else
try self.encode(URLEncodedForm.iso8601Formatter.string(from: date))
#endif
case .formatted(let formatter):
try self.encode(formatter.string(from: date))
case .custom(let closure):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Internal representation of URL encoded form data used by both encode and decode
enum URLEncodedFormNode: CustomStringConvertible, Equatable {
/// holds a value
Expand Down Expand Up @@ -30,7 +44,7 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
let node = Self.map(.init())
for element in split {
if let equals = element.firstIndex(of: "=") {
let before = element[..<equals].removingPercentEncoding
let before = element[..<equals].removingURLPercentEncoding()
let afterEquals = element.index(after: equals)
let after = element[afterEquals...].replacingOccurrences(of: "+", with: " ")
guard let key = before else { throw Error.failedToDecode("Failed to percent decode \(element)") }
Expand Down Expand Up @@ -128,12 +142,12 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable {
}

init?(percentEncoded value: String) {
guard let value = value.removingPercentEncoding else { return nil }
guard let value = value.removingURLPercentEncoding() else { return nil }
self.value = value
}

var percentEncoded: String {
self.value.addingPercentEncoding(withAllowedCharacters: URLEncodedForm.unreservedCharacters) ?? self.value
self.value.addingPercentEncoding(forURLComponent: .queryItem)
}

static func == (lhs: URLEncodedFormNode.NodeValue, rhs: URLEncodedFormNode.NodeValue) -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Hummingbird/Middleware/FileMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ where Provider.FileAttributes: FileMiddlewareFileAttributes {
}

// Remove percent encoding from URI path
guard var path = request.uri.path.removingPercentEncoding else {
guard var path = request.uri.path.removingURLPercentEncoding() else {
throw HTTPError(.badRequest, message: "Invalid percent encoding in URL")
}

Expand Down
44 changes: 1 addition & 43 deletions Sources/HummingbirdCore/Utils/HBParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -602,49 +602,7 @@ extension Parser {

/// percent decode UTF8
public func percentDecode() -> String? {
struct DecodeError: Swift.Error {}
func _percentDecode(_ original: ArraySlice<UInt8>, _ bytes: UnsafeMutableBufferPointer<UInt8>) throws -> Int {
var newIndex = 0
var index = original.startIndex
while index < (original.endIndex - 2) {
// if we have found a percent sign
if original[index] == 0x25 {
let high = Self.asciiHexValues[Int(original[index + 1])]
let low = Self.asciiHexValues[Int(original[index + 2])]
index += 3
if ((high | low) & 0x80) != 0 {
throw DecodeError()
}
bytes[newIndex] = (high << 4) | low
newIndex += 1
} else {
bytes[newIndex] = original[index]
newIndex += 1
index += 1
}
}
while index < original.endIndex {
bytes[newIndex] = original[index]
newIndex += 1
index += 1
}
return newIndex
}
guard self.index != self.range.endIndex else { return "" }
do {
if #available(macOS 11, macCatalyst 14.0, iOS 14.0, tvOS 14.0, *) {
return try String(unsafeUninitializedCapacity: range.endIndex - index) { bytes -> Int in
try _percentDecode(self.buffer[self.index..<range.endIndex], bytes)
}
} else {
let newBuffer = try [UInt8](unsafeUninitializedCapacity: self.range.endIndex - self.index) { bytes, count in
try count = _percentDecode(self.buffer[self.index..<self.range.endIndex], bytes)
}
return self.makeString(newBuffer)
}
} catch {
return nil
}
String.removingURLPercentEncoding(utf8Buffer: self.buffer[self.index..<self.range.endIndex])
}
}

Expand Down
Loading

0 comments on commit f7a769b

Please sign in to comment.