Skip to content

Commit bff653c

Browse files
authored
Merge pull request #103 from 417-72KI/binary-contains-crlf
Fix parsing binary including CRLF
2 parents a9bb857 + 4d6dc32 commit bff653c

File tree

6 files changed

+122
-32
lines changed

6 files changed

+122
-32
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
- name: Merge xcresult files
9595
run:
9696
xcrun xcresulttool merge test_output/**/*.xcresult --output-path test_output/TestResults.xcresult
97-
- uses: kishikawakatsumi/xcresulttool@v1
97+
- uses: slidoapp/xcresulttool@v3.1.0
9898
if: success() || failure()
9999
with:
100100
path: test_output/TestResults.xcresult

Sources/MultipartFormDataParser/MultipartFormData.swift

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,41 @@ public extension MultipartFormData.Element {
3535
}
3636

3737
extension MultipartFormData.Element {
38-
static func from(_ data: [Data]) -> Self {
38+
static func from(_ data: [Data]) -> Self? {
3939
var element = Self(name: "", data: .init(), fileName: nil, mimeType: nil)
40-
for line in data {
41-
guard let string = String(data: line, encoding: .utf8) else {
42-
element.data = line
43-
continue
44-
}
45-
46-
if let contentDispositionMatches = try! #/Content-Disposition: form-data; name="(?<name>.*?)"(; filename="(?<filename>.*?)")?/#.firstMatch(in: string) {
47-
element.name = String(contentDispositionMatches.output.name)
48-
if let filename = contentDispositionMatches.output.filename {
49-
element.fileName = String(filename)
50-
}
51-
continue
52-
}
53-
if let mimeTypeMatches = try! #/Content-Type: (?<mimetype>.*/.*)/#.firstMatch(in: string) {
40+
guard let firstLine = data.first
41+
.flatMap({ String(data: $0, encoding: .utf8) }),
42+
let contentDispositionMatches = matchContentDisposition(in: firstLine) else {
43+
return nil
44+
}
45+
element.name = String(contentDispositionMatches.output.name)
46+
if let filename = contentDispositionMatches.output.filename {
47+
element.fileName = String(filename)
48+
}
49+
for line in data.dropFirst() {
50+
if let string = String(data: line, encoding: .utf8),
51+
let mimeTypeMatches = matchMimeType(in: string) {
5452
element.mimeType = String(mimeTypeMatches.output.mimetype)
5553
continue
5654
}
57-
if element.data.isEmpty {
58-
element.data = line
55+
if !element.data.isEmpty {
56+
element.data.append(Data("\r\n".utf8))
5957
}
58+
element.data.append(line)
6059
}
6160
return element
6261
}
6362
}
63+
64+
private extension MultipartFormData.Element {
65+
// swiftlint:disable:next large_tuple
66+
static func matchContentDisposition(in string: String) -> Regex<(Substring, name: Substring, Substring?, filename: Substring?)>.Match? {
67+
try! #/Content-Disposition: form-data; name="(?<name>.*?)"(; filename="(?<filename>.*?)")?/#
68+
.firstMatch(in: string)
69+
}
70+
71+
static func matchMimeType(in string: String) -> Regex<(Substring, mimetype: Substring)>.Match? {
72+
try! #/Content-Type: (?<mimetype>.*/.*)/#
73+
.firstMatch(in: string)
74+
}
75+
}

Tests/MultipartFormDataParserTests/MultipartFormDataParserTests.swift

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ final class MultipartFormDataParserTests: XCTestCase {
2424
let genbaNeko = try XCTUnwrap(genbaNeko)
2525
let denwaNeko = try XCTUnwrap(denwaNeko)
2626
let message = Data("Hello world!".utf8)
27-
let request = createRequest(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message)
27+
let request = createRequest(
28+
genbaNeko: genbaNeko,
29+
denwaNeko: denwaNeko,
30+
pdf: pdf,
31+
message: message
32+
)
2833
let data = try MultipartFormData.parse(from: request)
2934
XCTAssertEqual(data.element(forName: "genbaNeko")?.data, genbaNeko)
3035
XCTAssertEqual(data.element(forName: "denwaNeko")?.data, denwaNeko)
@@ -35,10 +40,15 @@ final class MultipartFormDataParserTests: XCTestCase {
3540
let genbaNeko = try XCTUnwrap(genbaNeko)
3641
let denwaNeko = try XCTUnwrap(denwaNeko)
3742
let message = Data("Hello world!".utf8)
38-
let request = createRequest(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message)
43+
let request = createRequest(
44+
genbaNeko: genbaNeko,
45+
denwaNeko: denwaNeko,
46+
pdf: pdf,
47+
message: message
48+
)
3949
let data = try MultipartFormData.parse(from: request)
40-
XCTAssertEqual(["genbaNeko", "denwaNeko", "message"], data.map(\.name))
41-
XCTAssertEqual([genbaNeko, denwaNeko, Data("Hello world!".utf8)], data.map(\.data))
50+
XCTAssertEqual(["genbaNeko", "denwaNeko", "pdf", "message"], data.map(\.name))
51+
XCTAssertEqual([genbaNeko, denwaNeko, pdf, Data("Hello world!".utf8)], data.map(\.data))
4252
}
4353

4454
// MARK: Failure
@@ -100,7 +110,13 @@ final class MultipartFormDataParserTests: XCTestCase {
100110
let denwaNeko = try XCTUnwrap(denwaNeko)
101111
let message = Data("Hello world!".utf8)
102112

103-
let result = try XCTUnwrap(uploadWithAlamoFire(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message))
113+
let result = try XCTUnwrap(
114+
uploadWithAlamoFire(
115+
genbaNeko: genbaNeko,
116+
denwaNeko: denwaNeko,
117+
message: message
118+
)
119+
)
104120
XCTAssertEqual(result.status, 200)
105121
XCTAssertNil(result.error)
106122
}
@@ -125,6 +141,7 @@ final class MultipartFormDataParserTests: XCTestCase {
125141
let request = try requestWithAPIKit(
126142
genbaNeko: genbaNeko,
127143
denwaNeko: denwaNeko,
144+
pdf: pdf,
128145
message: message
129146
)
130147
let data = try MultipartFormData.parse(from: request)
@@ -134,7 +151,14 @@ final class MultipartFormDataParserTests: XCTestCase {
134151
}
135152

136153
try runActivity(named: "stub") {
137-
let result = try XCTUnwrap(uploadWithAPIKit(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message))
154+
let result = try XCTUnwrap(
155+
uploadWithAPIKit(
156+
genbaNeko: genbaNeko,
157+
denwaNeko: denwaNeko,
158+
pdf: pdf,
159+
message: message
160+
)
161+
)
138162
XCTAssertEqual(result.status, 200)
139163
XCTAssertNil(result.error)
140164
}
@@ -150,7 +174,12 @@ final class MultipartFormDataParserTests: XCTestCase {
150174
let genbaNeko = try XCTUnwrap(genbaNeko)
151175
let denwaNeko = try XCTUnwrap(denwaNeko)
152176
let message = Data("Hello world!".utf8)
153-
let result = try await uploadURLSessionUpload(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message)
177+
let result = try await uploadURLSessionUpload(
178+
genbaNeko: genbaNeko,
179+
denwaNeko: denwaNeko,
180+
pdf: pdf,
181+
message: message
182+
)
154183
XCTAssertEqual(result.status, 200)
155184
XCTAssertNil(result.error)
156185
}
@@ -159,7 +188,12 @@ final class MultipartFormDataParserTests: XCTestCase {
159188
let genbaNeko = try XCTUnwrap(genbaNeko)
160189
let denwaNeko = try XCTUnwrap(denwaNeko)
161190
let message = Data("Hello world!".utf8)
162-
let result = try await uploadURLSessionData(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message)
191+
let result = try await uploadURLSessionData(
192+
genbaNeko: genbaNeko,
193+
denwaNeko: denwaNeko,
194+
pdf: pdf,
195+
message: message
196+
)
163197
XCTAssertEqual(result.status, 200)
164198
XCTAssertNil(result.error)
165199
}
@@ -193,4 +227,6 @@ private extension MultipartFormDataParserTests {
193227
return TestResource.denwaNeko
194228
#endif
195229
}
230+
231+
var pdf: Data { TestResource.pdf }
196232
}

Tests/MultipartFormDataParserTests/_TestFunctions/TestFunction_APIKit.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ extension XCTestCase {
1515
func requestWithAPIKit(
1616
genbaNeko: Data,
1717
denwaNeko: Data,
18+
pdf: Data,
1819
message: Data,
1920
file: StaticString = #filePath,
2021
line: UInt = #line
2122
) throws -> URLRequest {
2223
try TestRequest(
2324
genbaNeko: genbaNeko,
2425
denwaNeko: denwaNeko,
26+
pdf: pdf,
2527
message: message,
2628
file: file,
2729
line: line
@@ -31,6 +33,7 @@ extension XCTestCase {
3133
func uploadWithAPIKit(
3234
genbaNeko: Data,
3335
denwaNeko: Data,
36+
pdf: Data,
3437
message: Data,
3538
retryCount: UInt = 3,
3639
file: StaticString = #filePath,
@@ -40,6 +43,7 @@ extension XCTestCase {
4043
let request = TestRequest(
4144
genbaNeko: genbaNeko,
4245
denwaNeko: denwaNeko,
46+
pdf: pdf,
4347
message: message,
4448
file: file,
4549
line: line
@@ -58,7 +62,15 @@ extension XCTestCase {
5862
case let .failure(error):
5963
if retryCount > 0 {
6064
print("retry: \(retryCount)")
61-
return try uploadWithAPIKit(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message, retryCount: retryCount - 1, file: file, line: line)
65+
return try uploadWithAPIKit(
66+
genbaNeko: genbaNeko,
67+
denwaNeko: denwaNeko,
68+
pdf: pdf,
69+
message: message,
70+
retryCount: retryCount - 1,
71+
file: file,
72+
line: line
73+
)
6274
}
6375
throw error
6476
}
@@ -74,6 +86,7 @@ private struct TestRequest: APIKit.Request {
7486

7587
var genbaNeko: Data
7688
var denwaNeko: Data
89+
var pdf: Data
7790
var message: Data
7891

7992
var file: StaticString
@@ -84,14 +97,20 @@ private struct TestRequest: APIKit.Request {
8497
.init(
8598
data: genbaNeko,
8699
name: "genbaNeko",
87-
mimeType: "genbaNeko.jpeg",
88-
fileName: "image/jpeg"
100+
mimeType: "image/jpeg",
101+
fileName: "genbaNeko.jpeg"
89102
),
90103
.init(
91104
data: denwaNeko,
92105
name: "denwaNeko",
93-
mimeType: "denwaNeko.jpeg",
94-
fileName: "image/jpeg"
106+
mimeType: "image/jpeg",
107+
fileName: "denwaNeko.jpeg"
108+
),
109+
.init(
110+
data: pdf,
111+
name: "pdf",
112+
mimeType: "application/pdf",
113+
fileName: "example.pdf"
95114
),
96115
.init(data: message, name: "message"),
97116
]

Tests/MultipartFormDataParserTests/_TestFunctions/TestFunction_URLSession.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ extension XCTestCase {
1414
func uploadURLSessionData(
1515
genbaNeko: Data,
1616
denwaNeko: Data,
17+
pdf: Data,
1718
message: Data,
1819
retryCount: UInt = 3,
1920
file: StaticString = #filePath,
2021
line: UInt = #line
2122
) async throws -> TestEntity {
22-
let request = createRequest(genbaNeko: genbaNeko, denwaNeko: denwaNeko, message: message)
23+
let request = createRequest(
24+
genbaNeko: genbaNeko,
25+
denwaNeko: denwaNeko,
26+
pdf: pdf,
27+
message: message
28+
)
2329
do {
2430
let (data, _) = try await session.data(for: request)
2531
return try JSONDecoder().decode(TestEntity.self, from: data)
@@ -28,6 +34,7 @@ extension XCTestCase {
2834
return try await uploadURLSessionData(
2935
genbaNeko: genbaNeko,
3036
denwaNeko: denwaNeko,
37+
pdf: pdf,
3138
message: message,
3239
retryCount: retryCount - 1,
3340
file: file,
@@ -39,6 +46,7 @@ extension XCTestCase {
3946
func uploadURLSessionUpload(
4047
genbaNeko: Data,
4148
denwaNeko: Data,
49+
pdf: Data,
4250
message: Data,
4351
retryCount: UInt = 3,
4452
file: StaticString = #filePath,
@@ -52,6 +60,7 @@ extension XCTestCase {
5260
boundary: boundary,
5361
genbaNeko: genbaNeko,
5462
denwaNeko: denwaNeko,
63+
pdf: pdf,
5564
message: message
5665
)
5766
do {
@@ -62,6 +71,7 @@ extension XCTestCase {
6271
return try await uploadURLSessionUpload(
6372
genbaNeko: genbaNeko,
6473
denwaNeko: denwaNeko,
74+
pdf: pdf,
6575
message: message,
6676
retryCount: retryCount - 1,
6777
file: file,
@@ -75,6 +85,7 @@ extension XCTestCase {
7585
func createRequest(
7686
genbaNeko: Data,
7787
denwaNeko: Data,
88+
pdf: Data,
7889
message: Data,
7990
file: StaticString = #filePath,
8091
line: UInt = #line
@@ -87,6 +98,7 @@ extension XCTestCase {
8798
boundary: boundary,
8899
genbaNeko: genbaNeko,
89100
denwaNeko: denwaNeko,
101+
pdf: pdf,
90102
message: message
91103
)
92104
return request
@@ -97,6 +109,7 @@ private func createBody(
97109
boundary: String,
98110
genbaNeko: Data,
99111
denwaNeko: Data,
112+
pdf: Data,
100113
message: Data
101114
) -> Data {
102115
[
@@ -113,6 +126,12 @@ private func createBody(
113126
denwaNeko,
114127
Data("\r\n".utf8),
115128
Data("--\(boundary)\r\n".utf8),
129+
Data("Content-Disposition: form-data; name=\"pdf\"; filename=\"example.pdf\"\r\n".utf8),
130+
Data("Content-Type: application/pdf\r\n".utf8),
131+
Data("\r\n".utf8),
132+
pdf,
133+
Data("\r\n".utf8),
134+
Data("--\(boundary)\r\n".utf8),
116135
Data("Content-Disposition: form-data; name=\"message\"\r\n".utf8),
117136
Data("\r\n".utf8),
118137
message,

0 commit comments

Comments
 (0)