mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
skin studio updates, save sync CLOSES #1533
This commit is contained in:
@@ -67,6 +67,124 @@ public final class GRPickerBridge: NSObject {
|
||||
return succeeded
|
||||
}
|
||||
|
||||
// MARK: - General HTTP request (love.system.httpRequest)
|
||||
|
||||
private static let httpMaxResponse = 4 * 1024 * 1024
|
||||
|
||||
// URLSession turns a 301/302/303 POST into a GET on its own. Save sync
|
||||
// signs a method and a body, so every hop re-sends the original request
|
||||
// against the new URL instead, and only over https.
|
||||
private final class GRRedirectKeeper: NSObject, URLSessionTaskDelegate {
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask,
|
||||
willPerformHTTPRedirection response: HTTPURLResponse,
|
||||
newRequest request: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
guard let original = task.originalRequest,
|
||||
let target = request.url,
|
||||
target.scheme?.lowercased() == "https" else {
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
var next = original
|
||||
next.url = target
|
||||
completionHandler(next)
|
||||
}
|
||||
}
|
||||
|
||||
private static let httpSession = URLSession(configuration: .ephemeral,
|
||||
delegate: GRRedirectKeeper(),
|
||||
delegateQueue: nil)
|
||||
|
||||
private static func httpEnvelope(_ head: String, _ payload: Data?) -> NSData {
|
||||
var out = Data((head + "\n").utf8)
|
||||
if let payload { out.append(payload) }
|
||||
return out as NSData
|
||||
}
|
||||
|
||||
private static func httpErrorText(_ error: Error) -> String {
|
||||
var text = error.localizedDescription
|
||||
.replacingOccurrences(of: "\r", with: " ")
|
||||
.replacingOccurrences(of: "\n", with: " ")
|
||||
if text.isEmpty { text = "the request failed" }
|
||||
if text.count > 160 { text = String(text.prefix(160)) }
|
||||
return text
|
||||
}
|
||||
|
||||
/// Blocking HTTPS request with a chosen method, headers and byte body, the
|
||||
/// iOS half of love.system.httpRequest (see the Android GameActivity one).
|
||||
/// Headers arrive as "name: value" lines joined by newlines. The reply is
|
||||
/// an envelope: a head line of "STATUS <code>" or "ERROR <text>", a
|
||||
/// newline, then the raw response bytes -- read for 4xx and 5xx as well,
|
||||
/// because a sync conflict answers 409 with the save that won.
|
||||
@objc(httpRequestWithUrl:method:headers:body:bodyLength:userAgent:)
|
||||
public static func httpRequest(url: UnsafePointer<CChar>?,
|
||||
method: UnsafePointer<CChar>?,
|
||||
headers: UnsafePointer<CChar>?,
|
||||
body: UnsafePointer<UInt8>?,
|
||||
bodyLength: Int32,
|
||||
userAgent: UnsafePointer<CChar>?) -> NSData? {
|
||||
guard let url, let requestURL = URL(string: String(cString: url)) else {
|
||||
return httpEnvelope("ERROR missing url", nil)
|
||||
}
|
||||
guard requestURL.scheme?.lowercased() == "https" else {
|
||||
return httpEnvelope("ERROR https only", nil)
|
||||
}
|
||||
let verb = (method.map { String(cString: $0) } ?? "GET").uppercased()
|
||||
guard ["GET", "POST", "PUT", "DELETE"].contains(verb) else {
|
||||
return httpEnvelope("ERROR unsupported request method", nil)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: requestURL)
|
||||
request.httpMethod = verb
|
||||
request.timeoutInterval = 60
|
||||
request.setValue(userAgent.map { String(cString: $0) } ?? "gen1recomp",
|
||||
forHTTPHeaderField: "User-Agent")
|
||||
if let headers, headers.pointee != 0 {
|
||||
for line in String(cString: headers).split(separator: "\n") {
|
||||
guard let colon = line.firstIndex(of: ":") else {
|
||||
return httpEnvelope("ERROR bad request header", nil)
|
||||
}
|
||||
let name = line[line.startIndex..<colon]
|
||||
.trimmingCharacters(in: .whitespaces)
|
||||
let value = line[line.index(after: colon)...]
|
||||
.trimmingCharacters(in: .whitespaces)
|
||||
if name.isEmpty {
|
||||
return httpEnvelope("ERROR bad request header", nil)
|
||||
}
|
||||
request.setValue(value, forHTTPHeaderField: name)
|
||||
}
|
||||
}
|
||||
if verb != "GET", let body, bodyLength > 0 {
|
||||
request.httpBody = Data(bytes: body, count: Int(bodyLength))
|
||||
}
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var envelope = httpEnvelope("ERROR no response", nil)
|
||||
let task = httpSession.dataTask(with: request) { data, response, error in
|
||||
defer { semaphore.signal() }
|
||||
if let error {
|
||||
envelope = httpEnvelope("ERROR " + httpErrorText(error), nil)
|
||||
return
|
||||
}
|
||||
guard let http = response as? HTTPURLResponse else {
|
||||
envelope = httpEnvelope("ERROR no response", nil)
|
||||
return
|
||||
}
|
||||
let payload = data ?? Data()
|
||||
if payload.count > httpMaxResponse {
|
||||
envelope = httpEnvelope("ERROR the reply was too large", nil)
|
||||
return
|
||||
}
|
||||
envelope = httpEnvelope("STATUS \(http.statusCode)", payload)
|
||||
}
|
||||
task.resume()
|
||||
guard semaphore.wait(timeout: .now() + 65) == .success else {
|
||||
task.cancel()
|
||||
return httpEnvelope("ERROR the request timed out", nil)
|
||||
}
|
||||
return envelope
|
||||
}
|
||||
|
||||
// MARK: - Entry points called from liblove (C strings on purpose)
|
||||
|
||||
@objc(presentPickerWithKind:saveDir:)
|
||||
|
||||
@@ -9,7 +9,8 @@ What it does:
|
||||
1. Copies mobile/ios/native/ (GRPickerBridge.swift, GRHealthBridge.swift,
|
||||
GRBootstrap.m) and the HealthKit entitlements into the LÖVE tree.
|
||||
2. Patches liblove's wrap_System.cpp to expose love.system.pickFile,
|
||||
love.system.createFile, and love.system.syncHealthSteps on iOS (each
|
||||
love.system.createFile, love.system.syncHealthSteps,
|
||||
love.system.httpDownload and love.system.httpRequest on iOS (each
|
||||
calls a GR*Bridge Swift class through the Objective-C runtime, so
|
||||
liblove never links against Swift directly).
|
||||
3. Patches love.xcodeproj so the love-ios app target compiles the native
|
||||
@@ -50,6 +51,7 @@ WRAP_INCLUDES = """
|
||||
#include <objc/runtime.h>
|
||||
#include <objc/message.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "filesystem/Filesystem.h"
|
||||
#endif
|
||||
""" % MARKER
|
||||
@@ -159,6 +161,7 @@ WRAP_REGISTRATION = """#ifdef LOVE_IOS
|
||||
{ "createFile", w_createFile },
|
||||
{ "syncHealthSteps", w_syncHealthSteps },
|
||||
{ "httpDownload", w_httpDownload },
|
||||
{ "httpRequest", w_httpRequest },
|
||||
#endif
|
||||
"""
|
||||
|
||||
@@ -201,6 +204,7 @@ int w_syncHealthSteps(lua_State *L)
|
||||
WRAP_SYNC_REGISTRATION = """#ifdef LOVE_IOS
|
||||
{ "syncHealthSteps", w_syncHealthSteps },
|
||||
{ "httpDownload", w_httpDownload },
|
||||
{ "httpRequest", w_httpRequest },
|
||||
#endif
|
||||
"""
|
||||
|
||||
@@ -226,6 +230,80 @@ int w_httpDownload(lua_State *L)
|
||||
lua_pushboolean(L, ok != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// love.system.httpRequest(url, method, headers, body, userAgent) -> envelope
|
||||
//
|
||||
// The transport save sync needs: a chosen method, per-request auth headers,
|
||||
// and the response body of a 4xx as well as a 2xx. `headers` is a flat array
|
||||
// of alternating name and value strings, joined into "name: value" lines here
|
||||
// because the Swift bridge takes C strings and no Foundation type may be
|
||||
// NAMED in this translation unit (see w_pickFileKinds above).
|
||||
//
|
||||
// The single return is the response envelope -- a head line of
|
||||
// "STATUS <code>" or "ERROR <text>", a newline, then the raw body -- or nil
|
||||
// where the build carries no bridge at all, which src/core/HostShell.lua
|
||||
// turns into an "update the app" notice rather than a failed request.
|
||||
int w_httpRequest(lua_State *L)
|
||||
{
|
||||
const char *url = luaL_checkstring(L, 1);
|
||||
const char *method = luaL_optstring(L, 2, "GET");
|
||||
|
||||
std::string headerBlob;
|
||||
if (!lua_isnoneornil(L, 3))
|
||||
{
|
||||
luaL_checktype(L, 3, LUA_TTABLE);
|
||||
std::vector<std::string> fields;
|
||||
size_t count = luax_objlen(L, 3);
|
||||
for (size_t i = 1; i <= count; i++)
|
||||
{
|
||||
lua_rawgeti(L, 3, (int) i);
|
||||
const char *field = lua_tostring(L, -1);
|
||||
fields.push_back(field != nullptr ? field : "");
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
for (size_t i = 0; i + 1 < fields.size(); i += 2)
|
||||
headerBlob += fields[i] + ": " + fields[i + 1] + "\\n";
|
||||
}
|
||||
|
||||
size_t bodyLen = 0;
|
||||
const char *body = nullptr;
|
||||
if (!lua_isnoneornil(L, 4))
|
||||
body = luaL_checklstring(L, 4, &bodyLen);
|
||||
const char *ua = luaL_optstring(L, 5, "gen1recomp");
|
||||
|
||||
Class cls = objc_getClass("GRPickerBridge");
|
||||
if (cls == nullptr)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
typedef id (*GRRequest)(Class, SEL, const char *, const char *,
|
||||
const char *, const unsigned char *, int,
|
||||
const char *);
|
||||
id reply = ((GRRequest)objc_msgSend)(
|
||||
cls,
|
||||
sel_registerName("httpRequestWithUrl:method:headers:body:bodyLength:userAgent:"),
|
||||
url, method, headerBlob.c_str(), (const unsigned char *) body,
|
||||
(int) bodyLen, ua);
|
||||
if (reply == nullptr)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
// NSData read through the runtime, for the same reason as above: the
|
||||
// bytes are copied out immediately, before any autorelease pool drains.
|
||||
typedef const void *(*GRBytes)(id, SEL);
|
||||
typedef unsigned long (*GRLength)(id, SEL);
|
||||
const void *bytes = ((GRBytes)objc_msgSend)(reply, sel_registerName("bytes"));
|
||||
unsigned long length = ((GRLength)objc_msgSend)(reply, sel_registerName("length"));
|
||||
if (bytes == nullptr || length == 0)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
lua_pushlstring(L, (const char *) bytes, (size_t) length);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
"""
|
||||
|
||||
@@ -308,7 +386,7 @@ def patch_wrap_system():
|
||||
text = text.replace(reg_anchor, reg_anchor + registration, 1)
|
||||
WRAP_SYSTEM.write_text(text)
|
||||
print("patch_love_src: wrap_System.cpp patched "
|
||||
"(pickFile/createFile/syncHealthSteps/httpDownload)")
|
||||
"(pickFile/createFile/syncHealthSteps/httpDownload/httpRequest)")
|
||||
|
||||
|
||||
def patch_public_documents():
|
||||
|
||||
Reference in New Issue
Block a user