@@ -627,6 +627,36 @@ extension KeyValueStringSchema {
627627 }
628628}
629629
630+ internal enum KeyValueStringSchemaVerificationFailure : Error , CustomStringConvertible {
631+
632+ case preferredKeyValueDelimiterIsInvalid
633+ case preferredKeyValueDelimiterNotRecognized
634+ case preferredPairDelimiterIsInvalid
635+ case preferredPairDelimiterNotRecognized
636+ case invalidKeyValueDelimiterIsRecognized
637+ case invalidPairDelimiterIsRecognized
638+ case inconsistentSpaceEncoding
639+
640+ public var description : String {
641+ switch self {
642+ case . preferredKeyValueDelimiterIsInvalid:
643+ return " Schema's preferred key-value delimiter is invalid "
644+ case . preferredKeyValueDelimiterNotRecognized:
645+ return " Schema does not recognize its preferred key-value delimiter as a key-value delimiter "
646+ case . preferredPairDelimiterIsInvalid:
647+ return " Schema's preferred pair delimiter is invalid "
648+ case . preferredPairDelimiterNotRecognized:
649+ return " Schema does not recognize its preferred pair delimiter as a pair delimiter "
650+ case . invalidKeyValueDelimiterIsRecognized:
651+ return " isKeyValueDelimiter recognizes an invalid delimiter "
652+ case . invalidPairDelimiterIsRecognized:
653+ return " isPairDelimiter recognizes an invalid delimiter "
654+ case . inconsistentSpaceEncoding:
655+ return " encodeSpaceAsPlus is true, so decodePlusAsSpace must also be true "
656+ }
657+ }
658+ }
659+
630660extension KeyValueStringSchema {
631661
632662 /// Checks this schema for consistency.
@@ -658,19 +688,47 @@ extension KeyValueStringSchema {
658688 /// > For those that do, it is recommended to run this verification
659689 /// > as part of your regular unit tests.
660690 ///
661- public func verify( for component: KeyValuePairsSupportedComponent ) {
691+ public func verify( for component: KeyValuePairsSupportedComponent ) throws {
692+
693+ // Preferred delimiters must not require escaping.
662694
663- let delimiters = verifyDelimitersDoNotNeedEscaping ( in: component)
695+ let preferredDelimiters = verifyDelimitersDoNotNeedEscaping ( in: component)
664696
665- if !isKeyValueDelimiter ( delimiters . keyValue) {
666- fatalError ( " Inconsistent schema: preferred key-value delimiter is not recognized as a key-value delimiter " )
697+ if preferredDelimiters . keyValue == . max {
698+ throw KeyValueStringSchemaVerificationFailure . preferredKeyValueDelimiterIsInvalid
667699 }
668- if !isPairDelimiter ( delimiters . pair) {
669- fatalError ( " Inconsistent schema: preferred pair delimiter is not recognized as a pair delimiter " )
700+ if preferredDelimiters . pair == . max {
701+ throw KeyValueStringSchemaVerificationFailure . preferredPairDelimiterIsInvalid
670702 }
703+
704+ // isKeyValueDelimiter/isPairDelimiter must recognize preferred delimiters,
705+ // and must not recognize other reserved characters (e.g. %, ASCII hex digits, +).
706+
707+ if !isKeyValueDelimiter( preferredDelimiters. keyValue) {
708+ throw KeyValueStringSchemaVerificationFailure . preferredKeyValueDelimiterNotRecognized
709+ }
710+ if !isPairDelimiter( preferredDelimiters. pair) {
711+ throw KeyValueStringSchemaVerificationFailure . preferredPairDelimiterNotRecognized
712+ }
713+
714+ func delimiterPredicateIsInvalid( _ isDelimiter: ( UInt8 ) -> Bool ) -> Bool {
715+ " 0123456789abcdefABCDEF%+ " . utf8. contains ( where: isDelimiter)
716+ }
717+
718+ if delimiterPredicateIsInvalid ( isKeyValueDelimiter) {
719+ throw KeyValueStringSchemaVerificationFailure . invalidKeyValueDelimiterIsRecognized
720+ }
721+ if delimiterPredicateIsInvalid ( isPairDelimiter) {
722+ throw KeyValueStringSchemaVerificationFailure . invalidPairDelimiterIsRecognized
723+ }
724+
725+ // Space encoding must be consistent.
726+
671727 if encodeSpaceAsPlus, !decodePlusAsSpace {
672- fatalError ( " Inconsistent schema: encodeSpaceAsPlus is true, so decodePlusAsSpace must also be true " )
728+ throw KeyValueStringSchemaVerificationFailure . inconsistentSpaceEncoding
673729 }
730+
731+ // All checks passed.
674732 }
675733}
676734
@@ -1064,7 +1122,7 @@ extension WebURL.KeyValuePairs: CustomStringConvertible {
10641122
10651123
10661124// --------------------------------------------
1067- // MARK: - Reading: Collection
1125+ // MARK: - Reading: By Location.
10681126// --------------------------------------------
10691127
10701128
@@ -1218,7 +1276,7 @@ extension WebURL.KeyValuePairs: Collection {
12181276}
12191277
12201278
1221- // MARK: - TODO: BidirectionalCollection.
1279+ // MARK: TODO: BidirectionalCollection.
12221280
12231281
12241282extension WebURL . KeyValuePairs {
@@ -2522,41 +2580,40 @@ extension KeyValueStringSchema {
25222580 // - Must not be the percent sign (`%`), plus sign (`+`), space, or a hex digit, and
25232581 // - Must not require escaping in the URL component(s) used with this schema.
25242582
2525- precondition (
2526- ASCII ( keyValueDelimiter) ? . isHexDigit == false
2527- && keyValueDelimiter != ASCII . percentSign. codePoint
2528- && keyValueDelimiter != ASCII . plus. codePoint,
2529- " Schema's preferred key-value delimiter is invalid "
2530- )
2531- precondition (
2532- ASCII ( pairDelimiter) ? . isHexDigit == false
2533- && pairDelimiter != ASCII . percentSign. codePoint
2534- && pairDelimiter != ASCII . plus. codePoint,
2535- " Schema's preferred pair delimiter is invalid "
2536- )
2583+ guard
2584+ ASCII ( keyValueDelimiter) ? . isHexDigit == false ,
2585+ keyValueDelimiter != ASCII . percentSign. codePoint,
2586+ keyValueDelimiter != ASCII . plus. codePoint
2587+ else {
2588+ return ( . max, 0 )
2589+ }
2590+ guard
2591+ ASCII ( pairDelimiter) ? . isHexDigit == false ,
2592+ pairDelimiter != ASCII . percentSign. codePoint,
2593+ pairDelimiter != ASCII . plus. codePoint
2594+ else {
2595+ return ( 0 , . max)
2596+ }
25372597
25382598 switch component. value {
25392599 case . query:
25402600 let encodeSet = URLEncodeSet . SpecialQuery ( )
2541- precondition (
2542- !encodeSet. shouldPercentEncode ( ascii: keyValueDelimiter) ,
2543- " Schema's preferred key-value delimiter may not be used in the query "
2544- )
2545- precondition (
2546- !encodeSet. shouldPercentEncode ( ascii: pairDelimiter) ,
2547- " Schema's preferred pair delimiter may not be used in the query "
2548- )
2601+ guard !encodeSet. shouldPercentEncode ( ascii: keyValueDelimiter) else {
2602+ return ( . max, 0 )
2603+ }
2604+ guard !encodeSet. shouldPercentEncode ( ascii: pairDelimiter) else {
2605+ return ( 0 , . max)
2606+ }
25492607 case . fragment:
25502608 let encodeSet = URLEncodeSet . Fragment ( )
2551- precondition (
2552- !encodeSet. shouldPercentEncode ( ascii: keyValueDelimiter) ,
2553- " Schema's preferred key-value delimiter may not be used in the fragment "
2554- )
2555- precondition (
2556- !encodeSet. shouldPercentEncode ( ascii: pairDelimiter) ,
2557- " Schema's preferred pair delimiter may not be used in the fragment "
2558- )
2609+ guard !encodeSet. shouldPercentEncode ( ascii: keyValueDelimiter) else {
2610+ return ( . max, 0 )
2611+ }
2612+ guard !encodeSet. shouldPercentEncode ( ascii: pairDelimiter) else {
2613+ return ( 0 , . max)
2614+ }
25592615 }
2616+
25602617 return ( keyValueDelimiter, pairDelimiter)
25612618 }
25622619
@@ -2587,6 +2644,17 @@ extension KeyValuePairsSupportedComponent {
25872644 }
25882645}
25892646
2647+ @inlinable
2648+ internal func _trapOnInvalidDelimiters(
2649+ _ delimiters: ( keyValue: UInt8 , pair: UInt8 )
2650+ ) -> ( keyValue: UInt8 , pair: UInt8 ) {
2651+ precondition (
2652+ delimiters. keyValue != . max && delimiters. pair != . max,
2653+ " Schema has invalid delimiters "
2654+ )
2655+ return delimiters
2656+ }
2657+
25902658extension URLStorage {
25912659
25922660 /// Replaces the given range of key-value pairs with a new collection of key-value pairs.
@@ -2642,7 +2710,9 @@ extension URLStorage {
26422710 return . success( newUpper..< newUpper)
26432711 }
26442712
2645- let ( keyValueDelimiter, pairDelimiter) = schema. verifyDelimitersDoNotNeedEscaping ( in: component)
2713+ let ( keyValueDelimiter, pairDelimiter) = _trapOnInvalidDelimiters (
2714+ schema. verifyDelimitersDoNotNeedEscaping ( in: component)
2715+ )
26462716
26472717 // Measure the replacement string.
26482718
@@ -2971,7 +3041,7 @@ extension URLStorage {
29713041
29723042 if insertDelimiter {
29733043 precondition ( !buffer. isEmpty)
2974- buffer [ 0 ] = schema. verifyDelimitersDoNotNeedEscaping ( in: component) . keyValue
3044+ buffer [ 0 ] = _trapOnInvalidDelimiters ( schema. verifyDelimitersDoNotNeedEscaping ( in: component) ) . keyValue
29753045 buffer = UnsafeMutableBufferPointer (
29763046 start: buffer. baseAddress! + 1 ,
29773047 count: buffer. count &- 1
0 commit comments