Skip to content

Commit 2b34d0c

Browse files
committed
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
1 parent 4b0ee0a commit 2b34d0c

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2276,6 +2276,62 @@ 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+
@Test func testBase64DataOutputMatchesStingOutput() {
2310+
for count in 0..<10_000 {
2311+
let data = Data(repeating: 0, count: count)
2312+
let stringBase64 = data.base64EncodedString(options: .lineLength64Characters)
2313+
let dataBase64 = data.base64EncodedData(options: .lineLength64Characters)
2314+
2315+
#expect(stringBase64 == String(decoding: dataBase64, as: Unicode.UTF8.self))
2316+
}
2317+
2318+
for count in 0..<10_000 {
2319+
let data = Data(repeating: 0, count: count)
2320+
let stringBase64 = data.base64EncodedString(options: .lineLength76Characters)
2321+
let dataBase64 = data.base64EncodedData(options: .lineLength76Characters)
2322+
2323+
#expect(stringBase64 == String(decoding: dataBase64, as: Unicode.UTF8.self))
2324+
}
2325+
2326+
for count in 0..<10_000 {
2327+
let data = Data(repeating: 0, count: count)
2328+
let stringBase64 = data.base64EncodedString()
2329+
let dataBase64 = data.base64EncodedData()
2330+
2331+
#expect(stringBase64 == String(decoding: dataBase64, as: Unicode.UTF8.self))
2332+
}
2333+
}
2334+
22792335
@Test func anyHashableContainingData() {
22802336
let values: [Data] = [
22812337
Data(base64Encoded: "AAAA")!,

0 commit comments

Comments
 (0)