mirror of
https://github.com/dart-lang/sdk
synced 2024-10-02 23:49:17 +00:00
Add "url-safe" encoding to base64 in dart:convert.
Fixes issue #24813. Doesn't add a second codec. The codec is unimportant, only the BASE64 and BASE64URL constants need to be public anyway. BUG= http://dartbug.com/24813 R=floitsch@google.com, nweiz@google.com Review URL: https://codereview.chromium.org/1858113003.
This commit is contained in:
parent
63efb288a7
commit
3f0ad9d4f2
|
@ -2,6 +2,8 @@
|
|||
|
||||
### Core library changes
|
||||
|
||||
* `dart:convet`
|
||||
* Added `BASE64URL` codec and corresponding `Base64Codec.urlSafe` constructor.
|
||||
* `dart:io`
|
||||
* Added `SecurityContext.alpnSupported`, which is true if a platform
|
||||
supports ALPN, and false otherwise.
|
||||
|
|
|
@ -5,13 +5,11 @@
|
|||
part of dart.convert;
|
||||
|
||||
/**
|
||||
* An instance of [Base64Codec].
|
||||
* A [base64](https://tools.ietf.org/html/rfc4648) encoder and decoder.
|
||||
*
|
||||
* This instance provides a convenient access to
|
||||
* [base64](https://tools.ietf.org/html/rfc4648) encoding and decoding.
|
||||
*
|
||||
* It encodes and decodes using the default base64 alphabet, does not allow
|
||||
* any invalid characters when decoding, and requires padding.
|
||||
* It encodes using the default base64 alphabet,
|
||||
* decodes using both the base64 and base64url alphabets,
|
||||
* does not allow invalid characters and requires padding.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
|
@ -21,6 +19,21 @@ part of dart.convert;
|
|||
*/
|
||||
const Base64Codec BASE64 = const Base64Codec();
|
||||
|
||||
/**
|
||||
* A [base64url](https://tools.ietf.org/html/rfc4648) encoder and decoder.
|
||||
*
|
||||
* It encodes and decodes using the base64url alphabet,
|
||||
* decodes using both the base64 and base64url alphabets,
|
||||
* does not allow invalid characters and requires padding.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* var encoded = BASE64.encode([0x62, 0x6c, 0xc3, 0xa5, 0x62, 0xc3, 0xa6,
|
||||
* 0x72, 0x67, 0x72, 0xc3, 0xb8, 0x64]);
|
||||
* var decoded = BASE64.decode("YmzDpWLDpnJncsO4ZAo=");
|
||||
*/
|
||||
const Base64Codec BASE64URL = const Base64Codec.urlSafe();
|
||||
|
||||
// Constants used in more than one class.
|
||||
const int _paddingChar = 0x3d; // '='.
|
||||
|
||||
|
@ -30,15 +43,18 @@ const int _paddingChar = 0x3d; // '='.
|
|||
* A [Base64Codec] allows base64 encoding bytes into ASCII strings and
|
||||
* decoding valid encodings back to bytes.
|
||||
*
|
||||
* This implementation only handles the simplest RFC 4648 base-64 encoding.
|
||||
* This implementation only handles the simplest RFC 4648 base64 and base64url
|
||||
* encodings.
|
||||
* It does not allow invalid characters when decoding and it requires,
|
||||
* and generates, padding so that the input is always a multiple of four
|
||||
* characters.
|
||||
*/
|
||||
class Base64Codec extends Codec<List<int>, String> {
|
||||
const Base64Codec();
|
||||
final Base64Encoder _encoder;
|
||||
const Base64Codec() : _encoder = const Base64Encoder();
|
||||
const Base64Codec.urlSafe() : _encoder = const Base64Encoder.urlSafe();
|
||||
|
||||
Base64Encoder get encoder => const Base64Encoder();
|
||||
Base64Encoder get encoder => _encoder;
|
||||
|
||||
Base64Decoder get decoder => const Base64Decoder();
|
||||
}
|
||||
|
@ -48,44 +64,53 @@ class Base64Codec extends Codec<List<int>, String> {
|
|||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Base-64 encoding converter.
|
||||
* Base64 and base64url encoding converter.
|
||||
*
|
||||
* Encodes lists of bytes using base64 encoding.
|
||||
* The result are ASCII strings using a restricted alphabet.
|
||||
* Encodes lists of bytes using base64 or base64url encoding.
|
||||
*
|
||||
* The results are ASCII strings using a restricted alphabet.
|
||||
*/
|
||||
class Base64Encoder extends Converter<List<int>, String> {
|
||||
const Base64Encoder();
|
||||
final bool _urlSafe;
|
||||
|
||||
const Base64Encoder() : _urlSafe = false;
|
||||
const Base64Encoder.urlSafe() : _urlSafe = true;
|
||||
|
||||
String convert(List<int> input) {
|
||||
if (input.isEmpty) return "";
|
||||
var encoder = new _Base64Encoder();
|
||||
var encoder = new _Base64Encoder(_urlSafe);
|
||||
Uint8List buffer = encoder.encode(input, 0, input.length, true);
|
||||
return new String.fromCharCodes(buffer);
|
||||
}
|
||||
|
||||
ByteConversionSink startChunkedConversion(Sink<String> sink) {
|
||||
if (sink is StringConversionSink) {
|
||||
return new _Utf8Base64EncoderSink(sink.asUtf8Sink(false));
|
||||
return new _Utf8Base64EncoderSink(sink.asUtf8Sink(false), _urlSafe);
|
||||
}
|
||||
return new _AsciiBase64EncoderSink(sink);
|
||||
return new _AsciiBase64EncoderSink(sink, _urlSafe);
|
||||
}
|
||||
|
||||
Stream<String> bind(Stream<List<int>> stream) {
|
||||
return new Stream<String>.eventTransformed(
|
||||
stream,
|
||||
(EventSink sink) =>
|
||||
new _ConverterStreamEventSink<List<int>, String>(this, sink));
|
||||
new _ConverterStreamEventSink<List<int>, String>(
|
||||
this, sink));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for encoding bytes to BASE-64.
|
||||
* Helper class for encoding bytes to base64.
|
||||
*/
|
||||
class _Base64Encoder {
|
||||
/** The RFC 4648 base64 encoding alphabet. */
|
||||
static const String _base64Alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/** The RFC 4648 base64url encoding alphabet. */
|
||||
static const String _base64urlAlphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
|
||||
/** Shift-count to extract the values stored in [_state]. */
|
||||
static const int _valueShift = 2;
|
||||
|
||||
|
@ -103,6 +128,12 @@ class _Base64Encoder {
|
|||
*/
|
||||
int _state = 0;
|
||||
|
||||
/** Alphabet used for encoding. */
|
||||
final String _alphabet;
|
||||
|
||||
_Base64Encoder(bool urlSafe)
|
||||
: _alphabet = urlSafe ? _base64urlAlphabet : _base64Alphabet;
|
||||
|
||||
/** Encode count and bits into a value to be stored in [_state]. */
|
||||
static int _encodeState(int count, int bits) {
|
||||
assert(count <= _countMask);
|
||||
|
@ -148,15 +179,17 @@ class _Base64Encoder {
|
|||
bufferLength += 4; // Room for padding.
|
||||
}
|
||||
var output = createBuffer(bufferLength);
|
||||
_state = encodeChunk(bytes, start, end, isLast, output, 0, _state);
|
||||
_state = encodeChunk(_alphabet, bytes, start, end, isLast,
|
||||
output, 0, _state);
|
||||
if (bufferLength > 0) return output;
|
||||
// If the input plus the data in state is still less than three bytes,
|
||||
// there may not be any output.
|
||||
return null;
|
||||
}
|
||||
|
||||
static int encodeChunk(List<int> bytes, int start, int end, bool isLast,
|
||||
Uint8List output, int outputIndex, int state) {
|
||||
static int encodeChunk(String alphabet,
|
||||
List<int> bytes, int start, int end, bool isLast,
|
||||
Uint8List output, int outputIndex, int state) {
|
||||
int bits = _stateBits(state);
|
||||
// Count number of missing bytes in three-byte chunk.
|
||||
int expectedChars = 3 - _stateCount(state);
|
||||
|
@ -172,20 +205,20 @@ class _Base64Encoder {
|
|||
expectedChars--;
|
||||
if (expectedChars == 0) {
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits >> 18) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits >> 18) & _sixBitMask);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits >> 12) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits >> 12) & _sixBitMask);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits >> 6) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits >> 6) & _sixBitMask);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt(bits & _sixBitMask);
|
||||
alphabet.codeUnitAt(bits & _sixBitMask);
|
||||
expectedChars = 3;
|
||||
bits = 0;
|
||||
}
|
||||
}
|
||||
if (byteOr >= 0 && byteOr <= 255) {
|
||||
if (isLast && expectedChars < 3) {
|
||||
writeFinalChunk(output, outputIndex, 3 - expectedChars, bits);
|
||||
writeFinalChunk(alphabet, output, outputIndex, 3 - expectedChars, bits);
|
||||
return 0;
|
||||
}
|
||||
return _encodeState(3 - expectedChars, bits);
|
||||
|
@ -208,24 +241,25 @@ class _Base64Encoder {
|
|||
* Only used when the [_state] contains a partial (1 or 2 byte)
|
||||
* input.
|
||||
*/
|
||||
static void writeFinalChunk(Uint8List output, int outputIndex,
|
||||
static void writeFinalChunk(String alphabet,
|
||||
Uint8List output, int outputIndex,
|
||||
int count, int bits) {
|
||||
assert(count > 0);
|
||||
if (count == 1) {
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits >> 2) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits >> 2) & _sixBitMask);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits << 4) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits << 4) & _sixBitMask);
|
||||
output[outputIndex++] = _paddingChar;
|
||||
output[outputIndex++] = _paddingChar;
|
||||
} else {
|
||||
assert(count == 2);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits >> 10) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits >> 10) & _sixBitMask);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits >> 4) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits >> 4) & _sixBitMask);
|
||||
output[outputIndex++] =
|
||||
_base64Alphabet.codeUnitAt((bits << 2) & _sixBitMask);
|
||||
alphabet.codeUnitAt((bits << 2) & _sixBitMask);
|
||||
output[outputIndex++] = _paddingChar;
|
||||
}
|
||||
}
|
||||
|
@ -240,6 +274,8 @@ class _BufferCachingBase64Encoder extends _Base64Encoder {
|
|||
*/
|
||||
Uint8List bufferCache;
|
||||
|
||||
_BufferCachingBase64Encoder(bool urlSafe) : super(urlSafe);
|
||||
|
||||
Uint8List createBuffer(int bufferLength) {
|
||||
if (bufferCache == null || bufferCache.length < bufferLength) {
|
||||
bufferCache = new Uint8List(bufferLength);
|
||||
|
@ -268,11 +304,11 @@ abstract class _Base64EncoderSink extends ByteConversionSinkBase {
|
|||
}
|
||||
|
||||
class _AsciiBase64EncoderSink extends _Base64EncoderSink {
|
||||
final _Base64Encoder _encoder = new _BufferCachingBase64Encoder();
|
||||
|
||||
final Sink<String> _sink;
|
||||
final _Base64Encoder _encoder;
|
||||
|
||||
_AsciiBase64EncoderSink(this._sink);
|
||||
_AsciiBase64EncoderSink(this._sink, bool urlSafe)
|
||||
: _encoder = new _BufferCachingBase64Encoder(urlSafe);
|
||||
|
||||
void _add(List<int> source, int start, int end, bool isLast) {
|
||||
Uint8List buffer = _encoder.encode(source, start, end, isLast);
|
||||
|
@ -288,9 +324,10 @@ class _AsciiBase64EncoderSink extends _Base64EncoderSink {
|
|||
|
||||
class _Utf8Base64EncoderSink extends _Base64EncoderSink {
|
||||
final ByteConversionSink _sink;
|
||||
final _Base64Encoder _encoder = new _Base64Encoder();
|
||||
final _Base64Encoder _encoder;
|
||||
|
||||
_Utf8Base64EncoderSink(this._sink);
|
||||
_Utf8Base64EncoderSink(this._sink, bool urlSafe)
|
||||
: _encoder = new _Base64Encoder(urlSafe);
|
||||
|
||||
void _add(List<int> source, int start, int end, bool isLast) {
|
||||
Uint8List buffer = _encoder.encode(source, start, end, isLast);
|
||||
|
@ -304,6 +341,13 @@ class _Utf8Base64EncoderSink extends _Base64EncoderSink {
|
|||
// Decoder
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Decoder for base64 encoded data.
|
||||
*
|
||||
* This decoder accepts both base64 and base64url ("url-safe") encodings.
|
||||
*
|
||||
* The encoding is required to be properly padded.
|
||||
*/
|
||||
class Base64Decoder extends Converter<String, List<int>> {
|
||||
const Base64Decoder();
|
||||
|
||||
|
@ -349,7 +393,7 @@ class _Base64Decoder {
|
|||
*
|
||||
* Accepts the "URL-safe" alphabet as well (`-` and `_` are the
|
||||
* 62nd and 63rd alphabet characters), and considers `%` a padding
|
||||
* character which mush be followed by `3D`, the percent-escape
|
||||
* character, which mush then be followed by `3D`, the percent-escape
|
||||
* for `=`.
|
||||
*/
|
||||
static final List<int> _inverseAlphabet = new Int8List.fromList([
|
||||
|
@ -371,7 +415,7 @@ class _Base64Decoder {
|
|||
/**
|
||||
* Maintains the intermediate state of a partly-decoded input.
|
||||
*
|
||||
* BASE-64 is decoded in chunks of four characters. If a chunk does not
|
||||
* Base64 is decoded in chunks of four characters. If a chunk does not
|
||||
* contain a full block, the decoded bits (six per character) of the
|
||||
* available characters are stored in [_state] until the next call to
|
||||
* [_decode] or [_close].
|
||||
|
@ -618,7 +662,7 @@ class _Base64Decoder {
|
|||
* Check that the remainder of the string is valid padding.
|
||||
*
|
||||
* Valid padding is a correct number (0, 1 or 2) of `=` characters
|
||||
* or `%3D` sequences depending on the number of preceding BASE-64 characters.
|
||||
* or `%3D` sequences depending on the number of preceding base64 characters.
|
||||
* The [state] parameter encodes which padding continuations are allowed
|
||||
* as the number of expected characters. That number is the number of
|
||||
* expected padding characters times 3 minus the number of padding characters
|
||||
|
|
|
@ -31,16 +31,23 @@ void testRoundtrip(list, name) {
|
|||
// Direct.
|
||||
String encodedNormal = BASE64.encode(list);
|
||||
String encodedPercent = encodedNormal.replaceAll("=", "%3D");
|
||||
String uriEncoded = encodedNormal.replaceAll("+", "-").replaceAll("/", "_");
|
||||
String uriEncoded = BASE64URL.encode(list);
|
||||
String expectedUriEncoded =
|
||||
encodedNormal.replaceAll("+", "-").replaceAll("/", "_");
|
||||
Expect.equals(expectedUriEncoded, uriEncoded);
|
||||
|
||||
List result = BASE64.decode(encodedNormal);
|
||||
Expect.listEquals(list, result, name);
|
||||
result = BASE64.decode(encodedPercent);
|
||||
Expect.listEquals(list, result, name);
|
||||
result = BASE64.decode(uriEncoded);
|
||||
Expect.listEquals(list, result, name);
|
||||
|
||||
int increment = list.length ~/ 7 + 1;
|
||||
// Chunked.
|
||||
for (int i = 0; i < list.length; i += increment) {
|
||||
for (int j = i; j < list.length; j += increment) {
|
||||
// Normal
|
||||
{
|
||||
// Using add/close
|
||||
var results;
|
||||
|
@ -64,6 +71,30 @@ void testRoundtrip(list, name) {
|
|||
var name = "0-$i-$j-${list.length}: $list";
|
||||
Expect.equals(encodedNormal, results.join(""), name);
|
||||
}
|
||||
// URI
|
||||
{
|
||||
// Using add/close
|
||||
var results;
|
||||
var sink = new ChunkedConversionSink.withCallback((v) { results = v; });
|
||||
var encoder = BASE64URL.encoder.startChunkedConversion(sink);
|
||||
encoder.add(list.sublist(0, i));
|
||||
encoder.add(list.sublist(i, j));
|
||||
encoder.add(list.sublist(j, list.length));
|
||||
encoder.close();
|
||||
var name = "0-$i-$j-${list.length}: list";
|
||||
Expect.equals(uriEncoded, results.join(""), name);
|
||||
}
|
||||
{
|
||||
// Using addSlice
|
||||
var results;
|
||||
var sink = new ChunkedConversionSink.withCallback((v) { results = v; });
|
||||
var encoder = BASE64URL.encoder.startChunkedConversion(sink);
|
||||
encoder.addSlice(list, 0, i, false);
|
||||
encoder.addSlice(list, i, j, false);
|
||||
encoder.addSlice(list, j, list.length, true);
|
||||
var name = "0-$i-$j-${list.length}: $list";
|
||||
Expect.equals(uriEncoded, results.join(""), name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +150,7 @@ void testErrors() {
|
|||
}
|
||||
void badDecode(String string) {
|
||||
Expect.throws(() => BASE64.decode(string), isFormatException, string);
|
||||
Expect.throws(() => BASE64URL.decode(string), isFormatException, string);
|
||||
badChunkDecode([string]);
|
||||
badChunkDecode(["", string]);
|
||||
badChunkDecode([string, ""]);
|
||||
|
|
Loading…
Reference in a new issue