Skip to content

Fix Data Base64 length encoding bug #1425

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Sources/FoundationEssentials/Data/Data+Base64.swift
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,12 @@ extension Base64 {

let lineLength = options.contains(.lineLength64Characters) ? 64 : 76
let lineBreaks = capacityWithoutBreaks / lineLength
let lineBreakCapacity = lineBreaks * seperatorBytes
var lineBreakCapacity = lineBreaks * seperatorBytes
// in case the last row uses all available space, we don't need to add line breaks
// but we can't remove bytes if we have an empty input
if capacityWithoutBreaks % lineLength == 0 && capacityWithoutBreaks > seperatorBytes {
lineBreakCapacity -= seperatorBytes
}
return capacityWithoutBreaks + lineBreakCapacity
}

Expand Down
52 changes: 52 additions & 0 deletions Tests/FoundationEssentialsTests/DataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2276,6 +2276,58 @@ extension DataTests {
#expect("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gVXQgYXQgdGluY2lkdW50IGFyY3UuIFN1c3BlbmRpc3NlIG5lYyBzb2RhbGVzIGVyYXQsIHNpdCBhbWV0IGltcGVyZGlldCBpcHN1bS4gRXRpYW0gc2VkIG9ybmFyZSBmZWxpcy4gTnVuYyBtYXVyaXMgdHVycGlzLCBiaWJlbmR1bSBub24gbGVjdHVzIHF1aXMsIG1hbGVzdWFkYSBwbGFjZXJhdCB0dXJwaXMuIE5hbSBhZGlwaXNjaW5nIG5vbiBtYXNzYSBldCBzZW1wZXIuIE51bGxhIGNvbnZhbGxpcyBzZW1wZXIgYmliZW5kdW0uIEFsaXF1YW0gZGljdHVtIG51bGxhIGN1cnN1cyBtaSB1bHRyaWNpZXMsIGF0IHRpbmNpZHVudCBtaSBzYWdpdHRpcy4gTnVsbGEgZmF1Y2lidXMgYXQgZHVpIHF1aXMgc29kYWxlcy4gTW9yYmkgcnV0cnVtLCBkdWkgaWQgdWx0cmljZXMgdmVuZW5hdGlzLCBhcmN1IHVybmEgZWdlc3RhcyBmZWxpcywgdmVsIHN1c2NpcGl0IG1hdXJpcyBhcmN1IHF1aXMgcmlzdXMuIE51bmMgdmVuZW5hdGlzIGxpZ3VsYSBhdCBvcmNpIHRyaXN0aXF1ZSwgZXQgbWF0dGlzIHB1cnVzIHB1bHZpbmFyLiBFdGlhbSB1bHRyaWNpZXMgZXN0IG9kaW8uIE51bmMgZWxlaWZlbmQgbWFsZXN1YWRhIGp1c3RvLCBuZWMgZXVpc21vZCBzZW0gdWx0cmljZXMgcXVpcy4gRXRpYW0gbmVjIG5pYmggc2l0IGFtZXQgbG9yZW0gZmF1Y2lidXMgZGFwaWJ1cyBxdWlzIG5lYyBsZW8uIFByYWVzZW50IHNpdCBhbWV0IG1hdXJpcyB2ZWwgbGFjdXMgaGVuZHJlcml0IHBvcnRhIG1vbGxpcyBjb25zZWN0ZXR1ciBtaS4gRG9uZWMgZWdldCB0b3J0b3IgZHVpLiBNb3JiaSBpbXBlcmRpZXQsIGFyY3Ugc2l0IGFtZXQgZWxlbWVudHVtIGludGVyZHVtLCBxdWFtIG5pc2wgdGVtcG9yIHF1YW0sIHZpdGFlIGZldWdpYXQgYXVndWUgcHVydXMgc2VkIGxhY3VzLiBJbiBhYyB1cm5hIGFkaXBpc2NpbmcgcHVydXMgdmVuZW5hdGlzIHZvbHV0cGF0IHZlbCBldCBtZXR1cy4gTnVsbGFtIG5lYyBhdWN0b3IgcXVhbS4gUGhhc2VsbHVzIHBvcnR0aXRvciBmZWxpcyBhYyBuaWJoIGdyYXZpZGEgc3VzY2lwaXQgdGVtcHVzIGF0IGFudGUuIE51bmMgcGVsbGVudGVzcXVlIGlhY3VsaXMgc2FwaWVuIGEgbWF0dGlzLiBBZW5lYW4gZWxlaWZlbmQgZG9sb3Igbm9uIG51bmMgbGFvcmVldCwgbm9uIGRpY3R1bSBtYXNzYSBhbGlxdWFtLiBBZW5lYW4gcXVpcyB0dXJwaXMgYXVndWUuIFByYWVzZW50IGF1Z3VlIGxlY3R1cywgbW9sbGlzIG5lYyBlbGVtZW50dW0gZXUsIGRpZ25pc3NpbSBhdCB2ZWxpdC4gVXQgY29uZ3VlIG5lcXVlIGlkIHVsbGFtY29ycGVyIHBlbGxlbnRlc3F1ZS4gTWFlY2VuYXMgZXVpc21vZCBpbiBlbGl0IGV1IHZlaGljdWxhLiBOdWxsYW0gdHJpc3RpcXVlIGR1aSBudWxsYSwgbmVjIGNvbnZhbGxpcyBtZXR1cyBzdXNjaXBpdCBlZ2V0LiBDcmFzIHNlbXBlciBhdWd1ZSBuZWMgY3Vyc3VzIGJsYW5kaXQuIE51bGxhIHJob25jdXMgZXQgb2RpbyBxdWlzIGJsYW5kaXQuIFByYWVzZW50IGxvYm9ydGlzIGRpZ25pc3NpbSB2ZWxpdCB1dCBwdWx2aW5hci4gRHVpcyBpbnRlcmR1bSBxdWFtIGFkaXBpc2NpbmcgZG9sb3Igc2VtcGVyIHNlbXBlci4gTnVuYyBiaWJlbmR1bSBjb252YWxsaXMgZHVpLCBlZ2V0IG1vbGxpcyBtYWduYSBoZW5kcmVyaXQgZXQuIE1vcmJpIGZhY2lsaXNpcywgYXVndWUgZXUgZnJpbmdpbGxhIGNvbnZhbGxpcywgbWF1cmlzIGVzdCBjdXJzdXMgZG9sb3IsIGV1IHBvc3VlcmUgb2RpbyBudW5jIHF1aXMgb3JjaS4gVXQgZXUganVzdG8gc2VtLiBQaGFzZWxsdXMgdXQgZXJhdCByaG9uY3VzLCBmYXVjaWJ1cyBhcmN1IHZpdGFlLCB2dWxwdXRhdGUgZXJhdC4gQWxpcXVhbSBuZWMgbWFnbmEgdml2ZXJyYSwgaW50ZXJkdW0gZXN0IHZpdGFlLCByaG9uY3VzIHNhcGllbi4gRHVpcyB0aW5jaWR1bnQgdGVtcG9yIGlwc3VtIHV0IGRhcGlidXMuIE51bGxhbSBjb21tb2RvIHZhcml1cyBtZXR1cywgc2VkIHNvbGxpY2l0dWRpbiBlcm9zLiBFdGlhbSBuZWMgb2RpbyBldCBkdWkgdGVtcG9yIGJsYW5kaXQgcG9zdWVyZS4=" == base64, "medium base64 conversion should work")
}

@Test func testBase64LineLengthOptions() {
let expected46 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
let length46String = Data(repeating:0, count: 46).base64EncodedString(options: .lineLength64Characters)
#expect(length46String == expected46)
let length46Data = Data(repeating:0, count: 46).base64EncodedData(options: .lineLength64Characters)
#expect(length46Data.count == 64)
#expect(String(decoding: length46Data, as: Unicode.UTF8.self) == expected46)

let expected47 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
let length47String = Data(repeating:0, count: 47).base64EncodedString(options: .lineLength64Characters)
#expect(length47String == expected47)
let length47Data = Data(repeating:0, count: 47).base64EncodedData(options: .lineLength64Characters)
#expect(length47Data.count == 64)
#expect(String(decoding: length47Data, as: Unicode.UTF8.self) == expected47)

let expected48 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
let length48String = Data(repeating:0, count: 48).base64EncodedString(options: .lineLength64Characters)
#expect(length48String == expected48)
let length48Data = Data(repeating:0, count: 48).base64EncodedData(options: .lineLength64Characters)
#expect(length48Data.count == 64)
#expect(String(decoding: length48Data, as: Unicode.UTF8.self) == expected48)

let length49 = Data(repeating:0, count: 49).base64EncodedString(options: .lineLength64Characters)
#expect(length49 == #"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\#r\#nAA=="#)
#expect(Array(length49.utf8)[64] == 13)
#expect(Array(length49.utf8)[65] == 10)
}

// we have more encodeToStringTests than we have encodeToDataTests.
// lets fix this by ensuring data output matches string output.

@Test(
arguments: [
Data.Base64EncodingOptions.lineLength64Characters,
[.lineLength64Characters, .endLineWithCarriageReturn],
.lineLength76Characters,
[.lineLength64Characters, .endLineWithLineFeed],
[],
]
)
func testBase64DataOutputMatchesStingOutput(options: Data.Base64EncodingOptions) {
let iterations = 1_000

for count in 0..<iterations {
let data = Data(repeating: 0, count: count)
let stringBase64 = data.base64EncodedString(options: options)
let dataBase64 = data.base64EncodedData(options: options)

#expect(stringBase64 == String(decoding: dataBase64, as: Unicode.UTF8.self))
}
}

@Test func anyHashableContainingData() {
let values: [Data] = [
Data(base64Encoded: "AAAA")!,
Expand Down