///|
priv enum DMXToken {
DToken(String)
DPunctuation(Char)
DEnd
} derive(Eq)
///|
priv struct DMXTextReader {
chars : Array[Char]
mut pos : Int32
mut pending : DMXToken?
elements : Array[DMXElement]
options : BinaryOptions
mut values : Int32
}
///|
fn DMXTextReader::token(self : DMXTextReader) -> DMXToken raise {
if self.pending is Some(token) {
self.pending = None
return token
}
while self.pos < self.chars.length() {
let c = self.chars[self.pos]
if [' ', '\n', '\r', '\t'].contains(c) {
self.pos += 1
continue
}
if c == '/' && self.chars.get(self.pos + 1) == Some('/') {
while self.pos < self.chars.length() && self.chars[self.pos] != '\n' {
self.pos += 1
}
continue
}
if c == '/' && self.chars.get(self.pos + 1) == Some('*') {
self.pos += 2
while self.pos + 1 < self.chars.length() &&
!(self.chars[self.pos] == '*' && self.chars[self.pos + 1] == '/') {
self.pos += 1
}
if self.pos + 1 >= self.chars.length() {
raise InvalidBinary(self.pos, "Unclosed DMX comment")
}
self.pos += 2
continue
}
self.pos += 1
if ['{', '}', '[', ']', ','].contains(c) {
return DPunctuation(c)
}
if c != '"' {
raise InvalidBinary(self.pos - 1, "DMX text tokens must be quoted")
}
let start = self.pos - 1
let mut escaped = false
while self.pos < self.chars.length() {
let current = self.chars[self.pos]
self.pos += 1
if current == '"' && !escaped {
let text = po_unquote(
String::from_array(self.chars[start:self.pos].to_owned()),
)
if @utf8.encode(text[:]).length() > self.options.max_string_bytes {
raise InvalidBinary(start, "DMX string exceeds budget")
}
return DToken(text)
}
if current == '\\' && !escaped {
escaped = true
} else {
escaped = false
}
}
raise InvalidBinary(start, "Unclosed DMX string")
}
DEnd
}
///|
fn DMXTextReader::peek(self : DMXTextReader) -> DMXToken raise {
let token = self.token()
self.pending = Some(token)
token
}
///|
fn DMXTextReader::string(self : DMXTextReader) -> String raise {
guard self.token() is DToken(text) else {
raise InvalidBinary(self.pos, "Expected DMX string")
}
text
}
///|
fn DMXTextReader::expect(self : DMXTextReader, c : Char) -> Unit raise {
if self.token() != DPunctuation(c) {
raise InvalidBinary(self.pos, "Unexpected DMX punctuation")
}
}
///|
fn dmx_float_bits(text : String) -> UInt32 raise {
match text.to_lower() {
"nan" => 0x7fc00000U
"inf" | "+inf" | "infinity" => 0x7f800000U
"-inf" | "-infinity" => 0xff800000U
_ => {
let number = @strconv.parse_double(text[:])
if number == 0.0 && text.trim_start().has_prefix("-") {
0x80000000U
} else {
Float::from_double(number).reinterpret_as_uint()
}
}
}
}
///|
fn dmx_words(text : String) -> Array[String] {
text
.replace_all(old="\t", new=" ")
.replace_all(old="\r", new=" ")
.replace_all(old="\n", new=" ")
.split(" ")
.filter(s => !s.is_empty())
.map(s => s.to_owned())
.collect()
}
///|
fn dmx_scalar_text(tag : Int32, text : String) -> DMXValue raise {
match tag {
1 => DElement(if text.is_empty() { None } else { Some(dmx_uuid(text)) })
2 => DInt(@strconv.parse_int(text[:]))
3 => DFloatBits(dmx_float_bits(text))
4 =>
match text.to_lower() {
"1" | "true" => DBool(true)
"0" | "false" => DBool(false)
_ => raise InvalidBinary(0, "Invalid DMX boolean")
}
5 => DString(text)
6 => {
let chars = text
.to_array()
.filter(c => ![' ', '\t', '\n', '\r'].contains(c))
if chars.length() % 2 != 0 {
raise InvalidBinary(0, "Odd DMX blob hex length")
}
let output = @buffer.Buffer()
for i in 0..<(chars.length() / 2) {
guard kv3_hex(chars[2 * i]) is Some(a) &&
kv3_hex(chars[2 * i + 1]) is Some(b) else {
raise InvalidBinary(0, "Invalid DMX blob hex")
}
output.write_byte((a * 16 + b).to_byte())
}
DBinary(output.to_bytes())
}
7 => {
let ticks = @strconv.parse_double(text[:]) * 10000.0
if ticks.is_nan() || ticks < -2147483648.0 || ticks > 2147483647.0 {
raise InvalidBinary(0, "DMX time exceeds tick range")
}
DTime(ticks.round().to_int())
}
8 => {
let parts = dmx_words(text)
if parts.length() != 4 {
raise InvalidBinary(0, "DMX color requires RGBA")
}
let mut bits = 0U
for i in 0..<4 {
let n = @strconv.parse_int(parts[i][:])
if n < 0 || n > 255 {
raise InvalidBinary(0, "DMX color channel outside byte range")
}
bits = bits | (n.reinterpret_as_uint() << (i * 8))
}
DColor(bits)
}
9..<=14 => {
let parts = dmx_words(text)
if parts.length() != [2, 3, 4, 3, 4, 16][tag - 9] {
raise InvalidBinary(0, "DMX vector dimension mismatch")
}
DVector(tag, parts.map(dmx_float_bits))
}
_ => raise InvalidBinary(0, "Unsupported DMX text type")
}
}
///|
fn DMXTextReader::element(
self : DMXTextReader,
kind : String,
depth : Int32,
) -> String raise {
if depth > self.options.max_depth ||
self.elements.length() >= self.options.max_entries {
raise InvalidBinary(self.pos, "DMX element/depth budget exceeded")
}
self.expect('{')
let index = self.elements.length()
let placeholder = "#" + index.to_string()
self.elements.push({ id: placeholder, name: "", kind, attributes: [], })
let mut id = placeholder
let mut name = ""
let mut named = false
let attributes : Array[DMXAttribute] = []
while self.peek() != DPunctuation('}') {
let key = self.string()
let type_name = self.string()
let lower = type_name.to_lower()
if key == "id" && lower == "elementid" {
if id != placeholder {
raise InvalidBinary(self.pos, "Duplicate DMX element id")
}
id = dmx_uuid(self.string())
continue
}
if key == "name" {
if lower != "string" || named {
raise InvalidBinary(self.pos, "Invalid/duplicate DMX element name")
}
name = self.string()
named = true
continue
}
self.values += 1
if self.values > self.options.max_entries {
raise InvalidBinary(self.pos, "DMX value budget exceeded")
}
let array = lower.has_suffix("_array")
let base = if array { lower[:lower.length() - 6].to_owned() } else { lower }
let mut tag = 0
let types = dmx_types()
for i in 1.. self.options.max_entries {
raise InvalidBinary(self.pos, "DMX array budget exceeded")
}
let value = if tag == 1 {
let token = self.string()
if token.to_lower() == "element" {
dmx_scalar_text(1, self.string())
} else {
DElement(Some(self.element(token, depth + 1)))
}
} else {
dmx_scalar_text(tag, self.string())
}
values.push(value)
if self.peek() == DPunctuation(',') {
self.token() |> ignore
}
}
self.expect(']')
DArray(tag, values)
} else {
dmx_scalar_text(tag, self.string())
}
attributes.push({ name: key, value, })
}
self.expect('}')
self.elements[index] = { id, name, kind, attributes, }
id
}
///|
fn dmx_fix_reference(value : DMXValue, ids : Map[String, String]) -> DMXValue {
match value {
DElement(Some(id)) => DElement(Some(ids.get(id).unwrap_or(id)))
DArray(tag, values) =>
DArray(tag, values.map(v => dmx_fix_reference(v, ids)))
_ => value
}
}
///|
fn dmx_read_text(
text : String,
format_name : String,
format_version : Int32,
options : BinaryOptions,
) -> DMXDocument raise {
let reader = DMXTextReader::{
chars: text.to_array(),
pos: 0,
pending: None,
elements: [],
options,
values: 0,
}
while reader.peek() != DEnd {
reader.element(reader.string(), 0) |> ignore
}
if reader.elements.is_empty() {
raise InvalidBinary(0, "Empty DMX graph")
}
let existing : Map[String, Bool] = Map([])
for element in reader.elements {
if !element.id.has_prefix("#") {
existing[element.id] = true
}
}
let generated : Map[String, String] = Map([])
let mut ordinal = 1
for element in reader.elements {
if element.id.has_prefix("#") {
while true {
let hex = ordinal.to_string(radix=16)
ordinal += 1
let candidate = "00000000-0000-4000-8000-" +
String::make(12 - hex.length(), '0') +
hex
if !existing.contains(candidate) {
existing[candidate] = true
generated[element.id] = candidate
break
}
}
}
}
let elements = reader.elements.map(e => {
..e,
id: generated.get(e.id).unwrap_or(e.id),
attributes: e.attributes.map(a => {
..a,
value: dmx_fix_reference(a.value, generated),
}),
})
let doc = { format_name, format_version, root: elements[0].id, elements, }
dmx_validate(doc, options)
doc
}
///|
pub fn read_dmx(
bytes : Bytes,
options~ : BinaryOptions = BinaryOptions::default(),
) -> DMXDocument raise {
check_binary_options(options)
if bytes.length() > options.max_bytes {
raise InvalidBinary(0, "DMX input exceeds byte budget")
}
let mut end = 0
while end + 2 < bytes.length() &&
end < 512 &&
bytes[end:end + 3].to_owned() != b"-->" {
end += 1
}
if end + 2 >= bytes.length() || end >= 512 {
raise InvalidBinary(0, "Missing/oversized DMX header")
}
let header = @utf8.decode(bytes[:end], ignore_bom=false)
if !header.has_prefix("\n",
)
let elements = [document.element(document.root).unwrap()] +
document.elements.filter(e => e.id != document.root)
let types = dmx_types()
for element in elements {
output.write_string(
po_quote(element.kind) +
"\n{\n\t\"id\" \"elementid\" " +
po_quote(element.id) +
"\n\t\"name\" \"string\" " +
po_quote(element.name) +
"\n",
)
for attribute in element.attributes {
output.write_string("\t" + po_quote(attribute.name) + " ")
let tag = dmx_type(attribute.value)
if attribute.value is DArray(_, values) {
output.write_string(po_quote(types[tag] + "_array") + "\n\t[\n")
for value in values {
output.write_string(
"\t\t" +
(if tag == 1 { "\"element\" " } else { "" }) +
po_quote(dmx_scalar_string(value)) +
",\n",
)
}
output.write_string("\t]\n")
} else {
output.write_string(
po_quote(types[tag]) +
" " +
po_quote(dmx_scalar_string(attribute.value)) +
"\n",
)
}
}
output.write_string("}\n")
}
let text = output.to_string()
if @utf8.encode(text[:]).length() > options.max_bytes {
raise InvalidBinary(0, "DMX text output exceeds byte budget")
}
text
}