// etcd serves the same messages over a second transport: its HTTP/JSON gateway,
// where a message is a JSON object named the way the .proto names its fields, with
// 64-bit integers as decimal strings, `bytes` as standard base64, and enumerations
// by name (https://protobuf.dev/programming-guides/json/). The tables in
// `gateway_fields*.mbt` describe each message once; this file walks the protobuf
// wire form with them in either direction, so requests and replies share one
// description and neither can drift from the other.
///|
/// How a field appears in the gateway's JSON, which is also how to read it off the
/// wire: a scalar maps to a JSON type, a message to its own name, an enumeration to
/// its names indexed by number.
priv enum FieldKind {
Str
Bytes
Bool
Int64
UInt32
UInt64
Enum(Array[String])
Message(String)
Rep(FieldKind)
}
///|
/// One field of a message: the number it carries on the wire and the name it
/// carries in the JSON. `presence` marks the arms of a oneof, which are written
/// even when they hold the default value — otherwise the arm would not be selected.
priv struct JsonField {
number : Int32
name : String
kind : FieldKind
presence : Bool
}
///|
/// A body the gateway cannot frame. The code is the one the gateway would have
/// answered with for the same input: 3 for a request it cannot read, 13 for a reply
/// that does not decode.
fn gateway_error(code : Int32, message : String) -> EtcdError {
EtcdError::Refused(Status::{ code, message, })
}
///|
/// The protobuf wire reader with its failures turned into gateway errors, so the
/// read side below raises one error type and says nothing about who broke the bytes.
priv struct WireReader {
inner : @proto.PbReader
}
///|
fn WireReader::new(bytes : Bytes) -> WireReader {
{ inner: @proto.PbReader::new(bytes), }
}
///|
fn WireReader::eof(self : WireReader) -> Bool {
self.inner.eof()
}
///|
fn WireReader::tag(
self : WireReader,
) -> (Int32, @proto.WireType) raise EtcdError {
self.inner.read_tag() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::skip(
self : WireReader,
wire : @proto.WireType,
) -> Unit raise EtcdError {
self.inner.skip(wire) catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::string(self : WireReader) -> String raise EtcdError {
self.inner.read_string() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::bytes(self : WireReader) -> Bytes raise EtcdError {
self.inner.read_bytes() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::block(self : WireReader) -> Bytes raise EtcdError {
self.inner.read_len_delim() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::flag(self : WireReader) -> Bool raise EtcdError {
self.inner.read_bool() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::int32(self : WireReader) -> Int32 raise EtcdError {
self.inner.read_int32() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::int64(self : WireReader) -> Int64 raise EtcdError {
self.inner.read_int64() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::uint32(self : WireReader) -> UInt32 raise EtcdError {
self.inner.read_uint32() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::uint64(self : WireReader) -> UInt64 raise EtcdError {
self.inner.read_uint64() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
fn WireReader::varint(self : WireReader) -> UInt64 raise EtcdError {
self.inner.read_varint() catch {
_ => raise gateway_error(13, "gateway: the reply is not protobuf wire")
}
}
///|
/// The field numbered `number`, if this message has one.
fn field_at(fields : Array[JsonField], number : Int32) -> JsonField? {
for field in fields {
if field.number == number {
return Some(field)
}
}
None
}
///|
/// One message in the gateway's JSON, from its protobuf wire form.
fn message_json(message : String, wire : Bytes) -> Json raise EtcdError {
let fields = gateway_fields(message)
let values : Map[String, Json] = Map([])
let lists : Map[String, Array[Json]] = Map([])
let reader = WireReader::new(wire)
while !reader.eof() {
let (number, wire_type) = reader.tag()
match field_at(fields, number) {
None => reader.skip(wire_type)
Some(field) =>
match field.kind {
Rep(item) => {
let items = match lists.get(field.name) {
Some(items) => items
None => {
let items : Array[Json] = []
lists.set(field.name, items)
items
}
}
append_values(item, reader, wire_type, items)
}
kind => values.set(field.name, read_value(kind, reader))
}
}
}
let object : Map[String, Json] = Map([])
for field in fields {
match values.get(field.name) {
Some(value) => object.set(field.name, value)
None =>
match lists.get(field.name) {
Some(items) =>
if items.length() > 0 {
object.set(field.name, Json::array(items))
}
None => ()
}
}
}
Json::object(object)
}
///|
/// Append the value of one repetition of a repeated field. A repeated scalar arrives
/// packed from a conforming encoder, and etcd's own are; an unpacked one is read too.
fn append_values(
item : FieldKind,
reader : WireReader,
wire_type : @proto.WireType,
items : Array[Json],
) -> Unit raise EtcdError {
match wire_type {
LengthDelim =>
match item {
Int64 | UInt32 | UInt64 | Bool | Enum(_) => {
let packed = WireReader::new(reader.block())
while !packed.eof() {
items.push(read_packed(item, packed))
}
}
_ => items.push(read_value(item, reader))
}
_ => items.push(read_value(item, reader))
}
}
///|
/// One element of a packed repeated scalar.
fn read_packed(item : FieldKind, reader : WireReader) -> Json raise EtcdError {
match item {
Int64 => Json::string(reader.int64().to_string())
UInt32 => Json::number(reader.uint32().to_double())
UInt64 => Json::string(reader.uint64().to_string())
Bool => Json::boolean(reader.varint() != 0UL)
Enum(names) => Json::string(enum_name(names, reader.int32()))
_ => raise gateway_error(13, "gateway: packed field is not a scalar")
}
}
///|
/// One value of a field.
fn read_value(kind : FieldKind, reader : WireReader) -> Json raise EtcdError {
match kind {
Str => Json::string(reader.string())
Bytes => Json::string(@base64.encode(reader.bytes()[:]))
Bool => Json::boolean(reader.flag())
Int64 => Json::string(reader.int64().to_string())
UInt32 => {
let n = reader.uint32()
Json::number(n.to_double(), repr=n.to_string())
}
UInt64 => Json::string(reader.uint64().to_string())
Enum(names) => Json::string(enum_name(names, reader.int32()))
Message(name) => message_json(name, reader.block())
Rep(_) => raise gateway_error(13, "gateway: a repetition reads one value")
}
}
///|
/// The name of an enumeration value. A number the table does not know stays a
/// number, which is what the JSON mapping does for unknown values.
fn enum_name(names : Array[String], number : Int32) -> String {
if number >= 0 && number < names.length() {
names[number]
} else {
number.to_string()
}
}
///|
/// The wire form of one message, from its gateway JSON.
fn message_wire(message : String, json : Json) -> Bytes raise EtcdError {
let fields = gateway_fields(message)
let object = match json {
Object(entries) => entries
_ =>
raise gateway_error(
3,
"gateway: " + message + " is expected as an object",
)
}
let writer = @proto.PbWriter::new()
for field in fields {
match object.get(field.name) {
None => ()
Some(Null) => ()
Some(value) =>
match field.kind {
Rep(item) =>
match value {
Array(items) =>
for element in items {
write_value(writer, field, item, element)
}
_ =>
raise gateway_error(
3,
"gateway: " +
message +
"." +
field.name +
" is expected as an array",
)
}
kind => write_value(writer, field, kind, value)
}
}
}
writer.to_bytes()
}
///|
/// Write one value of a field into the message being built. A scalar holding the
/// default is left out, as every protobuf encoder does, unless the field is an arm
/// of a oneof — there the arm has to be present to be selected.
fn write_value(
writer : @proto.PbWriter,
field : JsonField,
kind : FieldKind,
value : Json,
) -> Unit raise EtcdError {
match kind {
Str => {
let text = text_of(value, field.name)
if field.presence || text != "" {
writer.string_(field.number, text)
}
}
Bytes => {
let raw = bytes_of(value, field.name)
if field.presence || raw.length() > 0 {
writer.bytes_(field.number, raw)
}
}
Bool => {
let flag = match value {
True => true
False => false
_ =>
raise gateway_error(
3,
"gateway: " + field.name + " is expected as a bool",
)
}
if field.presence || flag {
writer.bool_(field.number, flag)
}
}
Int64 => {
let n = int64_of(value, field.name)
if field.presence || n != 0L {
writer.int64(field.number, n)
}
}
UInt32 => {
let n = uint64_of(value, field.name).to_uint()
if field.presence || n != 0U {
writer.uint32(field.number, n)
}
}
UInt64 => {
let n = uint64_of(value, field.name)
if field.presence || n != 0UL {
writer.uint64(field.number, n)
}
}
Enum(names) => {
let n = enum_number(names, value, field.name)
if field.presence || n != 0 {
writer.enum_(field.number, n)
}
}
Message(name) => writer.message_(field.number, message_wire(name, value))
Rep(_) => raise gateway_error(3, "gateway: a repetition writes one value")
}
}
///|
/// A string value.
fn text_of(value : Json, field : String) -> String raise EtcdError {
match value {
String(text) => text
_ =>
raise gateway_error(3, "gateway: " + field + " is expected as a string")
}
}
///|
/// A `bytes` value: standard base64, padded, as the JSON mapping requires.
fn bytes_of(value : Json, field : String) -> Bytes raise EtcdError {
let text = text_of(value, field)
if text == "" {
return b""
}
@base64.decode(text[:]) catch {
_ => raise gateway_error(3, "gateway: " + field + " is not base64")
}
}
///|
/// The decimal text of a 64-bit value: the JSON mapping writes it as a string and an
/// encoder that writes a number instead is accepted too. `Double` cannot hold every
/// 64-bit value, so the text comes from the JSON literal rather than from a number
/// parsed out of it.
fn decimal_of(value : Json, field : String) -> String raise EtcdError {
match value {
String(text) => text
Number(_, ..) => Json::stringify(value)
_ =>
raise gateway_error(3, "gateway: " + field + " is expected as a number")
}
}
///|
fn int64_of(value : Json, field : String) -> Int64 raise EtcdError {
let (negative, magnitude) = parse_digits(
decimal_of(value, field),
field,
true,
)
let magnitude = magnitude.reinterpret_as_int64()
if negative {
0L - magnitude
} else {
magnitude
}
}
///|
fn uint64_of(value : Json, field : String) -> UInt64 raise EtcdError {
parse_digits(decimal_of(value, field), field, false).1
}
///|
/// The sign and magnitude of a decimal integer, refusing a value wider than the
/// field: a lease id or a revision that silently changes is worse than a failed call.
/// An `int64` reaches 2^63 either way and a `uint64` reaches 2^64 - 1.
fn parse_digits(
text : String,
field : String,
signed : Bool,
) -> (Bool, UInt64) raise EtcdError {
let digits = text.to_array()
let mut i = 0
let mut negative = false
if i < digits.length() {
let first = digits[i].to_int()
if first == 45 || first == 43 {
guard signed else {
raise gateway_error(3, "gateway: " + field + " is not negative")
}
negative = first == 45
i = i + 1
}
}
guard i < digits.length() else {
raise gateway_error(3, "gateway: " + field + " has no digits")
}
let limit : UInt64 = if !signed {
18446744073709551615UL
} else if negative {
9223372036854775808UL
} else {
9223372036854775807UL
}
let width = if signed { "64 bits of a signed integer" } else { "64 bits" }
let mut magnitude = 0UL
while i < digits.length() {
let digit = digits[i].to_int()
guard digit >= 48 && digit <= 57 else {
raise gateway_error(3, "gateway: " + field + " is not decimal")
}
magnitude = magnitude * 10UL + (digit - 48).to_uint64()
guard magnitude <= limit else {
raise gateway_error(3, "gateway: " + field + " overflows " + width)
}
i = i + 1
}
(negative, magnitude)
}
///|
/// The number of an enumeration value: its name, or a number for a value the table
/// does not name.
fn enum_number(
names : Array[String],
value : Json,
field : String,
) -> Int32 raise EtcdError {
match value {
String(text) => {
for i = 0; i < names.length(); i = i + 1 {
if names[i] == text {
return i
}
}
int64_of(Json::string(text), field).to_int()
}
_ => int64_of(value, field).to_int()
}
}