Skip to content

Commit 2d2e249

Browse files
authored
Fix Data Base64 length encoding bug (#1425)
* Fix Data Base64 length encoding bug ### Motivation We have an issue in encodeComputeCapacity. If we add lineBreaks, we assumed to add line breaks for lines that ended at the max capacity. ### Changes - encodeComputeCapacity checks if the last line uses the full length and removes unnecessary seperatorBytes if needed rdar://155204772 * Made the test range smaller
1 parent d1b4798 commit 2d2e249

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

Sources/FoundationEssentials/Data/Data+Base64.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,12 @@ extension Base64 {
395395

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

Tests/FoundationEssentialsTests/DataTests.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2276,6 +2276,58 @@ extension DataTests {
22762276
#expect("TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gVXQgYXQgdGluY2lkdW50IGFyY3UuIFN1c3BlbmRpc3NlIG5lYyBzb2RhbGVzIGVyYXQsIHNpdCBhbWV0IGltcGVyZGlldCBpcHN1bS4gRXRpYW0gc2VkIG9ybmFyZSBmZWxpcy4gTnVuYyBtYXVyaXMgdHVycGlzLCBiaWJlbmR1bSBub24gbGVjdHVzIHF1aXMsIG1hbGVzdWFkYSBwbGFjZXJhdCB0dXJwaXMuIE5hbSBhZGlwaXNjaW5nIG5vbiBtYXNzYSBldCBzZW1wZXIuIE51bGxhIGNvbnZhbGxpcyBzZW1wZXIgYmliZW5kdW0uIEFsaXF1YW0gZGljdHVtIG51bGxhIGN1cnN1cyBtaSB1bHRyaWNpZXMsIGF0IHRpbmNpZHVudCBtaSBzYWdpdHRpcy4gTnVsbGEgZmF1Y2lidXMgYXQgZHVpIHF1aXMgc29kYWxlcy4gTW9yYmkgcnV0cnVtLCBkdWkgaWQgdWx0cmljZXMgdmVuZW5hdGlzLCBhcmN1IHVybmEgZWdlc3RhcyBmZWxpcywgdmVsIHN1c2NpcGl0IG1hdXJpcyBhcmN1IHF1aXMgcmlzdXMuIE51bmMgdmVuZW5hdGlzIGxpZ3VsYSBhdCBvcmNpIHRyaXN0aXF1ZSwgZXQgbWF0dGlzIHB1cnVzIHB1bHZpbmFyLiBFdGlhbSB1bHRyaWNpZXMgZXN0IG9kaW8uIE51bmMgZWxlaWZlbmQgbWFsZXN1YWRhIGp1c3RvLCBuZWMgZXVpc21vZCBzZW0gdWx0cmljZXMgcXVpcy4gRXRpYW0gbmVjIG5pYmggc2l0IGFtZXQgbG9yZW0gZmF1Y2lidXMgZGFwaWJ1cyBxdWlzIG5lYyBsZW8uIFByYWVzZW50IHNpdCBhbWV0IG1hdXJpcyB2ZWwgbGFjdXMgaGVuZHJlcml0IHBvcnRhIG1vbGxpcyBjb25zZWN0ZXR1ciBtaS4gRG9uZWMgZWdldCB0b3J0b3IgZHVpLiBNb3JiaSBpbXBlcmRpZXQsIGFyY3Ugc2l0IGFtZXQgZWxlbWVudHVtIGludGVyZHVtLCBxdWFtIG5pc2wgdGVtcG9yIHF1YW0sIHZpdGFlIGZldWdpYXQgYXVndWUgcHVydXMgc2VkIGxhY3VzLiBJbiBhYyB1cm5hIGFkaXBpc2NpbmcgcHVydXMgdmVuZW5hdGlzIHZvbHV0cGF0IHZlbCBldCBtZXR1cy4gTnVsbGFtIG5lYyBhdWN0b3IgcXVhbS4gUGhhc2VsbHVzIHBvcnR0aXRvciBmZWxpcyBhYyBuaWJoIGdyYXZpZGEgc3VzY2lwaXQgdGVtcHVzIGF0IGFudGUuIE51bmMgcGVsbGVudGVzcXVlIGlhY3VsaXMgc2FwaWVuIGEgbWF0dGlzLiBBZW5lYW4gZWxlaWZlbmQgZG9sb3Igbm9uIG51bmMgbGFvcmVldCwgbm9uIGRpY3R1bSBtYXNzYSBhbGlxdWFtLiBBZW5lYW4gcXVpcyB0dXJwaXMgYXVndWUuIFByYWVzZW50IGF1Z3VlIGxlY3R1cywgbW9sbGlzIG5lYyBlbGVtZW50dW0gZXUsIGRpZ25pc3NpbSBhdCB2ZWxpdC4gVXQgY29uZ3VlIG5lcXVlIGlkIHVsbGFtY29ycGVyIHBlbGxlbnRlc3F1ZS4gTWFlY2VuYXMgZXVpc21vZCBpbiBlbGl0IGV1IHZlaGljdWxhLiBOdWxsYW0gdHJpc3RpcXVlIGR1aSBudWxsYSwgbmVjIGNvbnZhbGxpcyBtZXR1cyBzdXNjaXBpdCBlZ2V0LiBDcmFzIHNlbXBlciBhdWd1ZSBuZWMgY3Vyc3VzIGJsYW5kaXQuIE51bGxhIHJob25jdXMgZXQgb2RpbyBxdWlzIGJsYW5kaXQuIFByYWVzZW50IGxvYm9ydGlzIGRpZ25pc3NpbSB2ZWxpdCB1dCBwdWx2aW5hci4gRHVpcyBpbnRlcmR1bSBxdWFtIGFkaXBpc2NpbmcgZG9sb3Igc2VtcGVyIHNlbXBlci4gTnVuYyBiaWJlbmR1bSBjb252YWxsaXMgZHVpLCBlZ2V0IG1vbGxpcyBtYWduYSBoZW5kcmVyaXQgZXQuIE1vcmJpIGZhY2lsaXNpcywgYXVndWUgZXUgZnJpbmdpbGxhIGNvbnZhbGxpcywgbWF1cmlzIGVzdCBjdXJzdXMgZG9sb3IsIGV1IHBvc3VlcmUgb2RpbyBudW5jIHF1aXMgb3JjaS4gVXQgZXUganVzdG8gc2VtLiBQaGFzZWxsdXMgdXQgZXJhdCByaG9uY3VzLCBmYXVjaWJ1cyBhcmN1IHZpdGFlLCB2dWxwdXRhdGUgZXJhdC4gQWxpcXVhbSBuZWMgbWFnbmEgdml2ZXJyYSwgaW50ZXJkdW0gZXN0IHZpdGFlLCByaG9uY3VzIHNhcGllbi4gRHVpcyB0aW5jaWR1bnQgdGVtcG9yIGlwc3VtIHV0IGRhcGlidXMuIE51bGxhbSBjb21tb2RvIHZhcml1cyBtZXR1cywgc2VkIHNvbGxpY2l0dWRpbiBlcm9zLiBFdGlhbSBuZWMgb2RpbyBldCBkdWkgdGVtcG9yIGJsYW5kaXQgcG9zdWVyZS4=" == base64, "medium base64 conversion should work")
22772277
}
22782278

2279+
@Test func testBase64LineLengthOptions() {
2280+
let expected46 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
2281+
let length46String = Data(repeating:0, count: 46).base64EncodedString(options: .lineLength64Characters)
2282+
#expect(length46String == expected46)
2283+
let length46Data = Data(repeating:0, count: 46).base64EncodedData(options: .lineLength64Characters)
2284+
#expect(length46Data.count == 64)
2285+
#expect(String(decoding: length46Data, as: Unicode.UTF8.self) == expected46)
2286+
2287+
let expected47 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
2288+
let length47String = Data(repeating:0, count: 47).base64EncodedString(options: .lineLength64Characters)
2289+
#expect(length47String == expected47)
2290+
let length47Data = Data(repeating:0, count: 47).base64EncodedData(options: .lineLength64Characters)
2291+
#expect(length47Data.count == 64)
2292+
#expect(String(decoding: length47Data, as: Unicode.UTF8.self) == expected47)
2293+
2294+
let expected48 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
2295+
let length48String = Data(repeating:0, count: 48).base64EncodedString(options: .lineLength64Characters)
2296+
#expect(length48String == expected48)
2297+
let length48Data = Data(repeating:0, count: 48).base64EncodedData(options: .lineLength64Characters)
2298+
#expect(length48Data.count == 64)
2299+
#expect(String(decoding: length48Data, as: Unicode.UTF8.self) == expected48)
2300+
2301+
let length49 = Data(repeating:0, count: 49).base64EncodedString(options: .lineLength64Characters)
2302+
#expect(length49 == #"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\#r\#nAA=="#)
2303+
#expect(Array(length49.utf8)[64] == 13)
2304+
#expect(Array(length49.utf8)[65] == 10)
2305+
}
2306+
2307+
// we have more encodeToStringTests than we have encodeToDataTests.
2308+
// lets fix this by ensuring data output matches string output.
2309+
2310+
@Test(
2311+
arguments: [
2312+
Data.Base64EncodingOptions.lineLength64Characters,
2313+
[.lineLength64Characters, .endLineWithCarriageReturn],
2314+
.lineLength76Characters,
2315+
[.lineLength64Characters, .endLineWithLineFeed],
2316+
[],
2317+
]
2318+
)
2319+
func testBase64DataOutputMatchesStingOutput(options: Data.Base64EncodingOptions) {
2320+
let iterations = 1_000
2321+
2322+
for count in 0..<iterations {
2323+
let data = Data(repeating: 0, count: count)
2324+
let stringBase64 = data.base64EncodedString(options: options)
2325+
let dataBase64 = data.base64EncodedData(options: options)
2326+
2327+
#expect(stringBase64 == String(decoding: dataBase64, as: Unicode.UTF8.self))
2328+
}
2329+
}
2330+
22792331
@Test func anyHashableContainingData() {
22802332
let values: [Data] = [
22812333
Data(base64Encoded: "AAAA")!,

0 commit comments

Comments
 (0)