Skip to content

Commit 56a5d40

Browse files
committed
Inititial multipartform structures
1 parent 67105c3 commit 56a5d40

File tree

7 files changed

+708
-10
lines changed

7 files changed

+708
-10
lines changed

FlyingFox/Sources/HTTPDecoder.swift

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,20 +98,11 @@ struct HTTPDecoder {
9898
return (path, query ?? [])
9999
}
100100

101-
@Sendable
102-
func readHeader(from line: String) -> (header: HTTPHeader, value: String)? {
103-
let comps = line.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: true)
104-
guard comps.count > 1 else { return nil }
105-
let name = comps[0].trimmingCharacters(in: .whitespacesAndNewlines)
106-
let value = comps[1].trimmingCharacters(in: .whitespacesAndNewlines)
107-
return (HTTPHeader(name), value)
108-
}
109-
110101
func readHeaders(from bytes: some AsyncBufferedSequence<UInt8>) async throws -> [HTTPHeader : String] {
111102
try await bytes
112103
.lines
113104
.prefix { $0 != "\r" && $0 != "" }
114-
.compactMap(readHeader)
105+
.compactMap(Self.readHeader)
115106
.reduce(into: [HTTPHeader: String]()) { $0[$1.header] = $1.value }
116107
}
117108

@@ -140,6 +131,54 @@ struct HTTPDecoder {
140131
}
141132
}
142133

134+
extension HTTPDecoder {
135+
136+
static func readHeader(from line: String) -> (header: HTTPHeader, value: String)? {
137+
guard let (name, value) = Self.readField(from: line, separator: ":") else {
138+
return nil
139+
}
140+
return (HTTPHeader(name), value)
141+
}
142+
143+
static func readField<S: StringProtocol>(from line: S, separator: S.Element) -> (name: String, value: String)? {
144+
let comps = line.split(separator: separator, maxSplits: 1, omittingEmptySubsequences: true)
145+
guard comps.count > 1 else { return nil }
146+
let name = comps[0].trimmingCharacters(in: .whitespacesAndNewlines)
147+
let value = comps[1].trimmingCharacters(in: .whitespacesAndNewlines)
148+
return (name, value)
149+
}
150+
151+
static func multipartFormDataBoundary(from contentType: String?) throws -> String {
152+
guard let components = contentType?.split(separator: ";"),
153+
components.first?.trimmingCharacters(in: .whitespaces) == "multipart/form-data" else {
154+
throw Error("Expected Content-Type: multipart/form-data")
155+
}
156+
157+
for comp in components {
158+
if let field = readField(from: comp, separator: "="),
159+
field.name == "boundary" {
160+
return field.value
161+
}
162+
}
163+
throw Error("Expected boundary=")
164+
}
165+
166+
static func multipartFormDataName(from contentDisposition: String?) -> String? {
167+
guard let components = contentDisposition?.split(separator: ";"),
168+
components.first?.trimmingCharacters(in: .whitespaces) == "form-data" else {
169+
return nil
170+
}
171+
172+
for comp in components {
173+
if let field = readField(from: comp, separator: "="),
174+
field.name == "name" {
175+
return field.value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
176+
}
177+
}
178+
return nil
179+
}
180+
}
181+
143182
extension HTTPDecoder {
144183

145184
struct Error: LocalizedError {
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//
2+
// DelimitedDataIterator.swift
3+
// FlyingFox
4+
//
5+
// Created by Simon Whitty on 12/11/2023.
6+
// Copyright © 2023 Simon Whitty. All rights reserved.
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/FlyingFox
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
import Foundation
33+
34+
struct DelimitedDataIterator<I: AsyncIteratorProtocol> where I.Element == Data {
35+
36+
private var iterator: I
37+
private let delimiter: Data
38+
39+
init(iterator: I, delimiter: Data) {
40+
self.iterator = iterator
41+
self.delimiter = delimiter
42+
}
43+
44+
private var buffer = Data()
45+
private var isComplete: Bool = false
46+
47+
private mutating func bufferedRange(of delimiter: Data) async throws -> Range<Int>? {
48+
if let range = buffer.firstRange(of: delimiter) {
49+
return range
50+
}
51+
guard !isComplete else { return nil }
52+
while let data = try await iterator.next() {
53+
buffer.append(data)
54+
if let range = buffer.firstRange(of: delimiter) {
55+
return range
56+
}
57+
}
58+
isComplete = true
59+
return nil
60+
}
61+
62+
mutating func next(of match: Data) async throws -> Data? {
63+
if let range = try await bufferedRange(of: match) {
64+
buffer = Data(buffer[range.endIndex...])
65+
return match
66+
}
67+
return nil
68+
}
69+
70+
mutating func nextUntil(_ match: Data) async throws -> Data? {
71+
guard let range = try await bufferedRange(of: match) else {
72+
defer { buffer = Data() }
73+
return buffer.isEmpty ? nil : buffer
74+
}
75+
76+
let slice = Data(buffer[..<range.startIndex])
77+
buffer = Data(buffer[range.endIndex...])
78+
79+
return slice
80+
}
81+
82+
mutating func next() async throws -> Data? {
83+
try await nextUntil(delimiter)
84+
}
85+
86+
private func firstBufferedRange(of delimiters: Data...) -> (delimited: Data, range: Range<Int>)? {
87+
for delimiter in delimiters {
88+
if let range = buffer.firstRange(of: delimiter) {
89+
return (delimiter, range)
90+
}
91+
}
92+
return nil
93+
}
94+
95+
private mutating func AbufferedRange(of delimiter: Data) async throws -> Range<Int>? {
96+
if let range = buffer.firstRange(of: delimiter) {
97+
return range
98+
}
99+
guard !isComplete else { return nil }
100+
while let data = try await iterator.next() {
101+
buffer.append(data)
102+
if let range = buffer.firstRange(of: delimiter) {
103+
return range
104+
}
105+
}
106+
isComplete = true
107+
return nil
108+
}
109+
110+
enum Part {
111+
case data(Data)
112+
case delimiter(Data)
113+
case end
114+
}
115+
116+
117+
}
118+
119+
extension Data {
120+
121+
enum Match: Equatable {
122+
case partial(Range<Index>)
123+
case complete(Range<Index>)
124+
}
125+
126+
func firstMatch(of data: Data) -> Match? {
127+
firstMatch(of: data, from: startIndex)
128+
}
129+
130+
func firstMatch(of data: Data, from idx: Index) -> Match? {
131+
guard !data.isEmpty else { return nil }
132+
133+
var position = idx
134+
135+
while position < endIndex {
136+
switch matches(data, at: position) {
137+
case .complete(let range):
138+
return .complete(range)
139+
case .partial(let range):
140+
position = range.upperBound
141+
if position == endIndex {
142+
return .partial(range)
143+
}
144+
case .none:
145+
position = index(after: position)
146+
}
147+
}
148+
return nil
149+
}
150+
151+
func matches(_ data: Data, at idx: Index) -> Match? {
152+
var haystackIndex = idx
153+
var needleIndex = data.startIndex
154+
155+
while true {
156+
guard self[haystackIndex] == data[needleIndex] else {
157+
if needleIndex == data.startIndex {
158+
// at start no match
159+
return nil
160+
} else {
161+
// partial match
162+
return .partial(idx..<haystackIndex)
163+
}
164+
}
165+
166+
haystackIndex = index(after: haystackIndex)
167+
needleIndex = data.index(after: needleIndex)
168+
169+
if needleIndex == data.endIndex {
170+
return .complete(idx..<haystackIndex)
171+
} else if haystackIndex == endIndex {
172+
return .partial(idx..<haystackIndex)
173+
}
174+
}
175+
}
176+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// FormData.swift
3+
// FlyingFox
4+
//
5+
// Created by Simon Whitty on 09/11/2023.
6+
// Copyright © 2023 Simon Whitty. All rights reserved.
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/FlyingFox
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
import Foundation
33+
34+
public struct FormData: Sendable, Hashable {
35+
public var headers: [FormHeader: String]
36+
37+
// required property
38+
public var name: String? {
39+
HTTPDecoder.multipartFormDataName(from: headers[.contentDisposition])
40+
}
41+
42+
// todo Sequence of bytes
43+
public var body: Data
44+
45+
public init(headers: [FormHeader: String],
46+
body: Data) {
47+
self.headers = headers
48+
self.body = body
49+
}
50+
}

0 commit comments

Comments
 (0)