///|
/// Helpers for the HELLO handshake negotiation of the SPOP protocol.
///|
/// The SPOP protocol version implemented by this library.
pub let supported_version : String = "2.0"
///|
/// Minimum acceptable max-frame-size (256 bytes, per the SPOE spec).
pub let min_frame_size : UInt = 256
///|
/// Parse a comma-separated list such as the values of the
/// "supported-versions" and "capabilities" HELLO keys. ASCII whitespace
/// around items is ignored and empty items are dropped.
pub fn parse_list(s : String) -> Array[String] {
let items = []
for part in s.split(",") {
let item = part.trim().to_owned()
if item != "" {
items.push(item)
}
}
items
}
///|
/// The major version of a "Major.Minor" version string; `None` if the
/// string does not start with a decimal number.
fn major_version(version : String) -> Int? {
let mut major = 0
let mut seen_digit = false
for c in version {
if c == '.' {
break
}
if c is ('0'..='9') {
major = major * 10 + (c.to_int() - '0'.to_int())
seen_digit = true
} else {
return None
}
}
if seen_digit {
Some(major)
} else {
None
}
}
///|
/// Choose the protocol version to use given the versions announced by the
/// peer in its "supported-versions" HELLO key. Versions have the form
/// "Major.Minor"; announcing a major version implies support for all its
/// minor versions. This library supports version "2.0", so any announced
/// version with major version 2 matches. Raises
/// `StatusCode::UnsupportedVersion` when there is no common version.
pub fn choose_version(announced : Array[String]) -> String raise SpopError {
for version in announced {
if major_version(version) is Some(2) {
return supported_version
}
}
raise SpopError::new(
UnsupportedVersion,
"no common SPOP version; peer announced: \{announced.join(", ")}",
)
}
///|
/// Compute the effective max-frame-size from our own and the peer's
/// announced value: the minimum of both. Raises
/// `StatusCode::InvalidMaxFrameSize` if the result is smaller than 256.
pub fn negotiate_max_frame_size(
ours : UInt,
theirs : UInt,
) -> UInt raise SpopError {
let effective = if ours < theirs { ours } else { theirs }
if effective < min_frame_size {
raise SpopError::new(
InvalidMaxFrameSize,
"max-frame-size \{effective} is smaller than the minimum \{min_frame_size}",
)
}
effective
}
///|
/// The parameters extracted from a HELLO frame (either direction).
pub(all) struct HelloInfo {
versions : Array[String]
max_frame_size : UInt
capabilities : Array[String]
healthcheck : Bool
engine_id : String?
} derive(Eq, Debug)
///|
/// Parse the key-value list of a HELLO frame. Missing required keys raise
/// the corresponding status code (`VersionNotFound`,
/// `MaxFrameSizeNotFound`, `CapabilitiesNotFound`); keys of the wrong type
/// raise `InvalidFrame`. Unknown keys are ignored.
pub fn parse_hello_args(
args : Array[(String, Data)],
) -> HelloInfo raise SpopError {
let mut versions : Array[String]? = None
let mut max_frame_size : UInt? = None
let mut capabilities : Array[String]? = None
let mut healthcheck = false
let mut engine_id : String? = None
for kv in args {
match kv {
("supported-versions", Str(s)) => versions = Some(parse_list(s))
("supported-versions", _) =>
raise SpopError::new(
InvalidFrame,
"supported-versions must be a STRING",
)
("max-frame-size", UInt32(v)) => max_frame_size = Some(v)
("max-frame-size", _) =>
raise SpopError::new(InvalidFrame, "max-frame-size must be a UINT32")
("capabilities", Str(s)) => capabilities = Some(parse_list(s))
("capabilities", _) =>
raise SpopError::new(InvalidFrame, "capabilities must be a STRING")
("healthcheck", Bool(v)) => healthcheck = v
("engine-id", Str(s)) => engine_id = Some(s)
_ => ()
}
}
guard versions is Some(versions) else {
raise SpopError::new(
VersionNotFound,
"missing supported-versions key in HELLO frame",
)
}
guard max_frame_size is Some(max_frame_size) else {
raise SpopError::new(
MaxFrameSizeNotFound,
"missing max-frame-size key in HELLO frame",
)
}
guard capabilities is Some(capabilities) else {
raise SpopError::new(
CapabilitiesNotFound,
"missing capabilities key in HELLO frame",
)
}
{ versions, max_frame_size, capabilities, healthcheck, engine_id, }
}
///|
/// Build the key-value list of a HELLO frame announcing `versions`,
/// `max_frame_size` and `capabilities`, plus optional `healthcheck` and
/// `engine_id`.
pub fn hello_args(
versions? : Array[String] = [supported_version],
max_frame_size? : UInt = default_max_frame_size,
capabilities? : Array[String] = [],
healthcheck? : Bool = false,
engine_id? : String? = None,
) -> Array[(String, Data)] {
let args : Array[(String, Data)] = [
("supported-versions", Str(versions.join(", "))),
("max-frame-size", UInt32(max_frame_size)),
("capabilities", Str(capabilities.join(", "))),
]
if healthcheck {
args.push(("healthcheck", Bool(true)))
}
if engine_id is Some(id) {
args.push(("engine-id", Str(id)))
}
args
}