///|
fn read_text(
b : Bytes,
start : Int,
end : Int,
delimiter : Byte?,
latin1? : Bool = false,
) -> Array[(String, String)] raise FcsError {
if end < start || end - start > 2097152 {
raise Limit("TEXT range")
}
need(b, start, end - start + 1)
let sep = b[start]
if sep.to_int() < 1 || sep.to_int() > 126 {
raise Invalid("TEXT delimiter must be ASCII 1..126")
}
if delimiter is Some(expected) && sep != expected {
raise Invalid("inconsistent TEXT delimiter")
}
if b[end] != sep {
raise Invalid("TEXT is missing its terminating delimiter")
}
let tokens : Array[String] = []
let buf : Array[Byte] = []
let mut i = start + 1
while i <= end {
if b[i] == sep {
if i < end && b[i + 1] == sep {
if buf.is_empty() {
raise Invalid(
"delimiter cannot be first character of a keyword/value",
)
}
buf.push(sep)
i = i + 2
continue
}
if buf.is_empty() {
raise Invalid("empty TEXT keyword or value")
}
tokens.push(
@utf8.decode(Bytes::from_array(buf), ignore_bom=false) catch {
_ =>
if latin1 && tokens.length() % 2 == 1 {
String::from_iter(
buf.iter().map(b => b.to_int().to_char().unwrap()),
)
} else {
raise Invalid(
"invalid UTF-8 in TEXT; legacy encoding requires explicit latin1 fallback",
)
}
},
)
buf.clear()
if tokens.length() > 8192 {
raise Limit("more than 4096 metadata pairs")
}
} else {
buf.push(b[i])
}
i = i + 1
}
if !buf.is_empty() || tokens.length() % 2 != 0 {
raise Invalid("unpaired TEXT keyword/value")
}
let result : Array[(String, String)] = []
let keys : Map[String, Bool] = Map([])
for i = 0; i < tokens.length(); i = i + 2 {
let key = tokens[i]
for c in key.iter() {
if c.to_int() < 32 || c.to_int() > 126 {
raise Invalid("nonprintable TEXT keyword")
}
}
let folded = key.to_upper()
if keys.contains(folded) {
raise Invalid("duplicate TEXT keyword: \{key}")
}
keys[folded] = true
result.push((key, tokens[i + 1]))
}
result
}
///|
fn read_segment(
b : Bytes,
start : Int,
end : Int,
delimiter : Byte?,
latin1 : Bool,
allow_text_padding : Bool,
warnings : Array[String],
segment : String,
) -> Array[(String, String)] raise FcsError {
// Keep the declared range for overlap checks. Only the token parser's end
// moves, and only after checking the original extent against the limits.
if end < start || end - start > 2097152 {
raise Limit("TEXT range")
}
need(b, start, end - start + 1)
let mut stop = end
if allow_text_padding && b[start] != b' ' {
while stop > start && b[stop] == b' ' {
stop -= 1
}
}
let pairs = read_text(b, start, stop, delimiter, latin1~)
if stop != end {
warnings.push(
"ignored \{end - stop} trailing ASCII spaces in \{segment}; declared extent retained",
)
}
pairs
}
///|
fn merge_text(
primary : Array[(String, String)],
extra : Array[(String, String)],
) -> Unit raise FcsError {
for (key, value) in extra {
if lookup(primary, key) is Some(_) {
raise Invalid("duplicate supplemental keyword: \{key}")
}
primary.push((key, value))
}
if primary.length() > 4096 {
raise Limit("combined metadata count")
}
}
///|
fn encode_text(pairs : Array[(String, String)]) -> Bytes raise FcsError {
// An ASCII control delimiter remains valid and avoids common slash-containing values.
let sep = "\u001c"
let chunks = [sep]
let seen : Map[String, Bool] = Map([])
for (key, value) in pairs {
if key.is_empty() || value.is_empty() {
raise Invalid("cannot write empty metadata")
}
for c in key.iter() {
if c.to_int() < 32 || c.to_int() > 126 {
raise Invalid("non-ASCII metadata key")
}
}
if value.has_prefix(sep) {
raise Invalid("metadata value starts with delimiter")
}
let k = key.to_upper()
if seen.contains(k) {
raise Invalid("duplicate output metadata")
}
seen[k] = true
chunks.push(k)
chunks.push(sep)
chunks.push(value.replace(old=sep, new=sep + sep))
chunks.push(sep)
}
let b = @utf8.encode(chunks.join(""))
if b.length() > 2097152 {
raise Limit("output TEXT size")
}
b
}