react-native-instantpay-code-push 1.1.8 → 1.2.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.
@@ -0,0 +1,93 @@
1
+ //
2
+ // VersionedPreferencesService.swift
3
+ // InstantpayCodePush
4
+ //
5
+ // Created by Dhananjay kumar on 09/02/26.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ enum PreferencesError: Error {
11
+ case configurationError
12
+ case setItemError(String)
13
+ case getItemError(String)
14
+ }
15
+
16
+ protocol PreferencesService {
17
+ func setItem(_ value: String?, forKey key: String) throws
18
+ func getItem(forKey key: String) throws -> String?
19
+ }
20
+
21
+ class VersionedPreferencesService: PreferencesService {
22
+
23
+ private let userDefaults: UserDefaults
24
+ private var isolationKey: String = ""
25
+
26
+ let CLASS_TAG = "*PreferencesService"
27
+
28
+ init(userDefaults: UserDefaults = .standard) {
29
+ self.userDefaults = userDefaults
30
+ }
31
+
32
+ /**
33
+ * Configures the service with isolation key.
34
+ * @param isolationKey The complete isolation key to use for storage
35
+ */
36
+ func configure(isolationKey: String) {
37
+ self.isolationKey = isolationKey
38
+ IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Configured with isolation key: \(self.isolationKey)")
39
+ }
40
+
41
+ /**
42
+ * Creates a prefixed key for UserDefaults storage.
43
+ * @param key The base key to prefix
44
+ * @return The prefixed key
45
+ * @throws PreferencesError if configuration is missing
46
+ */
47
+ private func prefixedKey(forKey key: String) throws -> String {
48
+ guard !isolationKey.isEmpty else {
49
+ IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Warning: PreferencesService used before configure(isolationKey:) was called. Isolation key is empty.")
50
+ throw PreferencesError.configurationError
51
+ }
52
+ return "\(isolationKey)\(key)"
53
+ }
54
+
55
+ /**
56
+ * Sets a value in preferences.
57
+ * @param value The value to store (or nil to remove)
58
+ * @param key The key to store under
59
+ * @throws PreferencesError if key prefixing fails
60
+ */
61
+ func setItem(_ value: String?, forKey key: String) throws {
62
+ do {
63
+ let fullKey = try prefixedKey(forKey: key)
64
+ if let valueToSet = value {
65
+ userDefaults.set(valueToSet, forKey: fullKey)
66
+ IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Set '\(fullKey)' = '\(valueToSet)'")
67
+ } else {
68
+ userDefaults.removeObject(forKey: fullKey)
69
+ IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Removed '\(fullKey)'")
70
+ }
71
+ } catch {
72
+ IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Error setting key '\(key)': \(error)")
73
+ throw PreferencesError.setItemError(key)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Gets a value from preferences.
79
+ * @param key The key to retrieve
80
+ * @return The stored value or nil if not found
81
+ * @throws PreferencesError if key prefixing fails
82
+ */
83
+ func getItem(forKey key: String) throws -> String? {
84
+ do {
85
+ let fullKey = try prefixedKey(forKey: key)
86
+ return userDefaults.string(forKey: fullKey)
87
+ } catch {
88
+ IpayCodePushHelper.logPrint(classTag: self.CLASS_TAG, log: "Error getting key '\(key)': \(error)")
89
+ throw PreferencesError.getItemError(key)
90
+ }
91
+ }
92
+ }
93
+
@@ -0,0 +1,175 @@
1
+ //
2
+ // ZipDecompressionStrategy.swift
3
+ // InstantpayCodePush
4
+ //
5
+ // Created by Dhananjay kumar on 09/02/26.
6
+ //
7
+
8
+ import Foundation
9
+ import SWCompression
10
+
11
+ /**
12
+ * Strategy for handling ZIP compressed files
13
+ */
14
+ class ZipDecompressionStrategy: DecompressionStrategy {
15
+
16
+ private static let ZIP_MAGIC_NUMBER: [UInt8] = [0x50, 0x4B, 0x03, 0x04]
17
+ private static let MIN_ZIP_SIZE: UInt64 = 22
18
+ let CLASS_TAG = "ZipStrategy"
19
+
20
+ func isValid(file: String) -> Bool {
21
+ guard FileManager.default.fileExists(atPath: file) else {
22
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: file doesn't exist")
23
+ return false
24
+ }
25
+
26
+ do {
27
+ let attributes = try FileManager.default.attributesOfItem(atPath: file)
28
+ guard let fileSize = attributes[.size] as? UInt64, fileSize >= Self.MIN_ZIP_SIZE else {
29
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: file too small")
30
+ return false
31
+ }
32
+ } catch {
33
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot read attributes - \(error.localizedDescription)")
34
+ return false
35
+ }
36
+
37
+ guard let fileHandle = FileHandle(forReadingAtPath: file) else {
38
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot open file")
39
+ return false
40
+ }
41
+
42
+ defer {
43
+ fileHandle.closeFile()
44
+ }
45
+
46
+ guard let header = try? fileHandle.read(upToCount: 4), header.count == 4 else {
47
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot read header")
48
+ return false
49
+ }
50
+
51
+ let magicBytes = [UInt8](header)
52
+ guard magicBytes == Self.ZIP_MAGIC_NUMBER else {
53
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: wrong magic number")
54
+ return false
55
+ }
56
+
57
+ guard let zipData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
58
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: cannot read file data")
59
+ return false
60
+ }
61
+
62
+ do {
63
+ _ = try ZipContainer.open(container: zipData)
64
+ return true
65
+ } catch {
66
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Invalid ZIP: structure validation failed - \(error.localizedDescription)")
67
+ return false
68
+ }
69
+ }
70
+
71
+ func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
72
+
73
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Starting extraction of \(file) to \(destination)")
74
+
75
+ guard let zipData = try? Data(contentsOf: URL(fileURLWithPath: file)) else {
76
+ throw NSError(
77
+ domain: "IpayCodePush",
78
+ code: 1,
79
+ userInfo: [NSLocalizedDescriptionKey: "Failed to read ZIP file at: \(file)"]
80
+ )
81
+ }
82
+
83
+ progressHandler(0.1)
84
+
85
+ let zipEntries: [ZipEntry]
86
+ do {
87
+ zipEntries = try ZipContainer.open(container: zipData)
88
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "ZIP extraction successful, found \(zipEntries.count) entries")
89
+ } catch {
90
+ throw NSError(
91
+ domain: "IpayCodePush",
92
+ code: 2,
93
+ userInfo: [NSLocalizedDescriptionKey: "ZIP extraction failed: \(error.localizedDescription)"]
94
+ )
95
+ }
96
+
97
+ progressHandler(0.2)
98
+
99
+ let destinationURL = URL(fileURLWithPath: destination)
100
+ let canonicalDestination = destinationURL.standardized.path
101
+
102
+ let fileManager = FileManager.default
103
+ if !fileManager.fileExists(atPath: canonicalDestination) {
104
+ try fileManager.createDirectory(
105
+ atPath: canonicalDestination,
106
+ withIntermediateDirectories: true,
107
+ attributes: nil
108
+ )
109
+ }
110
+
111
+ let totalEntries = Double(zipEntries.count)
112
+ for (index, entry) in zipEntries.enumerated() {
113
+ try extractZipEntry(entry, to: canonicalDestination)
114
+ progressHandler(0.2 + (Double(index + 1) / totalEntries * 0.8))
115
+ }
116
+
117
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Successfully extracted all entries")
118
+ }
119
+
120
+ private func extractZipEntry(_ entry: ZipEntry, to destination: String) throws {
121
+ let fileManager = FileManager.default
122
+ let entryPath = entry.info.name.trimmingCharacters(in: .init(charactersIn: "/"))
123
+
124
+ guard !entryPath.isEmpty,
125
+ !entryPath.contains(".."),
126
+ !entryPath.hasPrefix("/") else {
127
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Skipping suspicious path: \(entry.info.name)")
128
+ return
129
+ }
130
+
131
+ let fullPath = (destination as NSString).appendingPathComponent(entryPath)
132
+ let fullURL = URL(fileURLWithPath: fullPath)
133
+ let canonicalFullPath = fullURL.standardized.path
134
+ let canonicalDestination = URL(fileURLWithPath: destination).standardized.path
135
+
136
+ guard canonicalFullPath.hasPrefix(canonicalDestination + "/") ||
137
+ canonicalFullPath == canonicalDestination else {
138
+ throw NSError(
139
+ domain: "IpayCodePush",
140
+ code: 3,
141
+ userInfo: [NSLocalizedDescriptionKey: "Path traversal attempt detected: \(entry.info.name)"]
142
+ )
143
+ }
144
+
145
+ if entry.info.type == .directory {
146
+ if !fileManager.fileExists(atPath: canonicalFullPath) {
147
+ try fileManager.createDirectory(
148
+ atPath: canonicalFullPath,
149
+ withIntermediateDirectories: true,
150
+ attributes: nil
151
+ )
152
+ }
153
+ return
154
+ }
155
+
156
+ if entry.info.type == .regular {
157
+ let parentPath = (canonicalFullPath as NSString).deletingLastPathComponent
158
+ if !fileManager.fileExists(atPath: parentPath) {
159
+ try fileManager.createDirectory(
160
+ atPath: parentPath,
161
+ withIntermediateDirectories: true,
162
+ attributes: nil
163
+ )
164
+ }
165
+
166
+ guard let data = entry.data else {
167
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Skipping file with no data: \(entry.info.name)")
168
+ return
169
+ }
170
+
171
+ try data.write(to: URL(fileURLWithPath: canonicalFullPath))
172
+ IpayCodePushHelper.logPrint(classTag: CLASS_TAG, log: "Extracted: \(entryPath)")
173
+ }
174
+ }
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-instantpay-code-push",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",