PureYAML/Sources/Parsing/Parsing.PlainScalar.swift
Mihaela Mihaljevic d2421aa0ce fix(parsing): a zero followed by a sign is text, not an octal integer
Int(_, radix:) accepts a sign, so 0-3 (a CPU range, a port range) was read as the integer -3, and 0x-1, 0b-1 and 0o-1 as negative numbers. After a radix prefix only digits may follow. Octal, hex, binary and signed integers read as before.
2026-11-02 18:25:06 +01:00

100 lines
3.8 KiB
Swift

extension PureYAML.Parsing {
/// How a plain (unquoted) scalar reads: as null, a boolean, an integer, a floating-point number, or else a string.
///
/// This is the one definition of that reading. The parser takes the value of every plain scalar from it, and the emitter asks it whether a string can be
/// written without quotes, since a string that reads as anything else would come back changed. Two lists of the same words kept apart drift apart: the
/// emitter once wrote the string `yes` plain while the parser read `yes` as a boolean.
enum PlainScalar {
/// The value `text` has as a plain scalar. `text` has no surrounding whitespace.
static func resolve(_ text: String) -> PureYAML.Model.Value {
switch text {
case "", "~", "null", "Null", "NULL":
return .null
default:
break
}
if let bool = bool(text) {
return .bool(bool)
}
if let int = integer(text), !text.contains(".") {
return .int(int)
}
if text.contains(".") || text.contains("e") || text.contains("E"), let double = double(text) {
return .double(double)
}
return .string(text)
}
/// Whether `text` written plain reads back as the same string.
static func readsAsString(_ text: String) -> Bool {
if case .string = resolve(text) {
return true
}
return false
}
static func bool(_ text: String) -> Bool? {
switch text {
case "true", "True", "TRUE", "yes", "Yes", "YES", "on", "On", "ON":
true
case "false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF":
false
default:
nil
}
}
static func integer(_ text: String) -> Int? {
let value = removingNumericSeparators(from: text)
let negative = value.hasPrefix("-")
let hasSign = negative || value.hasPrefix("+")
let unsigned = hasSign ? String(value.dropFirst()) : value
guard !unsigned.isEmpty else {
return nil
}
guard unsigned != "0" else {
return 0
}
let radixPrefixes: [(prefix: String, radix: Int)] = [
("0x", 16),
("0X", 16),
("0b", 2),
("0B", 2),
("0o", 8),
("0O", 8),
("0", 8),
]
for radixPrefix in radixPrefixes where unsigned.hasPrefix(radixPrefix.prefix) {
let digits = String(unsigned.dropFirst(radixPrefix.prefix.count))
// `Int(_, radix:)` accepts a sign, so `0-3` would read as minus three: after the prefix only digits may follow
guard let first = digits.first, first != "-", first != "+", let magnitude = Int(digits, radix: radixPrefix.radix) else {
return nil
}
return negative ? -magnitude : magnitude
}
return Int(value)
}
static func double(_ text: String) -> Double? {
switch text {
case ".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF":
.infinity
case "-.inf", "-.Inf", "-.INF":
-.infinity
case ".nan", ".NaN", ".NAN", "+.nan", "+.NaN", "+.NAN", "-.nan", "-.NaN", "-.NAN":
.nan
default:
Double(removingNumericSeparators(from: text))
}
}
private static func removingNumericSeparators(from text: String) -> String {
text.filter { $0 != "_" }
}
}
}