1 //  Copyright © 2019 650 Industries. All rights reserved.
2 
3 import Foundation
4 import ABI49_0_0EXManifests
5 
6 internal final class LegacyUpdate: Update {
7   /**
8    * Method for initializing updates with manifests in the classic format (i.e. come from Expo's
9    * classic updates service or a self-hosted service following the classic updates format, such as
10    * one making use of `expo-cli export`).
11    *
12    * Asset URLs are relative in this format, and we assume that if no base URL is explicitly provided,
13    * the base URL is Expo's classic asset CDN.
14    */
15   static func update(
16     withLegacyManifest: LegacyManifest,
17     config: UpdatesConfig,
18     database: UpdatesDatabase?
19   ) -> Update {
20     let manifest = withLegacyManifest
21 
22     var updateId: UUID
23     var commitTime: Date
24     if manifest.isUsingDeveloperTool() {
25       // XDL does not set a releaseId or commitTime for development manifests.
26       // we do not need these so we just stub them out
27       updateId = UUID()
28       commitTime = Date()
29     } else {
30       updateId = UUID(uuidString: manifest.releaseID()).require("updateId should be a valid UUID")
31       let commitTimeString = manifest.commitTime()
32       commitTime = ABI49_0_0RCTConvert.nsDate(commitTimeString)
33     }
34 
35     var isDevelopmentMode: Bool = false
36     var status: UpdateStatus
37     if manifest.isDevelopmentMode() {
38       isDevelopmentMode = true
39       status = UpdateStatus.StatusDevelopment
40     } else {
41       status = UpdateStatus.StatusPending
42     }
43 
44     let bundleUrlString = manifest.bundleUrl()
45     let assets = manifest.bundledAssets() ?? []
46 
47     var runtimeVersion: String
48     let manifestRuntimeVersion = manifest.runtimeVersion()
49     if let manifestRuntimeVersion = manifestRuntimeVersion as? String {
50       runtimeVersion = manifestRuntimeVersion
51     } else {
52       runtimeVersion = manifest.expoGoSDKVersion().require("Manifest JSON must have either a valid runtimeVersion property or a valid sdkVersion property")
53     }
54 
55     let bundleUrl = URL(string: bundleUrlString).require("Manifest JSON must have a valid URL as the bundleUrl property")
56 
57     var processedAssets: [UpdateAsset] = []
58 
59     let bundleKey = manifest.bundleKey()
60     let jsBundleAsset = UpdateAsset(key: bundleKey, type: EmbeddedAppLoader.ABI49_0_0EXUpdatesEmbeddedBundleFileType)
61     jsBundleAsset.url = bundleUrl
62     jsBundleAsset.isLaunchAsset = true
63     jsBundleAsset.mainBundleFilename = EmbeddedAppLoader.ABI49_0_0EXUpdatesEmbeddedBundleFilename
64     processedAssets.append(jsBundleAsset)
65 
66     let bundledAssetBaseUrl = LegacyUpdate.bundledAssetBaseUrl(withManifest: manifest, config: config)
67 
68     assets.forEach { bundledAsset in
69       let bundledAssetString: String = assertType(
70         value: bundledAsset,
71         description: "Manifest JSON bundledAssets property must be an array of strings, found unexpected value"
72       )
73       let extensionStartIndex = bundledAssetString.lastIndex(of: ".")
74       let prefixLength = "asset_".count
75 
76       var filename: String
77       var hash: String
78       var type: String
79 
80       if let extensionStartIndex = extensionStartIndex {
81         filename = String(bundledAssetString[..<extensionStartIndex])
82         let hashStartIndex = bundledAssetString.index(bundledAssetString.startIndex, offsetBy: prefixLength)
83         hash = String(bundledAssetString[hashStartIndex..<extensionStartIndex])
84         let typeStartIndex = bundledAssetString.index(extensionStartIndex, offsetBy: 1)
85         type = String(bundledAssetString[typeStartIndex...])
86       } else {
87         filename = bundledAssetString
88         let bundledAssetStringIndex = bundledAssetString.index(bundledAssetString.startIndex, offsetBy: prefixLength)
89         hash = String(bundledAssetString[bundledAssetStringIndex...])
90         type = ""
91       }
92 
93       let url = bundledAssetBaseUrl.appendingPathComponent(hash)
94       let key = hash
95       let asset = UpdateAsset(key: key, type: type)
96       asset.url = url
97       asset.mainBundleFilename = filename
98 
99       processedAssets.append(asset)
100     }
101 
102     return Update.init(
103       manifest: manifest,
104       config: config,
105       database: database,
106       updateId: updateId,
107       scopeKey: config.scopeKey.require("Must supply scopeKey in configuration"),
108       commitTime: commitTime,
109       runtimeVersion: runtimeVersion,
110       keep: true,
111       status: status,
112       isDevelopmentMode: isDevelopmentMode,
113       assetsFromManifest: processedAssets
114     )
115   }
116 
117   private static let ABI49_0_0EXUpdatesExpoAssetBaseUrl = "https://classic-assets.eascdn.net/~assets/"
118   private static let ABI49_0_0EXUpdatesExpoIoDomain = "expo.io"
119   private static let ABI49_0_0EXUpdatesExpHostDomain = "exp.host"
120   private static let ABI49_0_0EXUpdatesExpoTestDomain = "expo.test"
121 
122   static func bundledAssetBaseUrl(withManifest: LegacyManifest, config: UpdatesConfig) -> URL {
123     let manifestUrl = config.updateUrl
124     let host = manifestUrl?.host
125 
126     guard let host = host else {
127       // The URL is valid and constant, so it'll never throw
128       // swiftlint:disable:next force_unwrapping
129       return URL(string: ABI49_0_0EXUpdatesExpoAssetBaseUrl)!
130     }
131 
132     if host.contains(ABI49_0_0EXUpdatesExpoIoDomain) ||
133       host.contains(ABI49_0_0EXUpdatesExpHostDomain) ||
134       host.contains(ABI49_0_0EXUpdatesExpoTestDomain) {
135       // The URL is valid and constant, so it'll never throw
136       // swiftlint:disable:next force_unwrapping
137       return URL(string: ABI49_0_0EXUpdatesExpoAssetBaseUrl)!
138     }
139 
140     let assetsPathOrUrl = withManifest.assetUrlOverride() ?? "assets"
141     // assetUrlOverride may be an absolute or relative URL
142     // if relative, we should resolve with respect to the manifest URL
143     return URL(string: assetsPathOrUrl, relativeTo: manifestUrl).require(
144       "Invalid assetUrlOverride"
145     ).absoluteURL.standardized
146   }
147 }
148