Index: sdk/lib/convert/base64.dart |
diff --git a/sdk/lib/convert/base64.dart b/sdk/lib/convert/base64.dart |
index 0254f853dbacd699704530b9be2596bbff3411b8..ee65ae7487ffd82d7ae994c3cffd834440642c00 100644 |
--- a/sdk/lib/convert/base64.dart |
+++ b/sdk/lib/convert/base64.dart |
@@ -57,6 +57,140 @@ class Base64Codec extends Codec<List<int>, String> { |
Base64Encoder get encoder => _encoder; |
Base64Decoder get decoder => const Base64Decoder(); |
+ |
+ /** |
+ * Validates and normalizes the base64 encoded data in [source]. |
+ * |
+ * Only acts on the substring from [start] to [end], with [end] |
+ * defaulting to the end of the string. |
+ * |
+ * Normalization will: |
+ * * Unescape any `%`-escapes. |
+ * * Only allow valid characters (`A`-`Z`, `a`-`z`, `0`-`9`, `/` and `+`). |
+ * * Normalize a `_` or `-` character to `/` or `+`. |
+ * * Validate that existing padding (trailing `=` characters) is correct. |
+ * * If no padding exists, add correct padding if necessary and possible. |
+ * * Validate that the length is correct (a multiple of four). |
+ */ |
+ String normalize(String source, [int start = 0, int end]) { |
+ end = RangeError.checkValidRange(start, end, source.length); |
+ const int percent = 0x25; |
+ const int equals = 0x3d; |
+ StringBuffer buffer = null; |
+ int sliceStart = start; |
+ var alphabet = _Base64Encoder._base64Alphabet; |
+ var inverseAlphabet = _Base64Decoder._inverseAlphabet; |
+ int firstPadding = -1; |
+ int firstPaddingSourceIndex = -1; |
+ int paddingCount = 0; |
+ for (int i = start; i < end;) { |
+ int sliceEnd = i; |
+ int char = source.codeUnitAt(i++); |
+ int originalChar = char; |
+ // Normalize char, keep originalChar to see if it matches the source. |
+ if (char == percent) { |
+ if (i + 2 <= end) { |
+ char = parseHexByte(source, i); // May be negative. |
+ i += 2; |
+ // We know that %25 isn't valid, but our table considers it |
+ // a potential padding start, so skip the checks. |
+ if (char == percent) char = -1; |
+ } else { |
+ // An invalid HEX escape (too short). |
+ // Just skip past the handling and reach the throw below. |
+ char = -1; |
+ } |
+ } |
+ // If char is negative here, hex-decoding failed in some way. |
+ if (0 <= char && char <= 127) { |
+ int value = inverseAlphabet[char]; |
+ if (value >= 0) { |
+ char = alphabet.codeUnitAt(value); |
+ if (char == originalChar) continue; |
+ } else if (value == _Base64Decoder._padding) { |
+ // We have ruled out percent, so char is '='. |
+ if (firstPadding < 0) { |
+ // Mark position in normalized output where padding occurs. |
+ firstPadding = (buffer?.length ?? 0) + (sliceEnd - sliceStart); |
+ firstPaddingSourceIndex = sliceEnd; |
+ } |
+ paddingCount++; |
+ // It could have been an escaped equals (%3D). |
+ if (originalChar == equals) continue; |
+ } |
+ if (value != _Base64Decoder._invalid) { |
+ buffer ??= new StringBuffer(); |
+ buffer.write(source.substring(sliceStart, sliceEnd)); |
+ buffer.writeCharCode(char); |
+ sliceStart = i; |
+ continue; |
+ } |
+ } |
+ throw new FormatException("Invalid base64 data", source, sliceEnd); |
+ } |
+ if (buffer != null) { |
+ buffer.write(source.substring(sliceStart, end)); |
+ if (firstPadding >= 0) { |
+ // There was padding in the source. Check that it is valid: |
+ // * result length a multiple of four |
+ // * one or two padding characters at the end. |
+ _checkPadding(source, firstPaddingSourceIndex, end, |
+ firstPadding, paddingCount, buffer.length); |
+ } else { |
+ // Length of last chunk (1-4 chars) in the encoding. |
+ int endLength = ((buffer.length - 1) % 4) + 1; |
+ if (endLength == 1) { |
+ // The data must have length 0, 2 or 3 modulo 4. |
+ throw new FormatException("Invalid base64 encoding length ", |
+ source, end); |
+ } |
+ while (endLength < 4) { |
+ buffer.write("="); |
+ endLength++; |
+ } |
+ } |
+ return source.replaceRange(start, end, buffer.toString()); |
+ } |
+ // Original was already normalized, only check padding. |
+ int length = end - start; |
+ if (firstPadding >= 0) { |
+ _checkPadding(source, firstPaddingSourceIndex, end, |
+ firstPadding, paddingCount, length); |
+ } else { |
+ // No padding given, so add some if needed it. |
+ int endLength = length % 4; |
+ if (endLength == 1) { |
+ // The data must have length 0, 2 or 3 modulo 4. |
+ throw new FormatException("Invalid base64 encoding length ", |
+ source, end); |
+ } |
+ if (endLength > 1) { |
+ // There is no "insertAt" on String, but this works as well. |
+ source = source.replaceRange(end, end, (endLength == 2) ? "==" : "="); |
+ } |
+ } |
+ return source; |
+ } |
+ |
+ static int _checkPadding(String source, int sourceIndex, int sourceEnd, |
+ int firstPadding, int paddingCount, int length) { |
+ if (length % 4 != 0) { |
+ throw new FormatException( |
+ "Invalid base64 padding, padded length must be multiple of four, " |
+ "is $length", |
+ source, sourceEnd); |
+ } |
+ if (firstPadding + paddingCount != length) { |
+ throw new FormatException( |
+ "Invalid base64 padding, '=' not at the end", |
+ source, sourceIndex); |
+ } |
+ if (paddingCount > 2) { |
+ throw new FormatException( |
+ "Invalid base64 padding, more than two '=' characters", |
+ source, sourceIndex); |
+ } |
+ } |
} |
// ------------------------------------------------------------------------ |
@@ -389,7 +523,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 then be followed by `3D`, the percent-escape |
+ * character, which must then be followed by `3D`, the percent-escape |
* for `=`. |
*/ |
static final List<int> _inverseAlphabet = new Int8List.fromList([ |