@tileflow/react-native 0.0.0-bootstrap.0 → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +216 -4
  2. package/TileflowNativeAdmission.podspec +34 -0
  3. package/android/build.gradle +43 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
  6. package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
  7. package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
  8. package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
  9. package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
  10. package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
  11. package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
  12. package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
  13. package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
  14. package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
  15. package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
  16. package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
  17. package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
  18. package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
  19. package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
  20. package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
  21. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +234 -0
  22. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
  23. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
  24. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +80 -0
  25. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
  26. package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
  27. package/dist/contract-DWs-lDPo.d.ts +144 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +6989 -0
  30. package/dist/internal/camera.d.ts +65 -0
  31. package/dist/internal/camera.js +325 -0
  32. package/dist/internal/hosted-binding.d.ts +18 -0
  33. package/dist/internal/hosted-binding.js +277 -0
  34. package/dist/internal/initial-view.d.ts +11 -0
  35. package/dist/internal/initial-view.js +8 -0
  36. package/dist/internal/interactions.d.ts +76 -0
  37. package/dist/internal/interactions.js +794 -0
  38. package/dist/internal/native-admission-bridge.d.ts +29 -0
  39. package/dist/internal/native-admission-bridge.js +351 -0
  40. package/dist/internal/native-admission.d.ts +78 -0
  41. package/dist/internal/native-admission.js +1614 -0
  42. package/dist/internal/native-appearance.d.ts +21 -0
  43. package/dist/internal/native-appearance.js +110 -0
  44. package/dist/internal/native-configuration-bridge.d.ts +9 -0
  45. package/dist/internal/native-configuration-bridge.js +343 -0
  46. package/dist/internal/session.d.ts +125 -0
  47. package/dist/internal/session.js +887 -0
  48. package/dist/internal/source-state.d.ts +11 -0
  49. package/dist/internal/source-state.js +26 -0
  50. package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
  51. package/docs/native-admission.md +108 -0
  52. package/docs/native-configuration.md +210 -0
  53. package/docs/native-interactions.md +161 -0
  54. package/docs/native-location.md +148 -0
  55. package/ios/TFAdmissionBootstrap.h +17 -0
  56. package/ios/TFAdmissionBootstrap.mm +168 -0
  57. package/ios/TFAdmissionCatalog.h +13 -0
  58. package/ios/TFAdmissionCatalog.mm +193 -0
  59. package/ios/TFAdmissionDocumentScope.h +7 -0
  60. package/ios/TFAdmissionDocumentScope.mm +45 -0
  61. package/ios/TFAdmissionEngine.h +57 -0
  62. package/ios/TFAdmissionEngine.mm +371 -0
  63. package/ios/TFAdmissionInstallation.h +27 -0
  64. package/ios/TFAdmissionInstallation.mm +117 -0
  65. package/ios/TFAdmissionNetwork.h +16 -0
  66. package/ios/TFAdmissionNetwork.mm +268 -0
  67. package/ios/TFAdmissionURL.mm +116 -0
  68. package/ios/TFDocumentBounds.h +9 -0
  69. package/ios/TFDocumentBounds.mm +19 -0
  70. package/ios/TFMobileConfiguration.h +23 -0
  71. package/ios/TFMobileConfiguration.mm +139 -0
  72. package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
  73. package/ios/TFNativeDocumentRegistry.h +28 -0
  74. package/ios/TFNativeDocumentRegistry.mm +193 -0
  75. package/ios/TFNativeDocumentURL.h +6 -0
  76. package/ios/TFNativeSurfaceState.h +28 -0
  77. package/ios/TFNativeSurfaceState.mm +128 -0
  78. package/ios/TileflowNativeAdmission.h +9 -0
  79. package/ios/TileflowNativeAdmission.mm +184 -0
  80. package/ios/TileflowNativeConfiguration.mm +41 -0
  81. package/ios/TileflowNativeDocuments.h +12 -0
  82. package/ios/TileflowNativeDocuments.mm +105 -0
  83. package/ios/TileflowNativeSurface.mm +403 -0
  84. package/ios/tileflow_native_admission.rb +45 -0
  85. package/package.json +51 -7
  86. package/react-native.config.cjs +13 -0
@@ -0,0 +1,268 @@
1
+ #import "TFAdmissionNetwork.h"
2
+ #import "TFDocumentBounds.h"
3
+ #import <mach/mach_time.h>
4
+ #import <atomic>
5
+
6
+ @interface TFScheduledAdmission : NSObject <TFAdmissionCancel>
7
+ @property (nonatomic, copy, nullable) dispatch_block_t action;
8
+ @property (nonatomic, strong, nullable) dispatch_source_t timer;
9
+ @end
10
+ @implementation TFScheduledAdmission
11
+ - (void)cancel {
12
+ @synchronized(self) {
13
+ self.action = nil;
14
+ if (self.timer) dispatch_source_cancel(self.timer);
15
+ self.timer = nil;
16
+ }
17
+ }
18
+ - (void)fire {
19
+ dispatch_block_t action;
20
+ @synchronized(self) { action = self.action; [self cancel]; }
21
+ if (action) action();
22
+ }
23
+ - (void)dealloc { if (_timer) dispatch_source_cancel(_timer); }
24
+ @end
25
+
26
+ @implementation TFContinuousAdmissionScheduler
27
+ - (NSTimeInterval)nowMs {
28
+ static mach_timebase_info_data_t timebase;
29
+ static dispatch_once_t once;
30
+ dispatch_once(&once, ^{ mach_timebase_info(&timebase); });
31
+ return (double)mach_continuous_time() * (double)timebase.numer / (double)timebase.denom / 1000000.0;
32
+ }
33
+ - (void)enqueue:(dispatch_block_t)block { dispatch_async(dispatch_get_main_queue(), block); }
34
+ - (id<TFAdmissionCancel>)after:(NSTimeInterval)milliseconds perform:(dispatch_block_t)block {
35
+ TFScheduledAdmission *token = [TFScheduledAdmission new];
36
+ token.action = block;
37
+ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
38
+ token.timer = timer;
39
+ __weak TFScheduledAdmission *weakToken = token;
40
+ // Cancellation releases captured work without waiting for the timer date.
41
+ dispatch_source_set_event_handler(timer, ^{ [weakToken fire]; });
42
+ dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(milliseconds * NSEC_PER_MSEC)), DISPATCH_TIME_FOREVER, 0);
43
+ dispatch_resume(timer);
44
+ return token;
45
+ }
46
+ @end
47
+
48
+ @interface TFAdmissionSessionOperation : NSObject <TFAdmissionCancel, NSURLSessionDataDelegate, NSURLSessionTaskDelegate> {
49
+ std::atomic_bool _cancelled;
50
+ std::atomic_bool _finished;
51
+ std::atomic_bool _started;
52
+ std::atomic_bool _cleaned;
53
+ }
54
+ @property (nonatomic, strong, nullable) NSURLRequest *request;
55
+ @property (nonatomic, strong, nullable) NSURLSessionConfiguration *configuration;
56
+ @property (nonatomic, strong) NSOperationQueue *delegateQueue;
57
+ @property (nonatomic) BOOL followsRedirects;
58
+ @property (nonatomic) NSUInteger responseByteLimit;
59
+ @property (atomic, copy, nullable) TFAdmissionStartGuard guard;
60
+ @property (nonatomic, copy, nullable) TFAdmissionNetworkCompletion completion;
61
+ @property (nonatomic, copy, nullable) dispatch_block_t cleanup;
62
+ @property (atomic, strong, nullable) NSURLSession *session;
63
+ @property (atomic, strong, nullable) NSURLSessionDataTask *task;
64
+ @property (nonatomic, strong, nullable) NSMutableData *body;
65
+ @property (nonatomic, strong, nullable) NSHTTPURLResponse *response;
66
+ - (BOOL)isCancelled;
67
+ - (void)start;
68
+ - (void)finish:(nullable NSHTTPURLResponse *)response body:(nullable NSData *)body;
69
+ - (void)releaseNativeResources;
70
+ @end
71
+
72
+ @implementation TFAdmissionSessionOperation
73
+ - (instancetype)init {
74
+ if ((self = [super init])) {
75
+ _cancelled.store(false); _finished.store(false); _started.store(false); _cleaned.store(false);
76
+ _body = [NSMutableData data];
77
+ }
78
+ return self;
79
+ }
80
+ - (NSString *)description { return @"TFAdmissionSessionOperation(redacted)"; }
81
+ - (BOOL)isCancelled { return _cancelled.load(); }
82
+ - (BOOL)mayStart {
83
+ TFAdmissionStartGuard guard = self.guard;
84
+ return guard && !_cancelled.load() && !_finished.load() && guard();
85
+ }
86
+ - (void)start {
87
+ if (_started.exchange(true)) return;
88
+ BOOL noSession = NO;
89
+ @try {
90
+ // Protect task construction against native cancellation, not against
91
+ // JavaScript. There is no asynchronous wait or I/O under this lock.
92
+ @synchronized(self) {
93
+ if ([self mayStart]) {
94
+ NSURLSessionConfiguration *configuration = [self.configuration copy];
95
+ configuration.waitsForConnectivity = NO;
96
+ configuration.discretionary = NO;
97
+ configuration.timeoutIntervalForResource = MIN(30.0, self.request.timeoutInterval);
98
+ configuration.timeoutIntervalForRequest = MIN(30.0, self.request.timeoutInterval);
99
+ self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.delegateQueue];
100
+ self.task = [self.session dataTaskWithRequest:self.request];
101
+ // Recheck after task creation, immediately before native resume.
102
+ if (self.task && [self mayStart]) [self.task resume];
103
+ else [self cancel];
104
+ } else [self finish:nil body:nil];
105
+ noSession = self.session == nil;
106
+ }
107
+ } @catch (NSException *exception) {
108
+ [self cancel];
109
+ @synchronized(self) { noSession = self.session == nil; }
110
+ }
111
+ // A request cancelled before queue drain has no native terminal event.
112
+ // Only processing its queued start may release that reservation.
113
+ if (noSession) [self releaseNativeResources];
114
+ }
115
+ - (void)cancel {
116
+ _cancelled.store(true);
117
+ [self finish:nil body:nil];
118
+ NSURLSession *session;
119
+ NSURLSessionDataTask *task;
120
+ @synchronized(self) { session = self.session; task = self.task; }
121
+ [task cancel]; [session invalidateAndCancel];
122
+ }
123
+ - (void)finish:(NSHTTPURLResponse *)response body:(NSData *)body {
124
+ if (_finished.exchange(true)) return;
125
+ NSURLSession *session;
126
+ @synchronized(self) { session = self.session; }
127
+ [session invalidateAndCancel];
128
+ TFAdmissionNetworkCompletion completion = self.completion;
129
+ self.completion = nil;
130
+ if (completion) completion(response, body);
131
+ // Logical completion is not physical cleanup. A cancelled URLSession
132
+ // retains its native capacity slot until didBecomeInvalidWithError.
133
+ }
134
+ - (void)releaseNativeResources {
135
+ if (_cleaned.exchange(true)) return;
136
+ dispatch_block_t cleanup;
137
+ @synchronized(self) {
138
+ self.task = nil; self.session = nil;
139
+ self.request = nil; self.configuration = nil; self.guard = nil;
140
+ self.body = nil; self.response = nil;
141
+ cleanup = self.cleanup; self.cleanup = nil;
142
+ }
143
+ if (cleanup) cleanup();
144
+ }
145
+ - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
146
+ [self finish:nil body:nil];
147
+ [self releaseNativeResources];
148
+ }
149
+ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
150
+ if (![self mayStart] || ![response isKindOfClass:NSHTTPURLResponse.class] || response.expectedContentLength > (int64_t)self.responseByteLimit) {
151
+ completionHandler(NSURLSessionResponseCancel); [self cancel]; return;
152
+ }
153
+ self.response = (NSHTTPURLResponse *)response;
154
+ completionHandler(NSURLSessionResponseAllow);
155
+ }
156
+ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
157
+ if (![self mayStart] || data.length > self.responseByteLimit - self.body.length) { [self cancel]; return; }
158
+ [self.body appendData:data];
159
+ }
160
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
161
+ if (error || ![self mayStart]) [self finish:nil body:nil];
162
+ else [self finish:self.response body:[self.body copy]];
163
+ }
164
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *_Nullable))completionHandler {
165
+ if (![self mayStart]) { completionHandler(nil); [self cancel]; return; }
166
+ if (self.followsRedirects) { completionHandler(request); return; }
167
+ // Observe the response here, not through MapLibre delegate forwarding.
168
+ completionHandler(nil);
169
+ [self finish:response body:[NSData data]];
170
+ }
171
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willBeginDelayedRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLSessionDelayedRequestDisposition, NSURLRequest *_Nullable))completionHandler {
172
+ completionHandler([self mayStart] ? NSURLSessionDelayedRequestContinueLoading : NSURLSessionDelayedRequestCancel, nil);
173
+ }
174
+ - (void)URLSession:(NSURLSession *)session taskIsWaitingForConnectivity:(NSURLSessionTask *)task { [self cancel]; }
175
+ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
176
+ completionHandler([self mayStart] ? NSURLSessionAuthChallengePerformDefaultHandling : NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
177
+ }
178
+ - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
179
+ completionHandler([self mayStart] ? NSURLSessionAuthChallengePerformDefaultHandling : NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
180
+ }
181
+ @end
182
+
183
+ @interface TFAdmissionURLSessionNetwork ()
184
+ @property (nonatomic, strong) NSURLSessionConfiguration *configuration;
185
+ @property (nonatomic) BOOL followsRedirects;
186
+ @property (nonatomic) NSUInteger responseByteLimit;
187
+ @property (nonatomic) NSUInteger queueDepth;
188
+ @property (nonatomic) NSUInteger concurrency;
189
+ @property (nonatomic) NSUInteger reservations;
190
+ @property (nonatomic, strong) dispatch_queue_t queue;
191
+ @property (nonatomic, strong) NSOperationQueue *delegateQueue;
192
+ @property (nonatomic, strong) NSMutableArray<TFAdmissionSessionOperation *> *pending;
193
+ @property (nonatomic, strong) NSMutableSet<TFAdmissionSessionOperation *> *active;
194
+ @property (atomic) BOOL closed;
195
+ @end
196
+
197
+ @implementation TFAdmissionURLSessionNetwork
198
+ - (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration followsRedirects:(BOOL)followsRedirects {
199
+ return [self initWithConfiguration:configuration followsRedirects:followsRedirects responseByteLimit:8388608 queueDepth:2048 concurrency:16];
200
+ }
201
+ - (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration followsRedirects:(BOOL)followsRedirects responseByteLimit:(NSUInteger)responseByteLimit queueDepth:(NSUInteger)queueDepth concurrency:(NSUInteger)concurrency {
202
+ return [self initWithConfiguration:configuration followsRedirects:followsRedirects responseByteLimit:responseByteLimit queueDepth:queueDepth concurrency:concurrency
203
+ workQueue:dispatch_queue_create("dev.tileflow.native-admission.network", DISPATCH_QUEUE_SERIAL)];
204
+ }
205
+ - (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration followsRedirects:(BOOL)followsRedirects responseByteLimit:(NSUInteger)responseByteLimit queueDepth:(NSUInteger)queueDepth concurrency:(NSUInteger)concurrency workQueue:(dispatch_queue_t)workQueue {
206
+ if ((self = [super init])) {
207
+ if (!workQueue || !responseByteLimit || responseByteLimit > 8388608 || !queueDepth || queueDepth > 2048 || !concurrency || concurrency > 16 || concurrency > queueDepth) {
208
+ [NSException raise:@"TFNativeAdmissionBounds" format:@"Invalid native transport bounds"];
209
+ }
210
+ _configuration = [configuration copy]; _followsRedirects = followsRedirects;
211
+ _responseByteLimit = responseByteLimit; _queueDepth = queueDepth; _concurrency = concurrency;
212
+ _queue = workQueue;
213
+ _delegateQueue = [NSOperationQueue new]; _delegateQueue.maxConcurrentOperationCount = 1;
214
+ _pending = [NSMutableArray array]; _active = [NSMutableSet set];
215
+ }
216
+ return self;
217
+ }
218
+ - (NSString *)description { return @"TFAdmissionURLSessionNetwork(redacted)"; }
219
+ - (id<TFAdmissionCancel>)start:(NSURLRequest *)request mayStart:(TFAdmissionStartGuard)guard completion:(TFAdmissionNetworkCompletion)completion {
220
+ NSUInteger responseByteLimit = TFDocumentResponseByteLimit(request, self.responseByteLimit);
221
+ BOOL reserved;
222
+ @synchronized(self) {
223
+ reserved = !self.closed && self.reservations < self.queueDepth;
224
+ if (reserved) self.reservations++;
225
+ }
226
+ TFAdmissionSessionOperation *operation = [TFAdmissionSessionOperation new];
227
+ operation.request = [request copy]; operation.guard = guard; operation.configuration = self.configuration;
228
+ operation.delegateQueue = self.delegateQueue; operation.followsRedirects = self.followsRedirects;
229
+ operation.responseByteLimit = responseByteLimit;
230
+ operation.completion = completion;
231
+ __weak TFAdmissionSessionOperation *weakOperation = operation;
232
+ operation.cleanup = ^{
233
+ if (reserved) dispatch_async(self.queue, ^{
234
+ TFAdmissionSessionOperation *finished = weakOperation;
235
+ if (finished) { [self.active removeObject:finished]; [self.pending removeObject:finished]; }
236
+ @synchronized(self) { self.reservations--; }
237
+ [self drain];
238
+ });
239
+ };
240
+ if (!reserved) { [operation cancel]; [operation start]; return operation; }
241
+ // Capacity is reserved before posting. Native terminal cleanup, not
242
+ // promise rejection, releases it after a URLSession has been created.
243
+ dispatch_async(self.queue, ^{
244
+ if (self.closed || operation.isCancelled) { [operation cancel]; [operation start]; return; }
245
+ [self.pending addObject:operation];
246
+ [self drain];
247
+ });
248
+ return operation;
249
+ }
250
+ - (void)drain {
251
+ while (!self.closed && self.active.count < self.concurrency && self.pending.count) {
252
+ TFAdmissionSessionOperation *operation = self.pending.firstObject;
253
+ [self.pending removeObjectAtIndex:0];
254
+ if (operation.isCancelled) { [operation start]; continue; }
255
+ [self.active addObject:operation];
256
+ [operation start];
257
+ }
258
+ }
259
+ - (void)close {
260
+ @synchronized(self) { if (self.closed) return; self.closed = YES; }
261
+ dispatch_async(self.queue, ^{
262
+ for (TFAdmissionSessionOperation *operation in [self.pending copy]) { [operation cancel]; [operation start]; }
263
+ [self.pending removeAllObjects];
264
+ // Keep active sessions and their reservations until invalidation.
265
+ for (TFAdmissionSessionOperation *operation in [self.active copy]) [operation cancel];
266
+ });
267
+ }
268
+ @end
@@ -0,0 +1,116 @@
1
+ #import "TFAdmissionEngine.h"
2
+ #import "TFNativeDocumentURL.h"
3
+
4
+ NSString *const TFAdmissionGrantHeader = @"X-Tileflow-Native-Grant";
5
+
6
+ static void TFInvalidURL(void) {
7
+ @throw [NSException exceptionWithName:@"TFNativeAdmissionInvalid" reason:@"Invalid native resource" userInfo:nil];
8
+ }
9
+ static BOOL TFMatches(NSString *value, NSString *pattern) {
10
+ if (![value isKindOfClass:NSString.class]) return NO;
11
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
12
+ return [expression numberOfMatchesInString:value options:0 range:NSMakeRange(0, value.length)] == 1;
13
+ }
14
+ static NSString *TFDecodeKey(NSString *value) {
15
+ NSMutableString *decoded = [NSMutableString string];
16
+ for (NSUInteger index = 0; index < value.length; index++) {
17
+ unichar character = [value characterAtIndex:index];
18
+ if (character == '%' && index + 2 < value.length) {
19
+ NSString *hex = [value substringWithRange:NSMakeRange(index + 1, 2)];
20
+ if (TFMatches(hex, @"^[0-9a-fA-F]{2}$")) {
21
+ unsigned int byte = 0;
22
+ [[NSScanner scannerWithString:hex] scanHexInt:&byte];
23
+ [decoded appendFormat:@"%C", (unichar)byte];
24
+ index += 2;
25
+ continue;
26
+ }
27
+ }
28
+ [decoded appendFormat:@"%C", character];
29
+ }
30
+ return decoded;
31
+ }
32
+ BOOL TFAdmissionValidToken(id value) {
33
+ return [value isKindOfClass:NSString.class] && TFMatches(value, @"^[A-Za-z0-9_.-]{1,96}$") && ![value hasPrefix:@"tf_"];
34
+ }
35
+ BOOL TFAdmissionHasReservedContext(NSString *url) {
36
+ if (![url isKindOfClass:NSString.class]) return NO;
37
+ NSRange query = [url rangeOfString:@"?"];
38
+ if (query.location == NSNotFound) return NO;
39
+ for (NSString *part in [[url substringFromIndex:query.location + 1] componentsSeparatedByString:@"&"]) {
40
+ NSString *key = [part componentsSeparatedByString:@"="].firstObject;
41
+ if ([TFDecodeKey(key).lowercaseString hasPrefix:@"__tf_native"]) return YES;
42
+ }
43
+ return NO;
44
+ }
45
+ static NSString *TFURLOrigin(NSString *url, BOOL permitsHTTP) {
46
+ NSURLComponents *components = [NSURLComponents componentsWithString:url];
47
+ if ((![[components scheme] isEqual:@"https"] && !(permitsHTTP && [components.scheme isEqual:@"http"])) || !components.host.length || components.user != nil || components.password != nil) TFInvalidURL();
48
+ NSString *host = components.host.lowercaseString;
49
+ if ([host containsString:@":"] && ![host hasPrefix:@"["]) host = [NSString stringWithFormat:@"[%@]", host];
50
+ NSNumber *port = components.port;
51
+ if (port && (port.integerValue < 1 || port.integerValue > 65535)) TFInvalidURL();
52
+ NSInteger defaultPort = [components.scheme isEqual:@"https"] ? 443 : 80;
53
+ return [NSString stringWithFormat:@"%@://%@%@", components.scheme, host, port && port.integerValue != defaultPort ? [NSString stringWithFormat:@":%@", port] : @""];
54
+ }
55
+ NSString *TFAdmissionOrigin(NSString *url) { return TFURLOrigin(url, NO); }
56
+ NSString *TFNativeDocumentOrigin(NSString *url) { return TFURLOrigin(url, YES); }
57
+ BOOL TFAdmissionStyleMatchesMap(NSString *url, NSString *mapId) {
58
+ @try {
59
+ if (![url isKindOfClass:NSString.class] || url.length > 2048) return NO;
60
+ NSString *origin = TFAdmissionOrigin(url);
61
+ if (![url hasPrefix:[origin stringByAppendingString:@"/"]]) return NO;
62
+ NSString *path = [url substringFromIndex:origin.length];
63
+ if (!TFMatches(TFDecodeKey(path), @"^/maps/[^/?#]+/native(?:[/?#]|$)")) return YES;
64
+ NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"^/maps/(map_[A-Za-z0-9_-]{16})/native/v([1-9][0-9]{0,15})/([A-Za-z][A-Za-z0-9_-]{0,63})\\.json$" options:0 error:nil];
65
+ NSTextCheckingResult *match = [pattern firstMatchInString:path options:0 range:NSMakeRange(0, path.length)];
66
+ if (!match || ![[path substringWithRange:[match rangeAtIndex:1]] isEqual:mapId]) return NO;
67
+ long long version = [[path substringWithRange:[match rangeAtIndex:2]] longLongValue];
68
+ return version > 0 && version <= 9007199254740991LL;
69
+ } @catch (NSException *exception) { return NO; }
70
+ }
71
+ static NSString *TFCleanURL(NSString *url, NSString *developmentOrigin) {
72
+ if (![url isKindOfClass:NSString.class] || url.length > 2048 || !TFMatches(url, @"^[\\x21-\\x7e]+$") || [url containsString:@"\\"] || [url containsString:@"#"] || TFAdmissionHasReservedContext(url)) TFInvalidURL();
73
+ NSURLComponents *components = [NSURLComponents componentsWithString:url];
74
+ NSString *origin = TFURLOrigin(url, developmentOrigin != nil);
75
+ if ([components.scheme isEqual:@"http"] && ![origin isEqual:developmentOrigin]) TFInvalidURL();
76
+ if (![url hasPrefix:[origin stringByAppendingString:@"/"]] || ![components.URL.absoluteString isEqual:url]) TFInvalidURL();
77
+ for (NSString *segment in [components.percentEncodedPath componentsSeparatedByString:@"/"]) {
78
+ NSString *decoded = TFDecodeKey(segment);
79
+ if ([decoded isEqual:@"."] || [decoded isEqual:@".."]) TFInvalidURL();
80
+ }
81
+ NSString *decoded = TFDecodeKey(url).lowercaseString;
82
+ if ([decoded containsString:@"tf_native_"] || [decoded containsString:@"tf_public_"]) TFInvalidURL();
83
+ return url;
84
+ }
85
+ NSString *TFAdmissionCleanURL(NSString *url) { return TFCleanURL(url, nil); }
86
+ NSString *TFNativeDocumentCleanURL(NSString *url, NSString *developmentOrigin) {
87
+ if (developmentOrigin) {
88
+ if (![developmentOrigin isKindOfClass:NSString.class] || ![developmentOrigin hasPrefix:@"http://"]) TFInvalidURL();
89
+ NSString *root = [developmentOrigin stringByAppendingString:@"/"];
90
+ if (![TFNativeDocumentOrigin(root) isEqual:developmentOrigin]) TFInvalidURL();
91
+ TFCleanURL(root, developmentOrigin);
92
+ }
93
+ return TFCleanURL(url, developmentOrigin);
94
+ }
95
+ NSDictionary<NSString *, NSString *> *TFAdmissionStripContext(NSString *url) {
96
+ if (![url isKindOfClass:NSString.class] || url.length > 2048 || [url containsString:@"#"] || [url containsString:@"\\"]) TFInvalidURL();
97
+ NSRange query = [url rangeOfString:@"?"];
98
+ if (query.location == NSNotFound) TFInvalidURL();
99
+ NSArray<NSString *> *parts = [[url substringFromIndex:query.location + 1] componentsSeparatedByString:@"&"];
100
+ NSMutableArray<NSString *> *remaining = [NSMutableArray array];
101
+ NSString *context = nil;
102
+ for (NSString *part in parts) {
103
+ NSString *key = [part componentsSeparatedByString:@"="].firstObject;
104
+ if ([TFDecodeKey(key).lowercaseString hasPrefix:@"__tf_native"]) {
105
+ if (context || ![part hasPrefix:@"__tf_native_context="]) TFInvalidURL();
106
+ context = [part substringFromIndex:@"__tf_native_context=".length];
107
+ if (!TFAdmissionValidToken(context)) TFInvalidURL();
108
+ } else [remaining addObject:part];
109
+ }
110
+ if (!context) TFInvalidURL();
111
+ NSString *clean = [[url substringToIndex:query.location] stringByAppendingString:remaining.count ? [@"?" stringByAppendingString:[remaining componentsJoinedByString:@"&"]] : @""];
112
+ return @{@"context": context, @"url": TFAdmissionCleanURL(clean)};
113
+ }
114
+ NSError *TFAdmissionSafeError(void) {
115
+ return [NSError errorWithDomain:@"TileflowNativeAdmission" code:1 userInfo:@{NSLocalizedDescriptionKey: @"Native resource admission failed"}];
116
+ }
@@ -0,0 +1,9 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ // Private request metadata, never an HTTP header or URL parameter.
6
+ FOUNDATION_EXPORT void TFDocumentSetByteLimit(NSMutableURLRequest *request, NSUInteger maximumBytes);
7
+ FOUNDATION_EXPORT NSUInteger TFDocumentResponseByteLimit(NSURLRequest *request, NSUInteger transportMaximum);
8
+
9
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,19 @@
1
+ #import "TFDocumentBounds.h"
2
+ #import <cmath>
3
+
4
+ static NSString *const TFDocumentLimitKey = @"dev.tileflow.native.document-byte-limit";
5
+ static void TFInvalidDocumentLimit(void) {
6
+ @throw [NSException exceptionWithName:@"TFNativeDocumentInvalid" reason:@"Native document acquisition failed." userInfo:nil];
7
+ }
8
+ void TFDocumentSetByteLimit(NSMutableURLRequest *request, NSUInteger maximumBytes) {
9
+ if (!request || !maximumBytes || maximumBytes > 8388608) TFInvalidDocumentLimit();
10
+ [NSURLProtocol setProperty:@(maximumBytes) forKey:TFDocumentLimitKey inRequest:request];
11
+ }
12
+ NSUInteger TFDocumentResponseByteLimit(NSURLRequest *request, NSUInteger transportMaximum) {
13
+ id value = [NSURLProtocol propertyForKey:TFDocumentLimitKey inRequest:request];
14
+ if (!value) return transportMaximum;
15
+ if (![value isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID() ||
16
+ !std::isfinite([value doubleValue]) || std::floor([value doubleValue]) != [value doubleValue] ||
17
+ [value doubleValue] < 1 || [value doubleValue] > 8388608) TFInvalidDocumentLimit();
18
+ return MIN(transportMaximum, [value unsignedIntegerValue]);
19
+ }
@@ -0,0 +1,23 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /** Application data only; not a Map, session or transport identity. */
6
+ @interface TFMobileConfiguration : NSObject
7
+ @property(nonatomic, copy, readonly) NSString *apiOrigin;
8
+ @property(nonatomic, copy, readonly) NSString *credential;
9
+ - (instancetype)init NS_UNAVAILABLE;
10
+ + (instancetype)new NS_UNAVAILABLE;
11
+ + (nullable instancetype)parse:(nullable id)values error:(NSError *_Nullable *_Nullable)error;
12
+ + (nullable instancetype)fromInfo:(nullable NSDictionary *)info
13
+ localized:(nullable NSDictionary *)localized
14
+ error:(NSError *_Nullable *_Nullable)error;
15
+ @end
16
+
17
+ /** A synchronized process snapshot. Source providers and React contexts are never retained. */
18
+ @interface TFMobileConfigurationCache : NSObject
19
+ - (nullable TFMobileConfiguration *)read:(TFMobileConfiguration *_Nullable (^)(void))source
20
+ error:(NSError *_Nullable *_Nullable)error;
21
+ @end
22
+
23
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,139 @@
1
+ #import "TFMobileConfiguration.h"
2
+
3
+ static NSError *TFMobileConfigurationFailure(void) {
4
+ return [NSError errorWithDomain:@"TileflowNativeConfiguration" code:1 userInfo:@{
5
+ NSLocalizedDescriptionKey: @"Native application configuration is invalid."
6
+ }];
7
+ }
8
+
9
+ static BOOL TFMobileMatches(NSString *value, NSString *pattern) {
10
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
11
+ NSTextCheckingResult *match = [expression firstMatchInString:value options:0 range:NSMakeRange(0, value.length)];
12
+ return match != nil && NSEqualRanges(match.range, NSMakeRange(0, value.length));
13
+ }
14
+
15
+ /** Keep this narrow lexical grammar identical to MobileConfiguration.kt and mobile-configuration.ts. */
16
+ static NSString *TFMobileOrigin(NSString *value) {
17
+ if (value.length == 0 || value.length > 2048) return nil;
18
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:
19
+ @"\\A[Hh][Tt][Tt][Pp][Ss]://([A-Za-z0-9.-]+)(?::([1-9][0-9]{0,4}))?/?\\z" options:0 error:nil];
20
+ NSTextCheckingResult *match = [expression firstMatchInString:value options:0 range:NSMakeRange(0, value.length)];
21
+ if (!match || !NSEqualRanges(match.range, NSMakeRange(0, value.length))) return nil;
22
+ NSString *host = [[value substringWithRange:[match rangeAtIndex:1]] lowercaseString];
23
+ NSArray<NSString *> *labels = [host componentsSeparatedByString:@"."];
24
+ if (host.length > 253) return nil;
25
+ for (NSString *label in labels) {
26
+ if (!TFMobileMatches(label, @"\\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\z")) return nil;
27
+ }
28
+ unichar first = [labels.lastObject characterAtIndex:0];
29
+ if (first < 'a' || first > 'z') {
30
+ if (labels.count != 4) return nil;
31
+ for (NSString *label in labels) {
32
+ if (!TFMobileMatches(label, @"\\A(?:0|[1-9][0-9]{0,2})\\z") || label.integerValue > 255) return nil;
33
+ }
34
+ }
35
+ NSRange portRange = [match rangeAtIndex:2];
36
+ NSInteger port = portRange.location == NSNotFound ? 443 : [value substringWithRange:portRange].integerValue;
37
+ if (port > 65535) return nil;
38
+ return port == 443 ? [@"https://" stringByAppendingString:host] : [NSString stringWithFormat:@"https://%@:%ld", host, (long)port];
39
+ }
40
+
41
+ @interface TFMobileConfiguration ()
42
+ - (instancetype)initWithOrigin:(NSString *)origin credential:(NSString *)credential;
43
+ @end
44
+
45
+ @implementation TFMobileConfiguration
46
+ - (instancetype)initWithOrigin:(NSString *)origin credential:(NSString *)credential {
47
+ if ((self = [super init])) {
48
+ _apiOrigin = [origin copy];
49
+ _credential = [credential copy];
50
+ }
51
+ return self;
52
+ }
53
+ - (NSString *)description { return @"TFMobileConfiguration"; }
54
+ - (NSString *)debugDescription { return @"TFMobileConfiguration"; }
55
+
56
+ + (instancetype)parse:(id)input error:(NSError **)error {
57
+ if (error) *error = nil;
58
+ @try {
59
+ if (![input isKindOfClass:NSArray.class] || [input count] != 2) {
60
+ if (error) *error = TFMobileConfigurationFailure();
61
+ return nil;
62
+ }
63
+ NSString *origin = nil;
64
+ NSString *credential = nil;
65
+ for (id item in (NSArray *)input) {
66
+ if (![item isKindOfClass:NSString.class] || [item length] > 2080) {
67
+ if (error) *error = TFMobileConfigurationFailure();
68
+ return nil;
69
+ }
70
+ NSString *text = item;
71
+ NSRange separator = [text rangeOfString:@"="];
72
+ if (separator.location == NSNotFound || separator.location == 0) {
73
+ if (error) *error = TFMobileConfigurationFailure();
74
+ return nil;
75
+ }
76
+ NSString *key = [text substringToIndex:separator.location];
77
+ NSString *value = [text substringFromIndex:separator.location + 1];
78
+ if ([key isEqualToString:@"apiOrigin"] && !origin) {
79
+ origin = TFMobileOrigin(value);
80
+ if (origin) continue;
81
+ } else if ([key isEqualToString:@"credential"] && !credential) {
82
+ if (value.length == 58 && TFMobileMatches(value, @"\\Atf_public_[0-9a-f]{48}\\z")) {
83
+ credential = [value copy];
84
+ continue;
85
+ }
86
+ }
87
+ // Missing, repeated, unknown or malformed fields have the same safe failure.
88
+ if (error) *error = TFMobileConfigurationFailure();
89
+ return nil;
90
+ }
91
+ if (origin && credential) return [[self alloc] initWithOrigin:origin credential:credential];
92
+ } @catch (NSException *exception) {
93
+ // Do not inspect or forward an exception that could contain a configured value.
94
+ (void)exception;
95
+ }
96
+ if (error) *error = TFMobileConfigurationFailure();
97
+ return nil;
98
+ }
99
+
100
+ + (instancetype)fromInfo:(NSDictionary *)info localized:(NSDictionary *)localized error:(NSError **)error {
101
+ if (error) *error = nil;
102
+ @try {
103
+ if ([info isKindOfClass:NSDictionary.class] &&
104
+ (!localized || [localized isKindOfClass:NSDictionary.class]) &&
105
+ !localized[@"TileflowMobileConfiguration"]) {
106
+ return [self parse:info[@"TileflowMobileConfiguration"] error:error];
107
+ }
108
+ } @catch (NSException *exception) { (void)exception; }
109
+ if (error) *error = TFMobileConfigurationFailure();
110
+ return nil;
111
+ }
112
+ @end
113
+
114
+ @implementation TFMobileConfigurationCache {
115
+ NSInteger _phase;
116
+ TFMobileConfiguration *_configuration;
117
+ }
118
+ - (TFMobileConfiguration *)read:(TFMobileConfiguration *(^)(void))source error:(NSError **)error {
119
+ if (error) *error = nil;
120
+ @synchronized (self) {
121
+ if (_phase == 2) return _configuration;
122
+ if (_phase == 0) {
123
+ _phase = 1;
124
+ @try {
125
+ TFMobileConfiguration *value = source();
126
+ if (value && _phase == 1) {
127
+ _configuration = value;
128
+ _phase = 2;
129
+ return value;
130
+ }
131
+ } @catch (NSException *exception) { (void)exception; }
132
+ }
133
+ _phase = 3;
134
+ _configuration = nil;
135
+ if (error) *error = TFMobileConfigurationFailure();
136
+ return nil;
137
+ }
138
+ }
139
+ @end
@@ -0,0 +1,69 @@
1
+ #import "TFAdmissionInstallation.h"
2
+ #import <atomic>
3
+
4
+ static __weak TFAdmissionInstallation *TFActiveInstallation;
5
+
6
+ @interface TFNativeAdmissionURLProtocol () {
7
+ std::atomic_bool _stopped;
8
+ }
9
+ @property (atomic, strong, nullable) id<TFAdmissionCancel> cancellation;
10
+ @end
11
+
12
+ @implementation TFNativeAdmissionURLProtocol
13
+ + (TFAdmissionInstallation *)activeInstallation {
14
+ @synchronized(self) { return TFActiveInstallation; }
15
+ }
16
+ + (void)setActiveInstallation:(TFAdmissionInstallation *)installation {
17
+ @synchronized(self) { TFActiveInstallation = installation; }
18
+ }
19
+ + (BOOL)canInitWithRequest:(NSURLRequest *)request {
20
+ // This class is installed only in MapLibre's configuration. Reserved
21
+ // requests remain ours even after retirement or loss of installation.
22
+ return TFAdmissionHasReservedContext(request.URL.absoluteString);
23
+ }
24
+ + (BOOL)canInitWithTask:(NSURLSessionTask *)task {
25
+ return [self canInitWithRequest:task.currentRequest ?: task.originalRequest];
26
+ }
27
+ + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
28
+ // Preserve the non-secret discriminator in MapLibre's cache identity.
29
+ return request;
30
+ }
31
+ - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
32
+ if ((self = [super initWithRequest:request cachedResponse:cachedResponse client:client])) _stopped.store(false);
33
+ return self;
34
+ }
35
+ - (void)startLoading {
36
+ TFAdmissionInstallation *installation = [TFNativeAdmissionURLProtocol activeInstallation];
37
+ if (!installation) {
38
+ if (!_stopped.exchange(true)) [self.client URLProtocol:self didFailWithError:TFAdmissionSafeError()];
39
+ return;
40
+ }
41
+ __weak TFNativeAdmissionURLProtocol *weakSelf = self;
42
+ self.cancellation = [installation.engine request:self.request
43
+ response:^(NSHTTPURLResponse *response, NSData *body) {
44
+ TFNativeAdmissionURLProtocol *strongSelf = weakSelf;
45
+ if (!strongSelf || strongSelf->_stopped.load()) return;
46
+ [strongSelf.client URLProtocol:strongSelf didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
47
+ if (!strongSelf->_stopped.load()) [strongSelf.client URLProtocol:strongSelf didLoadData:body];
48
+ if (!strongSelf->_stopped.load()) [strongSelf.client URLProtocolDidFinishLoading:strongSelf];
49
+ strongSelf->_stopped.store(true); strongSelf.cancellation = nil;
50
+ }
51
+ failure:^{
52
+ TFNativeAdmissionURLProtocol *strongSelf = weakSelf;
53
+ if (strongSelf && !strongSelf->_stopped.exchange(true)) [strongSelf.client URLProtocol:strongSelf didFailWithError:TFAdmissionSafeError()];
54
+ strongSelf.cancellation = nil;
55
+ }
56
+ delegate:^id<TFAdmissionCancel>(NSURLRequest *request, TFAdmissionNetworkCompletion completion) {
57
+ return [installation delegateRequest:request mayStart:^BOOL {
58
+ TFNativeAdmissionURLProtocol *strongSelf = weakSelf;
59
+ return strongSelf && !strongSelf->_stopped.load() && [installation.engine isOwner];
60
+ } completion:completion];
61
+ }];
62
+ if (_stopped.load()) [self.cancellation cancel];
63
+ }
64
+ - (void)stopLoading {
65
+ _stopped.store(true);
66
+ [self.cancellation cancel];
67
+ self.cancellation = nil;
68
+ }
69
+ @end