PureYAML/Sources/Parsing/LibYAML.Scanner.Tag.swift
Mihaela Mihaljevic 6fa49b7ffc style: bring four files back under the swiftformat gate
swiftformat --lint failed on these pre-existing files and the pre-push
hook checks the whole tree, blocking every push until they conform.
The wrapped if bodies pushed YAMLTestSuiteGapsTests over the 400-line
lint limit, so its private JSONStreamScanner helper moves to its own
support file.
2026-07-12 13:29:05 +02:00

164 lines
6.6 KiB
Swift

extension PureYAML.Parsing.LibYAML.Scanner {
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
/// Port of `yaml_parser_scan_tag`. Scans a tag in verbatim `!<uri>`, shorthand
/// `!handle!suffix`, or `!suffix` form, producing the handle and suffix; the
/// parser resolves them against the tag directives.
mutating func scanTag() throws -> PureYAML.Parsing.LibYAML.Token {
let startMark = reader.mark
var handle = ""
var suffix = ""
if reader.peek(1) == "<" {
reader.skip()
reader.skip()
suffix = try scanTagURI(uriChar: true, directive: false, head: "", startMark: startMark)
guard reader.peek() == ">" else {
throw tagError("did not find the expected '>'", startMark)
}
reader.skip()
} else {
handle = try scanTagHandle(directive: false, startMark: startMark)
if handle.first == "!", handle.count > 1, handle.last == "!" {
suffix = try scanTagURI(uriChar: false, directive: false, head: "", startMark: startMark)
} else {
suffix = try scanTagURI(uriChar: false, directive: false, head: handle, startMark: startMark)
handle = "!"
if suffix.isEmpty {
swap(&handle, &suffix)
}
}
}
guard Classify.isBlankZ(reader.peek()) || (flowLevel > 0 && reader.peek() == ",") else {
throw tagError("did not find expected whitespace or line break", startMark)
}
return PureYAML.Parsing.LibYAML.Token(type: .tag(handle: handle, suffix: suffix), startMark: startMark, endMark: reader.mark)
}
/// Port of `yaml_parser_scan_tag_handle`. A handle is `!`, optionally followed
/// by alphanumeric characters and a closing `!`.
mutating func scanTagHandle(directive: Bool, startMark: PureYAML.Parsing.Mark) throws -> String {
guard reader.peek() == "!" else {
throw tagError("did not find expected '!'", startMark)
}
var handle = "!"
reader.skip()
while let character = reader.peek(), Classify.isAlpha(character) {
handle.unicodeScalars.append(character)
reader.skip()
}
if reader.peek() == "!" {
handle.unicodeScalars.append("!")
reader.skip()
} else if directive, handle != "!" {
throw tagError("did not find expected '!'", startMark)
}
return handle
}
/// Port of `yaml_parser_scan_tag_uri`. Consumes the URI character set (plus the
/// flow indicators inside a verbatim tag), decoding `%`-escapes.
mutating func scanTagURI(uriChar: Bool, directive: Bool, head: String, startMark: PureYAML.Parsing.Mark) throws -> String {
var string = head.count > 1 ? String(head.dropFirst()) : ""
var length = head.isEmpty ? 0 : head.count
while let character = reader.peek(), isURICharacter(character, uriChar: uriChar) {
if character == "%" {
try scanURIEscapes(directive: directive, startMark: startMark, into: &string)
} else {
string.unicodeScalars.append(character)
reader.skip()
}
length += 1
}
guard length > 0 else {
throw tagError("did not find expected tag URI", startMark)
}
return string
}
/// Port of `yaml_parser_scan_uri_escapes`. Decodes one UTF-8 character from a
/// run of `%XX` octets.
private mutating func scanURIEscapes(directive _: Bool, startMark: PureYAML.Parsing.Mark, into string: inout String) throws {
var bytes: [UInt8] = []
var width = 0
repeat {
guard reader.peek() == "%", let high = reader.peek(1), Classify.isHex(high), let low = reader.peek(2), Classify.isHex(low) else {
throw tagError("did not find URI escaped octet", startMark)
}
let octet = UInt8((hexDigit(high) << 4) + hexDigit(low))
if width == 0 {
width = utf8Width(of: octet)
guard width > 0 else {
throw tagError("found an incorrect leading UTF-8 octet", startMark)
}
} else if octet & 0xC0 != 0x80 {
throw tagError("found an incorrect trailing UTF-8 octet", startMark)
}
bytes.append(octet)
reader.skip()
reader.skip()
reader.skip()
width -= 1
} while width > 0
guard let scalar = Unicode.Scalar(decodeUTF8(bytes)) else {
throw tagError("found invalid UTF-8 in a URI escape", startMark)
}
string.unicodeScalars.append(scalar)
}
/// Assemble a code point from the validated 1-to-4 UTF-8 octets of one escape.
private func decodeUTF8(_ bytes: [UInt8]) -> UInt32 {
switch bytes.count {
case 1:
UInt32(bytes[0])
case 2:
(UInt32(bytes[0] & 0x1F) << 6) | UInt32(bytes[1] & 0x3F)
case 3:
(UInt32(bytes[0] & 0x0F) << 12) | (UInt32(bytes[1] & 0x3F) << 6) | UInt32(bytes[2] & 0x3F)
default:
(UInt32(bytes[0] & 0x07) << 18) | (UInt32(bytes[1] & 0x3F) << 12) | (UInt32(bytes[2] & 0x3F) << 6) | UInt32(bytes[3] & 0x3F)
}
}
private func utf8Width(of octet: UInt8) -> Int {
if octet & 0x80 == 0x00 {
return 1
}
if octet & 0xE0 == 0xC0 {
return 2
}
if octet & 0xF0 == 0xE0 {
return 3
}
if octet & 0xF8 == 0xF0 {
return 4
}
return 0
}
private func isURICharacter(_ scalar: Unicode.Scalar, uriChar: Bool) -> Bool {
if Classify.isAlpha(scalar) {
return true
}
switch scalar {
case ";", "/", "?", ":", "@", "&", "=", "+", "$", ".", "%", "!", "~", "*", "'", "(", ")":
return true
case ",", "[", "]":
return uriChar
default:
return false
}
}
private func hexDigit(_ scalar: Unicode.Scalar) -> Int {
switch scalar {
case "0" ... "9": Int(scalar.value - Unicode.Scalar("0").value)
case "A" ... "F": Int(scalar.value - Unicode.Scalar("A").value) + 10
default: Int(scalar.value - Unicode.Scalar("a").value) + 10
}
}
private func tagError(_ problem: String, _ mark: PureYAML.Parsing.Mark) -> PureYAML.Parsing.LibYAML.ScannerError {
PureYAML.Parsing.LibYAML.ScannerError(context: "while scanning a tag", problem: problem, mark: mark)
}
}