#include "maze32_protocol.h" #include #include 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"; } }