mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Remove minitool cuz it's no longer useful
This commit is contained in:
parent
59c1403ea4
commit
e9d9f3578e
27 changed files with 0 additions and 4009 deletions
|
|
@ -1,211 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridge.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/24.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceHandle
|
||||
|
||||
@end
|
||||
|
||||
|
||||
NSDictionary *dictionaryFromPlist(plist_t plist, NSError **error) {
|
||||
if (!plist) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"PlistConversionErrorDomain"
|
||||
code:1001
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Input plist is nil."}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
char* buf = 0;
|
||||
uint32_t plistlen = 0;
|
||||
// try bin plist first, if failed, we try xml plist
|
||||
plist_to_bin(plist, &buf, &plistlen);
|
||||
|
||||
NSPropertyListFormat format;
|
||||
NSDictionary *result = [NSPropertyListSerialization propertyListWithData:[NSData dataWithBytes:buf length:plistlen]
|
||||
options:NSPropertyListImmutable format:&format error:error];
|
||||
plist_mem_free(buf);
|
||||
if (![result isKindOfClass:[NSDictionary class]]) {
|
||||
plist_to_xml(plist, &buf, &plistlen);
|
||||
if (error) {
|
||||
*error = nil;
|
||||
}
|
||||
result = [NSPropertyListSerialization propertyListWithData:[NSData dataWithBytes:buf length:plistlen]
|
||||
options:NSPropertyListImmutable format:&format error:error];
|
||||
plist_free(buf);
|
||||
}
|
||||
|
||||
if (![result isKindOfClass:[NSDictionary class]]) {
|
||||
if (error && !*error) {
|
||||
*error = [NSError errorWithDomain:@"PlistConversionErrorDomain"
|
||||
code:1002
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Plist is not a dictionary."}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NSData *plistDataFromDictionary(NSDictionary *dictionary, NSError **error) {
|
||||
if (!dictionary || ![dictionary isKindOfClass:[NSDictionary class]]) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:@"PlistSerializationErrorDomain"
|
||||
code:2001
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Input is not a valid dictionary."}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *data = [NSPropertyListSerialization dataWithPropertyList:dictionary
|
||||
format:NSPropertyListXMLFormat_v1_0
|
||||
options:0
|
||||
error:error];
|
||||
return data;
|
||||
}
|
||||
|
||||
const char** cstrArrFromNSArray(NSArray* arr, int* validCount) {
|
||||
const char** ans = 0;
|
||||
*validCount = 0;
|
||||
if(![arr isKindOfClass:NSArray.class]) {
|
||||
return 0;
|
||||
} else {
|
||||
ans = malloc([arr count] * sizeof(void*));
|
||||
for (id str in arr) {
|
||||
if([str isKindOfClass:NSString.class]) {
|
||||
ans[*validCount] = [str UTF8String];
|
||||
(*validCount)++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
@implementation IDeviceJSBridge {
|
||||
int maxHandleId;
|
||||
int maxDataId;
|
||||
}
|
||||
|
||||
- (instancetype)initWithContext:(JSContext*)context allowedFunctions:(NSArray<NSString*>*)allowedFunctions {
|
||||
maxHandleId = 0;
|
||||
maxDataId = 0;
|
||||
handles = [[NSMutableDictionary alloc] init];
|
||||
dataPool = [[NSMutableDictionary alloc] init];
|
||||
self->allowedFunctions = allowedFunctions;
|
||||
self->context = context;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString*)errFreeFromIdeviceFfiError:(IdeviceFfiError*)err {
|
||||
NSString* ans = [NSString stringWithFormat:@"%s (%d)", err->message, err->code];
|
||||
idevice_error_free(err);
|
||||
return ans;
|
||||
}
|
||||
|
||||
- (int)registerIdeviceHandle:(void*)handle freeFunc:(void*)freeFunc {
|
||||
maxHandleId++;
|
||||
int ans = maxHandleId;
|
||||
IDeviceHandle* handleObj = [IDeviceHandle alloc];
|
||||
handleObj.handle = handle;
|
||||
handleObj.freeFunc = freeFunc;
|
||||
handles[@(maxHandleId)] = handleObj;
|
||||
return ans;
|
||||
}
|
||||
|
||||
- (BOOL)freeIdeviceHandle:(int)handleId {
|
||||
if(handles[@(handleId)]) {
|
||||
IDeviceHandle* handleObj = handles[@(handleId)];
|
||||
void (*freeFunc)(void*) = handleObj.freeFunc;
|
||||
freeFunc(handleObj.handle);
|
||||
|
||||
|
||||
[handles removeObjectForKey:@(handleId)];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (bool)freeNSData:(int)handleId {
|
||||
if(dataPool[@(handleId)]) {
|
||||
[dataPool removeObjectForKey:@(handleId)];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
- (int)registerNSData:(NSData*)data {
|
||||
maxHandleId++;
|
||||
int ans = maxHandleId;
|
||||
dataPool[@(maxHandleId)] = data;
|
||||
return ans;
|
||||
}
|
||||
|
||||
- (void)cleanUp {
|
||||
NSArray<NSNumber *> *sortedKeys = [[handles allKeys] sortedArrayUsingSelector:@selector(compare:)];
|
||||
for(NSNumber* a in sortedKeys) {
|
||||
[self freeIdeviceHandle:[a intValue]];
|
||||
}
|
||||
for(NSNumber* a in dataPool) {
|
||||
[self freeNSData:[a intValue]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didReceiveScriptMessage:(nonnull NSDictionary *)message resolve:(JSValue*)resolveFunc reject:(JSValue*)rejectFunc {
|
||||
if(![message isKindOfClass:NSDictionary.class]) {
|
||||
[rejectFunc callWithArguments:@[@"Input is not a dictionary"]];
|
||||
return;
|
||||
}
|
||||
|
||||
if(![self->allowedFunctions containsObject:message[@"command"]]) {
|
||||
[rejectFunc callWithArguments:@[[NSString stringWithFormat:@"calling %@ is not allowed! Declare it in toolInfo.json", message[@"command"]]]];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* handlerSelectorStr = [NSString stringWithFormat:@"%@WithBody:replyHandler:", message[@"command"]];
|
||||
if(![self respondsToSelector:NSSelectorFromString(handlerSelectorStr)]) {
|
||||
[rejectFunc callWithArguments:@[@"Invalid idevice function!"]];
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
void (^replyHandler)(id _Nullable, NSString * _Nullable) = ^(id result, NSString* errMsg) {
|
||||
if(errMsg) {
|
||||
[rejectFunc callWithArguments:@[errMsg]];
|
||||
} else {
|
||||
[resolveFunc callWithArguments:@[result]];
|
||||
}
|
||||
};
|
||||
|
||||
NSInvocation* invocation = [NSInvocation invocationWithTarget:self selector:NSSelectorFromString(handlerSelectorStr) retainArguments:YES, message, replyHandler];
|
||||
[invocation invoke];
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
- (void)idevice_freeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if(!handles[@(handleId)] || handles[@(handleId)].freeFunc != adapter_free) {
|
||||
replyHandler(nil, @"Invalid handle");
|
||||
return;
|
||||
}
|
||||
|
||||
[self freeIdeviceHandle:handleId];
|
||||
|
||||
replyHandler(@(YES), nil);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self cleanUp];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,370 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeAFC.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (AFC)
|
||||
|
||||
- (void)afc_client_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSError* heartbeatErr = nil;
|
||||
[JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
|
||||
if(heartbeatErr) {
|
||||
replyHandler(nil, heartbeatErr.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle];
|
||||
|
||||
AfcClientHandle* client = NULL;
|
||||
IdeviceFfiError* err = afc_client_connect(provider, &client);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int clientHandleId = [self registerIdeviceHandle:client freeFunc:(void*)afc_client_free];
|
||||
replyHandler([NSNumber numberWithInt:clientHandleId], nil);
|
||||
}
|
||||
|
||||
- (void)afc_list_directoryWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString* path = body[@"path"];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
if (![path isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid path");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
char** entries = NULL;
|
||||
size_t count = 0;
|
||||
IdeviceFfiError* err = afc_list_directory(client, [path UTF8String], &entries, &count);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableArray* result = [NSMutableArray arrayWithCapacity:count];
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
[result addObject:[NSString stringWithUTF8String:entries[i]]];
|
||||
free(entries[i]);
|
||||
}
|
||||
free(entries);
|
||||
|
||||
replyHandler(result, nil);
|
||||
}
|
||||
|
||||
- (void)afc_make_directoryWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString* path = body[@"path"];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
if (![path isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid path");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = afc_make_directory(client, [path UTF8String]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)afc_remove_pathWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString* path = body[@"path"];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
if (![path isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid path");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = afc_remove_path(client, [path UTF8String]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)afc_remove_path_and_contentsWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString* path = body[@"path"];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
if (![path isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid path");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = afc_remove_path_and_contents(client, [path UTF8String]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)afc_rename_pathWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString* source = body[@"source"];
|
||||
NSString* target = body[@"target"];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
if (![source isKindOfClass:NSString.class] || ![target isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid source or target path");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = afc_rename_path(client, [source UTF8String], [target UTF8String]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)afc_get_file_infoWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString* path = body[@"path"];
|
||||
|
||||
if (!path || ![path isKindOfClass:[NSString class]]) {
|
||||
replyHandler(nil, @"Missing or invalid path");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
struct AfcFileInfo info;
|
||||
struct IdeviceFfiError* err = afc_get_file_info(client, [path UTF8String], &info);
|
||||
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* ans = @{
|
||||
@"size": @(info.size),
|
||||
@"blocks": @(info.blocks),
|
||||
@"creation": @(info.creation),
|
||||
@"modified": @(info.modified),
|
||||
@"st_nlink": @(info.st_nlink),
|
||||
@"st_ifmt": @(info.st_ifmt),
|
||||
@"st_link_target": @(info.st_link_target)
|
||||
};
|
||||
afc_file_info_free(&info);
|
||||
|
||||
replyHandler(ans, nil);
|
||||
}
|
||||
|
||||
- (void)afc_get_device_infoWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
struct AfcDeviceInfo info;
|
||||
struct IdeviceFfiError* err = afc_get_device_info(client, &info);
|
||||
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* ans = @{
|
||||
@"model": @(info.model),
|
||||
@"total_bytes": @(info.total_bytes),
|
||||
@"free_bytes": @(info.free_bytes),
|
||||
@"block_size": @(info.block_size),
|
||||
};
|
||||
afc_device_info_free(&info);
|
||||
|
||||
replyHandler(ans, nil);
|
||||
}
|
||||
|
||||
- (void)afc_file_openWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString *path = body[@"path"];
|
||||
NSNumber *modeNumber = body[@"mode"];
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![path isKindOfClass:NSString.class] || ![modeNumber isKindOfClass:NSNumber.class]) {
|
||||
replyHandler(nil, @"Invalid path or mode");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle *client = clientHandleObj.handle;
|
||||
|
||||
AfcFileHandle *fileHandle = NULL;
|
||||
IdeviceFfiError* err = afc_file_open(client, [path UTF8String], (AfcFopenMode)[modeNumber intValue], &fileHandle);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:fileHandle freeFunc:(void *)afc_file_close];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)afc_file_closeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != afc_file_close) {
|
||||
replyHandler(nil, @"Invalid afc file handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *fileHandleObj = handles[@(handleId)];
|
||||
AfcFileHandle *fileHandle = fileHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = afc_file_close(fileHandle);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
[handles removeObjectForKey:@(handleId)];
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)afc_file_readWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != afc_file_close) {
|
||||
replyHandler(nil, @"Invalid afc file handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *fileHandleObj = handles[@(handleId)];
|
||||
AfcFileHandle *fileHandle = fileHandleObj.handle;
|
||||
|
||||
unsigned char* file_data = 0;
|
||||
size_t len = 0;
|
||||
IdeviceFfiError* err = afc_file_read(fileHandle, &file_data, &len);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* data = [NSData dataWithBytes:file_data length:len];
|
||||
free(file_data);
|
||||
int nsdataHandleId = [self registerNSData:data];
|
||||
replyHandler(@(nsdataHandleId), nil);
|
||||
}
|
||||
|
||||
- (void)afc_file_writeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != afc_file_close) {
|
||||
replyHandler(nil, @"Invalid afc file handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *fileHandleObj = handles[@(handleId)];
|
||||
AfcFileHandle *fileHandle = fileHandleObj.handle;
|
||||
|
||||
int nsdataHandleId = [body[@"data_handle"] intValue];
|
||||
if (!dataPool[@(nsdataHandleId)]) {
|
||||
replyHandler(nil, @"Invalid NSData handle");
|
||||
return;
|
||||
}
|
||||
NSData* data = dataPool[@(nsdataHandleId)];
|
||||
|
||||
IdeviceFfiError* err = afc_file_write(fileHandle, [data bytes], [data length]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@(YES), nil);
|
||||
}
|
||||
|
||||
- (void)afc_make_linkWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString *target = body[@"target"];
|
||||
NSString *source = body[@"source"];
|
||||
NSNumber *linkTypeNum = body[@"link_type"];
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != afc_client_free) {
|
||||
replyHandler(nil, @"Invalid afc client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![target isKindOfClass:NSString.class] || ![source isKindOfClass:NSString.class] || ![linkTypeNum isKindOfClass:NSNumber.class]) {
|
||||
replyHandler(nil, @"Invalid target/source/link_type");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *clientHandleObj = handles[@(clientId)];
|
||||
AfcClientHandle *client = clientHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = afc_make_link(client,
|
||||
[target UTF8String],
|
||||
[source UTF8String],
|
||||
(AfcLinkType)[linkTypeNum intValue]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeAMFI.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/26.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
#import "../../idevice/JITEnableContext.h"
|
||||
#import "../../idevice/idevice.h"
|
||||
|
||||
@implementation IDeviceJSBridge (AMFI)
|
||||
|
||||
- (void)amfi_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSError* heartbeatErr = nil;
|
||||
[JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
|
||||
if(heartbeatErr) {
|
||||
replyHandler(nil, heartbeatErr.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceProviderHandle *provider = [JITEnableContext.shared getTcpProviderHandle];
|
||||
|
||||
AmfiClientHandle *client = NULL;
|
||||
IdeviceFfiError* err = amfi_connect(provider, &client);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:client freeFunc:(void *)amfi_client_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)amfi_reveal_developer_mode_option_in_uiWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != amfi_client_free) {
|
||||
replyHandler(nil, @"Invalid AmfiClient handle");
|
||||
return;
|
||||
}
|
||||
|
||||
AmfiClientHandle *client = handles[@(clientId)].handle;
|
||||
IdeviceFfiError* err = amfi_reveal_developer_mode_option_in_ui(client);
|
||||
if (err ) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)amfi_enable_developer_modeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != amfi_client_free) {
|
||||
replyHandler(nil, @"Invalid AmfiClient handle");
|
||||
return;
|
||||
}
|
||||
|
||||
AmfiClientHandle *client = handles[@(clientId)].handle;
|
||||
IdeviceFfiError* err = amfi_enable_developer_mode(client);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)amfi_accept_developer_modeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != amfi_client_free) {
|
||||
replyHandler(nil, @"Invalid AmfiClient handle");
|
||||
return;
|
||||
}
|
||||
|
||||
AmfiClientHandle *client = handles[@(clientId)].handle;
|
||||
IdeviceFfiError* err = amfi_accept_developer_mode(client);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeAdapter.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (Adapter)
|
||||
|
||||
- (void)adapter_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"adapter"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != adapter_free) {
|
||||
replyHandler(nil, @"Invalid adapter handle");
|
||||
return;
|
||||
}
|
||||
|
||||
int port = [body[@"port"] intValue];
|
||||
if(port < 0 || port > 65536 ) {
|
||||
replyHandler(nil, @"Invalid port");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AdapterHandle* handle = clientHandleObj.handle;
|
||||
AdapterStreamHandle *stream = NULL;
|
||||
IdeviceFfiError* err = adapter_connect(handle, port, (ReadWriteOpaque **)&stream);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
int handleId = [self registerIdeviceHandle:stream freeFunc:(void *)adapter_stream_close];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeCoreDevice.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
@implementation IDeviceJSBridge (CoreDevice)
|
||||
|
||||
- (void)core_device_proxy_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSError* heartbeatErr = nil;
|
||||
[JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
|
||||
if(heartbeatErr) {
|
||||
replyHandler(nil, heartbeatErr.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle];
|
||||
|
||||
CoreDeviceProxyHandle *core_device = NULL;
|
||||
IdeviceFfiError* err = core_device_proxy_connect(provider, &core_device);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
int handleId = [self registerIdeviceHandle:core_device freeFunc:(void*)core_device_proxy_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)core_device_proxy_get_server_rsd_portWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != core_device_proxy_free) {
|
||||
replyHandler(nil, @"Invalid core device proxy handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
CoreDeviceProxyHandle* core_device = clientHandleObj.handle;
|
||||
|
||||
// Get server RSD port
|
||||
uint16_t rsd_port;
|
||||
IdeviceFfiError* err = core_device_proxy_get_server_rsd_port(core_device, &rsd_port);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
replyHandler(@(rsd_port), nil);
|
||||
}
|
||||
|
||||
- (void)core_device_proxy_create_tcp_adapterWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != core_device_proxy_free) {
|
||||
replyHandler(nil, @"Invalid core device proxy handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
CoreDeviceProxyHandle* core_device = clientHandleObj.handle;
|
||||
|
||||
AdapterHandle *adapter = NULL;
|
||||
IdeviceFfiError* err = core_device_proxy_create_tcp_adapter(core_device, &adapter);
|
||||
[handles removeObjectForKey:@(clientId)];
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:adapter freeFunc:(void*)adapter_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeDebugProxy.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (DebugProxy)
|
||||
|
||||
- (void)debug_proxy_connect_rsdWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"adapter"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != adapter_free) {
|
||||
replyHandler(nil, @"Invalid adapter handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AdapterHandle* adapter = clientHandleObj.handle;
|
||||
|
||||
int handshakeId = [body[@"handshake"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid handshake handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* handshakeHandleObj = handles[@(handshakeId)];
|
||||
RsdHandshakeHandle* handshake = handshakeHandleObj.handle;
|
||||
|
||||
DebugProxyHandle *debug_proxy = NULL;
|
||||
IdeviceFfiError* err = debug_proxy_connect_rsd(adapter, handshake, &debug_proxy);
|
||||
[handles removeObjectForKey:@(clientId)];
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:debug_proxy freeFunc:(void*)debug_proxy_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)debug_proxy_send_commandWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != debug_proxy_free) {
|
||||
replyHandler(nil, @"Invalid debug proxy handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
DebugProxyHandle* debug_proxy = clientHandleObj.handle;
|
||||
|
||||
NSObject* debugCommandObj = body[@"debug_command"];
|
||||
DebugserverCommandHandle* command = 0;
|
||||
if([debugCommandObj isKindOfClass:NSString.class]) {
|
||||
command = debugserver_command_new([(NSString*)debugCommandObj UTF8String], NULL, 0);
|
||||
} else if ([debugCommandObj isKindOfClass:NSDictionary.class]) {
|
||||
NSDictionary* commandBody = (NSDictionary*)debugCommandObj;
|
||||
|
||||
NSString* name = commandBody[@"name"];
|
||||
if(![name isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid command name");
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray* args = commandBody[@"args"];
|
||||
int argsCount = 0;
|
||||
const char** argsCharArr = cstrArrFromNSArray(args, &argsCount);
|
||||
|
||||
command = debugserver_command_new([name UTF8String], argsCharArr, argsCount);
|
||||
} else {
|
||||
replyHandler(nil, @"Invalid command");
|
||||
return;
|
||||
}
|
||||
|
||||
char* attach_response = 0;
|
||||
IdeviceFfiError* err = debug_proxy_send_command(debug_proxy, command, &attach_response);
|
||||
debugserver_command_free(command);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
NSString* commandResponse = nil;
|
||||
if(attach_response) {
|
||||
commandResponse = @(attach_response);
|
||||
}
|
||||
idevice_string_free(attach_response);
|
||||
replyHandler(commandResponse, nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,301 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeInstallationProxy.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
@import JavaScriptCore;
|
||||
#import "JSSupport.h"
|
||||
|
||||
struct InstallationProxyCallbackContext {
|
||||
int callbackId;
|
||||
JSContext* context;
|
||||
};
|
||||
|
||||
void installationProxyCallback(uint64_t progress, struct InstallationProxyCallbackContext* context) {
|
||||
[context->context evaluateScript:[NSString stringWithFormat:@"handle_installation_proxy_js_callback(%d, %llu)", context->callbackId, progress]];
|
||||
}
|
||||
|
||||
@implementation IDeviceJSBridge (InstallationProxy)
|
||||
|
||||
- (void)installation_proxy_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSError* heartbeatErr = nil;
|
||||
[JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
|
||||
if(heartbeatErr) {
|
||||
replyHandler(nil, heartbeatErr.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle];
|
||||
|
||||
InstallationProxyClientHandle *client = NULL;
|
||||
IdeviceFfiError* err = installation_proxy_connect(provider, &client);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
int handleId = [self registerIdeviceHandle:client freeFunc:(void*)installation_proxy_client_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)installation_proxy_get_appsWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"client"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) {
|
||||
replyHandler(nil, @"Invalid client handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
InstallationProxyClientHandle* client = clientHandleObj.handle;
|
||||
NSString* applicationType = body[@"application_type"];
|
||||
if(![applicationType isKindOfClass:NSString.class]) {
|
||||
applicationType = nil;
|
||||
}
|
||||
|
||||
NSArray* bundleIdentifiers = body[@"bundle_identifiers"];
|
||||
|
||||
int bundleIdentifiersCount;
|
||||
const char** bundleIdentifiersCharArr = cstrArrFromNSArray(bundleIdentifiers, &bundleIdentifiersCount);
|
||||
void *apps = NULL;
|
||||
size_t apps_len = 0;
|
||||
IdeviceFfiError* err = installation_proxy_get_apps(client, [applicationType UTF8String], bundleIdentifiersCharArr, bundleIdentifiersCount, &apps, &apps_len);
|
||||
if(bundleIdentifiersCharArr){
|
||||
free(bundleIdentifiersCharArr);
|
||||
}
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
plist_t *app_list = (plist_t *)apps;
|
||||
do {
|
||||
NSMutableArray* ans = [[NSMutableArray alloc] init];
|
||||
for(int i = 0; i < apps_len; ++i) {
|
||||
NSError* err2 = 0;
|
||||
NSDictionary* appDict = dictionaryFromPlist(app_list[i], &err2);
|
||||
|
||||
if(err2) {
|
||||
replyHandler(nil, @"failed to parse plist data");
|
||||
break;
|
||||
}
|
||||
[ans addObject:appDict];
|
||||
}
|
||||
replyHandler(ans, nil);
|
||||
} while(0);
|
||||
|
||||
for(int i = 0; i < apps_len; ++i) {
|
||||
plist_free(app_list[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)installation_proxy_installWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
|
||||
int clientId = [body[@"client"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) {
|
||||
replyHandler(nil, @"Invalid client handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
InstallationProxyClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
NSString* packagePath = body[@"package_path"];
|
||||
if(![packagePath isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid package path");
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* optionsDict = body[@"options"];
|
||||
plist_t optionsPlist = 0;
|
||||
if([optionsDict isKindOfClass:NSDictionary.class]) {
|
||||
NSError* error = 0;
|
||||
NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error);
|
||||
if(error) {
|
||||
replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]);
|
||||
return;
|
||||
}
|
||||
plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0);
|
||||
}
|
||||
|
||||
int callbackId = [body[@"callback_id"] intValue];
|
||||
IdeviceFfiError* err = 0;
|
||||
if(callbackId == -1) {
|
||||
err = installation_proxy_install(client, [packagePath UTF8String], optionsPlist);
|
||||
} else {
|
||||
struct InstallationProxyCallbackContext context;
|
||||
context.callbackId = callbackId;
|
||||
context.context = self->context;
|
||||
err = installation_proxy_install_with_callback(client, [packagePath UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context);
|
||||
|
||||
}
|
||||
|
||||
if(optionsPlist) {
|
||||
plist_free(optionsPlist);
|
||||
}
|
||||
|
||||
if(err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)installation_proxy_upgradeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
|
||||
int clientId = [body[@"client"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) {
|
||||
replyHandler(nil, @"Invalid client handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
InstallationProxyClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
NSString* packagePath = body[@"package_path"];
|
||||
if(![packagePath isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid package path");
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* optionsDict = body[@"options"];
|
||||
plist_t optionsPlist = 0;
|
||||
if([optionsDict isKindOfClass:NSDictionary.class]) {
|
||||
NSError* error = 0;
|
||||
NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error);
|
||||
if(error) {
|
||||
replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]);
|
||||
return;
|
||||
}
|
||||
plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0);
|
||||
}
|
||||
|
||||
int callbackId = [body[@"callback_id"] intValue];
|
||||
IdeviceFfiError* err = 0;
|
||||
if(callbackId == -1) {
|
||||
err = installation_proxy_upgrade(client, [packagePath UTF8String], optionsPlist);
|
||||
} else {
|
||||
struct InstallationProxyCallbackContext context;
|
||||
context.callbackId = callbackId;
|
||||
context.context = self->context;
|
||||
err = installation_proxy_upgrade_with_callback(client, [packagePath UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context);
|
||||
|
||||
}
|
||||
|
||||
if(optionsPlist) {
|
||||
plist_free(optionsPlist);
|
||||
}
|
||||
|
||||
if(err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)installation_proxy_uninstallWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
|
||||
int clientId = [body[@"client"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) {
|
||||
replyHandler(nil, @"Invalid client handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
InstallationProxyClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
NSString* bundleId = body[@"bundle_id"];
|
||||
if(![bundleId isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid bundle id");
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* optionsDict = body[@"options"];
|
||||
plist_t optionsPlist = 0;
|
||||
if([optionsDict isKindOfClass:NSDictionary.class]) {
|
||||
NSError* error = 0;
|
||||
NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error);
|
||||
if(error) {
|
||||
replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]);
|
||||
return;
|
||||
}
|
||||
plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0);
|
||||
}
|
||||
|
||||
int callbackId = [body[@"callback_id"] intValue];
|
||||
IdeviceFfiError* err = 0;
|
||||
if(callbackId == -1) {
|
||||
err = installation_proxy_uninstall(client, [bundleId UTF8String], optionsPlist);
|
||||
} else {
|
||||
struct InstallationProxyCallbackContext context;
|
||||
context.callbackId = callbackId;
|
||||
context.context = self->context;
|
||||
err = installation_proxy_uninstall_with_callback(client, [bundleId UTF8String], optionsPlist, (void (*)(uint64_t, void *))installationProxyCallback, &context);
|
||||
|
||||
}
|
||||
|
||||
if(optionsPlist) {
|
||||
plist_free(optionsPlist);
|
||||
}
|
||||
|
||||
if(err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)installation_proxy_browseWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"client"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != installation_proxy_client_free) {
|
||||
replyHandler(nil, @"Invalid client handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
InstallationProxyClientHandle* client = clientHandleObj.handle;
|
||||
|
||||
NSDictionary* optionsDict = body[@"options"];
|
||||
plist_t optionsPlist = 0;
|
||||
if([optionsDict isKindOfClass:NSDictionary.class]) {
|
||||
NSError* error = 0;
|
||||
NSData* optionsNSData = plistDataFromDictionary(optionsDict, &error);
|
||||
if(error) {
|
||||
replyHandler(nil, [NSString stringWithFormat:@"failed to parse options %@", error.localizedDescription]);
|
||||
return;
|
||||
}
|
||||
plist_from_memory((void*)[optionsNSData bytes], (uint32_t)[optionsNSData length], &optionsPlist, 0);
|
||||
}
|
||||
|
||||
plist_t *app_list = NULL;
|
||||
size_t apps_len = 0;
|
||||
IdeviceFfiError* err = installation_proxy_browse(client, optionsPlist, &app_list, &apps_len);
|
||||
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
NSMutableArray* ans = [[NSMutableArray alloc] init];
|
||||
for(int i = 0; i < apps_len; ++i) {
|
||||
NSError* err2 = 0;
|
||||
NSDictionary* appDict = dictionaryFromPlist(app_list[i], &err2);
|
||||
|
||||
if(err2) {
|
||||
replyHandler(nil, @"failed to parse plist data");
|
||||
break;
|
||||
}
|
||||
[ans addObject:appDict];
|
||||
}
|
||||
replyHandler(ans, nil);
|
||||
} while(0);
|
||||
|
||||
for(int i = 0; i < apps_len; ++i) {
|
||||
plist_free(app_list[i]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeLocationSimulation.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/26.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (LocationSimulation)
|
||||
|
||||
- (void)location_simulation_newWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int serverId = [body[@"server"] intValue];
|
||||
if (!handles[@(serverId)] || handles[@(serverId)].freeFunc != remote_server_free) {
|
||||
replyHandler(nil, @"Invalid remote server handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *serverHandleObj = handles[@(serverId)];
|
||||
RemoteServerHandle *server = serverHandleObj.handle;
|
||||
|
||||
LocationSimulationHandle *simulation = NULL;
|
||||
IdeviceFfiError* err = location_simulation_new(server, &simulation);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:simulation freeFunc:(void *)location_simulation_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)location_simulation_clearWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != location_simulation_free) {
|
||||
replyHandler(nil, @"Invalid LocationSimulationAdapterHandle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *handleObj = handles[@(handleId)];
|
||||
LocationSimulationHandle *simulation = handleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = location_simulation_clear(simulation);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)location_simulation_setWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != location_simulation_free) {
|
||||
replyHandler(nil, @"Invalid LocationSimulationAdapterHandle");
|
||||
return;
|
||||
}
|
||||
|
||||
NSNumber *lat = body[@"latitude"];
|
||||
NSNumber *lon = body[@"longitude"];
|
||||
if (![lat isKindOfClass:NSNumber.class] || ![lon isKindOfClass:NSNumber.class]) {
|
||||
replyHandler(nil, @"latitude or longitude is invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *handleObj = handles[@(handleId)];
|
||||
LocationSimulationHandle *simulation = handleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = location_simulation_set(simulation, lat.doubleValue, lon.doubleValue);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeMisagent.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/26.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (Misagent)
|
||||
|
||||
- (void)misagent_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSError* heartbeatErr = nil;
|
||||
[JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
|
||||
if(heartbeatErr) {
|
||||
replyHandler(nil, heartbeatErr.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle];
|
||||
|
||||
MisagentClientHandle* client = NULL;
|
||||
IdeviceFfiError* err = misagent_connect(provider, &client);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int clientHandleId = [self registerIdeviceHandle:client freeFunc:(void*)misagent_client_free];
|
||||
replyHandler(@(clientHandleId), nil);
|
||||
}
|
||||
|
||||
- (void)misagent_installWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
int dataHandleId = [body[@"data_handle"] intValue];
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != misagent_client_free) {
|
||||
replyHandler(nil, @"Invalid misagent client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataPool[@(dataHandleId)]) {
|
||||
replyHandler(nil, @"Invalid NSData handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *clientHandleObj = handles[@(clientId)];
|
||||
MisagentClientHandle *client = clientHandleObj.handle;
|
||||
NSData *data = dataPool[@(dataHandleId)];
|
||||
|
||||
IdeviceFfiError* err = misagent_install(client, data.bytes, data.length);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)misagent_removeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
NSString *profileId = body[@"profile_id"];
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != misagent_client_free) {
|
||||
replyHandler(nil, @"Invalid misagent client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![profileId isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid profile_id");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *clientHandleObj = handles[@(clientId)];
|
||||
MisagentClientHandle *client = clientHandleObj.handle;
|
||||
|
||||
IdeviceFfiError* err = misagent_remove(client, [profileId UTF8String]);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@YES, nil);
|
||||
}
|
||||
|
||||
- (void)misagent_copy_allWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
|
||||
if (!handles[@(clientId)] || handles[@(clientId)].freeFunc != misagent_client_free) {
|
||||
replyHandler(nil, @"Invalid misagent client handle");
|
||||
return;
|
||||
}
|
||||
|
||||
IDeviceHandle *clientHandleObj = handles[@(clientId)];
|
||||
MisagentClientHandle *client = clientHandleObj.handle;
|
||||
|
||||
uint8_t **profiles = NULL;
|
||||
size_t *profile_lens = NULL;
|
||||
size_t count = 0;
|
||||
|
||||
IdeviceFfiError* err = misagent_copy_all(client, &profiles, &profile_lens, &count);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableArray *profileHandleIds = [NSMutableArray array];
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
NSData *profileData = [NSData dataWithBytes:profiles[i] length:profile_lens[i]];
|
||||
free(profiles[i]);
|
||||
int nsdataHandleId = [self registerNSData:profileData];
|
||||
[profileHandleIds addObject:@(nsdataHandleId)];
|
||||
}
|
||||
|
||||
misagent_free_profiles(profiles, profile_lens, count);
|
||||
|
||||
replyHandler(profileHandleIds, nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeNSData.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (NSData)
|
||||
|
||||
- (void)nsdata_freeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!dataPool[@(handleId)]) {
|
||||
replyHandler(nil, @"Invalid NSData handle");
|
||||
return;
|
||||
}
|
||||
bool ans = [self freeNSData:handleId];
|
||||
replyHandler(@(ans), nil);
|
||||
}
|
||||
|
||||
- (void)nsdata_readWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!dataPool[@(handleId)]) {
|
||||
replyHandler(nil, @"Invalid NSData handle");
|
||||
return;
|
||||
}
|
||||
NSData* data = dataPool[@(handleId)];
|
||||
NSString* ans = [data base64EncodedStringWithOptions:0];
|
||||
replyHandler(ans, nil);
|
||||
}
|
||||
|
||||
- (void)nsdata_read_rangeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!dataPool[@(handleId)]) {
|
||||
replyHandler(nil, @"Invalid NSData handle");
|
||||
return;
|
||||
}
|
||||
NSData* data = dataPool[@(handleId)];
|
||||
NSUInteger begin = [body[@"begin"] intValue];
|
||||
NSUInteger end = [body[@"end"] intValue];
|
||||
if(begin < 0 || end >= [data length] || begin > end) {
|
||||
replyHandler(nil, @"Invalid range");
|
||||
return;
|
||||
}
|
||||
|
||||
[data subdataWithRange:NSMakeRange(begin, end)];
|
||||
NSString* ans = [data base64EncodedStringWithOptions:0];
|
||||
replyHandler(ans, nil);
|
||||
}
|
||||
|
||||
- (void)nsdata_get_sizeWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!dataPool[@(handleId)]) {
|
||||
replyHandler(nil, @"Invalid NSData handle");
|
||||
return;
|
||||
}
|
||||
NSData* data = dataPool[@(handleId)];
|
||||
NSUInteger ans = [data length];
|
||||
replyHandler(@(ans), nil);
|
||||
}
|
||||
|
||||
- (void)nsdata_createWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSString* dataStr = body[@"base64Data"];
|
||||
if (![dataStr isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid base64Data");
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* data = [[NSData alloc] initWithBase64EncodedString:dataStr options:0];
|
||||
if (!data) {
|
||||
replyHandler(nil, @"Failed to decode base64Data");
|
||||
return;
|
||||
}
|
||||
int ans = [self registerNSData:data];
|
||||
replyHandler(@(ans), nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation IDeviceJSBridge (LocalFile)
|
||||
|
||||
- (void)local_file_openWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSString *relativePath = body[@"path"];
|
||||
NSURL *miniToolDataURL = [[NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]
|
||||
.lastObject URLByAppendingPathComponent:@"MiniToolData"] ;
|
||||
BOOL isDir = false;
|
||||
if(![NSFileManager.defaultManager fileExistsAtPath:miniToolDataURL.path isDirectory:&isDir] || !isDir) {
|
||||
[NSFileManager.defaultManager removeItemAtURL:miniToolDataURL error:nil];
|
||||
[NSFileManager.defaultManager createDirectoryAtURL:miniToolDataURL withIntermediateDirectories:YES attributes:@{} error:nil];
|
||||
}
|
||||
NSString* path = [miniToolDataURL.path stringByAppendingPathComponent:relativePath];
|
||||
NSString *mode = body[@"mode"];
|
||||
|
||||
// Prevent path traversal attacks by ensuring the resolved path stays within MiniToolData directory
|
||||
NSString *resolvedPath = [path stringByStandardizingPath];
|
||||
NSString *baseDir = [miniToolDataURL.path stringByStandardizingPath];
|
||||
if (![resolvedPath hasPrefix:baseDir]) {
|
||||
replyHandler(nil, @"Path traversal is not allowed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![path isKindOfClass:NSString.class] || ![mode isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid file path or mode");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = fopen([path UTF8String], [mode UTF8String]);
|
||||
if (!file) {
|
||||
replyHandler(nil, @"Failed to open file");
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:(void*)file freeFunc:(void *)fclose];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)local_file_closeWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int fileId = [body[@"file"] intValue];
|
||||
|
||||
if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) {
|
||||
replyHandler(nil, @"Invalid file handle");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = handles[@(fileId)].handle;
|
||||
fclose(file);
|
||||
[handles removeObjectForKey:@(fileId)];
|
||||
|
||||
replyHandler(@(YES), nil);
|
||||
}
|
||||
|
||||
- (void)local_file_get_sizeWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int fileId = [body[@"file"] intValue];
|
||||
|
||||
if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) {
|
||||
replyHandler(nil, @"Invalid file handle");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = handles[@(fileId)].handle;
|
||||
fseek(file, 0, SEEK_END);
|
||||
long size = ftell(file);
|
||||
fseek(file, 0, SEEK_SET); // Reset position
|
||||
replyHandler(@(size), nil);
|
||||
}
|
||||
|
||||
- (void)local_file_read_chunkWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int fileId = [body[@"file"] intValue];
|
||||
long offset = [body[@"offset"] longValue];
|
||||
long length = [body[@"length"] longValue];
|
||||
|
||||
if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) {
|
||||
replyHandler(nil, @"Invalid file handle");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = handles[@(fileId)].handle;
|
||||
fseek(file, offset, SEEK_SET);
|
||||
void *buffer = malloc(length);
|
||||
size_t read = fread(buffer, 1, length, file);
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:buffer length:read freeWhenDone:YES];
|
||||
int dataHandleId = [self registerNSData:data];
|
||||
replyHandler(@(dataHandleId), nil);
|
||||
}
|
||||
|
||||
- (void)local_file_write_chunkWithBody:(NSDictionary *)body replyHandler:(void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int fileId = [body[@"file"] intValue];
|
||||
int dataId = [body[@"data"] intValue];
|
||||
long offset = [body[@"offset"] longValue];
|
||||
|
||||
if (!handles[@(fileId)] || handles[@(fileId)].freeFunc != fclose) {
|
||||
replyHandler(nil, @"Invalid file handle");
|
||||
return;
|
||||
}
|
||||
if (!dataPool[@(dataId)]) {
|
||||
replyHandler(nil, @"Invalid data handle");
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *file = handles[@(fileId)].handle;
|
||||
NSData *data = dataPool[@(dataId)];
|
||||
fseek(file, offset, SEEK_SET);
|
||||
size_t written = fwrite(data.bytes, 1, data.length, file);
|
||||
|
||||
replyHandler(@(written), nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeProcessControl.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
@implementation IDeviceJSBridge (ProcessControl)
|
||||
|
||||
- (void)process_control_newWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"server"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != remote_server_free) {
|
||||
replyHandler(nil, @"Invalid remote server handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
RemoteServerHandle* remote_server = clientHandleObj.handle;
|
||||
|
||||
ProcessControlHandle *process_control = NULL;
|
||||
IdeviceFfiError* err = process_control_new(remote_server, &process_control);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:process_control freeFunc:(void*)process_control_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)process_control_launch_appWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != process_control_free) {
|
||||
replyHandler(nil, @"Invalid process control handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
ProcessControlHandle* process_control = clientHandleObj.handle;
|
||||
|
||||
NSString* bundleId = body[@"bundle_id"];
|
||||
if(![bundleId isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid bundle id");
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray* envVars = body[@"env_vars"];
|
||||
int envVarsCount = 0;
|
||||
const char** envVarsCharArr = cstrArrFromNSArray(envVars, &envVarsCount);
|
||||
|
||||
NSArray* arguments = body[@"arguments"];
|
||||
int argumentsCount = 0;
|
||||
const char** argumentsCharArr = cstrArrFromNSArray(arguments, &argumentsCount);
|
||||
|
||||
bool startSuspended = [body[@"start_suspended"] boolValue];
|
||||
bool killExisting = [body[@"kill_existing"] boolValue];
|
||||
|
||||
uint64_t pid = 0;
|
||||
IdeviceFfiError* err = process_control_launch_app(process_control, [bundleId UTF8String], envVarsCharArr, envVarsCount, argumentsCharArr, argumentsCount,
|
||||
startSuspended, killExisting, &pid);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
if(envVarsCharArr) {
|
||||
free(envVarsCharArr);
|
||||
}
|
||||
|
||||
if(argumentsCharArr) {
|
||||
free(argumentsCharArr);
|
||||
}
|
||||
|
||||
replyHandler(@(pid), nil);
|
||||
}
|
||||
|
||||
- (void)process_control_disable_memory_limitWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != process_control_free) {
|
||||
replyHandler(nil, @"Invalid process control handle");
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
ProcessControlHandle* process_control = clientHandleObj.handle;
|
||||
|
||||
uint64_t pid = [body[@"pid"] unsignedLongLongValue];
|
||||
if(pid == 0) {
|
||||
replyHandler(nil, @"Invalid pid");
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceFfiError* err = process_control_disable_memory_limit(process_control, pid);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@(YES), nil);
|
||||
}
|
||||
|
||||
- (void)process_control_kill_appWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"handle"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != process_control_free) {
|
||||
replyHandler(nil, @"Invalid process control handle");
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
ProcessControlHandle* process_control = clientHandleObj.handle;
|
||||
|
||||
uint64_t pid = [body[@"pid"] unsignedLongLongValue];
|
||||
if(pid == 0) {
|
||||
replyHandler(nil, @"Invalid pid");
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceFfiError* err = process_control_kill_app(process_control, pid);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@(YES), nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeRSD.m
|
||||
// StikDebug
|
||||
//
|
||||
// Created by s s on 2025/12/13.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
static NSDictionary *RsdServiceToDictionary(const CRsdService *service) {
|
||||
if (!service) {
|
||||
return @{};
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *features = [[NSMutableArray alloc] initWithCapacity:service->features_count];
|
||||
for (size_t i = 0; i < service->features_count; ++i) {
|
||||
if (service->features[i]) {
|
||||
[features addObject:@(service->features[i])];
|
||||
}
|
||||
}
|
||||
|
||||
NSString *name = service->name ? @(service->name) : @"";
|
||||
NSString *entitlement = service->entitlement ? @(service->entitlement) : @"";
|
||||
|
||||
return @{
|
||||
@"name": name,
|
||||
@"entitlement": entitlement,
|
||||
@"port": @(service->port),
|
||||
@"uses_remote_xpc": @(service->uses_remote_xpc),
|
||||
@"features_count": @(service->features_count),
|
||||
@"features": features,
|
||||
@"service_version": @(service->service_version)
|
||||
};
|
||||
}
|
||||
|
||||
@implementation IDeviceJSBridge (RSD)
|
||||
|
||||
- (void)rsd_handshake_newWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int socketId = [body[@"socket"] intValue];
|
||||
if (!handles[@(socketId)] || handles[@(socketId)].freeFunc != adapter_stream_close) {
|
||||
replyHandler(nil, @"Invalid socket handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle *socketHandleObj = handles[@(socketId)];
|
||||
ReadWriteOpaque *socket = socketHandleObj.handle;
|
||||
|
||||
RsdHandshakeHandle *handshake = NULL;
|
||||
IdeviceFfiError *err = rsd_handshake_new(socket, &handshake);
|
||||
[handles removeObjectForKey:@(socketId)];
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:handshake freeFunc:(void *)rsd_handshake_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
- (void)rsd_get_protocol_versionWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid RSD handshake handle");
|
||||
return;
|
||||
}
|
||||
RsdHandshakeHandle *handshake = handles[@(handleId)].handle;
|
||||
|
||||
size_t version = 0;
|
||||
IdeviceFfiError *err = rsd_get_protocol_version(handshake, &version);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@(version), nil);
|
||||
}
|
||||
|
||||
- (void)rsd_get_uuidWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid RSD handshake handle");
|
||||
return;
|
||||
}
|
||||
RsdHandshakeHandle *handshake = handles[@(handleId)].handle;
|
||||
|
||||
char *uuid = NULL;
|
||||
IdeviceFfiError *err = rsd_get_uuid(handshake, &uuid);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *uuidStr = uuid ? @(uuid) : nil;
|
||||
if (uuid) {
|
||||
rsd_free_string(uuid);
|
||||
}
|
||||
|
||||
replyHandler(uuidStr, nil);
|
||||
}
|
||||
|
||||
- (void)rsd_get_servicesWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid RSD handshake handle");
|
||||
return;
|
||||
}
|
||||
RsdHandshakeHandle *handshake = handles[@(handleId)].handle;
|
||||
|
||||
CRsdServiceArray *services = NULL;
|
||||
IdeviceFfiError *err = rsd_get_services(handshake, &services);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableArray *ans = [[NSMutableArray alloc] init];
|
||||
if (services && services->services) {
|
||||
for (size_t i = 0; i < services->count; ++i) {
|
||||
[ans addObject:RsdServiceToDictionary(&services->services[i])];
|
||||
}
|
||||
}
|
||||
|
||||
if (services) {
|
||||
rsd_free_services(services);
|
||||
}
|
||||
replyHandler(ans, nil);
|
||||
}
|
||||
|
||||
- (void)rsd_service_availableWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid RSD handshake handle");
|
||||
return;
|
||||
}
|
||||
RsdHandshakeHandle *handshake = handles[@(handleId)].handle;
|
||||
|
||||
NSString *serviceName = body[@"service_name"];
|
||||
if (![serviceName isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid service name");
|
||||
return;
|
||||
}
|
||||
|
||||
bool available = false;
|
||||
IdeviceFfiError *err = rsd_service_available(handshake, [serviceName UTF8String], &available);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
replyHandler(@(available), nil);
|
||||
}
|
||||
|
||||
- (void)rsd_get_service_infoWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
int handleId = [body[@"handle"] intValue];
|
||||
if (!handles[@(handleId)] || handles[@(handleId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid RSD handshake handle");
|
||||
return;
|
||||
}
|
||||
RsdHandshakeHandle *handshake = handles[@(handleId)].handle;
|
||||
|
||||
NSString *serviceName = body[@"service_name"];
|
||||
if (![serviceName isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid service name");
|
||||
return;
|
||||
}
|
||||
|
||||
CRsdService *serviceInfo = NULL;
|
||||
IdeviceFfiError *err = rsd_get_service_info(handshake, [serviceName UTF8String], &serviceInfo);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *ans = RsdServiceToDictionary(serviceInfo);
|
||||
if (serviceInfo) {
|
||||
rsd_free_service(serviceInfo);
|
||||
}
|
||||
|
||||
replyHandler(ans, nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeRemoteServer.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (RemoteServer)
|
||||
|
||||
- (void)remote_server_connect_rsdWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"adapter"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != adapter_free) {
|
||||
replyHandler(nil, @"Invalid adapter handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
AdapterHandle* adapter = clientHandleObj.handle;
|
||||
|
||||
int handshakeId = [body[@"handshake"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != rsd_handshake_free) {
|
||||
replyHandler(nil, @"Invalid handshake handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* handshakeHandleObj = handles[@(handshakeId)];
|
||||
RsdHandshakeHandle* handshake = handshakeHandleObj.handle;
|
||||
|
||||
RemoteServerHandle *remote_server = NULL;
|
||||
IdeviceFfiError* err = remote_server_connect_rsd(adapter, handshake, &remote_server);
|
||||
[handles removeObjectForKey:@(clientId)];
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
int handleId = [self registerIdeviceHandle:remote_server freeFunc:(void*)remote_server_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
//
|
||||
// IDeviceJSBridgeSBServices.m
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/25.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import "JSSupport.h"
|
||||
|
||||
@implementation IDeviceJSBridge (SBServices)
|
||||
|
||||
- (void)springboard_services_connectWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
NSError* heartbeatErr = nil;
|
||||
[JITEnableContext.shared ensureHeartbeatWithError:&heartbeatErr];
|
||||
if(heartbeatErr) {
|
||||
replyHandler(nil, heartbeatErr.localizedDescription);
|
||||
return;
|
||||
}
|
||||
|
||||
IdeviceProviderHandle* provider = [JITEnableContext.shared getTcpProviderHandle];
|
||||
|
||||
SpringBoardServicesClientHandle *sb_services = NULL;
|
||||
IdeviceFfiError* err = springboard_services_connect(provider, &sb_services);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
int handleId = [self registerIdeviceHandle:sb_services freeFunc:(void*)springboard_services_free];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
|
||||
- (void)springboard_services_get_iconWithBody:(NSDictionary *)body replyHandler:(nonnull void (^)(id _Nullable, NSString * _Nullable))replyHandler {
|
||||
|
||||
int clientId = [body[@"client"] intValue];
|
||||
if(!handles[@(clientId)] || handles[@(clientId)].freeFunc != springboard_services_free) {
|
||||
replyHandler(nil, @"Invalid springboard services client handle");
|
||||
return;
|
||||
}
|
||||
IDeviceHandle* clientHandleObj = handles[@(clientId)];
|
||||
SpringBoardServicesClientHandle* client = clientHandleObj.handle;
|
||||
NSString* bundleID = body[@"bundle_id"];
|
||||
if(![bundleID isKindOfClass:NSString.class]) {
|
||||
replyHandler(nil, @"Invalid bundle id");
|
||||
return;
|
||||
}
|
||||
|
||||
void *pngData = NULL;
|
||||
size_t data_len = 0;
|
||||
IdeviceFfiError* err = springboard_services_get_icon(client, [bundleID UTF8String], &pngData, &data_len);
|
||||
if (err) {
|
||||
replyHandler(nil, [self errFreeFromIdeviceFfiError:err]);
|
||||
return;
|
||||
}
|
||||
|
||||
if(data_len == 0) {
|
||||
replyHandler(@(-1), nil);
|
||||
}
|
||||
|
||||
NSData* data = [NSData dataWithBytes:pngData length:data_len];
|
||||
free(pngData);
|
||||
int handleId = [self registerNSData:data];
|
||||
replyHandler(@(handleId), nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// JSSupport.h
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/24.
|
||||
//
|
||||
#import "../../idevice/JITEnableContext.h"
|
||||
#import "../../idevice/idevice.h"
|
||||
@import JavaScriptCore;
|
||||
|
||||
@interface NSInvocation(MCUtilities)
|
||||
-(void)invokeOnMainThreadWaitUntilDone:(BOOL)wait;
|
||||
+(NSInvocation*)invocationWithTarget:(id)target
|
||||
selector:(SEL)aSelector
|
||||
retainArguments:(BOOL)retainArguments, ...;
|
||||
@end
|
||||
|
||||
@interface IDeviceHandle : NSObject
|
||||
@property void* handle;
|
||||
@property void* freeFunc;
|
||||
@end
|
||||
|
||||
@interface IDeviceJSBridge : NSObject {
|
||||
NSMutableDictionary<NSNumber*, IDeviceHandle*>* handles;
|
||||
NSMutableDictionary<NSNumber*, NSData*>* dataPool;
|
||||
NSArray<NSString*>* allowedFunctions;
|
||||
JSContext* context;
|
||||
}
|
||||
- (instancetype)initWithContext:(JSContext*)context allowedFunctions:(NSArray<NSString*>*)allowedFunctions;
|
||||
- (void)didReceiveScriptMessage:(NSDictionary *)message resolve:(JSValue*)resolveFunc reject:(JSValue*)rejectFunc;
|
||||
- (void)cleanUp;
|
||||
- (NSString*)errFreeFromIdeviceFfiError:(IdeviceFfiError*)err;
|
||||
- (int)registerIdeviceHandle:(void*)handle freeFunc:(void*)freeFunc;
|
||||
- (BOOL)freeIdeviceHandle:(int)handleId;
|
||||
- (int)registerNSData:(NSData*)data;
|
||||
- (bool)freeNSData:(int)handleId;
|
||||
@end
|
||||
|
||||
|
||||
NSDictionary *dictionaryFromPlist(plist_t plist, NSError **error);
|
||||
NSData *plistDataFromDictionary(NSDictionary *dictionary, NSError **error);
|
||||
const char** cstrArrFromNSArray(NSArray* arr, int* validCount);
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// NSInvocation.m
|
||||
// StikJIT
|
||||
// Source: http://blog.jayway.com/2010/03/30/performing-any-selector-on-the-main-thread/
|
||||
// Created by s s on 2025/4/24.
|
||||
//
|
||||
@import Foundation;
|
||||
|
||||
@implementation NSInvocation(MCUtilities)
|
||||
-(void)invokeOnMainThreadWaitUntilDone:(BOOL)wait
|
||||
{
|
||||
[self performSelectorOnMainThread:@selector(invoke)
|
||||
withObject:nil
|
||||
waitUntilDone:wait];
|
||||
}
|
||||
+(NSInvocation*)invocationWithTarget:(id)target
|
||||
selector:(SEL)aSelector
|
||||
retainArguments:(BOOL)retainArguments, ...
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, retainArguments);
|
||||
char* args = (char*)ap;
|
||||
NSMethodSignature* signature = [target methodSignatureForSelector:aSelector];
|
||||
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||||
if (retainArguments) {
|
||||
[invocation retainArguments];
|
||||
}
|
||||
[invocation setTarget:target];
|
||||
[invocation setSelector:aSelector];
|
||||
for (int index = 2; index < [signature numberOfArguments]; index++) {
|
||||
const char *type = [signature getArgumentTypeAtIndex:index];
|
||||
NSUInteger size, align;
|
||||
NSGetSizeAndAlignment(type, &size, &align);
|
||||
NSUInteger mod = (NSUInteger)args % align;
|
||||
if (mod != 0) {
|
||||
args += (align - mod);
|
||||
}
|
||||
[invocation setArgument:args atIndex:index];
|
||||
args += size;
|
||||
}
|
||||
va_end(ap);
|
||||
return invocation;
|
||||
}
|
||||
@end
|
||||
|
|
@ -1,515 +0,0 @@
|
|||
//
|
||||
// idevice.js
|
||||
// StikJIT
|
||||
//
|
||||
// Created by s s on 2025/4/24.
|
||||
//
|
||||
|
||||
async function core_device_proxy_connect() {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "core_device_proxy_connect"
|
||||
});
|
||||
}
|
||||
|
||||
async function core_device_proxy_get_server_rsd_port(core_device_handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "core_device_proxy_get_server_rsd_port",
|
||||
"handle": core_device_handle
|
||||
});
|
||||
}
|
||||
|
||||
async function core_device_proxy_create_tcp_adapter(core_device_handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "core_device_proxy_create_tcp_adapter",
|
||||
"handle": core_device_handle
|
||||
});
|
||||
}
|
||||
|
||||
async function adapter_connect(adapter, port) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "adapter_connect",
|
||||
"adapter": adapter,
|
||||
"port": port
|
||||
});
|
||||
}
|
||||
|
||||
async function rsd_handshake_new(socket) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "rsd_handshake_new",
|
||||
"socket": socket
|
||||
});
|
||||
}
|
||||
|
||||
async function rsd_get_protocol_version(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "rsd_get_protocol_version",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function rsd_get_uuid(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "rsd_get_uuid",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function rsd_get_services(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "rsd_get_services",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function rsd_service_available(handle, service_name) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "rsd_service_available",
|
||||
"handle": handle,
|
||||
"service_name": service_name
|
||||
});
|
||||
}
|
||||
|
||||
async function rsd_get_service_info(handle, service_name) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "rsd_get_service_info",
|
||||
"handle": handle,
|
||||
"service_name": service_name
|
||||
});
|
||||
}
|
||||
|
||||
async function remote_server_connect_rsd(adapter, handshake) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "remote_server_connect_rsd",
|
||||
"adapter": adapter,
|
||||
"handshake": handshake
|
||||
});
|
||||
}
|
||||
|
||||
async function process_control_new(server) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "process_control_new",
|
||||
"server": server
|
||||
});
|
||||
}
|
||||
|
||||
async function process_control_launch_app(handle, bundle_id, env_vars, arguments, start_suspended, kill_existing) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "process_control_launch_app",
|
||||
"handle": handle,
|
||||
"bundle_id": bundle_id,
|
||||
"env_vars": env_vars,
|
||||
"arguments": arguments,
|
||||
"start_suspended": start_suspended,
|
||||
"kill_existing": kill_existing
|
||||
});
|
||||
}
|
||||
|
||||
async function process_control_disable_memory_limit(handle, pid) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "process_control_disable_memory_limit",
|
||||
"handle": handle,
|
||||
"pid": pid
|
||||
});
|
||||
}
|
||||
|
||||
async function process_control_kill_app(handle, pid) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "process_control_kill_app",
|
||||
"handle": handle,
|
||||
"pid": pid
|
||||
});
|
||||
}
|
||||
|
||||
async function debug_proxy_connect_rsd(adapter, handshake) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "debug_proxy_connect_rsd",
|
||||
"adapter": adapter,
|
||||
"handshake": handshake
|
||||
});
|
||||
}
|
||||
|
||||
async function debug_proxy_send_command(handle, command) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "debug_proxy_send_command",
|
||||
"handle": handle,
|
||||
"debug_command": command
|
||||
});
|
||||
}
|
||||
|
||||
async function springboard_services_connect() {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "springboard_services_connect"
|
||||
});
|
||||
}
|
||||
|
||||
async function springboard_services_get_icon(client, bundle_id) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "springboard_services_get_icon",
|
||||
"client": client,
|
||||
"bundle_id": bundle_id
|
||||
});
|
||||
}
|
||||
|
||||
async function nsdata_read(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "nsdata_read",
|
||||
"handle": handle,
|
||||
});
|
||||
}
|
||||
|
||||
async function nsdata_read_range(handle, begin, end) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "nsdata_read_range",
|
||||
"handle": handle,
|
||||
"begin": begin,
|
||||
"end": end
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_client_connect() {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_client_connect"
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_list_directory(handle, path) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_list_directory",
|
||||
"handle": handle,
|
||||
"path": path
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_make_directory(handle, path) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_make_directory",
|
||||
"handle": handle,
|
||||
"path": path
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_remove_path(handle, path) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_remove_path",
|
||||
"handle": handle,
|
||||
"path": path
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_remove_path_and_contents(handle, path) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_remove_path_and_contents",
|
||||
"handle": handle,
|
||||
"path": path
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_rename_path(handle, source, target) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_rename_path",
|
||||
"handle": handle,
|
||||
"source": source,
|
||||
"target": target
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_get_file_info(handle, path) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_get_file_info",
|
||||
"handle": handle,
|
||||
"path": path
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_get_device_info(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_get_device_info",
|
||||
"handle": handle,
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_file_open(handle, path, mode) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_file_open",
|
||||
"handle": handle,
|
||||
"path": path,
|
||||
"mode": mode
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_file_close(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_file_close",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_file_read(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_file_read",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function afc_file_write(handle, data_handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "afc_file_write",
|
||||
"handle": handle,
|
||||
"data_handle": data_handle
|
||||
});
|
||||
}
|
||||
|
||||
// for installation_proxy_callback
|
||||
|
||||
var handle_installation_proxy_callback_dict = {"max": 0}
|
||||
function handle_installation_proxy_js_callback(id, progress) {
|
||||
let func = handle_installation_proxy_callback_dict[id];
|
||||
if(func) {
|
||||
func(progress)
|
||||
}
|
||||
}
|
||||
|
||||
function installation_proxy_js_callback_register(callback) {
|
||||
let cur = handle_installation_proxy_callback_dict['max']
|
||||
handle_installation_proxy_callback_dict['max']++
|
||||
handle_installation_proxy_callback_dict[cur] = callback
|
||||
return cur;
|
||||
}
|
||||
|
||||
function installation_proxy_js_callback_unregister(id) {
|
||||
delete handle_installation_proxy_callback_dict[id]
|
||||
}
|
||||
|
||||
async function installation_proxy_connect() {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "installation_proxy_connect"
|
||||
});
|
||||
}
|
||||
|
||||
async function installation_proxy_get_apps(client, applicationType, bundleIdentifiers) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "installation_proxy_get_apps",
|
||||
"client": client,
|
||||
"application_type": applicationType,
|
||||
"bundle_identifiers": bundleIdentifiers
|
||||
});
|
||||
}
|
||||
|
||||
async function installation_proxy_browse(client, options) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "installation_proxy_browse",
|
||||
"client": client,
|
||||
"options": options
|
||||
});
|
||||
}
|
||||
|
||||
async function installation_proxy_install(client, package_path, options, callback) {
|
||||
if(callback) {
|
||||
let id = installation_proxy_js_callback_register(callback)
|
||||
let ans = await __postIdeviceMessage({
|
||||
"command": "installation_proxy_install",
|
||||
"client": client,
|
||||
"package_path": package_path,
|
||||
"options": options,
|
||||
"callback_id": id
|
||||
});
|
||||
installation_proxy_js_callback_unregister(id)
|
||||
return ans;
|
||||
} else {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "installation_proxy_install",
|
||||
"client": client,
|
||||
"package_path": package_path,
|
||||
"options": options,
|
||||
"callback_id": -1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function installation_proxy_upgrade(client, package_path, options, callback) {
|
||||
if(callback) {
|
||||
let id = installation_proxy_js_callback_register(callback)
|
||||
let ans = await __postIdeviceMessage({
|
||||
"command": "installation_proxy_upgrade",
|
||||
"client": client,
|
||||
"package_path": package_path,
|
||||
"options": options,
|
||||
"callback_id": id
|
||||
});
|
||||
installation_proxy_js_callback_unregister(id)
|
||||
return ans;
|
||||
} else {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "installation_proxy_install",
|
||||
"client": client,
|
||||
"package_path": package_path,
|
||||
"options": options,
|
||||
"callback_id": -1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function installation_proxy_uninstall(client, bundle_id, options, callback) {
|
||||
if(callback) {
|
||||
let id = installation_proxy_js_callback_register(callback)
|
||||
let ans = await __postIdeviceMessage({
|
||||
"command": "installation_proxy_uninstall",
|
||||
"client": client,
|
||||
"bundle_id": bundle_id,
|
||||
"options": options,
|
||||
"callback_id": id
|
||||
});
|
||||
installation_proxy_js_callback_unregister(id)
|
||||
return ans;
|
||||
} else {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "installation_proxy_install",
|
||||
"client": client,
|
||||
"bundle_id": bundle_id,
|
||||
"options": options,
|
||||
"callback_id": -1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function amfi_connect() {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "amfi_connect"
|
||||
});
|
||||
}
|
||||
|
||||
async function amfi_reveal_developer_mode_option_in_ui(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "amfi_reveal_developer_mode_option_in_ui",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function amfi_enable_developer_mode(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "amfi_enable_developer_mode",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function amfi_accept_developer_mode(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "amfi_accept_developer_mode",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function misagent_connect() {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "misagent_connect"
|
||||
});
|
||||
}
|
||||
|
||||
async function misagent_install(handle, data_handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "misagent_install",
|
||||
"handle": handle,
|
||||
"data_handle": data_handle
|
||||
});
|
||||
}
|
||||
|
||||
async function misagent_remove(handle, profile_id) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "misagent_remove",
|
||||
"handle": handle,
|
||||
"profile_id": profile_id
|
||||
});
|
||||
}
|
||||
|
||||
async function misagent_copy_all(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "misagent_copy_all",
|
||||
"handle": handle,
|
||||
});
|
||||
}
|
||||
|
||||
async function location_simulation_new(server) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "location_simulation_new",
|
||||
"server": server
|
||||
});
|
||||
}
|
||||
|
||||
async function location_simulation_clear(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "location_simulation_clear",
|
||||
"handle": handle
|
||||
});
|
||||
}
|
||||
|
||||
async function location_simulation_set(handle, latitude, longitude) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "location_simulation_set",
|
||||
"handle": handle,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
});
|
||||
}
|
||||
|
||||
// data related, not idevice
|
||||
|
||||
async function nsdata_get_size(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "nsdata_get_size",
|
||||
"handle": handle,
|
||||
});
|
||||
}
|
||||
|
||||
async function nsdata_free(handle) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "nsdata_free",
|
||||
"handle": handle,
|
||||
});
|
||||
}
|
||||
|
||||
async function nsdata_create(base64Data) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "nsdata_create",
|
||||
"base64Data": base64Data,
|
||||
});
|
||||
}
|
||||
|
||||
async function local_file_open(path, mode) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "local_file_open",
|
||||
"path": path,
|
||||
"mode": mode
|
||||
});
|
||||
}
|
||||
|
||||
async function local_file_close(file) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "local_file_close",
|
||||
"file": file,
|
||||
});
|
||||
}
|
||||
|
||||
async function local_file_get_size(file) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "local_file_get_size",
|
||||
"file": file,
|
||||
});
|
||||
}
|
||||
|
||||
async function local_file_read_chunk(file, offset, length) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "local_file_read_chunk",
|
||||
"file": file,
|
||||
"offset": offset,
|
||||
"length": length
|
||||
});
|
||||
}
|
||||
|
||||
async function local_file_write_chunk(file, data, offset) {
|
||||
return await __postIdeviceMessage({
|
||||
"command": "local_file_write_chunk",
|
||||
"file": file,
|
||||
"offset": offset,
|
||||
"data": data
|
||||
});
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import SwiftUI
|
||||
import CodeEditorView
|
||||
import LanguageSupport
|
||||
|
||||
struct MiniToolEditorView: View {
|
||||
let tool: MiniToolBundle
|
||||
|
||||
private enum Target: String, CaseIterable, Identifiable {
|
||||
case indexHTML = "index.html"
|
||||
case backgroundJS = "background.js"
|
||||
|
||||
var id: String { rawValue }
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .indexHTML: return "index.html"
|
||||
case .backgroundJS: return "background.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var htmlContent: String = ""
|
||||
@State private var backgroundContent: String = ""
|
||||
@State private var selectedTarget: Target = .indexHTML
|
||||
@State private var position: CodeEditor.Position = .init()
|
||||
@State private var messages: Set<TextLocated<Message>> = []
|
||||
|
||||
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.themeExpansionManager) private var themeExpansion
|
||||
|
||||
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
|
||||
private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) }
|
||||
private var editorTheme: Theme {
|
||||
colorScheme == .dark ? Theme.defaultDark : Theme.defaultLight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ThemedBackground(style: backgroundStyle)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Picker("File", selection: $selectedTarget) {
|
||||
ForEach(Target.allCases) { target in
|
||||
Text(target.displayName).tag(target)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
CodeEditor(
|
||||
text: binding(for: selectedTarget),
|
||||
position: $position,
|
||||
messages: $messages,
|
||||
language: .swift()
|
||||
)
|
||||
.font(.system(.footnote, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.environment(\.codeEditorTheme, editorTheme)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.navigationTitle(tool.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear(perform: load)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(preferredScheme)
|
||||
.tint(Color.white)
|
||||
.toolbar(.hidden, for: .tabBar)
|
||||
}
|
||||
|
||||
private func binding(for target: Target) -> Binding<String> {
|
||||
switch target {
|
||||
case .indexHTML: return $htmlContent
|
||||
case .backgroundJS: return $backgroundContent
|
||||
}
|
||||
}
|
||||
|
||||
private func load() {
|
||||
htmlContent = (try? String(contentsOf: tool.indexURL)) ?? ""
|
||||
backgroundContent = (try? String(contentsOf: tool.backgroundURL)) ?? ""
|
||||
}
|
||||
|
||||
private func save() {
|
||||
try? htmlContent.write(to: tool.indexURL, atomically: true, encoding: .utf8)
|
||||
try? backgroundContent.write(to: tool.backgroundURL, atomically: true, encoding: .utf8)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,440 +0,0 @@
|
|||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import UIKit
|
||||
|
||||
class ToolCombo : Hashable, ObservableObject {
|
||||
static func == (lhs: ToolCombo, rhs: ToolCombo) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self))
|
||||
}
|
||||
|
||||
init(tool: MiniToolBundle, info: ToolInfo) {
|
||||
self.tool = tool
|
||||
self.info = info
|
||||
}
|
||||
|
||||
init() {
|
||||
self.tool = nil
|
||||
self.info = nil
|
||||
}
|
||||
|
||||
@Published var tool: MiniToolBundle?
|
||||
@Published var info: ToolInfo?
|
||||
}
|
||||
|
||||
struct MiniToolListView: View {
|
||||
@StateObject private var store = MiniToolStore()
|
||||
@State private var searchText = ""
|
||||
@State private var showImporter = false
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var pendingDelete: MiniToolBundle?
|
||||
@State private var alertVisible = false
|
||||
@State private var alertTitle = ""
|
||||
@State private var alertMessage = ""
|
||||
@State private var showInfoSheet = false
|
||||
@State private var navigationToolCombo: ToolCombo?
|
||||
@StateObject private var pendingToolCombo: ToolCombo = ToolCombo()
|
||||
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
||||
@Environment(\.themeExpansionManager) private var themeExpansion
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
|
||||
private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) }
|
||||
|
||||
private var filteredTools: [MiniToolBundle] {
|
||||
guard !searchText.isEmpty else { return store.tools }
|
||||
return store.tools.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
ThemedBackground(style: backgroundStyle)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
headerCard
|
||||
|
||||
if filteredTools.isEmpty {
|
||||
emptyCard
|
||||
} else {
|
||||
ForEach(filteredTools) { tool in
|
||||
toolRow(tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 30)
|
||||
}
|
||||
|
||||
if store.isBusy {
|
||||
Color.black.opacity(0.35).ignoresSafeArea()
|
||||
ProgressView("Working…")
|
||||
.padding(16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
|
||||
}
|
||||
|
||||
if alertVisible {
|
||||
CustomErrorView(
|
||||
title: alertTitle,
|
||||
message: alertMessage,
|
||||
onDismiss: { alertVisible = false },
|
||||
messageType: .error
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Mini Tools")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button { showImporter = true } label: {
|
||||
Label("Import", systemImage: "tray.and.arrow.down")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { store.refresh() }
|
||||
.onChange(of: store.lastError) { _, message in
|
||||
guard let message else { return }
|
||||
presentError(title: "Mini Tool", message: message)
|
||||
store.lastError = nil
|
||||
}
|
||||
.alert("Delete Mini Tool?", isPresented: $showDeleteConfirmation, presenting: pendingDelete) { tool in
|
||||
Button("Delete", role: .destructive) { delete(tool) }
|
||||
Button("Cancel", role: .cancel) { pendingDelete = nil }
|
||||
} message: { tool in
|
||||
Text("Delete \(tool.name)? This removes its files permanently.")
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $showImporter,
|
||||
allowedContentTypes: [UTType("com.stik.StikJIT.stiktool") ?? .data],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
switch result {
|
||||
case .success(let urls):
|
||||
guard let url = urls.first else { return }
|
||||
store.importTool(from: url)
|
||||
case .failure(let error):
|
||||
presentError(title: "Import Failed", message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $navigationToolCombo) { combo in
|
||||
MiniToolRunnerView(tool: combo.tool!, toolInfo: combo.info!)
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(preferredScheme)
|
||||
.sheet(isPresented: $showInfoSheet) {
|
||||
toolInfoSheet
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cards
|
||||
|
||||
private var headerCard: some View {
|
||||
VStack(spacing: 12) {
|
||||
TextField("Search tools…", text: $searchText)
|
||||
.padding(12)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1)
|
||||
)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
WideGlassyButton(title: "Import", systemImage: "tray.and.arrow.down") {
|
||||
showImporter = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(glassyBackground)
|
||||
}
|
||||
|
||||
private func toolRow(_ tool: MiniToolBundle) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
handleRun(tool)
|
||||
} label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "shippingbox.fill")
|
||||
.foregroundColor(.blue)
|
||||
.imageScale(.large)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(tool.name)
|
||||
.font(.body.weight(.medium))
|
||||
.lineLimit(1)
|
||||
Text(tool.url.lastPathComponent)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
NavigationLink {
|
||||
MiniToolEditorView(tool: tool)
|
||||
} label: {
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
Button(role: .destructive) {
|
||||
pendingDelete = tool
|
||||
showDeleteConfirmation = true
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
}
|
||||
.padding(20)
|
||||
.background(glassyBackground)
|
||||
.contextMenu {
|
||||
Button { copy(tool.url.lastPathComponent) } label: {
|
||||
Label("Copy Name", systemImage: "doc.on.doc")
|
||||
}
|
||||
Button { copy(tool.url.path) } label: {
|
||||
Label("Copy Path", systemImage: "folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyCard: some View {
|
||||
VStack(spacing: 6) {
|
||||
Label("No mini tools found", systemImage: "shippingbox")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text("Tap New or Import to add a tool.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(40)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(glassyBackground)
|
||||
}
|
||||
|
||||
private var glassyBackground: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(.ultraThinMaterial)
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: overlayColors()),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.opacity(0.32)
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.15), lineWidth: 1)
|
||||
}
|
||||
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4)
|
||||
}
|
||||
|
||||
private func overlayColors() -> [Color] {
|
||||
let colors: [Color]
|
||||
switch backgroundStyle {
|
||||
case .staticGradient(let palette):
|
||||
colors = palette
|
||||
case .animatedGradient(let palette, _):
|
||||
colors = palette
|
||||
case .blobs(_, let background):
|
||||
colors = background
|
||||
case .particles(let particle, let background):
|
||||
colors = background.isEmpty ? [particle, particle.opacity(0.4)] : background
|
||||
case .customGradient(let palette):
|
||||
colors = palette
|
||||
case .adaptiveGradient(let light, let dark):
|
||||
colors = colorScheme == .dark ? dark : light
|
||||
}
|
||||
if colors.count >= 2 { return colors }
|
||||
if let first = colors.first { return [first, first.opacity(0.6)] }
|
||||
return [Color.blue, Color.purple]
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func delete(_ tool: MiniToolBundle) {
|
||||
store.delete(tool)
|
||||
if let error = store.lastError {
|
||||
presentError(title: "Delete Failed", message: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentError(title: String, message: String) {
|
||||
alertTitle = title
|
||||
alertMessage = message
|
||||
alertVisible = true
|
||||
}
|
||||
|
||||
private func copy(_ text: String) {
|
||||
UIPasteboard.general.string = text
|
||||
}
|
||||
|
||||
private func handleRun(_ tool: MiniToolBundle) {
|
||||
do {
|
||||
guard let info = try loadToolInfo(for: tool) else {
|
||||
presentError(title: "Mini Tool", message: "toolInfo.json is missing or unreadable for this tool.")
|
||||
return
|
||||
}
|
||||
if try isTrusted(tool) {
|
||||
navigationToolCombo = ToolCombo(tool: tool, info: info)
|
||||
return
|
||||
}
|
||||
|
||||
pendingToolCombo.info = info
|
||||
pendingToolCombo.tool = tool
|
||||
print("set pendingToolInfo")
|
||||
showInfoSheet = true
|
||||
print("pendingToolInfo = true")
|
||||
} catch {
|
||||
presentError(title: "Mini Tool", message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadToolInfo(for tool: MiniToolBundle) throws -> ToolInfo? {
|
||||
let infoURL = tool.url.appendingPathComponent("toolInfo.json")
|
||||
guard FileManager.default.fileExists(atPath: infoURL.path) else { return nil }
|
||||
let data = try Data(contentsOf: infoURL)
|
||||
return try JSONDecoder().decode(ToolInfo.self, from: data)
|
||||
}
|
||||
|
||||
private func isTrusted(_ tool: MiniToolBundle) throws -> Bool {
|
||||
guard let uuid = UIDevice.current.identifierForVendor?.uuidString else { return false }
|
||||
let checkURL = tool.url.appendingPathComponent("check")
|
||||
guard FileManager.default.fileExists(atPath: checkURL.path) else { return false }
|
||||
let contents = try String(contentsOf: checkURL, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return contents == uuid
|
||||
}
|
||||
|
||||
private func authorizeAndRun() {
|
||||
guard let tool = pendingToolCombo.tool else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try writeCheck(for: tool)
|
||||
navigationToolCombo = pendingToolCombo
|
||||
} catch {
|
||||
presentError(title: "Mini Tool", message: error.localizedDescription)
|
||||
}
|
||||
resetPending()
|
||||
}
|
||||
|
||||
private func writeCheck(for tool: MiniToolBundle) throws {
|
||||
guard let uuid = UIDevice.current.identifierForVendor?.uuidString else {
|
||||
throw NSError(domain: "MiniTool", code: -1, userInfo: [NSLocalizedDescriptionKey: "identifierForVendor is unavailable."])
|
||||
}
|
||||
let checkURL = tool.url.appendingPathComponent("check")
|
||||
try uuid.write(to: checkURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func resetPending() {
|
||||
showInfoSheet = false
|
||||
// pendingTool = MiniToolBundle()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var toolInfoSheet: some View {
|
||||
if let tool = pendingToolCombo.tool, let info = pendingToolCombo.info {
|
||||
ToolInfoSheet(tool: tool, info: info) {
|
||||
authorizeAndRun()
|
||||
} onCancel: {
|
||||
resetPending()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ToolInfoSheet: View {
|
||||
let tool: MiniToolBundle
|
||||
let info: ToolInfo
|
||||
let onAllow: () -> Void
|
||||
let onCancel: () -> Void
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(tool.name)
|
||||
.font(.title2.weight(.semibold))
|
||||
Text(info.desc)
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Author", systemImage: "person")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(info.author)
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
if let capabilities = info.capabilities {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Capabilities", systemImage: "checkmark.shield")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
CapabilityRow(title: "Internet Access", enabled: capabilities.internetAccess ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
if let functions = info.requiredIDeviceFunctions, !functions.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Required iDevice Functions", systemImage: "bolt.horizontal.circle")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
ForEach(functions, id: \.self) { function in
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundColor(.blue)
|
||||
Text(function)
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(20)
|
||||
}
|
||||
.navigationTitle("Allow Tool?")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { onCancel() }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Allow & Run") { onAllow() }
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CapabilityRow: View {
|
||||
let title: String
|
||||
let enabled: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: enabled ? "checkmark.circle.fill" : "xmark.circle")
|
||||
.foregroundColor(enabled ? .green : .red)
|
||||
Text(title)
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
struct ToolInfo: Decodable {
|
||||
struct Capabilities: Decodable {
|
||||
let internetAccess: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case internetAccess = "internet_access"
|
||||
}
|
||||
}
|
||||
|
||||
let author: String
|
||||
let desc: String
|
||||
let capabilities: Capabilities?
|
||||
let requiredIDeviceFunctions: [String]?
|
||||
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case author
|
||||
case desc
|
||||
case capabilities
|
||||
case requiredIDeviceFunctions = "required_idevice_functions"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MiniToolBundle: ObservableObject, Identifiable {
|
||||
static func == (lhs: MiniToolBundle, rhs: MiniToolBundle) -> Bool {
|
||||
return lhs.name == rhs.name
|
||||
}
|
||||
|
||||
@Published var url: URL
|
||||
|
||||
init(url: URL) {
|
||||
self.url = url
|
||||
}
|
||||
|
||||
var id: String { url.lastPathComponent }
|
||||
var name: String { url.deletingPathExtension().lastPathComponent }
|
||||
var indexURL: URL { url.appendingPathComponent("index.html") }
|
||||
var backgroundURL: URL { url.appendingPathComponent("background.js") }
|
||||
|
||||
var isValid: Bool {
|
||||
let fm = FileManager.default
|
||||
return fm.fileExists(atPath: indexURL.path) && fm.fileExists(atPath: backgroundURL.path)
|
||||
}
|
||||
|
||||
func getHostName() -> String {
|
||||
let d = name.data(using: .utf8)
|
||||
let b = d!.base64EncodedString().replacingOccurrences(of:"+", with:"").replacingOccurrences(of:"/", with:"").replacingOccurrences(of:"=", with:"")
|
||||
|
||||
return "\(b).stiktool"
|
||||
}
|
||||
}
|
||||
|
||||
final class MiniToolStore: ObservableObject {
|
||||
@Published private(set) var tools: [MiniToolBundle] = []
|
||||
@Published var isBusy: Bool = false
|
||||
@Published var lastError: String?
|
||||
|
||||
func refresh() {
|
||||
let directory = toolsDirectory()
|
||||
let contents = (try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)) ?? []
|
||||
let bundles = contents
|
||||
.filter { url in
|
||||
let ext = url.pathExtension.lowercased()
|
||||
return ext == "stiktool" || url.hasDirectoryPath
|
||||
}
|
||||
.map { MiniToolBundle(url: $0) }
|
||||
.filter { $0.isValid }
|
||||
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||
DispatchQueue.main.async {
|
||||
self.tools = bundles
|
||||
}
|
||||
}
|
||||
|
||||
func delete(_ tool: MiniToolBundle) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: tool.url)
|
||||
refresh()
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
func importTool(from externalURL: URL) {
|
||||
isBusy = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
defer { DispatchQueue.main.async { self.isBusy = false } }
|
||||
do {
|
||||
let accessing = externalURL.startAccessingSecurityScopedResource()
|
||||
defer { if accessing { externalURL.stopAccessingSecurityScopedResource() } }
|
||||
let destination = self.toolsDirectory().appendingPathComponent(externalURL.lastPathComponent)
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: destination.path) {
|
||||
try fm.removeItem(at: destination)
|
||||
}
|
||||
try fm.copyItem(at: externalURL, to: destination)
|
||||
DispatchQueue.main.async { self.refresh() }
|
||||
} catch {
|
||||
DispatchQueue.main.async { self.lastError = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toolsDirectory() -> URL {
|
||||
let dir = URL.documentsDirectory.appendingPathComponent("tools", isDirectory: true)
|
||||
var isDir: ObjCBool = false
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: dir.path, isDirectory: &isDir) {
|
||||
if !isDir.boolValue {
|
||||
try? fm.removeItem(at: dir)
|
||||
}
|
||||
}
|
||||
if !fm.fileExists(atPath: dir.path) {
|
||||
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
static func toolsDataDirectory() -> URL {
|
||||
let dir = URL.documentsDirectory.appendingPathComponent("MiniToolData", isDirectory: true)
|
||||
var isDir: ObjCBool = false
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: dir.path, isDirectory: &isDir) {
|
||||
if !isDir.boolValue {
|
||||
try? fm.removeItem(at: dir)
|
||||
}
|
||||
}
|
||||
if !fm.fileExists(atPath: dir.path) {
|
||||
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
private func sanitizedToolName(_ raw: String) -> String {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
||||
let filtered = trimmed.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
|
||||
let name = String(filtered).replacingOccurrences(of: "__+", with: "_", options: .regularExpression)
|
||||
return name.isEmpty ? "Untitled" : name
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct MiniToolRunnerView: View {
|
||||
let tool: MiniToolBundle
|
||||
let toolInfo: ToolInfo
|
||||
@StateObject private var runtime: MiniToolRuntime
|
||||
@State private var showLogs = false
|
||||
@State private var initiated = false
|
||||
|
||||
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
||||
@Environment(\.themeExpansionManager) private var themeExpansion
|
||||
|
||||
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
|
||||
private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) }
|
||||
|
||||
init(tool: MiniToolBundle, toolInfo: ToolInfo) {
|
||||
self.tool = tool
|
||||
self.toolInfo = toolInfo
|
||||
_runtime = StateObject(wrappedValue: MiniToolRuntime(tool: tool, toolInfo: toolInfo))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ThemedBackground(style: backgroundStyle)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 12) {
|
||||
MiniToolWebContainer(runtime: runtime)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
|
||||
if !runtime.logs.isEmpty && showLogs {
|
||||
logList
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.navigationTitle(tool.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear {
|
||||
if !initiated {
|
||||
runtime.start()
|
||||
initiated = true
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showLogs.toggle()
|
||||
} label: {
|
||||
Label("Toggle Log", systemImage: "bubble.left")
|
||||
}
|
||||
Button {
|
||||
runtime.reload()
|
||||
} label: {
|
||||
Label("Reload", systemImage: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(preferredScheme)
|
||||
}
|
||||
|
||||
private var logList: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Label("Messages", systemImage: "bubble.left")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Spacer()
|
||||
}
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 6) {
|
||||
ForEach(Array(runtime.logs.enumerated()), id: \.offset) { index, line in
|
||||
Text(line)
|
||||
.font(.caption.monospaced())
|
||||
.id(index)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: runtime.logs.count) { _, newCount in
|
||||
guard newCount > 0 else { return }
|
||||
withAnimation { proxy.scrollTo(newCount - 1, anchor: .bottom) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct MiniToolWebContainer: UIViewRepresentable {
|
||||
@ObservedObject var runtime: MiniToolRuntime
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
runtime.webView!
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: WKWebView, context: Context) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,630 +0,0 @@
|
|||
import Foundation
|
||||
import UniformTypeIdentifiers
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import JavaScriptCore
|
||||
|
||||
final class MiniToolRuntime: NSObject, ObservableObject {
|
||||
let tool: MiniToolBundle
|
||||
let toolInfo: ToolInfo
|
||||
@Published var logs: [String] = []
|
||||
@Published var isReady: Bool = false
|
||||
|
||||
var webView: WKWebView?
|
||||
private var context: JSContext?
|
||||
|
||||
private var appXHRTasks: [String: URLSessionDataTask] = [:]
|
||||
|
||||
private let messageHandlerName = "miniToolBridge"
|
||||
|
||||
private var ideviceJSBridge : IDeviceJSBridge? = nil
|
||||
|
||||
init(tool: MiniToolBundle, toolInfo: ToolInfo) {
|
||||
self.tool = tool
|
||||
self.toolInfo = toolInfo
|
||||
super.init()
|
||||
let configuration = WKWebViewConfiguration()
|
||||
let controller = WKUserContentController()
|
||||
controller.addUserScript(WKUserScript(source: MiniToolRuntime.frontendBridgeScript,
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true))
|
||||
configuration.userContentController = controller
|
||||
let leakAvoider = LeakAvoider(delegate: self)
|
||||
configuration.setURLSchemeHandler(leakAvoider, forURLScheme: "app")
|
||||
webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
controller.add(leakAvoider, name: messageHandlerName)
|
||||
webView!.navigationDelegate = self
|
||||
webView!.isInspectable = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
webView!.configuration.userContentController.removeScriptMessageHandler(forName: messageHandlerName)
|
||||
}
|
||||
|
||||
func start() {
|
||||
DispatchQueue.main.async {
|
||||
self.logs.removeAll()
|
||||
}
|
||||
loadBackground()
|
||||
loadFrontend()
|
||||
}
|
||||
|
||||
func reload() {
|
||||
start()
|
||||
}
|
||||
|
||||
// MARK: - Loading
|
||||
|
||||
private func loadFrontend() {
|
||||
guard FileManager.default.fileExists(atPath: tool.indexURL.path) else {
|
||||
appendLog("index.html missing for \(tool.name)")
|
||||
return
|
||||
}
|
||||
isReady = false
|
||||
webView!.load(URLRequest(url: URL(string: "app://\(tool.getHostName())")!))
|
||||
}
|
||||
|
||||
private func loadBackground() {
|
||||
context = JSContext()
|
||||
ideviceJSBridge = IDeviceJSBridge(context: context, allowedFunctions: toolInfo.requiredIDeviceFunctions ?? [])
|
||||
context?.exceptionHandler = { [weak self] _, exception in
|
||||
if let message = exception?.toString() {
|
||||
self?.appendLog("Background exception: \(message)")
|
||||
}
|
||||
}
|
||||
|
||||
let sendToFrontend: @convention(block) (Any?) -> Void = { [weak self] payload in
|
||||
self?.deliverToFrontend(payload ?? NSNull())
|
||||
}
|
||||
|
||||
let logFunction: @convention(block) (Any?) -> Void = { [weak self] msg in
|
||||
self?.appendLog(msg as? String ?? "Unable to decode log message.")
|
||||
|
||||
}
|
||||
|
||||
let ideviceFunction: @convention(block) (Any?) -> JSValue = { [weak self] msg in
|
||||
return JSValue.init(newPromiseIn: self?.context!) { resolve, reject in
|
||||
if let msg = msg as? [String: Any] {
|
||||
if let bridge = self?.ideviceJSBridge {
|
||||
bridge.didReceiveScriptMessage(msg, resolve: resolve, reject: reject);
|
||||
} else {
|
||||
resolve?.call(withArguments: ["Current Runtime is Terminated."])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context?.setObject(sendToFrontend, forKeyedSubscript: "__miniToolPostMessage" as NSString)
|
||||
context?.setObject(logFunction, forKeyedSubscript: "__miniToolLog" as NSString)
|
||||
context?.setObject(ideviceFunction, forKeyedSubscript: "__postIdeviceMessage" as NSString)
|
||||
|
||||
context?.evaluateScript(MiniToolRuntime.backgroundBridgeScript)
|
||||
if let ideviceJSURL = Bundle.main.url(forResource: "idevice", withExtension: "js"),
|
||||
let ideviceJSText = try? String(contentsOf: ideviceJSURL, encoding: .utf8) {
|
||||
context?.evaluateScript(ideviceJSText)
|
||||
}
|
||||
|
||||
do {
|
||||
let script = try String(contentsOf: tool.backgroundURL)
|
||||
context?.evaluateScript(script)
|
||||
} catch {
|
||||
appendLog("Failed to load background.js: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func deliverToBackground(_ payload: Any) {
|
||||
guard let receiver = context?.objectForKeyedSubscript("__miniToolReceive"),
|
||||
!receiver.isUndefined else {
|
||||
appendLog("Background handler is not ready")
|
||||
return
|
||||
}
|
||||
_ = receiver.call(withArguments: [payload])
|
||||
}
|
||||
|
||||
private func deliverToFrontend(_ payload: Any) {
|
||||
guard let json = MiniToolRuntime.encodePayload(payload) else {
|
||||
appendLog("Unable to encode payload for frontend")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
let script = "window.miniTool && window.miniTool.__receive(\(json))"
|
||||
self.webView!.evaluateJavaScript(script) { _, error in
|
||||
if let error {
|
||||
self.appendLog("Frontend dispatch error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func appendLog(_ text: String) {
|
||||
DispatchQueue.main.async {
|
||||
self.logs.append(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MiniToolRuntime : WKURLSchemeHandler {
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
start urlSchemeTask: WKURLSchemeTask
|
||||
) {
|
||||
guard let url = urlSchemeTask.request.url else { return }
|
||||
|
||||
guard let host = url.host(), host == tool.getHostName() else {
|
||||
urlSchemeTask.didFailWithError(NSError(domain: "Invalid Host Name", code: 404))
|
||||
return
|
||||
}
|
||||
|
||||
let path = url.path.isEmpty ? "index.html" : url.path
|
||||
|
||||
let fileURL = tool.url
|
||||
.appendingPathComponent(path).standardizedFileURL
|
||||
|
||||
if fileURL.path().hasPrefix(tool.url.path()) {
|
||||
urlSchemeTask.didFailWithError(NSError(domain: "Path traversal is not allowed.", code: -1))
|
||||
return
|
||||
}
|
||||
|
||||
if fileURL.path() == tool.url.appending(path: "check").path() {
|
||||
urlSchemeTask.didFailWithError(NSError(domain: "Reading this file is not allowed.", code: -1))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let mimeType : String
|
||||
if let type = UTType(filenameExtension: fileURL.pathExtension), let mimetype = type.preferredMIMEType {
|
||||
mimeType = mimetype
|
||||
} else {
|
||||
mimeType = UTType.data.preferredMIMEType!
|
||||
}
|
||||
|
||||
let response = URLResponse(
|
||||
url: url,
|
||||
mimeType: mimeType,
|
||||
expectedContentLength: data.count,
|
||||
textEncodingName: nil
|
||||
)
|
||||
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(data)
|
||||
urlSchemeTask.didFinish()
|
||||
} catch {
|
||||
urlSchemeTask.didFailWithError(error)
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WKScriptMessageHandler
|
||||
|
||||
extension MiniToolRuntime: WKScriptMessageHandler {
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard message.name == messageHandlerName else { return }
|
||||
if let dict = message.body as? [String: Any], dict["__appXHR"] as? Bool == true {
|
||||
handleAppXHRMessage(dict)
|
||||
return
|
||||
}
|
||||
if let dict = message.body as? [String: Any], let payload = dict["payload"] {
|
||||
deliverToBackground(payload)
|
||||
} else {
|
||||
deliverToBackground(message.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WKNavigationDelegate
|
||||
|
||||
extension MiniToolRuntime: WKNavigationDelegate {
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
|
||||
// Don't allow loading other contents
|
||||
guard let url = navigationAction.request.url else {
|
||||
return WKNavigationActionPolicy.cancel
|
||||
}
|
||||
guard let scheme = url.scheme, scheme == "app" else {
|
||||
return WKNavigationActionPolicy.cancel
|
||||
}
|
||||
guard let host = url.host(), host == tool.getHostName() else {
|
||||
return WKNavigationActionPolicy.cancel
|
||||
}
|
||||
return WKNavigationActionPolicy.allow
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
isReady = true
|
||||
deliverToBackground(["type": "ui-ready", "tool": tool.name])
|
||||
deliverToFrontend(["type": "ready", "tool": tool.name])
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
|
||||
download.delegate = self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MiniToolRuntime: WKDownloadDelegate {
|
||||
|
||||
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? {
|
||||
return MiniToolStore.toolsDataDirectory().appending(path: suggestedFilename, directoryHint: .notDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scripts & Encoding
|
||||
|
||||
extension MiniToolRuntime {
|
||||
static let frontendBridgeScript = """
|
||||
window.miniTool = window.miniTool || {};
|
||||
window.miniTool.__handler = null;
|
||||
window.miniTool.onMessage = function(handler) { window.miniTool.__handler = handler; };
|
||||
window.miniTool.postMessage = function(payload) {
|
||||
window.webkit.messageHandlers.miniToolBridge.postMessage({ payload: payload });
|
||||
};
|
||||
window.miniTool.__receive = function(payload) {
|
||||
try {
|
||||
if (typeof window.miniTool.__handler === 'function') {
|
||||
window.miniTool.__handler(payload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
(function() {
|
||||
const handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.miniToolBridge;
|
||||
function safePostMessage(body) {
|
||||
if (!handler || !handler.postMessage) {
|
||||
console.error('miniToolBridge unavailable for AppXMLHttpRequest');
|
||||
return;
|
||||
}
|
||||
handler.postMessage(body);
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64) {
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
class AppXMLHttpRequest {
|
||||
constructor() {
|
||||
this.readyState = 0;
|
||||
this.status = 0;
|
||||
this.statusText = '';
|
||||
this.responseText = '';
|
||||
this.response = null;
|
||||
this.responseType = '';
|
||||
this.onreadystatechange = null;
|
||||
this.onload = null;
|
||||
this.onerror = null;
|
||||
this.onabort = null;
|
||||
this._headers = {};
|
||||
this._method = null;
|
||||
this._url = null;
|
||||
this._async = true;
|
||||
this._aborted = false;
|
||||
this._id = AppXMLHttpRequest.__nextId();
|
||||
}
|
||||
|
||||
open(method, url, async = true) {
|
||||
this._method = method;
|
||||
this._url = url;
|
||||
this._async = async !== false;
|
||||
this.readyState = 1; // OPENED
|
||||
this._emitReadyState();
|
||||
}
|
||||
|
||||
setRequestHeader(key, value) {
|
||||
this._headers[key] = value;
|
||||
}
|
||||
|
||||
send(body = null) {
|
||||
if (!this._method || !this._url) {
|
||||
throw new Error('AppXMLHttpRequest: call open() before send().');
|
||||
}
|
||||
|
||||
this.readyState = 2; // HEADERS_RECEIVED (simulated)
|
||||
this._emitReadyState();
|
||||
|
||||
AppXMLHttpRequest.__pending[this._id] = this;
|
||||
|
||||
let encodedBody = null;
|
||||
let bodyIsBase64 = false;
|
||||
if (typeof body === 'string') {
|
||||
encodedBody = body;
|
||||
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
||||
const view = body instanceof ArrayBuffer ? new Uint8Array(body) : new Uint8Array(body.buffer, body.byteOffset || 0, body.byteLength || body.length || 0);
|
||||
let binary = '';
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
binary += String.fromCharCode(view[i]);
|
||||
}
|
||||
encodedBody = btoa(binary);
|
||||
bodyIsBase64 = true;
|
||||
} else if (body != null) {
|
||||
try {
|
||||
encodedBody = JSON.stringify(body);
|
||||
} catch (err) {
|
||||
console.error('AppXMLHttpRequest: unable to serialize body', err);
|
||||
}
|
||||
}
|
||||
|
||||
safePostMessage({
|
||||
__appXHR: true,
|
||||
action: 'request',
|
||||
id: this._id,
|
||||
method: this._method,
|
||||
url: this._url,
|
||||
async: this._async,
|
||||
headers: this._headers,
|
||||
body: encodedBody,
|
||||
bodyIsBase64: bodyIsBase64,
|
||||
responseType: this.responseType
|
||||
});
|
||||
}
|
||||
|
||||
abort() {
|
||||
this._aborted = true;
|
||||
safePostMessage({ __appXHR: true, action: 'abort', id: this._id });
|
||||
}
|
||||
|
||||
_complete(payload) {
|
||||
if (this._aborted && !payload.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = payload.status || 0;
|
||||
this.statusText = payload.statusText || '';
|
||||
this.responseText = payload.responseText || '';
|
||||
const base64 = payload.base64 || null;
|
||||
|
||||
if (this.responseType === 'json') {
|
||||
try {
|
||||
this.response = this.responseText ? JSON.parse(this.responseText) : null;
|
||||
} catch (err) {
|
||||
console.error('AppXMLHttpRequest: failed to parse JSON response', err);
|
||||
this.response = null;
|
||||
}
|
||||
} else if (this.responseType === 'arraybuffer' && base64) {
|
||||
this.response = base64ToArrayBuffer(base64);
|
||||
} else {
|
||||
this.response = this.responseText;
|
||||
}
|
||||
|
||||
this.readyState = 4; // DONE
|
||||
this._emitReadyState();
|
||||
|
||||
if (payload.aborted) {
|
||||
if (typeof this.onabort === 'function') {
|
||||
try { this.onabort(); } catch (err) { console.error(err); }
|
||||
}
|
||||
delete AppXMLHttpRequest.__pending[this._id];
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.error) {
|
||||
if (typeof this.onerror === 'function') {
|
||||
try { this.onerror(new Error(payload.error)); } catch (err) { console.error(err); }
|
||||
}
|
||||
} else if (typeof this.onload === 'function') {
|
||||
try { this.onload(); } catch (err) { console.error(err); }
|
||||
}
|
||||
|
||||
delete AppXMLHttpRequest.__pending[this._id];
|
||||
}
|
||||
|
||||
_emitReadyState() {
|
||||
if (typeof this.onreadystatechange === 'function') {
|
||||
try {
|
||||
this.onreadystatechange();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static __nextId() {
|
||||
AppXMLHttpRequest.__counter += 1;
|
||||
return `app-xhr-${AppXMLHttpRequest.__counter}`;
|
||||
}
|
||||
|
||||
static __receive(payload) {
|
||||
const instance = AppXMLHttpRequest.__pending[payload.id];
|
||||
if (instance) {
|
||||
instance._complete(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppXMLHttpRequest.__counter = 0;
|
||||
AppXMLHttpRequest.__pending = {};
|
||||
|
||||
window.__XMLHttpRequest = window.XMLHttpRequest;
|
||||
window.XMLHttpRequest = AppXMLHttpRequest; // alias with requested casing
|
||||
})();
|
||||
"""
|
||||
|
||||
static let backgroundBridgeScript = """
|
||||
var miniTool = this.miniTool || {};
|
||||
miniTool.__handler = null;
|
||||
miniTool.onMessage = function(handler) { miniTool.__handler = handler; };
|
||||
miniTool.postMessage = function(payload) { __miniToolPostMessage(payload); };
|
||||
miniTool.log = function(log) { __miniToolLog(log); }
|
||||
function __miniToolReceive(payload) {
|
||||
try {
|
||||
if (typeof miniTool.__handler === 'function') {
|
||||
miniTool.__handler(payload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
this.miniTool = miniTool;
|
||||
"""
|
||||
|
||||
static func encodePayload(_ payload: Any) -> String? {
|
||||
if JSONSerialization.isValidJSONObject(payload) {
|
||||
if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
|
||||
let string = String(data: data, encoding: .utf8) {
|
||||
return string
|
||||
}
|
||||
}
|
||||
if let string = payload as? String {
|
||||
let escaped = string.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
|
||||
return "\"\(escaped)\""
|
||||
}
|
||||
if let number = payload as? NSNumber {
|
||||
return number.stringValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App-backed XHR
|
||||
|
||||
private extension MiniToolRuntime {
|
||||
func handleAppXHRMessage(_ payload: [String: Any]) {
|
||||
guard let capabilities = toolInfo.capabilities, capabilities.internetAccess ?? false else {
|
||||
appendLog("Internet access is disabled! To enable it, enable internetAccess in toolInfo.json")
|
||||
return
|
||||
}
|
||||
|
||||
guard let id = payload["id"] as? String else {
|
||||
appendLog("AppXHR missing id")
|
||||
return
|
||||
}
|
||||
|
||||
let action = payload["action"] as? String ?? "request"
|
||||
|
||||
if action == "abort" {
|
||||
if let task = appXHRTasks[id] {
|
||||
task.cancel()
|
||||
appXHRTasks[id] = nil
|
||||
}
|
||||
deliverAppXHRResponse(["id": id, "status": 0, "statusText": "aborted", "aborted": true])
|
||||
return
|
||||
}
|
||||
|
||||
guard let urlString = payload["url"] as? String, let url = URL(string: urlString) else {
|
||||
appendLog("AppXHR invalid URL for id \(id)")
|
||||
deliverAppXHRResponse(["id": id, "status": 0, "statusText": "invalid-url", "error": "Invalid URL"])
|
||||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = (payload["method"] as? String) ?? "GET"
|
||||
|
||||
if let headers = payload["headers"] as? [String: Any] {
|
||||
headers.forEach { key, value in
|
||||
if let valueString = value as? String ?? (value as? NSNumber)?.stringValue {
|
||||
request.setValue(valueString, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let body = payload["body"] {
|
||||
let isBase64 = payload["bodyIsBase64"] as? Bool ?? false
|
||||
if let stringBody = body as? String {
|
||||
if isBase64, let data = Data(base64Encoded: stringBody) {
|
||||
request.httpBody = data
|
||||
} else {
|
||||
request.httpBody = stringBody.data(using: .utf8)
|
||||
}
|
||||
} else if JSONSerialization.isValidJSONObject(body),
|
||||
let data = try? JSONSerialization.data(withJSONObject: body, options: []) {
|
||||
request.httpBody = data
|
||||
}
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
|
||||
guard let self else { return }
|
||||
|
||||
var responsePayload: [String: Any] = [
|
||||
"id": id,
|
||||
"status": 0,
|
||||
"statusText": ""
|
||||
]
|
||||
|
||||
if let http = response as? HTTPURLResponse {
|
||||
responsePayload["status"] = http.statusCode
|
||||
responsePayload["statusText"] = HTTPURLResponse.localizedString(forStatusCode: http.statusCode)
|
||||
|
||||
let headers = http.allHeaderFields.reduce(into: [String: String]()) { partialResult, entry in
|
||||
if let key = entry.key as? String {
|
||||
partialResult[key] = String(describing: entry.value)
|
||||
}
|
||||
}
|
||||
responsePayload["headers"] = headers
|
||||
}
|
||||
|
||||
if let error = error as NSError? {
|
||||
if error.code == NSURLErrorCancelled {
|
||||
responsePayload["aborted"] = true
|
||||
responsePayload["statusText"] = "aborted"
|
||||
} else {
|
||||
responsePayload["error"] = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
responsePayload["responseText"] = String(data: data, encoding: .utf8) ?? ""
|
||||
responsePayload["base64"] = data.base64EncodedString()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.appXHRTasks[id] = nil
|
||||
self.deliverAppXHRResponse(responsePayload)
|
||||
}
|
||||
}
|
||||
|
||||
appXHRTasks[id] = task
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func deliverAppXHRResponse(_ payload: [String: Any]) {
|
||||
guard let json = MiniToolRuntime.encodePayload(payload) else {
|
||||
appendLog("AppXHR: Unable to encode response for id \(payload["id"] ?? "<unknown>")")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let script = "window.XMLHttpRequest.__receive(\(json))"
|
||||
self.webView!.evaluateJavaScript(script) { _, error in
|
||||
if let error {
|
||||
self.appendLog("AppXHR deliver error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LeakAvoider : NSObject, WKURLSchemeHandler, WKScriptMessageHandler, WKDownloadDelegate {
|
||||
func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? {
|
||||
return await self.delegate?.download(download, decideDestinationUsing: response, suggestedFilename: suggestedFilename)
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
self.delegate?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
||||
self.delegate?.webView(webView, start: urlSchemeTask)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
||||
self.delegate?.webView(webView, stop: urlSchemeTask)
|
||||
}
|
||||
|
||||
|
||||
|
||||
weak var delegate : (WKURLSchemeHandler & WKScriptMessageHandler & WKDownloadDelegate)?
|
||||
init (delegate:WKURLSchemeHandler & WKScriptMessageHandler & WKDownloadDelegate) {
|
||||
self.delegate = delegate
|
||||
super.init()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,4 +11,3 @@
|
|||
#include "idevice/ls.h"
|
||||
#include "idevice/profiles.h"
|
||||
#include "idevice/something.h"
|
||||
#include "MiniToolSupport/JSSupport/JSSupport.h"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,4 @@ enum FeatureFlags {
|
|||
static let showBetaTabs = true
|
||||
/// Forces the Theme Expansion to be unlocked and visible everywhere.
|
||||
static let alwaysUnlockThemeExpansion = true
|
||||
/// Controls visibility of the Mini Tools tab and related UI.
|
||||
static let isMiniToolsEnabled = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@ enum TabConfiguration {
|
|||
private static let coreIDs: [String] = ["home", "console", "scripts", "deviceinfo"]
|
||||
static var allowedIDs: [String] {
|
||||
var ids = coreIDs
|
||||
if FeatureFlags.isMiniToolsEnabled {
|
||||
ids.append("tools")
|
||||
}
|
||||
if FeatureFlags.showBetaTabs {
|
||||
ids.append("profiles")
|
||||
ids.append("processes")
|
||||
|
|
|
|||
|
|
@ -56,9 +56,6 @@ struct MainTabView: View {
|
|||
TabDescriptor(id: "scripts", title: "Scripts", systemImage: "scroll") { AnyView(ScriptListView()) },
|
||||
TabDescriptor(id: "deviceinfo", title: "Device Info", systemImage: "iphone.and.arrow.forward") { AnyView(DeviceInfoView()) }
|
||||
]
|
||||
if FeatureFlags.isMiniToolsEnabled {
|
||||
tabs.append(TabDescriptor(id: "tools", title: "Mini Tools", systemImage: "shippingbox.fill") { AnyView(MiniToolListView()) })
|
||||
}
|
||||
if FeatureFlags.showBetaTabs {
|
||||
tabs.append(TabDescriptor(id: "profiles", title: "Profiles", systemImage: "magazine.fill") { AnyView(ProfileView()) })
|
||||
tabs.append(TabDescriptor(id: "processes", title: "Processes", systemImage: "rectangle.stack.person.crop") { AnyView(ProcessInspectorView()) })
|
||||
|
|
|
|||
|
|
@ -90,9 +90,6 @@ struct SettingsView: View {
|
|||
TabOption(id: "console", title: "Console", detail: "Live device logs", icon: "terminal", isBeta: false),
|
||||
TabOption(id: "scripts", title: "Scripts", detail: "Manage automation scripts", icon: "scroll", isBeta: false)
|
||||
]
|
||||
if FeatureFlags.isMiniToolsEnabled {
|
||||
options.append(TabOption(id: "tools", title: "Mini Tools", detail: "Import and run stiktool bundles", icon: "shippingbox", isBeta: true))
|
||||
}
|
||||
options.append(TabOption(id: "deviceinfo", title: "Device Info", detail: "View detailed device metadata", icon: "iphone.and.arrow.forward", isBeta: false))
|
||||
if FeatureFlags.showBetaTabs {
|
||||
options.append(TabOption(id: "profiles", title: "Profiles", detail: "Install/remove profiles", icon: "magazine", isBeta: true))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue