PureYAML/Sources/PureYAMLJSON/JSON.Source.swift
Mihaela Mihaljevic 27cc8cbfbb
Some checks failed
rules-freshness / rules (push) Failing after 12m24s
fix(json): keep every character of a JSON string through the trip
The JSON writer escapes the characters a YAML reader mangles. PureYAML.JSON.parse and parseLocated read JSON with those characters held back and given back, so no position moves, and Convert.convert takes the format of its source.
2026-11-15 12:12:44 +01:00

106 lines
4.7 KiB
Swift

import PureYAML
extension PureYAML.JSON {
/// JSON text with the characters the YAML reader cannot be given held back.
///
/// Each U+007F to U+009F, U+2028, U+2029, U+FFFE and U+FFFF that is inside a string is replaced, one scalar for one scalar, by a private-use character that is not
/// in the text, so that the reader sees a plain character and every position of the text is the position it had. The replacement is undone in the value that
/// comes out of the reader. A private-use character that the text itself holds is never used as a replacement, so what comes back is never mistaken for it.
struct Source {
let masked: String
/// What each replacement stands for.
private let originals: [Unicode.Scalar: Unicode.Scalar]
init(_ text: String) throws {
let present = Set(text.unicodeScalars.filter { Self.privateUse.contains($0.value) })
var free = Self.privateUse.lazy.compactMap(Unicode.Scalar.init).filter { !present.contains($0) }.makeIterator()
var replacement: [Unicode.Scalar: Unicode.Scalar] = [:]
var originals: [Unicode.Scalar: Unicode.Scalar] = [:]
var output = String.UnicodeScalarView()
var inString = false
var escaped = false
for scalar in text.unicodeScalars {
guard inString else {
inString = scalar == "\""
output.append(scalar)
continue
}
if escaped {
escaped = false
} else if scalar == "\\" {
escaped = true
} else if scalar == "\"" {
inString = false
} else if Self.isHeldBack(scalar) {
if replacement[scalar] == nil {
guard let next = free.next() else {
throw PureYAML.JSON.Convert.Failure.sourceInvalid("the text holds too many private-use characters to read it as JSON")
}
replacement[scalar] = next
originals[next] = scalar
}
output.append(replacement[scalar] ?? scalar)
continue
}
output.append(scalar)
}
masked = String(output)
self.originals = originals
}
private static let privateUse: ClosedRange<UInt32> = 0xE000 ... 0xF8FF
/// The characters RFC 8259 lets a string hold as they are and the YAML reader folds or refuses.
static func isHeldBack(_ scalar: Unicode.Scalar) -> Bool {
(0x7F ... 0x9F).contains(scalar.value) || [0x2028, 0x2029, 0xFFFE, 0xFFFF].contains(scalar.value)
}
func restoring(_ text: String) -> String {
guard !originals.isEmpty else { return text }
var output = String.UnicodeScalarView()
for scalar in text.unicodeScalars {
output.append(originals[scalar] ?? scalar)
}
return String(output)
}
func restoring(_ value: PureYAML.Model.Value) -> PureYAML.Model.Value {
guard !originals.isEmpty else { return value }
switch value {
case .null, .bool, .int, .double:
return value
case let .string(text):
return .string(restoring(text))
case let .sequence(items):
return .sequence(items.map(restoring))
case let .mapping(mapping):
return .mapping(restoring(mapping))
}
}
func restoring(_ located: PureYAML.Model.LocatedValue) -> PureYAML.Model.LocatedValue {
guard !originals.isEmpty else { return located }
switch located {
case let .scalar(value, range):
return .scalar(restoring(value), range)
case let .sequence(items, range):
return .sequence(items.map(restoring), range)
case let .mapping(pairs, range):
return .mapping(pairs.map { .init(keyNode: restoring($0.keyNode), keyRange: $0.keyRange, value: restoring($0.value)) }, range)
}
}
private func restoring(_ mapping: PureYAML.Model.Mapping) -> PureYAML.Model.Mapping {
.init(mapping.pairs.map { .init(keyNode: restoring($0.keyNode), value: restoring($0.value)) })
}
private func restoring(_ key: PureYAML.Model.Key) -> PureYAML.Model.Key {
switch key {
case let .string(text): .string(restoring(text))
case let .sequence(items): .sequence(items.map(restoring))
case let .mapping(mapping): .mapping(restoring(mapping))
}
}
}
}