| Index: sdk/lib/convert/base64.dart
|
| diff --git a/sdk/lib/convert/base64.dart b/sdk/lib/convert/base64.dart
|
| index 64a63e663349b37102ac0a0e61fdc243ed978d52..77f7158008902eced9c39244400b72a4209e6166 100644
|
| --- a/sdk/lib/convert/base64.dart
|
| +++ b/sdk/lib/convert/base64.dart
|
| @@ -310,10 +310,6 @@ class Base64Decoder extends Converter<String, List<int>> {
|
| List<int> convert(String input) {
|
| if (input.isEmpty) return new Uint8List(0);
|
| int length = input.length;
|
| - if (length % 4 != 0) {
|
| - throw new FormatException("Invalid length, must be multiple of four",
|
| - input, length);
|
| - }
|
| var decoder = new _Base64Decoder();
|
| Uint8List buffer = decoder.decode(input, 0, input.length);
|
| decoder.close(input, input.length);
|
| @@ -350,18 +346,28 @@ class _Base64Decoder {
|
| *
|
| * Uses [_invalid] for invalid indices and [_padding] for the padding
|
| * character.
|
| + *
|
| + * 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
|
| + * for `=`.
|
| */
|
| static final List<int> _inverseAlphabet = new Int8List.fromList([
|
| __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __,
|
| __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __,
|
| - __, __, __, __, __, __, __, __, __, __, __, 62, __, __, __, 63,
|
| + __, __, __, __, __, _p, __, __, __, __, __, 62, __, 62, __, 63,
|
| 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, __, __, __, _p, __, __,
|
| __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
| - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, __, __, __, __, __,
|
| + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, __, __, __, __, 63,
|
| __, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
| 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, __, __, __, __, __,
|
| ]);
|
|
|
| + // Character constants.
|
| + static const int _char_percent = 0x25; // '%'.
|
| + static const int _char_3 = 0x33; // '3'.
|
| + static const int _char_d = 0x64; // 'd'.
|
| +
|
| /**
|
| * Maintains the intermediate state of a partly-decoded input.
|
| *
|
| @@ -412,7 +418,7 @@ class _Base64Decoder {
|
| */
|
| static int _encodePaddingState(int expectedPadding) {
|
| assert(expectedPadding >= 0);
|
| - assert(expectedPadding <= 1);
|
| + assert(expectedPadding <= 5);
|
| return -expectedPadding - 1; // ~expectedPadding adapted to dart2js.
|
| }
|
|
|
| @@ -498,6 +504,7 @@ class _Base64Decoder {
|
| }
|
| continue;
|
| } else if (code == _padding && count > 1) {
|
| + if (charOr < 0 || charOr > asciiMax) break;
|
| if (count == 3) {
|
| if ((bits & 0x03) != 0) {
|
| throw new FormatException(
|
| @@ -512,7 +519,13 @@ class _Base64Decoder {
|
| }
|
| output[outIndex++] = bits >> 4;
|
| }
|
| - int expectedPadding = 3 - count;
|
| + // Expected padding is the number of expected padding characters,
|
| + // where `=` counts as three and `%3D` counts as one per character.
|
| + //
|
| + // Expect either zero or one padding depending on count (2 or 3),
|
| + // plus two more characters if the code was `%` (a partial padding).
|
| + int expectedPadding = (3 - count) * 3;
|
| + if (char == _char_percent) expectedPadding += 2;
|
| state = _encodePaddingState(expectedPadding);
|
| return _checkPadding(input, i + 1, end, state);
|
| }
|
| @@ -538,53 +551,134 @@ class _Base64Decoder {
|
| static Uint8List _allocateBuffer(String input, int start, int end,
|
| int state) {
|
| assert(state >= 0);
|
| - int padding = 0;
|
| - int length = _stateCount(state) + (end - start);
|
| - if (end > start && input.codeUnitAt(end - 1) == _paddingChar) {
|
| - padding++;
|
| - if (end - 1 > start && input.codeUnitAt(end - 2) == _paddingChar) {
|
| - padding++;
|
| - }
|
| - }
|
| + int paddingStart = _trimPaddingChars(input, start, end);
|
| + int length = _stateCount(state) + (paddingStart - start);
|
| // Three bytes per full four bytes in the input.
|
| int bufferLength = (length >> 2) * 3;
|
| - // If padding was seen, then remove the padding if it was counter, or
|
| - // add the last partial chunk it it wasn't counted.
|
| + // If padding was seen, then this is the last chunk, and the final partial
|
| + // chunk should be decoded too.
|
| int remainderLength = length & 3;
|
| - if (remainderLength == 0) {
|
| - bufferLength -= padding;
|
| - } else if (padding != 0 && remainderLength - padding > 1) {
|
| - bufferLength += remainderLength - 1 - padding;
|
| + if (remainderLength != 0 && paddingStart < end) {
|
| + bufferLength += remainderLength - 1;
|
| }
|
| if (bufferLength > 0) return new Uint8List(bufferLength);
|
| - // If the input plus state is less than four characters, no buffer
|
| - // is needed.
|
| + // If the input plus state is less than four characters, and it's not
|
| + // at the end of input, no buffer is needed.
|
| return null;
|
| }
|
|
|
| /**
|
| + * Returns the position of the start of padding at the end of the input.
|
| + *
|
| + * Returns the end of input if there is no padding.
|
| + *
|
| + * This is used to ensure that the decoding buffer has the exact size
|
| + * it needs when input is valid, and at least enough bytes to reach the error
|
| + * when input is invalid.
|
| + *
|
| + * Never count more than two padding sequences since any more than that
|
| + * will raise an error anyway, and we only care about being precise for
|
| + * successful conversions.
|
| + */
|
| + static int _trimPaddingChars(String input, int start, int end) {
|
| + // This may count '%=' as two paddings. That's ok, it will err later,
|
| + // but the buffer will be large enough to reach the error.
|
| + int padding = 0;
|
| + int index = end;
|
| + int newEnd = end;
|
| + while (index > start && padding < 2) {
|
| + index--;
|
| + int char = input.codeUnitAt(index);
|
| + if (char == _paddingChar) {
|
| + padding++;
|
| + newEnd = index;
|
| + continue;
|
| + }
|
| + if ((char | 0x20) == _char_d) {
|
| + if (index == start) break;
|
| + index--;
|
| + char = input.codeUnitAt(index);
|
| + }
|
| + if (char == _char_3) {
|
| + if (index == start) break;
|
| + index--;
|
| + char = input.codeUnitAt(index);
|
| + }
|
| + if (char == _char_percent) {
|
| + padding++;
|
| + newEnd = index;
|
| + continue;
|
| + }
|
| + break;
|
| + }
|
| + return newEnd;
|
| + }
|
| +
|
| + /**
|
| * Check that the remainder of the string is valid padding.
|
| *
|
| - * That means zero or one padding character (depending on [_state])
|
| - * and nothing else.
|
| + * Valid padding is a correct number (0, 1 or 2) of `=` characters
|
| + * or `%3D` sequences depending on the number of preceding BASE-64 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
|
| + * seen so far, where `=` counts as 3 counts as three characters,
|
| + * and the padding sequence `%3D` counts as one character per character.
|
| + *
|
| + * The number of missing characters is always between 0 and 5 because we
|
| + * only call this function after having seen at least one `=` or `%`
|
| + * character.
|
| + * If the number of missing characters is not 3 or 0, we have seen (at least)
|
| + * a `%` character and expects the rest of the `%3D` sequence, and a `=` is
|
| + * not allowed. When missing 3 characters, either `=` or `%` is allowed.
|
| + *
|
| + * When the value is 0, no more padding (or any other character) is allowed.
|
| */
|
| static int _checkPadding(String input, int start, int end, int state) {
|
| assert(_hasSeenPadding(state));
|
| if (start == end) return state;
|
| int expectedPadding = _statePadding(state);
|
| - if (expectedPadding > 0) {
|
| - int firstChar = input.codeUnitAt(start);
|
| - if (firstChar != _paddingChar) {
|
| - throw new FormatException("Missing padding character", input, start);
|
| + assert(expectedPadding >= 0);
|
| + assert(expectedPadding < 6);
|
| + while (expectedPadding > 0) {
|
| + int char = input.codeUnitAt(start);
|
| + if (expectedPadding == 3) {
|
| + if (char == _paddingChar) {
|
| + expectedPadding -= 3;
|
| + start++;
|
| + break;
|
| + }
|
| + if (char == _char_percent) {
|
| + expectedPadding--;
|
| + start++;
|
| + if (start == end) break;
|
| + char = input.codeUnitAt(start);
|
| + } else {
|
| + break;
|
| + }
|
| + }
|
| + // Partial padding means we have seen part of a "%3D" escape.
|
| + int expectedPartialPadding = expectedPadding;
|
| + if (expectedPartialPadding > 3) expectedPartialPadding -= 3;
|
| + if (expectedPartialPadding == 2) {
|
| + // Expects '3'
|
| + if (char != _char_3) break;
|
| + start++;
|
| + expectedPadding--;
|
| + if (start == end) break;
|
| + char = input.codeUnitAt(start);
|
| }
|
| - state = _encodePaddingState(0);
|
| + // Expects 'D' or 'd'.
|
| + if ((char | 0x20) != _char_d) break;
|
| start++;
|
| + expectedPadding--;
|
| + if (start == end) break;
|
| }
|
| if (start != end) {
|
| - throw new FormatException("Invalid character after padding",
|
| + throw new FormatException("Invalid padding character",
|
| input, start);
|
| }
|
| - return state;
|
| + return _encodePaddingState(expectedPadding);
|
| }
|
| }
|
|
|
|
|