expo-updates 29.0.6 → 29.0.7
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.
- package/CHANGELOG.md +6 -0
- package/android/build.gradle +2 -2
- package/ios/EXUpdates/AppLoader/FileDownloader.swift +17 -12
- package/ios/EXUpdates/Multipart/UpdatesMultipartStreamReader.swift +169 -0
- package/ios/EXUpdates/Multipart/UpdatesParameterParser.swift +148 -0
- package/ios/Tests/UpdatesMultipartStreamReaderSpec.swift +142 -0
- package/ios/Tests/UpdatesParameterParserSpec.swift +53 -0
- package/package.json +5 -5
- package/ios/EXUpdates/Multipart/EXUpdatesMultipartStreamReader.h +0 -20
- package/ios/EXUpdates/Multipart/EXUpdatesMultipartStreamReader.m +0 -137
- package/ios/EXUpdates/Multipart/EXUpdatesParameterParser.h +0 -23
- package/ios/EXUpdates/Multipart/EXUpdatesParameterParser.m +0 -143
- package/ios/Tests/EXUpdatesMultipartStreamReaderTests.m +0 -135
- package/ios/Tests/EXUpdatesParameterParserTests.m +0 -44
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 29.0.7 — 2025-08-31
|
|
14
|
+
|
|
15
|
+
### 💡 Others
|
|
16
|
+
|
|
17
|
+
- [iOS] Migrate multipart parsers to Swift. ([#39144](https://github.com/expo/expo/pull/39144) by [@alanjhughes](https://github.com/alanjhughes))
|
|
18
|
+
|
|
13
19
|
## 29.0.6 — 2025-08-28
|
|
14
20
|
|
|
15
21
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -42,7 +42,7 @@ expoModule {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
group = 'host.exp.exponent'
|
|
45
|
-
version = '29.0.
|
|
45
|
+
version = '29.0.7'
|
|
46
46
|
|
|
47
47
|
// Utility method to derive boolean values from the environment or from Java properties,
|
|
48
48
|
// and return them as strings to be used in BuildConfig fields
|
|
@@ -88,7 +88,7 @@ android {
|
|
|
88
88
|
namespace "expo.modules.updates"
|
|
89
89
|
defaultConfig {
|
|
90
90
|
versionCode 31
|
|
91
|
-
versionName '29.0.
|
|
91
|
+
versionName '29.0.7'
|
|
92
92
|
consumerProguardFiles("proguard-rules.pro")
|
|
93
93
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
94
94
|
|
|
@@ -373,10 +373,11 @@ public final class FileDownloader {
|
|
|
373
373
|
let contentType = httpResponse.value(forHTTPHeaderField: "content-type") ?? ""
|
|
374
374
|
|
|
375
375
|
if contentType.lowercased().hasPrefix("multipart/") {
|
|
376
|
-
guard let
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
376
|
+
guard let scalar = UnicodeScalar(FileDownloader.ParameterParserSemicolonDelimiter) else {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
let contentTypeParameters = UpdatesParameterParser().parseParameterString(contentType, withDelimiter: Character(scalar))
|
|
380
|
+
guard
|
|
380
381
|
let boundaryParameterValue: String = contentTypeParameters.optionalValue(forKey: "boundary") else {
|
|
381
382
|
let cause = UpdatesError.fileDownloaderMissingMultipartBoundary
|
|
382
383
|
logger.error(cause: cause, code: UpdatesErrorCode.unknown)
|
|
@@ -425,7 +426,7 @@ public final class FileDownloader {
|
|
|
425
426
|
successBlock: @escaping RemoteUpdateDownloadSuccessBlock,
|
|
426
427
|
errorBlock: @escaping RemoteUpdateDownloadErrorBlock
|
|
427
428
|
) {
|
|
428
|
-
let reader =
|
|
429
|
+
let reader = UpdatesMultipartStreamReader(inputStream: InputStream(data: data), boundary: boundary)
|
|
429
430
|
|
|
430
431
|
var manifestPartHeadersAndData: ([String: Any], Data)?
|
|
431
432
|
var extensionsData: Data?
|
|
@@ -433,19 +434,23 @@ public final class FileDownloader {
|
|
|
433
434
|
var directivePartHeadersAndData: ([String: Any], Data)?
|
|
434
435
|
|
|
435
436
|
let completed = data.isEmpty || reader.readAllParts { headers, content, _ in
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
)
|
|
437
|
+
guard let headers else {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
if let contentDisposition = headers.stringValueForCaseInsensitiveKey("content-disposition") {
|
|
441
|
+
guard let scalar = UnicodeScalar(FileDownloader.ParameterParserSemicolonDelimiter) else {
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
let contentDispositionParameters = UpdatesParameterParser().parseParameterString(contentDisposition, withDelimiter: Character(scalar))
|
|
445
|
+
if
|
|
441
446
|
let contentDispositionNameFieldValue: String = contentDispositionParameters.optionalValue(forKey: "name") {
|
|
442
447
|
switch contentDispositionNameFieldValue {
|
|
443
448
|
case FileDownloader.MultipartManifestPartName:
|
|
444
|
-
if let
|
|
449
|
+
if let content {
|
|
445
450
|
manifestPartHeadersAndData = (headers, content)
|
|
446
451
|
}
|
|
447
452
|
case FileDownloader.MultipartDirectivePartName:
|
|
448
|
-
if let
|
|
453
|
+
if let content {
|
|
449
454
|
directivePartHeadersAndData = (headers, content)
|
|
450
455
|
}
|
|
451
456
|
case FileDownloader.MultipartExtensionsPartName:
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Copyright © 2019 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
public typealias MultipartCallback = (_ headers: [String: Any]?, _ content: Data?, _ done: Bool) -> Void
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fork of React Native's RCTMultipartStreamReader that doesn't necessarily
|
|
9
|
+
* expect a preamble (first boundary is not necessarily preceded by CRLF).
|
|
10
|
+
*/
|
|
11
|
+
public final class UpdatesMultipartStreamReader {
|
|
12
|
+
private enum Constants {
|
|
13
|
+
static let crlf = "\r\n"
|
|
14
|
+
static let bufferLength = 4 * 1024
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private let stream: InputStream
|
|
18
|
+
private let boundary: String
|
|
19
|
+
|
|
20
|
+
public init(inputStream: InputStream, boundary: String) {
|
|
21
|
+
self.stream = inputStream
|
|
22
|
+
self.boundary = boundary
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public func readAllParts(withCompletionCallback callback: @escaping MultipartCallback) -> Bool {
|
|
26
|
+
var chunkStart = 0
|
|
27
|
+
var bytesSeen = 0
|
|
28
|
+
|
|
29
|
+
// First delimiter doesn't necessarily need to be preceded by CRLF (boundary can be first thing in body)
|
|
30
|
+
let firstDelimiter = Data("--\(boundary)\(Constants.crlf)".utf8)
|
|
31
|
+
let restDelimiter = Data("\(Constants.crlf)--\(boundary)\(Constants.crlf)".utf8)
|
|
32
|
+
let closeDelimiter = Data("\(Constants.crlf)--\(boundary)--\(Constants.crlf)".utf8)
|
|
33
|
+
|
|
34
|
+
var delimiter = firstDelimiter
|
|
35
|
+
var content = Data()
|
|
36
|
+
var currentHeaders: [String: Any]?
|
|
37
|
+
|
|
38
|
+
var buffer = [UInt8](repeating: 0, count: Constants.bufferLength)
|
|
39
|
+
|
|
40
|
+
stream.open()
|
|
41
|
+
defer { stream.close() }
|
|
42
|
+
|
|
43
|
+
while true {
|
|
44
|
+
var isCloseDelimiter = false
|
|
45
|
+
|
|
46
|
+
// Search only a subset of chunk that we haven't seen before + few bytes
|
|
47
|
+
// to allow for the edge case when the delimiter is cut by read call
|
|
48
|
+
let searchStart = max(bytesSeen - closeDelimiter.count, chunkStart)
|
|
49
|
+
let remainingBufferRange = NSRange(location: searchStart, length: content.count - searchStart)
|
|
50
|
+
|
|
51
|
+
// Check for delimiters
|
|
52
|
+
var range = content.range(of: delimiter, options: [], in: remainingBufferRange)
|
|
53
|
+
if range.location == NSNotFound {
|
|
54
|
+
isCloseDelimiter = true
|
|
55
|
+
range = content.range(of: closeDelimiter, options: [], in: remainingBufferRange)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if range.location == NSNotFound {
|
|
59
|
+
if currentHeaders == nil {
|
|
60
|
+
if let newHeaders = parseHeadersIfFound(in: content, range: remainingBufferRange, chunkStart: chunkStart) {
|
|
61
|
+
currentHeaders = newHeaders
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
bytesSeen = content.count
|
|
66
|
+
let bytesRead = buffer.withUnsafeMutableBytes { bytes in
|
|
67
|
+
guard let baseAddress = bytes.bindMemory(to: UInt8.self).baseAddress else {
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
return stream.read(baseAddress, maxLength: Constants.bufferLength)
|
|
71
|
+
}
|
|
72
|
+
guard bytesRead > 0, stream.streamError == nil else {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
content.append(contentsOf: buffer.prefix(bytesRead))
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let chunkEnd = range.location
|
|
80
|
+
let length = chunkEnd - chunkStart
|
|
81
|
+
bytesSeen = chunkEnd
|
|
82
|
+
|
|
83
|
+
// Ignore preamble
|
|
84
|
+
if chunkStart > 0 {
|
|
85
|
+
let startIndex = content.index(content.startIndex, offsetBy: chunkStart)
|
|
86
|
+
let endIndex = content.index(startIndex, offsetBy: length)
|
|
87
|
+
let chunk = Data(content[startIndex..<endIndex])
|
|
88
|
+
emitChunk(chunk, headers: currentHeaders, callback: callback, done: isCloseDelimiter)
|
|
89
|
+
currentHeaders = nil
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if isCloseDelimiter {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
chunkStart = chunkEnd + delimiter.count
|
|
97
|
+
|
|
98
|
+
if delimiter == firstDelimiter {
|
|
99
|
+
delimiter = restDelimiter
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func parseHeadersIfFound(
|
|
105
|
+
in content: Data,
|
|
106
|
+
range: NSRange,
|
|
107
|
+
chunkStart: Int
|
|
108
|
+
) -> [String: Any]? {
|
|
109
|
+
let marker = Data("\(Constants.crlf)\(Constants.crlf)".utf8)
|
|
110
|
+
let range = content.range(of: marker, options: [], in: range)
|
|
111
|
+
guard range.location != NSNotFound else {
|
|
112
|
+
return nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let startIndex = content.index(content.startIndex, offsetBy: chunkStart)
|
|
116
|
+
let endIndex = content.index(content.startIndex, offsetBy: range.location)
|
|
117
|
+
let data = content[startIndex..<endIndex]
|
|
118
|
+
return parseHeaders(Data(data))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func parseHeaders(_ data: Data) -> [String: Any] {
|
|
122
|
+
guard let text = String(data: data, encoding: .utf8) else {
|
|
123
|
+
return [:]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return text.components(separatedBy: Constants.crlf)
|
|
127
|
+
.compactMap { line -> (String, String)? in
|
|
128
|
+
guard let colonRange = line.range(of: ":") else {
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
131
|
+
let key = String(line[..<colonRange.lowerBound])
|
|
132
|
+
let value = String(line[colonRange.upperBound...])
|
|
133
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
134
|
+
return (key, value)
|
|
135
|
+
}
|
|
136
|
+
.reduce(into: [String: Any]()) { headers, valueTuple in
|
|
137
|
+
headers[valueTuple.0] = valueTuple.1
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func emitChunk(
|
|
142
|
+
_ data: Data,
|
|
143
|
+
headers: [String: Any]?,
|
|
144
|
+
callback: @escaping MultipartCallback,
|
|
145
|
+
done: Bool
|
|
146
|
+
) {
|
|
147
|
+
let marker = Data("\(Constants.crlf)\(Constants.crlf)".utf8)
|
|
148
|
+
guard let range = data.range(of: marker) else {
|
|
149
|
+
callback(nil, data, done)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let bodyData = data.subdata(in: range.upperBound..<data.endIndex)
|
|
154
|
+
|
|
155
|
+
if let headers {
|
|
156
|
+
callback(headers, bodyData, done)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let headersData = data.subdata(in: data.startIndex..<range.lowerBound)
|
|
161
|
+
callback(parseHeaders(headersData), bodyData, done)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private extension Data {
|
|
166
|
+
func range(of data: Data, options: Data.SearchOptions = [], in range: NSRange) -> NSRange {
|
|
167
|
+
return (self as NSData).range(of: data, options: options, in: range)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Copyright © 2019 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Derivation of ParameterParser class in the apache commons file upload
|
|
7
|
+
* project: https://commons.apache.org/proper/commons-fileupload/
|
|
8
|
+
*
|
|
9
|
+
* A simple parser intended to parse sequences of name/value pairs.
|
|
10
|
+
*
|
|
11
|
+
* Parameter values are expected to be enclosed in quotes if they
|
|
12
|
+
* contain unsafe characters, such as '=' characters or separators.
|
|
13
|
+
* Parameter values are optional and can be omitted.
|
|
14
|
+
*/
|
|
15
|
+
public final class UpdatesParameterParser {
|
|
16
|
+
private enum Constants {
|
|
17
|
+
static let quote: Character = "\""
|
|
18
|
+
static let backslash: Character = "\\"
|
|
19
|
+
static let equals: Character = "="
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private var parameterString: String = ""
|
|
23
|
+
private var currentPosition: Int = 0
|
|
24
|
+
private var tokenStart: Int = 0
|
|
25
|
+
private var tokenEnd: Int = 0
|
|
26
|
+
|
|
27
|
+
public init() {}
|
|
28
|
+
|
|
29
|
+
public func parseParameterString(_ parameterString: String, withDelimiter delimiter: Character) -> [String: Any] {
|
|
30
|
+
self.parameterString = parameterString
|
|
31
|
+
self.currentPosition = 0
|
|
32
|
+
|
|
33
|
+
var params = [String: Any]()
|
|
34
|
+
|
|
35
|
+
let delimiterSet = CharacterSet(charactersIn: String(delimiter))
|
|
36
|
+
let delimiterAndEqualsSet = CharacterSet(charactersIn: String(delimiter) + String(Constants.equals))
|
|
37
|
+
|
|
38
|
+
while hasChar() {
|
|
39
|
+
let paramName = parseToken(withTerminators: delimiterAndEqualsSet)
|
|
40
|
+
var paramValue: Any?
|
|
41
|
+
|
|
42
|
+
if hasChar() && currentChar() == Constants.equals {
|
|
43
|
+
currentPosition += 1 // skip '='
|
|
44
|
+
paramValue = parseQuotedToken(withTerminators: delimiterSet)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if hasChar() && currentChar() == delimiter {
|
|
48
|
+
currentPosition += 1 // skip separator
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if let paramName = paramName, !paramName.isEmpty {
|
|
52
|
+
params[paramName] = paramValue ?? NSNull()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return params
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private func getToken(quoted: Bool) -> String? {
|
|
60
|
+
guard let range = extractTokenRange(quoted: quoted) else { return nil }
|
|
61
|
+
return String(parameterString[range])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func extractTokenRange(quoted: Bool) -> Range<String.Index>? {
|
|
65
|
+
var start = tokenStart
|
|
66
|
+
var end = tokenEnd
|
|
67
|
+
|
|
68
|
+
while start < end && parameterString[stringIndex(at: start)].isWhitespace {
|
|
69
|
+
start += 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
while end > start && parameterString[stringIndex(at: end - 1)].isWhitespace {
|
|
73
|
+
end -= 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if quoted && (end - start) >= 2 {
|
|
77
|
+
let startChar = parameterString[stringIndex(at: start)]
|
|
78
|
+
let endChar = parameterString[stringIndex(at: end - 1)]
|
|
79
|
+
if startChar == Constants.quote && endChar == Constants.quote {
|
|
80
|
+
start += 1
|
|
81
|
+
end -= 1
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
guard end > start else {
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let startIndex = stringIndex(at: start)
|
|
90
|
+
let endIndex = stringIndex(at: end)
|
|
91
|
+
return startIndex..<endIndex
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private func parseToken(withTerminators terminators: CharacterSet) -> String? {
|
|
95
|
+
setTokenBounds()
|
|
96
|
+
|
|
97
|
+
while hasChar() {
|
|
98
|
+
let char = currentChar()
|
|
99
|
+
guard let scalar = char.unicodeScalars.first, !terminators.contains(scalar) else {
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
tokenEnd += 1
|
|
103
|
+
currentPosition += 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return getToken(quoted: false)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private func parseQuotedToken(withTerminators terminators: CharacterSet) -> String? {
|
|
110
|
+
setTokenBounds()
|
|
111
|
+
var quoted = false
|
|
112
|
+
var charEscaped = false
|
|
113
|
+
|
|
114
|
+
while hasChar() {
|
|
115
|
+
let char = currentChar()
|
|
116
|
+
|
|
117
|
+
if !quoted, let scalar = char.unicodeScalars.first, terminators.contains(scalar) {
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if !charEscaped && char == Constants.quote {
|
|
122
|
+
quoted.toggle()
|
|
123
|
+
}
|
|
124
|
+
charEscaped = (!charEscaped && char == Constants.backslash)
|
|
125
|
+
tokenEnd += 1
|
|
126
|
+
currentPosition += 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return getToken(quoted: true)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private func setTokenBounds() {
|
|
133
|
+
tokenStart = currentPosition
|
|
134
|
+
tokenEnd = currentPosition
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func hasChar() -> Bool {
|
|
138
|
+
currentPosition < parameterString.count
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func currentChar() -> Character {
|
|
142
|
+
parameterString[stringIndex(at: currentPosition)]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func stringIndex(at position: Int) -> String.Index {
|
|
146
|
+
parameterString.index(parameterString.startIndex, offsetBy: position)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Copyright (c) 2020 650 Industries, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import XCTest
|
|
4
|
+
@testable import EXUpdates
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests for UpdatesMultipartStreamReader
|
|
8
|
+
*/
|
|
9
|
+
class UpdatesMultipartStreamReaderSpec: XCTestCase {
|
|
10
|
+
func testSimpleCase() {
|
|
11
|
+
let response = "preamble, should be ignored\r\n" +
|
|
12
|
+
"--sample_boundary\r\n" +
|
|
13
|
+
"Content-Type: application/json; charset=utf-8\r\n" +
|
|
14
|
+
"Content-Length: 2\r\n\r\n" +
|
|
15
|
+
"{}\r\n" +
|
|
16
|
+
"--sample_boundary--\r\n" +
|
|
17
|
+
"epilogue, should be ignored"
|
|
18
|
+
|
|
19
|
+
let inputStream = InputStream(data: response.data(using: .utf8)!)
|
|
20
|
+
let reader = UpdatesMultipartStreamReader(inputStream: inputStream, boundary: "sample_boundary")
|
|
21
|
+
|
|
22
|
+
var count = 0
|
|
23
|
+
let success = reader.readAllParts { headers, content, done in
|
|
24
|
+
XCTAssertTrue(done)
|
|
25
|
+
XCTAssertEqual(headers?["Content-Type"] as? String, "application/json; charset=utf-8")
|
|
26
|
+
XCTAssertEqual(String(data: content!, encoding: .utf8), "{}")
|
|
27
|
+
count += 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
XCTAssertTrue(success)
|
|
31
|
+
XCTAssertEqual(count, 1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func testMultipleParts() {
|
|
35
|
+
let response = "preamble, should be ignored\r\n" +
|
|
36
|
+
"--sample_boundary\r\n" +
|
|
37
|
+
"1\r\n" +
|
|
38
|
+
"--sample_boundary\r\n" +
|
|
39
|
+
"2\r\n" +
|
|
40
|
+
"--sample_boundary\r\n" +
|
|
41
|
+
"3\r\n" +
|
|
42
|
+
"--sample_boundary--\r\n" +
|
|
43
|
+
"epilogue, should be ignored"
|
|
44
|
+
|
|
45
|
+
let inputStream = InputStream(data: response.data(using: .utf8)!)
|
|
46
|
+
let reader = UpdatesMultipartStreamReader(inputStream: inputStream, boundary: "sample_boundary")
|
|
47
|
+
|
|
48
|
+
var count = 0
|
|
49
|
+
let expectedContents = ["1", "2", "3"]
|
|
50
|
+
|
|
51
|
+
let success = reader.readAllParts { _, content, done in
|
|
52
|
+
if count < expectedContents.count {
|
|
53
|
+
let expectedContent = expectedContents[count]
|
|
54
|
+
XCTAssertEqual(String(data: content!, encoding: .utf8), expectedContent)
|
|
55
|
+
XCTAssertEqual(done, count == expectedContents.count - 1)
|
|
56
|
+
count += 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
XCTAssertTrue(success)
|
|
61
|
+
XCTAssertEqual(count, 3)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func testNoDelimiter() {
|
|
65
|
+
let response = "content with no delimiter"
|
|
66
|
+
|
|
67
|
+
guard let responseData = response.data(using: .utf8) else {
|
|
68
|
+
XCTFail("Failed to convert response to UTF-8 data")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
let inputStream = InputStream(data: responseData)
|
|
72
|
+
let reader = UpdatesMultipartStreamReader(inputStream: inputStream, boundary: "sample_boundary")
|
|
73
|
+
|
|
74
|
+
var count = 0
|
|
75
|
+
let success = reader.readAllParts { _, _, _ in
|
|
76
|
+
count += 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
XCTAssertFalse(success)
|
|
80
|
+
XCTAssertEqual(count, 0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func testEmptyContent() {
|
|
84
|
+
let response = "--sample_boundary\r\n" +
|
|
85
|
+
"Content-Type: application/json\r\n" +
|
|
86
|
+
"\r\n" +
|
|
87
|
+
"\r\n" +
|
|
88
|
+
"--sample_boundary--\r\n"
|
|
89
|
+
|
|
90
|
+
guard let responseData = response.data(using: .utf8) else {
|
|
91
|
+
XCTFail("Failed to convert response to UTF-8 data")
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
let inputStream = InputStream(data: responseData)
|
|
95
|
+
let reader = UpdatesMultipartStreamReader(inputStream: inputStream, boundary: "sample_boundary")
|
|
96
|
+
|
|
97
|
+
var count = 0
|
|
98
|
+
let success = reader.readAllParts { headers, content, done in
|
|
99
|
+
XCTAssertTrue(done)
|
|
100
|
+
XCTAssertEqual(headers?["Content-Type"] as? String, "application/json")
|
|
101
|
+
guard let contentData = content else {
|
|
102
|
+
XCTFail("Content should not be nil")
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
XCTAssertEqual(String(data: contentData, encoding: .utf8), "")
|
|
106
|
+
count += 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
XCTAssertTrue(success)
|
|
110
|
+
XCTAssertEqual(count, 1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func testFirstBoundaryAsBoundary() {
|
|
114
|
+
let response = "--sample_boundary\r\n" +
|
|
115
|
+
"Content-Type: application/json\r\n" +
|
|
116
|
+
"\r\n" +
|
|
117
|
+
"{}\r\n" +
|
|
118
|
+
"--sample_boundary--\r\n"
|
|
119
|
+
|
|
120
|
+
guard let responseData = response.data(using: .utf8) else {
|
|
121
|
+
XCTFail("Failed to convert response to UTF-8 data")
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
let inputStream = InputStream(data: responseData)
|
|
125
|
+
let reader = UpdatesMultipartStreamReader(inputStream: inputStream, boundary: "sample_boundary")
|
|
126
|
+
|
|
127
|
+
var count = 0
|
|
128
|
+
let success = reader.readAllParts { headers, content, done in
|
|
129
|
+
XCTAssertTrue(done)
|
|
130
|
+
XCTAssertEqual(headers?["Content-Type"] as? String, "application/json")
|
|
131
|
+
guard let contentData = content else {
|
|
132
|
+
XCTFail("Content should not be nil")
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
XCTAssertEqual(String(data: contentData, encoding: .utf8), "{}")
|
|
136
|
+
count += 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
XCTAssertTrue(success)
|
|
140
|
+
XCTAssertEqual(count, 1)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Copyright (c) 2020 650 Industries, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import XCTest
|
|
4
|
+
@testable import EXUpdates
|
|
5
|
+
|
|
6
|
+
class UpdatesParameterParserSpec: XCTestCase {
|
|
7
|
+
func testParameterParser() {
|
|
8
|
+
let testCases: [(String, [String: Any])] = [
|
|
9
|
+
("", [:]),
|
|
10
|
+
("test; test1 = stuff ; test2 = \"stuff; stuff\"; test3=\"stuff",
|
|
11
|
+
["test": NSNull(), "test1": "stuff", "test2": "stuff; stuff", "test3": "\"stuff"]),
|
|
12
|
+
(" test ; test1=stuff ; ; test2=; test3; ",
|
|
13
|
+
["test": NSNull(), "test1": "stuff", "test2": NSNull(), "test3": NSNull()]),
|
|
14
|
+
(" test", ["test": NSNull()]),
|
|
15
|
+
(" ", [:]),
|
|
16
|
+
(" = stuff ", [:]),
|
|
17
|
+
("text/plain; Charset=UTF-8", ["text/plain": NSNull(), "Charset": "UTF-8"]),
|
|
18
|
+
("param = \"stuff\\\"; more stuff\"", ["param": "stuff\\\"; more stuff"]),
|
|
19
|
+
("param = \"stuff\\\\\"; anotherparam", ["param": "stuff\\\\", "anotherparam": NSNull()]),
|
|
20
|
+
("foo/bar; param=\"baz=bat\"", ["foo/bar": NSNull(), "param": "baz=bat"]),
|
|
21
|
+
|
|
22
|
+
// Expo-specific tests
|
|
23
|
+
("multipart/mixed; boundary=BbC04y", ["multipart/mixed": NSNull(), "boundary": "BbC04y"]),
|
|
24
|
+
("form-data; name=\"manifest\"; filename=\"hello2\"", ["form-data": NSNull(), "name": "manifest", "filename": "hello2"])
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
let parser = UpdatesParameterParser()
|
|
28
|
+
|
|
29
|
+
for (parameterString, expectedDictionary) in testCases {
|
|
30
|
+
let parameters = parser.parseParameterString(parameterString, withDelimiter: ";")
|
|
31
|
+
|
|
32
|
+
XCTAssertEqual(parameters.count, expectedDictionary.count,
|
|
33
|
+
"Parameter count mismatch for: '\(parameterString)'")
|
|
34
|
+
|
|
35
|
+
for (key, expectedValue) in expectedDictionary {
|
|
36
|
+
XCTAssertTrue(parameters.keys.contains(key),
|
|
37
|
+
"Missing key '\(key)' for: '\(parameterString)'")
|
|
38
|
+
|
|
39
|
+
let actualValue = parameters[key]
|
|
40
|
+
|
|
41
|
+
if expectedValue is NSNull && actualValue is NSNull {
|
|
42
|
+
continue
|
|
43
|
+
} else if let expectedString = expectedValue as? String,
|
|
44
|
+
let actualString = actualValue as? String {
|
|
45
|
+
XCTAssertEqual(actualString, expectedString,
|
|
46
|
+
"Value mismatch for key '\(key)' in: '\(parameterString)'")
|
|
47
|
+
} else {
|
|
48
|
+
XCTFail("Type mismatch for key '\(key)' in: '\(parameterString)'. Expected: \(type(of: expectedValue)), Actual: \(type(of: actualValue ?? "nil"))")
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-updates",
|
|
3
|
-
"version": "29.0.
|
|
3
|
+
"version": "29.0.7",
|
|
4
4
|
"description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@expo/code-signing-certificates": "0.0.5",
|
|
42
|
-
"@expo/plist": "^0.4.
|
|
42
|
+
"@expo/plist": "^0.4.5",
|
|
43
43
|
"@expo/spawn-async": "^1.7.2",
|
|
44
44
|
"arg": "4.1.0",
|
|
45
45
|
"chalk": "^4.1.2",
|
|
46
46
|
"debug": "^4.3.4",
|
|
47
|
-
"expo-eas-client": "~1.0.
|
|
48
|
-
"expo-manifests": "~1.0.
|
|
47
|
+
"expo-eas-client": "~1.0.5",
|
|
48
|
+
"expo-manifests": "~1.0.6",
|
|
49
49
|
"expo-structured-headers": "~5.0.0",
|
|
50
50
|
"expo-updates-interface": "~2.0.0",
|
|
51
51
|
"getenv": "^2.0.0",
|
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"react": "*",
|
|
74
74
|
"react-native": "*"
|
|
75
75
|
},
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "1dafdaa317c182191909a94a355c8b08a2654783"
|
|
77
77
|
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Copyright © 2019 650 Industries. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <Foundation/Foundation.h>
|
|
4
|
-
|
|
5
|
-
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
-
|
|
7
|
-
typedef void (^EXMultipartCallback)(NSDictionary * _Nullable headers, NSData * _Nullable content, BOOL done);
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Fork of {@link RCTMultipartStreamReader} that doesn't necessarily
|
|
11
|
-
* expect a preamble (first boundary is not necessarily preceded by CRLF).
|
|
12
|
-
*/
|
|
13
|
-
@interface EXUpdatesMultipartStreamReader : NSObject
|
|
14
|
-
|
|
15
|
-
- (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary;
|
|
16
|
-
- (BOOL)readAllPartsWithCompletionCallback:(EXMultipartCallback)callback;
|
|
17
|
-
|
|
18
|
-
@end
|
|
19
|
-
|
|
20
|
-
NS_ASSUME_NONNULL_END
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// Copyright © 2019 650 Industries. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <EXUpdates/EXUpdatesMultipartStreamReader.h>
|
|
4
|
-
|
|
5
|
-
#define CRLF @"\r\n"
|
|
6
|
-
|
|
7
|
-
@implementation EXUpdatesMultipartStreamReader {
|
|
8
|
-
__strong NSInputStream *_stream;
|
|
9
|
-
__strong NSString *_boundary;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
- (instancetype)initWithInputStream:(NSInputStream *)stream boundary:(NSString *)boundary
|
|
13
|
-
{
|
|
14
|
-
if (self = [super init]) {
|
|
15
|
-
_stream = stream;
|
|
16
|
-
_boundary = boundary;
|
|
17
|
-
}
|
|
18
|
-
return self;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
- (NSDictionary<NSString *, NSString *> *)parseHeaders:(NSData *)data
|
|
22
|
-
{
|
|
23
|
-
NSMutableDictionary *headers = [NSMutableDictionary new];
|
|
24
|
-
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
25
|
-
NSArray<NSString *> *lines = [text componentsSeparatedByString:CRLF];
|
|
26
|
-
for (NSString *line in lines) {
|
|
27
|
-
NSUInteger location = [line rangeOfString:@":"].location;
|
|
28
|
-
if (location == NSNotFound) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
NSString *key = [line substringToIndex:location];
|
|
32
|
-
NSString *value = [[line substringFromIndex:location + 1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
33
|
-
[headers setValue:value forKey:key];
|
|
34
|
-
}
|
|
35
|
-
return headers;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
- (void)emitChunk:(NSData *)data headers:(NSDictionary *)headers callback:(EXMultipartCallback)callback done:(BOOL)done
|
|
39
|
-
{
|
|
40
|
-
NSData *marker = [CRLF CRLF dataUsingEncoding:NSUTF8StringEncoding];
|
|
41
|
-
NSRange range = [data rangeOfData:marker options:0 range:NSMakeRange(0, data.length)];
|
|
42
|
-
if (range.location == NSNotFound) {
|
|
43
|
-
callback(nil, data, done);
|
|
44
|
-
} else if (headers != nil) {
|
|
45
|
-
// If headers were parsed already just use that to avoid doing it twice.
|
|
46
|
-
NSInteger bodyStart = range.location + marker.length;
|
|
47
|
-
NSData *bodyData = [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)];
|
|
48
|
-
callback(headers, bodyData, done);
|
|
49
|
-
} else {
|
|
50
|
-
NSData *headersData = [data subdataWithRange:NSMakeRange(0, range.location)];
|
|
51
|
-
NSInteger bodyStart = range.location + marker.length;
|
|
52
|
-
NSData *bodyData = [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)];
|
|
53
|
-
callback([self parseHeaders:headersData], bodyData, done);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
- (BOOL)readAllPartsWithCompletionCallback:(EXMultipartCallback)callback
|
|
58
|
-
{
|
|
59
|
-
NSInteger chunkStart = 0;
|
|
60
|
-
NSInteger bytesSeen = 0;
|
|
61
|
-
|
|
62
|
-
// first delimiter doesn't necessarily need to be preceded by CRLF (boundary can be first thing in body)
|
|
63
|
-
NSData *firstDelimiter = [[NSString stringWithFormat:@"--%@%@", _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
|
|
64
|
-
NSData *restDelimiter = [[NSString stringWithFormat:@"%@--%@%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
|
|
65
|
-
NSData *delimiter = firstDelimiter;
|
|
66
|
-
|
|
67
|
-
NSData *closeDelimiter = [[NSString stringWithFormat:@"%@--%@--%@", CRLF, _boundary, CRLF] dataUsingEncoding:NSUTF8StringEncoding];
|
|
68
|
-
NSMutableData *content = [[NSMutableData alloc] initWithCapacity:1];
|
|
69
|
-
NSDictionary *currentHeaders = nil;
|
|
70
|
-
NSUInteger currentHeadersLength = 0;
|
|
71
|
-
|
|
72
|
-
const NSUInteger bufferLen = 4 * 1024;
|
|
73
|
-
uint8_t buffer[bufferLen];
|
|
74
|
-
|
|
75
|
-
[_stream open];
|
|
76
|
-
while (true) {
|
|
77
|
-
BOOL isCloseDelimiter = NO;
|
|
78
|
-
// Search only a subset of chunk that we haven't seen before + few bytes
|
|
79
|
-
// to allow for the edge case when the delimiter is cut by read call
|
|
80
|
-
NSInteger searchStart = MAX(bytesSeen - (NSInteger)closeDelimiter.length, chunkStart);
|
|
81
|
-
NSRange remainingBufferRange = NSMakeRange(searchStart, content.length - searchStart);
|
|
82
|
-
|
|
83
|
-
// Check for delimiters.
|
|
84
|
-
NSRange range = [content rangeOfData:delimiter options:0 range:remainingBufferRange];
|
|
85
|
-
if (range.location == NSNotFound) {
|
|
86
|
-
isCloseDelimiter = YES;
|
|
87
|
-
range = [content rangeOfData:closeDelimiter options:0 range:remainingBufferRange];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (range.location == NSNotFound) {
|
|
91
|
-
if (currentHeaders == nil) {
|
|
92
|
-
// Check for the headers delimiter.
|
|
93
|
-
NSData *headersMarker = [CRLF CRLF dataUsingEncoding:NSUTF8StringEncoding];
|
|
94
|
-
NSRange headersRange = [content rangeOfData:headersMarker options:0 range:remainingBufferRange];
|
|
95
|
-
if (headersRange.location != NSNotFound) {
|
|
96
|
-
NSData *headersData = [content subdataWithRange:NSMakeRange(chunkStart, headersRange.location - chunkStart)];
|
|
97
|
-
currentHeadersLength = headersData.length;
|
|
98
|
-
currentHeaders = [self parseHeaders:headersData];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
bytesSeen = content.length;
|
|
103
|
-
NSInteger bytesRead = [_stream read:buffer maxLength:bufferLen];
|
|
104
|
-
if (bytesRead <= 0 || _stream.streamError) {
|
|
105
|
-
[_stream close];
|
|
106
|
-
return NO;
|
|
107
|
-
}
|
|
108
|
-
[content appendBytes:buffer length:bytesRead];
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
NSInteger chunkEnd = range.location;
|
|
113
|
-
NSInteger length = chunkEnd - chunkStart;
|
|
114
|
-
bytesSeen = chunkEnd;
|
|
115
|
-
|
|
116
|
-
// Ignore preamble
|
|
117
|
-
if (chunkStart > 0) {
|
|
118
|
-
NSData *chunk = [content subdataWithRange:NSMakeRange(chunkStart, length)];
|
|
119
|
-
[self emitChunk:chunk headers:currentHeaders callback:callback done:isCloseDelimiter];
|
|
120
|
-
currentHeaders = nil;
|
|
121
|
-
currentHeadersLength = 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (isCloseDelimiter) {
|
|
125
|
-
[_stream close];
|
|
126
|
-
return YES;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
chunkStart = chunkEnd + delimiter.length;
|
|
130
|
-
|
|
131
|
-
if (delimiter == firstDelimiter) {
|
|
132
|
-
delimiter = restDelimiter;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
@end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Copyright © 2019 650 Industries. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <Foundation/Foundation.h>
|
|
4
|
-
|
|
5
|
-
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Derivation of ParameterParser class in the apache commons file upload
|
|
9
|
-
* project: https://commons.apache.org/proper/commons-fileupload/
|
|
10
|
-
*
|
|
11
|
-
* A simple parser intended to parse sequences of name/value pairs.
|
|
12
|
-
*
|
|
13
|
-
* Parameter values are expected to be enclosed in quotes if they
|
|
14
|
-
* contain unsafe characters, such as '=' characters or separators.
|
|
15
|
-
* Parameter values are optional and can be omitted.
|
|
16
|
-
*/
|
|
17
|
-
@interface EXUpdatesParameterParser : NSObject
|
|
18
|
-
|
|
19
|
-
- (NSDictionary *)parseParameterString:(NSString *)parameterString withDelimiter:(unichar)delimiter;
|
|
20
|
-
|
|
21
|
-
@end
|
|
22
|
-
|
|
23
|
-
NS_ASSUME_NONNULL_END
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// Copyright © 2019 650 Industries. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <EXUpdates/EXUpdatesParameterParser.h>
|
|
4
|
-
|
|
5
|
-
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
-
|
|
7
|
-
@implementation EXUpdatesParameterParser {
|
|
8
|
-
// String to be parsed.
|
|
9
|
-
NSString *_parameterString;
|
|
10
|
-
|
|
11
|
-
// Current position in the string.
|
|
12
|
-
int _currentPosition;
|
|
13
|
-
|
|
14
|
-
// Start of a token.
|
|
15
|
-
int _i1;
|
|
16
|
-
|
|
17
|
-
// End of a token.
|
|
18
|
-
int _i2;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A helper method to process the parsed token. This method removes
|
|
23
|
-
* leading and trailing blanks as well as enclosing quotation marks,
|
|
24
|
-
* when necessary.
|
|
25
|
-
*/
|
|
26
|
-
- (nullable NSString *)getTokenWithQuoted:(BOOL)quoted {
|
|
27
|
-
// Trim leading white spaces
|
|
28
|
-
while ((_i1 < _i2) && [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[_parameterString characterAtIndex:_i1]]) {
|
|
29
|
-
_i1++;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Trim trailing white spaces
|
|
33
|
-
while ((_i2 > _i1) && [[NSCharacterSet whitespaceCharacterSet] characterIsMember:[_parameterString characterAtIndex:(_i2 - 1)]]) {
|
|
34
|
-
_i2--;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Strip away quotation marks if necessary
|
|
38
|
-
if (quoted
|
|
39
|
-
&& ((_i2 - _i1) >= 2)
|
|
40
|
-
&& ([_parameterString characterAtIndex:_i1] == '"')
|
|
41
|
-
&& ([_parameterString characterAtIndex:(_i2 - 1)] == '"')) {
|
|
42
|
-
_i1++;
|
|
43
|
-
_i2--;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
NSString *result = nil;
|
|
47
|
-
if (_i2 > _i1) {
|
|
48
|
-
result = [_parameterString substringWithRange:NSMakeRange(_i1, _i2 - _i1)];
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Parses out a token until any of the given terminators
|
|
55
|
-
* is encountered.
|
|
56
|
-
*/
|
|
57
|
-
- (NSString *)parseTokenWithTerminators:(NSCharacterSet *)terminators {
|
|
58
|
-
unichar ch;
|
|
59
|
-
_i1 = _currentPosition;
|
|
60
|
-
_i2 = _currentPosition;
|
|
61
|
-
while ([self hasChar]) {
|
|
62
|
-
ch = [_parameterString characterAtIndex:_currentPosition];
|
|
63
|
-
if ([terminators characterIsMember:ch]) {
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
_i2++;
|
|
67
|
-
_currentPosition++;
|
|
68
|
-
}
|
|
69
|
-
return [self getTokenWithQuoted:false];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Parses out a token until any of the given terminators
|
|
74
|
-
* is encountered outside the quotation marks.
|
|
75
|
-
*/
|
|
76
|
-
- (NSString *)parseQuotedTokenWithTerminators:(NSCharacterSet *)terminators {
|
|
77
|
-
unichar ch;
|
|
78
|
-
_i1 = _currentPosition;
|
|
79
|
-
_i2 = _currentPosition;
|
|
80
|
-
BOOL quoted = false;
|
|
81
|
-
BOOL charEscaped = false;
|
|
82
|
-
|
|
83
|
-
while ([self hasChar]) {
|
|
84
|
-
ch = [_parameterString characterAtIndex:_currentPosition];
|
|
85
|
-
if (!quoted && [terminators characterIsMember:ch]) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
if (!charEscaped && ch == '"') {
|
|
89
|
-
quoted = !quoted;
|
|
90
|
-
}
|
|
91
|
-
charEscaped = (!charEscaped && ch == '\\');
|
|
92
|
-
_i2++;
|
|
93
|
-
_currentPosition++;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return [self getTokenWithQuoted:true];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Are there any characters left to parse?
|
|
101
|
-
*/
|
|
102
|
-
- (BOOL)hasChar {
|
|
103
|
-
return _currentPosition < _parameterString.length;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Extracts a map of name/value pairs from the given string. Names are expected to be unique.
|
|
108
|
-
*/
|
|
109
|
-
- (NSDictionary *)parseParameterString:(NSString *)parameterString withDelimiter:(unichar)delimiter {
|
|
110
|
-
_parameterString = parameterString;
|
|
111
|
-
|
|
112
|
-
NSMutableDictionary *params = [NSMutableDictionary new];
|
|
113
|
-
|
|
114
|
-
NSString *paramName;
|
|
115
|
-
NSString *paramValue;
|
|
116
|
-
|
|
117
|
-
NSCharacterSet *charSetDelimiter = [NSCharacterSet characterSetWithCharactersInString:[NSString stringWithFormat:@"%C", delimiter]];
|
|
118
|
-
NSCharacterSet *charSetDelimiterAndEquals = [NSCharacterSet characterSetWithCharactersInString:[NSString stringWithFormat:@"%C=", delimiter]];
|
|
119
|
-
|
|
120
|
-
while ([self hasChar]) {
|
|
121
|
-
paramName = [self parseTokenWithTerminators:charSetDelimiterAndEquals];
|
|
122
|
-
paramValue = nil;
|
|
123
|
-
|
|
124
|
-
if ([self hasChar] && [_parameterString characterAtIndex:_currentPosition] == '=') {
|
|
125
|
-
_currentPosition++; // skip '='
|
|
126
|
-
paramValue = [self parseQuotedTokenWithTerminators:charSetDelimiter];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if ([self hasChar] && ([_parameterString characterAtIndex:_currentPosition] == delimiter)) {
|
|
130
|
-
_currentPosition++; // skip separator
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (paramName != nil && paramName.length > 0) {
|
|
134
|
-
[params setValue:(paramValue ?: [NSNull null]) forKey:paramName];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return params;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
@end
|
|
142
|
-
|
|
143
|
-
NS_ASSUME_NONNULL_END
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2020 650 Industries, Inc. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <XCTest/XCTest.h>
|
|
4
|
-
|
|
5
|
-
#import <EXUpdates/EXUpdatesMultipartStreamReader.h>
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Fork of {@link EXUpdatesMultipartStreamReaderTests}.
|
|
9
|
-
*/
|
|
10
|
-
@interface EXUpdatesMultipartStreamReaderTests : XCTestCase
|
|
11
|
-
|
|
12
|
-
@end
|
|
13
|
-
|
|
14
|
-
@implementation EXUpdatesMultipartStreamReaderTests
|
|
15
|
-
|
|
16
|
-
- (void)testSimpleCase
|
|
17
|
-
{
|
|
18
|
-
NSString *response =
|
|
19
|
-
@"preamble, should be ignored\r\n"
|
|
20
|
-
@"--sample_boundary\r\n"
|
|
21
|
-
@"Content-Type: application/json; charset=utf-8\r\n"
|
|
22
|
-
@"Content-Length: 2\r\n\r\n"
|
|
23
|
-
@"{}\r\n"
|
|
24
|
-
@"--sample_boundary--\r\n"
|
|
25
|
-
@"epilogue, should be ignored";
|
|
26
|
-
|
|
27
|
-
NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]];
|
|
28
|
-
EXUpdatesMultipartStreamReader *reader = [[EXUpdatesMultipartStreamReader alloc] initWithInputStream:inputStream
|
|
29
|
-
boundary:@"sample_boundary"];
|
|
30
|
-
__block NSInteger count = 0;
|
|
31
|
-
BOOL success = [reader readAllPartsWithCompletionCallback:^(NSDictionary *headers, NSData *content, BOOL done) {
|
|
32
|
-
XCTAssertTrue(done);
|
|
33
|
-
XCTAssertEqualObjects(headers[@"Content-Type"], @"application/json; charset=utf-8");
|
|
34
|
-
XCTAssertEqualObjects([[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding], @"{}");
|
|
35
|
-
count++;
|
|
36
|
-
}];
|
|
37
|
-
XCTAssertTrue(success);
|
|
38
|
-
XCTAssertEqual(count, 1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
- (void)testMultipleParts
|
|
42
|
-
{
|
|
43
|
-
NSString *response =
|
|
44
|
-
@"preamble, should be ignored\r\n"
|
|
45
|
-
@"--sample_boundary\r\n"
|
|
46
|
-
@"1\r\n"
|
|
47
|
-
@"--sample_boundary\r\n"
|
|
48
|
-
@"2\r\n"
|
|
49
|
-
@"--sample_boundary\r\n"
|
|
50
|
-
@"3\r\n"
|
|
51
|
-
@"--sample_boundary--\r\n"
|
|
52
|
-
@"epilogue, should be ignored";
|
|
53
|
-
|
|
54
|
-
NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]];
|
|
55
|
-
EXUpdatesMultipartStreamReader *reader = [[EXUpdatesMultipartStreamReader alloc] initWithInputStream:inputStream
|
|
56
|
-
boundary:@"sample_boundary"];
|
|
57
|
-
__block NSInteger count = 0;
|
|
58
|
-
BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, NSData *content, BOOL done) {
|
|
59
|
-
count++;
|
|
60
|
-
XCTAssertEqual(done, count == 3);
|
|
61
|
-
NSString *expectedBody = [NSString stringWithFormat:@"%ld", (long)count];
|
|
62
|
-
NSString *actualBody = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding];
|
|
63
|
-
XCTAssertEqualObjects(actualBody, expectedBody);
|
|
64
|
-
}];
|
|
65
|
-
XCTAssertTrue(success);
|
|
66
|
-
XCTAssertEqual(count, 3);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
- (void)testMultiplePartsNoPreamble
|
|
70
|
-
{
|
|
71
|
-
NSString *response =
|
|
72
|
-
@"--sample_boundary\r\n"
|
|
73
|
-
@"1\r\n"
|
|
74
|
-
@"--sample_boundary\r\n"
|
|
75
|
-
@"2\r\n"
|
|
76
|
-
@"--sample_boundary\r\n"
|
|
77
|
-
@"3\r\n"
|
|
78
|
-
@"--sample_boundary--\r\n"
|
|
79
|
-
@"epilogue, should be ignored";
|
|
80
|
-
|
|
81
|
-
NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]];
|
|
82
|
-
EXUpdatesMultipartStreamReader *reader = [[EXUpdatesMultipartStreamReader alloc] initWithInputStream:inputStream
|
|
83
|
-
boundary:@"sample_boundary"];
|
|
84
|
-
__block NSInteger count = 0;
|
|
85
|
-
BOOL success = [reader readAllPartsWithCompletionCallback:^(__unused NSDictionary *headers, NSData *content, BOOL done) {
|
|
86
|
-
count++;
|
|
87
|
-
XCTAssertEqual(done, count == 3);
|
|
88
|
-
NSString *expectedBody = [NSString stringWithFormat:@"%ld", (long)count];
|
|
89
|
-
NSString *actualBody = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding];
|
|
90
|
-
XCTAssertEqualObjects(actualBody, expectedBody);
|
|
91
|
-
}];
|
|
92
|
-
XCTAssertTrue(success);
|
|
93
|
-
XCTAssertEqual(count, 3);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
- (void)testNoDelimiter
|
|
97
|
-
{
|
|
98
|
-
NSString *response = @"Yolo";
|
|
99
|
-
|
|
100
|
-
NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]];
|
|
101
|
-
EXUpdatesMultipartStreamReader *reader = [[EXUpdatesMultipartStreamReader alloc] initWithInputStream:inputStream
|
|
102
|
-
boundary:@"sample_boundary"];
|
|
103
|
-
__block NSInteger count = 0;
|
|
104
|
-
BOOL success = [reader readAllPartsWithCompletionCallback:^(
|
|
105
|
-
__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) {
|
|
106
|
-
count++;
|
|
107
|
-
}];
|
|
108
|
-
XCTAssertFalse(success);
|
|
109
|
-
XCTAssertEqual(count, 0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
- (void)testNoCloseDelimiter
|
|
113
|
-
{
|
|
114
|
-
NSString *response =
|
|
115
|
-
@"preamble, should be ignored\r\n"
|
|
116
|
-
@"--sample_boundary\r\n"
|
|
117
|
-
@"Content-Type: application/json; charset=utf-8\r\n"
|
|
118
|
-
@"Content-Length: 2\r\n\r\n"
|
|
119
|
-
@"{}\r\n"
|
|
120
|
-
@"--sample_boundary\r\n"
|
|
121
|
-
@"incomplete message...";
|
|
122
|
-
|
|
123
|
-
NSInputStream *inputStream = [NSInputStream inputStreamWithData:[response dataUsingEncoding:NSUTF8StringEncoding]];
|
|
124
|
-
EXUpdatesMultipartStreamReader *reader = [[EXUpdatesMultipartStreamReader alloc] initWithInputStream:inputStream
|
|
125
|
-
boundary:@"sample_boundary"];
|
|
126
|
-
__block NSInteger count = 0;
|
|
127
|
-
BOOL success = [reader readAllPartsWithCompletionCallback:^(
|
|
128
|
-
__unused NSDictionary *headers, __unused NSData *content, __unused BOOL done) {
|
|
129
|
-
count++;
|
|
130
|
-
}];
|
|
131
|
-
XCTAssertFalse(success);
|
|
132
|
-
XCTAssertEqual(count, 1);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
@end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2020 650 Industries, Inc. All rights reserved.
|
|
2
|
-
|
|
3
|
-
#import <XCTest/XCTest.h>
|
|
4
|
-
|
|
5
|
-
#import <EXUpdates/EXUpdatesParameterParser.h>
|
|
6
|
-
|
|
7
|
-
@interface EXUpdatesParameterParserTests : XCTestCase
|
|
8
|
-
@end
|
|
9
|
-
|
|
10
|
-
@implementation EXUpdatesParameterParserTests
|
|
11
|
-
|
|
12
|
-
- (void)testParameterParser
|
|
13
|
-
{
|
|
14
|
-
NSArray<NSArray *> *testCases = @[
|
|
15
|
-
@[@"", @{}],
|
|
16
|
-
@[@"test; test1 = stuff ; test2 = \"stuff; stuff\"; test3=\"stuff",
|
|
17
|
-
@{@"test": [NSNull null], @"test1": @"stuff", @"test2": @"stuff; stuff", @"test3": @"\"stuff"}
|
|
18
|
-
],
|
|
19
|
-
@[@" test ; test1=stuff ; ; test2=; test3; ",
|
|
20
|
-
@{@"test": [NSNull null], @"test1": @"stuff", @"test2": [NSNull null], @"test3": [NSNull null]}
|
|
21
|
-
],
|
|
22
|
-
@[@" test", @{@"test": [NSNull null]}],
|
|
23
|
-
@[@" ", @{}],
|
|
24
|
-
@[@" = stuff ", @{}],
|
|
25
|
-
@[@"text/plain; Charset=UTF-8", @{@"text/plain": [NSNull null], @"Charset": @"UTF-8"}],
|
|
26
|
-
@[@"param = \"stuff\\\"; more stuff\"", @{@"param": @"stuff\\\"; more stuff"}],
|
|
27
|
-
@[@"param = \"stuff\\\\\"; anotherparam", @{@"param": @"stuff\\\\", @"anotherparam": [NSNull null]}],
|
|
28
|
-
@[@"foo/bar; param=\"baz=bat\"", @{@"foo/bar": [NSNull null], @"param": @"baz=bat"}],
|
|
29
|
-
|
|
30
|
-
// Expo-specific tests
|
|
31
|
-
@[@"multipart/mixed; boundary=BbC04y", @{@"multipart/mixed": [NSNull null], @"boundary": @"BbC04y"}],
|
|
32
|
-
@[@"form-data; name=\"manifest\"; filename=\"hello2\"", @{@"form-data": [NSNull null], @"name": @"manifest", @"filename": @"hello2"}],
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
for (NSArray *testCase in testCases) {
|
|
36
|
-
NSString *parameterString = testCase[0];
|
|
37
|
-
NSDictionary *expectedDictionary = testCase[1];
|
|
38
|
-
|
|
39
|
-
NSDictionary *parameters = [[EXUpdatesParameterParser new] parseParameterString:parameterString withDelimiter:';'];
|
|
40
|
-
XCTAssertTrue([expectedDictionary isEqualToDictionary:parameters], @"result did not match expected");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@end
|