///|
fn word_hex(value : UInt32) -> String raise OscError {
let w = Writer::{ out: [], limits: Limits::default(), }
w.word(value)
hex_encode(Bytes::from_array(w.out))
}
///|
fn wide_hex(value : UInt64) -> String raise OscError {
let w = Writer::{ out: [], limits: Limits::default(), }
w.wide(value)
hex_encode(Bytes::from_array(w.out))
}
///|
fn argument_json(arg : Argument) -> Json raise OscError {
let tag = match arg {
Array(_) => "["
_ => argument_tag(arg).to_string()
}
match arg {
Array(items) =>
@json.to_json([
@json.to_json("["),
@json.to_json(items.map(argument_json)),
])
True | False | Nil | Impulse => @json.to_json([@json.to_json(tag)])
_ => {
let value = match arg {
I32(n) => word_hex(n.reinterpret_as_uint())
F32(n) | Rgba(n) | Midi(n) => word_hex(n)
I64(n) => wide_hex(n.reinterpret_as_uint64())
F64(n) => wide_hex(n)
Time(t) => wide_hex(t.bits())
Character(c) => word_hex(c.reinterpret_as_uint())
Text(s) | Symbol(s) => s
Blob(b) => hex_encode(b, max_bytes=16777216)
_ => abort("unreachable array or payloadless argument")
}
@json.to_json([@json.to_json(tag), @json.to_json(value)])
}
}
}
///|
fn packet_json(packet : Packet) -> Json raise OscError {
match packet {
Message(m) =>
@json.to_json([
@json.to_json("message"),
@json.to_json(m.address),
@json.to_json(m.arguments.map(argument_json)),
])
Bundle(t, children) =>
@json.to_json([
@json.to_json("bundle"),
@json.to_json(wide_hex(t.bits())),
@json.to_json(children.map(packet_json)),
])
}
}
///|
/// Versioned tagged-array schema. Integers and IEEE/NTP bits are fixed-width hex,
/// never JSON numbers: NaN payloads, signed zero and all 64-bit values survive.
pub fn to_json(
packet : Packet,
limits~ : Limits = Limits::default(),
) -> Json raise OscError {
let validated = decode(encode(packet, limits~), limits~)
@json.to_json([@json.to_json("moonosc/1"), packet_json(validated)])
}
///|
fn json_array(value : Json) -> Array[Json] raise OscError {
match value {
Json::Array(items) => items
_ => raise Invalid("JSON array expected", 0)
}
}
///|
fn json_string(value : Json) -> String raise OscError {
match value {
Json::String(s) => s
_ => raise Invalid("JSON string expected", 0)
}
}
///|
fn parse_hex_word(text : String) -> UInt32 raise OscError {
if text.length() != 8 {
raise Invalid("expected 8 hex digits", 0)
}
let bytes = hex_decode(text, max_bytes=4)
Reader::{ data: bytes, pos: 0, end: 4, }.word()
}
///|
fn parse_hex_wide(text : String) -> UInt64 raise OscError {
if text.length() != 16 {
raise Invalid("expected 16 hex digits", 0)
}
let bytes = hex_decode(text, max_bytes=8)
Reader::{ data: bytes, pos: 0, end: 8, }.wide()
}
///|
fn argument_from_json(
value : Json,
depth : Int32,
limits : Limits,
budget : Budget,
) -> Argument raise OscError {
if depth > limits.depth {
raise Limit("JSON argument depth")
}
budget.argument(limits)
let items = json_array(value)
if items.length() < 1 || items.length() > 2 {
raise Invalid("JSON argument arity", 0)
}
let tag = json_string(items[0])
if tag == "T" || tag == "F" || tag == "N" || tag == "I" {
if items.length() != 1 {
raise Invalid("payloadless JSON tag", 0)
}
return match tag {
"T" => True
"F" => False
"N" => Nil
_ => Impulse
}
}
if items.length() != 2 {
raise Invalid("JSON argument payload required", 0)
}
if tag == "[" {
let children = json_array(items[1])
if children.length() > limits.arguments - budget.args {
raise Limit("JSON arguments")
}
return Array(
children.map(x => argument_from_json(x, depth + 1, limits, budget)),
)
}
let text = json_string(items[1])
match tag {
"i" => I32(parse_hex_word(text).reinterpret_as_int())
"f" => F32(parse_hex_word(text))
"h" => I64(parse_hex_wide(text).reinterpret_as_int64())
"d" => F64(parse_hex_wide(text))
"t" => Time(TimeTag::from_bits(parse_hex_wide(text)))
"r" => Rgba(parse_hex_word(text))
"m" => Midi(parse_hex_word(text))
"c" => {
let c = parse_hex_word(text)
if c > 127U {
raise Invalid("ASCII character required", 0)
}
Character(c.reinterpret_as_int())
}
"s" | "S" => {
if text.length() > limits.string_bytes {
raise Limit("JSON string bytes")
}
if !ascii_valid(text) {
raise Invalid("ASCII string required", 0)
}
if tag == "s" {
Text(text)
} else {
Symbol(text)
}
}
"b" => Blob(hex_decode(text, max_bytes=limits.blob_bytes))
_ => raise Invalid("unknown JSON type tag", 0)
}
}
///|
fn packet_from_json(
value : Json,
depth : Int32,
limits : Limits,
budget : Budget,
) -> Packet raise OscError {
if depth > limits.depth {
raise Limit("JSON bundle depth")
}
budget.node(limits)
let items = json_array(value)
if items.length() != 3 {
raise Invalid("JSON packet arity", 0)
}
let kind = json_string(items[0])
let header = json_string(items[1])
let children = json_array(items[2])
match kind {
"message" => {
if header.length() > limits.string_bytes {
raise Limit("JSON address bytes")
}
validate_address(header)
if children.length() > limits.arguments - budget.args {
raise Limit("JSON arguments")
}
Message({
address: header,
arguments: children.map(x => argument_from_json(x, 1, limits, budget)),
})
}
"bundle" => {
let t = TimeTag::from_bits(parse_hex_wide(header))
if children.length() > limits.elements - budget.nodes {
raise Limit("JSON packet nodes")
}
Bundle(
t,
children.map(x => packet_from_json(x, depth + 1, limits, budget)),
)
}
_ => raise Invalid("unknown JSON packet kind", 0)
}
}
///|
pub fn from_json(
value : Json,
limits~ : Limits = Limits::default(),
) -> Packet raise OscError {
limits.validate()
let root = json_array(value)
if root.length() != 2 || json_string(root[0]) != "moonosc/1" {
raise Invalid("JSON schema version", 0)
}
let packet = packet_from_json(root[1], 1, limits, Budget::{
args: 0,
nodes: 0,
})
ignore(encode(packet, limits~))
packet
}
///|
/// Text parsing has both a byte/code-unit cap and parser nesting cap.
// Core 20260920 deprecates this safety bound without a replacement.
#warnings("-20")
pub fn parse_json(
text : String,
limits~ : Limits = Limits::default(),
) -> Packet raise OscError {
limits.validate()
if text.length() > limits.packet_bytes * 4 + 65536 {
raise Limit("JSON text size")
}
let value = @json.parse(text[:], max_nesting_depth=limits.depth * 4 + 8) catch {
_ => raise Invalid("malformed JSON text", 0)
}
from_json(value, limits~)
}