Skip to content

Commit 4f8243f

Browse files
committed
Implement feedback
1 parent 1094d86 commit 4f8243f

File tree

3 files changed

+84
-40
lines changed

3 files changed

+84
-40
lines changed

Sources/RawStructuredFieldValues/ASCII.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ let asciiSlash = UInt8(ascii: "/")
4545
let asciiPeriod = UInt8(ascii: ".")
4646
let asciiComma = UInt8(ascii: ",")
4747
let asciiCapitalA = UInt8(ascii: "A")
48+
let asciiCapitalF = UInt8(ascii: "F")
4849
let asciiCapitalZ = UInt8(ascii: "Z")
4950
let asciiLowerA = UInt8(ascii: "a")
51+
let asciiLowerF = UInt8(ascii: "f")
5052
let asciiLowerZ = UInt8(ascii: "z")
5153
let asciiCapitals = asciiCapitalA...asciiCapitalZ
5254
let asciiLowercases = asciiLowerA...asciiLowerZ
55+
let asciiHexCapitals = asciiCapitalA...asciiCapitalF
56+
let asciiHexLowercases = asciiLowerA...asciiLowerF

Sources/RawStructuredFieldValues/FieldParser.swift

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -516,36 +516,31 @@ extension StructuredFieldValueParser {
516516
throw StructuredHeaderError.invalidDisplayString
517517
}
518518

519-
let startIndex = self.underlyingData.startIndex
520-
let secondIndex = self.underlyingData.index(after: startIndex)
521-
let octetHex = self.underlyingData[...secondIndex]
519+
let octetHex = EncodedHex(ArraySlice(self.underlyingData.prefix(2)))
522520

523521
self.underlyingData = self.underlyingData.dropFirst(2)
524522

525-
guard
526-
octetHex.allSatisfy({ asciiDigits.contains($0) || asciiLowercases.contains($0) }),
527-
let octet = UInt8.decodeHex(octetHex)
528-
else {
523+
guard let octet = octetHex.decode() else {
529524
throw StructuredHeaderError.invalidDisplayString
530525
}
531526

532527
byteArray.append(octet)
533528
case asciiDquote:
534-
let unicodeSequence = try byteArray.withUnsafeBytes {
535-
try $0.withMemoryRebound(to: CChar.self) {
536-
guard let baseAddress = $0.baseAddress else {
537-
throw StructuredHeaderError.invalidDisplayString
538-
}
529+
#if compiler(>=6.0)
530+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) {
531+
let unicodeSequence = String(validating: byteArray, as: UTF8.self)
539532

540-
return String(validatingUTF8: baseAddress)
533+
guard let unicodeSequence else {
534+
throw StructuredHeaderError.invalidDisplayString
541535
}
542-
}
543536

544-
guard let unicodeSequence else {
545-
throw StructuredHeaderError.invalidDisplayString
537+
return .displayString(unicodeSequence)
538+
} else {
539+
return try _decodeDisplayString(byteArray: &byteArray)
546540
}
547-
548-
return .displayString(unicodeSequence)
541+
#else
542+
return try _decodeDisplayString(byteArray: &byteArray)
543+
#endif
549544
default:
550545
byteArray.append(char)
551546
}
@@ -555,6 +550,30 @@ extension StructuredFieldValueParser {
555550
throw StructuredHeaderError.invalidDisplayString
556551
}
557552

553+
/// This method is called in environments where `String(validating:as:)` is unavailable. It uses
554+
/// `String(validatingUTF8:)` which requires `byteArray` to be null terminated. `String(validating:as:)`
555+
/// does not require that requirement. Therefore, it does not perform null checks, which makes it more optimal.
556+
private func _decodeDisplayString(byteArray: inout [UInt8]) throws -> RFC9651BareItem {
557+
// String(validatingUTF8:) requires byteArray to be null-terminated.
558+
byteArray.append(0)
559+
560+
let unicodeSequence = try byteArray.withUnsafeBytes {
561+
try $0.withMemoryRebound(to: CChar.self) {
562+
guard let baseAddress = $0.baseAddress else {
563+
throw StructuredHeaderError.invalidDisplayString
564+
}
565+
566+
return String(validatingUTF8: baseAddress)
567+
}
568+
}
569+
570+
guard let unicodeSequence else {
571+
throw StructuredHeaderError.invalidDisplayString
572+
}
573+
574+
return .displayString(unicodeSequence)
575+
}
576+
558577
private mutating func _parseParameters() throws -> OrderedMap<Key, RFC9651BareItem> {
559578
var parameters = OrderedMap<Key, RFC9651BareItem>()
560579

@@ -708,33 +727,36 @@ extension StrippingStringEscapesCollection.Index: Comparable {
708727
}
709728
}
710729

711-
extension UInt8 {
712-
/// Converts a hex value given in UTF8 to base 10.
713-
fileprivate static func decodeHex<Bytes: RandomAccessCollection>(_ bytes: Bytes) -> Self?
714-
where Bytes.Element == Self {
715-
var result = Self(0)
716-
var power = Self(bytes.count)
730+
/// `EncodedHex` represents a (possibly invalid) hex value in UTF8.
731+
struct EncodedHex {
732+
private(set) var firstChar: UInt8
733+
private(set) var secondChar: UInt8
717734

718-
for byte in bytes {
719-
power -= 1
735+
init(_ slice: ArraySlice<UInt8>) {
736+
precondition(slice.count == 2)
737+
self.firstChar = slice[slice.startIndex]
738+
self.secondChar = slice[slice.index(after: slice.startIndex)]
739+
}
720740

721-
guard let integer = Self.htoi(byte) else { return nil }
722-
result += integer << (power * 4)
723-
}
741+
/// Validates and converts `EncodedHex` to a base 10 UInt8.
742+
///
743+
/// If `EncodedHex` does not represent a valid hex value, the result of this method is nil.
744+
fileprivate func decode() -> UInt8? {
745+
guard
746+
let firstCharAsInteger = self.htoi(self.firstChar),
747+
let secondCharAsInteger = self.htoi(self.secondChar)
748+
else { return nil }
724749

725-
return result
750+
return (firstCharAsInteger << 4) + secondCharAsInteger
726751
}
727752

728753
/// Converts a hex character given in UTF8 to its integer value.
729-
private static func htoi(_ value: Self) -> Self? {
730-
let charA = Self(UnicodeScalar("a").value)
731-
let char0 = Self(UnicodeScalar("0").value)
732-
733-
switch value {
734-
case char0...char0 + 9:
735-
return value - char0
736-
case charA...charA + 5:
737-
return value - charA + 10
754+
private func htoi(_ asciiChar: UInt8) -> UInt8? {
755+
switch asciiChar {
756+
case asciiZero...asciiNine:
757+
return asciiChar - asciiZero
758+
case asciiLowerA...asciiLowerF:
759+
return asciiChar - asciiLowerA + 10
738760
default:
739761
return nil
740762
}

Sources/RawStructuredFieldValues/FieldSerializer.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,10 @@ extension StructuredFieldValueSerializer {
226226
|| (0x7F...).contains(byte)
227227
{
228228
self.data.append(asciiPercent)
229-
self.data.append(contentsOf: String(byte, radix: 16, uppercase: false).utf8)
229+
230+
let encodedByte = UInt8.encodeToHex(byte)
231+
self.data.append(encodedByte.firstChar)
232+
self.data.append(encodedByte.secondChar)
230233
} else {
231234
self.data.append(byte)
232235
}
@@ -265,3 +268,18 @@ extension String {
265268
}
266269
}
267270
}
271+
272+
extension UInt8 {
273+
/// Converts an integer in base 10 to hex of type `EncodedHex`.
274+
fileprivate static func encodeToHex(_ int: Self) -> EncodedHex {
275+
let firstChar = self.itoh(int >> 4)
276+
let secondChar = self.itoh(int & 0x0F)
277+
278+
return EncodedHex([firstChar, secondChar])
279+
}
280+
281+
/// Converts an integer to its hex character in UTF8.
282+
private static func itoh(_ int: Self) -> Self {
283+
return (int > 9) ? (asciiLowerA + int - 10) : (asciiZero + int)
284+
}
285+
}

0 commit comments

Comments
 (0)