Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/ExtrasJSON/ArrayKey.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
struct ArrayKey: CodingKey, Equatable {
init(index: Int) {
self.intValue = index
Expand All @@ -24,3 +24,4 @@ func == (lhs: ArrayKey, rhs: ArrayKey) -> Bool {
precondition(rhs.intValue != nil)
return lhs.intValue == rhs.intValue
}
#endif
3 changes: 2 additions & 1 deletion Sources/ExtrasJSON/Decoding/JSONDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
public struct XJSONDecoder {
public var userInfo: [CodingUserInfoKey: Any] = [:]

Expand Down Expand Up @@ -80,3 +80,4 @@ extension JSONDecoderImpl: Decoder {
)
}
}
#endif
2 changes: 2 additions & 0 deletions Sources/ExtrasJSON/Decoding/JSONKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if !hasFeature(Embedded)
struct JSONKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
typealias Key = K

Expand Down Expand Up @@ -188,3 +189,4 @@ extension JSONKeyedDecodingContainer {
return floatingPoint
}
}
#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
struct JSONSingleValueDecodingContainter: SingleValueDecodingContainer {
let impl: JSONDecoderImpl
let value: JSONValue
Expand Down Expand Up @@ -121,3 +121,4 @@ extension JSONSingleValueDecodingContainter {
return floatingPoint
}
}
#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
struct JSONUnkeyedDecodingContainer: UnkeyedDecodingContainer {
let impl: JSONDecoderImpl
let codingPath: [CodingKey]
Expand Down Expand Up @@ -205,3 +205,4 @@ extension JSONUnkeyedDecodingContainer {
return float
}
}
#endif
3 changes: 2 additions & 1 deletion Sources/ExtrasJSON/Encoding/JSONEncoder.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
enum JSONFuture {
case value(JSONValue)
case nestedArray(JSONArray)
Expand Down Expand Up @@ -179,3 +179,4 @@ extension JSONEncoderImpl: Encoder {
return JSONSingleValueEncodingContainer(impl: self, codingPath: self.codingPath)
}
}
#endif
3 changes: 2 additions & 1 deletion Sources/ExtrasJSON/Encoding/JSONKeyedEncodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
struct JSONKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
typealias Key = K

Expand Down Expand Up @@ -141,3 +141,4 @@ extension JSONKeyedEncodingContainer {
self.object.set(.number(value.description), for: key.stringValue)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
struct JSONSingleValueEncodingContainer: SingleValueEncodingContainer {
let impl: JSONEncoderImpl
let codingPath: [CodingKey]
Expand Down Expand Up @@ -107,3 +107,4 @@ extension JSONSingleValueEncodingContainer {
self.impl.singleValue = .number(value.description)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
struct JSONUnkeyedEncodingContainer: UnkeyedEncodingContainer {
let impl: JSONEncoderImpl
let array: JSONArray
Expand Down Expand Up @@ -136,3 +136,4 @@ extension JSONUnkeyedEncodingContainer {
self.array.append(.number(value.description))
}
}
#endif
3 changes: 2 additions & 1 deletion Sources/ExtrasJSON/JSONError+DecodingError.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

#if !hasFeature(Embedded)
extension JSONError {
@inlinable var decodingError: DecodingError {
switch self {
Expand Down Expand Up @@ -66,3 +66,4 @@ extension JSONError {
}
}
}
#endif
2 changes: 1 addition & 1 deletion Sources/ExtrasJSON/JSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public enum JSONError: Swift.Error, Equatable {
case numberWithLeadingZero(index: Int)
}

public enum JSONValue {
public enum JSONValue: Sendable {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realised just before submitting this PR that this is not strictly necessary. But given that all of the enum cases themselves are Sendable, I think this is correct and desirable. Let me know if you'd like me to revert the change

case string(String)
case number(String)
case bool(Bool)
Expand Down
54 changes: 29 additions & 25 deletions Sources/ExtrasJSON/Parsing/DocumentReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@
case expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: Int)
case unexpectedEscapedCharacter(ascii: UInt8, index: Int)
case couldNotCreateUnicodeScalarFromUInt32(index: Int, unicodeScalarValue: UInt32)
case jsonError(JSONError)
Copy link
Author

@ephemer ephemer Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is arguably the biggest "functional change" of the PR, and I'm not extremely happy about it, but I couldn't find a better way of keeping to a single error type per function (noting that some of these functions throw JSONError or EscapedSequenceError)

I think it's fine though, especially given that EscapedSequenceError is internal to the library.

}

@inlinable mutating func readUTF8StringTillNextUnescapedQuote() throws -> String {
@inlinable mutating func readUTF8StringTillNextUnescapedQuote() throws(JSONError) -> String {
precondition(self.value == UInt8(ascii: "\""), "Expected to have read a quote character last")
var stringStartIndex = self.index + 1
var output: String?
Expand All @@ -66,7 +67,7 @@
// through U+001F).
var string = output ?? ""
string += self.makeStringFast(self.array[stringStartIndex ... index])
throw JSONError.unescapedControlCharacterInString(ascii: byte, in: string, index: index)
throw .unescapedControlCharacterInString(ascii: byte, in: string, index: index)

case UInt8(ascii: "\\"):
if output != nil {
Expand All @@ -79,25 +80,28 @@
let (escaped, newIndex) = try parseEscapeSequence()
output! += escaped
stringStartIndex = newIndex + 1
} catch EscapedSequenceError.unexpectedEscapedCharacter(let ascii, let failureIndex) {
} catch (let error) {
output! += makeStringFast(array[index ... self.index])
throw JSONError.unexpectedEscapedCharacter(ascii: ascii, in: output!, index: failureIndex)
} catch EscapedSequenceError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(let failureIndex) {
output! += makeStringFast(array[index ... self.index])
throw JSONError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(in: output!, index: failureIndex)
} catch EscapedSequenceError.couldNotCreateUnicodeScalarFromUInt32(let failureIndex, let unicodeScalarValue) {
output! += makeStringFast(array[index ... self.index])
throw JSONError.couldNotCreateUnicodeScalarFromUInt32(
in: output!, index: failureIndex, unicodeScalarValue: unicodeScalarValue
)
switch error {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving this to a single switch statement rather than multiple catch statements cleans up the code somewhat by removing repetition. More importantly, Swift didn't seem to recognise that all cases are satisfied without this change

case let .unexpectedEscapedCharacter(ascii, failureIndex):
throw .unexpectedEscapedCharacter(ascii: ascii, in: output!, index: failureIndex)
case let .expectedLowSurrogateUTF8SequenceAfterHighSurrogate(failureIndex):
throw .expectedLowSurrogateUTF8SequenceAfterHighSurrogate(in: output!, index: failureIndex)
case let .couldNotCreateUnicodeScalarFromUInt32(failureIndex, unicodeScalarValue):
throw .couldNotCreateUnicodeScalarFromUInt32(
in: output!, index: failureIndex, unicodeScalarValue: unicodeScalarValue
)
case let .jsonError(jsonError):
throw jsonError
}
}

default:
continue
}
}

throw JSONError.unexpectedEndOfFile
throw .unexpectedEndOfFile
}

// can be removed as soon https://bugs.swift.org/browse/SR-12126 and
Expand All @@ -111,9 +115,9 @@
}
}

@inlinable mutating func parseEscapeSequence() throws -> (String, Int) {
@inlinable mutating func parseEscapeSequence() throws(EscapedSequenceError) -> (String, Int) {
guard let (byte, index) = read() else {
throw JSONError.unexpectedEndOfFile
throw .jsonError(.unexpectedEndOfFile)
}

switch byte {
Expand All @@ -129,11 +133,11 @@
let (character, newIndex) = try parseUnicodeSequence()
return (String(character), newIndex)
default:
throw EscapedSequenceError.unexpectedEscapedCharacter(ascii: byte, index: index)
throw .unexpectedEscapedCharacter(ascii: byte, index: index)
}
}

@inlinable mutating func parseUnicodeSequence() throws -> (Unicode.Scalar, Int) {
@inlinable mutating func parseUnicodeSequence() throws(EscapedSequenceError) -> (Unicode.Scalar, Int) {
// we build this for utf8 only for now.
let bitPattern = try parseUnicodeHexSequence()

Expand All @@ -145,49 +149,49 @@
guard let (escapeChar, _) = read(),
let (uChar, _) = read()
else {
throw JSONError.unexpectedEndOfFile
throw .jsonError(.unexpectedEndOfFile)
}

guard escapeChar == UInt8(ascii: #"\"#), uChar == UInt8(ascii: "u") else {
throw EscapedSequenceError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: self.index)
throw .expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: self.index)
}

let lowSurrogateBitBattern = try parseUnicodeHexSequence()
let isSecondByteLowSurrogate = lowSurrogateBitBattern & 0xFC00 // nil everything except first six bits
guard isSecondByteLowSurrogate == 0xDC00 else {
// we are in an escaped sequence. for this reason an output string must have
// been initialized
throw EscapedSequenceError.expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: self.index)
throw .expectedLowSurrogateUTF8SequenceAfterHighSurrogate(index: self.index)
}

let highValue = UInt32(highSurrogateBitPattern - 0xD800) * 0x400
let lowValue = UInt32(lowSurrogateBitBattern - 0xDC00)
let unicodeValue = highValue + lowValue + 0x10000
guard let unicode = Unicode.Scalar(unicodeValue) else {
throw EscapedSequenceError.couldNotCreateUnicodeScalarFromUInt32(
throw .couldNotCreateUnicodeScalarFromUInt32(
index: self.index, unicodeScalarValue: unicodeValue
)
}
return (unicode, self.index)
}

guard let unicode = Unicode.Scalar(bitPattern) else {
throw EscapedSequenceError.couldNotCreateUnicodeScalarFromUInt32(
throw .couldNotCreateUnicodeScalarFromUInt32(
index: self.index, unicodeScalarValue: UInt32(bitPattern)
)
}
return (unicode, self.index)
}

@inlinable mutating func parseUnicodeHexSequence() throws -> UInt16 {
@inlinable mutating func parseUnicodeHexSequence() throws(EscapedSequenceError) -> UInt16 {
// As stated in RFC-8259 an escaped unicode character is 4 HEXDIGITs long
// https://tools.ietf.org/html/rfc8259#section-7
guard let (firstHex, startIndex) = read(),
let (secondHex, _) = read(),
let (thirdHex, _) = read(),
let (forthHex, _) = read()
else {
throw JSONError.unexpectedEndOfFile
throw .jsonError(.unexpectedEndOfFile)
}

guard let first = DocumentReader.hexAsciiTo4Bits(firstHex),
Expand All @@ -196,7 +200,7 @@
let forth = DocumentReader.hexAsciiTo4Bits(forthHex)
else {
let hexString = String(decoding: [firstHex, secondHex, thirdHex, forthHex], as: Unicode.UTF8.self)
throw JSONError.invalidHexDigitSequence(hexString, index: startIndex)
throw .jsonError(.invalidHexDigitSequence(hexString, index: startIndex))
}
let firstByte = UInt16(first) << 4 | UInt16(second)
let secondByte = UInt16(third) << 4 | UInt16(forth)
Expand Down
Loading