///|
pub suberror ReplayError {
InvalidCommand(String)
}
///|
pub struct Replay {
engine : @engine.Engine
priv mut now_ms : Int64
priv seed : UInt32
priv mut started : Bool
priv mut configured : Bool
}
///|
pub fn Replay::new(seed~ : UInt32 = 1U) -> Replay raise {
{
engine: @engine.Engine::new(seed~),
now_ms: 0,
seed,
started: false,
configured: false,
}
}
///|
fn field(object : Map[String, Json], key : String) -> Json raise ReplayError {
match object.get(key) {
Some(value) => value
None => raise InvalidCommand("missing field: " + key)
}
}
///|
fn text(object : Map[String, Json], key : String) -> String raise {
@json.from_json(field(object, key))
}
///|
pub fn Replay::step_line(self : Replay, line : String) -> Array[Json] raise {
guard line.length() <= 32768 else {
raise InvalidCommand("line exceeds 32768 characters")
}
let object : Map[String, Json] = @json.from_json(
@json.parse(line, max_nesting_depth=16, reject_duplicate_keys=true),
)
let kind = text(object, "kind")
let numeric_time : Double = @json.from_json(field(object, "at_ms"))
guard numeric_time >= 0.0 && numeric_time <= 9007199254740991.0 else {
raise InvalidCommand("time outside exact JSON integer range")
}
let at = numeric_time.to_int64()
guard at.to_double() == numeric_time else {
raise InvalidCommand("fractional replay time")
}
guard at >= self.now_ms && at <= 9223367741887480807L else {
raise InvalidCommand("non-monotonic or overflowing time")
}
let output : Array[Json] = []
match kind {
"config" => {
guard !self.started &&
!self.configured &&
at == 0L &&
read_config_seed(object) == self.seed else {
raise InvalidCommand(
"config must be first, unique and match the replay seed",
)
}
self.configured = true
output.push(
Json::object({
"event": Json("configured"),
"seed": Json(self.seed),
"at_ms": Json(0),
}),
)
}
"browse" => {
let id = self.engine.browse(
@model.DnsName::parse(text(object, "name")),
text(object, "scope"),
at,
)
output.push(
Json::object({
"event": Json("subscribed"),
"id": Json(id),
"at_ms": Json(at.to_double()),
}),
)
}
"resolve" => {
let timeout : Int32 = match object.get("timeout_ms") {
Some(value) => @json.from_json(value)
None => 5000
}
let id = self.engine.resolve(
@model.DnsName::parse(text(object, "name")),
text(object, "scope"),
at,
timeout_ms=timeout,
)
output.push(
Json::object({
"event": Json("resolving"),
"id": Json(id),
"at_ms": Json(at.to_double()),
}),
)
}
"packet" => {
let scope = text(object, "scope")
let packet = @model.bytes_from_hex(text(object, "packet_hex"))
self.engine.ingest(packet, scope, at) catch {
error => {
self.engine.advance(at)
output.push(
Json::object({
"event": Json("diagnostic"),
"code": Json("packet_rejected"),
"detail": Json("\{error}"),
"at_ms": Json(at.to_double()),
}),
)
}
}
}
"advance" => self.engine.advance(at)
"cancel" => {
let id : Int32 = @json.from_json(field(object, "id"))
self.engine.cancel(id, at)
}
"close" => {
self.engine.advance(at)
self.engine.close()
}
_ => raise InvalidCommand("unknown kind: " + kind)
}
self.now_ms = at
if kind != "config" {
self.started = true
}
self.drain_events(output, at)
while self.engine.poll_outbound() is Some(packet) {
self.drain_events(output, at)
output.push(
Json::object({
"event": Json("outbound"),
"at_ms": Json(at.to_double()),
"scope": Json(packet.scope),
"rr_type": Json(packet.rr_type),
"packet_hex": Json(@model.bytes_hex(packet.bytes)),
}),
)
}
self.drain_events(output, at)
output
}
///|
fn Replay::drain_events(
self : Replay,
output : Array[Json],
at : Int64,
) -> Unit {
while self.engine.poll_event() is Some(event) {
let json = event_json(event)
if json is Json::Object(fields) {
fields["at_ms"] = Json(at.to_double())
}
output.push(json)
}
}
///|
fn read_config_seed(object : Map[String, Json]) -> UInt32 raise {
let kind = text(object, "kind")
let format : Int32 = @json.from_json(field(object, "format"))
guard kind == "config" && format == 1 else {
raise InvalidCommand("unsupported trace configuration/format")
}
let value : Double = @json.from_json(field(object, "seed"))
guard value >= 0.0 && value <= 4294967295.0 else {
raise InvalidCommand("seed outside UInt32 range")
}
let n = value.to_int64()
guard n.to_double() == value else {
raise InvalidCommand("fractional replay seed")
}
n.to_int().reinterpret_as_uint()
}
///|
pub fn Replay::from_config(line : String) -> Replay raise {
guard line.length() <= 32768 else {
raise InvalidCommand("config line too long")
}
let object : Map[String, Json] = @json.from_json(
@json.parse(line, max_nesting_depth=16, reject_duplicate_keys=true),
)
Replay::new(seed=read_config_seed(object))
}