flatten 20260225

This commit is contained in:
Timothy Prepscius
2026-02-25 12:39:24 -05:00
commit fa54be052a
315 changed files with 49791 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
// TJP COPYRIGHT HEADER
#include <tjp/core/header_only/compile.h>
#include "../Internet.hpp"
#include "../Internet+Internal.hpp"
namespace tjp::core::internet {
void perform (const WeakPtr<internal::Request> &request);
} // namespace
#ifdef TJP_CORE_HEADER_ONLY
#ifdef __OBJC__
#include "Internet.mm"
#endif
#endif

614
tjp/core/internet/ios/Internet.mm Executable file
View File

@@ -0,0 +1,614 @@
#include <tjp/core/header_only/compile.h>
#ifdef TJP_CORE_HEADER_ONLY
#pragma once
#endif
#pragma clang diagnostic ignored "-Wnullability-completeness"
#import <Foundation/Foundation.h>
#include "Internet.hpp"
#include <string.h>
#include <stdlib.h>
#include <string>
#include <tjp/core/future/Future.hpp>
#include <tjp/core/log/Log.h>
#include <tjp/core/log/LogOf.h>
#include <tjp/core/string/as_string.hpp>
// http://iphoneincubator.com/blog/server-communication/how-to-download-a-file-only-if-it-has-been-updated/comment-page-1
//===============================================================================
typedef bool (*PartialCallback)(const void *bytes, NSUInteger length, void * _Nonnull context);
typedef void (*CompleteCallback)(int statusCode, const char * _Nullable errorMessage, void * _Nonnull context);
typedef bool (*HeaderLineCallback)(const void *line, NSUInteger length, void * _Nonnull context);
@interface ProgressiveDownloader : NSObject <NSURLSessionDataDelegate>
{
PartialCallback partialCallback;
CompleteCallback completeCallback;
HeaderLineCallback headerLineCallback;
NSString *method;
size_t maxRedirects;
void *callbackContext;
}
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSMutableData *receivedData;
- (instancetype)initWithPartial:(PartialCallback)partial
complete:(CompleteCallback)complete
headers:(HeaderLineCallback)headers
context:(void *)context
maxRedirects:(size_t)maxRedirects
method:(NSString *)method;
- (void)startDownload:(NSMutableURLRequest *)url;
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;
@end
@implementation ProgressiveDownloader
- (instancetype)initWithPartial:(PartialCallback)partial
complete:(CompleteCallback)complete
headers:(HeaderLineCallback)headers
context:(void *)context
maxRedirects:(size_t)maxRedirects_
method:(NSString *)method_
{
self = [super init];
if (self)
{
partialCallback = partial;
completeCallback = complete;
headerLineCallback = headers;
callbackContext = context;
maxRedirects = maxRedirects_;
method = method_;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPCookieStorage = nil;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
self.session = session;
// NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
// self.session = [NSURLSession sessionWithConfiguration:config
// delegate:self
// delegateQueue:[NSOperationQueue mainQueue]];
self.receivedData = [NSMutableData data];
}
return self;
}
- (void)startDownload:(NSMutableURLRequest *)request
{
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
[task resume];
}
// Response received
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode/100 == 3)
if (maxRedirects)
{
sLogDebug("core::internet::headers", "getting headers while redirecting - ignoring");
return completionHandler(NSURLSessionResponseAllow);
}
auto continue_task = true;
if (headerLineCallback)
{
NSString *headerLine = [NSString stringWithFormat:@"HTTP/1.1 %d\r\n", httpResponse.statusCode];
headerLineCallback(headerLine.UTF8String, [headerLine length], callbackContext);
[httpResponse.allHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
NSString *k = [key description];
NSString *v = [value description];
NSString *headerLine = [NSString stringWithFormat:@"%@: %@\r\n", k, v];
headerLineCallback(headerLine.UTF8String, [headerLine length], callbackContext);
}];
continue_task = headerLineCallback("\r\n", 2, callbackContext);
}
if (!continue_task)
[dataTask cancel];
completionHandler(NSURLSessionResponseAllow);
}
// Data arrives progressively
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
auto continue_task = true;
if (partialCallback)
continue_task = partialCallback(data.bytes, data.length, callbackContext);
[self.receivedData appendData:data];
if (!continue_task)
[dataTask cancel];
}
// Completion or error
- (void)URLSession:(NSURLSession * _Nonnull)session
task:(NSURLSessionTask * _Nonnull)task
didCompleteWithError:(NSError * _Nullable)error
{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
if (completeCallback)
{
if (error)
completeCallback(response.statusCode, error.localizedDescription.UTF8String, callbackContext);
else
completeCallback(response.statusCode, NULL, callbackContext);
}
}
// Optionally handle redirect logic if needed
- (void)URLSession:(NSURLSession * _Nonnull)session
task:(NSURLSessionTask * _Nonnull)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response_
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler
{
// NSHTTPURLResponse *response = (NSHTTPURLResponse *)response_;
sLogDebug("core::internet", "redirect requested " <<
logVar(maxRedirects)
logVar(request.URL.absoluteString.UTF8String) <<
logVar(request.HTTPMethod.UTF8String)
);
if (maxRedirects)
{
maxRedirects--;
if (![request.HTTPMethod isEqualToString:method])
{
sLogDebug("core::internet", "modifying method to match request");
NSMutableURLRequest *request_ = request.mutableCopy;
[request_ setHTTPMethod:method];
request = request_;
}
completionHandler(request);
}
else
{
completionHandler(nil);
}
}
@end
namespace tjp {
namespace core {
namespace internet {
struct ProgressiveContext {
internal::ProgressiveData data;
dispatch_semaphore_t semaphor;
} ;
bool onPartial(const void * _Nullable bytes, NSUInteger length, void * _Nonnull context_)
{
return internal::receiveContentsCallback((char *)bytes, length, context_) == length;
}
bool onHeaders(const void * _Nullable bytes, NSUInteger length, void * _Nonnull context_)
{
return internal::receiveHeadersCallback((char *)bytes, length, context_) == length;
}
void onComplete(int statusCode, const char * _Nullable errorMessage, void * _Nonnull context_)
{
auto *context = static_cast<ProgressiveContext *>(context_);
context->data.receive.data.statusCode = statusCode;
dispatch_semaphore_signal(context->semaphor);
}
Future<FileContents> get (const std::string &url, const HttpOptions &options)
{
auto request = strong<internal::Request>();
request->type = internal::RequestType::GET;
request->url = url;
request->options = options;
perform(request);
return request->promise.get_future();
}
Future<FileContents> post (const std::string &url, Data &&data, const HttpOptions &options)
{
auto request = strong<internal::Request>();
request->type = internal::RequestType::POST;
request->url = url;
request->data = std::move(data);
request->options = options;
perform(request);
return request->promise.get_future();
}
void perform_progressive(const WeakPtr<internal::Request> &request_)
{
auto getFile = [](const WeakPtr<internal::Request> &request_)
{
ProgressiveContext context;
auto &data = context.data;
data.request = request_;
context.semaphor = dispatch_semaphore_create(0);
String url;
ProgressiveDownloader *downloader;
NSString *method;
if (auto request__ = strong(request_))
{
url = request__->url;
data.options = std::move(request__->options);
if (request__->data)
data.send.data = std::move(*request__->data);
NSString *stringURL = [NSString stringWithUTF8String:url.c_str()];
// without this, iOS <= 16 doesn't work for all urls
NSCharacterSet *allowed = [NSCharacterSet URLFragmentAllowedCharacterSet];
NSString *stringURLEncoded = [stringURL stringByAddingPercentEncodingWithAllowedCharacters:allowed];
NSURL *url_ = [NSURL URLWithString:stringURLEncoded];
sLogDebug("core::internet", "starting " << logVar(url) << logVar(stringURLEncoded.UTF8String));
// Prepare HTTP request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url_];
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
// [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
if (data.options.timeout)
request.timeoutInterval = data.options.timeout;
auto &requestType = request__->type;
if (requestType == internal::RequestType::POST)
{
method = @"POST";
sLogDebug("core::internet", "starting " << "POST");
[request setHTTPMethod:method];
NSData *data_ = [NSData dataWithBytes:data.send.data.c_str() length:data.send.data.size()];
[request setHTTPBody:data_];
}
else
if (requestType == internal::RequestType::PUT)
{
method = @"PUT";
sLogDebug("core::internet", "starting " << "PUT");
[request setHTTPMethod:method];
NSData *data_ = [NSData dataWithBytes:data.send.data.data() length:data.send.data.size()];
[request setHTTPBody:data_];
}
else
if (requestType == internal::RequestType::GET)
{
method = @"GET";
sLogDebug("core::internet", "starting " << "GET");
[request setHTTPMethod:method];
}
else
if (requestType == internal::RequestType::HEAD)
{
method = @"HEAD";
sLogDebug("core::internet", "starting " << "HEAD");
[request setHTTPMethod:method];
}
for (auto &header : data.options.headers)
{
auto &[key, value] = header;
sLogDebug("core::internet::headers", "send header " << logVar(key) << logVar(value));
NSString *key_ = [NSString stringWithUTF8String:key.c_str()];
NSString *value_ = [NSString stringWithUTF8String:value.c_str()];
[request setValue:value_ forHTTPHeaderField:key_];
}
if (data.options.lastModifiedTime)
{
NSDate *date = [NSDate dateWithTimeIntervalSince1970:data.options.lastModifiedTime];
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss Z";
df.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
NSString *dateString = [df stringFromDate:date];
[request setValue:dateString forHTTPHeaderField:@"If-Modified-Since"];
}
if (url_.user)
{
NSString *user = url_.user;
NSString *pass = url_.password ?: @"";
NSString *auth = [NSString stringWithFormat:@"%@:%@", user, pass];
NSData *authData = [auth dataUsingEncoding:NSUTF8StringEncoding];
NSString *authValue = [NSString stringWithFormat:@"Basic %@",
[authData base64EncodedStringWithOptions:0]];
[request setValue:authValue forHTTPHeaderField:@"Authorization"];
}
[request setValue:@"close" forHTTPHeaderField:@"Connection"];
// --- Define progressive callbacks ---
downloader =
[[ProgressiveDownloader alloc]
initWithPartial:onPartial
complete:onComplete
headers:onHeaders
context:&context
maxRedirects:data.options.maxRedirects
method:method
];
[downloader startDownload:request];
}
else
{
throw Exception (
"HTTPConnection canceled",
{}
);
}
dispatch_semaphore_wait(context.semaphor, DISPATCH_TIME_FOREVER);
sLogDebug("core::internet", "finishing 0");
if (data.receive.data.statusCode < 0)
data.receive.data.statusCode = internet::GeneralFailure;
if (data.interrupted)
data.receive.data.statusCode = internet::GeneralFailure;
if (data.options.onHeaders)
data.options.onHeaders(data.receive.data.headers, data.receive.data.statusCode);
if (data.options.onComplete)
data.options.onComplete(data.receive.data.statusCode);
sLogDebug("core::internet", "finishing 1");
auto got_error = data.receive.data.statusCode >= 400;
sLogReleaseIf(got_error, "debug", "finished " << logVar(url) << logVar(got_error));
if (got_error)
{
throw Exception("HTTPConnection getFile failed", std::move(data.receive.data));
}
if (data.options.throwOnFailure && !data.receive.data.ok())
{
sLogRelease("errors", "get http failed: " << logVar(url) << logVar(data.receive.data.statusCode));
throw Exception(
"HTTPConnection getFile failed code " + std::to_string(data.receive.data.statusCode),
std::move(data.receive.data));
}
return data.receive.data;
};
Promise<FileContents> promise;
if (auto request = strong(request_))
promise = std::move(request->promise);
promise.consume(getFile, request_);
}
#if REMOVE_CODE_2025_11_11
# code is superseded by perform
void perform_completion (const std::string &url, const std::string &method, Data &&data, const HttpOptions &options, Promise<FileContents> &promise)
{
xLogDebug (logVar(url));
auto getFile = [&]() {
__block FileContents contents;
sLogDebug("core::internet", "starting " << url);
NSString *stringURL = [NSString stringWithUTF8String:url.c_str()];
NSURL *url_ = [NSURL URLWithString:stringURL];
// NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // No cookie storage
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; // No cookie storage
// config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
config.HTTPCookieStorage = nil;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
// NSURLSession *session = [NSURLSession sharedSession];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url_];
if (options.timeout)
request.timeoutInterval = options.timeout;
[request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"];
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
if (method == "POST")
{
[request setHTTPMethod:@"POST"];
NSString *str = [NSString stringWithUTF8String:data.c_str()];
NSData *data_ = [str dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPBody:data_];
}
else
if (method == "GET")
{
[request setHTTPMethod:@"GET"];
}
else
if (method == "HEAD")
{
[request setHTTPMethod:@"HEAD"];
}
else
if (method == "PUT")
{
[request setHTTPMethod:@"PUT"];
NSData *data_ = [NSData dataWithBytes:data.data() length:data.size()];
[request setHTTPBody:data_];
}
for (auto &header : options.headers)
{
auto &[key, value] = header;
NSString *key_ = [NSString stringWithUTF8String:key.c_str()];
NSString *value_ = [NSString stringWithUTF8String:value.c_str()];
[request setValue:value_ forHTTPHeaderField:key_];
}
if (options.lastModifiedTime)
{
NSString *key_ = @"If-Modified-Since";
NSDate *date = [NSDate dateWithTimeIntervalSince1970:options.lastModifiedTime];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; // maybe there exist a new-method now
dateFormatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss Z"; //RFC2822-Format
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier: @"en_US_POSIX"];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
NSString *dateString = [dateFormatter stringFromDate:date];
[request setValue:dateString forHTTPHeaderField:key_];
}
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block bool got_error = false;
NSURLSessionDataTask *downloadTask =
[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response_, NSError *error) {
if (!error)
{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)response_;
NSString *lastModifiedString = [[response allHeaderFields] objectForKey:@"Last-Modified"];
if (lastModifiedString)
{
NSDate *lastModifiedServer = nil;
@try {
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
df.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
lastModifiedServer = [df dateFromString:lastModifiedString];
}
@catch (NSException * e) {
NSLog(@"Error parsing last modified date: %@ - %@", lastModifiedString, [e description]);
}
contents.lastModifiedTime = [lastModifiedServer timeIntervalSince1970];
}
NSDictionary *headers = [response allHeaderFields];
NSArray *keys = [headers allKeys];
for (NSString* key in keys)
{
NSString *value = [headers objectForKey:key];
contents.headers.push_back({ key.UTF8String, value.UTF8String });
}
contents.data.resize(data.length);
contents.statusCode = response.statusCode;
memcpy(contents.data.data(), data.bytes, contents.data.size());
}
else
{
got_error = true;
}
dispatch_semaphore_signal(semaphore);
}];
[downloadTask resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (options.onHeaders)
options.onHeaders(contents.headers, contents.statusCode);
if (options.onData)
options.onData(contents.data.data(), contents.data.size());
if (options.onComplete)
options.onComplete(contents.statusCode);
sLogReleaseIf(got_error, "debug", "finished " << logVar(url) << logVar(got_error));
if (got_error)
throw Exception (
"HTTPConnection getFile failed",
std::move(contents)
);
if (options.throwOnFailure && !contents.ok())
{
sLogRelease("errors", "get http failed: " << logVar(url) << logVar(contents.statusCode));
throw Exception (
"HTTPConnection getFile failed code " + std::to_string(contents.statusCode),
std::move(contents)
);
}
return contents;
};
promise_of(promise, getFile);
}
#endif
void perform(const WeakPtr<internal::Request> &request_)
{
perform_progressive(request_);
}
} // namespace
} // namespace
} // namespace

106
tjp/core/internet/ios/Queue.cpp Executable file
View File

@@ -0,0 +1,106 @@
// TJP COPYRIGHT HEADER
#ifdef TJP_CORE_HEADER_ONLY
#pragma once
#endif
#include "../Queue.hpp"
#include "Internet.hpp"
#include <tjp/core/threads/Lock.hpp>
#include <tjp/core/threads/process_queue.hpp>
#include <tjp/core/ptr/strong_of.hpp>
#include <tjp/core/future/Future.hpp>
#include <queue>
//===============================================================================
namespace tjp::core::internet {
struct Queue::Internal
{
std::thread thread;
bool finished = false;
std::queue<WeakPtr<internal::Request>> queue;
Event event;
Event::Mutex mutex;
void run ();
void runOne (const WeakPtr<internal::Request> &i);
} ;
TJP_CORE_HEADER_ONLY_INLINE
Queue::Queue()
{
}
TJP_CORE_HEADER_ONLY_INLINE
Queue::~Queue ()
{
}
TJP_CORE_HEADER_ONLY_INLINE
void Queue::Internal::runOne (const WeakPtr<internal::Request> &i_)
{
perform(i_);
}
TJP_CORE_HEADER_ONLY_INLINE
void Queue::Internal::run ()
{
threads::process_queue(
finished,
event,
mutex,
queue,
[this](auto &i) { return runOne(i); }
);
}
TJP_CORE_HEADER_ONLY_INLINE
void Queue::prepare()
{
if (i)
return;
static Mutex mutex;
auto lock = lock_of(mutex);
if (i)
return;
i = strong<Internal>();
i->thread = std::thread([s=i](){ s->run(); });
}
TJP_CORE_HEADER_ONLY_INLINE
Future<FileContents> Queue::execute(internal::RequestType type, const std::string &url, Data &&data, const HttpOptions &options)
{
prepare();
auto item = strong_of_value(
internal::Request {
.type = type,
.url = url,
.options = options,
.data = std::move(data)
}
);
{
auto lock = lock_of(i->mutex);
i->queue.push(weak(item));
}
i->event.notify_all();
return item->promise.get_future().then(
[item](auto &&f) {
return f.get();
}
);
}
} // namespace

View File

@@ -0,0 +1,7 @@
// TJP COPYRIGHT HEADER
#pragma once
#ifdef TJP_CORE_HEADER_ONLY
#include "Queue.cpp"
#endif