summaryrefslogtreecommitdiff
path: root/mcproto
diff options
context:
space:
mode:
authorLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:11:21 +0200
committerLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:51:35 +0200
commitae44478b30d890fe0fb04022f44d474dcdcc3f9d (patch)
tree5f462459ae4b47d22114eed717d1382d08cf4dfe /mcproto
Initial commit (import old repo)HEADmain
Diffstat (limited to 'mcproto')
-rw-r--r--mcproto/+test.ha47
-rw-r--r--mcproto/decode.ha193
-rw-r--r--mcproto/encode.ha51
-rw-r--r--mcproto/error.ha1
-rw-r--r--mcproto/frame.ha22
-rw-r--r--mcproto/handshake.ha46
-rw-r--r--mcproto/packet.ha13
-rw-r--r--mcproto/status/ids.ha5
-rw-r--r--mcproto/status/ping.ha9
-rw-r--r--mcproto/status/response.ha111
10 files changed, 498 insertions, 0 deletions
diff --git a/mcproto/+test.ha b/mcproto/+test.ha
new file mode 100644
index 0000000..5ce070c
--- /dev/null
+++ b/mcproto/+test.ha
@@ -0,0 +1,47 @@
+use bytes;
+use trace;
+
+fn varint_roundtrip(value: i32, bytes: []u8) void = {
+ let buf: []u8 = [];
+ encode_varint(&buf, value);
+ assert(bytes::equal(buf, bytes));
+ let dec = Decoder {
+ input = buf,
+ pos = 0,
+ tracer = &trace::silent,
+ };
+ assert(decode_varint(&root(&dec))! == value);
+};
+
+fn varint_invalid(bytes: []u8) void = {
+ let dec = Decoder {
+ input = bytes,
+ pos = 0,
+ tracer = &trace::silent,
+ };
+ assert(decode_varint(&root(&dec)) is trace::failed);
+};
+
+@test fn varints() void = {
+ // sample varints from https://wiki.vg/protocol?oldid=17184#varint_and_varlong
+ varint_roundtrip(0, [0x00]);
+ varint_roundtrip(1, [0x01]);
+ varint_roundtrip(2, [0x02]);
+ varint_roundtrip(127, [0x7f]);
+ varint_roundtrip(128, [0x80, 0x01]);
+ varint_roundtrip(255, [0xff, 0x01]);
+ varint_roundtrip(25565, [0xdd, 0xc7, 0x01]);
+ varint_roundtrip(2097151, [0xff, 0xff, 0x7f]);
+ varint_roundtrip(2147483647, [0xff, 0xff, 0xff, 0xff, 0x07]);
+ varint_roundtrip(-1, [0xff, 0xff, 0xff, 0xff, 0x0f]);
+ varint_roundtrip(-2147483648, [0x80, 0x80, 0x80, 0x80, 0x08]);
+};
+
+@test fn varints_invalid() void = {
+ varint_invalid([]);
+ varint_invalid([0x8f]);
+ varint_invalid([0x8f, 0x8f]);
+ varint_invalid([0x80, 0x80, 0x80, 0x80, 0x80, 0x00]);
+ varint_invalid([0x80, 0x80, 0x80, 0x80, 0x10]);
+ varint_invalid([0xff, 0xff, 0xff, 0xff, 0xff]);
+};
diff --git a/mcproto/decode.ha b/mcproto/decode.ha
new file mode 100644
index 0000000..bd020c4
--- /dev/null
+++ b/mcproto/decode.ha
@@ -0,0 +1,193 @@
+use encoding::utf8;
+use endian;
+use fmt;
+use io;
+use strings;
+use trace;
+use uuid;
+
+export type Decoder = struct {
+ input: []u8,
+ pos: size,
+ tracer: *trace::tracer,
+};
+
+export type Context = struct {
+ dec: *Decoder,
+ pos: size,
+ fmt: str,
+ fields: []fmt::field,
+ up: nullable *Context,
+};
+
+export fn log(
+ ctx: *Context,
+ lvl: trace::level,
+ fmt: str,
+ fields: fmt::field...
+) void = {
+ log_(ctx, null, lvl, fmt, fields...);
+};
+
+fn log_(
+ ctx: *Context,
+ trace_ctx: nullable *trace::context,
+ lvl: trace::level,
+ fmt: str,
+ fields: fmt::field...
+) void = {
+ let s = "";
+ defer free(s);
+ if (len(ctx.fmt) != 0) {
+ s = fmt::asprintf(ctx.fmt, ctx.fields...);
+ // TODO: is this legal? works at the moment due to qbe
+ // semantics, but who knows...
+ trace_ctx = &trace::context {
+ fmt = "{} (offset {})",
+ fields = [s, ctx.pos],
+ next = trace_ctx,
+ };
+ };
+ match (ctx.up) {
+ case let ctx_: *Context =>
+ log_(ctx_, trace_ctx, lvl, fmt, fields...);
+ case null =>
+ trace::log(ctx.dec.tracer, trace_ctx, lvl, fmt, fields...);
+ };
+};
+
+export fn error(ctx: *Context, fmt: str, fields: fmt::field...) trace::failed = {
+ log(ctx, trace::level::ERROR, fmt, fields...);
+ return trace::failed;
+};
+
+export fn root(dec: *Decoder) Context = {
+ return Context {
+ dec = dec,
+ pos = dec.pos,
+ ...
+ };
+};
+
+export fn context(ctx: *Context, fmt: str, fields: fmt::field...) Context = {
+ return Context {
+ dec = ctx.dec,
+ pos = ctx.dec.pos,
+ fmt = fmt,
+ fields = fields,
+ up = ctx,
+ };
+};
+
+export fn decode_nbytes(ctx: *Context, length: size)
+ ([]u8 | trace::failed) = {
+ const dec = ctx.dec;
+ if (len(dec.input) - dec.pos < length) {
+ return error(ctx, "Expected {} bytes, found only {}",
+ length, len(dec.input) - dec.pos);
+ };
+ const res = dec.input[dec.pos..dec.pos + length];
+ dec.pos += length;
+ return res;
+};
+
+export fn decode_byte(ctx: *Context) (u8 | trace::failed) = {
+ const b = decode_nbytes(ctx, 1)?;
+ return b[0];
+};
+export fn decode_short(ctx: *Context) (u16 | trace::failed) = {
+ const b = decode_nbytes(ctx, 2)?;
+ return endian::begetu16(b);
+};
+export fn decode_int(ctx: *Context) (u32 | trace::failed) = {
+ const b = decode_nbytes(ctx, 4)?;
+ return endian::begetu32(b);
+};
+export fn decode_long(ctx: *Context) (u64 | trace::failed) = {
+ const b = decode_nbytes(ctx, 8)?;
+ return endian::begetu64(b);
+};
+
+export fn decode_bool(ctx: *Context) (bool | trace::failed) = {
+ const b = decode_byte(ctx)?;
+ if (b >= 2) {
+ return error(ctx, "Invalid boolean");
+ };
+ return b != 0;
+};
+
+export fn decode_float(ctx: *Context) (f32 | trace::failed) = {
+ const v = decode_int(ctx)?;
+ return *(&v: *f32);
+};
+export fn decode_double(ctx: *Context) (f64 | trace::failed) = {
+ const v = decode_long(ctx)?;
+ return *(&v: *f64);
+};
+
+export fn try_decode_varint(ctx: *Context) (i32 | !(trace::failed | TooShort)) = {
+ let res = 0u32;
+ const dec = ctx.dec;
+
+ for (let i = 0u32; dec.pos + i < len(dec.input); i += 1) {
+ const b = dec.input[dec.pos + i];
+
+ if (i == 4 && b & 0xf0 != 0) {
+ return error(ctx, "VarInt too long");
+ };
+
+ res |= (b & 0x7f): u32 << (7 * i);
+
+ if (b & 0x80 == 0) {
+ dec.pos += i + 1;
+ return res: i32;
+ };
+ };
+
+ return TooShort;
+};
+
+export fn decode_varint(ctx: *Context) (i32 | trace::failed) = {
+ match (try_decode_varint(ctx)) {
+ case let res: i32 =>
+ return res;
+ case =>
+ return error(ctx, "VarInt too short");
+ };
+};
+
+export fn decode_string(ctx: *Context, maxlen: size) (str | trace::failed) = {
+ const length = decode_varint(&context(ctx, "string length"))?;
+ const length = length: size;
+
+ if (length >= maxlen * 4) {
+ return error(ctx,
+ "String length {} exceeds limit of {} bytes",
+ length, maxlen * 4);
+ };
+
+ const ctx_ = context(ctx, "string data ({} bytes)", length);
+ const bytes = decode_nbytes(&ctx_, length)?;
+ match (strings::fromutf8(bytes)) {
+ case let string: str =>
+ // don't bother checking length in code points. doesn't seem
+ // very useful.
+ return string;
+ case utf8::invalid =>
+ return error(&ctx_, "Invalid UTF-8");
+ };
+};
+
+export fn decode_uuid(ctx: *Context) (uuid::uuid | trace::failed) = {
+ let uuid: [16]u8 = [0...];
+ uuid[..] = decode_nbytes(ctx, 16)?;
+ return uuid;
+};
+
+export fn expect_end(ctx: *Context) (void | trace::failed) = {
+ if (ctx.dec.pos != len(ctx.dec.input)) {
+ return error(ctx,
+ "Expected end of input, but found {} extra bytes starting at {}",
+ len(ctx.dec.input) - ctx.dec.pos, ctx.dec.pos);
+ };
+};
diff --git a/mcproto/encode.ha b/mcproto/encode.ha
new file mode 100644
index 0000000..de30871
--- /dev/null
+++ b/mcproto/encode.ha
@@ -0,0 +1,51 @@
+use endian;
+use strings;
+use uuid;
+
+export fn encode_short(out: *[]u8, v: u16) void = {
+ const buf: [2]u8 = [0...];
+ endian::beputu16(buf, v);
+ append(out, buf...);
+};
+export fn encode_int(out: *[]u8, v: u32) void = {
+ const buf: [4]u8 = [0...];
+ endian::beputu32(buf, v);
+ append(out, buf...);
+};
+export fn encode_long(out: *[]u8, v: u64) void = {
+ const buf: [8]u8 = [0...];
+ endian::beputu64(buf, v);
+ append(out, buf...);
+};
+
+export fn encode_bool(out: *[]u8, v: bool) void = {
+ append(out, if (v) 1 else 0);
+};
+
+export fn encode_float(out: *[]u8, v: f32) void = {
+ encode_int(out, *(&v: *u32));
+};
+export fn encode_double(out: *[]u8, v: f64) void = {
+ encode_long(out, *(&v: *u64));
+};
+
+export fn encode_varint(out: *[]u8, v: i32) void = {
+ let v = v: u32;
+
+ for (true) {
+ const continues = v & 0x7f != v;
+ const high_bit: u8 = if (continues) 0x80 else 0x00;
+ append(out, (v & 0x7f): u8 | high_bit);
+ v >>= 7;
+ if (!continues) return;
+ };
+};
+
+export fn encode_string(out: *[]u8, string: str) void = {
+ encode_varint(out, len(string): i32);
+ append(out, strings::toutf8(string)...);
+};
+
+export fn encode_uuid(out: *[]u8, uuid: uuid::uuid) void = {
+ append(out, uuid...);
+};
diff --git a/mcproto/error.ha b/mcproto/error.ha
new file mode 100644
index 0000000..092210d
--- /dev/null
+++ b/mcproto/error.ha
@@ -0,0 +1 @@
+export type TooShort = !void;
diff --git a/mcproto/frame.ha b/mcproto/frame.ha
new file mode 100644
index 0000000..d5ee4c1
--- /dev/null
+++ b/mcproto/frame.ha
@@ -0,0 +1,22 @@
+use bufio;
+use io;
+use trace;
+
+export fn write_frame(out: io::handle, in: []u8) (void | io::error) = {
+ assert(len(in) <= 0x1fffff, "write_frame: too long");
+ let length_buf: [3]u8 = [0...];
+ let length_buf = length_buf[..0];
+ encode_varint(&length_buf, len(in): i32);
+ io::writeall(out, length_buf)?;
+ io::writeall(out, in)?;
+};
+
+export fn write_packet(out: io::handle, packet_id: i32, payload: []u8)
+ (void | io::error) = {
+ static let packet_buf: [0x1fffff]u8 = [0...];
+
+ let packet = packet_buf[..0];
+ encode_packet(&packet, packet_id, payload);
+
+ write_frame(out, packet)?;
+};
diff --git a/mcproto/handshake.ha b/mcproto/handshake.ha
new file mode 100644
index 0000000..8df5254
--- /dev/null
+++ b/mcproto/handshake.ha
@@ -0,0 +1,46 @@
+use trace;
+
+export def SB_HANDSHAKE: i32 = 0x00;
+
+export type Handshake = struct {
+ proto_ver: i32,
+ server_addr: str,
+ server_port: u16,
+ next_state: NextState,
+};
+
+export type NextState = enum i32 {
+ STATUS = 1,
+ LOGIN = 2,
+};
+
+export fn encode_handshake(out: *[]u8, pkt: *Handshake) void = {
+ encode_varint(out, pkt.proto_ver);
+ encode_string(out, pkt.server_addr);
+ encode_short(out, pkt.server_port);
+ encode_varint(out, pkt.next_state);
+};
+
+export fn decode_handshake(ctx: *Context) (Handshake | trace::failed) = {
+ const ctx_ = context(ctx, "protocol version");
+ const proto_ver = decode_varint(&ctx_)?;
+ const ctx_ = context(ctx, "server address");
+ const server_addr = decode_string(&ctx_, 255)?;
+ const ctx_ = context(ctx, "server port");
+ const server_port = decode_short(&ctx_)?;
+ const ctx_ = context(ctx, "next state");
+ const next_state = decode_varint(&ctx_)?: NextState;
+ if (next_state != NextState::STATUS
+ && next_state != NextState::LOGIN) {
+ return error(&ctx_, "Invalid next state 0x{:02x}",
+ next_state: i32);
+ };
+ expect_end(ctx)?;
+
+ return Handshake {
+ proto_ver = proto_ver,
+ server_addr = server_addr,
+ server_port = server_port,
+ next_state = next_state,
+ };
+};
diff --git a/mcproto/packet.ha b/mcproto/packet.ha
new file mode 100644
index 0000000..669df56
--- /dev/null
+++ b/mcproto/packet.ha
@@ -0,0 +1,13 @@
+use math;
+use trace;
+
+export fn encode_packet(out: *[]u8, packet_id: i32, in: []u8) void = {
+ let id_size = math::bit_size_u32(packet_id: u32);
+ if (id_size == 0) id_size = 1;
+ const id_size = id_size / 7 + id_size % 7;
+ assert(len(in) + id_size <= 0x1fffff,
+ "TODO: sent packet too long. this should be handled better.");
+
+ encode_varint(out, packet_id);
+ append(out, in...);
+};
diff --git a/mcproto/status/ids.ha b/mcproto/status/ids.ha
new file mode 100644
index 0000000..89d6bb4
--- /dev/null
+++ b/mcproto/status/ids.ha
@@ -0,0 +1,5 @@
+export def SB_REQUEST: i32 = 0x00;
+export def SB_PING: i32 = 0x01;
+
+export def CB_RESPONSE: i32 = 0x00;
+export def CB_PONG: i32 = 0x01;
diff --git a/mcproto/status/ping.ha b/mcproto/status/ping.ha
new file mode 100644
index 0000000..00ff7cf
--- /dev/null
+++ b/mcproto/status/ping.ha
@@ -0,0 +1,9 @@
+use mcproto;
+use trace;
+
+export fn decode_ping(ctx: *mcproto::Context) (i64 | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "ping id");
+ const ping_id = mcproto::decode_long(&ctx_)?: i64;
+ mcproto::expect_end(ctx)?;
+ return ping_id;
+};
diff --git a/mcproto/status/response.ha b/mcproto/status/response.ha
new file mode 100644
index 0000000..e93df47
--- /dev/null
+++ b/mcproto/status/response.ha
@@ -0,0 +1,111 @@
+use dejson;
+use encoding::json;
+use mcproto;
+use strings;
+use trace;
+use uuid;
+
+export type Response = struct {
+ description: str,
+ version_name: str,
+ version_protocol: i32,
+ players_online: u32,
+ players_max: u32,
+ players_sample: [](str, uuid::uuid),
+ previews_chat: bool,
+ forces_reportable_chat: bool,
+};
+
+export fn decode_response(ctx: *mcproto::Context) (Response | trace::failed) = {
+ const ctx_ = mcproto::context(ctx, "json");
+ const json = mcproto::decode_string(&ctx_, 32767)?;
+ mcproto::expect_end(ctx)?;
+
+ const json = match (json::loadstr(json)) {
+ case let json: json::value =>
+ yield json;
+ case let err: json::error =>
+ return mcproto::error(&ctx_, "Invalid JSON: {}", json::strerror(err));
+ };
+ defer json::finish(json);
+
+ const deser = dejson::newdeser(&json);
+ match (deser_response(&deser)) {
+ case let res: Response =>
+ return res;
+ case let err: dejson::error =>
+ defer free(err);
+ return mcproto::error(&ctx_, "Invalid response contents: {}", err);
+ };
+};
+
+export fn deser_response(de: *dejson::deser) (Response | dejson::error) = {
+ let success = false;
+
+ let res = Response { ... };
+ defer if (!success) response_finish(res);
+
+ res.description = match (dejson::optfield(de, "description")?) {
+ case let de_description: dejson::deser =>
+ yield json::dumpstr(*de_description.val);
+ case void =>
+ yield "";
+ };
+
+ const de_version = dejson::field(de, "version")?;
+ res.version_name = strings::dup(dejson::string(
+ &dejson::field(&de_version, "name")?)?);
+ res.version_protocol = dejson::number_i32(
+ &dejson::field(&de_version, "protocol")?)?;
+
+ const de_players = dejson::field(de, "players")?;
+ res.players_online = dejson::number_u32(
+ &dejson::field(&de_players, "online")?)?;
+ res.players_max = dejson::number_u32(
+ &dejson::field(&de_players, "max")?)?;
+ match (dejson::optfield(&de_players, "sample")?) {
+ case let de_sample: dejson::deser =>
+ const nsample = dejson::length(&de_sample)?;
+ for (let i = 0z; i < nsample; i += 1) {
+ const de_player = dejson::index(&de_sample, i)?;
+ const name = dejson::string(
+ &dejson::field(&de_player, "name")?)?;
+ const de_id = dejson::field(&de_player, "id")?;
+ const id = dejson::string(&de_id)?;
+ const id = match (uuid::decodestr(id)) {
+ case let id: uuid::uuid =>
+ yield id;
+ case uuid::invalid =>
+ return dejson::fail(&de_id, "Invalid UUID");
+ };
+ append(res.players_sample, (strings::dup(name), id));
+ };
+ case void => void;
+ };
+
+ res.previews_chat = match (dejson::optfield(de, "previewsChat")) {
+ case let de_previews_chat: dejson::deser =>
+ yield dejson::boolean(&de_previews_chat)?;
+ case void =>
+ yield false;
+ };
+ res.forces_reportable_chat = match (
+ dejson::optfield(de, "enforcesSecureChat")) {
+ case let de_forces_reportable_chat: dejson::deser =>
+ yield dejson::boolean(&de_forces_reportable_chat)?;
+ case void =>
+ yield false;
+ };
+
+ success = true;
+ return res;
+};
+
+export fn response_finish(res: Response) void = {
+ free(res.description);
+ free(res.version_name);
+ for (let i = 0z; i < len(res.players_sample); i += 1) {
+ free(res.players_sample[i].0);
+ };
+ free(res.players_sample);
+};