feat(lobby): implement bounded WebSocket sessions and countdown
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
idf_component_register(
|
||||
SRCS "maze32_lobby.c" "maze32_protocol.c"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
#ifndef MAZE32_LOBBY_H
|
||||
#define MAZE32_LOBBY_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAZE32_PLAYER_LIMIT 2U
|
||||
#define MAZE32_OBSERVER_LIMIT 4U
|
||||
#define MAZE32_SESSION_LIMIT (MAZE32_PLAYER_LIMIT + MAZE32_OBSERVER_LIMIT)
|
||||
#define MAZE32_PLAYER_NAME_MAX_BYTES 32U
|
||||
#define MAZE32_SESSION_TOKEN_BYTES 16U
|
||||
#define MAZE32_SESSION_TOKEN_HEX_LENGTH (MAZE32_SESSION_TOKEN_BYTES * 2U)
|
||||
#define MAZE32_COUNTDOWN_DURATION_MS 3000U
|
||||
|
||||
typedef enum {
|
||||
MAZE32_ROLE_NONE = 0,
|
||||
MAZE32_ROLE_PLAYER,
|
||||
MAZE32_ROLE_OBSERVER,
|
||||
} maze32_role_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_LOBBY_WAITING = 0,
|
||||
MAZE32_LOBBY_COUNTDOWN,
|
||||
MAZE32_LOBBY_ACTIVE,
|
||||
} maze32_lobby_phase_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_LOBBY_OK = 0,
|
||||
MAZE32_LOBBY_INVALID_NAME,
|
||||
MAZE32_LOBBY_DUPLICATE_NAME,
|
||||
MAZE32_LOBBY_ROLE_FULL,
|
||||
MAZE32_LOBBY_INVALID_TOKEN,
|
||||
MAZE32_LOBBY_SESSION_CONNECTED,
|
||||
MAZE32_LOBBY_NOT_OWNER,
|
||||
MAZE32_LOBBY_NOT_READY,
|
||||
MAZE32_LOBBY_INVALID_PHASE,
|
||||
} maze32_lobby_result_t;
|
||||
|
||||
typedef struct {
|
||||
int connection_id;
|
||||
maze32_role_t role;
|
||||
unsigned int player_slot;
|
||||
int connected;
|
||||
char name[MAZE32_PLAYER_NAME_MAX_BYTES + 1U];
|
||||
char token[MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U];
|
||||
} maze32_session_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_session_t sessions[MAZE32_SESSION_LIMIT];
|
||||
maze32_lobby_phase_t phase;
|
||||
uint64_t countdown_started_ms;
|
||||
uint64_t activation_ms;
|
||||
uint32_t revision;
|
||||
} maze32_lobby_t;
|
||||
|
||||
void maze32_lobby_init(maze32_lobby_t* lobby);
|
||||
maze32_lobby_result_t maze32_lobby_join_player(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* name, const char* token,
|
||||
maze32_session_t** session);
|
||||
maze32_lobby_result_t maze32_lobby_join_observer(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session);
|
||||
maze32_lobby_result_t maze32_lobby_restore(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session);
|
||||
void maze32_lobby_disconnect(maze32_lobby_t* lobby, int connection_id);
|
||||
maze32_lobby_result_t maze32_lobby_start(maze32_lobby_t* lobby, int connection_id, uint64_t now_ms);
|
||||
int maze32_lobby_activate_if_due(maze32_lobby_t* lobby, uint64_t now_ms);
|
||||
const maze32_session_t* maze32_lobby_session_for_connection(const maze32_lobby_t* lobby,
|
||||
int connection_id);
|
||||
unsigned int maze32_lobby_connected_observers(const maze32_lobby_t* lobby);
|
||||
int maze32_lobby_ready(const maze32_lobby_t* lobby);
|
||||
int maze32_lobby_can_reconfigure(const maze32_lobby_t* lobby);
|
||||
const char* maze32_lobby_phase_name(maze32_lobby_phase_t phase);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef MAZE32_PROTOCOL_H
|
||||
#define MAZE32_PROTOCOL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "maze32_lobby.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAZE32_PROTOCOL_VERSION 1U
|
||||
#define MAZE32_PROTOCOL_MESSAGE_LIMIT 512U
|
||||
#define MAZE32_REQUEST_ID_MAX_BYTES 32U
|
||||
|
||||
typedef enum {
|
||||
MAZE32_MESSAGE_INVALID = 0,
|
||||
MAZE32_MESSAGE_JOIN,
|
||||
MAZE32_MESSAGE_RESTORE,
|
||||
MAZE32_MESSAGE_START,
|
||||
MAZE32_MESSAGE_HEARTBEAT,
|
||||
MAZE32_MESSAGE_SUGGEST_NAME,
|
||||
} maze32_message_type_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_PROTOCOL_OK = 0,
|
||||
MAZE32_PROTOCOL_MALFORMED,
|
||||
MAZE32_PROTOCOL_TOO_LARGE,
|
||||
MAZE32_PROTOCOL_UNSUPPORTED_VERSION,
|
||||
MAZE32_PROTOCOL_UNKNOWN_TYPE,
|
||||
MAZE32_PROTOCOL_INVALID_PAYLOAD,
|
||||
} maze32_protocol_result_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_message_type_t type;
|
||||
uint32_t sequence;
|
||||
char request_id[MAZE32_REQUEST_ID_MAX_BYTES + 1U];
|
||||
maze32_role_t role;
|
||||
char name[MAZE32_PLAYER_NAME_MAX_BYTES + 1U];
|
||||
char token[MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U];
|
||||
} maze32_client_message_t;
|
||||
|
||||
maze32_protocol_result_t maze32_protocol_parse(const char* data, size_t length,
|
||||
maze32_client_message_t* message);
|
||||
const char* maze32_protocol_result_name(maze32_protocol_result_t result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "maze32_lobby.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static int is_valid_name(const char* name) {
|
||||
if (name == NULL) {
|
||||
return 0;
|
||||
}
|
||||
size_t length = strlen(name);
|
||||
if (length == 0U || length > MAZE32_PLAYER_NAME_MAX_BYTES || name[0] == ' ' ||
|
||||
name[length - 1U] == ' ') {
|
||||
return 0;
|
||||
}
|
||||
for (size_t index = 0U; index < length; ++index) {
|
||||
unsigned char byte = (unsigned char)name[index];
|
||||
if (byte < 0x20U || byte == 0x7fU) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int is_valid_token(const char* token) {
|
||||
if (token == NULL || strlen(token) != MAZE32_SESSION_TOKEN_HEX_LENGTH) {
|
||||
return 0;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_TOKEN_HEX_LENGTH; ++index) {
|
||||
char character = token[index];
|
||||
if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static maze32_session_t* find_player_slot(maze32_lobby_t* lobby, unsigned int slot) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER && session->player_slot == slot) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static maze32_session_t* find_free_session(maze32_lobby_t* lobby) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
if (lobby->sessions[index].role == MAZE32_ROLE_NONE) {
|
||||
return &lobby->sessions[index];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static maze32_session_t* find_reusable_observer(maze32_lobby_t* lobby) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_OBSERVER && !session->connected) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void maze32_lobby_init(maze32_lobby_t* lobby) {
|
||||
if (lobby == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(lobby, 0, sizeof(*lobby));
|
||||
lobby->phase = MAZE32_LOBBY_WAITING;
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_join_player(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* name, const char* token,
|
||||
maze32_session_t** session) {
|
||||
if (lobby == NULL || session == NULL || !is_valid_name(name) || !is_valid_token(token)) {
|
||||
return MAZE32_LOBBY_INVALID_NAME;
|
||||
}
|
||||
if (lobby->phase != MAZE32_LOBBY_WAITING) {
|
||||
return MAZE32_LOBBY_INVALID_PHASE;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
if (lobby->sessions[index].role == MAZE32_ROLE_PLAYER &&
|
||||
strcmp(lobby->sessions[index].name, name) == 0) {
|
||||
return MAZE32_LOBBY_DUPLICATE_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int slot = find_player_slot(lobby, 1U) == NULL ? 1U : 2U;
|
||||
if (find_player_slot(lobby, slot) != NULL) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
maze32_session_t* target = find_free_session(lobby);
|
||||
if (target == NULL) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
target->connection_id = connection_id;
|
||||
target->role = MAZE32_ROLE_PLAYER;
|
||||
target->player_slot = slot;
|
||||
target->connected = 1;
|
||||
memcpy(target->name, name, strlen(name) + 1U);
|
||||
memcpy(target->token, token, MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U);
|
||||
lobby->revision++;
|
||||
*session = target;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_join_observer(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session) {
|
||||
if (lobby == NULL || session == NULL || !is_valid_token(token)) {
|
||||
return MAZE32_LOBBY_INVALID_TOKEN;
|
||||
}
|
||||
if (maze32_lobby_connected_observers(lobby) >= MAZE32_OBSERVER_LIMIT) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
maze32_session_t* target = find_free_session(lobby);
|
||||
if (target == NULL) {
|
||||
target = find_reusable_observer(lobby);
|
||||
}
|
||||
if (target == NULL) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
memset(target, 0, sizeof(*target));
|
||||
target->connection_id = connection_id;
|
||||
target->role = MAZE32_ROLE_OBSERVER;
|
||||
target->connected = 1;
|
||||
memcpy(target->token, token, MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U);
|
||||
lobby->revision++;
|
||||
*session = target;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_restore(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session) {
|
||||
if (lobby == NULL || session == NULL || !is_valid_token(token)) {
|
||||
return MAZE32_LOBBY_INVALID_TOKEN;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* candidate = &lobby->sessions[index];
|
||||
if (candidate->role != MAZE32_ROLE_NONE && strcmp(candidate->token, token) == 0) {
|
||||
if (candidate->connected) {
|
||||
return MAZE32_LOBBY_SESSION_CONNECTED;
|
||||
}
|
||||
candidate->connected = 1;
|
||||
candidate->connection_id = connection_id;
|
||||
lobby->revision++;
|
||||
*session = candidate;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
}
|
||||
return MAZE32_LOBBY_INVALID_TOKEN;
|
||||
}
|
||||
|
||||
void maze32_lobby_disconnect(maze32_lobby_t* lobby, int connection_id) {
|
||||
if (lobby == NULL) {
|
||||
return;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role != MAZE32_ROLE_NONE && session->connected &&
|
||||
session->connection_id == connection_id) {
|
||||
session->connected = 0;
|
||||
session->connection_id = -1;
|
||||
lobby->revision++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_start(maze32_lobby_t* lobby, int connection_id,
|
||||
uint64_t now_ms) {
|
||||
if (lobby == NULL || lobby->phase != MAZE32_LOBBY_WAITING) {
|
||||
return MAZE32_LOBBY_INVALID_PHASE;
|
||||
}
|
||||
const maze32_session_t* session = maze32_lobby_session_for_connection(lobby, connection_id);
|
||||
if (session == NULL || session->role != MAZE32_ROLE_PLAYER || session->player_slot != 1U) {
|
||||
return MAZE32_LOBBY_NOT_OWNER;
|
||||
}
|
||||
if (!maze32_lobby_ready(lobby)) {
|
||||
return MAZE32_LOBBY_NOT_READY;
|
||||
}
|
||||
lobby->phase = MAZE32_LOBBY_COUNTDOWN;
|
||||
lobby->countdown_started_ms = now_ms;
|
||||
lobby->activation_ms = now_ms + MAZE32_COUNTDOWN_DURATION_MS;
|
||||
lobby->revision++;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
|
||||
int maze32_lobby_activate_if_due(maze32_lobby_t* lobby, uint64_t now_ms) {
|
||||
if (lobby == NULL || lobby->phase != MAZE32_LOBBY_COUNTDOWN || now_ms < lobby->activation_ms) {
|
||||
return 0;
|
||||
}
|
||||
lobby->phase = MAZE32_LOBBY_ACTIVE;
|
||||
lobby->revision++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const maze32_session_t* maze32_lobby_session_for_connection(const maze32_lobby_t* lobby,
|
||||
int connection_id) {
|
||||
if (lobby == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role != MAZE32_ROLE_NONE && session->connected &&
|
||||
session->connection_id == connection_id) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned int maze32_lobby_connected_observers(const maze32_lobby_t* lobby) {
|
||||
unsigned int count = 0U;
|
||||
if (lobby == NULL) {
|
||||
return count;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_OBSERVER && session->connected) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int maze32_lobby_ready(const maze32_lobby_t* lobby) {
|
||||
if (lobby == NULL) {
|
||||
return 0;
|
||||
}
|
||||
for (unsigned int slot = 1U; slot <= MAZE32_PLAYER_LIMIT; ++slot) {
|
||||
int found = 0;
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER && session->player_slot == slot &&
|
||||
session->connected) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int maze32_lobby_can_reconfigure(const maze32_lobby_t* lobby) {
|
||||
return lobby != NULL && lobby->phase == MAZE32_LOBBY_WAITING;
|
||||
}
|
||||
|
||||
const char* maze32_lobby_phase_name(maze32_lobby_phase_t phase) {
|
||||
switch (phase) {
|
||||
case MAZE32_LOBBY_WAITING:
|
||||
return "waiting";
|
||||
case MAZE32_LOBBY_COUNTDOWN:
|
||||
return "countdown";
|
||||
case MAZE32_LOBBY_ACTIVE:
|
||||
return "active";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "maze32_protocol.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char* data;
|
||||
size_t length;
|
||||
size_t position;
|
||||
} json_cursor_t;
|
||||
|
||||
static void skip_space(json_cursor_t* cursor) {
|
||||
while (cursor->position < cursor->length) {
|
||||
char character = cursor->data[cursor->position];
|
||||
if (character != ' ' && character != '\t' && character != '\r' && character != '\n') {
|
||||
return;
|
||||
}
|
||||
cursor->position++;
|
||||
}
|
||||
}
|
||||
|
||||
static int consume(json_cursor_t* cursor, char expected) {
|
||||
skip_space(cursor);
|
||||
if (cursor->position >= cursor->length || cursor->data[cursor->position] != expected) {
|
||||
return 0;
|
||||
}
|
||||
cursor->position++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_string(json_cursor_t* cursor, char* output, size_t capacity) {
|
||||
skip_space(cursor);
|
||||
if (cursor->position >= cursor->length || cursor->data[cursor->position++] != '"') {
|
||||
return 0;
|
||||
}
|
||||
size_t output_length = 0U;
|
||||
while (cursor->position < cursor->length) {
|
||||
unsigned char character = (unsigned char)cursor->data[cursor->position++];
|
||||
if (character == '"') {
|
||||
if (output_length >= capacity) {
|
||||
return 0;
|
||||
}
|
||||
output[output_length] = '\0';
|
||||
return 1;
|
||||
}
|
||||
if (character < 0x20U) {
|
||||
return 0;
|
||||
}
|
||||
if (character == '\\') {
|
||||
if (cursor->position >= cursor->length) {
|
||||
return 0;
|
||||
}
|
||||
character = (unsigned char)cursor->data[cursor->position++];
|
||||
if (character == '"' || character == '\\' || character == '/') {
|
||||
/* The escaped byte is copied below. */
|
||||
} else if (character == 'b') {
|
||||
character = '\b';
|
||||
} else if (character == 'f') {
|
||||
character = '\f';
|
||||
} else if (character == 'n') {
|
||||
character = '\n';
|
||||
} else if (character == 'r') {
|
||||
character = '\r';
|
||||
} else if (character == 't') {
|
||||
character = '\t';
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (output_length + 1U >= capacity) {
|
||||
return 0;
|
||||
}
|
||||
output[output_length++] = (char)character;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint32(json_cursor_t* cursor, uint32_t* value) {
|
||||
skip_space(cursor);
|
||||
if (cursor->position >= cursor->length || cursor->data[cursor->position] < '0' ||
|
||||
cursor->data[cursor->position] > '9') {
|
||||
return 0;
|
||||
}
|
||||
uint64_t parsed = 0U;
|
||||
while (cursor->position < cursor->length && cursor->data[cursor->position] >= '0' &&
|
||||
cursor->data[cursor->position] <= '9') {
|
||||
parsed = parsed * 10U + (uint64_t)(cursor->data[cursor->position] - '0');
|
||||
if (parsed > UINT32_MAX) {
|
||||
return 0;
|
||||
}
|
||||
cursor->position++;
|
||||
}
|
||||
*value = (uint32_t)parsed;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int payload_field_allowed(maze32_message_type_t type, const char* key, char* value,
|
||||
maze32_client_message_t* message, unsigned int* fields) {
|
||||
if (type == MAZE32_MESSAGE_JOIN && strcmp(key, "role") == 0 && !(*fields & 1U)) {
|
||||
if (strcmp(value, "player") == 0) {
|
||||
message->role = MAZE32_ROLE_PLAYER;
|
||||
} else if (strcmp(value, "observer") == 0) {
|
||||
message->role = MAZE32_ROLE_OBSERVER;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
*fields |= 1U;
|
||||
return 1;
|
||||
}
|
||||
if (type == MAZE32_MESSAGE_JOIN && strcmp(key, "name") == 0 && !(*fields & 2U)) {
|
||||
if (strlen(value) > MAZE32_PLAYER_NAME_MAX_BYTES) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(message->name, value, strlen(value) + 1U);
|
||||
*fields |= 2U;
|
||||
return 1;
|
||||
}
|
||||
if (type == MAZE32_MESSAGE_RESTORE && strcmp(key, "token") == 0 && !(*fields & 1U)) {
|
||||
if (strlen(value) != MAZE32_SESSION_TOKEN_HEX_LENGTH) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(message->token, value, MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U);
|
||||
*fields |= 1U;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_payload(json_cursor_t* cursor, maze32_client_message_t* message) {
|
||||
if (!consume(cursor, '{')) {
|
||||
return 0;
|
||||
}
|
||||
unsigned int fields = 0U;
|
||||
skip_space(cursor);
|
||||
if (cursor->position < cursor->length && cursor->data[cursor->position] == '}') {
|
||||
cursor->position++;
|
||||
} else {
|
||||
for (;;) {
|
||||
char key[16];
|
||||
char value[MAZE32_PLAYER_NAME_MAX_BYTES + 1U];
|
||||
if (!parse_string(cursor, key, sizeof(key)) || !consume(cursor, ':') ||
|
||||
!parse_string(cursor, value, sizeof(value)) ||
|
||||
!payload_field_allowed(message->type, key, value, message, &fields)) {
|
||||
return 0;
|
||||
}
|
||||
skip_space(cursor);
|
||||
if (consume(cursor, '}')) {
|
||||
break;
|
||||
}
|
||||
if (!consume(cursor, ',')) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message->type == MAZE32_MESSAGE_JOIN) {
|
||||
return message->role == MAZE32_ROLE_PLAYER ? fields == 3U : fields == 1U;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_RESTORE) {
|
||||
return fields == 1U;
|
||||
}
|
||||
return fields == 0U;
|
||||
}
|
||||
|
||||
static maze32_message_type_t message_type(const char* value) {
|
||||
if (strcmp(value, "join") == 0) {
|
||||
return MAZE32_MESSAGE_JOIN;
|
||||
}
|
||||
if (strcmp(value, "restore") == 0) {
|
||||
return MAZE32_MESSAGE_RESTORE;
|
||||
}
|
||||
if (strcmp(value, "start") == 0) {
|
||||
return MAZE32_MESSAGE_START;
|
||||
}
|
||||
if (strcmp(value, "heartbeat") == 0) {
|
||||
return MAZE32_MESSAGE_HEARTBEAT;
|
||||
}
|
||||
if (strcmp(value, "suggest_name") == 0) {
|
||||
return MAZE32_MESSAGE_SUGGEST_NAME;
|
||||
}
|
||||
return MAZE32_MESSAGE_INVALID;
|
||||
}
|
||||
|
||||
maze32_protocol_result_t maze32_protocol_parse(const char* data, size_t length,
|
||||
maze32_client_message_t* message) {
|
||||
if (data == NULL || message == NULL || length == 0U) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (length > MAZE32_PROTOCOL_MESSAGE_LIMIT) {
|
||||
return MAZE32_PROTOCOL_TOO_LARGE;
|
||||
}
|
||||
memset(message, 0, sizeof(*message));
|
||||
json_cursor_t cursor = {.data = data, .length = length, .position = 0U};
|
||||
if (!consume(&cursor, '{')) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
|
||||
unsigned int envelope_fields = 0U;
|
||||
uint32_t version = 0U;
|
||||
char type_value[24] = "";
|
||||
for (;;) {
|
||||
char key[24];
|
||||
if (!parse_string(&cursor, key, sizeof(key)) || !consume(&cursor, ':')) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (strcmp(key, "v") == 0 && !(envelope_fields & 1U)) {
|
||||
if (!parse_uint32(&cursor, &version)) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
envelope_fields |= 1U;
|
||||
} else if (strcmp(key, "type") == 0 && !(envelope_fields & 2U)) {
|
||||
if (!parse_string(&cursor, type_value, sizeof(type_value))) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
message->type = message_type(type_value);
|
||||
envelope_fields |= 2U;
|
||||
} else if (strcmp(key, "seq") == 0 && !(envelope_fields & 4U)) {
|
||||
if (!parse_uint32(&cursor, &message->sequence) || message->sequence == 0U) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
envelope_fields |= 4U;
|
||||
} else if (strcmp(key, "requestId") == 0 && !(envelope_fields & 8U)) {
|
||||
if (!parse_string(&cursor, message->request_id, sizeof(message->request_id))) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
envelope_fields |= 8U;
|
||||
} else if (strcmp(key, "payload") == 0 && !(envelope_fields & 16U)) {
|
||||
if (!(envelope_fields & 2U)) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_INVALID) {
|
||||
return MAZE32_PROTOCOL_UNKNOWN_TYPE;
|
||||
}
|
||||
if (!parse_payload(&cursor, message)) {
|
||||
return MAZE32_PROTOCOL_INVALID_PAYLOAD;
|
||||
}
|
||||
envelope_fields |= 16U;
|
||||
} else {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
skip_space(&cursor);
|
||||
if (consume(&cursor, '}')) {
|
||||
break;
|
||||
}
|
||||
if (!consume(&cursor, ',')) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
}
|
||||
skip_space(&cursor);
|
||||
if (cursor.position != cursor.length ||
|
||||
(envelope_fields & (1U | 2U | 4U | 16U)) != (1U | 2U | 4U | 16U)) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (version != MAZE32_PROTOCOL_VERSION) {
|
||||
return MAZE32_PROTOCOL_UNSUPPORTED_VERSION;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_INVALID) {
|
||||
return MAZE32_PROTOCOL_UNKNOWN_TYPE;
|
||||
}
|
||||
return MAZE32_PROTOCOL_OK;
|
||||
}
|
||||
|
||||
const char* maze32_protocol_result_name(maze32_protocol_result_t result) {
|
||||
switch (result) {
|
||||
case MAZE32_PROTOCOL_OK:
|
||||
return "ok";
|
||||
case MAZE32_PROTOCOL_MALFORMED:
|
||||
return "malformed";
|
||||
case MAZE32_PROTOCOL_TOO_LARGE:
|
||||
return "too_large";
|
||||
case MAZE32_PROTOCOL_UNSUPPORTED_VERSION:
|
||||
return "unsupported_version";
|
||||
case MAZE32_PROTOCOL_UNKNOWN_TYPE:
|
||||
return "unknown_type";
|
||||
case MAZE32_PROTOCOL_INVALID_PAYLOAD:
|
||||
return "invalid_payload";
|
||||
default:
|
||||
return "unknown_error";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user