This commit is contained in:
2026-07-16 19:08:32 -05:00
parent dcf5f434c5
commit d4a17bd98d
32 changed files with 2410 additions and 4 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
errorChain(systemInit());
errorChain(inputInit());
errorChain(assetInit());
// errorChain(saveInit());
errorChain(saveInit());
errorChain(localeManagerInit());
errorChain(displayInit());
errorChain(uiInit());
@@ -88,7 +88,7 @@ errorret_t engineDispose(void) {
errorChain(uiDispose());
consoleDispose();
errorChain(displayDispose());
// errorChain(saveDispose());
errorChain(saveDispose());
errorChain(assetDispose());
errorOk();
+3
View File
@@ -8,3 +8,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
network.c
networkinfo.c
)
# Subdirs
add_subdirectory(http)
+14
View File
@@ -0,0 +1,14 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networkhttpheader.c
networkhttpurl.c
networkhttprequest.c
networkhttpprocess.c
networkhttpthread.c
networkhttp.c
)
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttp.h"
#include "networkhttprequest.h"
#include "networkhttpthread.h"
networkhttp_t NETWORK_HTTP;
errorret_t networkHttpInit(void) {
networkHttpRequestPoolInit();
threadInit(&NETWORK_HTTP.thread, networkHttpThreadRun);
threadStart(&NETWORK_HTTP.thread);
errorOk();
}
errorret_t networkHttpUpdate(void) {
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
threadMutexLock(&request->mutex);
const networkhttprequeststate_t state = request->state;
threadMutexUnlock(&request->mutex);
if(state == NETWORK_HTTP_REQUEST_STATE_DONE) {
eventInvoke(&request->onComplete, request);
} else if(state == NETWORK_HTTP_REQUEST_STATE_ERROR) {
eventInvoke(&request->onError, request);
} else {
continue;
}
networkHttpRequestReset(request);
}
errorOk();
}
errorret_t networkHttpDispose(void) {
threadStop(&NETWORK_HTTP.thread);
errorOk();
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "thread/thread.h"
typedef struct {
thread_t thread;
} networkhttp_t;
extern networkhttp_t NETWORK_HTTP;
/**
* Initializes the HTTP client: prepares the request pool and starts
* the background worker thread. Called once during engine startup.
*
* @return Any error that occurs.
*/
errorret_t networkHttpInit(void);
/**
* Dispatches any requests that finished on the background thread since
* the last call: fires onComplete/onError on the main thread, frees
* the response, and returns the slot to the pool. Call once per frame.
*
* @return Any error that occurs.
*/
errorret_t networkHttpUpdate(void);
/**
* Stops the background worker thread and disposes of the HTTP client.
*
* @return Any error that occurs.
*/
errorret_t networkHttpDispose(void);
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttpheader.h"
#include "util/memory.h"
#include "util/string.h"
void networkHttpHeaderListClear(networkhttpheaderlist_t *list) {
memoryZero(list, sizeof(networkhttpheaderlist_t));
}
bool_t networkHttpHeaderIsReserved(const char_t *name) {
return
stringCompareInsensitive(name, "Host") == 0 ||
stringCompareInsensitive(name, "Connection") == 0 ||
stringCompareInsensitive(name, "Content-Length") == 0;
}
void networkHttpHeaderListAdd(
networkhttpheaderlist_t *list,
const char_t *name,
const char_t *value,
const bool_t skipReserved
) {
if(list->count >= NETWORK_HTTP_HEADER_COUNT_MAX) return;
if(skipReserved && networkHttpHeaderIsReserved(name)) return;
networkhttpheader_t *header = &list->headers[list->count];
stringCopy(header->name, name, NETWORK_HTTP_HEADER_NAME_MAX - 1);
stringCopy(header->value, value, NETWORK_HTTP_HEADER_VALUE_MAX - 1);
list->count++;
}
const networkhttpheader_t * networkHttpHeaderListFind(
const networkhttpheaderlist_t *list,
const char_t *name
) {
for(uint32_t i = 0; i < list->count; i++) {
if(stringCompareInsensitive(list->headers[i].name, name) == 0) {
return &list->headers[i];
}
}
return NULL;
}
+70
View File
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
#define NETWORK_HTTP_HEADER_COUNT_MAX 16
#define NETWORK_HTTP_HEADER_NAME_MAX 64
#define NETWORK_HTTP_HEADER_VALUE_MAX 256
typedef struct {
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
} networkhttpheader_t;
typedef struct {
networkhttpheader_t headers[NETWORK_HTTP_HEADER_COUNT_MAX];
uint32_t count;
} networkhttpheaderlist_t;
/**
* Clears a header list, removing all entries.
*
* @param list The header list to clear.
*/
void networkHttpHeaderListClear(networkhttpheaderlist_t *list);
/**
* Determines whether a header name is reserved, meaning it is computed
* internally by the request writer and must not be set by the caller.
* This is Host, Connection and Content-Length.
*
* @param name The header name to check, compared case-insensitively.
* @return true if the header name is reserved.
*/
bool_t networkHttpHeaderIsReserved(const char_t *name);
/**
* Appends a header to the list, silently doing nothing if the list is
* already full or if skipReserved is true and the name is reserved (see
* networkHttpHeaderIsReserved).
*
* @param list The header list to append to.
* @param name The header name.
* @param value The header value.
* @param skipReserved If true, reserved header names are ignored.
*/
void networkHttpHeaderListAdd(
networkhttpheaderlist_t *list,
const char_t *name,
const char_t *value,
const bool_t skipReserved
);
/**
* Finds the first header in the list matching the given name, compared
* case-insensitively.
*
* @param list The header list to search.
* @param name The header name to search for.
* @return Pointer to the matching header, or NULL if not found.
*/
const networkhttpheader_t * networkHttpHeaderListFind(
const networkhttpheaderlist_t *list,
const char_t *name
);
+346
View File
@@ -0,0 +1,346 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttpprocess.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
const char_t * networkHttpProcessMethodName(
const networkhttpmethod_t method
) {
switch(method) {
case NETWORK_HTTP_METHOD_POST: return "POST";
case NETWORK_HTTP_METHOD_PUT: return "PUT";
case NETWORK_HTTP_METHOD_GET:
default:
return "GET";
}
}
errorret_t networkHttpProcessBuildHead(
const networkhttprequest_t *request,
const networkhttpurl_t *target,
char_t *dest,
const size_t destSize,
size_t *outLength
) {
size_t cursor = 0;
errorChain(networkHttpUrlAppend(
dest, destSize, &cursor, networkHttpProcessMethodName(request->method)
));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " "));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->path));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, " HTTP/1.1\r\n"));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "Host: "));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, target->host));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
errorChain(networkHttpUrlAppend(
dest, destSize, &cursor, "Connection: close\r\n"
));
for(uint32_t i = 0; i < request->requestHeaders.count; i++) {
const networkhttpheader_t *header = &request->requestHeaders.headers[i];
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->name));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, ": "));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, header->value));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
}
if(request->bodyLength > 0) {
char_t lengthStr[24];
stringFormat(lengthStr, sizeof(lengthStr) - 1, "%zu", request->bodyLength);
errorChain(networkHttpUrlAppend(
dest, destSize, &cursor, "Content-Length: "
));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, lengthStr));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
}
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "\r\n"));
*outLength = cursor;
errorOk();
}
errorret_t networkHttpProcessSendAll(
networksocketplatform_t *sock,
const uint8_t *data,
const size_t length
) {
size_t sentTotal = 0;
while(sentTotal < length) {
size_t sent = 0;
errorChain(networkSocketPlatformSend(
sock, data + sentTotal, length - sentTotal, &sent
));
if(sent == 0) errorThrow("Connection closed while sending data");
sentTotal += sent;
}
errorOk();
}
errorret_t networkHttpProcessReceiveHeaders(
networksocketplatform_t *sock,
char_t *headerBuf,
const size_t headerBufSize,
uint8_t *leftover,
const size_t leftoverBufSize,
size_t *outLeftoverLength
) {
size_t headerLength = 0;
headerBuf[0] = '\0';
uint8_t chunk[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
for(;;) {
size_t received = 0;
errorChain(networkSocketPlatformReceive(
sock, chunk, sizeof(chunk), &received
));
if(received == 0) {
errorThrow("Connection closed before response headers completed");
}
if(headerLength + received >= headerBufSize) {
errorThrow("Response headers too large");
}
memoryCopy(headerBuf + headerLength, chunk, received);
headerLength += received;
headerBuf[headerLength] = '\0';
char_t *terminator = strstr(headerBuf, "\r\n\r\n");
if(terminator == NULL) continue;
const size_t terminatorOffset = (size_t)(terminator - headerBuf);
const size_t bodyStart = terminatorOffset + 4;
const size_t extra = headerLength - bodyStart;
if(extra > leftoverBufSize) errorThrow("Too much body data buffered");
if(extra > 0) memoryCopy(leftover, headerBuf + bodyStart, extra);
*outLeftoverLength = extra;
// Keep the trailing "\r\n" of the last header line, drop the blank
// line so callers can split on "\r\n" without a special case.
headerBuf[terminatorOffset + 2] = '\0';
break;
}
errorOk();
}
errorret_t networkHttpProcessParseStatusLine(
const char_t *line,
uint16_t *outStatus
) {
const char_t *space = strchr(line, ' ');
if(space == NULL) errorThrow("Malformed status line: %s", line);
char_t statusStr[4];
if(strlen(space + 1) < 3) errorThrow("Malformed status line: %s", line);
memoryCopy(statusStr, space + 1, 3);
statusStr[3] = '\0';
if(!stringToU16(statusStr, outStatus)) {
errorThrow("Malformed status code: %s", line);
}
errorOk();
}
void networkHttpProcessParseHeaderLine(
const char_t *line,
networkhttpheaderlist_t *outHeaders
) {
const char_t *colon = strchr(line, ':');
if(colon == NULL) return;
const size_t nameLength = (size_t)(colon - line);
if(nameLength == 0 || nameLength >= NETWORK_HTTP_HEADER_NAME_MAX) return;
char_t name[NETWORK_HTTP_HEADER_NAME_MAX];
memoryCopy(name, line, nameLength);
name[nameLength] = '\0';
const char_t *valueStart = colon + 1;
while(*valueStart == ' ') valueStart++;
const size_t valueLength = strlen(valueStart);
if(valueLength >= NETWORK_HTTP_HEADER_VALUE_MAX) return;
char_t value[NETWORK_HTTP_HEADER_VALUE_MAX];
memoryCopy(value, valueStart, valueLength + 1);
networkHttpHeaderListAdd(outHeaders, name, value, false);
}
errorret_t networkHttpProcessParseHeaders(
char_t *headerBuf,
uint16_t *outStatus,
networkhttpheaderlist_t *outHeaders
) {
char_t *cursor = headerBuf;
bool_t first = true;
for(;;) {
char_t *lineEnd = strstr(cursor, "\r\n");
const bool_t isLast = lineEnd == NULL;
if(lineEnd != NULL) *lineEnd = '\0';
if(first) {
errorChain(networkHttpProcessParseStatusLine(cursor, outStatus));
first = false;
} else if(cursor[0] != '\0') {
networkHttpProcessParseHeaderLine(cursor, outHeaders);
}
if(isLast) break;
cursor = lineEnd + 2;
}
errorOk();
}
errorret_t networkHttpProcessReadBody(
networksocketplatform_t *sock,
const networkhttpheaderlist_t *headers,
const uint8_t *leftover,
const size_t leftoverLength,
uint8_t **outBody,
size_t *outBodyLength
) {
const networkhttpheader_t *contentLength =
networkHttpHeaderListFind(headers, "Content-Length");
if(contentLength != NULL) {
int64_t expected = 0;
if(!stringToI64(contentLength->value, &expected) || expected < 0) {
errorThrow("Malformed Content-Length header: %s", contentLength->value);
}
if(expected == 0) {
*outBody = NULL;
*outBodyLength = 0;
errorOk();
}
uint8_t *body = memoryAllocate((size_t)expected);
const size_t initialCopy =
leftoverLength < (size_t)expected ? leftoverLength : (size_t)expected;
if(initialCopy > 0) memoryCopy(body, leftover, initialCopy);
size_t received = initialCopy;
while(received < (size_t)expected) {
size_t got = 0;
errorret_t ret = networkSocketPlatformReceive(
sock, body + received, (size_t)expected - received, &got
);
if(errorIsNotOk(ret)) {
memoryFree(body);
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
}
if(got == 0) {
memoryFree(body);
errorThrow("Connection closed before full body was received");
}
received += got;
}
*outBody = body;
*outBodyLength = (size_t)expected;
errorOk();
}
// No Content-Length: read until the connection closes.
size_t capacity = NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE;
if(leftoverLength > capacity) capacity = leftoverLength;
uint8_t *body = memoryAllocate(capacity);
size_t length = 0;
if(leftoverLength > 0) {
memoryCopy(body, leftover, leftoverLength);
length = leftoverLength;
}
for(;;) {
if(length == capacity) {
const size_t newCapacity = capacity * 2;
memoryResize((void **)&body, capacity, newCapacity);
capacity = newCapacity;
}
size_t got = 0;
errorret_t ret = networkSocketPlatformReceive(
sock, body + length, capacity - length, &got
);
if(errorIsNotOk(ret)) {
memoryFree(body);
return errorChainImpl(ret, __FILE__, __func__, __LINE__);
}
if(got == 0) break;
length += got;
}
*outBody = body;
*outBodyLength = length;
errorOk();
}
errorret_t networkHttpProcessAttempt(
const networkhttprequest_t *request,
const networkhttpurl_t *target,
networkhttpresponse_t *outResponse
) {
memoryZero(outResponse, sizeof(networkhttpresponse_t));
networksocketplatform_t sock;
errorChain(networkSocketPlatformConnect(&sock, target->host, target->port));
char_t head[NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE];
size_t headLength = 0;
networkHttpProcessErrorChain(&sock, networkHttpProcessBuildHead(
request, target, head, NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE, &headLength
));
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
&sock, (const uint8_t *)head, headLength
));
if(request->bodyLength > 0) {
networkHttpProcessErrorChain(&sock, networkHttpProcessSendAll(
&sock, request->body, request->bodyLength
));
}
char_t headerBuf[NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE];
uint8_t leftover[NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE];
size_t leftoverLength = 0;
networkHttpProcessErrorChain(&sock, networkHttpProcessReceiveHeaders(
&sock, headerBuf, NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE,
leftover, NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE, &leftoverLength
));
networkHttpProcessErrorChain(&sock, networkHttpProcessParseHeaders(
headerBuf, &outResponse->status, &outResponse->headers
));
errorret_t bodyRet = networkHttpProcessReadBody(
&sock, &outResponse->headers, leftover, leftoverLength,
&outResponse->body, &outResponse->bodyLength
);
networkSocketPlatformClose(&sock);
errorChain(bodyRet);
errorOk();
}
+185
View File
@@ -0,0 +1,185 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "network/http/networkhttprequest.h"
#include "network/http/networkhttpurl.h"
#include "network/networksocketplatform.h"
#define NETWORK_HTTP_PROCESS_HEAD_BUF_SIZE 4096
#define NETWORK_HTTP_PROCESS_HEADER_BUF_SIZE 8192
#define NETWORK_HTTP_PROCESS_READ_CHUNK_SIZE 2048
#define NETWORK_HTTP_PROCESS_BODY_INITIAL_SIZE 4096
/**
* Shorthand to chain an error, closing sock first if expr failed. Used
* for every step of an attempt after the socket has connected, since
* failing partway through must not leak the socket.
*/
#define networkHttpProcessErrorChain(sock, _expr) { \
errorret_t _nhpErr = (_expr); \
if(errorIsNotOk(_nhpErr)) { \
networkSocketPlatformClose(sock); \
return errorChainImpl(_nhpErr, __FILE__, __func__, __LINE__); \
} \
}
/**
* Returns the wire method name ("GET", "POST", "PUT") for method.
*
* @param method The method to name.
* @return A static string naming the method.
*/
const char_t * networkHttpProcessMethodName(
const networkhttpmethod_t method
);
/**
* Builds the request line, headers and blank line terminator (but not
* the body) for request, targeting target, into dest.
*
* @param request The request being sent.
* @param target The resolved host/port/path for this attempt.
* @param dest The destination buffer.
* @param destSize The size of dest, including the null terminator.
* @param outLength The number of bytes written is written here.
* @return An error if the result would not fit in dest.
*/
errorret_t networkHttpProcessBuildHead(
const networkhttprequest_t *request,
const networkhttpurl_t *target,
char_t *dest,
const size_t destSize,
size_t *outLength
);
/**
* Sends length bytes of data over sock, looping until all of it has
* been written.
*
* @param sock The connected socket.
* @param data The data to send.
* @param length The number of bytes in data.
* @return An error if the send failed or the connection closed early.
*/
errorret_t networkHttpProcessSendAll(
networksocketplatform_t *sock,
const uint8_t *data,
const size_t length
);
/**
* Reads from sock until the blank line ("\r\n\r\n") terminating the
* response headers has been seen, writing the status line and header
* lines (without the blank line) into headerBuf as a null-terminated
* string. Any body bytes read past the terminator in the same receive
* are written to leftover.
*
* @param sock The connected socket.
* @param headerBuf The destination buffer for the header text.
* @param headerBufSize The size of headerBuf, including the null
* terminator.
* @param leftover Destination buffer for any body bytes read early.
* @param leftoverBufSize The size of leftover.
* @param outLeftoverLength The number of bytes written to leftover is
* written here.
* @return An error if the headers could not be read, were malformed,
* or did not fit within the given buffers.
*/
errorret_t networkHttpProcessReceiveHeaders(
networksocketplatform_t *sock,
char_t *headerBuf,
const size_t headerBufSize,
uint8_t *leftover,
const size_t leftoverBufSize,
size_t *outLeftoverLength
);
/**
* Parses a "HTTP/1.1 <status> <reason>" status line.
*
* @param line The status line, null-terminated.
* @param outStatus The parsed status code is written here.
* @return An error if the status line is malformed.
*/
errorret_t networkHttpProcessParseStatusLine(
const char_t *line,
uint16_t *outStatus
);
/**
* Parses a single "Name: Value" response header line and appends it to
* outHeaders. Silently does nothing if the line has no colon or the
* name/value are too long to fit a networkhttpheader_t.
*
* @param line The header line, null-terminated.
* @param outHeaders The header list to append to.
*/
void networkHttpProcessParseHeaderLine(
const char_t *line,
networkhttpheaderlist_t *outHeaders
);
/**
* Parses the status line and every header line out of headerBuf (as
* produced by networkHttpProcessReceiveHeaders). Mutates headerBuf in
* place to split it into lines.
*
* @param headerBuf The header text to parse.
* @param outStatus The parsed status code is written here.
* @param outHeaders The header list to populate.
* @return An error if the status line is malformed.
*/
errorret_t networkHttpProcessParseHeaders(
char_t *headerBuf,
uint16_t *outStatus,
networkhttpheaderlist_t *outHeaders
);
/**
* Reads the response body from sock. If headers contains a
* Content-Length, reads exactly that many bytes; otherwise reads until
* the connection closes. Either way, leftover/leftoverLength (body
* bytes already read while looking for the header terminator) are
* included at the start of the result.
*
* @param sock The connected socket.
* @param headers The parsed response headers.
* @param leftover Body bytes already read past the header terminator.
* @param leftoverLength The number of bytes in leftover.
* @param outBody A memoryAllocate'd buffer holding the body is written
* here, or NULL if the body is empty. Owned by the caller.
* @param outBodyLength The number of bytes in *outBody is written here.
* @return An error if the body could not be fully read.
*/
errorret_t networkHttpProcessReadBody(
networksocketplatform_t *sock,
const networkhttpheaderlist_t *headers,
const uint8_t *leftover,
const size_t leftoverLength,
uint8_t **outBody,
size_t *outBodyLength
);
/**
* Performs a single blocking HTTP exchange: connects to target, writes
* the request head + body, and reads back one parsed response. Does
* not follow redirects -- that is the caller's responsibility.
*
* @param request The request to send (method, headers, body).
* @param target The resolved host/port/path to connect to for this
* attempt (may differ from the request's own URL if this is a
* redirected attempt).
* @param outResponse The parsed response is written here on success.
* @return An error if the exchange could not be completed.
*/
errorret_t networkHttpProcessAttempt(
const networkhttprequest_t *request,
const networkhttpurl_t *target,
networkhttpresponse_t *outResponse
);
+121
View File
@@ -0,0 +1,121 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttprequest.h"
#include "util/memory.h"
#include "assert/assert.h"
networkhttprequest_t NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
void networkHttpRequestPoolInit(void) {
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
memoryZero(request, sizeof(networkhttprequest_t));
threadMutexInit(&request->mutex);
eventInit(
&request->onComplete, request->onCompleteCallbacks,
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
);
eventInit(
&request->onError, request->onErrorCallbacks,
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
);
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
}
}
networkhttprequest_t * networkHttpRequestClaim(void) {
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
threadMutexLock(&request->mutex);
if(request->state != NETWORK_HTTP_REQUEST_STATE_FREE) {
threadMutexUnlock(&request->mutex);
continue;
}
threadMutexUnlock(&request->mutex);
return request;
}
assertUnreachable("No available HTTP request slots.");
return NULL;
}
void networkHttpRequestReset(networkhttprequest_t *request) {
assertNotNull(request, "request must not be NULL");
threadMutexLock(&request->mutex);
if(request->body != NULL) memoryFree(request->body);
request->body = NULL;
request->bodyLength = 0;
if(request->response.body != NULL) memoryFree(request->response.body);
memoryZero(&request->response, sizeof(networkhttpresponse_t));
networkHttpHeaderListClear(&request->requestHeaders);
eventInit(
&request->onComplete, request->onCompleteCallbacks,
request->onCompleteUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
);
eventInit(
&request->onError, request->onErrorCallbacks,
request->onErrorUsers, NETWORK_HTTP_REQUEST_EVENT_MAX
);
request->state = NETWORK_HTTP_REQUEST_STATE_FREE;
threadMutexUnlock(&request->mutex);
}
errorret_t networkHttpRequest(
const networkhttpmethod_t method,
const char_t *url,
const networkhttpheader_t *headers,
const uint32_t headerCount,
const networkhttpheader_t *queryParams,
const uint32_t queryParamCount,
const uint8_t *body,
const size_t bodyLength,
const eventcallback_t onComplete,
const eventcallback_t onError,
void *user
) {
assertNotNull(url, "url must not be NULL");
networkhttprequest_t *request = networkHttpRequestClaim();
request->method = method;
errorChain(networkHttpUrlBuild(
request->url, NETWORK_HTTP_URL_MAX, url, queryParams, queryParamCount
));
networkHttpHeaderListClear(&request->requestHeaders);
for(uint32_t i = 0; i < headerCount; i++) {
networkHttpHeaderListAdd(
&request->requestHeaders, headers[i].name, headers[i].value, true
);
}
request->bodyLength = bodyLength;
if(bodyLength > 0) {
assertNotNull(body, "body must not be NULL when bodyLength > 0");
request->body = memoryAllocate(bodyLength);
memoryCopy(request->body, body, bodyLength);
}
if(onComplete != NULL) eventSubscribe(&request->onComplete, onComplete, user);
if(onError != NULL) eventSubscribe(&request->onError, onError, user);
threadMutexLock(&request->mutex);
request->state = NETWORK_HTTP_REQUEST_STATE_PENDING;
threadMutexUnlock(&request->mutex);
errorOk();
}
+137
View File
@@ -0,0 +1,137 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "event/event.h"
#include "thread/threadmutex.h"
#include "network/http/networkhttpheader.h"
#include "network/http/networkhttpurl.h"
#define NETWORK_HTTP_REQUEST_COUNT_MAX 4
#define NETWORK_HTTP_REQUEST_EVENT_MAX 1
typedef enum {
NETWORK_HTTP_METHOD_GET,
NETWORK_HTTP_METHOD_POST,
NETWORK_HTTP_METHOD_PUT
} networkhttpmethod_t;
typedef enum {
NETWORK_HTTP_REQUEST_STATE_FREE,
NETWORK_HTTP_REQUEST_STATE_PENDING,
NETWORK_HTTP_REQUEST_STATE_ACTIVE,
NETWORK_HTTP_REQUEST_STATE_DONE,
NETWORK_HTTP_REQUEST_STATE_ERROR
} networkhttprequeststate_t;
typedef struct {
uint16_t status;
networkhttpheaderlist_t headers;
// Owned, memoryAllocate'd. Only valid until the onComplete/onError
// event has finished firing, at which point it is freed.
uint8_t *body;
size_t bodyLength;
} networkhttpresponse_t;
typedef struct {
threadmutex_t mutex;
networkhttprequeststate_t state;
networkhttpmethod_t method;
// Fully built, including any query params, by networkHttpRequest.
char_t url[NETWORK_HTTP_URL_MAX];
networkhttpheaderlist_t requestHeaders;
// Owned copy, memoryAllocate'd.
uint8_t *body;
size_t bodyLength;
/** Fired on the main thread once a response has been received. */
event_t onComplete;
eventcallback_t onCompleteCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
void *onCompleteUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
/** Fired on the main thread if the request could not be completed. */
event_t onError;
eventcallback_t onErrorCallbacks[NETWORK_HTTP_REQUEST_EVENT_MAX];
void *onErrorUsers[NETWORK_HTTP_REQUEST_EVENT_MAX];
networkhttpresponse_t response;
} networkhttprequest_t;
extern networkhttprequest_t
NETWORK_HTTP_REQUESTS[NETWORK_HTTP_REQUEST_COUNT_MAX];
/**
* Initializes the request pool: prepares every slot's mutex and events
* and marks them all FREE. Called once by networkHttpInit.
*/
void networkHttpRequestPoolInit(void);
/**
* Finds and returns a FREE slot in the request pool. The slot is not
* marked as in-use by this call; the caller must populate it and then
* set its state to PENDING.
*
* @return A free request slot.
*/
networkhttprequest_t * networkHttpRequestClaim(void);
/**
* Resets a request slot back to FREE, freeing any owned buffers
* (request body, response body) and clearing its event subscribers.
*
* @param request The request slot to reset.
*/
void networkHttpRequestReset(networkhttprequest_t *request);
/**
* Sends an HTTP request asynchronously. The request runs on a
* background thread; onComplete or onError is invoked on the main
* thread (from networkHttpUpdate) once it finishes. 301 redirects are
* followed automatically.
*
* @param method The HTTP method to use.
* @param url The "http://host[:port]/path" URL to request. Must not
* use any scheme other than http.
* @param headers Request headers to send, may be NULL if headerCount
* is 0. The Host, Connection and Content-Length headers are
* reserved and computed internally; any of those passed here
* are ignored.
* @param headerCount Number of entries in headers.
* @param queryParams Query string parameters to append to url, may be
* NULL if queryParamCount is 0.
* @param queryParamCount Number of entries in queryParams.
* @param body Request body to send for POST/PUT, may be NULL if
* bodyLength is 0. Ignored for GET.
* @param bodyLength Number of bytes in body.
* @param onComplete Invoked with (networkhttprequest_t *, user) once a
* response has been received, whatever its status code. May be
* NULL to ignore.
* @param onError Invoked with (networkhttprequest_t *, user) if the
* request could not be completed (DNS/connect/send/receive
* failure, or a malformed response). May be NULL to ignore.
* @param user Arbitrary pointer forwarded to onComplete/onError.
* @return An error if the request could not be queued (e.g. a
* malformed url).
*/
errorret_t networkHttpRequest(
const networkhttpmethod_t method,
const char_t *url,
const networkhttpheader_t *headers,
const uint32_t headerCount,
const networkhttpheader_t *queryParams,
const uint32_t queryParamCount,
const uint8_t *body,
const size_t bodyLength,
const eventcallback_t onComplete,
const eventcallback_t onError,
void *user
);
+88
View File
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttpthread.h"
#include "networkhttpprocess.h"
#include "util/memory.h"
#include "assert/assert.h"
#include <unistd.h>
void networkHttpThreadRun(thread_t *thread) {
assertNotMainThread("networkHttpThreadRun must not run on the main thread.");
while(!threadShouldStop(thread)) {
bool_t didWork = false;
for(uint32_t i = 0; i < NETWORK_HTTP_REQUEST_COUNT_MAX; i++) {
networkhttprequest_t *request = &NETWORK_HTTP_REQUESTS[i];
threadMutexLock(&request->mutex);
if(request->state != NETWORK_HTTP_REQUEST_STATE_PENDING) {
threadMutexUnlock(&request->mutex);
continue;
}
request->state = NETWORK_HTTP_REQUEST_STATE_ACTIVE;
threadMutexUnlock(&request->mutex);
didWork = true;
networkHttpThreadProcessRequest(request);
}
if(threadShouldStop(thread)) break;
if(!didWork) usleep(1000);
}
}
void networkHttpThreadProcessRequest(networkhttprequest_t *request) {
networkhttpurl_t target;
errorret_t parseRet = networkHttpUrlParse(request->url, &target);
networkhttprequeststate_t finalState = NETWORK_HTTP_REQUEST_STATE_DONE;
if(errorIsNotOk(parseRet)) {
errorCatch(errorPrint(parseRet));
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
} else {
for(uint32_t redirect = 0; ; redirect++) {
errorret_t ret = networkHttpProcessAttempt(
request, &target, &request->response
);
if(errorIsNotOk(ret)) {
errorCatch(errorPrint(ret));
finalState = NETWORK_HTTP_REQUEST_STATE_ERROR;
break;
}
const bool_t isRedirect = request->response.status == 301;
const networkhttpheader_t *location = isRedirect ?
networkHttpHeaderListFind(&request->response.headers, "Location") :
NULL;
if(location == NULL || redirect >= NETWORK_HTTP_REDIRECT_COUNT_MAX) {
break;
}
networkhttpurl_t nextTarget;
errorret_t nextRet = networkHttpUrlParse(location->value, &nextTarget);
if(errorIsNotOk(nextRet)) {
// Not an absolute URL we can follow -- treat the 301 itself as
// the final response instead of failing the whole request.
errorCatch(errorPrint(nextRet));
break;
}
// Free the intermediate response body before following further.
if(request->response.body != NULL) memoryFree(request->response.body);
target = nextTarget;
}
}
threadMutexLock(&request->mutex);
request->state = finalState;
threadMutexUnlock(&request->mutex);
}
+31
View File
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "thread/thread.h"
#include "network/http/networkhttprequest.h"
#define NETWORK_HTTP_REDIRECT_COUNT_MAX 5
/**
* The background worker thread body: repeatedly scans the request
* pool for PENDING slots, processes each to completion, and marks it
* DONE or ERROR. Idles briefly when there is nothing to do. Runs
* until the thread is stopped.
*
* @param thread The thread runner.
*/
void networkHttpThreadRun(thread_t *thread);
/**
* Processes a single ACTIVE request slot to completion, following up
* to NETWORK_HTTP_REDIRECT_COUNT_MAX HTTP 301 redirects, and leaves it
* in the DONE or ERROR state. Only called from the background thread.
*
* @param request The request slot to process.
*/
void networkHttpThreadProcessRequest(networkhttprequest_t *request);
+136
View File
@@ -0,0 +1,136 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networkhttpurl.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out) {
assertNotNull(url, "url must not be NULL");
assertNotNull(out, "out must not be NULL");
if(strlen(url) <= 7 || strncasecmp(url, "http://", 7) != 0) {
errorThrow("Unsupported URL scheme (only http:// is supported): %s", url);
}
const char_t *rest = url + 7;
const size_t hostLen = strcspn(rest, ":/");
if(hostLen == 0 || hostLen >= NETWORK_HTTP_HOST_MAX) {
errorThrow("Invalid host in URL: %s", url);
}
memoryCopy(out->host, rest, hostLen);
out->host[hostLen] = '\0';
rest += hostLen;
if(*rest == ':') {
rest++;
const size_t portLen = strcspn(rest, "/");
char_t portStr[8];
if(portLen == 0 || portLen >= sizeof(portStr)) {
errorThrow("Invalid port in URL: %s", url);
}
memoryCopy(portStr, rest, portLen);
portStr[portLen] = '\0';
if(!stringToU16(portStr, &out->port)) {
errorThrow("Invalid port in URL: %s", url);
}
rest += portLen;
} else {
out->port = NETWORK_HTTP_PORT_DEFAULT;
}
const char_t *path = *rest == '\0' ? "/" : rest;
const size_t pathLen = strlen(path);
if(pathLen >= NETWORK_HTTP_PATH_MAX) {
errorThrow("Path too long in URL: %s", url);
}
memoryCopy(out->path, path, pathLen + 1);
errorOk();
}
errorret_t networkHttpUrlBuild(
char_t *dest,
const size_t destSize,
const char_t *baseUrl,
const networkhttpheader_t *queryParams,
const uint32_t queryParamCount
) {
assertNotNull(dest, "dest must not be NULL");
assertNotNull(baseUrl, "baseUrl must not be NULL");
const size_t baseLen = strlen(baseUrl);
if(baseLen >= destSize) errorThrow("URL too long: %s", baseUrl);
memoryCopy(dest, baseUrl, baseLen + 1);
size_t cursor = baseLen;
bool_t hasQuery = stringIncludesString(baseUrl, "?");
for(uint32_t i = 0; i < queryParamCount; i++) {
errorChain(networkHttpUrlAppend(
dest, destSize, &cursor, hasQuery ? "&" : "?"
));
hasQuery = true;
errorChain(networkHttpUrlEncodeComponent(
dest, destSize, &cursor, queryParams[i].name
));
errorChain(networkHttpUrlAppend(dest, destSize, &cursor, "="));
errorChain(networkHttpUrlEncodeComponent(
dest, destSize, &cursor, queryParams[i].value
));
}
errorOk();
}
errorret_t networkHttpUrlAppend(
char_t *dest,
const size_t destSize,
size_t *cursor,
const char_t *str
) {
const size_t len = strlen(str);
if(*cursor + len >= destSize) errorThrow("URL buffer too small");
memoryCopy(dest + *cursor, str, len + 1);
*cursor += len;
errorOk();
}
errorret_t networkHttpUrlEncodeComponent(
char_t *dest,
const size_t destSize,
size_t *cursor,
const char_t *src
) {
const char_t *hex = "0123456789ABCDEF";
for(const char_t *p = src; *p != '\0'; p++) {
const uint8_t c = (uint8_t)*p;
const bool_t unreserved =
(c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~';
if(unreserved) {
if(*cursor + 1 >= destSize) errorThrow("URL buffer too small");
dest[(*cursor)++] = (char_t)c;
} else {
if(*cursor + 3 >= destSize) errorThrow("URL buffer too small");
dest[(*cursor)++] = '%';
dest[(*cursor)++] = hex[(c >> 4) & 0xF];
dest[(*cursor)++] = hex[c & 0xF];
}
}
dest[*cursor] = '\0';
errorOk();
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "network/http/networkhttpheader.h"
#define NETWORK_HTTP_URL_MAX 1024
#define NETWORK_HTTP_HOST_MAX 256
#define NETWORK_HTTP_PATH_MAX 768
#define NETWORK_HTTP_PORT_DEFAULT 80
typedef struct {
char_t host[NETWORK_HTTP_HOST_MAX];
uint16_t port;
// Includes the leading slash and, if present, the query string.
char_t path[NETWORK_HTTP_PATH_MAX];
} networkhttpurl_t;
/**
* Parses a "http://host[:port]/path[?query]" URL. Any other scheme is
* rejected, since this client does not support TLS.
*
* @param url The URL to parse.
* @param out The parsed URL is written here.
* @return An error if the URL is malformed or uses an unsupported scheme.
*/
errorret_t networkHttpUrlParse(const char_t *url, networkhttpurl_t *out);
/**
* Copies baseUrl into dest, then appends each queryParam as a
* percent-encoded "key=value" pair, joined with "&" (or "?" for the
* first one, unless baseUrl already contains a "?").
*
* @param dest The destination buffer.
* @param destSize The size of dest, including the null terminator.
* @param baseUrl The URL to copy before appending query params.
* @param queryParams Array of query parameters to append, may be NULL if
* queryParamCount is 0.
* @param queryParamCount Number of entries in queryParams.
* @return An error if the result would not fit in dest.
*/
errorret_t networkHttpUrlBuild(
char_t *dest,
const size_t destSize,
const char_t *baseUrl,
const networkhttpheader_t *queryParams,
const uint32_t queryParamCount
);
/**
* Appends a raw (not percent-encoded) string to dest at *cursor,
* advancing *cursor and keeping dest null-terminated.
*
* @param dest The destination buffer.
* @param destSize The size of dest, including the null terminator.
* @param cursor The current write offset into dest, updated in place.
* @param str The string to append.
* @return An error if the result would not fit in dest.
*/
errorret_t networkHttpUrlAppend(
char_t *dest,
const size_t destSize,
size_t *cursor,
const char_t *str
);
/**
* Percent-encodes src and appends it to dest at *cursor, advancing
* *cursor and keeping dest null-terminated. Unreserved characters
* (letters, digits, "-", "_", ".", "~") are copied as-is.
*
* @param dest The destination buffer.
* @param destSize The size of dest, including the null terminator.
* @param cursor The current write offset into dest, updated in place.
* @param src The string to percent-encode and append.
* @return An error if the result would not fit in dest.
*/
errorret_t networkHttpUrlEncodeComponent(
char_t *dest,
const size_t destSize,
size_t *cursor,
const char_t *src
);
+5 -2
View File
@@ -6,6 +6,7 @@
*/
#include "network.h"
#include "network/http/networkhttp.h"
#include "util/memory.h"
#include "assert/assert.h"
#include "log/log.h"
@@ -18,11 +19,13 @@ errorret_t networkInit() {
NETWORK.errorState.code = ERROR_OK;
NETWORK.onDisconnect = NULL;
return networkPlatformInit();
errorChain(networkPlatformInit());
return networkHttpInit();
}
errorret_t networkUpdate() {
errorChain(networkPlatformUpdate());
errorChain(networkHttpUpdate());
if(NETWORK.state == NETWORK_STATE_CONNECTED && !networkIsConnected()) {
NETWORK.state = NETWORK_STATE_DISCONNECTED;
@@ -112,5 +115,5 @@ errorret_t networkDispose() {
}
errorChain(networkPlatformDispose());
errorOk();
return networkHttpDispose();
}
+1
View File
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networkdolphin.c
networksocketdolphin.c
)
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networksocketdolphin.h"
#include "util/memory.h"
#include "assert/assert.h"
#ifdef DUSK_WII
#include <network.h>
#endif
errorret_t networkSocketDolphinConnect(
networksocketdolphin_t *sock,
const char_t *host,
const uint16_t port
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(host, "host must not be NULL");
#ifdef DUSK_WII
struct hostent *entry = net_gethostbyname(host);
if(entry == NULL || entry->h_addr_list[0] == NULL) {
errorThrow("Failed to resolve host %s", host);
}
const s32 fd = net_socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0) errorThrow("Failed to create socket: %d", (int_t)fd);
struct sockaddr_in serverAddr;
memoryZero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);
memoryCopy(
&serverAddr.sin_addr, entry->h_addr_list[0], sizeof(serverAddr.sin_addr)
);
const s32 ret = net_connect(fd, (void *)&serverAddr, sizeof(serverAddr));
if(ret < 0) {
net_close(fd);
errorThrow("Failed to connect to %s:%u: %d", host, port, (int_t)ret);
}
sock->fd = fd;
errorOk();
#else
errorThrow(
"Networking is not supported on this platform (%s:%u)", host, port
);
#endif
}
errorret_t networkSocketDolphinSend(
networksocketdolphin_t *sock,
const uint8_t *data,
const size_t length,
size_t *outSent
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(data, "data must not be NULL");
assertNotNull(outSent, "outSent must not be NULL");
#ifdef DUSK_WII
const s32 sent = net_send(sock->fd, data, (s32)length, 0);
if(sent < 0) errorThrow("Failed to send data: %d", (int_t)sent);
*outSent = (size_t)sent;
errorOk();
#else
errorThrow("Networking is not supported on this platform");
#endif
}
errorret_t networkSocketDolphinReceive(
networksocketdolphin_t *sock,
uint8_t *buffer,
const size_t bufferSize,
size_t *outReceived
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(buffer, "buffer must not be NULL");
assertNotNull(outReceived, "outReceived must not be NULL");
#ifdef DUSK_WII
const s32 received = net_recv(sock->fd, buffer, (s32)bufferSize, 0);
if(received < 0) errorThrow("Failed to receive data: %d", (int_t)received);
*outReceived = (size_t)received;
errorOk();
#else
errorThrow("Networking is not supported on this platform");
#endif
}
void networkSocketDolphinClose(networksocketdolphin_t *sock) {
assertNotNull(sock, "sock must not be NULL");
#ifdef DUSK_WII
if(sock->fd >= 0) net_close(sock->fd);
#endif
sock->fd = -1;
}
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
int_t fd;
} networksocketdolphin_t;
/**
* Resolves host and opens a blocking TCP connection to host:port.
* Always fails under plain DUSK_GAMECUBE -- retail GameCube units have
* no network hardware.
*
* @param sock The socket structure to initialize.
* @param host The hostname or IP address to connect to.
* @param port The port to connect to.
* @return An error if resolution or connection failed.
*/
errorret_t networkSocketDolphinConnect(
networksocketdolphin_t *sock,
const char_t *host,
const uint16_t port
);
/**
* Sends data over the socket. May write fewer bytes than requested; the
* caller is responsible for looping until all data is sent.
*
* @param sock The connected socket.
* @param data The data to send.
* @param length The number of bytes in data.
* @param outSent The number of bytes actually sent is written here.
* @return An error if the send failed.
*/
errorret_t networkSocketDolphinSend(
networksocketdolphin_t *sock,
const uint8_t *data,
const size_t length,
size_t *outSent
);
/**
* Receives data from the socket.
*
* @param sock The connected socket.
* @param buffer The buffer to receive into.
* @param bufferSize The size of buffer.
* @param outReceived The number of bytes actually received is written
* here; 0 means the peer closed the connection.
* @return An error if the receive failed.
*/
errorret_t networkSocketDolphinReceive(
networksocketdolphin_t *sock,
uint8_t *buffer,
const size_t bufferSize,
size_t *outReceived
);
/**
* Closes the socket.
*
* @param sock The socket to close.
*/
void networkSocketDolphinClose(networksocketdolphin_t *sock);
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "networksocketdolphin.h"
typedef networksocketdolphin_t networksocketplatform_t;
#define networkSocketPlatformConnect networkSocketDolphinConnect
#define networkSocketPlatformSend networkSocketDolphinSend
#define networkSocketPlatformReceive networkSocketDolphinReceive
#define networkSocketPlatformClose networkSocketDolphinClose
+1
View File
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networklinux.c
networksocketlinux.c
)
+102
View File
@@ -0,0 +1,102 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networksocketlinux.h"
#include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h"
#include <sys/socket.h>
#include <sys/types.h>
#include <netdb.h>
#include <unistd.h>
errorret_t networkSocketLinuxConnect(
networksocketlinux_t *sock,
const char_t *host,
const uint16_t port
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(host, "host must not be NULL");
char_t portStr[8];
stringFormat(portStr, sizeof(portStr) - 1, "%u", port);
struct addrinfo hints;
memoryZero(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *result = NULL;
const int_t gaiRet = getaddrinfo(host, portStr, &hints, &result);
if(gaiRet != 0) {
errorThrow("Failed to resolve host %s: %s", host, gai_strerror(gaiRet));
}
int_t fd = -1;
for(struct addrinfo *addr = result; addr != NULL; addr = addr->ai_next) {
fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
if(fd < 0) continue;
if(connect(fd, addr->ai_addr, addr->ai_addrlen) == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(result);
if(fd < 0) errorThrow("Failed to connect to %s:%u", host, port);
struct timeval timeout;
timeout.tv_sec = NETWORK_SOCKET_LINUX_TIMEOUT_SECONDS;
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
sock->fd = fd;
errorOk();
}
errorret_t networkSocketLinuxSend(
networksocketlinux_t *sock,
const uint8_t *data,
const size_t length,
size_t *outSent
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(data, "data must not be NULL");
assertNotNull(outSent, "outSent must not be NULL");
const ssize_t sent = send(sock->fd, data, length, 0);
if(sent < 0) errorThrow("Failed to send data: %s", strerror(errno));
*outSent = (size_t)sent;
errorOk();
}
errorret_t networkSocketLinuxReceive(
networksocketlinux_t *sock,
uint8_t *buffer,
const size_t bufferSize,
size_t *outReceived
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(buffer, "buffer must not be NULL");
assertNotNull(outReceived, "outReceived must not be NULL");
const ssize_t received = recv(sock->fd, buffer, bufferSize, 0);
if(received < 0) errorThrow("Failed to receive data: %s", strerror(errno));
*outReceived = (size_t)received;
errorOk();
}
void networkSocketLinuxClose(networksocketlinux_t *sock) {
assertNotNull(sock, "sock must not be NULL");
if(sock->fd >= 0) close(sock->fd);
sock->fd = -1;
}
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#define NETWORK_SOCKET_LINUX_TIMEOUT_SECONDS 10
typedef struct {
int_t fd;
} networksocketlinux_t;
/**
* Resolves host and opens a blocking TCP connection to host:port.
*
* @param sock The socket structure to initialize.
* @param host The hostname or IP address to connect to.
* @param port The port to connect to.
* @return An error if resolution or connection failed.
*/
errorret_t networkSocketLinuxConnect(
networksocketlinux_t *sock,
const char_t *host,
const uint16_t port
);
/**
* Sends data over the socket. May write fewer bytes than requested; the
* caller is responsible for looping until all data is sent.
*
* @param sock The connected socket.
* @param data The data to send.
* @param length The number of bytes in data.
* @param outSent The number of bytes actually sent is written here.
* @return An error if the send failed.
*/
errorret_t networkSocketLinuxSend(
networksocketlinux_t *sock,
const uint8_t *data,
const size_t length,
size_t *outSent
);
/**
* Receives data from the socket.
*
* @param sock The connected socket.
* @param buffer The buffer to receive into.
* @param bufferSize The size of buffer.
* @param outReceived The number of bytes actually received is written
* here; 0 means the peer closed the connection.
* @return An error if the receive failed.
*/
errorret_t networkSocketLinuxReceive(
networksocketlinux_t *sock,
uint8_t *buffer,
const size_t bufferSize,
size_t *outReceived
);
/**
* Closes the socket.
*
* @param sock The socket to close.
*/
void networkSocketLinuxClose(networksocketlinux_t *sock);
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "networksocketlinux.h"
typedef networksocketlinux_t networksocketplatform_t;
#define networkSocketPlatformConnect networkSocketLinuxConnect
#define networkSocketPlatformSend networkSocketLinuxSend
#define networkSocketPlatformReceive networkSocketLinuxReceive
#define networkSocketPlatformClose networkSocketLinuxClose
+1
View File
@@ -6,4 +6,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
networkpsp.c
networksocketpsp.c
)
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "networksocketpsp.h"
typedef networksocketpsp_t networksocketplatform_t;
#define networkSocketPlatformConnect networkSocketPSPConnect
#define networkSocketPlatformSend networkSocketPSPSend
#define networkSocketPlatformReceive networkSocketPSPReceive
#define networkSocketPlatformClose networkSocketPSPClose
+110
View File
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "networksocketpsp.h"
#include "util/memory.h"
#include "assert/assert.h"
#include <pspnet_inet.h>
#include <pspnet_resolver.h>
#include <netinet/in.h>
errorret_t networkSocketPSPConnect(
networksocketpsp_t *sock,
const char_t *host,
const uint16_t port
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(host, "host must not be NULL");
int_t rid = -1;
uint8_t resolverBuf[NETWORK_SOCKET_PSP_RESOLVER_BUF_SIZE];
int_t ret = sceNetResolverCreate(
&rid, resolverBuf, sizeof(resolverBuf)
);
if(ret < 0) errorThrow("Failed to create resolver: 0x%08X", ret);
struct in_addr addr;
ret = sceNetResolverStartNtoA(
rid, host, &addr,
NETWORK_SOCKET_PSP_RESOLVER_TIMEOUT_SECONDS,
NETWORK_SOCKET_PSP_RESOLVER_RETRY_COUNT
);
sceNetResolverDelete(rid);
if(ret < 0) errorThrow("Failed to resolve host %s: 0x%08X", host, ret);
const int_t fd = sceNetInetSocket(AF_INET, SOCK_STREAM, 0);
if(fd < 0) errorThrow("Failed to create socket: 0x%08X", fd);
struct sockaddr_in serverAddr;
memoryZero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);
serverAddr.sin_addr = addr;
ret = sceNetInetConnect(
fd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)
);
if(ret < 0) {
sceNetInetClose(fd);
errorThrow("Failed to connect to %s:%u: 0x%08X", host, port, ret);
}
struct SceNetInetTimeval timeout;
timeout.tv_sec = NETWORK_SOCKET_PSP_TIMEOUT_SECONDS;
timeout.tv_usec = 0;
sceNetInetSetsockopt(
fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)
);
sceNetInetSetsockopt(
fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)
);
sock->fd = fd;
errorOk();
}
errorret_t networkSocketPSPSend(
networksocketpsp_t *sock,
const uint8_t *data,
const size_t length,
size_t *outSent
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(data, "data must not be NULL");
assertNotNull(outSent, "outSent must not be NULL");
const int_t sent = sceNetInetSend(sock->fd, data, length, 0);
if(sent < 0) errorThrow("Failed to send data: 0x%08X", sceNetInetGetErrno());
*outSent = (size_t)sent;
errorOk();
}
errorret_t networkSocketPSPReceive(
networksocketpsp_t *sock,
uint8_t *buffer,
const size_t bufferSize,
size_t *outReceived
) {
assertNotNull(sock, "sock must not be NULL");
assertNotNull(buffer, "buffer must not be NULL");
assertNotNull(outReceived, "outReceived must not be NULL");
const int_t received = sceNetInetRecv(sock->fd, buffer, bufferSize, 0);
if(received < 0) {
errorThrow("Failed to receive data: 0x%08X", sceNetInetGetErrno());
}
*outReceived = (size_t)received;
errorOk();
}
void networkSocketPSPClose(networksocketpsp_t *sock) {
assertNotNull(sock, "sock must not be NULL");
if(sock->fd >= 0) sceNetInetClose(sock->fd);
sock->fd = -1;
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#define NETWORK_SOCKET_PSP_TIMEOUT_SECONDS 10
#define NETWORK_SOCKET_PSP_RESOLVER_BUF_SIZE 1024
#define NETWORK_SOCKET_PSP_RESOLVER_TIMEOUT_SECONDS 5
#define NETWORK_SOCKET_PSP_RESOLVER_RETRY_COUNT 3
typedef struct {
int_t fd;
} networksocketpsp_t;
/**
* Resolves host and opens a blocking TCP connection to host:port.
*
* @param sock The socket structure to initialize.
* @param host The hostname or IP address to connect to.
* @param port The port to connect to.
* @return An error if resolution or connection failed.
*/
errorret_t networkSocketPSPConnect(
networksocketpsp_t *sock,
const char_t *host,
const uint16_t port
);
/**
* Sends data over the socket. May write fewer bytes than requested; the
* caller is responsible for looping until all data is sent.
*
* @param sock The connected socket.
* @param data The data to send.
* @param length The number of bytes in data.
* @param outSent The number of bytes actually sent is written here.
* @return An error if the send failed.
*/
errorret_t networkSocketPSPSend(
networksocketpsp_t *sock,
const uint8_t *data,
const size_t length,
size_t *outSent
);
/**
* Receives data from the socket.
*
* @param sock The connected socket.
* @param buffer The buffer to receive into.
* @param bufferSize The size of buffer.
* @param outReceived The number of bytes actually received is written
* here; 0 means the peer closed the connection.
* @return An error if the receive failed.
*/
errorret_t networkSocketPSPReceive(
networksocketpsp_t *sock,
uint8_t *buffer,
const size_t bufferSize,
size_t *outReceived
);
/**
* Closes the socket.
*
* @param sock The socket to close.
*/
void networkSocketPSPClose(networksocketpsp_t *sock);
+1
View File
@@ -6,6 +6,7 @@
add_subdirectory(assert)
add_subdirectory(asset)
add_subdirectory(error)
add_subdirectory(network)
add_subdirectory(thread)
add_subdirectory(display)
# add_subdirectory(rpg)
+6
View File
@@ -0,0 +1,6 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_subdirectory(http)
+8
View File
@@ -0,0 +1,8 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
include(dusktest)
dusktest(test_networkhttp.c)
+450
View File
@@ -0,0 +1,450 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "dusktest.h"
#include "network/http/networkhttp.h"
#include "network/http/networkhttprequest.h"
#include "util/memory.h"
#include "util/string.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
// ============================================================
// Fake single-connection HTTP server, driven on a background thread_t
// ============================================================
typedef struct {
int_t listenFd;
uint16_t port;
thread_t thread;
const uint8_t *responseData;
size_t responseLength;
uint8_t receivedData[8192];
size_t receivedLength;
} fakeserver_t;
static void fakeServerRun(thread_t *thread) {
fakeserver_t *server = (fakeserver_t *)thread->data;
struct sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
const int_t clientFd = accept(
server->listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen
);
if(clientFd < 0) return;
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 300000;
setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
server->receivedLength = 0;
for(;;) {
if(server->receivedLength >= sizeof(server->receivedData) - 1) break;
const ssize_t got = recv(
clientFd,
server->receivedData + server->receivedLength,
sizeof(server->receivedData) - 1 - server->receivedLength,
0
);
if(got <= 0) break;
server->receivedLength += (size_t)got;
}
send(clientFd, server->responseData, server->responseLength, 0);
close(clientFd);
close(server->listenFd);
server->listenFd = -1;
}
static void fakeServerStart(
fakeserver_t *server,
const uint8_t *responseData,
const size_t responseLength
) {
memoryZero(server, sizeof(fakeserver_t));
server->responseData = responseData;
server->responseLength = responseLength;
server->listenFd = socket(AF_INET, SOCK_STREAM, 0);
assert_true(server->listenFd >= 0);
struct sockaddr_in addr;
memoryZero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
assert_int_equal(
bind(server->listenFd, (struct sockaddr *)&addr, sizeof(addr)), 0
);
socklen_t addrLen = sizeof(addr);
assert_int_equal(
getsockname(server->listenFd, (struct sockaddr *)&addr, &addrLen), 0
);
server->port = ntohs(addr.sin_port);
assert_int_equal(listen(server->listenFd, 1), 0);
threadInit(&server->thread, fakeServerRun);
server->thread.data = server;
threadStart(&server->thread);
}
static void fakeServerStop(fakeserver_t *server) {
threadStop(&server->thread);
if(server->listenFd >= 0) close(server->listenFd);
}
// ============================================================
// networkHttpRequest completion capture
// ============================================================
typedef struct {
bool_t completed;
bool_t errored;
uint16_t status;
uint8_t body[4096];
size_t bodyLength;
} capturedresponse_t;
static void captureOnComplete(void *params, void *user) {
const networkhttprequest_t *request = (const networkhttprequest_t *)params;
capturedresponse_t *captured = (capturedresponse_t *)user;
captured->status = request->response.status;
captured->bodyLength = request->response.bodyLength;
if(request->response.bodyLength > 0) {
memoryCopy(
captured->body, request->response.body, request->response.bodyLength
);
}
captured->completed = true;
}
static void captureOnError(void *params, void *user) {
capturedresponse_t *captured = (capturedresponse_t *)user;
captured->errored = true;
captured->completed = true;
}
static void waitForCompletion(capturedresponse_t *captured) {
for(int32_t i = 0; i < 5000 && !captured->completed; i++) {
networkHttpUpdate();
usleep(1000);
}
assert_true(captured->completed);
}
// ============================================================
// Per-test setup / teardown
// ============================================================
static int http_setup(void **state) {
networkHttpInit();
return 0;
}
static int http_teardown(void **state) {
networkHttpDispose();
return 0;
}
// ============================================================
// Tests
// ============================================================
static void test_get_request_headers_and_body(void **state) {
const char_t *responseText =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 5\r\n"
"\r\n"
"hello";
fakeserver_t server;
fakeServerStart(
&server, (const uint8_t *)responseText, strlen(responseText)
);
char_t url[128];
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/foo", server.port);
const networkhttpheader_t headers[1] = {
{ .name = "X-Test", .value = "abc" }
};
capturedresponse_t captured;
memoryZero(&captured, sizeof(captured));
const errorret_t ret = networkHttpRequest(
NETWORK_HTTP_METHOD_GET, url,
headers, 1,
NULL, 0,
NULL, 0,
captureOnComplete, captureOnError, &captured
);
assert_true(errorIsOk(ret));
waitForCompletion(&captured);
fakeServerStop(&server);
assert_false(captured.errored);
assert_int_equal(captured.status, 200);
assert_int_equal(captured.bodyLength, 5);
assert_memory_equal(captured.body, "hello", 5);
server.receivedData[server.receivedLength] = '\0';
const char_t *received = (const char_t *)server.receivedData;
assert_non_null(strstr(received, "GET /foo HTTP/1.1"));
assert_non_null(strstr(received, "X-Test: abc"));
assert_non_null(strstr(received, "Connection: close"));
assert_non_null(strstr(received, "Host: 127.0.0.1"));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_query_params_are_encoded_and_appended(void **state) {
const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
fakeserver_t server;
fakeServerStart(
&server, (const uint8_t *)responseText, strlen(responseText)
);
char_t url[128];
stringFormat(
url, sizeof(url) - 1, "http://127.0.0.1:%u/search", server.port
);
const networkhttpheader_t query[2] = {
{ .name = "q", .value = "hello world" },
{ .name = "page", .value = "2" }
};
capturedresponse_t captured;
memoryZero(&captured, sizeof(captured));
const errorret_t ret = networkHttpRequest(
NETWORK_HTTP_METHOD_GET, url,
NULL, 0,
query, 2,
NULL, 0,
captureOnComplete, captureOnError, &captured
);
assert_true(errorIsOk(ret));
waitForCompletion(&captured);
fakeServerStop(&server);
assert_false(captured.errored);
server.receivedData[server.receivedLength] = '\0';
const char_t *received = (const char_t *)server.receivedData;
assert_non_null(
strstr(received, "GET /search?q=hello%20world&page=2 HTTP/1.1")
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_post_sends_body_with_content_length(void **state) {
const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
fakeserver_t server;
fakeServerStart(
&server, (const uint8_t *)responseText, strlen(responseText)
);
char_t url[128];
stringFormat(
url, sizeof(url) - 1, "http://127.0.0.1:%u/submit", server.port
);
const char_t *body = "{\"a\":1}";
capturedresponse_t captured;
memoryZero(&captured, sizeof(captured));
const errorret_t ret = networkHttpRequest(
NETWORK_HTTP_METHOD_POST, url,
NULL, 0,
NULL, 0,
(const uint8_t *)body, strlen(body),
captureOnComplete, captureOnError, &captured
);
assert_true(errorIsOk(ret));
waitForCompletion(&captured);
fakeServerStop(&server);
assert_false(captured.errored);
assert_int_equal(captured.status, 200);
assert_memory_equal(captured.body, "OK", 2);
server.receivedData[server.receivedLength] = '\0';
const char_t *received = (const char_t *)server.receivedData;
assert_non_null(strstr(received, "POST /submit HTTP/1.1"));
assert_non_null(strstr(received, "Content-Length: 7"));
assert_non_null(strstr(received, "{\"a\":1}"));
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_put_method_is_used(void **state) {
const char_t *responseText = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
fakeserver_t server;
fakeServerStart(
&server, (const uint8_t *)responseText, strlen(responseText)
);
char_t url[128];
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/item/1", server.port);
capturedresponse_t captured;
memoryZero(&captured, sizeof(captured));
const errorret_t ret = networkHttpRequest(
NETWORK_HTTP_METHOD_PUT, url,
NULL, 0, NULL, 0, NULL, 0,
captureOnComplete, captureOnError, &captured
);
assert_true(errorIsOk(ret));
waitForCompletion(&captured);
fakeServerStop(&server);
assert_false(captured.errored);
server.receivedData[server.receivedLength] = '\0';
assert_non_null(
strstr((const char_t *)server.receivedData, "PUT /item/1 HTTP/1.1")
);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_301_redirect_is_followed_automatically(void **state) {
const char_t *finalResponseText =
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
fakeserver_t target;
fakeServerStart(
&target, (const uint8_t *)finalResponseText, strlen(finalResponseText)
);
char_t targetUrl[128];
stringFormat(
targetUrl, sizeof(targetUrl) - 1,
"http://127.0.0.1:%u/target", target.port
);
char_t redirectResponseText[256];
stringFormat(
redirectResponseText, sizeof(redirectResponseText) - 1,
"HTTP/1.1 301 Moved Permanently\r\n"
"Location: %s\r\n"
"Content-Length: 0\r\n"
"\r\n",
targetUrl
);
fakeserver_t initial;
fakeServerStart(
&initial,
(const uint8_t *)redirectResponseText,
strlen(redirectResponseText)
);
char_t url[128];
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/old", initial.port);
capturedresponse_t captured;
memoryZero(&captured, sizeof(captured));
const errorret_t ret = networkHttpRequest(
NETWORK_HTTP_METHOD_GET, url,
NULL, 0, NULL, 0, NULL, 0,
captureOnComplete, captureOnError, &captured
);
assert_true(errorIsOk(ret));
waitForCompletion(&captured);
fakeServerStop(&initial);
fakeServerStop(&target);
assert_false(captured.errored);
assert_int_equal(captured.status, 200);
assert_memory_equal(captured.body, "OK", 2);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
static void test_connection_refused_triggers_onError(void **state) {
const int_t fd = socket(AF_INET, SOCK_STREAM, 0);
assert_true(fd >= 0);
struct sockaddr_in addr;
memoryZero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
assert_int_equal(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), 0);
socklen_t addrLen = sizeof(addr);
assert_int_equal(getsockname(fd, (struct sockaddr *)&addr, &addrLen), 0);
const uint16_t port = ntohs(addr.sin_port);
close(fd); // Nothing is listening on this port now.
char_t url[64];
stringFormat(url, sizeof(url) - 1, "http://127.0.0.1:%u/", port);
capturedresponse_t captured;
memoryZero(&captured, sizeof(captured));
const errorret_t ret = networkHttpRequest(
NETWORK_HTTP_METHOD_GET, url,
NULL, 0, NULL, 0, NULL, 0,
captureOnComplete, captureOnError, &captured
);
assert_true(errorIsOk(ret));
waitForCompletion(&captured);
assert_true(captured.errored);
assert_int_equal(memoryGetAllocatedCount(), 0);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(
test_get_request_headers_and_body, http_setup, http_teardown
),
cmocka_unit_test_setup_teardown(
test_query_params_are_encoded_and_appended, http_setup, http_teardown
),
cmocka_unit_test_setup_teardown(
test_post_sends_body_with_content_length, http_setup, http_teardown
),
cmocka_unit_test_setup_teardown(
test_put_method_is_used, http_setup, http_teardown
),
cmocka_unit_test_setup_teardown(
test_301_redirect_is_followed_automatically, http_setup, http_teardown
),
cmocka_unit_test_setup_teardown(
test_connection_refused_triggers_onError, http_setup, http_teardown
),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}