summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE5
-rw-r--r--README4
-rw-r--r--TODO4
-rw-r--r--source/kit/_lib.c1
-rw-r--r--source/kit/http1.h466
-rw-r--r--source/kit/math.h5
-rw-r--r--source/kit/sockets.h1
-rw-r--r--source/kit/string_builder.c1
-rw-r--r--source/kit/string_builder.h50
-rw-r--r--source/tests/_exe.c1
-rw-r--r--source/tests/http1.test.c6
11 files changed, 425 insertions, 119 deletions
diff --git a/LICENSE b/LICENSE
index 6281517..bacfeb0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
-Copyright (c) 2012 yohhoy
-Copyright (c) 2022 Yonggang Luo
Copyright (c) 2022-2023 Mitya Selivanov
+Copyright (c) 2022 Yonggang Luo
+Copyright (c) 2016 Christian C. Sachs
+Copyright (c) 2012 yohhoy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README b/README
index afa93cf..3dfc507 100644
--- a/README
+++ b/README
@@ -28,6 +28,7 @@ Features
- Async functions (coroutines)
- Vector and color math
- XML
+ - HTTP 1 client
Preprocessor options
- KIT_DISABLE_SHORT_NAMES - Disable short names for functions and types.
@@ -40,5 +41,8 @@ Preprocessor options
C11 threads implementation was forked from the Mesa source code.
* https://gitlab.freedesktop.org/mesa/mesa
+HTTP1 implementation was forked from the picohttpclient
+* https://github.com/csachs/picohttpclient
+
Folder "/include" contains single header-only version of this library.
Use "#define KIT_IMPLEMENTATION" to enable implementation code.
diff --git a/TODO b/TODO
index 87f872a..97823a2 100644
--- a/TODO
+++ b/TODO
@@ -2,7 +2,8 @@ To-Do
- Cryptography
- hmac_sha256 https://github.com/h5p9sl/hmac_sha256
- - ecdh https://github.com/kokke/tiny-ECDH-c
+ - ecdh https://github.com/kokke/tiny-ECDH-c
+ - TLS
- Allocators
- Memory pool
- Memory stack
@@ -21,4 +22,3 @@ To-Do
- Utils
- parse, print
- String builder
- - HTTP 1 client
diff --git a/source/kit/_lib.c b/source/kit/_lib.c
index 53bd818..3b5d97a 100644
--- a/source/kit/_lib.c
+++ b/source/kit/_lib.c
@@ -1,7 +1,6 @@
#include "allocator.c"
#include "array_ref.c"
#include "dynamic_array.c"
-#include "string_builder.c"
#include "file.c"
#include "input_buffer.c"
#include "input_stream.c"
diff --git a/source/kit/http1.h b/source/kit/http1.h
index 0493d46..f4e5fc6 100644
--- a/source/kit/http1.h
+++ b/source/kit/http1.h
@@ -1,3 +1,7 @@
+// TODO
+// - Error handling
+// - HTTPS support
+
#ifndef KIT_HTTP1_H
#define KIT_HTTP1_H
@@ -5,24 +9,19 @@
#include "string_builder.h"
#include "sockets.h"
+#include <stdio.h>
+#include <assert.h>
+
#ifdef __cplusplus
extern "C" {
#endif
-// TODO
-// - Add source to README
-// https://github.com/csachs/picohttpclient/tree/master
-//
-
-// FIXME
-// Rewrite in C.
-//
-
#define KIT_HTTP1_CONTENT_TYPE \
"Content-Type: text/plain; version=0.0.4; charset=utf-8";
#define KIT_HTTP1_NEWLINE "\r\n"
#define KIT_HTTP1_SPACE " "
#define KIT_HTTP1_HEADER_SEPARATOR ": "
+#define KIT_HTTP1_BUFFER_SIZE 8192
enum {
KIT_HTTP1_OPTIONS,
@@ -36,24 +35,24 @@ enum {
};
typedef struct {
- kit_str_builder_t key;
- kit_str_builder_t value;
+ kit_str_t key;
+ kit_str_t value;
} kit_http1_str_pair_t;
typedef KIT_DA(kit_http1_str_pair_t) kit_http1_str_map_t;
typedef struct {
- kit_str_builder_t str;
- i64 position;
+ kit_str_t str;
+ i64 position;
} kit_http1_tok_t;
typedef struct {
- kit_str_builder_t protocol;
- kit_str_builder_t host;
- kit_str_builder_t port;
- kit_str_builder_t address;
- kit_str_builder_t query_string;
- kit_str_builder_t hash;
+ kit_str_t protocol;
+ kit_str_t host;
+ kit_str_t port;
+ kit_str_t address;
+ kit_str_t query_string;
+ kit_str_t hash;
kit_http1_str_map_t parameters;
} kit_http1_uri_t;
@@ -66,93 +65,188 @@ typedef struct {
kit_str_builder_t body;
} kit_http1_response_t;
-static kit_str_builder_t kit_http1_tok_next(kit_http1_tok_t *tok,
- kit_str_t search,
- i8 return_tail) {
- i64 hit = str.find(search, position);
- if (hit == string::npos) {
- if (return_tail) {
- return tail();
- } else {
- return "";
- }
- }
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+# pragma GCC diagnostic ignored "-Wunknown-pragmas"
+#endif
- i64 previous_position = position;
- position = hit + search.length();
+static kit_str_t kit_http1_tok_tail(kit_http1_tok_t *tok) {
+ assert(tok != NULL);
- return str.substr(previous_position, hit - previous_position);
-}
+ if (tok == NULL)
+ return (kit_str_t) { .size = 0, .values = NULL };
-static kit_str_builder_t kit_http1_tok_tail(kit_http1_tok_t *tok) {
- i64 previous_position = position;
- position = str.length();
- return str.substr(previous_position);
+ i64 previous_position = tok->position;
+ tok->position = tok->str.size;
+
+ return (kit_str_t) { .size = tok->position - previous_position,
+ .values = tok->str.values +
+ previous_position };
}
-static void kit_http1_uri_parse_params(kit_http1_uri_t *uri) {
- kit_http1_tok_t qt(query_string);
- for (;;) {
- kit_str_builder_t key = qt.next("=", 0);
- if (key == "")
+static kit_str_t kit_http1_tok_next(kit_http1_tok_t *tok,
+ kit_str_t search,
+ i8 return_tail) {
+ assert(tok != NULL);
+ if (tok == NULL)
+ return (kit_str_t) { .size = 0, .values = NULL };
+
+ i64 hit = tok->position;
+
+ while (hit + search.size <= tok->str.size) {
+ kit_str_t s = { .size = search.size,
+ .values = tok->str.values + hit };
+ if (KIT_AR_EQUAL(s, search))
break;
- parameters[key] = qt.next("&", 1);
}
+
+ if (hit + search.size > tok->str.size) {
+ if (return_tail)
+ return kit_http1_tok_tail(tok);
+ else
+ return (kit_str_t) { .size = 0, .values = NULL };
+ }
+
+ i64 previous_position = tok->position;
+ tok->position = hit + search.size;
+
+ return (kit_str_t) { .size = hit - previous_position,
+ .values = tok->str.values +
+ previous_position };
}
-static void kit_http1_uri_init(kit_http1_uri_t *uri,
- kit_str_builder_t input,
- i8 should_use_params) {
- kit_http1_tok_t t = kit_http1_tok_t(input);
- protocol = t.next("://", 0);
- kit_str_builder_t host_port_str = t.next("/", 0);
+// TODO
+// - Return error status
+static void kit_http1_uri_init(kit_http1_uri_t *uri, kit_str_t input,
+ kit_allocator_t *alloc) {
+ static const char port_80[] = "80";
+
+ assert(uri != NULL);
+ if (uri == NULL)
+ return;
- kit_http1_tok_t host_port(host_port_str);
+ memset(uri, 0, sizeof *uri);
- host = host_port.next(host_port_str[0] == '[' ? "]:" : ":", 1);
+ kit_http1_tok_t input_tok = { .str = input, .position = 0 };
- if (host[0] == '[')
- host = host.substr(1, host.size() - 1);
+ uri->protocol = kit_http1_tok_next(&input_tok, SZ("://"), 0);
+ kit_str_t host_port_str = kit_http1_tok_next(&input_tok, SZ("/"),
+ 0);
- port = host_port.tail();
- if (port.empty())
- port = "80";
+ // FIXME
+ // Handle invalid host port error
+ assert(host_port_str.size > 0);
+ if (host_port_str.size == 0)
+ return;
- address = t.next("?", 1);
- query_string = t.next("#", 1);
+ kit_http1_tok_t host_port_tok = { .str = host_port_str,
+ .position = 0 };
- hash = t.tail();
+ uri->host = kit_http1_tok_next(
+ &host_port_tok,
+ host_port_str.values[0] == '[' ? SZ("]:") : SZ(":"), 1);
- if (should_use_params) {
- kit_http1_uri_parse_params();
+ if (uri->host.values[0] == '[')
+ uri->host = (kit_str_t) { .size = uri->host.size - 1,
+ .values = uri->host.values + 1 };
+
+ // TODO
+ // - HTTPS default port 443
+ uri->port = kit_http1_tok_tail(&host_port_tok);
+ if (uri->port.size == 0)
+ uri->port = SZ(port_80);
+
+ uri->address = kit_http1_tok_next(&input_tok, SZ("?"), 1);
+ uri->query_string = kit_http1_tok_next(&input_tok, SZ("#"), 1);
+
+ uri->hash = kit_http1_tok_tail(&input_tok);
+
+ KIT_DA_INIT(uri->parameters, 0, alloc);
+
+ kit_http1_tok_t query_tok = {
+ .str = { .size = uri->query_string.size,
+ .values = uri->query_string.values },
+ .position = 0
+ };
+
+ for (;;) {
+ kit_str_t key = kit_http1_tok_next(&query_tok, SZ("="), 0);
+ if (key.size == 0)
+ break;
+ kit_str_t value = kit_http1_tok_next(&query_tok, SZ("&"), 1);
+
+ i64 index = uri->parameters.size;
+
+ KIT_DA_RESIZE(uri->parameters, index + 1);
+
+ assert(uri->parameters.size == index + 1);
+ if (uri->parameters.size != index + 1)
+ // FIXME
+ // Handle bad alloc error
+ break;
+
+ uri->parameters.values[index] = (kit_http1_str_pair_t) {
+ .key = key, .value = value
+ };
}
}
-static str_t kit_http1_method_to_str(i32 method) {
- static str_t methods[] = { SZ("OPTIONS"), SZ("GET"),
- SZ("HEAD"), SZ("POST"),
- SZ("PUT"), SZ("DELETE"),
- SZ("TRACE"), SZ("CONNECT") };
+static void kit_http1_uri_destroy(kit_http1_uri_t *uri) {
+ // TODO
+}
+
+static kit_str_t kit_http1_method_to_str(i32 method) {
+ static kit_str_t methods[] = { { .size = 7, .values = "OPTIONS" },
+ { .size = 3, .values = "GET" },
+ { .size = 4, .values = "HEAD" },
+ { .size = 4, .values = "POST" },
+ { .size = 3, .values = "PUT" },
+ { .size = 6, .values = "DELETE" },
+ { .size = 5, .values = "TRACE" },
+ { .size = 7, .values = "CONNECT" } };
+
+ assert(method >= 0 && method < sizeof methods / sizeof *methods);
+ if (method < 0 || method >= sizeof methods / sizeof *methods)
+ return (kit_str_t) { .size = 0, .values = NULL };
+
return methods[method];
}
+// TODO
+// - Return error status
static socket_t kit_http1_connect_to_uri(kit_http1_uri_t *uri) {
+ assert(uri != NULL);
+ if (uri == NULL)
+ return INVALID_SOCKET;
+
struct addrinfo hints, *result, *rp;
- memset(&hints, 0, sizeof(addrinfo));
+ memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
- i32 getaddrinfo_result = getaddrinfo(
- uri.host.c_str(), uri.port.c_str(), &hints, &result);
+ char host_str[128];
+ char port_str[128];
+
+ assert(uri->host.size < sizeof host_str);
+ memcpy(host_str, uri->host.values, uri->host.size);
+ host_str[uri->host.size] = '\0';
+
+ assert(uri->port.size < sizeof port_str);
+ memcpy(port_str, uri->port.values, uri->port.size);
+ port_str[uri->port.size] = '\0';
+
+ i32 getaddrinfo_result = getaddrinfo(host_str, port_str, &hints,
+ &result);
if (getaddrinfo_result != 0)
return INVALID_SOCKET;
socket_t fd = INVALID_SOCKET;
- for (rp = result; rp != nullptr; rp = rp->ai_next) {
+ for (rp = result; rp != NULL; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd == INVALID_SOCKET)
@@ -175,74 +269,226 @@ static socket_t kit_http1_connect_to_uri(kit_http1_uri_t *uri) {
return fd;
}
-static kit_str_builder_t kit_http1_buffered_read(socket_t fd) {
- i64 initial_factor = 4, buffer_increment_size = 8192,
- buffer_size = 0, bytes_read = 0;
- kit_str_builder_t buffer;
+// TODO
+// - Return error status
+static kit_str_builder_t kit_http1_buffered_read(
+ socket_t fd, kit_allocator_t *alloc) {
+ kit_str_builder_t buf;
+ KIT_DA_INIT(buf, KIT_HTTP1_BUFFER_SIZE, alloc);
+
+ // FIXME
+ // Handle bad alloc error
+ assert(buf.size == KIT_HTTP1_BUFFER_SIZE);
+ if (buf.size != KIT_HTTP1_BUFFER_SIZE)
+ return buf;
- buffer.resize(initial_factor * buffer_increment_size);
+ i64 offset = 0;
- bytes_read = recv(
- fd, ((char *) buffer.c_str()) + buffer_size,
- static_cast<socklen_t>(buffer.size() - buffer_size), 0);
+ for (;;) {
+ i64 chunk = buf.size - offset;
+ i64 n = recv(fd, buf.values + offset, (socklen_t) chunk, 0);
+ if (n < chunk)
+ break;
+ offset += n;
+ KIT_DA_RESIZE(buf, offset + KIT_HTTP1_BUFFER_SIZE);
- buffer_size += bytes_read;
+ // FIXME
+ // Handle bad alloc error
+ assert(buf.size == offset + KIT_HTTP1_BUFFER_SIZE);
+ if (buf.size != offset + KIT_HTTP1_BUFFER_SIZE)
+ break;
+ }
- buffer.resize(buffer_size);
- return buffer;
+ return buf;
}
+// TODO
+// - Return error status
static kit_http1_response_t kit_http1_request(
- i32 method, kit_http1_uri_t *uri, kit_str_builder_t *body) {
+ i32 method, kit_http1_uri_t *uri, kit_str_t body,
+ kit_allocator_t *alloc) {
socket_t fd = kit_http1_connect_to_uri(uri);
+ assert(fd != INVALID_SOCKET);
if (fd == INVALID_SOCKET) {
- return { .success = 0 };
+ kit_http1_response_t res;
+ memset(&res, 0, sizeof res);
+ res.success = 0;
+ return res;
+ }
+
+ kit_str_builder_t req;
+
+ // reserve
+ KIT_DA_INIT(req, 512, alloc);
+
+ assert(req.size == 512);
+ if (req.size != 512) {
+ // bad alloc
+ //
+
+ closesocket(fd);
+
+ kit_http1_response_t res;
+ memset(&res, 0, sizeof res);
+ res.success = 0;
+ return res;
+ }
+
+ req.size = 0;
+
+ // FIXME
+ // Handle errors
+ //
+
+ kit_str_append(&req, kit_http1_method_to_str(method));
+ kit_str_append(&req, SZ(" /"));
+ kit_str_append(&req, uri->address);
+ kit_str_append(&req,
+ uri->query_string.size == 0 ? SZ("") : SZ("?"));
+ kit_str_append(&req, uri->query_string);
+ kit_str_append(&req, SZ(" HTTP/1.1"));
+ kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
+
+ kit_str_append(&req, SZ("Host: "));
+ kit_str_append(&req, uri->host);
+ kit_str_append(&req, SZ(":"));
+ kit_str_append(&req, uri->port);
+ kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
+
+ kit_str_append(&req, SZ("Accept: */*"));
+ kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
+
+ kit_str_append(
+ &req,
+ SZ("Content-Type: text/plain; version=0.0.4; charset=utf-8"));
+ kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
+
+ // FIXME
+ // Use our own print instead
+ char content_length_str[11];
+ sprintf(content_length_str, "%lld", body.size);
+
+ kit_str_append(&req, SZ("Content-Length: "));
+ kit_str_append(&req,
+ (kit_str_t) { .size = strlen(content_length_str),
+ .values = content_length_str });
+ kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
+
+ kit_str_append(&req, SZ(KIT_HTTP1_NEWLINE));
+ kit_str_append(&req, body);
+
+ // Append '\0' at the end
+ //
+ {
+ i64 n = req.size;
+ KIT_DA_RESIZE(req, n + 1);
+
+ assert(req.size == n + 1);
+ if (req.size != n + 1) {
+ KIT_DA_DESTROY(req);
+ closesocket(fd);
+
+ kit_http1_response_t res;
+ memset(&res, 0, sizeof res);
+ res.success = 0;
+ return res;
+ }
+
+ req.size = n;
+ req.values[n] = '\0';
}
- kit_str_builder_t request =
- string(kit_http1_method_to_str(method)) + string(" /") +
- uri.address + ((uri.query_string == "") ? "" : "?") +
- uri.query_string + " HTTP/1.1" + KIT_HTTP1_NEWLINE +
- "Host: " + uri.host + ":" + uri.port + KIT_HTTP1_NEWLINE +
- "Accept: */*" + KIT_HTTP1_NEWLINE + content_type +
- KIT_HTTP1_NEWLINE +
- "Content-Length: " + to_string(body.size()) +
- KIT_HTTP1_NEWLINE + KIT_HTTP1_NEWLINE + body;
+ i64 req_size = req.size;
+ i64 n = send(fd, req.values, (socklen_t) req.size, 0);
+
+ KIT_DA_DESTROY(req);
+
+ if (n != req_size) {
+ // send failed
+ //
- send(fd, request.c_str(), static_cast<socklen_t>(request.size()),
- 0);
+ closesocket(fd);
- kit_str_builder_t buffer = kit_http1_buffered_read(fd);
+ kit_http1_response_t res;
+ memset(&res, 0, sizeof res);
+ res.success = 0;
+ return res;
+ }
+
+ kit_str_builder_t buffer = kit_http1_buffered_read(fd, alloc);
closesocket(fd);
- kit_http1_response_t result = { .success = 1 };
+ kit_http1_response_t res;
+ memset(&res, 0, sizeof res);
+ res.success = 1;
+
+ kit_http1_tok_t buf_tok = { .str = { .size = buffer.size,
+ .values = buffer.values },
+ .position = 0 };
- kit_http1_tok_t bt(buffer);
+ res.protocol = kit_str_build(
+ kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_SPACE), 0), alloc);
+ res.response = kit_str_build(
+ kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_SPACE), 0), alloc);
+ res.response_str = kit_str_build(
+ kit_http1_tok_next(&buf_tok, SZ(KIT_HTTP1_NEWLINE), 0), alloc);
- result.protocol = bt.next(KIT_HTTP1_SPACE, 0);
- result.response = bt.next(KIT_HTTP1_SPACE, 0);
- result.response_str = bt.next(KIT_HTTP1_NEWLINE, 0);
+ kit_str_builder_t header = kit_str_build(
+ kit_http1_tok_next(&buf_tok,
+ SZ(KIT_HTTP1_NEWLINE KIT_HTTP1_NEWLINE), 0),
+ alloc);
- kit_str_builder_t header = bt.next(
- KIT_HTTP1_NEWLINE KIT_HTTP1_NEWLINE, 0);
+ res.body = kit_str_build(kit_http1_tok_tail(&buf_tok), alloc);
- result.body = bt.tail();
+ kit_http1_tok_t header_tok = {
+ .str = (kit_str_t) { .size = header.size,
+ .values = header.values },
+ .position = 0
+ };
- kit_http1_tok_t ht(header);
+ KIT_DA_INIT(res.header, 0, alloc);
- do {
- kit_str_builder_t key = ht.next(KIT_HTTP1_HEADER_SEPARATOR, 0);
+ for (;;) {
+ kit_str_t key = kit_http1_tok_next(
+ &header_tok, SZ(KIT_HTTP1_HEADER_SEPARATOR), 0);
if (key.size == 0)
break;
- result.header[key] = ht.next(KIT_HTTP1_NEWLINE, 1);
- } while (1);
+ kit_str_t value = kit_http1_tok_next(&header_tok,
+ SZ(KIT_HTTP1_NEWLINE), 1);
+
+ i64 index = res.header.size;
+
+ KIT_DA_RESIZE(res.header, index + 1);
+
+ assert(res.header.size == index + 1);
+ if (res.header.size != index + 1) {
+ // FIXME
+ // Handle bad alloc error
+ res.success = 0;
+ break;
+ }
+
+ res.header.values[index] = (kit_http1_str_pair_t) {
+ .key = key, .value = value
+ };
+ }
+
+ return res;
+}
- return result;
+static void kit_http1_response_destroy(
+ kit_http1_response_t *response) {
+ // TODO
+ //
}
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+
#ifdef __cplusplus
}
#endif
diff --git a/source/kit/math.h b/source/kit/math.h
index 45508b7..f4ab33a 100644
--- a/source/kit/math.h
+++ b/source/kit/math.h
@@ -63,9 +63,8 @@ typedef struct {
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wunused-function"
# pragma GCC diagnostic ignored "-Wunknown-pragmas"
-
-# pragma GCC push_options
-# pragma GCC optimize("O3")
+# pragma GCC push_options
+# pragma GCC optimize("O3")
#endif
static vec2_t vec2(vec_t x, vec_t y) {
diff --git a/source/kit/sockets.h b/source/kit/sockets.h
index 31253fc..a9e2754 100644
--- a/source/kit/sockets.h
+++ b/source/kit/sockets.h
@@ -63,6 +63,7 @@ static i32 kit_socket_set_nonblocking(socket_t s) {
# include <sys/socket.h>
# include <sys/types.h>
# include <unistd.h>
+# include <netdb.h>
# define socket_t i32
# define closesocket close
diff --git a/source/kit/string_builder.c b/source/kit/string_builder.c
deleted file mode 100644
index f76c3a4..0000000
--- a/source/kit/string_builder.c
+++ /dev/null
@@ -1 +0,0 @@
-#include "string_builder.h"
diff --git a/source/kit/string_builder.h b/source/kit/string_builder.h
index c6d89ce..322f6e2 100644
--- a/source/kit/string_builder.h
+++ b/source/kit/string_builder.h
@@ -1,13 +1,63 @@
#ifndef KIT_STRING_BUILDER_H
#define KIT_STRING_BUILDER_H
+#include "status.h"
#include "string_ref.h"
#include "dynamic_array.h"
+#include <assert.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
typedef KIT_DA(char) kit_str_builder_t;
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-function"
+# pragma GCC diagnostic ignored "-Wunknown-pragmas"
+# pragma GCC push_options
+# pragma GCC optimize("O3")
+#endif
+
+static kit_str_builder_t kit_str_build(kit_str_t s,
+ kit_allocator_t *alloc) {
+ kit_str_builder_t builder;
+ KIT_DA_INIT(builder, s.size, alloc);
+ assert(builder.size == s.size);
+ if (builder.size == s.size)
+ memcpy(builder.values, s.values, s.size);
+ return builder;
+}
+
+static kit_status_t kit_str_append(kit_str_builder_t *a,
+ kit_str_t b) {
+ assert(a != NULL);
+ if (a == NULL)
+ return KIT_ERROR_INVALID_ARGUMENT;
+ if (b.size <= 0)
+ return KIT_OK;
+ i64 n = a->size;
+ KIT_DA_RESIZE(*a, n + b.size);
+ if (a->size != n + b.size)
+ return KIT_ERROR_BAD_ALLOC;
+ memcpy(a->values + n, b.values, b.size);
+ return KIT_OK;
+}
+
+#ifdef __GNUC__
+# pragma GCC pop_options
+# pragma GCC diagnostic pop
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
#ifndef KIT_DISABLE_SHORT_NAMES
# define str_builder_t kit_str_builder_t
+# define str_append kit_str_append
#endif
#endif
diff --git a/source/tests/_exe.c b/source/tests/_exe.c
index b406f48..f14e620 100644
--- a/source/tests/_exe.c
+++ b/source/tests/_exe.c
@@ -19,4 +19,5 @@
#include "duration.test.c"
#include "thread.test.c"
#include "xml.test.c"
+#include "http1.test.c"
#include "bench.test.c"
diff --git a/source/tests/http1.test.c b/source/tests/http1.test.c
new file mode 100644
index 0000000..d7befcb
--- /dev/null
+++ b/source/tests/http1.test.c
@@ -0,0 +1,6 @@
+#include "../kit/http1.h"
+
+#define KIT_TEST_FILE http1
+#include "../kit/kit_test.h"
+
+TEST("http1") { }