Skip to content

Commit ea91113

Browse files
committed
Merge branch 'pr/120'
2 parents 4d611b8 + 8a89278 commit ea91113

File tree

2 files changed

+347
-4
lines changed

2 files changed

+347
-4
lines changed

Foundation/NSData.swift

Lines changed: 204 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -556,25 +556,225 @@ extension NSData {
556556
/* Create an NSData from a Base-64 encoded NSString using the given options. By default, returns nil when the input is not recognized as valid Base-64.
557557
*/
558558
public convenience init?(base64EncodedString base64String: String, options: NSDataBase64DecodingOptions) {
559-
NSUnimplemented()
559+
let encodedBytes = Array(base64String.utf8)
560+
guard let decodedBytes = NSData.base64DecodeBytes(encodedBytes, options: options) else {
561+
return nil
562+
}
563+
self.init(bytes: decodedBytes, length: decodedBytes.count)
560564
}
561565

562566
/* Create a Base-64 encoded NSString from the receiver's contents using the given options.
563567
*/
564568
public func base64EncodedStringWithOptions(options: NSDataBase64EncodingOptions) -> String {
565-
NSUnimplemented()
569+
var decodedBytes = [UInt8](count: self.length, repeatedValue: 0)
570+
getBytes(&decodedBytes, length: decodedBytes.count)
571+
let encodedBytes = NSData.base64EncodeBytes(decodedBytes, options: options)
572+
let characters = encodedBytes.map { Character(UnicodeScalar($0)) }
573+
return String(characters)
566574
}
567575

568576
/* Create an NSData from a Base-64, UTF-8 encoded NSData. By default, returns nil when the input is not recognized as valid Base-64.
569577
*/
570578
public convenience init?(base64EncodedData base64Data: NSData, options: NSDataBase64DecodingOptions) {
571-
NSUnimplemented()
579+
var encodedBytes = [UInt8](count: base64Data.length, repeatedValue: 0)
580+
base64Data.getBytes(&encodedBytes, length: encodedBytes.count)
581+
guard let decodedBytes = NSData.base64DecodeBytes(encodedBytes, options: options) else {
582+
return nil
583+
}
584+
self.init(bytes: decodedBytes, length: decodedBytes.count)
572585
}
573586

574587
/* Create a Base-64, UTF-8 encoded NSData from the receiver's contents using the given options.
575588
*/
576589
public func base64EncodedDataWithOptions(options: NSDataBase64EncodingOptions) -> NSData {
577-
NSUnimplemented()
590+
var decodedBytes = [UInt8](count: self.length, repeatedValue: 0)
591+
getBytes(&decodedBytes, length: decodedBytes.count)
592+
let encodedBytes = NSData.base64EncodeBytes(decodedBytes, options: options)
593+
return NSData(bytes: encodedBytes, length: encodedBytes.count)
594+
}
595+
596+
/**
597+
The ranges of ASCII characters that are used to encode data in Base64.
598+
*/
599+
private static var base64ByteMappings: [Range<UInt8>] {
600+
return [
601+
65 ..< 91, // A-Z
602+
97 ..< 123, // a-z
603+
48 ..< 58, // 0-9
604+
43 ..< 44, // +
605+
47 ..< 48, // /
606+
61 ..< 62 // =
607+
]
608+
}
609+
610+
/**
611+
This method takes a byte with a character from Base64-encoded string
612+
and gets the binary value that the character corresponds to.
613+
614+
If the byte is not a valid character in Base64, this will return nil.
615+
616+
- parameter byte: The byte with the Base64 character.
617+
- returns: The numeric value that the character corresponds
618+
to.
619+
*/
620+
private static func base64DecodeByte(byte: UInt8) -> UInt8? {
621+
var decodedStart: UInt8 = 0
622+
for range in base64ByteMappings {
623+
if range.contains(byte) {
624+
let result = decodedStart + (byte - range.startIndex)
625+
return result == 64 ? 0 : result
626+
}
627+
decodedStart += range.endIndex - range.startIndex
628+
}
629+
return nil
630+
}
631+
632+
/**
633+
This method takes six bits of binary data and encodes it as a character
634+
in Base64.
635+
636+
The value in the byte must be less than 64, because a Base64 character
637+
can only represent 6 bits.
638+
639+
- parameter byte: The byte to encode
640+
- returns: The ASCII value for the encoded character.
641+
*/
642+
private static func base64EncodeByte(byte: UInt8) -> UInt8 {
643+
assert(byte < 64)
644+
var decodedStart: UInt8 = 0
645+
for range in base64ByteMappings {
646+
let decodedRange = decodedStart ..< decodedStart + (range.endIndex - range.startIndex)
647+
if decodedRange.contains(byte) {
648+
return range.startIndex + (byte - decodedStart)
649+
}
650+
decodedStart += range.endIndex - range.startIndex
651+
}
652+
return 0
653+
}
654+
655+
/**
656+
This method takes an array of bytes and either adds or removes padding
657+
as part of Base64 encoding and decoding.
658+
659+
If the fromSize is larger than the toSize, this will inflate the bytes
660+
by adding zero between the bits. If the fromSize is smaller than the
661+
toSize, this will deflate the bytes by removing the most significant
662+
bits and recompacting the bytes.
663+
664+
For instance, if you were going from 6 bits to 8 bits, and you had
665+
an array of bytes with `[0b00010000, 0b00010101, 0b00001001 0b00001101]`,
666+
this would give a result of: `[0b01000001 0b01010010 0b01001101]`.
667+
This transition is done when decoding Base64 data.
668+
669+
If you were going from 8 bits to 6 bits, and you had an array of bytes
670+
with `[0b01000011 0b01101111 0b01101110], this would give a result of:
671+
`[0b00010000 0b00110110 0b00111101 0b00101110]. This transition is done
672+
when encoding data in Base64.
673+
674+
- parameter bytes: The original bytes
675+
- parameter fromSize: The number of useful bits in each byte of the
676+
input.
677+
- parameter toSize: The number of useful bits in each byte of the
678+
output.
679+
- returns: The resized bytes
680+
*/
681+
private static func base64ResizeBytes(bytes: [UInt8], fromSize: UInt32, toSize: UInt32) -> [UInt8] {
682+
var bitBuffer: UInt32 = 0
683+
var bitCount: UInt32 = 0
684+
685+
var result = [UInt8]()
686+
687+
result.reserveCapacity(bytes.count * Int(fromSize) / Int(toSize))
688+
689+
let mask = UInt32(1 << toSize - 1)
690+
691+
for byte in bytes {
692+
bitBuffer = bitBuffer << fromSize | UInt32(byte)
693+
bitCount += fromSize
694+
if bitCount % toSize == 0 {
695+
while(bitCount > 0) {
696+
let byte = UInt8(mask & (bitBuffer >> (bitCount - toSize)))
697+
result.append(byte)
698+
bitCount -= toSize
699+
}
700+
}
701+
}
702+
703+
let paddingBits = toSize - (bitCount % toSize)
704+
if paddingBits != toSize {
705+
bitBuffer = bitBuffer << paddingBits
706+
bitCount += paddingBits
707+
}
708+
709+
while(bitCount > 0) {
710+
let byte = UInt8(mask & (bitBuffer >> (bitCount - toSize)))
711+
result.append(byte)
712+
bitCount -= toSize
713+
}
714+
715+
return result
716+
717+
}
718+
719+
/**
720+
This method decodes Base64-encoded data.
721+
722+
If the input contains any bytes that are not valid Base64 characters,
723+
this will return nil.
724+
725+
- parameter bytes: The Base64 bytes
726+
- parameter options: Options for handling invalid input
727+
- returns: The decoded bytes.
728+
*/
729+
private static func base64DecodeBytes(bytes: [UInt8], options: NSDataBase64DecodingOptions = []) -> [UInt8]? {
730+
var decodedBytes = [UInt8]()
731+
decodedBytes.reserveCapacity(bytes.count)
732+
for byte in bytes {
733+
guard let decoded = base64DecodeByte(byte) else {
734+
if options.contains(.IgnoreUnknownCharacters) {
735+
continue
736+
}
737+
else {
738+
return nil
739+
}
740+
}
741+
decodedBytes.append(decoded)
742+
}
743+
return base64ResizeBytes(decodedBytes, fromSize: 6, toSize: 8)
744+
}
745+
746+
747+
/**
748+
This method encodes data in Base64.
749+
750+
- parameter bytes: The bytes you want to encode
751+
- parameter options: Options for formatting the result
752+
- returns: The Base64-encoding for those bytes.
753+
*/
754+
private static func base64EncodeBytes(bytes: [UInt8], options: NSDataBase64EncodingOptions = []) -> [UInt8] {
755+
var encodedBytes = base64ResizeBytes(bytes, fromSize: 8, toSize: 6)
756+
encodedBytes = encodedBytes.map(base64EncodeByte)
757+
758+
let paddingBytes = (4 - (encodedBytes.count % 4)) % 4
759+
for _ in 0..<paddingBytes {
760+
encodedBytes.append(61)
761+
}
762+
let lineLength: Int
763+
if options.contains(.Encoding64CharacterLineLength) { lineLength = 64 }
764+
else if options.contains(.Encoding76CharacterLineLength) { lineLength = 76 }
765+
else { lineLength = 0 }
766+
if lineLength > 0 {
767+
var separator = [UInt8]()
768+
if options.contains(.EncodingEndLineWithCarriageReturn) { separator.append(13) }
769+
if options.contains(.EncodingEndLineWithLineFeed) { separator.append(10) }
770+
let lines = encodedBytes.count / lineLength
771+
for line in 0..<lines {
772+
for (index,character) in separator.enumerate() {
773+
encodedBytes.insert(character, atIndex: (lineLength + separator.count) * line + index + lineLength)
774+
}
775+
}
776+
}
777+
return encodedBytes
578778
}
579779
}
580780

TestFoundation/TestNSData.swift

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ class TestNSData: XCTestCase {
2828
("test_edgeDebugDescription", test_edgeDebugDescription),
2929
("test_writeToURLOptions", test_writeToURLOptions),
3030
("test_edgeNoCopyDescription", test_edgeNoCopyDescription),
31+
("test_initializeWithBase64EncodedDataGetsDecodedData", test_initializeWithBase64EncodedDataGetsDecodedData),
32+
("test_initializeWithBase64EncodedDataWithNonBase64CharacterIsNil", test_initializeWithBase64EncodedDataWithNonBase64CharacterIsNil),
33+
("test_initializeWithBase64EncodedDataWithNonBase64CharacterWithOptionToAllowItSkipsCharacter", test_initializeWithBase64EncodedDataWithNonBase64CharacterWithOptionToAllowItSkipsCharacter),
34+
("test_base64EncodedDataGetsEncodedText", test_base64EncodedDataGetsEncodedText),
35+
("test_base64EncodedDataWithOptionToInsertCarriageReturnContainsCarriageReturn", test_base64EncodedDataWithOptionToInsertCarriageReturnContainsCarriageReturn),
36+
("test_base64EncodedDataWithOptionToInsertLineFeedsContainsLineFeed", test_base64EncodedDataWithOptionToInsertLineFeedsContainsLineFeed),
37+
("test_base64EncodedDataWithOptionToInsertCarriageReturnAndLineFeedContainsBoth", test_base64EncodedDataWithOptionToInsertCarriageReturnAndLineFeedContainsBoth),
38+
("test_base64EncodedStringGetsEncodedText", test_base64EncodedStringGetsEncodedText),
39+
("test_initializeWithBase64EncodedStringGetsDecodedData", test_initializeWithBase64EncodedStringGetsDecodedData),
3140
]
3241
}
3342

@@ -109,4 +118,138 @@ class TestNSData: XCTestCase {
109118
XCTAssertEqual(data.debugDescription, expected)
110119
XCTAssertEqual(data.bytes, bytes)
111120
}
121+
122+
func test_initializeWithBase64EncodedDataGetsDecodedData() {
123+
let plainText = "ARMA virumque cano, Troiae qui primus ab oris\nItaliam, fato profugus, Laviniaque venit"
124+
let encodedText = "QVJNQSB2aXJ1bXF1ZSBjYW5vLCBUcm9pYWUgcXVpIHByaW11cyBhYiBvcmlzCkl0YWxpYW0sIGZhdG8gcHJvZnVndXMsIExhdmluaWFxdWUgdmVuaXQ="
125+
guard let encodedData = encodedText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
126+
XCTFail("Could not get UTF-8 data")
127+
return
128+
}
129+
guard let decodedData = NSData(base64EncodedData: encodedData, options: []) else {
130+
XCTFail("Could not Base-64 decode data")
131+
return
132+
}
133+
guard let decodedText = NSString(data: decodedData, encoding: NSUTF8StringEncoding)?.bridge() else {
134+
XCTFail("Could not convert decoded data to a UTF-8 String")
135+
return
136+
}
137+
138+
XCTAssertEqual(decodedText, plainText)
139+
}
140+
141+
func test_initializeWithBase64EncodedDataWithNonBase64CharacterIsNil() {
142+
let encodedText = "QVJNQSB2aXJ1bXF1ZSBjYW5vLCBUcm9pYWUgcXVpIHBya$W11cyBhYiBvcmlzCkl0YWxpYW0sIGZhdG8gcHJvZnVndXMsIExhdmluaWFxdWUgdmVuaXQ="
143+
guard let encodedData = encodedText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
144+
XCTFail("Could not get UTF-8 data")
145+
return
146+
}
147+
let decodedData = NSData(base64EncodedData: encodedData, options: [])
148+
XCTAssertNil(decodedData)
149+
}
150+
151+
func test_initializeWithBase64EncodedDataWithNonBase64CharacterWithOptionToAllowItSkipsCharacter() {
152+
let plainText = "ARMA virumque cano, Troiae qui primus ab oris\nItaliam, fato profugus, Laviniaque venit"
153+
let encodedText = "QVJNQSB2aXJ1bXF1ZSBjYW5vLCBUcm9pYWUgcXVpIHBya$W11cyBhYiBvcmlzCkl0YWxpYW0sIGZhdG8gcHJvZnVndXMsIExhdmluaWFxdWUgdmVuaXQ="
154+
guard let encodedData = encodedText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
155+
XCTFail("Could not get UTF-8 data")
156+
return
157+
}
158+
guard let decodedData = NSData(base64EncodedData: encodedData, options: [.IgnoreUnknownCharacters]) else {
159+
XCTFail("Could not Base-64 decode data")
160+
return
161+
}
162+
guard let decodedText = NSString(data: decodedData, encoding: NSUTF8StringEncoding)?.bridge() else {
163+
XCTFail("Could not convert decoded data to a UTF-8 String")
164+
return
165+
}
166+
167+
XCTAssertEqual(decodedText, plainText)
168+
}
169+
170+
func test_initializeWithBase64EncodedStringGetsDecodedData() {
171+
let plainText = "ARMA virumque cano, Troiae qui primus ab oris\nItaliam, fato profugus, Laviniaque venit"
172+
let encodedText = "QVJNQSB2aXJ1bXF1ZSBjYW5vLCBUcm9pYWUgcXVpIHByaW11cyBhYiBvcmlzCkl0YWxpYW0sIGZhdG8gcHJvZnVndXMsIExhdmluaWFxdWUgdmVuaXQ="
173+
guard let decodedData = NSData(base64EncodedString: encodedText, options: []) else {
174+
XCTFail("Could not Base-64 decode data")
175+
return
176+
}
177+
guard let decodedText = NSString(data: decodedData, encoding: NSUTF8StringEncoding)?.bridge() else {
178+
XCTFail("Could not convert decoded data to a UTF-8 String")
179+
return
180+
}
181+
182+
XCTAssertEqual(decodedText, plainText)
183+
}
184+
185+
func test_base64EncodedDataGetsEncodedText() {
186+
let plainText = "Constitit, et lacrimans, `Quis iam locus’ inquit `Achate,\nquae regio in terris nostri non plena laboris?`"
187+
let encodedText = "Q29uc3RpdGl0LCBldCBsYWNyaW1hbnMsIGBRdWlzIGlhbSBsb2N1c+KAmSBpbnF1aXQgYEFjaGF0ZSwKcXVhZSByZWdpbyBpbiB0ZXJyaXMgbm9zdHJpIG5vbiBwbGVuYSBsYWJvcmlzP2A="
188+
guard let data = plainText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
189+
XCTFail("Could not encode UTF-8 string")
190+
return
191+
}
192+
let encodedData = data.base64EncodedDataWithOptions([])
193+
guard let encodedTextResult = NSString(data: encodedData, encoding: NSASCIIStringEncoding)?.bridge() else {
194+
XCTFail("Could not convert encoded data to an ASCII String")
195+
return
196+
}
197+
XCTAssertEqual(encodedTextResult, encodedText)
198+
}
199+
200+
func test_base64EncodedDataWithOptionToInsertLineFeedsContainsLineFeed() {
201+
let plainText = "Constitit, et lacrimans, `Quis iam locus’ inquit `Achate,\nquae regio in terris nostri non plena laboris?`"
202+
let encodedText = "Q29uc3RpdGl0LCBldCBsYWNyaW1hbnMsIGBRdWlzIGlhbSBsb2N1c+KAmSBpbnF1\naXQgYEFjaGF0ZSwKcXVhZSByZWdpbyBpbiB0ZXJyaXMgbm9zdHJpIG5vbiBwbGVu\nYSBsYWJvcmlzP2A="
203+
guard let data = plainText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
204+
XCTFail("Could not encode UTF-8 string")
205+
return
206+
}
207+
let encodedData = data.base64EncodedDataWithOptions([.Encoding64CharacterLineLength, .EncodingEndLineWithLineFeed])
208+
guard let encodedTextResult = NSString(data: encodedData, encoding: NSASCIIStringEncoding)?.bridge() else {
209+
XCTFail("Could not convert encoded data to an ASCII String")
210+
return
211+
}
212+
XCTAssertEqual(encodedTextResult, encodedText)
213+
}
214+
215+
func test_base64EncodedDataWithOptionToInsertCarriageReturnContainsCarriageReturn() {
216+
let plainText = "Constitit, et lacrimans, `Quis iam locus’ inquit `Achate,\nquae regio in terris nostri non plena laboris?`"
217+
let encodedText = "Q29uc3RpdGl0LCBldCBsYWNyaW1hbnMsIGBRdWlzIGlhbSBsb2N1c+KAmSBpbnF1aXQgYEFjaGF0\rZSwKcXVhZSByZWdpbyBpbiB0ZXJyaXMgbm9zdHJpIG5vbiBwbGVuYSBsYWJvcmlzP2A="
218+
guard let data = plainText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
219+
XCTFail("Could not encode UTF-8 string")
220+
return
221+
}
222+
let encodedData = data.base64EncodedDataWithOptions([.Encoding76CharacterLineLength, .EncodingEndLineWithCarriageReturn])
223+
guard let encodedTextResult = NSString(data: encodedData, encoding: NSASCIIStringEncoding)?.bridge() else {
224+
XCTFail("Could not convert encoded data to an ASCII String")
225+
return
226+
}
227+
XCTAssertEqual(encodedTextResult, encodedText)
228+
}
229+
230+
func test_base64EncodedDataWithOptionToInsertCarriageReturnAndLineFeedContainsBoth() {
231+
let plainText = "Revocate animos, maestumque timorem mittite: forsan et haec olim meminisse iuvabit."
232+
let encodedText = "UmV2b2NhdGUgYW5pbW9zLCBtYWVzdHVtcXVlIHRpbW9yZW0gbWl0dGl0ZTogZm9yc2FuIGV0IGhh\r\nZWMgb2xpbSBtZW1pbmlzc2UgaXV2YWJpdC4="
233+
guard let data = plainText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
234+
XCTFail("Could not encode UTF-8 string")
235+
return
236+
}
237+
let encodedData = data.base64EncodedDataWithOptions([.Encoding76CharacterLineLength, .EncodingEndLineWithCarriageReturn, .EncodingEndLineWithLineFeed])
238+
guard let encodedTextResult = NSString(data: encodedData, encoding: NSASCIIStringEncoding)?.bridge() else {
239+
XCTFail("Could not convert encoded data to an ASCII String")
240+
return
241+
}
242+
XCTAssertEqual(encodedTextResult, encodedText)
243+
}
244+
245+
func test_base64EncodedStringGetsEncodedText() {
246+
let plainText = "Revocate animos, maestumque timorem mittite: forsan et haec olim meminisse iuvabit."
247+
let encodedText = "UmV2b2NhdGUgYW5pbW9zLCBtYWVzdHVtcXVlIHRpbW9yZW0gbWl0dGl0ZTogZm9yc2FuIGV0IGhhZWMgb2xpbSBtZW1pbmlzc2UgaXV2YWJpdC4="
248+
guard let data = plainText.bridge().dataUsingEncoding(NSUTF8StringEncoding) else {
249+
XCTFail("Could not encode UTF-8 string")
250+
return
251+
}
252+
let encodedTextResult = data.base64EncodedStringWithOptions([])
253+
XCTAssertEqual(encodedTextResult, encodedText)
254+
}
112255
}

0 commit comments

Comments
 (0)