Skip to content

Commit ea2a90e

Browse files
authored
Merge pull request apple#656 from tbkka/json-alternate-base64
Issue 653: Accept both RFC4648 Section 4 and Section 5 Base 64 encodings
2 parents 4a85863 + 38757b9 commit ea2a90e

File tree

3 files changed

+54
-18
lines changed

3 files changed

+54
-18
lines changed
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
Recommended.Proto3.JsonInput.BytesFieldBase64Url.JsonOutput
2-
Recommended.Proto3.JsonInput.BytesFieldBase64Url.ProtobufOutput
1+
# No known failures

Sources/SwiftProtobuf/JSONScanner.swift

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ private let asciiSingleQuote = UInt8(ascii: "\'")
3535
private let asciiBackslash = UInt8(ascii: "\\")
3636
private let asciiForwardSlash = UInt8(ascii: "/")
3737
private let asciiHash = UInt8(ascii: "#")
38+
private let asciiEqualSign = UInt8(ascii: "=")
3839
private let asciiUnderscore = UInt8(ascii: "_")
3940
private let asciiQuestionMark = UInt8(ascii: "?")
4041
private let asciiSpace = UInt8(ascii: " ")
@@ -79,14 +80,15 @@ private func fromHexDigit(_ c: UnicodeScalar) -> UInt32? {
7980
}
8081
}
8182

82-
// Decode the RFC 4648 section 4 Base 64 encoding.
83+
// Decode both the RFC 4648 section 4 Base 64 encoding and the
84+
// RFC 4648 section 5 Base 64 variant.
8385
let base64Values: [Int] = [
8486
/* 0x00 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
8587
/* 0x10 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
86-
/* 0x20 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
88+
/* 0x20 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63,
8789
/* 0x30 */ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
8890
/* 0x40 */ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
89-
/* 0x50 */ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
91+
/* 0x50 */ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
9092
/* 0x60 */ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
9193
/* 0x70 */ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
9294
/* 0x80 */ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
@@ -107,12 +109,9 @@ let base64Values: [Int] = [
107109
/// mixed in with the base-64 characters
108110
/// * Google's C++ implementation ignores missing '=' characters
109111
/// but if present, there must be the exact correct number of them.
112+
/// * The conformance test requires us to accept both standard RFC4648
113+
/// Base 64 encoding and the "URL and Filename Safe Alphabet" variant.
110114
///
111-
/// Note: Google's C++ code seems to allow many base-64 extensions
112-
/// (including websafe and '.' as padding), but the Java version just
113-
/// uses uses Guava's BaseEncoding.base64() (which only supports the
114-
/// RFC4648 standard encoding) and the conformance test explicitly
115-
/// requires us to reject '-' and '_' characters.
116115
private func parseBytes(
117116
source: UnsafeBufferPointer<UInt8>,
118117
index: inout UnsafeBufferPointer<UInt8>.Index,
@@ -125,12 +124,20 @@ private func parseBytes(
125124
source.formIndex(after: &index)
126125

127126
// Count the base-64 digits
127+
// Ignore unrecognized characters in this first pass,
128+
// stop at the closing double quote.
128129
let digitsStart = index
129130
var rawChars = 0
131+
var sawSection4Characters = false
132+
var sawSection5Characters = false
130133
while index != end {
131134
let digit = source[index]
132135
if digit == asciiDoubleQuote {
133136
break
137+
} else if digit == asciiPlus || digit == asciiForwardSlash {
138+
sawSection4Characters = true
139+
} else if digit == asciiMinus || digit == asciiUnderscore {
140+
sawSection5Characters = true
134141
}
135142
let k = base64Values[Int(digit)]
136143
if k >= 0 {
@@ -143,11 +150,19 @@ private func parseBytes(
143150
if index == end {
144151
throw JSONDecodingError.malformedString
145152
}
153+
// Reject mixed encodings.
154+
if sawSection4Characters && sawSection5Characters {
155+
throw JSONDecodingError.malformedString
156+
}
146157

147158
// Allocate a Data object of exactly the right size
148159
var value = Data(count: rawChars * 3 / 4)
149160

150-
// Scan the digits again and populate the Data object
161+
// Scan the digits again and populate the Data object.
162+
// In this pass, we check for (and fail) if there are
163+
// unexpected characters. But we don't check for end-of-input,
164+
// because the loop above already verified that there was
165+
// a closing double quote.
151166
index = digitsStart
152167
try value.withUnsafeMutableBytes {
153168
(dataPointer: UnsafeMutablePointer<UInt8>) in
@@ -177,7 +192,7 @@ private func parseBytes(
177192
break digits
178193
case asciiSpace:
179194
break
180-
case 61: // Count padding
195+
case asciiEqualSign: // Count padding
181196
while true {
182197
switch source[index] {
183198
case asciiDoubleQuote:
@@ -202,12 +217,12 @@ private func parseBytes(
202217
case 3:
203218
p[0] = UInt8(extendingOrTruncating: n >> 10)
204219
p[1] = UInt8(extendingOrTruncating: n >> 2)
205-
if padding == 1 {
220+
if padding == 1 || padding == 0 {
206221
return
207222
}
208223
case 2:
209224
p[0] = UInt8(extendingOrTruncating: n >> 4)
210-
if padding == 2 {
225+
if padding == 2 || padding == 0 {
211226
return
212227
}
213228
case 0:

Tests/SwiftProtobufTests/Test_JSON.swift

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -682,26 +682,48 @@ class Test_JSON: XCTestCase, PBTestHelpers {
682682
o.optionalBytes = Data(bytes: [65])
683683
}
684684
assertJSONDecodeFails("{\"optionalBytes\":\"QQ=\"}")
685-
assertJSONDecodeFails("{\"optionalBytes\":\"QQ\"}")
685+
assertJSONDecodeSucceeds("{\"optionalBytes\":\"QQ\"}") {
686+
$0.optionalBytes == Data(bytes: [65])
687+
}
686688
assertJSONEncode("{\"optionalBytes\":\"QUI=\"}") {(o: inout MessageTestType) in
687689
o.optionalBytes = Data(bytes: [65, 66])
688690
}
689-
assertJSONDecodeFails("{\"optionalBytes\":\"QUI\"}")
691+
assertJSONDecodeSucceeds("{\"optionalBytes\":\"QUI\"}") {
692+
$0.optionalBytes == Data(bytes: [65, 66])
693+
}
690694
assertJSONEncode("{\"optionalBytes\":\"QUJD\"}") {(o: inout MessageTestType) in
691695
o.optionalBytes = Data(bytes: [65, 66, 67])
692696
}
693697
assertJSONEncode("{\"optionalBytes\":\"QUJDRA==\"}") {(o: inout MessageTestType) in
694698
o.optionalBytes = Data(bytes: [65, 66, 67, 68])
695699
}
700+
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDRA===\"}")
696701
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDRA=\"}")
697-
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDRA\"}")
702+
assertJSONDecodeSucceeds("{\"optionalBytes\":\"QUJDRA\"}") {
703+
$0.optionalBytes == Data(bytes: [65, 66, 67, 68])
704+
}
698705
assertJSONEncode("{\"optionalBytes\":\"QUJDREU=\"}") {(o: inout MessageTestType) in
699706
o.optionalBytes = Data(bytes: [65, 66, 67, 68, 69])
700707
}
701-
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDREU\"}")
708+
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDREU==\"}")
709+
assertJSONDecodeSucceeds("{\"optionalBytes\":\"QUJDREU\"}") {
710+
$0.optionalBytes == Data(bytes: [65, 66, 67, 68, 69])
711+
}
702712
assertJSONEncode("{\"optionalBytes\":\"QUJDREVG\"}") {(o: inout MessageTestType) in
703713
o.optionalBytes = Data(bytes: [65, 66, 67, 68, 69, 70])
704714
}
715+
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDREVG=\"}")
716+
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDREVG==\"}")
717+
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDREVG===\"}")
718+
assertJSONDecodeFails("{\"optionalBytes\":\"QUJDREVG====\"}")
719+
// Accept both RFC4648 Section 4 and Section 5 base64 variants, but reject mixed coding:
720+
assertJSONDecodeSucceeds("{\"optionalBytes\":\"-_-_\"}") {
721+
$0.optionalBytes == Data(bytes: [251, 255, 191])
722+
}
723+
assertJSONDecodeSucceeds("{\"optionalBytes\":\"+/+/\"}") {
724+
$0.optionalBytes == Data(bytes: [251, 255, 191])
725+
}
726+
assertJSONDecodeFails("{\"optionalBytes\":\"-_+/\"}")
705727
}
706728

707729
func testOptionalBytes2() {

0 commit comments

Comments
 (0)