1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import "ABI47_0_0RCTJavaScriptLoader.h"
9
10#import <sys/stat.h>
11
12#import <ABI47_0_0cxxreact/ABI47_0_0JSBundleType.h>
13
14#import "ABI47_0_0RCTBridge.h"
15#import "ABI47_0_0RCTConvert.h"
16#import "ABI47_0_0RCTMultipartDataTask.h"
17#import "ABI47_0_0RCTPerformanceLogger.h"
18#import "ABI47_0_0RCTUtils.h"
19
20NSString *const ABI47_0_0RCTJavaScriptLoaderErrorDomain = @"ABI47_0_0RCTJavaScriptLoaderErrorDomain";
21
22const uint32_t ABI47_0_0RCT_BYTECODE_ALIGNMENT = 4;
23
24@interface ABI47_0_0RCTSource () {
25 @public
26  NSURL *_url;
27  NSData *_data;
28  NSUInteger _length;
29  NSInteger _filesChangedCount;
30}
31
32@end
33
34@implementation ABI47_0_0RCTSource
35
36static ABI47_0_0RCTSource *ABI47_0_0RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_RETURNS_RETAINED
37{
38  using ABI47_0_0facebook::ABI47_0_0React::ScriptTag;
39  ABI47_0_0facebook::ABI47_0_0React::BundleHeader header;
40  [data getBytes:&header length:sizeof(header)];
41
42  ABI47_0_0RCTSource *source = [ABI47_0_0RCTSource new];
43  source->_url = url;
44  // Multipart responses may give us an unaligned view into the buffer. This ensures memory is aligned.
45  if (parseTypeFromHeader(header) == ScriptTag::MetroHBCBundle && ((long)[data bytes] % ABI47_0_0RCT_BYTECODE_ALIGNMENT)) {
46    source->_data = [[NSData alloc] initWithData:data];
47  } else {
48    source->_data = data;
49  }
50  source->_length = length;
51  source->_filesChangedCount = ABI47_0_0RCTSourceFilesChangedCountNotBuiltByBundler;
52  return source;
53}
54
55@end
56
57@implementation ABI47_0_0RCTLoadingProgress
58
59- (NSString *)description
60{
61  NSMutableString *desc = [NSMutableString new];
62  [desc appendString:_status ?: @"Bundling"];
63
64  if ([_total integerValue] > 0 && [_done integerValue] > [_total integerValue]) {
65    [desc appendFormat:@" %ld%%", (long)100];
66  } else if ([_total integerValue] > 0) {
67    [desc appendFormat:@" %ld%%", (long)(100 * [_done integerValue] / [_total integerValue])];
68  } else {
69    [desc appendFormat:@" %ld%%", (long)0];
70  }
71  [desc appendString:@"\u2026"];
72  return desc;
73}
74
75@end
76
77@implementation ABI47_0_0RCTJavaScriptLoader
78
79ABI47_0_0RCT_NOT_IMPLEMENTED(-(instancetype)init)
80
81+ (void)loadBundleAtURL:(NSURL *)scriptURL
82             onProgress:(ABI47_0_0RCTSourceLoadProgressBlock)onProgress
83             onComplete:(ABI47_0_0RCTSourceLoadBlock)onComplete
84{
85  int64_t sourceLength;
86  NSError *error;
87  NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL sourceLength:&sourceLength error:&error];
88  if (data) {
89    onComplete(nil, ABI47_0_0RCTSourceCreate(scriptURL, data, sourceLength));
90    return;
91  }
92
93  const BOOL isCannotLoadSyncError = [error.domain isEqualToString:ABI47_0_0RCTJavaScriptLoaderErrorDomain] &&
94      error.code == ABI47_0_0RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously;
95
96  if (isCannotLoadSyncError) {
97    attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete);
98  } else {
99    onComplete(error, nil);
100  }
101}
102
103+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
104                                   sourceLength:(int64_t *)sourceLength
105                                          error:(NSError **)error
106{
107  NSString *unsanitizedScriptURLString = scriptURL.absoluteString;
108  // Sanitize the script URL
109  scriptURL = sanitizeURL(scriptURL);
110
111  if (!scriptURL) {
112    if (error) {
113      *error = [NSError
114          errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
115                     code:ABI47_0_0RCTJavaScriptLoaderErrorNoScriptURL
116                 userInfo:@{
117                   NSLocalizedDescriptionKey : [NSString
118                       stringWithFormat:@"No script URL provided. Make sure the packager is "
119                                        @"running or you have embedded a JS bundle in your application bundle.\n\n"
120                                        @"unsanitizedScriptURLString = %@",
121                                        unsanitizedScriptURLString]
122                 }];
123    }
124    return nil;
125  }
126
127  // Load local script file
128  if (!scriptURL.fileURL) {
129    if (error) {
130      *error = [NSError errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
131                                   code:ABI47_0_0RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
132                               userInfo:@{
133                                 NSLocalizedDescriptionKey :
134                                     [NSString stringWithFormat:@"Cannot load %@ URLs synchronously", scriptURL.scheme]
135                               }];
136    }
137    return nil;
138  }
139
140  // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle).
141  // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`.
142  // The benefit of RAM bundle over a regular bundle is that we can lazily inject
143  // modules into JSC as they're required.
144  FILE *bundle = fopen(scriptURL.path.UTF8String, "r");
145  if (!bundle) {
146    if (error) {
147      *error = [NSError
148          errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
149                     code:ABI47_0_0RCTJavaScriptLoaderErrorFailedOpeningFile
150                 userInfo:@{
151                   NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]
152                 }];
153    }
154    return nil;
155  }
156
157  ABI47_0_0facebook::ABI47_0_0React::BundleHeader header;
158  size_t readResult = fread(&header, sizeof(header), 1, bundle);
159  fclose(bundle);
160  if (readResult != 1) {
161    if (error) {
162      *error = [NSError
163          errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
164                     code:ABI47_0_0RCTJavaScriptLoaderErrorFailedReadingFile
165                 userInfo:@{
166                   NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]
167                 }];
168    }
169    return nil;
170  }
171
172  ABI47_0_0facebook::ABI47_0_0React::ScriptTag tag = ABI47_0_0facebook::ABI47_0_0React::parseTypeFromHeader(header);
173  switch (tag) {
174    case ABI47_0_0facebook::ABI47_0_0React::ScriptTag::MetroHBCBundle:
175    case ABI47_0_0facebook::ABI47_0_0React::ScriptTag::RAMBundle:
176      break;
177
178    case ABI47_0_0facebook::ABI47_0_0React::ScriptTag::String: {
179#if ABI47_0_0RCT_ENABLE_INSPECTOR
180      NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:error];
181      if (sourceLength && source != nil) {
182        *sourceLength = source.length;
183      }
184      return source;
185#else
186      if (error) {
187        *error =
188            [NSError errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
189                                code:ABI47_0_0RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
190                            userInfo:@{NSLocalizedDescriptionKey : @"Cannot load text/javascript files synchronously"}];
191      }
192      return nil;
193#endif
194    }
195  }
196
197  struct stat statInfo;
198  if (stat(scriptURL.path.UTF8String, &statInfo) != 0) {
199    if (error) {
200      *error = [NSError
201          errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
202                     code:ABI47_0_0RCTJavaScriptLoaderErrorFailedStatingFile
203                 userInfo:@{
204                   NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path]
205                 }];
206    }
207    return nil;
208  }
209  if (sourceLength) {
210    *sourceLength = statInfo.st_size;
211  }
212  return [NSData dataWithBytes:&header length:sizeof(header)];
213}
214
215static void parseHeaders(NSDictionary *headers, ABI47_0_0RCTSource *source)
216{
217  source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue];
218}
219
220static void attemptAsynchronousLoadOfBundleAtURL(
221    NSURL *scriptURL,
222    ABI47_0_0RCTSourceLoadProgressBlock onProgress,
223    ABI47_0_0RCTSourceLoadBlock onComplete)
224{
225  scriptURL = sanitizeURL(scriptURL);
226
227  if (scriptURL.fileURL) {
228    // Reading in a large bundle can be slow. Dispatch to the background queue to do it.
229    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
230      NSError *error = nil;
231      NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:&error];
232      onComplete(error, ABI47_0_0RCTSourceCreate(scriptURL, source, source.length));
233    });
234    return;
235  }
236
237  ABI47_0_0RCTMultipartDataTask *task = [[ABI47_0_0RCTMultipartDataTask alloc] initWithURL:scriptURL
238      partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) {
239        if (!done) {
240          if (onProgress) {
241            onProgress(progressEventFromData(data));
242          }
243          return;
244        }
245
246        // Handle general request errors
247        if (error) {
248          if ([error.domain isEqualToString:NSURLErrorDomain]) {
249            error = [NSError
250                errorWithDomain:ABI47_0_0RCTJavaScriptLoaderErrorDomain
251                           code:ABI47_0_0RCTJavaScriptLoaderErrorURLLoadFailed
252                       userInfo:@{
253                         NSLocalizedDescriptionKey :
254                             [@"Could not connect to development server.\n\n"
255                               "Ensure the following:\n"
256                               "- Node server is running and available on the same network - run 'npm start' from ABI47_0_0React-native root\n"
257                               "- Node server URL is correctly set in AppDelegate\n"
258                               "- WiFi is enabled and connected to the same network as the Node Server\n\n"
259                               "URL: " stringByAppendingString:scriptURL.absoluteString],
260                         NSLocalizedFailureReasonErrorKey : error.localizedDescription,
261                         NSUnderlyingErrorKey : error,
262                       }];
263          }
264          onComplete(error, nil);
265          return;
266        }
267
268        // For multipart responses packager sets X-Http-Status header in case HTTP status code
269        // is different from 200 OK
270        NSString *statusCodeHeader = headers[@"X-Http-Status"];
271        if (statusCodeHeader) {
272          statusCode = [statusCodeHeader integerValue];
273        }
274
275        if (statusCode != 200) {
276          error =
277              [NSError errorWithDomain:@"JSServer"
278                                  code:statusCode
279                              userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data
280                                                                                    encoding:NSUTF8StringEncoding])];
281          onComplete(error, nil);
282          return;
283        }
284
285        // Validate that the packager actually returned javascript.
286        NSString *contentType = headers[@"Content-Type"];
287        NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject];
288        if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"] &&
289            ![mimeType isEqualToString:@"application/x-metro-bytecode-bundle"]) {
290          NSString *description;
291          if ([mimeType isEqualToString:@"application/json"]) {
292            NSError *parseError;
293            NSDictionary *jsonError = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
294            if (!parseError && [jsonError isKindOfClass:[NSDictionary class]] &&
295                [[jsonError objectForKey:@"message"] isKindOfClass:[NSString class]] &&
296                [[jsonError objectForKey:@"message"] length]) {
297              description = [jsonError objectForKey:@"message"];
298            } else {
299              description = [NSString stringWithFormat:@"Unknown error fetching '%@'.", scriptURL.absoluteString];
300            }
301          } else {
302            description = [NSString
303                stringWithFormat:
304                    @"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", mimeType];
305          }
306
307          error = [NSError
308              errorWithDomain:@"JSServer"
309                         code:NSURLErrorCannotParseResponse
310                     userInfo:@{NSLocalizedDescriptionKey : description, @"headers" : headers, @"data" : data}];
311          onComplete(error, nil);
312          return;
313        }
314
315        ABI47_0_0RCTSource *source = ABI47_0_0RCTSourceCreate(scriptURL, data, data.length);
316        parseHeaders(headers, source);
317        onComplete(nil, source);
318      }
319      progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) {
320        // Only care about download progress events for the javascript bundle part.
321        if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"] ||
322            [headers[@"Content-Type"] isEqualToString:@"application/x-metro-bytecode-bundle"]) {
323          onProgress(progressEventFromDownloadProgress(loaded, total));
324        }
325      }];
326
327  [task startTask];
328}
329
330static NSURL *sanitizeURL(NSURL *url)
331{
332  // Why we do this is lost to time. We probably shouldn't; passing a valid URL is the caller's responsibility not ours.
333  return [ABI47_0_0RCTConvert NSURL:url.absoluteString];
334}
335
336static ABI47_0_0RCTLoadingProgress *progressEventFromData(NSData *rawData)
337{
338  NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
339  id info = ABI47_0_0RCTJSONParse(text, nil);
340  if (!info || ![info isKindOfClass:[NSDictionary class]]) {
341    return nil;
342  }
343
344  ABI47_0_0RCTLoadingProgress *progress = [ABI47_0_0RCTLoadingProgress new];
345  progress.status = info[@"status"];
346  progress.done = info[@"done"];
347  progress.total = info[@"total"];
348  return progress;
349}
350
351static ABI47_0_0RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done)
352{
353  ABI47_0_0RCTLoadingProgress *progress = [ABI47_0_0RCTLoadingProgress new];
354  progress.status = @"Downloading";
355  // Progress values are in bytes transform them to kilobytes for smaller numbers.
356  progress.done = done != nil ? @([done integerValue] / 1024) : nil;
357  progress.total = total != nil ? @([total integerValue] / 1024) : nil;
358  return progress;
359}
360
361static NSDictionary *userInfoForRawResponse(NSString *rawText)
362{
363  NSDictionary *parsedResponse = ABI47_0_0RCTJSONParse(rawText, nil);
364  if (![parsedResponse isKindOfClass:[NSDictionary class]]) {
365    return @{NSLocalizedDescriptionKey : rawText};
366  }
367  NSArray *errors = parsedResponse[@"errors"];
368  if (![errors isKindOfClass:[NSArray class]]) {
369    return @{NSLocalizedDescriptionKey : rawText};
370  }
371  NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new];
372  for (NSDictionary *err in errors) {
373    [fakeStack addObject:@{
374      @"methodName" : err[@"description"] ?: @"",
375      @"file" : err[@"filename"] ?: @"",
376      @"lineNumber" : err[@"lineNumber"] ?: @0
377    }];
378  }
379  return
380      @{NSLocalizedDescriptionKey : parsedResponse[@"message"] ?: @"No message provided", @"stack" : [fakeStack copy]};
381}
382
383@end
384