1 //  Copyright © 2019 650 Industries. All rights reserved.
2 
3 // swiftlint:disable identifier_name
4 // swiftlint:disable legacy_objc_type
5 
6 // this class uses an abstract class pattern
7 // swiftlint:disable unavailable_function
8 
9 import Foundation
10 
11 /**
12  * Subclass of AppLoader which handles copying the embedded update's assets into the
13  * expo-updates cache location.
14  *
15  * Rather than launching the embedded update directly from its location in the app bundle/apk, we
16  * first try to read it into the expo-updates cache and database and launch it like any other
17  * update. The benefits of this include (a) a single code path for launching most updates and (b)
18  * assets included in embedded updates and copied into the cache in this way do not need to be
19  * redownloaded if included in future updates.
20  */
21 @objc(EXUpdatesEmbeddedAppLoader)
22 @objcMembers
23 public final class EmbeddedAppLoader: AppLoader {
24   public static let EXUpdatesEmbeddedManifestName = "app"
25   public static let EXUpdatesEmbeddedManifestType = "manifest"
26   public static let EXUpdatesEmbeddedBundleFilename = "app"
27   public static let EXUpdatesEmbeddedBundleFileType = "bundle"
28   public static let EXUpdatesBareEmbeddedBundleFilename = "main"
29   public static let EXUpdatesBareEmbeddedBundleFileType = "jsbundle"
30 
31   private static let ErrorDomain = "EXUpdatesEmbeddedAppLoader"
32 
33   private static var embeddedManifestInternal: Update?
embeddedManifestnull34   public static func embeddedManifest(withConfig config: UpdatesConfig, database: UpdatesDatabase?) -> Update? {
35     guard config.hasEmbeddedUpdate else {
36       return nil
37     }
38     if let embeddedManifestInternal = embeddedManifestInternal {
39       return embeddedManifestInternal
40     }
41 
42     var manifestNSData: NSData?
43 
44     let frameworkBundle = Bundle(for: EmbeddedAppLoader.self)
45     if let resourceUrl = frameworkBundle.resourceURL,
46       let bundle = Bundle(url: resourceUrl.appendingPathComponent("EXUpdates.bundle")),
47       let path = bundle.path(
48         forResource: EmbeddedAppLoader.EXUpdatesEmbeddedManifestName,
49         ofType: EmbeddedAppLoader.EXUpdatesEmbeddedManifestType
50       ) {
51       manifestNSData = NSData(contentsOfFile: path)
52     }
53 
54     // Fallback to main bundle if the embedded manifest is not found in EXUpdates.bundle. This is a special case
55     // to support the existing structure of Expo "shell apps"
56     if manifestNSData == nil,
57       let path = Bundle.main.path(
58         forResource: EmbeddedAppLoader.EXUpdatesEmbeddedManifestName,
59         ofType: EmbeddedAppLoader.EXUpdatesEmbeddedManifestType
60       ) {
61       manifestNSData = NSData(contentsOfFile: path)
62     }
63 
64     let manifestData = manifestNSData.let { it in
65       it as Data
66     }
67 
68     // Not found in EXUpdates.bundle or main bundle
69     guard let manifestData = manifestData else {
70       NSException(
71         name: .internalInconsistencyException,
72         reason: "The embedded manifest is invalid or could not be read. Make sure you have configured expo-updates correctly in your Xcode Build Phases."
73       )
74       .raise()
75       return nil
76     }
77 
78     guard let manifest = try? JSONSerialization.jsonObject(with: manifestData) else {
79       NSException(
80         name: .internalInconsistencyException,
81         reason: "The embedded manifest is invalid or could not be read. Make sure you have configured expo-updates correctly in your Xcode Build Phases."
82       )
83       .raise()
84       return nil
85     }
86 
87     guard let manifestDictionary = manifest as? [String: Any] else {
88       NSException(
89         name: .internalInconsistencyException,
90         reason: "embedded manifest should be a valid JSON file"
91       )
92       .raise()
93       return nil
94     }
95 
96     var mutableManifest = manifestDictionary
97     // automatically verify embedded manifest since it was already codesigned
98     mutableManifest["isVerified"] = true
99     embeddedManifestInternal = Update.update(withEmbeddedManifest: mutableManifest, config: config, database: database)
100     return embeddedManifestInternal
101   }
102 
103   internal func loadUpdateResponseFromEmbeddedManifest(
104     withCallback updateResponseBlock: @escaping AppLoaderUpdateResponseBlock,
105     asset assetBlock: @escaping AppLoaderAssetBlock,
106     success successBlock: @escaping AppLoaderSuccessBlock,
107     error errorBlock: @escaping AppLoaderErrorBlock
108   ) {
109     guard let embeddedManifest = EmbeddedAppLoader.embeddedManifest(withConfig: config, database: database) else {
110       errorBlock(NSError(
111         domain: EmbeddedAppLoader.ErrorDomain,
112         code: 1008,
113         userInfo: [
114           NSLocalizedDescriptionKey: "Failed to load embedded manifest. Make sure you have configured expo-updates correctly."
115         ]
116       ))
117       return
118     }
119 
120     self.updateResponseBlock = updateResponseBlock
121     self.assetBlock = assetBlock
122     self.successBlock = successBlock
123     self.errorBlock = errorBlock
124     startLoading(fromUpdateResponse: UpdateResponse(
125       responseHeaderData: nil,
126       manifestUpdateResponsePart: ManifestUpdateResponsePart(updateManifest: embeddedManifest),
127       directiveUpdateResponsePart: nil
128     ))
129   }
130 
downloadAssetnull131   override public func downloadAsset(_ asset: UpdateAsset) {
132     let destinationUrl = directory.appendingPathComponent(asset.filename)
133     FileDownloader.assetFilesQueue.async {
134       if FileManager.default.fileExists(atPath: destinationUrl.path) {
135         DispatchQueue.global().async {
136           self.handleAssetDownloadAlreadyExists(asset)
137         }
138       } else {
139         let mainBundleFilename = asset.mainBundleFilename.require("embedded asset mainBundleFilename must be nonnull")
140         let bundlePath = UpdatesUtils.path(forBundledAsset: asset).require(
141           String(
142             format: "Could not find the expected embedded asset in NSBundle %@.%@. " +
143               "Check that expo-updates is installed correctly, and verify that assets are present in the ipa file.",
144             mainBundleFilename,
145             asset.type ?? ""
146           )
147         )
148 
149         do {
150           try FileManager.default.copyItem(atPath: bundlePath, toPath: destinationUrl.path)
151           let data = try NSData(contentsOfFile: bundlePath) as Data
152           DispatchQueue.global().async {
153             self.handleAssetDownload(withData: data, response: nil, asset: asset)
154           }
155         } catch {
156           DispatchQueue.global().async {
157             self.handleAssetDownload(withError: error, asset: asset)
158           }
159         }
160       }
161     }
162   }
163 
164   override public func loadUpdate(
165     fromURL url: URL,
166     onUpdateResponse updateResponseBlock: @escaping AppLoaderUpdateResponseBlock,
167     asset assetBlock: @escaping AppLoaderAssetBlock,
168     success successBlock: @escaping AppLoaderSuccessBlock,
169     error errorBlock: @escaping AppLoaderErrorBlock
170   ) {
171     preconditionFailure("Should not call EmbeddedAppLoader#loadUpdateFromUrl")
172   }
173 }
174