@@ -224,6 +224,8 @@ extension StructuredFieldValueParser {
224224 return try self . _parseAToken ( )
225225 case asciiAt:
226226 return try self . _parseADate ( )
227+ case asciiPercent:
228+ return try self . _parseADisplayString ( )
227229 default :
228230 throw StructuredHeaderError . invalidItem
229231 }
@@ -491,6 +493,84 @@ extension StructuredFieldValueParser {
491493 return try self . _parseAnIntegerOrDecimal ( isDate: true )
492494 }
493495
496+ private mutating func _parseADisplayString( ) throws -> RFC9651 BareItem {
497+ assert ( self . underlyingData. first == asciiPercent)
498+ self . underlyingData. consumeFirst ( )
499+
500+ guard self . underlyingData. first == asciiDquote else {
501+ throw StructuredHeaderError . invalidDisplayString
502+ }
503+
504+ self . underlyingData. consumeFirst ( )
505+
506+ var byteArray = [ UInt8] ( )
507+
508+ while let char = self . underlyingData. first {
509+ self . underlyingData. consumeFirst ( )
510+
511+ switch char {
512+ case 0x00 ... 0x1F , 0x7F ... :
513+ throw StructuredHeaderError . invalidDisplayString
514+ case asciiPercent:
515+ if self . underlyingData. count < 2 {
516+ throw StructuredHeaderError . invalidDisplayString
517+ }
518+
519+ let octetHex = EncodedHex ( self . underlyingData. prefix ( 2 ) )
520+
521+ self . underlyingData = self . underlyingData. dropFirst ( 2 )
522+
523+ guard let octet = octetHex. decode ( ) else {
524+ throw StructuredHeaderError . invalidDisplayString
525+ }
526+
527+ byteArray. append ( octet)
528+ case asciiDquote:
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)
532+
533+ guard let unicodeSequence else {
534+ throw StructuredHeaderError . invalidDisplayString
535+ }
536+
537+ return . displayString( unicodeSequence)
538+ } else {
539+ return try _decodeDisplayString ( byteArray: & byteArray)
540+ }
541+ #else
542+ return try _decodeDisplayString ( byteArray: & byteArray)
543+ #endif
544+ default :
545+ byteArray. append ( char)
546+ }
547+ }
548+
549+ // Fail parsing — reached the end of the string without finding a closing DQUOTE.
550+ throw StructuredHeaderError . invalidDisplayString
551+ }
552+
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 -> RFC9651 BareItem {
557+ // String(validatingUTF8:) requires byteArray to be null-terminated.
558+ byteArray. append ( 0 )
559+
560+ let unicodeSequence = byteArray. withUnsafeBytes {
561+ $0. withMemoryRebound ( to: CChar . self) {
562+ // This force-unwrap is safe, as the buffer must successfully bind to CChar.
563+ String ( validatingUTF8: $0. baseAddress!)
564+ }
565+ }
566+
567+ guard let unicodeSequence else {
568+ throw StructuredHeaderError . invalidDisplayString
569+ }
570+
571+ return . displayString( unicodeSequence)
572+ }
573+
494574 private mutating func _parseParameters( ) throws -> OrderedMap< Key, RFC9651 BareItem> {
495575 var parameters = OrderedMap < Key , RFC9651BareItem > ( )
496576
@@ -643,3 +723,39 @@ extension StrippingStringEscapesCollection.Index: Comparable {
643723 lhs. _baseIndex < rhs. _baseIndex
644724 }
645725}
726+
727+ /// `EncodedHex` represents a (possibly invalid) hex value in UTF8.
728+ struct EncodedHex {
729+ private( set) var firstChar : UInt8
730+ private( set) var secondChar : UInt8
731+
732+ init < Bytes: RandomAccessCollection > ( _ bytes: Bytes ) where Bytes. Element == UInt8 {
733+ precondition ( bytes. count == 2 )
734+ self . firstChar = bytes [ bytes. startIndex]
735+ self . secondChar = bytes [ bytes. index ( after: bytes. startIndex) ]
736+ }
737+
738+ /// Validates and converts `EncodedHex` to a base 10 UInt8.
739+ ///
740+ /// If `EncodedHex` does not represent a valid hex value, the result of this method is nil.
741+ fileprivate func decode( ) -> UInt8 ? {
742+ guard
743+ let firstCharAsInteger = self . htoi ( self . firstChar) ,
744+ let secondCharAsInteger = self . htoi ( self . secondChar)
745+ else { return nil }
746+
747+ return ( firstCharAsInteger << 4 ) + secondCharAsInteger
748+ }
749+
750+ /// Converts a hex character given in UTF8 to its integer value.
751+ private func htoi( _ asciiChar: UInt8 ) -> UInt8 ? {
752+ switch asciiChar {
753+ case asciiZero... asciiNine:
754+ return asciiChar - asciiZero
755+ case asciiLowerA... asciiLowerF:
756+ return asciiChar - asciiLowerA + 10
757+ default :
758+ return nil
759+ }
760+ }
761+ }
0 commit comments