Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MultipartFormData ContentHeaderEncoding UTF-8 #3754

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
86 changes: 72 additions & 14 deletions Source/MultipartFormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,20 @@ open class MultipartFormData {
/// - name: Name to associate with the `Data` in the `Content-Disposition` HTTP header.
/// - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header.
/// - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header.
public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
/// - encoding: Encoding for the `Content-Type` HTTP header.
public func append(
_ data: Data,
withName name: String,
fileName: String? = nil,
mimeType: String? = nil,
encoding: ContentHeaderEncoding? = nil
) {
let headers = contentHeaders(
withName: name,
fileName: fileName,
mimeType: mimeType,
encoding: encoding
)
let stream = InputStream(data: data)
let length = UInt64(data.count)

Expand All @@ -171,13 +183,18 @@ open class MultipartFormData {
/// - Parameters:
/// - fileURL: `URL` of the file whose content will be encoded into the instance.
/// - name: Name to associate with the file content in the `Content-Disposition` HTTP header.
public func append(_ fileURL: URL, withName name: String) {
/// - encoding: Encoding for the `Content-Type` HTTP header.
public func append(
_ fileURL: URL,
withName name: String,
encoding: ContentHeaderEncoding? = nil
) {
let fileName = fileURL.lastPathComponent
let pathExtension = fileURL.pathExtension

if !fileName.isEmpty && !pathExtension.isEmpty {
let mime = mimeType(forPathExtension: pathExtension)
append(fileURL, withName: name, fileName: fileName, mimeType: mime)
append(fileURL, withName: name, fileName: fileName, mimeType: mime, encoding: encoding)
} else {
setBodyPartError(withReason: .bodyPartFilenameInvalid(in: fileURL))
}
Expand All @@ -197,8 +214,20 @@ open class MultipartFormData {
/// - name: Name to associate with the file content in the `Content-Disposition` HTTP header.
/// - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header.
/// - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header.
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
/// - encoding: Encoding for the `Content-Type` HTTP header.
public func append(
_ fileURL: URL,
withName name: String,
fileName: String,
mimeType: String,
encoding: ContentHeaderEncoding? = nil
) {
let headers = contentHeaders(
withName: name,
fileName: fileName,
mimeType: mimeType,
encoding: encoding
)

//============================================================
// Check 1 - is file URL?
Expand Down Expand Up @@ -283,12 +312,21 @@ open class MultipartFormData {
/// - name: Name to associate with the stream content in the `Content-Disposition` HTTP header.
/// - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header.
/// - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header.
public func append(_ stream: InputStream,
withLength length: UInt64,
name: String,
fileName: String,
mimeType: String) {
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
/// - encoding: Encoding for the `Content-Type` HTTP header.
public func append(
_ stream: InputStream,
withLength length: UInt64,
name: String,
fileName: String,
mimeType: String,
encoding: ContentHeaderEncoding? = nil
) {
let headers = contentHeaders(
withName: name,
fileName: fileName,
mimeType: mimeType,
encoding: encoding
)
append(stream, withLength: length, headers: headers)
}

Expand Down Expand Up @@ -370,6 +408,12 @@ open class MultipartFormData {
}
}

// MARK: - Public - Content Header Encoding

public enum ContentHeaderEncoding: String {
case utf8 = "UTF-8"
}

// MARK: - Private - Body Part Encoding

private func encode(_ bodyPart: BodyPart) throws -> Data {
Expand Down Expand Up @@ -510,9 +554,23 @@ open class MultipartFormData {

// MARK: - Private - Content Headers

private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> HTTPHeaders {
private func contentHeaders(
withName name: String,
fileName: String? = nil,
mimeType: String? = nil,
encoding: ContentHeaderEncoding?
) -> HTTPHeaders {
var disposition = "form-data; name=\"\(name)\""
if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" }

if let fileName = fileName {
let encodingPrefix: String
if let encoding = encoding {
encodingPrefix = "*=\(encoding.rawValue)"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GIST, optional encoding Info with one of:
*=UTF-8
default is without only
=

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it look's like its not the client but the server, the server falls back to iso-8859-1 if there is only =.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the server uses the UTF-8 String but only with iso-8859-1 char set so there are unknown characters. With *=UTF-8 we can tell the server explicitly that we are using UTF-8.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed iso-8859-1 and added some tests
the change is optional and does not change current behaviour

} else {
encodingPrefix = "="
}
disposition += "; filename\(encodingPrefix)\"\(fileName)\""
}

var headers: HTTPHeaders = [.contentDisposition(disposition)]
if let mimeType = mimeType { headers.add(.contentType(mimeType)) }
Expand Down
82 changes: 82 additions & 0 deletions Tests/MultipartFormDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,88 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
}
}

func testEncodingFileBodyPartUTF8() {
// Given
let multipartFormData = MultipartFormData()

let unicornImageURL = url(forResource: "unicorn", withExtension: "png")
multipartFormData.append(
unicornImageURL,
withName: "unicorn",
encoding: .utf8
)

var encodedData: Data?

// When
do {
encodedData = try multipartFormData.encode()
} catch {
// No-op
}

// Then
XCTAssertNotNil(encodedData, "encoded data should not be nil")

if let encodedData = encodedData {
let boundary = multipartFormData.boundary

var expectedData = Data()
expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary))
expectedData.append(Data((
"Content-Disposition: form-data; name=\"unicorn\"; filename*=UTF-8\"unicorn.png\"\(crlf)" +
"Content-Type: image/png\(crlf)\(crlf)").utf8
)
)
expectedData.append(try! Data(contentsOf: unicornImageURL))
expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary))

XCTAssertEqual(encodedData, expectedData, "data should match expected data")
}
}

func testEncodingFileBodyPartFileNameMimeTypeUTF8() {
// Given
let multipartFormData = MultipartFormData()

let unicornImageURL = url(forResource: "unicorn", withExtension: "png")
multipartFormData.append(
unicornImageURL,
withName: "unicorn",
fileName: "unicorn",
mimeType: "image/png",
encoding: .utf8
)

var encodedData: Data?

// When
do {
encodedData = try multipartFormData.encode()
} catch {
// No-op
}

// Then
XCTAssertNotNil(encodedData, "encoded data should not be nil")

if let encodedData = encodedData {
let boundary = multipartFormData.boundary

var expectedData = Data()
expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .initial, boundaryKey: boundary))
expectedData.append(Data((
"Content-Disposition: form-data; name=\"unicorn\"; filename*=UTF-8\"unicorn.png\"\(crlf)" +
"Content-Type: image/png\(crlf)\(crlf)").utf8
)
)
expectedData.append(try! Data(contentsOf: unicornImageURL))
expectedData.append(BoundaryGenerator.boundaryData(boundaryType: .final, boundaryKey: boundary))

XCTAssertEqual(encodedData, expectedData, "data should match expected data")
}
}

func testEncodingMultipleFileBodyParts() {
// Given
let multipartFormData = MultipartFormData()
Expand Down