Skip to content

Commit b1a70e6

Browse files
committed
[JSON] Encode/decode escape sequence correctly
(cherry picked from commit 9246250)
1 parent fa601e8 commit b1a70e6

File tree

3 files changed

+100
-28
lines changed

3 files changed

+100
-28
lines changed

Sources/SwiftCompilerPluginMessageHandling/JSON/JSONDecoding.swift

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -408,36 +408,61 @@ private enum _JSONStringParser {
408408
}
409409
}
410410

411-
static func decodeStringWithEscapes(source: UnsafeBufferPointer<UInt8>) -> String {
411+
/// Helper iterator decoding UTF8 sequence to UnicodeScalar stream.
412+
struct ScalarIterator<S: Sequence>: IteratorProtocol where S.Element == UInt8 {
413+
var backing: S.Iterator
414+
var decoder: UTF8
415+
init(_ source: S) {
416+
self.backing = source.makeIterator()
417+
self.decoder = UTF8()
418+
}
419+
mutating func next() -> UnicodeScalar? {
420+
switch decoder.decode(&backing) {
421+
case .scalarValue(let scalar): return scalar
422+
case .emptyInput: return nil
423+
case .error: fatalError("invalid")
424+
}
425+
}
426+
}
427+
428+
static func decodeStringWithEscapes(source: UnsafeBufferPointer<UInt8>) -> String? {
412429
var string: String = ""
413-
var iter = source.makeIterator()
414-
var utf8Decoder = UTF8()
415-
DECODE: while true {
416-
switch utf8Decoder.decode(&iter) {
417-
case .scalarValue(let scalar):
418-
if scalar == "\\" {
419-
switch iter.next() {
420-
case UInt8(ascii: "\""): string.append("\"")
421-
case UInt8(ascii: "'"): string.append("'")
422-
case UInt8(ascii: "\\"): string.append("\\")
423-
case UInt8(ascii: "/"): string.append("/")
424-
case UInt8(ascii: "b"): string.append("\u{08}") // \b
425-
case UInt8(ascii: "f"): string.append("\u{0C}") // \f
426-
case UInt8(ascii: "n"): string.append("\u{0A}") // \n
427-
case UInt8(ascii: "r"): string.append("\u{0D}") // \r
428-
case UInt8(ascii: "t"): string.append("\u{09}") // \t
429-
case UInt8(ascii: "u"):
430-
fatalError("unimplemented")
431-
default:
432-
fatalError("invalid")
430+
var iter = ScalarIterator(source)
431+
while let scalar = iter.next() {
432+
// NOTE: We don't report detailed errors because we only care well-formed
433+
// payloads from the compiler.
434+
if scalar == "\\" {
435+
switch iter.next() {
436+
case "\"": string.append("\"")
437+
case "'": string.append("'")
438+
case "\\": string.append("\\")
439+
case "/": string.append("/")
440+
case "b": string.append("\u{08}")
441+
case "f": string.append("\u{0C}")
442+
case "n": string.append("\u{0A}")
443+
case "r": string.append("\u{0D}")
444+
case "t": string.append("\u{09}")
445+
case "u":
446+
// We don't care performance of this because \uFFFF style escape is
447+
// pretty rare. We only do it for control characters.
448+
let buffer: [UInt8] = [iter.next(), iter.next(), iter.next(), iter.next()]
449+
.compactMap { $0 }
450+
.compactMap { UInt8(exactly: $0.value) }
451+
452+
guard
453+
buffer.count == 4,
454+
let result: UInt16 = buffer.withUnsafeBufferPointer(_JSONNumberParser.parseHexIntegerDigits(source:)),
455+
let scalar = UnicodeScalar(result)
456+
else {
457+
return nil
433458
}
434-
} else {
435459
string.append(Character(scalar))
460+
default:
461+
// invalid escape sequence
462+
return nil
436463
}
437-
case .emptyInput:
438-
break DECODE
439-
case .error:
440-
fatalError("invalid")
464+
} else {
465+
string.append(Character(scalar))
441466
}
442467
}
443468
return string
@@ -488,6 +513,34 @@ private enum _JSONNumberParser {
488513
}
489514
return value
490515
}
516+
517+
static func parseHexIntegerDigits<Integer: FixedWidthInteger>(source: UnsafeBufferPointer<UInt8>) -> Integer? {
518+
var source = source[...]
519+
var value: Integer = 0
520+
var overflowed: Bool = false
521+
while let digit = source.popFirst() {
522+
let digitValue: Integer
523+
switch digit {
524+
case UInt8(ascii: "0")...UInt8(ascii: "9"):
525+
digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "0"))
526+
case UInt8(ascii: "a")...UInt8(ascii: "f"):
527+
digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "a") &+ 10)
528+
case UInt8(ascii: "A")...UInt8(ascii: "F"):
529+
digitValue = Integer(truncatingIfNeeded: digit &- UInt8(ascii: "A") &+ 10)
530+
default:
531+
return nil
532+
}
533+
(value, overflowed) = value.multipliedReportingOverflow(by: 16)
534+
guard !overflowed else {
535+
return nil
536+
}
537+
(value, overflowed) = value.addingReportingOverflow(digitValue)
538+
guard !overflowed else {
539+
return nil
540+
}
541+
}
542+
return value
543+
}
491544
}
492545

493546
extension JSONMapValue {

Sources/SwiftCompilerPluginMessageHandling/JSON/JSONEncoding.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,23 @@ private struct JSONWriter {
168168
flush()
169169
write(string: "\\r")
170170
case 0x0...0xF:
171+
let c = cursor.pointee
171172
flush()
172173
write(string: "\\u000")
173-
write(ascii: UInt8(ascii: "0") &+ cursor.pointee)
174+
if (c < 10) {
175+
write(ascii: UInt8(ascii: "0") &+ c)
176+
} else {
177+
write(ascii: UInt8(ascii: "A") &+ (c &- 10))
178+
}
174179
case 0x10...0x1F:
180+
let c = cursor.pointee & 0xF
175181
flush()
176182
write(string: "\\u001")
177-
write(ascii: UInt8(ascii: "0") &+ (cursor.pointee & 0xF))
183+
if (c < 10) {
184+
write(ascii: UInt8(ascii: "0") &+ c)
185+
} else {
186+
write(ascii: UInt8(ascii: "A") &+ (c &- 10))
187+
}
178188
default:
179189
// Accumulate this byte.
180190
cursor += 1

Tests/SwiftCompilerPluginTest/JSONTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ final class JSONTests: XCTestCase {
8282
)
8383
}
8484

85+
func testUnicodeEscape() {
86+
_testRoundTrip(
87+
of: "\n\u{A9}\u{0}\u{07}\u{1B}",
88+
expectedJSON: #"""
89+
"\n©\u0000\u0007\u001B"
90+
"""#
91+
)
92+
}
93+
8594
func testTypeCoercion() {
8695
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self)
8796
_testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self)

0 commit comments

Comments
 (0)