diff --git a/.gitmodules b/.gitmodules
index 8304380447..7aacc55d47 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -41,9 +41,6 @@
[submodule "ext/rcheevos"]
path = ext/rcheevos
url = https://github.com/RetroAchievements/rcheevos.git
-[submodule "ext/naett"]
- path = ext/naett
- url = https://github.com/erkkah/naett.git
[submodule "ext/libchdr"]
path = ext/libchdr
url = https://github.com/rtissera/libchdr.git
diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index d8d362c56c..d03f6a5365 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -366,7 +366,8 @@
-
+
+
@@ -868,7 +869,8 @@
-
+
+
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index 8ab76fd75d..a37ca9f313 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -500,8 +500,11 @@
Net
-
- ext\naett
+
+ ext\naett-lib
+
+
+ ext\naett-lib
Net
@@ -1231,8 +1234,11 @@
Net
-
- ext\naett
+
+ ext\naett-lib
+
+
+ ext\naett-lib
Net
@@ -1579,7 +1585,7 @@
{d6d5f6e0-1c72-496b-af11-6d52d5123033}
-
+
{34f45db9-5c08-49cb-b349-b9e760ce3213}
@@ -1680,4 +1686,4 @@
Data\Collections
-
\ No newline at end of file
+
diff --git a/Common/Net/HTTPNaettRequest.cpp b/Common/Net/HTTPNaettRequest.cpp
index 516a944045..f84accc98e 100644
--- a/Common/Net/HTTPNaettRequest.cpp
+++ b/Common/Net/HTTPNaettRequest.cpp
@@ -8,7 +8,7 @@
#include "Common/StringUtils.h"
#include "Common/Log.h"
-#include "ext/naett/naett.h"
+#include "ext/naett-lib/naett.h"
namespace http {
diff --git a/Common/Net/HTTPNaettRequest.h b/Common/Net/HTTPNaettRequest.h
index 71054d8a21..9d1a4d95b0 100644
--- a/Common/Net/HTTPNaettRequest.h
+++ b/Common/Net/HTTPNaettRequest.h
@@ -7,7 +7,7 @@
#ifndef HTTPS_NOT_AVAILABLE
-#include "ext/naett/naett.h"
+#include "ext/naett-lib/naett.h"
namespace http {
diff --git a/Common/Net/Resolve.cpp b/Common/Net/Resolve.cpp
index 13d8ff4c91..43538fb298 100644
--- a/Common/Net/Resolve.cpp
+++ b/Common/Net/Resolve.cpp
@@ -15,7 +15,7 @@
#include "Common/Net/Resolve.h"
#ifndef HTTPS_NOT_AVAILABLE
-#include "ext/naett/naett.h"
+#include "ext/naett-lib/naett.h"
#endif
#if PPSSPP_PLATFORM(ANDROID)
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj
index 3eadd1f69b..1306601e3b 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj
+++ b/UWP/CommonUWP/CommonUWP.vcxproj
@@ -294,7 +294,8 @@
-
+
+
@@ -477,7 +478,8 @@
-
+
+
@@ -505,4 +507,4 @@
-
\ No newline at end of file
+
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters
index ec52ec0f7a..c6c1527a4a 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj.filters
+++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters
@@ -94,7 +94,7 @@
{8b92cc15-8e3e-45b1-ba80-ad39b1a9cdc8}
-
+
{403c3154-5e8a-4da1-ba82-8f69bef25e14}
@@ -467,8 +467,11 @@
Net
-
- ext\naett
+
+ ext\naett-lib
+
+
+ ext\naett-lib
ext\minimp3
@@ -997,8 +1000,11 @@
Net
-
- ext\naett
+
+ ext\naett-lib
+
+
+ ext\naett-lib
Math
@@ -1193,4 +1199,4 @@
ext\at3_standalone
-
\ No newline at end of file
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 1cb776356d..eae8b2235a 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -165,7 +165,8 @@ SPIRV_CROSS_FILES := \
$(SRC)/ext/SPIRV-Cross/spirv_cross_parsed_ir.cpp
NAETT_FILES := \
- ${SRC}/ext/naett/naett.c
+ ${SRC}/ext/naett-lib/src/naett_core.c \
+ ${SRC}/ext/naett-lib/src/naett_android.c
MINIMP3_FILES := \
${SRC}/ext/minimp3/minimp3.cpp
diff --git a/ext/naett b/ext/naett
deleted file mode 160000
index 5f695cfa9f..0000000000
--- a/ext/naett
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 5f695cfa9fcbf30668a4d3ac4b4abf1cd89a1302
diff --git a/ext/naett-build/CMakeLists.txt b/ext/naett-build/CMakeLists.txt
index a4b81eeedd..bd3934e0b3 100644
--- a/ext/naett-build/CMakeLists.txt
+++ b/ext/naett-build/CMakeLists.txt
@@ -1,12 +1,22 @@
cmake_minimum_required (VERSION 3.10.0)
project (naett)
-set(SRC_DIR ../naett)
+set(SRC_DIR ../naett-lib/src)
set(ALL_SOURCE_FILES
- ${SRC_DIR}/naett.c
+ ${SRC_DIR}/naett_core.c
)
+if(WIN32)
+ list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_win.c)
+elseif(ANDROID)
+ list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_android.c)
+elseif(APPLE)
+ list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_osx.c)
+elseif(UNIX)
+ list(APPEND ALL_SOURCE_FILES ${SRC_DIR}/naett_linux.c)
+endif()
+
add_library(naett STATIC ${ALL_SOURCE_FILES})
if(WIN32)
target_link_libraries(naett winhttp)
diff --git a/ext/naett-lib/LICENSE b/ext/naett-lib/LICENSE
new file mode 100644
index 0000000000..a37fb793ec
--- /dev/null
+++ b/ext/naett-lib/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Erik Agsjö
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ext/naett-lib/README-ppsspp.md b/ext/naett-lib/README-ppsspp.md
new file mode 100644
index 0000000000..3a81953acd
--- /dev/null
+++ b/ext/naett-lib/README-ppsspp.md
@@ -0,0 +1,23 @@
+# naett in PPSSPP
+
+Vendored copy of [naett](https://github.com/erkkah/naett) by Erik Agsjö (MIT licensed, see `LICENSE`).
+
+Upstream version: **v0.3.3** (`5f695cfa9fcbf30668a4d3ac4b4abf1cd89a1302`, 2024-04-13)
+
+This used to be a git submodule. It's now in-tree because upstream has been dormant since
+April 2024 and we need to carry local changes (see below). It's ~1500 lines of C, which is
+smaller than several other things we already vendor.
+
+## Local changes
+
+Keep this list up to date - it's what makes it possible to move to a newer upstream later.
+
+- Dropped the generated single-file amalgam (`naett.c`) and `src/amalgam.h`, along with
+ `example/` and `testrig/`. We build `src/*.c` directly, so that the file you edit is the
+ file that gets compiled.
+- `src/naett_internal.h`: added `#include "../naett.h"`. The amalgam included `naett.h`
+ ahead of everything else; building the sources directly, each one needs it.
+- `src/naett_linux.c`: added `#include ` / `#include `. It uses
+ `exit`/`calloc`/`realloc`/`free`/`fprintf` but never included either header - in the amalgam it
+ got them from `naett_core.c` further up the concatenation. Building it on its own is an error
+ with a modern compiler.
diff --git a/ext/naett-lib/README.md b/ext/naett-lib/README.md
new file mode 100644
index 0000000000..6c69764ebd
--- /dev/null
+++ b/ext/naett-lib/README.md
@@ -0,0 +1,57 @@
+# naett /nɛt:/
+
+Tiny HTTP client library in C.
+
+Wraps native HTTP client functionality on macOS, Windows, Linux, iOS and Android in a single, simple non-blocking API.
+
+## Using `naett`
+
+Get the `naett.c` and `naett.h` files and throw them into your project. Check out the [example](./example) for a basic `Makefile` - based setup.
+
+The library needs to be initialized by a call to `naettInit()`. On Android, you need to provide a `JavaVM*` handle in the call to `naettInit()`.
+On the other platforms, call with `NULL`.
+
+See `naett.h` for reference docs.
+
+## Platform implementations
+
+`naett` uses the following HTTP client libraries on each platform:
+
+| Platform | Library / component | Build with |
+| --- | --- | --- |
+| macOS, iOS | NSURLRequest | -framework Foundation |
+| Windows | WinHTTP Sessions | -lwinhttp |
+| Android | java.net.URL | NDK |
+| Linux | libcurl | -lcurl -lpthread |
+
+### Example
+
+```C
+#include "naett.h"
+#include
+#include
+
+int main(int argc, char** argv) {
+ naettInit(NULL);
+
+ naettReq* req =
+ naettRequest("https://foo.site.net", naettMethod("GET"), naettHeader("accept", "application/json"));
+
+ naettRes* res = naettMake(req);
+
+ while (!naettComplete(res)) {
+ usleep(100 * 1000);
+ }
+
+ if (naettGetStatus(res) < 0) {
+ printf("Request failed\n");
+ return 1;
+ }
+
+ int bodyLength = 0;
+ const char* body = naettGetBody(res, &bodyLength);
+
+ printf("Got %d bytes of type '%s':\n", bodyLength, naettGetHeader(res, "Content-Type"));
+ printf("%.100s\n...\n", body);
+}
+```
diff --git a/ext/naett-lib/naett.h b/ext/naett-lib/naett.h
new file mode 100644
index 0000000000..b79130db3b
--- /dev/null
+++ b/ext/naett-lib/naett.h
@@ -0,0 +1,145 @@
+#ifndef LIBNAETT_H
+#define LIBNAETT_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if __ANDROID__
+#include
+typedef JavaVM* naettInitData;
+#else
+typedef void* naettInitData;
+#endif
+
+#define NAETT_UA "Naett/1.0"
+
+/**
+ * @brief Global init method.
+ * Call to initialize the library.
+ */
+void naettInit(naettInitData initThing);
+
+typedef struct naettReq naettReq;
+typedef struct naettRes naettRes;
+// If naettReadFunc is called with NULL dest, it must respond with the body size
+typedef int (*naettReadFunc)(void* dest, int bufferSize, void* userData);
+typedef int (*naettWriteFunc)(const void* source, int bytes, void* userData);
+typedef int (*naettHeaderLister)(const char* name, const char* value, void* userData);
+
+// Option to `naettRequest`
+typedef struct naettOption naettOption;
+
+// Sets request method. Defaults to "GET".
+naettOption* naettMethod(const char* method);
+// Adds a request header.
+naettOption* naettHeader(const char* name, const char* value);
+// Sets the request body. Ignored if a body reader is configured.
+// The body is not copied, and the passed pointer must be valid for the
+// lifetime of the request.
+naettOption* naettBody(const char* body, int size);
+// Sets a request body reader.
+naettOption* naettBodyReader(naettReadFunc reader, void* userData);
+// Sets a response body writer.
+naettOption* naettBodyWriter(naettWriteFunc writer, void* userData);
+// Sets connection timeout in milliseconds.
+naettOption* naettTimeout(int milliSeconds);
+// Sets the user agent.
+naettOption* naettUserAgent(const char *userAgent);
+
+/**
+ * @brief Creates a new request to the specified url.
+ * Use varargs options to configure the connection and following request.
+ */
+#define naettRequest(url, ...) naettRequest_va(url, countOptions(__VA_ARGS__), ##__VA_ARGS__)
+
+/**
+ * @brief Creates a new request to the specified url.
+ * Uses an array of options rather than varargs.
+ */
+naettReq* naettRequestWithOptions(const char* url, int numOptions, const naettOption** options);
+
+/**
+ * @brief Makes a request and returns a response object.
+ * The actual request is processed asynchronously, use `naettComplete`
+ * to check if the response is completed.
+ *
+ * A request object can be reused multiple times to make requests, but
+ * there can be only one active request using the same request object.
+ */
+naettRes* naettMake(naettReq* request);
+
+/**
+ * @brief Frees a previously allocated request object.
+ * The request must not have any pending responses.
+ */
+void naettFree(naettReq* request);
+
+/**
+ * @brief Checks if a response is complete, with a result
+ * or with an error.
+ * Use `naettGetStatus` to get the status.
+ */
+int naettComplete(const naettRes* response);
+
+enum naettStatus {
+ naettConnectionError = -1,
+ naettProtocolError = -2,
+ naettReadError = -3,
+ naettWriteError = -4,
+ naettGenericError = -5,
+ naettProcessing = 0,
+};
+
+/**
+ * @brief Returns the status of a response.
+ * Status codes > 0 are HTTP status codes as returned by the server.
+ * Status codes < 0 are processing errors according to the `naettStatus`
+ * enum values.
+ */
+int naettGetStatus(const naettRes* response);
+
+/**
+ * @brief Returns the response body.
+ * The body returned by this method is always empty when a custom
+ * body reader has been set up using the `naettBodyReader` option.
+ */
+const void* naettGetBody(naettRes* response, int* outSize);
+
+/**
+ * @brief Returns the HTTP header value for the specified header name.
+ */
+const char* naettGetHeader(naettRes* response, const char* name);
+
+/**
+ * @brief Returns how many bytes have been read from the response so far,
+ * and the integer pointed to by totalSize gets the Content-Length if available,
+ * or -1 if not (or 0 if headers have not been read yet).
+ */
+int naettGetTotalBytesRead(naettRes* response, int* totalSize);
+
+/**
+ * @brief Enumerates all response headers as long as the `lister`
+ * returns true.
+ */
+void naettListHeaders(naettRes* response, naettHeaderLister lister, void* userData);
+
+/**
+ * @brief Returns the request that initiated this response
+ */
+naettReq* naettGetRequest(naettRes* response);
+
+/**
+ * @brief Closes a response object.
+ */
+void naettClose(naettRes* response);
+
+// Varargs glue
+naettReq* naettRequest_va(const char* url, int numOptions, ...);
+#define countOptions(...) ((sizeof((void*[]){ __VA_ARGS__ }) / sizeof(void*)))
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // LIBNAETT_H
diff --git a/ext/naett-lib/src/naett_android.c b/ext/naett-lib/src/naett_android.c
new file mode 100644
index 0000000000..30d35550dd
--- /dev/null
+++ b/ext/naett-lib/src/naett_android.c
@@ -0,0 +1,281 @@
+#include "naett_internal.h"
+
+#ifdef __ANDROID__
+
+#ifdef __cplusplus
+#error "Cannot build in C++ mode"
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef NDEBUG
+#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, "naett", __VA_ARGS__))
+#else
+#define LOGD(...) ((void)0)
+#endif
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "naett", __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "naett", __VA_ARGS__))
+
+static JavaVM* globalVM = NULL;
+
+static JavaVM* getVM() {
+ if (globalVM == NULL) {
+ LOGE("Panic: No VM configured, exiting.");
+ exit(42);
+ }
+ return globalVM;
+}
+
+static JNIEnv* getEnv() {
+ JavaVM* vm = getVM();
+ JNIEnv* env;
+ (*vm)->AttachCurrentThread(vm, &env, NULL);
+ return env;
+}
+
+static int catch (JNIEnv* env) {
+ int thrown = (*env)->ExceptionCheck(env);
+ if (thrown) {
+ (*env)->ExceptionDescribe(env);
+ }
+ return thrown;
+}
+
+static jmethodID getMethod(JNIEnv* env, jobject instance, const char* method, const char* sig) {
+ jclass clazz = (*env)->GetObjectClass(env, instance);
+ jmethodID id = (*env)->GetMethodID(env, clazz, method, sig);
+ (*env)->DeleteLocalRef(env, clazz);
+ return id;
+}
+
+static jobject call(JNIEnv* env, jobject instance, const char* method, const char* sig, ...) {
+ jmethodID methodID = getMethod(env, instance, method, sig);
+ va_list args;
+ va_start(args, sig);
+ jobject result = (*env)->CallObjectMethodV(env, instance, methodID, args);
+ va_end(args);
+ return result;
+}
+
+static void voidCall(JNIEnv* env, jobject instance, const char* method, const char* sig, ...) {
+ jmethodID methodID = getMethod(env, instance, method, sig);
+ va_list args;
+ va_start(args, sig);
+ (*env)->CallVoidMethodV(env, instance, methodID, args);
+ va_end(args);
+}
+
+static jint intCall(JNIEnv* env, jobject instance, const char* method, const char* sig, ...) {
+ jmethodID methodID = getMethod(env, instance, method, sig);
+ va_list args;
+ va_start(args, sig);
+ jint result = (*env)->CallIntMethodV(env, instance, methodID, args);
+ va_end(args);
+ return result;
+}
+
+void naettPlatformInit(naettInitData initData) {
+ globalVM = initData;
+}
+
+int naettPlatformInitRequest(InternalRequest* req) {
+ JNIEnv* env = getEnv();
+ (*env)->PushLocalFrame(env, 10);
+ jclass URL = (*env)->FindClass(env, "java/net/URL");
+ jmethodID newURL = (*env)->GetMethodID(env, URL, "", "(Ljava/lang/String;)V");
+ jstring urlString = (*env)->NewStringUTF(env, req->url);
+ jobject url = (*env)->NewObject(env, URL, newURL, urlString);
+ if (catch (env)) {
+ (*env)->PopLocalFrame(env, NULL);
+ return 0;
+ }
+ req->urlObject = (*env)->NewGlobalRef(env, url);
+ (*env)->PopLocalFrame(env, NULL);
+ return 1;
+}
+
+static void* processRequest(void* data) {
+ const int bufSize = 10240;
+ char byteBuffer[bufSize];
+ InternalResponse* res = (InternalResponse*)data;
+ InternalRequest* req = res->request;
+
+ JNIEnv* env = getEnv();
+ (*env)->PushLocalFrame(env, 100);
+
+ jobject connection = call(env, req->urlObject, "openConnection", "()Ljava/net/URLConnection;");
+ if (catch (env)) {
+ res->code = naettConnectionError;
+ goto finally;
+ }
+
+ {
+ jstring name = (*env)->NewStringUTF(env, "User-Agent");
+ jstring value = (*env)->NewStringUTF(env, req->options.userAgent ? req->options.userAgent : NAETT_UA);
+ voidCall(env, connection, "addRequestProperty", "(Ljava/lang/String;Ljava/lang/String;)V", name, value);
+ (*env)->DeleteLocalRef(env, name);
+ (*env)->DeleteLocalRef(env, value);
+ }
+
+ KVLink* header = req->options.headers;
+ while (header != NULL) {
+ jstring name = (*env)->NewStringUTF(env, header->key);
+ jstring value = (*env)->NewStringUTF(env, header->value);
+ voidCall(env, connection, "addRequestProperty", "(Ljava/lang/String;Ljava/lang/String;)V", name, value);
+ (*env)->DeleteLocalRef(env, name);
+ (*env)->DeleteLocalRef(env, value);
+ header = header->next;
+ }
+
+ jobject outputStream = NULL;
+ if (strcmp(req->options.method, "POST") == 0 || strcmp(req->options.method, "PUT") == 0 ||
+ strcmp(req->options.method, "PATCH") == 0 || strcmp(req->options.method, "DELETE") == 0) {
+ voidCall(env, connection, "setDoOutput", "(Z)V", 1);
+ outputStream = call(env, connection, "getOutputStream", "()Ljava/io/OutputStream;");
+ }
+ jobject methodString = (*env)->NewStringUTF(env, req->options.method);
+ voidCall(env, connection, "setRequestMethod", "(Ljava/lang/String;)V", methodString);
+ voidCall(env, connection, "setConnectTimeout", "(I)V", req->options.timeoutMS);
+ voidCall(env, connection, "setInstanceFollowRedirects", "(Z)V", 1);
+
+ voidCall(env, connection, "connect", "()V");
+ if (catch (env)) {
+ res->code = naettConnectionError;
+ goto finally;
+ }
+
+ jbyteArray buffer = (*env)->NewByteArray(env, bufSize);
+
+ if (outputStream != NULL) {
+ int bytesRead = 0;
+ if (req->options.bodyReader != NULL)
+ do {
+ bytesRead = req->options.bodyReader(byteBuffer, bufSize, req->options.bodyReaderData);
+ if (bytesRead > 0) {
+ (*env)->SetByteArrayRegion(env, buffer, 0, bytesRead, (const jbyte*) byteBuffer);
+ voidCall(env, outputStream, "write", "([BII)V", buffer, 0, bytesRead);
+ } else {
+ break;
+ }
+ } while (!res->closeRequested);
+ voidCall(env, outputStream, "close", "()V");
+ }
+
+ jobject headerMap = call(env, connection, "getHeaderFields", "()Ljava/util/Map;");
+ if (catch (env)) {
+ res->code = naettProtocolError;
+ goto finally;
+ }
+
+ jobject headerSet = call(env, headerMap, "keySet", "()Ljava/util/Set;");
+ jarray headers = call(env, headerSet, "toArray", "()[Ljava/lang/Object;");
+ jsize headerCount = (*env)->GetArrayLength(env, headers);
+
+ KVLink *firstHeader = NULL;
+ for (int i = 0; i < headerCount; i++) {
+ jstring name = (*env)->GetObjectArrayElement(env, headers, i);
+ if (name == NULL) {
+ continue;
+ }
+ const char* nameString = (*env)->GetStringUTFChars(env, name, NULL);
+
+ jobject values = call(env, headerMap, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", name);
+ jstring value = call(env, values, "get", "(I)Ljava/lang/Object;", 0);
+ const char* valueString = (*env)->GetStringUTFChars(env, value, NULL);
+
+ naettAlloc(KVLink, node);
+ node->key = strdup(nameString);
+ node->value = strdup(valueString);
+ node->next = firstHeader;
+ firstHeader = node;
+
+ (*env)->ReleaseStringUTFChars(env, name, nameString);
+ (*env)->ReleaseStringUTFChars(env, value, valueString);
+
+ (*env)->DeleteLocalRef(env, name);
+ (*env)->DeleteLocalRef(env, value);
+ (*env)->DeleteLocalRef(env, values);
+ }
+ res->headers = firstHeader;
+
+ const char *contentLength = naettGetHeader((naettRes *)res, "Content-Length");
+ if (!contentLength || sscanf(contentLength, "%d", &res->contentLength) != 1) {
+ res->contentLength = -1;
+ }
+
+ int statusCode = intCall(env, connection, "getResponseCode", "()I");
+
+ jobject inputStream = NULL;
+
+ if (statusCode >= 400) {
+ inputStream = call(env, connection, "getErrorStream", "()Ljava/io/InputStream;");
+ } else {
+ inputStream = call(env, connection, "getInputStream", "()Ljava/io/InputStream;");
+ }
+ if (catch (env)) {
+ res->code = naettProtocolError;
+ goto finally;
+ }
+
+ jint bytesRead = 0;
+ do {
+ bytesRead = intCall(env, inputStream, "read", "([B)I", buffer);
+ if (catch (env)) {
+ res->code = naettReadError;
+ goto finally;
+ }
+ if (bytesRead < 0) {
+ break;
+ } else if (bytesRead > 0) {
+ (*env)->GetByteArrayRegion(env, buffer, 0, bytesRead, (jbyte*) byteBuffer);
+ req->options.bodyWriter(byteBuffer, bytesRead, req->options.bodyWriterData);
+ res->totalBytesRead += bytesRead;
+ }
+ } while (!res->closeRequested);
+
+ voidCall(env, inputStream, "close", "()V");
+
+ res->code = statusCode;
+
+finally:
+ res->complete = 1;
+ (*env)->PopLocalFrame(env, NULL);
+ JavaVM* vm = getVM();
+ (*env)->ExceptionClear(env);
+ (*vm)->DetachCurrentThread(vm);
+ return NULL;
+}
+
+static void startWorkerThread(InternalResponse* res) {
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
+ pthread_create(&res->workerThread, &attr, processRequest, res);
+ pthread_setname_np(res->workerThread, "naett worker thread");
+}
+
+void naettPlatformMakeRequest(InternalResponse* res) {
+ startWorkerThread(res);
+}
+
+void naettPlatformFreeRequest(InternalRequest* req) {
+ JNIEnv* env = getEnv();
+ (*env)->DeleteGlobalRef(env, req->urlObject);
+}
+
+void naettPlatformCloseResponse(InternalResponse* res) {
+ res->closeRequested = 1;
+ if (res->workerThread != 0) {
+ int joinResult = pthread_join(res->workerThread, NULL);
+ if (joinResult != 0) {
+ LOGE("Failed to join: %s", strerror(joinResult));
+ }
+ }
+}
+
+#endif // __ANDROID__
diff --git a/ext/naett-lib/src/naett_core.c b/ext/naett-lib/src/naett_core.c
new file mode 100644
index 0000000000..0e9ee529e7
--- /dev/null
+++ b/ext/naett-lib/src/naett_core.c
@@ -0,0 +1,405 @@
+#include "naett_internal.h"
+#include
+#include
+#include
+#include
+#include
+
+typedef struct InternalParam* InternalParamPtr;
+typedef void (*ParamSetter)(InternalParamPtr param, InternalRequest* req);
+
+typedef struct InternalParam {
+ ParamSetter setter;
+ int offset;
+ union {
+ int integer;
+ const char* string;
+ struct {
+ const char* key;
+ const char* value;
+ } kv;
+ void* ptr;
+ void (*func)(void);
+ };
+} InternalParam;
+
+typedef struct InternalOption {
+ #define maxParams 2
+ int numParams;
+ InternalParam params[maxParams];
+} InternalOption;
+
+static void stringSetter(InternalParamPtr param, InternalRequest* req) {
+ char* stringCopy = strdup(param->string);
+ char* opaque = (char*)&req->options;
+ char** stringField = (char**)(opaque + param->offset);
+ if (*stringField) {
+ free(*stringField);
+ }
+ *stringField = stringCopy;
+}
+
+static void intSetter(InternalParamPtr param, InternalRequest* req) {
+ char* opaque = (char*)&req->options;
+ int* intField = (int*)(opaque + param->offset);
+ *intField = param->integer;
+}
+
+static void ptrSetter(InternalParamPtr param, InternalRequest* req) {
+ char* opaque = (char*)&req->options;
+ void** ptrField = (void**)(opaque + param->offset);
+ *ptrField = param->ptr;
+}
+
+static void kvSetter(InternalParamPtr param, InternalRequest* req) {
+ char* opaque = (char*)&req->options;
+ KVLink** kvField = (KVLink**)(opaque + param->offset);
+
+ naettAlloc(KVLink, newNode);
+ newNode->key = strdup(param->kv.key);
+ newNode->value = strdup(param->kv.value);
+ newNode->next = *kvField;
+
+ *kvField = newNode;
+}
+
+static int defaultBodyReader(void* dest, int bufferSize, void* userData) {
+ Buffer* buffer = (Buffer*) userData;
+
+ if (dest == NULL) {
+ return buffer->size;
+ }
+
+ int bytesToRead = buffer->size - buffer->position;
+ if (bytesToRead > bufferSize) {
+ bytesToRead = bufferSize;
+ }
+
+ const char* source = ((const char*)buffer->data) + buffer->position;
+ memcpy(dest, source, bytesToRead);
+ buffer->position += bytesToRead;
+ return bytesToRead;
+}
+
+static int defaultBodyWriter(const void* source, int bytes, void* userData) {
+ Buffer* buffer = (Buffer*) userData;
+ int newCapacity = buffer->capacity;
+ if (newCapacity == 0) {
+ newCapacity = bytes;
+ }
+ while (newCapacity - buffer->size < bytes) {
+ newCapacity *= 2;
+ }
+ if (newCapacity != buffer->capacity) {
+ buffer->data = realloc(buffer->data, newCapacity);
+ buffer->capacity = newCapacity;
+ }
+ char* dest = ((char*)buffer->data) + buffer->size;
+ memcpy(dest, source, bytes);
+ buffer->size += bytes;
+ return bytes;
+}
+
+static int initialized = 0;
+
+static void initRequest(InternalRequest* req, const char* url) {
+ assert(initialized);
+ req->options.method = strdup("GET");
+ req->options.timeoutMS = 5000;
+ req->url = strdup(url);
+}
+
+static void applyOptionParams(InternalRequest* req, InternalOption* option) {
+ for (int j = 0; j < option->numParams; j++) {
+ InternalParam* param = option->params + j;
+ param->setter(param, req);
+ }
+}
+
+// Public API
+
+void naettInit(naettInitData initData) {
+ assert(!initialized);
+ naettPlatformInit(initData);
+ initialized = 1;
+}
+
+naettOption* naettMethod(const char* method) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 1;
+ InternalParam* param = &option->params[0];
+
+ param->string = method;
+ param->offset = offsetof(RequestOptions, method);
+ param->setter = stringSetter;
+
+ return (naettOption*)option;
+}
+
+naettOption* naettHeader(const char* name, const char* value) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 1;
+ InternalParam* param = &option->params[0];
+
+ param->kv.key = name;
+ param->kv.value = value;
+ param->offset = offsetof(RequestOptions, headers);
+ param->setter = kvSetter;
+
+ return (naettOption*)option;
+}
+
+naettOption* naettUserAgent(const char* userAgent) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 1;
+ InternalParam* param = &option->params[0];
+
+ param->string = userAgent;
+ param->offset = offsetof(RequestOptions, userAgent);
+ param->setter = stringSetter;
+
+ return (naettOption*)option;
+}
+
+naettOption* naettTimeout(int timeoutMS) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 1;
+ InternalParam* param = &option->params[0];
+
+ param->integer = timeoutMS;
+ param->offset = offsetof(RequestOptions, timeoutMS);
+ param->setter = intSetter;
+
+ return (naettOption*)option;
+}
+
+naettOption* naettBody(const char* body, int size) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 2;
+
+ InternalParam* bodyParam = &option->params[0];
+ InternalParam* sizeParam = &option->params[1];
+
+ bodyParam->ptr = (void*)body;
+ bodyParam->offset = offsetof(RequestOptions, body) + offsetof(Buffer, data);
+ bodyParam->setter = ptrSetter;
+
+ sizeParam->integer = size;
+ sizeParam->offset = offsetof(RequestOptions, body) + offsetof(Buffer, size);
+ sizeParam->setter = intSetter;
+
+ return (naettOption*)option;
+}
+
+naettOption* naettBodyReader(naettReadFunc reader, void* userData) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 2;
+
+ InternalParam* readerParam = &option->params[0];
+ InternalParam* dataParam = &option->params[1];
+
+ readerParam->func = (void (*)(void)) reader;
+ readerParam->offset = offsetof(RequestOptions, bodyReader);
+ readerParam->setter = ptrSetter;
+
+ dataParam->ptr = userData;
+ dataParam->offset = offsetof(RequestOptions, bodyReaderData);
+ dataParam->setter = ptrSetter;
+
+ return (naettOption*)option;
+}
+
+naettOption* naettBodyWriter(naettWriteFunc writer, void* userData) {
+ naettAlloc(InternalOption, option);
+ option->numParams = 2;
+
+ InternalParam* writerParam = &option->params[0];
+ InternalParam* dataParam = &option->params[1];
+
+ writerParam->func = (void(*)(void)) writer;
+ writerParam->offset = offsetof(RequestOptions, bodyWriter);
+ writerParam->setter = ptrSetter;
+
+ dataParam->ptr = userData;
+ dataParam->offset = offsetof(RequestOptions, bodyWriterData);
+ dataParam->setter = ptrSetter;
+
+ return (naettOption*)option;
+}
+
+void setupDefaultRW(InternalRequest* req) {
+ if (req->options.bodyReader == NULL) {
+ req->options.bodyReader = defaultBodyReader;
+ req->options.bodyReaderData = (void*) &req->options.body;
+ }
+ if (req->options.bodyReader == defaultBodyReader) {
+ req->options.body.position = 0;
+ }
+ if (req->options.bodyWriter == NULL) {
+ req->options.bodyWriter = defaultBodyWriter;
+ }
+}
+
+naettReq* naettRequest_va(const char* url, int numArgs, ...) {
+ assert(url != NULL);
+
+ va_list args;
+ InternalOption* option;
+ naettAlloc(InternalRequest, req);
+ initRequest(req, url);
+
+ va_start(args, numArgs);
+ for (int i = 0; i < numArgs; i++) {
+ option = va_arg(args, InternalOption*);
+ applyOptionParams(req, option);
+ free(option);
+ }
+ va_end(args);
+
+ setupDefaultRW(req);
+
+ if (naettPlatformInitRequest(req)) {
+ return (naettReq*)req;
+ }
+
+ naettFree((naettReq*) req);
+ return NULL;
+}
+
+naettReq* naettRequestWithOptions(const char* url, int numOptions, const naettOption** options) {
+ assert(url != NULL);
+ assert(numOptions == 0 || options != NULL);
+
+ naettAlloc(InternalRequest, req);
+ initRequest(req, url);
+
+ for (int i = 0; i < numOptions; i++) {
+ InternalOption* option = (InternalOption*)options[i];
+ applyOptionParams(req, option);
+ free(option);
+ }
+
+ setupDefaultRW(req);
+
+ if (naettPlatformInitRequest(req)) {
+ return (naettReq*)req;
+ }
+
+ naettFree((naettReq*) req);
+ return NULL;
+}
+
+naettRes* naettMake(naettReq* request) {
+ assert(initialized);
+ assert(request != NULL);
+
+ InternalRequest* req = (InternalRequest*)request;
+ naettAlloc(InternalResponse, res);
+ res->request = req;
+
+ if (req->options.bodyWriter == defaultBodyWriter) {
+ req->options.bodyWriterData = (void*) &res->body;
+ }
+
+ naettPlatformMakeRequest(res);
+ return (naettRes*) res;
+}
+
+const void* naettGetBody(naettRes* response, int* size) {
+ assert(response != NULL);
+ assert(size != NULL);
+
+ InternalResponse* res = (InternalResponse*)response;
+ *size = res->body.size;
+ return res->body.data;
+}
+
+int naettGetTotalBytesRead(naettRes* response, int* totalSize) {
+ assert(response != NULL);
+ assert(totalSize != NULL);
+
+ InternalResponse* res = (InternalResponse*)response;
+ *totalSize = res->contentLength;
+ return res->totalBytesRead;
+}
+
+const char* naettGetHeader(naettRes* response, const char* name) {
+ assert(response != NULL);
+ assert(name != NULL);
+
+ InternalResponse* res = (InternalResponse*)response;
+ KVLink* node = res->headers;
+ while (node) {
+ if (strcasecmp(name, node->key) == 0) {
+ return node->value;
+ }
+ node = node->next;
+ }
+ return NULL;
+}
+
+void naettListHeaders(naettRes* response, naettHeaderLister lister, void* userData) {
+ assert(response != NULL);
+ assert(lister != NULL);
+
+ InternalResponse* res = (InternalResponse*)response;
+ KVLink* node = res->headers;
+ while (node) {
+ if (!lister(node->key, node->value, userData)) {
+ return;
+ }
+ node = node->next;
+ }
+}
+
+naettReq* naettGetRequest(naettRes* response) {
+ assert(response != NULL);
+ InternalResponse* res = (InternalResponse*)response;
+ return (naettReq*) res->request;
+}
+
+int naettComplete(const naettRes* response) {
+ assert(response != NULL);
+ InternalResponse* res = (InternalResponse*)response;
+ return res->complete;
+}
+
+int naettGetStatus(const naettRes* response) {
+ assert(response != NULL);
+ InternalResponse* res = (InternalResponse*)response;
+ return res->code;
+}
+
+static void freeKVList(KVLink* node) {
+ while (node != NULL) {
+ free((void*) node->key);
+ free((void*) node->value);
+ KVLink* next = node->next;
+ free(node);
+ node = next;
+ }
+}
+
+void naettFree(naettReq* request) {
+ assert(request != NULL);
+
+ InternalRequest* req = (InternalRequest*)request;
+ naettPlatformFreeRequest(req);
+ KVLink* node = req->options.headers;
+ freeKVList(node);
+ free((void*)req->options.method);
+ free((void*)req->url);
+ free(request);
+}
+
+void naettClose(naettRes* response) {
+ assert(response != NULL);
+
+ InternalResponse* res = (InternalResponse*)response;
+ res->request = NULL;
+ naettPlatformCloseResponse(res);
+ KVLink* node = res->headers;
+ freeKVList(node);
+ free(res->body.data);
+ free(response);
+}
diff --git a/ext/naett-lib/src/naett_internal.h b/ext/naett-lib/src/naett_internal.h
new file mode 100644
index 0000000000..8631b0a83d
--- /dev/null
+++ b/ext/naett-lib/src/naett_internal.h
@@ -0,0 +1,120 @@
+#ifndef NAETT_INTERNAL_H
+#define NAETT_INTERNAL_H
+
+// PPSSPP: The upstream amalgamated build (naett.c) included naett.h before this header.
+// We build the sources directly, so include it here instead.
+#include "../naett.h"
+
+#ifdef _MSC_VER
+ #define strcasecmp _stricmp
+ #undef strdup
+ #define strdup _strdup
+#endif
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
+#include
+#include
+#ifndef min
+ #define min(x,y) ((x) < (y) ? (x) : (y))
+#endif
+#define __WINDOWS__ 1
+#endif
+
+#if __linux__ && !__ANDROID__
+#define __LINUX__ 1
+#include
+#endif
+
+#if __ANDROID__
+#include
+#include
+#endif
+
+#ifdef __APPLE__
+#include "TargetConditionals.h"
+#include
+#if TARGET_OS_IPHONE
+#define __IOS__ 1
+#else
+#define __MACOS__ 1
+#endif
+#endif
+
+#define naettAlloc(TYPE, VAR) TYPE* VAR = (TYPE*)calloc(1, sizeof(TYPE))
+
+typedef struct KVLink {
+ const char* key;
+ const char* value;
+ struct KVLink* next;
+} KVLink;
+
+typedef struct Buffer {
+ void* data;
+ int size;
+ int capacity;
+ int position;
+} Buffer;
+
+typedef struct {
+ const char* method;
+ const char* userAgent;
+ int timeoutMS;
+ naettReadFunc bodyReader;
+ void* bodyReaderData;
+ naettWriteFunc bodyWriter;
+ void* bodyWriterData;
+ KVLink* headers;
+ Buffer body;
+} RequestOptions;
+
+typedef struct {
+ RequestOptions options;
+ const char* url;
+#if __APPLE__
+ id urlRequest;
+#endif
+#if __ANDROID__
+ jobject urlObject;
+#endif
+#if __WINDOWS__
+ HINTERNET session;
+ HINTERNET connection;
+ HINTERNET request;
+ LPWSTR host;
+ LPWSTR resource;
+#endif
+} InternalRequest;
+
+typedef struct {
+ InternalRequest* request;
+ int code;
+ int complete;
+ KVLink* headers;
+ Buffer body;
+ int contentLength; // 0 if headers not read, -1 if Content-Length missing.
+ int totalBytesRead;
+#if __APPLE__
+ id session;
+#endif
+#if __ANDROID__
+ pthread_t workerThread;
+ int closeRequested;
+#endif
+#if __LINUX__
+ struct curl_slist* headerList;
+#endif
+#if __WINDOWS__
+ char buffer[10240];
+ size_t bytesLeft;
+#endif
+} InternalResponse;
+
+void naettPlatformInit(naettInitData initData);
+int naettPlatformInitRequest(InternalRequest* req);
+void naettPlatformMakeRequest(InternalResponse* res);
+void naettPlatformFreeRequest(InternalRequest* req);
+void naettPlatformCloseResponse(InternalResponse* res);
+
+#endif // NAETT_INTERNAL_H
diff --git a/ext/naett-lib/src/naett_linux.c b/ext/naett-lib/src/naett_linux.c
new file mode 100644
index 0000000000..0b7572592a
--- /dev/null
+++ b/ext/naett-lib/src/naett_linux.c
@@ -0,0 +1,234 @@
+#include "naett_internal.h"
+
+#if __linux__ && !__ANDROID__
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static pthread_t workerThread;
+static int handleReadFD = 0;
+static int handleWriteFD = 0;
+
+static void panic(const char* message) {
+ fprintf(stderr, "%s\n", message);
+ exit(1);
+}
+
+static void* curlWorker(void* data) {
+ CURLM* mc = (CURLM*)data;
+ int activeHandles = 0;
+ int messagesLeft = 1;
+
+ struct curl_waitfd readFd = { handleReadFD, CURL_WAIT_POLLIN };
+
+ union {
+ CURL* handle;
+ char buf[sizeof(CURL*)];
+ } newHandle;
+
+ int newHandlePos = 0;
+
+ while (1) {
+ int status = curl_multi_perform(mc, &activeHandles);
+ if (status != CURLM_OK) {
+ panic("CURL processing failure");
+ }
+
+ struct CURLMsg* message = curl_multi_info_read(mc, &messagesLeft);
+ if (message && message->msg == CURLMSG_DONE) {
+ CURL* handle = message->easy_handle;
+ InternalResponse* res = NULL;
+ curl_easy_getinfo(handle, CURLINFO_PRIVATE, (char**)&res);
+ curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &res->code);
+ res->complete = 1;
+ curl_easy_cleanup(handle);
+ }
+
+
+ int readyFDs = 0;
+ curl_multi_wait(mc, &readFd, 1, 1, &readyFDs);
+
+ if (readyFDs == 0 && activeHandles == 0 && messagesLeft == 0) {
+ usleep(100 * 1000);
+ }
+
+ int bytesRead = read(handleReadFD, newHandle.buf, sizeof(newHandle.buf) - newHandlePos);
+ if (bytesRead > 0) {
+ newHandlePos += bytesRead;
+ }
+ if (newHandlePos == sizeof(newHandle.buf)) {
+ curl_multi_add_handle(mc, newHandle.handle);
+ newHandlePos = 0;
+ }
+ }
+
+ return NULL;
+}
+
+void naettPlatformInit(naettInitData initData) {
+ curl_global_init(CURL_GLOBAL_ALL);
+ CURLM* mc = curl_multi_init();
+ int fds[2];
+ if (pipe(fds) != 0) {
+ panic("Failed to open pipe");
+ }
+ handleReadFD = fds[0];
+ handleWriteFD = fds[1];
+
+ int flags = fcntl(handleReadFD, F_GETFL, 0);
+ fcntl(handleReadFD, F_SETFL, flags | O_NONBLOCK);
+
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ pthread_create(&workerThread, &attr, curlWorker, mc);
+}
+
+int naettPlatformInitRequest(InternalRequest* req) {
+ return 1;
+}
+
+static size_t readCallback(char* buffer, size_t size, size_t numItems, void* userData) {
+ InternalResponse* res = (InternalResponse*)userData;
+ InternalRequest* req = res->request;
+ return req->options.bodyReader(buffer, size * numItems, req->options.bodyReaderData);
+}
+
+static size_t writeCallback(char* ptr, size_t size, size_t numItems, void* userData) {
+ InternalResponse* res = (InternalResponse*)userData;
+ InternalRequest* req = res->request;
+ size_t bytesWritten = req->options.bodyWriter(ptr, size * numItems, req->options.bodyWriterData);
+ res->totalBytesRead += bytesWritten;
+ return bytesWritten;
+}
+
+#define METHOD(A, B, C) (((A) << 16) | ((B) << 8) | (C))
+
+static void setupMethod(CURL* curl, const char* method) {
+ if (strlen(method) < 3) {
+ return;
+ }
+
+ int methodCode = (method[0] << 16) | (method[1] << 8) | method[2];
+
+ switch (methodCode) {
+ case METHOD('G', 'E', 'T'):
+ case METHOD('C', 'O', 'N'):
+ case METHOD('O', 'P', 'T'):
+ curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
+ break;
+ case METHOD('P', 'O', 'S'):
+ case METHOD('P', 'A', 'T'):
+ case METHOD('D', 'E', 'L'):
+ curl_easy_setopt(curl, CURLOPT_POST, 1);
+ break;
+ case METHOD('P', 'U', 'T'):
+ curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
+ break;
+ case METHOD('H', 'E', 'A'):
+ case METHOD('T', 'R', 'A'):
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1);
+ break;
+ }
+
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
+}
+
+static size_t headerCallback(char* buffer, size_t size, size_t nitems, void* userData) {
+ InternalResponse* res = (InternalResponse*) userData;
+ size_t headerSize = size * nitems;
+
+ char* headerName = strndup(buffer, headerSize);
+ char* split = strchr(headerName, ':');
+ if (split) {
+ *split = 0;
+ split++;
+ while (*split == ' ') {
+ split++;
+ }
+ char* headerValue = strdup(split);
+
+ char* cr = strchr(headerValue, 13);
+ if (cr) {
+ *cr = 0;
+ }
+
+ char* lf = strchr(headerValue, 10);
+ if (lf) {
+ *lf = 0;
+ }
+
+ naettAlloc(KVLink, node);
+ node->next = res->headers;
+ node->key = headerName;
+ node->value = headerValue;
+ res->headers = node;
+ }
+
+ return headerSize;
+}
+
+void naettPlatformMakeRequest(InternalResponse* res) {
+ InternalRequest* req = res->request;
+
+ CURL* c = curl_easy_init();
+ curl_easy_setopt(c, CURLOPT_URL, req->url);
+ curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, req->options.timeoutMS);
+
+ curl_easy_setopt(c, CURLOPT_READFUNCTION, readCallback);
+ curl_easy_setopt(c, CURLOPT_READDATA, res);
+
+ curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, writeCallback);
+ curl_easy_setopt(c, CURLOPT_WRITEDATA, res);
+
+ curl_easy_setopt(c, CURLOPT_HEADERFUNCTION, headerCallback);
+ curl_easy_setopt(c, CURLOPT_HEADERDATA, res);
+
+ curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1);
+
+ int bodySize = res->request->options.bodyReader(NULL, 0, res->request->options.bodyReaderData);
+ curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, bodySize);
+
+ setupMethod(c, req->options.method);
+
+ struct curl_slist* headerList = NULL;
+ char uaBuf[512];
+ snprintf(uaBuf, sizeof(uaBuf), "User-Agent: %s", req->options.userAgent ? req->options.userAgent : NAETT_UA);
+ headerList = curl_slist_append(headerList, uaBuf);
+
+ KVLink* header = req->options.headers;
+ size_t bufferSize = 0;
+ char* buffer = NULL;
+ while (header) {
+ size_t headerLength = strlen(header->key) + strlen(header->value) + 1 + 1; // colon + null
+ if (headerLength > bufferSize) {
+ bufferSize = headerLength;
+ buffer = (char*)realloc(buffer, bufferSize);
+ }
+ snprintf(buffer, bufferSize, "%s:%s", header->key, header->value);
+ headerList = curl_slist_append(headerList, buffer);
+ header = header->next;
+ }
+ curl_easy_setopt(c, CURLOPT_HTTPHEADER, headerList);
+ free(buffer);
+ res->headerList = headerList;
+
+ curl_easy_setopt(c, CURLOPT_PRIVATE, res);
+
+ write(handleWriteFD, &c, sizeof(c));
+}
+
+void naettPlatformFreeRequest(InternalRequest* req) {
+}
+
+void naettPlatformCloseResponse(InternalResponse* res) {
+ curl_slist_free_all(res->headerList);
+}
+
+#endif
diff --git a/ext/naett-lib/src/naett_objc.h b/ext/naett-lib/src/naett_objc.h
new file mode 100644
index 0000000000..da95a02a52
--- /dev/null
+++ b/ext/naett-lib/src/naett_objc.h
@@ -0,0 +1,65 @@
+#ifndef NAETT_OBJC_H
+#define NAETT_OBJC_H
+
+#if defined(__IOS__) || defined (__MACOS__)
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#if defined(__OBJC__) && __has_feature(objc_arc)
+#error "ARC is not supported"
+#endif
+
+// ABI is a bit different between platforms
+#ifdef __arm64__
+#define abi_objc_msgSend_stret objc_msgSend
+#else
+#define abi_objc_msgSend_stret objc_msgSend_stret
+#endif
+#ifdef __i386__
+#define abi_objc_msgSend_fpret objc_msgSend_fpret
+#else
+#define abi_objc_msgSend_fpret objc_msgSend
+#endif
+
+#define objc_msgSendSuper_t(RET, ...) ((RET(*)(struct objc_super*, SEL, ##__VA_ARGS__))objc_msgSendSuper)
+#define objc_msgSend_t(RET, ...) ((RET(*)(id, SEL, ##__VA_ARGS__))objc_msgSend)
+#define objc_msgSend_stret_t(RET, ...) ((RET(*)(id, SEL, ##__VA_ARGS__))abi_objc_msgSend_stret)
+#define objc_msgSend_id objc_msgSend_t(id)
+#define objc_msgSend_void objc_msgSend_t(void)
+#define objc_msgSend_void_id objc_msgSend_t(void, id)
+#define objc_msgSend_void_bool objc_msgSend_t(void, bool)
+
+#define sel(NAME) sel_registerName(NAME)
+#define class(NAME) ((id)objc_getClass(NAME))
+#define makeClass(NAME, SUPER) \
+ objc_allocateClassPair((Class)objc_getClass(SUPER), NAME, 0)
+
+// Check here to get the signature right:
+// https://nshipster.com/type-encodings/
+// https://ko9.org/posts/encode-types/
+#define addMethod(CLASS, NAME, IMPL, SIGNATURE) \
+ if (!class_addMethod(CLASS, sel(NAME), (IMP) (IMPL), (SIGNATURE))) assert(false)
+
+#define addIvar(CLASS, NAME, SIZE, SIGNATURE) \
+ if (!class_addIvar(CLASS, NAME, SIZE, rint(log2(SIZE)), SIGNATURE)) assert(false)
+
+#define objc_alloc(CLASS) objc_msgSend_id(class(CLASS), sel("alloc"))
+#define autorelease(OBJ) objc_msgSend_void(OBJ, sel("autorelease"))
+#define retain(OBJ) objc_msgSend_void(OBJ, sel("retain"))
+#define release(OBJ) objc_msgSend_void(OBJ, sel("release"))
+
+#if __LP64__ || NS_BUILD_32_LIKE_64
+#define NSIntegerEncoding "q"
+#define NSUIntegerEncoding "L"
+#else
+#define NSIntegerEncoding "i"
+#define NSUIntegerEncoding "I"
+#endif
+
+#endif // defined(__IOS__) || defined (__MACOS__)
+#endif // NAETT_OBJC_H
diff --git a/ext/naett-lib/src/naett_osx.c b/ext/naett-lib/src/naett_osx.c
new file mode 100644
index 0000000000..68c70992f9
--- /dev/null
+++ b/ext/naett-lib/src/naett_osx.c
@@ -0,0 +1,200 @@
+#include "naett_internal.h"
+
+#ifdef __APPLE__
+
+#include "naett_objc.h"
+#include
+#include
+#include
+
+#ifdef DEBUG
+static void _showPools(const char* context) {
+ fprintf(stderr, "NSAutoreleasePool@%s:\n", context);
+ objc_msgSend_void(class("NSAutoreleasePool"), sel("showPools"));
+}
+#define showPools(x) _showPools((x))
+#else
+#define showPools(x)
+#endif
+
+static id pool(void) {
+ return objc_msgSend_id(objc_alloc("NSAutoreleasePool"), sel("init"));
+}
+
+static id sessionConfiguration = nil;
+
+void naettPlatformInit(naettInitData initData) {
+ id NSThread = class("NSThread");
+ SEL isMultiThreaded = sel("isMultiThreaded");
+
+ if (!objc_msgSend_t(bool)(NSThread, isMultiThreaded)) {
+ // Make a dummy call from a new thread to kick Cocoa into multi-threaded mode
+ objc_msgSend_t(void, SEL, id, id)(
+ NSThread, sel("detachNewThreadSelector:toTarget:withObject:"), isMultiThreaded, NSThread, nil);
+ }
+
+ sessionConfiguration = objc_msgSend_id(class("NSURLSessionConfiguration"), sel("ephemeralSessionConfiguration"));
+ retain(sessionConfiguration);
+}
+
+id NSString(const char* string) {
+ return objc_msgSend_t(id, const char*)(class("NSString"), sel("stringWithUTF8String:"), string);
+}
+
+int naettPlatformInitRequest(InternalRequest* req) {
+ id p = pool();
+
+ id urlString = NSString(req->url);
+ id url = objc_msgSend_t(id, id)(class("NSURL"), sel("URLWithString:"), urlString);
+
+ id request = objc_msgSend_t(id, id)(class("NSMutableURLRequest"), sel("requestWithURL:"), url);
+
+ objc_msgSend_t(void, double)(request, sel("setTimeoutInterval:"), (double)(req->options.timeoutMS) / 1000.0);
+ id methodString = NSString(req->options.method);
+ objc_msgSend_t(void, id)(request, sel("setHTTPMethod:"), methodString);
+
+ {
+ id name = NSString("User-Agent");
+ id value = NSString(req->options.userAgent ? req->options.userAgent : NAETT_UA);
+ objc_msgSend_t(void, id, id)(request, sel("setValue:forHTTPHeaderField:"), value, name);
+ }
+
+ KVLink* header = req->options.headers;
+ while (header != NULL) {
+ id name = NSString(header->key);
+ id value = NSString(header->value);
+ objc_msgSend_t(void, id, id)(request, sel("setValue:forHTTPHeaderField:"), value, name);
+ header = header->next;
+ }
+
+ char byteBuffer[10240];
+ int bytesRead = 0;
+
+ if (req->options.bodyReader != NULL) {
+ id bodyData =
+ objc_msgSend_t(id, NSUInteger)(class("NSMutableData"), sel("dataWithCapacity:"), sizeof(byteBuffer));
+
+ int totalBytesRead = 0;
+ do {
+ bytesRead = req->options.bodyReader(byteBuffer, sizeof(byteBuffer), req->options.bodyReaderData);
+ totalBytesRead += bytesRead;
+ objc_msgSend_t(void, const void*, NSUInteger)(bodyData, sel("appendBytes:length:"), byteBuffer, bytesRead);
+ } while (bytesRead > 0);
+
+ if (totalBytesRead > 0) {
+ objc_msgSend_t(void, id)(request, sel("setHTTPBody:"), bodyData);
+ }
+ }
+
+ retain(request);
+ req->urlRequest = request;
+
+ release(p);
+ return 1;
+}
+
+void didReceiveData(id self, SEL _sel, id session, id dataTask, id data) {
+ InternalResponse* res = NULL;
+ id p = pool();
+
+ object_getInstanceVariable(self, "response", (void**)&res);
+
+ if (res->headers == NULL) {
+ id response = objc_msgSend_t(id)(dataTask, sel("response"));
+ res->code = objc_msgSend_t(NSInteger)(response, sel("statusCode"));
+ id allHeaders = objc_msgSend_t(id)(response, sel("allHeaderFields"));
+
+ NSUInteger headerCount = objc_msgSend_t(NSUInteger)(allHeaders, sel("count"));
+ id headerNames[headerCount];
+ id headerValues[headerCount];
+
+ objc_msgSend_t(NSInteger, id*, id*, NSUInteger)(
+ allHeaders, sel("getObjects:andKeys:count:"), headerValues, headerNames, headerCount);
+ KVLink* firstHeader = NULL;
+ for (int i = 0; i < headerCount; i++) {
+ naettAlloc(KVLink, node);
+ node->key = strdup(objc_msgSend_t(const char*)(headerNames[i], sel("UTF8String")));
+ node->value = strdup(objc_msgSend_t(const char*)(headerValues[i], sel("UTF8String")));
+ node->next = firstHeader;
+ firstHeader = node;
+ }
+ res->headers = firstHeader;
+
+ const char* contentLength = naettGetHeader((naettRes*)res, "Content-Length");
+ if (!contentLength || sscanf(contentLength, "%d", &res->contentLength) != 1) {
+ res->contentLength = -1;
+ }
+ }
+
+ const void* bytes = objc_msgSend_t(const void*)(data, sel("bytes"));
+ NSUInteger length = objc_msgSend_t(NSUInteger)(data, sel("length"));
+
+ res->request->options.bodyWriter(bytes, length, res->request->options.bodyWriterData);
+ res->totalBytesRead += (int)length;
+
+ release(p);
+}
+
+static void didComplete(id self, SEL _sel, id session, id dataTask, id error) {
+ InternalResponse* res = NULL;
+ object_getInstanceVariable(self, "response", (void**)&res);
+ if (res != NULL) {
+ if (error != nil) {
+ res->code = naettConnectionError;
+ }
+ res->complete = 1;
+ }
+}
+
+static id createDelegate(void) {
+ static Class TaskDelegateClass = nil;
+
+ if (!TaskDelegateClass) {
+ TaskDelegateClass = objc_allocateClassPair((Class)objc_getClass("NSObject"), "naettTaskDelegate", 0);
+ class_addProtocol(TaskDelegateClass, (Protocol* _Nonnull)objc_getProtocol("NSURLSessionDataDelegate"));
+
+ addMethod(TaskDelegateClass, "URLSession:dataTask:didReceiveData:", didReceiveData, "v@:@@@");
+ addMethod(TaskDelegateClass, "URLSession:task:didCompleteWithError:", didComplete, "v@:@@@");
+ addIvar(TaskDelegateClass, "response", sizeof(void*), "^v");
+ }
+
+ id delegate = objc_msgSend_id((id)TaskDelegateClass, sel("alloc"));
+ delegate = objc_msgSend_id(delegate, sel("init"));
+
+ return delegate;
+}
+
+void naettPlatformMakeRequest(InternalResponse* res) {
+ InternalRequest* req = res->request;
+ id p = pool();
+
+ id delegate = createDelegate();
+ // delegate will be retained by session below
+ autorelease(delegate);
+ object_setInstanceVariable(delegate, "response", (void*)res);
+
+ id session = objc_msgSend_t(id, id, id, id)(class("NSURLSession"),
+ sel("sessionWithConfiguration:delegate:delegateQueue:"),
+ sessionConfiguration,
+ delegate,
+ nil);
+
+ res->session = session;
+
+ id task = objc_msgSend_t(id, id)(session, sel("dataTaskWithRequest:"), req->urlRequest);
+ objc_msgSend_void(task, sel("resume"));
+
+ release(p);
+}
+
+void naettPlatformFreeRequest(InternalRequest* req) {
+ release(req->urlRequest);
+ req->urlRequest = nil;
+}
+
+void naettPlatformCloseResponse(InternalResponse* res) {
+ objc_msgSend_void(res->session, sel("invalidateAndCancel"));
+ res->session = nil;
+}
+
+#endif // __APPLE__
diff --git a/ext/naett-lib/src/naett_win.c b/ext/naett-lib/src/naett_win.c
new file mode 100644
index 0000000000..b75c6874a8
--- /dev/null
+++ b/ext/naett-lib/src/naett_win.c
@@ -0,0 +1,331 @@
+#include "naett_internal.h"
+
+#ifdef __WINDOWS__
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+void naettPlatformInit(naettInitData initData) {
+}
+
+static char* winToUTF8(LPWSTR source) {
+ int length = WideCharToMultiByte(CP_UTF8, 0, source, -1, NULL, 0, NULL, NULL);
+ char* chars = (char*)malloc(length);
+ int result = WideCharToMultiByte(CP_UTF8, 0, source, -1, chars, length, NULL, NULL);
+ if (!result) {
+ free(chars);
+ return NULL;
+ }
+ return chars;
+}
+
+static LPWSTR winFromUTF8(const char* source) {
+ int length = MultiByteToWideChar(CP_UTF8, 0, source, -1, NULL, 0);
+ LPWSTR chars = (LPWSTR)malloc(length * sizeof(WCHAR));
+ int result = MultiByteToWideChar(CP_UTF8, 0, source, -1, chars, length);
+ if (!result) {
+ free(chars);
+ return NULL;
+ }
+ return chars;
+}
+
+#define ASPRINTF(result, fmt, ...) \
+ { \
+ size_t len = snprintf(NULL, 0, fmt, __VA_ARGS__); \
+ *(result) = (char*)malloc(len + 1); \
+ snprintf(*(result), len + 1, fmt, __VA_ARGS__); \
+ }
+
+static LPWSTR wcsndup(LPCWSTR str, size_t len) {
+ LPWSTR result = calloc(1, sizeof(WCHAR) * (len + 1));
+ wcsncpy(result, str, len);
+ return result;
+}
+
+static LPCWSTR packHeaders(InternalRequest* req) {
+ char* packed = strdup("");
+
+ KVLink* node = req->options.headers;
+ while (node != NULL) {
+ char* update;
+ ASPRINTF(&update, "%s%s:%s%s", packed, node->key, node->value, node->next ? "\r\n" : "");
+ free(packed);
+ packed = update;
+ node = node->next;
+ }
+
+ LPCWSTR winHeaders = winFromUTF8(packed);
+ free(packed);
+ return winHeaders;
+}
+
+static void unpackHeaders(InternalResponse* res, LPWSTR packed) {
+ size_t len = 0;
+ KVLink* firstHeader = NULL;
+ while ((len = wcslen(packed)) != 0) {
+ char* header = winToUTF8(packed);
+ char* split = strchr(header, ':');
+ if (split) {
+ *split = 0;
+ split++;
+ while (*split == ' ') {
+ split++;
+ }
+ naettAlloc(KVLink, node);
+ node->key = strdup(header);
+ node->value = strdup(split);
+ node->next = firstHeader;
+ firstHeader = node;
+ }
+ free(header);
+ packed += len + 1;
+ }
+ res->headers = firstHeader;
+}
+
+static void CALLBACK
+callback(HINTERNET request, DWORD_PTR context, DWORD status, LPVOID statusInformation, DWORD statusInfoLength) {
+ InternalResponse* res = (InternalResponse*)context;
+
+ switch (status) {
+ case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: {
+ DWORD bufSize = 0;
+ WinHttpQueryHeaders(request,
+ WINHTTP_QUERY_RAW_HEADERS,
+ WINHTTP_HEADER_NAME_BY_INDEX,
+ NULL,
+ &bufSize,
+ WINHTTP_NO_HEADER_INDEX);
+ LPWSTR buffer = (LPWSTR)malloc(bufSize);
+ WinHttpQueryHeaders(request,
+ WINHTTP_QUERY_RAW_HEADERS,
+ WINHTTP_HEADER_NAME_BY_INDEX,
+ buffer,
+ &bufSize,
+ WINHTTP_NO_HEADER_INDEX);
+ unpackHeaders(res, buffer);
+ free(buffer);
+
+ const char* contentLength = naettGetHeader((naettRes*)res, "Content-Length");
+ if (!contentLength || sscanf(contentLength, "%d", &res->contentLength) != 1) {
+ res->contentLength = -1;
+ }
+
+ DWORD statusCode = 0;
+ DWORD statusCodeSize = sizeof(statusCode);
+
+ WinHttpQueryHeaders(request,
+ WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
+ WINHTTP_HEADER_NAME_BY_INDEX,
+ &statusCode,
+ &statusCodeSize,
+ WINHTTP_NO_HEADER_INDEX);
+ res->code = statusCode;
+
+ if (!WinHttpQueryDataAvailable(request, NULL)) {
+ res->code = naettProtocolError;
+ res->complete = 1;
+ }
+ } break;
+
+ case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: {
+ DWORD* available = (DWORD*)statusInformation;
+ res->bytesLeft = *available;
+ if (res->bytesLeft == 0) {
+ res->complete = 1;
+ break;
+ }
+
+ size_t bytesToRead = min(res->bytesLeft, sizeof(res->buffer));
+ if (!WinHttpReadData(request, res->buffer, (DWORD)bytesToRead, NULL)) {
+ res->code = naettReadError;
+ res->complete = 1;
+ }
+ } break;
+
+ case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: {
+ size_t bytesRead = statusInfoLength;
+
+ InternalRequest* req = res->request;
+ if (req->options.bodyWriter(res->buffer, (int)bytesRead, req->options.bodyWriterData) != bytesRead) {
+ res->code = naettReadError;
+ res->complete = 1;
+ }
+ res->totalBytesRead += (int)bytesRead;
+ res->bytesLeft -= bytesRead;
+ if (res->bytesLeft > 0) {
+ size_t bytesToRead = min(res->bytesLeft, sizeof(res->buffer));
+ if (!WinHttpReadData(request, res->buffer, (DWORD)bytesToRead, NULL)) {
+ res->code = naettReadError;
+ res->complete = 1;
+ }
+ } else {
+ if (!WinHttpQueryDataAvailable(request, NULL)) {
+ res->code = naettProtocolError;
+ res->complete = 1;
+ }
+ }
+ } break;
+
+ case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
+ case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: {
+ int bytesRead = res->request->options.bodyReader(
+ res->buffer, sizeof(res->buffer), res->request->options.bodyReaderData);
+ if (bytesRead) {
+ WinHttpWriteData(request, res->buffer, bytesRead, NULL);
+ } else {
+ if (!WinHttpReceiveResponse(request, NULL)) {
+ res->code = naettReadError;
+ res->complete = 1;
+ }
+ }
+ } break;
+
+ //
+ case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: {
+ WINHTTP_ASYNC_RESULT* result = (WINHTTP_ASYNC_RESULT*)statusInformation;
+ switch (result->dwResult) {
+ case API_RECEIVE_RESPONSE:
+ case API_QUERY_DATA_AVAILABLE:
+ case API_READ_DATA:
+ res->code = naettReadError;
+ break;
+ case API_WRITE_DATA:
+ res->code = naettWriteError;
+ break;
+ case API_SEND_REQUEST:
+ res->code = naettConnectionError;
+ break;
+ default:
+ res->code = naettGenericError;
+ }
+
+ res->complete = 1;
+ } break;
+ }
+}
+
+int naettPlatformInitRequest(InternalRequest* req) {
+ LPWSTR url = winFromUTF8(req->url);
+
+ URL_COMPONENTS components;
+ ZeroMemory(&components, sizeof(components));
+ components.dwStructSize = sizeof(components);
+ components.dwSchemeLength = (DWORD)-1;
+ components.dwHostNameLength = (DWORD)-1;
+ components.dwUrlPathLength = (DWORD)-1;
+ components.dwExtraInfoLength = (DWORD)-1;
+ BOOL cracked = WinHttpCrackUrl(url, 0, 0, &components);
+
+ if (!cracked) {
+ free(url);
+ return 0;
+ }
+
+ req->host = wcsndup(components.lpszHostName, components.dwHostNameLength);
+ req->resource = wcsndup(components.lpszUrlPath, components.dwUrlPathLength + components.dwExtraInfoLength);
+ free(url);
+
+ LPWSTR uaBuf = winFromUTF8(req->options.userAgent ? req->options.userAgent : NAETT_UA);
+ req->session = WinHttpOpen(uaBuf,
+ WINHTTP_ACCESS_TYPE_NO_PROXY,
+ WINHTTP_NO_PROXY_NAME,
+ WINHTTP_NO_PROXY_BYPASS,
+ WINHTTP_FLAG_ASYNC);
+ free(uaBuf);
+
+ if (!req->session) {
+ return 0;
+ }
+
+ WinHttpSetStatusCallback(req->session, callback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0);
+
+ // Set the connect timeout. Leave the other three timeouts at their default values.
+ WinHttpSetTimeouts(req->session, 0, req->options.timeoutMS, 30000, 30000);
+
+ req->connection = WinHttpConnect(req->session, req->host, components.nPort, 0);
+ if (!req->connection) {
+ naettPlatformFreeRequest(req);
+ return 0;
+ }
+
+ LPWSTR verb = winFromUTF8(req->options.method);
+ req->request = WinHttpOpenRequest(req->connection,
+ verb,
+ req->resource,
+ NULL,
+ WINHTTP_NO_REFERER,
+ WINHTTP_DEFAULT_ACCEPT_TYPES,
+ components.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0);
+ free(verb);
+ if (!req->request) {
+ naettPlatformFreeRequest(req);
+ return 0;
+ }
+
+ LPCWSTR headers = packHeaders(req);
+ if (headers[0] != 0) {
+ if (!WinHttpAddRequestHeaders(
+ req->request, headers, -1, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) {
+ naettPlatformFreeRequest(req);
+ free((LPWSTR)headers);
+ return 0;
+ }
+ }
+ free((LPWSTR)headers);
+
+ return 1;
+}
+
+void naettPlatformMakeRequest(InternalResponse* res) {
+ InternalRequest* req = res->request;
+
+ LPCWSTR extraHeaders = WINHTTP_NO_ADDITIONAL_HEADERS;
+ WCHAR contentLengthHeader[64];
+
+ int contentLength = req->options.bodyReader(NULL, 0, req->options.bodyReaderData);
+ if (contentLength > 0) {
+ swprintf(contentLengthHeader, 64, L"Content-Length: %d", contentLength);
+ extraHeaders = contentLengthHeader;
+ }
+
+ if (!WinHttpSendRequest(req->request, extraHeaders, -1, NULL, 0, 0, (DWORD_PTR)res)) {
+ res->code = naettConnectionError;
+ res->complete = 1;
+ }
+}
+
+void naettPlatformFreeRequest(InternalRequest* req) {
+ assert(req != NULL);
+
+ if (req->request != NULL) {
+ WinHttpCloseHandle(req->request);
+ req->request = NULL;
+ }
+ if (req->connection != NULL) {
+ WinHttpCloseHandle(req->connection);
+ req->connection = NULL;
+ }
+ if (req->session != NULL) {
+ WinHttpCloseHandle(req->session);
+ req->session = NULL;
+ }
+ if (req->host != NULL) {
+ free(req->host);
+ req->host = NULL;
+ }
+ if (req->resource != NULL) {
+ free(req->resource);
+ req->resource = NULL;
+ }
+}
+
+void naettPlatformCloseResponse(InternalResponse* res) {
+}
+
+#endif // __WINDOWS__
diff --git a/libcurl4-openssl-dev_8.20.0-2ubuntu1_arm64.deb b/libcurl4-openssl-dev_8.20.0-2ubuntu1_arm64.deb
new file mode 100644
index 0000000000..dc31cc5443
Binary files /dev/null and b/libcurl4-openssl-dev_8.20.0-2ubuntu1_arm64.deb differ