diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 400ff99..d2a84ba 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -15,7 +15,7 @@ jobs: os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: swift build -v test: @@ -26,5 +26,5 @@ jobs: os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: swift test -v diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index 986cf15..93c3f9f 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -22,7 +22,7 @@ jobs: name: SwiftLint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: norio-nomura/action-swiftlint@3.2.1 with: args: --strict diff --git a/Sources/MultipartFormData/Boundary.swift b/Sources/MultipartFormData/Boundary.swift index c75db98..251178f 100644 --- a/Sources/MultipartFormData/Boundary.swift +++ b/Sources/MultipartFormData/Boundary.swift @@ -104,6 +104,6 @@ extension Boundary: CustomDebugStringConvertible { extension Boundary { internal var _value: String { - return String(decoding: _asciiData, as: UTF8.self) // UTF-8 representation is exactly equivalent to ASCII + return String(bytes: _asciiData, encoding: .ascii) ?? "" } } diff --git a/Sources/MultipartFormData/Internal/String+helpers.swift b/Sources/MultipartFormData/Internal/String+helpers.swift index e349138..e7283d2 100644 --- a/Sources/MultipartFormData/Internal/String+helpers.swift +++ b/Sources/MultipartFormData/Internal/String+helpers.swift @@ -7,6 +7,7 @@ extension String { internal init(_ staticString: StaticString) { + // swiftlint:disable:next optional_data_string_conversion self = staticString.withUTF8Buffer { String(decoding: $0, as: UTF8.self) } } } diff --git a/Sources/MultipartFormData/MultipartFormData.swift b/Sources/MultipartFormData/MultipartFormData.swift index a29ac59..f8dac25 100644 --- a/Sources/MultipartFormData/MultipartFormData.swift +++ b/Sources/MultipartFormData/MultipartFormData.swift @@ -162,6 +162,7 @@ extension MultipartFormData { extension MultipartFormData: CustomDebugStringConvertible { public var debugDescription: String { - return String(decoding: contentType._data + ._crlf + ._crlf + httpBody, as: UTF8.self) + let bytes: Data = contentType._data + ._crlf + ._crlf + httpBody + return String(bytes: bytes, encoding: .utf8) ?? "" } } diff --git a/Sources/MultipartFormData/Subpart.swift b/Sources/MultipartFormData/Subpart.swift index 37ef512..c88e7c7 100644 --- a/Sources/MultipartFormData/Subpart.swift +++ b/Sources/MultipartFormData/Subpart.swift @@ -80,7 +80,7 @@ extension Subpart { extension Subpart: CustomDebugStringConvertible { public var debugDescription: String { - return String(decoding: _data, as: UTF8.self) + return String(bytes: _data, encoding: .utf8) ?? "" } } diff --git a/Tests/MultipartFormDataTests/ContentDispositionTests.swift b/Tests/MultipartFormDataTests/ContentDispositionTests.swift index 71ce853..227fd89 100644 --- a/Tests/MultipartFormDataTests/ContentDispositionTests.swift +++ b/Tests/MultipartFormDataTests/ContentDispositionTests.swift @@ -9,28 +9,36 @@ import XCTest @testable import MultipartFormData final class ContentDispositionTests: XCTestCase { - func testPercentEncodingError() throws { + func testUncheckedInitValid() { XCTAssertNoThrow(try ContentDisposition(uncheckedName: "a", uncheckedFilename: "a")) - + } + + func testUncheckedInitInvalid() throws { // https://stackoverflow.com/questions/33558933/why-is-the-return-value-of-string-addingpercentencoding-optional - // does not work on Linux, can still encoding there - let nonPercentEncodableString = try XCTUnwrap(String(bytes: [0xD8, 0x00] as [UInt8], encoding: .utf16BigEndian)) - if nonPercentEncodableString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) == nil { - XCTAssertThrowsError(try ContentDisposition(uncheckedName: nonPercentEncodableString, uncheckedFilename: nil)) - XCTAssertThrowsError(try ContentDisposition(uncheckedName: "", uncheckedFilename: nonPercentEncodableString)) + let bytes: [UInt8] = [0xD8, 0x00] + + // Ensure the non-encodable string can be created, e.g. on iOS 18 it no longer works. + guard let nonPercentEncodableString = String(bytes: bytes, encoding: .utf16BigEndian) else { + throw XCTSkip("UTF16 byte encoding failed") } + + // Ensure the percent-encoding fails on the current platform, e.g. on Linux it encodes. + guard nonPercentEncodableString.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) == nil else { + throw XCTSkip("percent encoding didn't fail") + } + + XCTAssertThrowsError(try ContentDisposition(uncheckedName: nonPercentEncodableString, uncheckedFilename: nil)) + XCTAssertThrowsError(try ContentDisposition(uncheckedName: "", uncheckedFilename: nonPercentEncodableString)) } func testParameters() throws { let contentDisposition = ContentDisposition(name: "a", filename: "a") - XCTAssertEqual(contentDisposition.parameters[0], HTTPHeaderParameter("name", value: "a")) XCTAssertEqual(contentDisposition.parameters[1], HTTPHeaderParameter("filename", value: "a")) } func testData() throws { let contentDisposition = ContentDisposition(name: "a", filename: "a") - XCTAssertEqual(contentDisposition._data, Data("Content-Disposition: form-data; name=\"a\"; filename=\"a\"".utf8)) } }