diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index a7074ab..f448f33 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -9,6 +9,6 @@ include(FetchContent) FetchContent_Declare( raylib GIT_REPOSITORY https://github.com/raysan5/raylib - GIT_TAG 5.5 + GIT_TAG 5aa3f0ccc374c1fbfc880402b37871b9c3bb7d8e ) FetchContent_MakeAvailable(raylib) \ No newline at end of file diff --git a/src/error/error.c b/src/error/error.c index 6a96885..8345db7 100644 --- a/src/error/error.c +++ b/src/error/error.c @@ -21,6 +21,7 @@ errorret_t errorCode(const errorret_t code, const char_t *message, ...) { "Multiple errors encountered." ); errorPrint(); + return code; } va_list args; diff --git a/src/main.c b/src/main.c index 72bf3b3..57f5956 100644 --- a/src/main.c +++ b/src/main.c @@ -7,13 +7,29 @@ #include "console/console.h" #include "server/server.h" +#include "util/string.h" + +bool_t exitRequested = false; void cmdExit(const consolecmdexec_t *exec) { - CloseWindow(); + exitRequested = true; } void cmdServe(const consolecmdexec_t *exec) { - serverStart(3030); + uint16_t port = 3030; + if(exec->argc != 0) { + if(!stringToU16(exec->argv[0], &port)) { + consolePrint("Invalid port number: %s", exec->argv[0]); + return; + } + } + + errorret_t ret = serverStart(port); + if(ret != ERROR_OK) { + consolePrint("Failed to start server: %s", errorString()); + errorFlush(); + return; + } } void cmdClose(const consolecmdexec_t *exec) { @@ -39,6 +55,8 @@ int main(void) { consoleDraw(); EndDrawing(); + + if(exitRequested) break; } CloseWindow(); diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index afed701..27744cb 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -8,4 +8,7 @@ target_sources(${DUSK_TARGET_NAME} PRIVATE server.c serverclient.c -) \ No newline at end of file +) + +# Subdirs +add_subdirectory(packet) \ No newline at end of file diff --git a/src/server/packet/CMakeLists.txt b/src/server/packet/CMakeLists.txt new file mode 100644 index 0000000..5162f7c --- /dev/null +++ b/src/server/packet/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2025 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_TARGET_NAME} + PRIVATE + packet.c + packetwelcome.c +) \ No newline at end of file diff --git a/src/server/packet/packet.c b/src/server/packet/packet.c new file mode 100644 index 0000000..67b33f8 --- /dev/null +++ b/src/server/packet/packet.c @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packet.h" +#include "assert/assert.h" +#include "util/memory.h" + +void packetInit( + packet_t *packet, + const packettype_t type, + const uint32_t length +) { + assertNotNull(packet, "Packet is NULL"); + + memoryZero(packet, sizeof(packet_t)); + + assertTrue(length > 0, "Packet length is 0"); + assertTrue( + length <= sizeof(packetdata_t), + "Packet length is too large" + ); + + packet->type = type; + packet->length = length; +} \ No newline at end of file diff --git a/src/server/packet/packet.h b/src/server/packet/packet.h new file mode 100644 index 0000000..8d921c2 --- /dev/null +++ b/src/server/packet/packet.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "packetwelcome.h" +#include "packetdisconnect.h" + +typedef enum { + PACKET_TYPE_INVALID = 0, + PACKET_TYPE_WELCOME = 1, + PACKET_TYPE_DISCONNECT = 2, +} packettype_t; + +typedef union { + packetwelcome_t welcome; + packetdisconnect_t disconnect; +} packetdata_t; + +typedef struct packet_s { + packettype_t type; + uint32_t length; + packetdata_t data; +} packet_t; + +/** + * Initializes a packet with the given type. This is only to be used by sub + * initializers, such as packetWelcomeCreate for example. + * + * @param packet Pointer to the packet structure to initialize. + * @param type The type of the packet. + * @param length The length of the packet data. + */ +void packetInit( + packet_t *packet, + const packettype_t type, + const uint32_t length +); \ No newline at end of file diff --git a/src/server/packet/packetdisconnect.c b/src/server/packet/packetdisconnect.c new file mode 100644 index 0000000..c6205e7 --- /dev/null +++ b/src/server/packet/packetdisconnect.c @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packet.h" +#include "util/memory.h" + +void packetDisconnectCreate( + packet_t *packet, + const packetdisconnectreason_t reason +) { + packetInit(packet, PACKET_TYPE_DISCONNECT, sizeof(packetdisconnect_t)); + packet->data.disconnect.reason = reason; +} \ No newline at end of file diff --git a/src/server/packet/packetdisconnect.h b/src/server/packet/packetdisconnect.h new file mode 100644 index 0000000..d13e1f2 --- /dev/null +++ b/src/server/packet/packetdisconnect.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct packet_s packet_t; + +typedef enum { + PACKET_DISCONNECT_REASON_UNKNOWN = 0, + PACKET_DISCONNECT_REASON_INVALID_VERSION = 1, + PACKET_DISCONNECT_REASON_MALFORMED_PACKET = 2, +} packetdisconnectreason_t; + +typedef struct { + packetdisconnectreason_t reason; +} packetdisconnect_t; + +/** + * Creates a disconnect packet. + * + * @param packet Pointer to the packet structure to initialize. + * @param reason The reason for the disconnect. + */ +void packetDisconnectCreate( + packet_t *packet, + const packetdisconnectreason_t reason +); \ No newline at end of file diff --git a/src/server/packet/packetwelcome.c b/src/server/packet/packetwelcome.c new file mode 100644 index 0000000..f571808 --- /dev/null +++ b/src/server/packet/packetwelcome.c @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "packet.h" +#include "util/memory.h" + +void packetWelcomeCreate(packet_t *packet) { + packetInit(packet, PACKET_TYPE_WELCOME, PACKET_WELCOME_SIZE); + memoryCopy( + packet->data.welcome.dusk, PACKET_WELCOME_STRING, PACKET_WELCOME_SIZE + ); +} \ No newline at end of file diff --git a/src/server/packet/packetwelcome.h b/src/server/packet/packetwelcome.h new file mode 100644 index 0000000..ed63405 --- /dev/null +++ b/src/server/packet/packetwelcome.h @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +typedef struct packet_s packet_t; + +#define PACKET_WELCOME_STRING "DUSK" +#define PACKET_WELCOME_SIZE 4 + +typedef struct { + char_t dusk[PACKET_WELCOME_SIZE]; +} packetwelcome_t; + +/** + * Creates a welcome packet. + * + * @param packet Pointer to the packet structure to initialize. + */ +void packetWelcomeCreate(packet_t *packet); \ No newline at end of file diff --git a/src/server/server.c b/src/server/server.c index 6fa0934..7b58e91 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -14,21 +14,20 @@ server_t SERVER; void serverInit() { memoryZero(&SERVER, sizeof(server_t)); - pthread_mutex_init(&SERVER.startMutex, NULL); - pthread_cond_init(&SERVER.startCond, NULL); + SERVER.state = SERVER_STATE_STOPPED; } errorret_t serverStart(uint16_t port) { - assertFalse(SERVER.isRunning, "Server is already running?"); - consolePrint("Starting server on port %d...\n", port); + if(SERVER.state != SERVER_STATE_STOPPED) { + return error("Server is already running"); + } - pthread_mutex_lock(&SERVER.startMutex); - SERVER.threadReady = false; + consolePrint("Starting server on port %d...\n", port); + memoryZero(&SERVER, sizeof(server_t)); // Initialize the server socket SERVER.serverSocket = socket(AF_INET, SOCK_STREAM, 0); if(SERVER.serverSocket < 0) { - pthread_mutex_unlock(&SERVER.startMutex); return error("Failed to create socket"); } SERVER.serverAddress.sin_family = AF_INET; @@ -41,35 +40,32 @@ errorret_t serverStart(uint16_t port) { (struct sockaddr *)&SERVER.serverAddress, sizeof(SERVER.serverAddress) ); - if(res < 0) { + if(res != 0) { close(SERVER.serverSocket); - pthread_mutex_unlock(&SERVER.startMutex); return error("Failed to bind socket"); } // Set the socket to listen for incoming connections res = listen(SERVER.serverSocket, SERVER_MAX_CLIENTS); - if(res < 0) { + if(res != 0) { close(SERVER.serverSocket); - pthread_mutex_unlock(&SERVER.startMutex); return error("Failed to listen on socket"); } - // Start the server thread - SERVER.isRunning = true; - if(pthread_create(&SERVER.thread, NULL, serverThread, NULL) != 0) { + // Start the server thread. + SERVER.state = SERVER_STATE_STARTING; + res = pthread_create(&SERVER.thread, NULL, serverThread, NULL); + if(res != 0) { perror("Failed to create server thread"); serverStop(); - pthread_mutex_unlock(&SERVER.startMutex); return error("Failed to create server thread"); } - // Wait for the server thread to be ready - while(!SERVER.threadReady) { - pthread_cond_wait(&SERVER.startCond, &SERVER.startMutex); + // Wait + while(SERVER.state == SERVER_STATE_STARTING) { + usleep(1000); } - - pthread_mutex_unlock(&SERVER.startMutex); + consolePrint("Server started."); return ERROR_OK; } @@ -78,18 +74,17 @@ void * serverThread(void *arg) { struct timeval timeout; fd_set readfds; - pthread_mutex_lock(&SERVER.startMutex); - SERVER.threadReady = true; - pthread_cond_signal(&SERVER.startCond); - pthread_mutex_unlock(&SERVER.startMutex); + SERVER.state = SERVER_STATE_RUNNING; - while(SERVER.isRunning) { - usleep(SERVER_TIMEOUT * 1000); + while(SERVER.state == SERVER_STATE_RUNNING) { + usleep(1000); FD_ZERO(&readfds); FD_SET(SERVER.serverSocket, &readfds); - timeout.tv_usec = SERVER_TIMEOUT * 1000; + // Fix timeout struct + timeout.tv_sec = 1; + timeout.tv_usec = 0; // Wait for int32_t activity = select( @@ -119,7 +114,6 @@ void * serverThread(void *arg) { continue; } - // Initialize the client serverclient_t *client = NULL; uint8_t i = 0; @@ -147,14 +141,17 @@ void * serverThread(void *arg) { errorFlush(); close(clientSocket); } - - usleep(1000); } printf("Server thread exiting.\n"); - assertFalse( - SERVER.isRunning, "Server thread exiting while server is running?" + assertTrue( + SERVER.state != SERVER_STATE_RUNNING, + "Server thread exiting while server is running?" ); + + SERVER.thread = 0; + SERVER.state = SERVER_STATE_STOPPED; + return NULL; } @@ -168,9 +165,36 @@ uint8_t serverGetClientCount() { } void serverStop() { - consolePrint("Stopping server..."); + if(SERVER.state == SERVER_STATE_STOPPED) return; - SERVER.isRunning = false; + assertTrue( + SERVER.state != SERVER_STATE_STARTING, + "Stopping a server that is starting?\n" + ); + + consolePrint("Stopping server..."); + SERVER.state = SERVER_STATE_STOPPING; + + // Wait for the server thread to finish + int32_t maxWaits = 0; + while(SERVER.state == SERVER_STATE_STOPPING) { + usleep(1000); + maxWaits++; + if(maxWaits < 1000) continue; + + consolePrint("Server thread did not stop in time, forcing exit."); + SERVER.state = SERVER_STATE_STOPPED; + break; + } + + // Kill the clients + uint8_t i = 0; + do { + serverclient_t *client = &SERVER.clients[i++]; + serverClientClose(client); + } while(i < SERVER_MAX_CLIENTS); + + // Stop the server if(SERVER.serverSocket >= 0) { close(SERVER.serverSocket); SERVER.serverSocket = -1; @@ -181,9 +205,6 @@ void serverStop() { SERVER.thread = 0; } - pthread_mutex_destroy(&SERVER.startMutex); - pthread_cond_destroy(&SERVER.startCond); - consolePrint("Server stopped."); } diff --git a/src/server/server.h b/src/server/server.h index 7b3d315..191bcf8 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -12,16 +12,19 @@ #define SERVER_MAX_CLIENTS 32 #define SERVER_MAX_CHANNELS 2 -#define SERVER_TIMEOUT 200 + +typedef enum { + SERVER_STATE_STOPPED, + SERVER_STATE_STARTING, + SERVER_STATE_RUNNING, + SERVER_STATE_STOPPING +} serverstate_t; typedef struct { - bool_t isRunning; + serverstate_t state; pthread_t thread; int serverSocket; struct sockaddr_in serverAddress; - pthread_mutex_t startMutex; - pthread_cond_t startCond; - bool_t threadReady; serverclient_t clients[SERVER_MAX_CLIENTS]; } server_t; diff --git a/src/server/serverclient.c b/src/server/serverclient.c index adbb5c7..54815d4 100644 --- a/src/server/serverclient.c +++ b/src/server/serverclient.c @@ -6,6 +6,8 @@ */ #include "serverclient.h" +#include "server.h" +#include "assert/assert.h" #include "util/memory.h" #include #include "console/console.h" @@ -18,8 +20,9 @@ errorret_t serverClientAccept( client->socket = socket; client->state = SERVER_CLIENT_STATE_ACCEPTING; - // Set timeout to 20 seconds - client->timeout.tv_usec = SERVER_CLIENT_TIMEOUT * 1000; + // Set timeout to 8 seconds + client->timeout.tv_sec = 8; + client->timeout.tv_usec = 0; // Create a thread for the client if(pthread_create(&client->thread, NULL, serverClientThread, client) != 0) { @@ -33,6 +36,10 @@ errorret_t serverClientSend( const uint8_t *data, const size_t len ) { + assertNotNull(client, "Client is NULL"); + assertNotNull(data, "Data is NULL"); + assertTrue(len > 0, "Data length is 0"); + if(client->state != SERVER_CLIENT_STATE_ACCEPTING) { return error("Client is not accepting"); } @@ -48,62 +55,156 @@ errorret_t serverClientSendString( return serverClientSend(client, (const uint8_t *)data, strlen(data)); } +errorret_t serverClientSendPacket( + serverclient_t * client, + const packet_t *packet +) { + assertNotNull(client, "Client is NULL"); + assertNotNull(packet, "Packet is NULL"); + assertTrue(packet->type != PACKET_TYPE_INVALID, "Packet type is INVALID"); + assertTrue(packet->length > 0, "Packet length is 0"); + assertTrue( + packet->length <= sizeof(packetdata_t), + "Packet length is too large (1)" + ); + + size_t fullSize = sizeof(packet_t) - sizeof(packet->data) + packet->length; + assertTrue(fullSize <= sizeof(packet_t), "Packet size is too large (2)"); + + return serverClientSend(client, (const uint8_t *)packet, fullSize); +} + ssize_t serverClientReceive( serverclient_t * client, uint8_t *buffer, const size_t len ) { - if(client->state != SERVER_CLIENT_STATE_ACCEPTING) return -1; - ssize_t received = recv(client->socket, buffer, len, 0); - if(received < 0) return received; - return received; + assertNotNull(client, "Client is NULL"); + assertNotNull(buffer, "Buffer is NULL"); + assertTrue(len > 0, "Buffer length is 0"); + + if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) return -1; + if(client->state == SERVER_CLIENT_STATE_DISCONNECTING) return -1; + + return recv(client->socket, buffer, len, 0); } void serverClientClose(serverclient_t *client) { - client->state = SERVER_CLIENT_STATE_DISCONNECTED; - close(client->socket); + assertNotNull(client, "Client is NULL"); + if(client->state == SERVER_CLIENT_STATE_DISCONNECTED) return; + + // Mark client as disconnecting + client->state = SERVER_CLIENT_STATE_DISCONNECTING; + + int32_t maxWaits = 0; + while(client->state == SERVER_CLIENT_STATE_DISCONNECTING) { + // Wait for the thread to finish + usleep(1000); + maxWaits++; + if(maxWaits > 10) break; + } + pthread_join(client->thread, NULL); client->thread = 0; + + // Close the socket + if(client->socket != -1) { + close(client->socket); + client->socket = -1; + } + + client->state = SERVER_CLIENT_STATE_DISCONNECTED; + consolePrint("Client %d disconnected.", client->socket); +} + +void serverClientCloseOnThread(serverclient_t *client, const char_t *reason) { + assertNotNull(client, "Client is NULL"); + assertNotNull(reason, "Reason is NULL"); + + client->state = SERVER_CLIENT_STATE_DISCONNECTING; + + close(client->socket); client->socket = -1; - consolePrint("Client disconnected."); + + client->state = SERVER_CLIENT_STATE_DISCONNECTED; + client->thread = 0; + + consolePrint("Client %d disconnected: %s", client->socket, reason); + pthread_exit(NULL); } void *serverClientThread(void *arg) { + assertNotNull(arg, "Client is NULL"); + serverclient_t *client = (serverclient_t *)arg; char_t buffer[1024]; ssize_t read; + errorret_t err; + packet_t packet; - printf("Client thread started for socket %d.\n", client->socket); + consolePrint("Accepting client on socket %d.", client->socket); // Set socket timeout - setsockopt( + if(setsockopt( client->socket, SOL_SOCKET, SO_RCVTIMEO, &client->timeout, sizeof(client->timeout) - ); - - // Send welcome message - serverClientSendString(client, "DUSK|"DUSK_VERSION); - - // Receive version from client - read = serverClientReceive(client, buffer, sizeof(buffer)); - - // First 5 bytes should be "DUSK|" - if(read < 5 || strncmp(buffer, "DUSK|", 5) != 0) { - serverClientSendString(client, "ERR|Invalid version (0)"); - serverClientClose(client); + ) < 0) { + serverClientCloseOnThread(client, "Failed to set socket timeout"); return NULL; } - // Next (up to X bytes where X is the length of the version string) - // should be the version string - buffer[read] = '\0'; // Null-terminate the string + // First message from the client should always be "DUSK|VERSION" to match + // the server version. + { + const char_t *expecting = "DUSK|"DUSK_VERSION; + read = serverClientReceive(client, buffer, sizeof(buffer)); + if(read <= 0) { + packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_INVALID_VERSION); + err = serverClientSendPacket(client, &packet); + serverClientCloseOnThread(client, "Failed to receive version"); + return NULL; + } + buffer[read] = '\0'; // Null-terminate the string + if(strncmp(buffer, expecting, strlen(expecting)) != 0) { + packetDisconnectCreate(&packet, PACKET_DISCONNECT_REASON_INVALID_VERSION); + err = serverClientSendPacket(client, &packet); + serverClientCloseOnThread(client, "Invalid version"); + return NULL; + } + } - printf("Received version: %s\n", buffer); - - serverClientClose(client); + // Send DUSK back! + packetWelcomeCreate(&packet); + err = serverClientSendPacket(client, &packet); + if(err != ERROR_OK) { + serverClientCloseOnThread(client, "Failed to send welcome message"); + return NULL; + } + + client->state = SERVER_CLIENT_STATE_CONNECTED; + while(client->state == SERVER_CLIENT_STATE_CONNECTED) { + read = serverClientReceive(client, buffer, sizeof(buffer)); + + if(read <= 0) { + serverClientCloseOnThread(client, "Failed to receive data"); + break; + } + + buffer[read] = '\0'; // Null-terminate the string + consolePrint("Received: %s", buffer); + + + if(SERVER.state != SERVER_STATE_RUNNING) { + serverClientCloseOnThread(client, "Server is shutting down"); + break; + } + } + + serverClientSendString(client, "Server is shutting down"); + client->state = SERVER_CLIENT_STATE_DISCONNECTED; return NULL; } \ No newline at end of file diff --git a/src/server/serverclient.h b/src/server/serverclient.h index b38addc..5f65a5c 100644 --- a/src/server/serverclient.h +++ b/src/server/serverclient.h @@ -7,23 +7,28 @@ #pragma once #include "error/error.h" - -#define SERVER_CLIENT_TIMEOUT 20000 // 20 seconds +#include "server/packet/packet.h" typedef enum { SERVER_CLIENT_STATE_DISCONNECTED, SERVER_CLIENT_STATE_ACCEPTING, + SERVER_CLIENT_STATE_CONNECTED, + SERVER_CLIENT_STATE_DISCONNECTING, } serverclientstate_t; typedef struct { int32_t socket; serverclientstate_t state; - pthread_t thread; // Add thread field - struct timeval timeout; // Add timeout field + pthread_t thread; + struct timeval timeout; } serverclient_t; /** * Accepts an incoming connection from a server client. + * + * @param client Pointer to the server client structure. + * @param socket The socket file descriptor for the client. + * @return Error code indicating success or failure. */ errorret_t serverClientAccept( serverclient_t *client, @@ -56,6 +61,18 @@ errorret_t serverClientSendString( const char_t *data ); +/** + * Sends a packet to a server client. + * + * @param client Pointer to the server client structure. + * @param packet Pointer to the packet to send. + * @return Error code indicating success or failure. + */ +errorret_t serverClientSendPacket( + serverclient_t * client, + const packet_t *packet +); + /** * Receives data from a server client. * @@ -71,15 +88,28 @@ ssize_t serverClientReceive( ); /** - * Closes the connection to a server client. + * Closes the connection to a server client. Waits for the thread to finish. + * + * @param client Pointer to the server client structure. */ -void serverClientClose( - serverclient_t *client +void serverClientClose(serverclient_t *client); + +/** + * Closes the connection to a server client without waiting for the thread to + * finish. + * + * @param client Pointer to the server client structure. + * @param reason Reason for closing the connection. + */ +void serverClientCloseOnThread( + serverclient_t *client, + const char_t *reason ); /** * Thread function for handling a server client. * * @param arg Pointer to the server client structure. + * @return NULL. */ void *serverClientThread(void *arg); \ No newline at end of file diff --git a/src/util/string.c b/src/util/string.c index 301db22..e5b9169 100644 --- a/src/util/string.c +++ b/src/util/string.c @@ -82,4 +82,46 @@ int32_t stringFormatVA( assertTrue(ret >= 0, "Failed to format string."); assertTrue(ret < destSize, "Formatted string is too long."); return ret; +} + +bool_t stringToI32(const char_t *str, int32_t *out) { + assertNotNull(str, "str must not be NULL"); + assertNotNull(out, "out must not be NULL"); + + char_t *endptr; + errno = 0; + long int result = strtol(str, &endptr, 10); + if (errno != 0 || *endptr != '\0') { + return false; + } + *out = (int32_t)result; + return true; +} + +bool_t stringToI64(const char_t *str, int64_t *out) { + assertNotNull(str, "str must not be NULL"); + assertNotNull(out, "out must not be NULL"); + + char_t *endptr; + errno = 0; + long long int result = strtoll(str, &endptr, 10); + if (errno != 0 || *endptr != '\0') { + return false; + } + *out = (int64_t)result; + return true; +} + +bool_t stringToU16(const char_t *str, uint16_t *out) { + assertNotNull(str, "str must not be NULL"); + assertNotNull(out, "out must not be NULL"); + + char_t *endptr; + errno = 0; + unsigned long int result = strtoul(str, &endptr, 10); + if (errno != 0 || *endptr != '\0' || result > UINT16_MAX) { + return false; + } + *out = (uint16_t)result; + return true; } \ No newline at end of file diff --git a/src/util/string.h b/src/util/string.h index 9b29a19..5484e33 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -79,4 +79,31 @@ int32_t stringFormatVA( const size_t destSize, char_t *format, va_list args -); \ No newline at end of file +); + +/** + * Converts a string to an integer. + * + * @param str The string to convert. + * @param out The output integer. + * @return TRUE if the conversion was successful, FALSE otherwise. + */ +bool_t stringToI32(const char_t *str, int32_t *out); + +/** + * Converts a string to a signed 16-bit integer. + * + * @param str The string to convert. + * @param out The output signed integer. + * @return TRUE if the conversion was successful, FALSE otherwise. + */ +bool_t stringToI16(const char_t *str, int16_t *out); + +/** + * Converts a string to an unsigned 16-bit integer. + * + * @param str The string to convert. + * @param out The output unsigned integer. + * @return TRUE if the conversion was successful, FALSE otherwise. + */ +bool_t stringToU16(const char_t *str, uint16_t *out); \ No newline at end of file