///|
/// Header scanning never decodes binary payload as text.
fn scan_header(
bytes : Bytes,
limits : Limits,
) -> (Map[String, String], Map[String, String], Int32) raise NrrdError {
limits.validate()
let fields : Map[String, String] = Map([])
let custom : Map[String, String] = Map([])
let mut offset = 0
let mut line_number = 0
while offset < bytes.length() {
let line = StringBuilder()
let mut ended = false
while offset < bytes.length() {
if offset >= limits.max_header_bytes {
raise Limit("header budget")
}
let b = bytes[offset].to_int()
offset = offset + 1
if b == 10 {
ended = true
break
}
if b == 13 {
if offset >= bytes.length() || bytes[offset] != b'\n' {
raise Invalid("bare CR in header")
}
continue
}
if b != 9 && (b < 32 || b > 126) {
raise Invalid("header must contain printable ASCII")
}
line.write_char(b.unsafe_to_char())
}
if !ended {
raise Truncated("unterminated header line")
}
let text = line.to_string()
if line_number == 0 {
match text {
"NRRD0001" | "NRRD0002" | "NRRD0003" | "NRRD0004" | "NRRD0005" => ()
_ => raise Invalid("NRRD magic/version")
}
} else if text.is_empty() {
return (fields, custom, offset)
} else if !text.has_prefix("#") {
let (key, rest) = match text.split_once(":") {
Some(pair) => pair
None => raise Invalid("header line requires colon")
}
let key = key.trim().to_owned()
if key.is_empty() {
raise Invalid("empty header key")
}
if rest.has_prefix("=") {
let value = rest.to_owned().split_at(1).1.to_owned()
if custom.contains(key) {
raise Invalid("duplicate custom key")
}
custom[key] = value
} else {
let key = key.to_lower()
if fields.contains(key) {
raise Invalid("duplicate field: " + key)
}
fields[key] = rest.trim().to_owned()
}
}
line_number = line_number + 1
}
raise Truncated("missing blank header terminator")
}
///|
fn words(text : String) -> Array[String] {
let result = []
let current = StringBuilder()
for c in text.iter() {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
if !current.is_empty() {
result.push(current.to_string())
current.reset()
}
} else {
current.write_char(c)
}
}
if !current.is_empty() {
result.push(current.to_string())
}
result
}
///|
fn integer(text : String) -> Int32 raise NrrdError {
@string.parse_int(text[:]) catch {
_ => raise Invalid("invalid integer: " + text)
}
}
///|
fn number(text : String) -> Double raise NrrdError {
match text.to_lower() {
"nan" | "+nan" | "-nan" => 0.0 / 0.0
"inf" | "+inf" | "infinity" | "+infinity" => 1.0 / 0.0
"-inf" | "-infinity" => -1.0 / 0.0
_ =>
@string.parse_double(text[:]) catch {
_ => raise Invalid("invalid number: " + text)
}
}
}
///|
/// NRRD quoted strings permit escaped quote and escaped backslash.
fn quoted_strings(text : String) -> Array[String] raise NrrdError {
let chars = text.to_array()
let result = []
let mut i = 0
while i < chars.length() {
if chars[i] == ' ' || chars[i] == '\t' {
i = i + 1
continue
}
if chars[i] != '"' {
raise Invalid("expected quoted axis string")
}
i = i + 1
let s = StringBuilder()
let mut closed = false
while i < chars.length() {
let c = chars[i]
i = i + 1
if c == '"' {
closed = true
break
}
if c == '\\' {
if i >= chars.length() || (chars[i] != '\\' && chars[i] != '"') {
raise Invalid("invalid quoted escape")
}
s.write_char(chars[i])
i = i + 1
} else {
s.write_char(c)
}
}
if !closed {
raise Invalid("unterminated quoted string")
}
if i < chars.length() && chars[i] != ' ' && chars[i] != '\t' {
raise Invalid("missing quoted-string separator")
}
result.push(s.to_string())
}
result
}
///|
fn vector(text : String) -> Array[Double] raise NrrdError {
let text = text.trim().to_owned()
if !text.has_prefix("(") || !text.has_suffix(")") {
raise Invalid("vector parentheses")
}
let inner = text[1:text.length() - 1].to_owned()
let values = []
for item in inner.split(",") {
values.push(number(item.trim().to_owned()))
}
values
}
///|
fn direction_list(text : String) -> Array[Array[Double]?] raise NrrdError {
let result = []
let chars = text.to_array()
let mut i = 0
while i < chars.length() {
if chars[i] == ' ' || chars[i] == '\t' {
i = i + 1
continue
}
let token = StringBuilder()
if chars[i] == '(' {
while i < chars.length() {
let c = chars[i]
token.write_char(c)
i = i + 1
if c == ')' {
break
}
}
result.push(Some(vector(token.to_string())))
} else {
while i < chars.length() && chars[i] != ' ' && chars[i] != '\t' {
token.write_char(chars[i])
i = i + 1
}
if token.to_string() != "none" {
raise Invalid("direction must be vector or none")
}
result.push(None)
}
if i < chars.length() && chars[i] != ' ' && chars[i] != '\t' {
raise Invalid("direction separator")
}
}
result
}