// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of "dart:convert"; /// A [base64](https://tools.ietf.org/html/rfc4648) encoder and decoder. /// /// It encodes using the default base64 alphabet, /// decodes using both the base64 and base64url alphabets, /// does not allow invalid characters and requires padding. /// /// Examples: /// ```dart /// var encoded = base64.encode([0x62, 0x6c, 0xc3, 0xa5, 0x62, 0xc3, 0xa6, /// 0x72, 0x67, 0x72, 0xc3, 0xb8, 0x64]); /// var decoded = base64.decode("YmzDpWLDpnJncsO4ZAo="); /// ``` /// The top-level [base64Encode] and [base64Decode] functions may be used /// instead if a local variable shadows the [base64] constant. const Base64Codec base64 = 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: /// ```dart /// var encoded = base64Url.encode([0x62, 0x6c, 0xc3, 0xa5, 0x62, 0xc3, 0xa6, /// 0x72, 0x67, 0x72, 0xc3, 0xb8, 0x64]); /// var decoded = base64Url.decode("YmzDpWLDpnJncsO4ZAo="); /// ``` const Base64Codec base64Url = Base64Codec.urlSafe(); /// Encodes [bytes] using [base64](https://tools.ietf.org/html/rfc4648) encoding. /// /// Shorthand for `base64.encode(bytes)`. Useful if a local variable shadows the global /// [base64] constant. String base64Encode(List bytes) => base64.encode(bytes); /// Encodes [bytes] using [base64url](https://tools.ietf.org/html/rfc4648) encoding. /// /// Shorthand for `base64url.encode(bytes)`. String base64UrlEncode(List bytes) => base64Url.encode(bytes); /// Decodes [base64](https://tools.ietf.org/html/rfc4648) or [base64url](https://tools.ietf.org/html/rfc4648) encoded bytes. /// /// Shorthand for `base64.decode(bytes)`. Useful if a local variable shadows the /// global [base64] constant. Uint8List base64Decode(String source) => base64.decode(source); // Constants used in more than one class. const int _paddingChar = 0x3d; // '='. /// A [base64](https://tools.ietf.org/html/rfc4648) encoder and decoder. /// /// A [Base64Codec] allows base64 encoding bytes into ASCII strings and /// decoding valid encodings back to bytes. /// /// 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. final class Base64Codec extends Codec, String> { final Base64Encoder _encoder; const Base64Codec() : _encoder = const Base64Encoder(); const Base64Codec.urlSafe() : _encoder = const Base64Encoder.urlSafe(); Base64Encoder get encoder => _encoder; Base64Decoder get decoder => const Base64Decoder(); /// Decodes [encoded]. /// /// The input is decoded as if by `decoder.convert`. /// /// The returned [Uint8List] contains exactly the decoded bytes, /// so the [Uint8List.length] is precisely the number of decoded bytes. /// The [Uint8List.buffer] may be larger than the decoded bytes. Uint8List decode(String encoded) => decoder.convert(encoded); /// 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 percent = 0x25; const equals = 0x3d; StringBuffer? buffer; var sliceStart = start; var alphabet = _Base64Encoder._base64Alphabet; var inverseAlphabet = _Base64Decoder._inverseAlphabet; var firstPadding = -1; var firstPaddingSourceIndex = -1; var paddingCount = 0; for (var i = start; i < end;) { var sliceEnd = i; var char = source.codeUnitAt(i++); var 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) { var 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 ??= StringBuffer()) ..write(source.substring(sliceStart, sliceEnd)) ..writeCharCode(char); sliceStart = i; continue; } } throw 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. var endLength = ((buffer.length - 1) % 4) + 1; if (endLength == 1) { // The data must have length 0, 2 or 3 modulo 4. throw 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. var length = end - start; if (firstPadding >= 0) { _checkPadding( source, firstPaddingSourceIndex, end, firstPadding, paddingCount, length, ); } else { // No padding given, so add some if needed it. var endLength = length % 4; if (endLength == 1) { // The data must have length 0, 2 or 3 modulo 4. throw 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 void _checkPadding( String source, int sourceIndex, int sourceEnd, int firstPadding, int paddingCount, int length, ) { if (length % 4 != 0) { throw FormatException( "Invalid base64 padding, padded length must be multiple of four, " "is $length", source, sourceEnd, ); } if (firstPadding + paddingCount != length) { throw FormatException( "Invalid base64 padding, '=' not at the end", source, sourceIndex, ); } if (paddingCount > 2) { throw FormatException( "Invalid base64 padding, more than two '=' characters", source, sourceIndex, ); } } } // ------------------------------------------------------------------------ // Encoder // ------------------------------------------------------------------------ /// Base64 and base64url encoding converter. /// /// Encodes lists of bytes using base64 or base64url encoding. /// /// The results are ASCII strings using a restricted alphabet. /// /// Example: /// ```dart /// final base64Encoder = base64.encoder; /// const sample = 'Dart is open source'; /// final encodedSample = base64Encoder.convert(sample.codeUnits); /// print(encodedSample); // RGFydCBpcyBvcGVuIHNvdXJjZQ== /// ``` final class Base64Encoder extends Converter, String> { final bool _urlSafe; const Base64Encoder() : _urlSafe = false; const Base64Encoder.urlSafe() : _urlSafe = true; String convert(List input) { if (input.isEmpty) return ""; var encoder = _Base64Encoder(_urlSafe); var buffer = encoder.encode(input, 0, input.length, true)!; return String.fromCharCodes(buffer); } ByteConversionSink startChunkedConversion(Sink sink) { if (sink is StringConversionSink) { return _Utf8Base64EncoderSink(sink.asUtf8Sink(false), _urlSafe); } return _AsciiBase64EncoderSink(sink, _urlSafe); } } /// 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; /// Mask to extract the count value stored in [_state]. static const int _countMask = 3; static const int _sixBitMask = 0x3F; /// Intermediate state between chunks. /// /// Encoding handles three bytes at a time. /// If fewer than three bytes has been seen, this value encodes /// the number of bytes seen (0, 1 or 2) and their values. 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); return bits << _valueShift | count; } /// Extract bits from encoded state. static int _stateBits(int state) => state >> _valueShift; /// Extract count from encoded state. static int _stateCount(int state) => state & _countMask; /// Create a [Uint8List] with the provided length. Uint8List createBuffer(int bufferLength) => Uint8List(bufferLength); /// Encode [bytes] from [start] to [end] and the bits in [_state]. /// /// Returns a [Uint8List] of the ASCII codes of the encoded data. /// /// If the input, including left over [_state] from earlier encodings, /// is not a multiple of three bytes, then the partial state is stored /// back into [_state]. /// If [isLast] is true, partial state is encoded in the output instead, /// with the necessary padding. /// /// Returns `null` if there is no output. Uint8List? encode(List bytes, int start, int end, bool isLast) { assert(0 <= start); assert(start <= end); assert(end <= bytes.length); var length = end - start; var count = _stateCount(_state); var byteCount = (count + length); var fullChunks = byteCount ~/ 3; var partialChunkLength = byteCount - fullChunks * 3; var bufferLength = fullChunks * 4; if (isLast && partialChunkLength > 0) { bufferLength += 4; // Room for padding. } var output = createBuffer(bufferLength); _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( String alphabet, List bytes, int start, int end, bool isLast, Uint8List output, int outputIndex, int state, ) { var bits = _stateBits(state); // Count number of missing bytes in three-byte chunk. var expectedChars = 3 - _stateCount(state); // The input must be a list of bytes (integers in the range 0..255). // The value of `byteOr` will be the bitwise or of all the values in // `bytes` and a later check will validate that they were all valid bytes. var byteOr = 0; for (var i = start; i < end; i++) { var byte = bytes[i]; byteOr |= byte; bits = ((bits << 8) | byte) & 0xFFFFFF; // Never store more than 24 bits. expectedChars--; if (expectedChars == 0) { output[outputIndex++] = alphabet.codeUnitAt((bits >> 18) & _sixBitMask); output[outputIndex++] = alphabet.codeUnitAt((bits >> 12) & _sixBitMask); output[outputIndex++] = alphabet.codeUnitAt((bits >> 6) & _sixBitMask); output[outputIndex++] = alphabet.codeUnitAt(bits & _sixBitMask); expectedChars = 3; bits = 0; } } if (byteOr >= 0 && byteOr <= 255) { if (isLast && expectedChars < 3) { writeFinalChunk(alphabet, output, outputIndex, 3 - expectedChars, bits); return 0; } return _encodeState(3 - expectedChars, bits); } // There was an invalid byte value somewhere in the input - find it! var i = start; while (i < end) { var byte = bytes[i]; if (byte < 0 || byte > 255) break; i++; } throw ArgumentError.value( bytes, "Not a byte value at index $i: 0x${bytes[i].toRadixString(16)}", ); } /// Writes a final encoded four-character chunk. /// /// Only used when the [_state] contains a partial (1 or 2 byte) /// input. static void writeFinalChunk( String alphabet, Uint8List output, int outputIndex, int count, int bits, ) { assert(count > 0); if (count == 1) { output[outputIndex++] = alphabet.codeUnitAt((bits >> 2) & _sixBitMask); output[outputIndex++] = alphabet.codeUnitAt((bits << 4) & _sixBitMask); output[outputIndex++] = _paddingChar; output[outputIndex++] = _paddingChar; } else { assert(count == 2); output[outputIndex++] = alphabet.codeUnitAt((bits >> 10) & _sixBitMask); output[outputIndex++] = alphabet.codeUnitAt((bits >> 4) & _sixBitMask); output[outputIndex++] = alphabet.codeUnitAt((bits << 2) & _sixBitMask); output[outputIndex++] = _paddingChar; } } } class _BufferCachingBase64Encoder extends _Base64Encoder { /// Reused buffer. /// /// When the buffer isn't released to the sink, only used to create another /// value (a string), the buffer can be reused between chunks. Uint8List? bufferCache; _BufferCachingBase64Encoder(bool urlSafe) : super(urlSafe); Uint8List createBuffer(int bufferLength) { Uint8List? buffer = bufferCache; if (buffer == null || buffer.length < bufferLength) { bufferCache = buffer = Uint8List(bufferLength); } // Return a view of the buffer, so it has the requested length. return Uint8List.view(buffer.buffer, buffer.offsetInBytes, bufferLength); } } abstract class _Base64EncoderSink extends ByteConversionSink { void add(List source) { _add(source, 0, source.length, false); } void close() { _add(const [], 0, 0, true); } void addSlice(List source, int start, int end, bool isLast) { if (end == null) throw ArgumentError.notNull("end"); RangeError.checkValidRange(start, end, source.length); _add(source, start, end, isLast); } void _add(List source, int start, int end, bool isLast); } class _AsciiBase64EncoderSink extends _Base64EncoderSink { final Sink _sink; final _Base64Encoder _encoder; _AsciiBase64EncoderSink(this._sink, bool urlSafe) : _encoder = _BufferCachingBase64Encoder(urlSafe); void _add(List source, int start, int end, bool isLast) { var buffer = _encoder.encode(source, start, end, isLast); if (buffer != null) { var string = String.fromCharCodes(buffer); _sink.add(string); } if (isLast) { _sink.close(); } } } class _Utf8Base64EncoderSink extends _Base64EncoderSink { final ByteConversionSink _sink; final _Base64Encoder _encoder; _Utf8Base64EncoderSink(this._sink, bool urlSafe) : _encoder = _Base64Encoder(urlSafe); void _add(List source, int start, int end, bool isLast) { var buffer = _encoder.encode(source, start, end, isLast); if (buffer != null) { _sink.addSlice(buffer, 0, buffer.length, isLast); } } } // ------------------------------------------------------------------------ // Decoder // ------------------------------------------------------------------------ /// Decoder for base64 encoded data. /// /// This decoder accepts both base64 and base64url ("url-safe") encodings. /// /// The encoding is required to be properly padded. /// /// Throws a [FormatException] if the input is not valid base64 data. /// /// Example: /// ```dart /// final base64Decoder = base64.decoder; /// const base64Bytes = 'RGFydCBpcyBvcGVuIHNvdXJjZQ=='; /// final decodedBytes = base64Decoder.convert(base64Bytes); /// // decodedBytes: [68, 97, 114, 116, 32, 105, 115, 32, 111, 112, 101, 110, /// // 32, 115, 111, 117, 114, 99, 101] /// /// // Print as string using UTF-8 decoder /// print(utf8.decode(decodedBytes)); // Dart is open source /// ``` final class Base64Decoder extends Converter> { const Base64Decoder(); /// Decodes the characters of [input] from [start] to [end] as base64. /// /// If [start] is omitted, it defaults to the start of [input]. /// If [end] is omitted, it defaults to the end of [input]. /// /// The returned [Uint8List] contains exactly the decoded bytes, /// so the [Uint8List.length] is precisely the number of decoded bytes. /// The [Uint8List.buffer] may be larger than the decoded bytes. Uint8List convert(String input, [int start = 0, int? end]) { end = RangeError.checkValidRange(start, end, input.length); if (start == end) return Uint8List(0); var decoder = _Base64Decoder(); var buffer = decoder.decode(input, start, end)!; decoder.close(input, end); return buffer; } StringConversionSink startChunkedConversion(Sink> sink) { return _Base64DecoderSink(sink); } } /// Helper class implementing base64 decoding with intermediate state. class _Base64Decoder { /// Shift-count to extract the values stored in [_state]. static const int _valueShift = 2; /// Mask to extract the count value stored in [_state]. static const int _countMask = 3; /// Invalid character in decoding table. static const int _invalid = -2; /// Padding character in decoding table. static const int _padding = -1; // Shorthands to make the table more readable. static const int __ = _invalid; static const int _p = _padding; /// Mapping from ASCII characters to their index in the base64 alphabet. /// /// 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 must then be followed by `3D`, the percent-escape /// for `=`. @pragma('vm:shared') static final List _inverseAlphabet = Int8List.fromList([ __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // __, __, __, __, __, _p, __, __, __, __, __, 62, __, 62, __, 63, // 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, __, __, __, _p, __, __, // __, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, // 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. /// /// 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]. /// /// If no padding has been seen, the value is /// `numberOfCharactersSeen | (decodedBits << 2)` /// where `numberOfCharactersSeen` is between 0 and 3 and decoded bits /// contains six bits per seen character. /// /// If padding has been seen the value is negative. It's the bitwise negation /// of the number of remaining allowed padding characters (always ~0 or ~1). /// /// A state of `0` or `~0` are valid places to end decoding, all other values /// mean that a four-character block has not been completed. int _state = 0; /// Encodes [count] and [bits] as a value to be stored in [_state]. static int _encodeCharacterState(int count, int bits) { assert(count == (count & _countMask)); return (bits << _valueShift | count); } /// Extracts count from a [_state] value. static int _stateCount(int state) { assert(state >= 0); return state & _countMask; } /// Extracts value bits from a [_state] value. static int _stateBits(int state) { assert(state >= 0); return state >> _valueShift; } /// Encodes a number of expected padding characters to be stored in [_state]. static int _encodePaddingState(int expectedPadding) { assert(expectedPadding >= 0); assert(expectedPadding <= 5); return -expectedPadding - 1; // ~expectedPadding adapted to dart2js. } /// Extracts expected padding character count from a [_state] value. static int _statePadding(int state) { assert(state < 0); return -state - 1; // ~state adapted to dart2js. } static bool _hasSeenPadding(int state) => state < 0; /// Decodes [input] from [start] to [end]. /// /// Returns a [Uint8List] with the decoded bytes. /// If a previous call had an incomplete four-character block, the bits from /// those are included in decoding Uint8List? decode(String input, int start, int end) { assert(0 <= start); assert(start <= end); assert(end <= input.length); if (_hasSeenPadding(_state)) { _state = _checkPadding(input, start, end, _state); return null; } if (start == end) return Uint8List(0); var buffer = _allocateBuffer(input, start, end, _state); _state = decodeChunk(input, start, end, buffer, 0, _state); return buffer; } /// Checks that [_state] represents a valid decoding. void close(String? input, int? end) { if (_state < _encodePaddingState(0)) { throw FormatException("Missing padding character", input, end); } if (_state > 0) { throw FormatException( "Invalid length, must be multiple of four", input, end, ); } _state = _encodePaddingState(0); } /// Decodes [input] from [start] to [end]. /// /// Includes the state returned by a previous call in the decoding. /// Writes the decoding to [output] at [outIndex], and there must /// be room in the output. static int decodeChunk( String input, int start, int end, Uint8List output, int outIndex, int state, ) { assert(!_hasSeenPadding(state)); const asciiMask = 127; const asciiMax = 127; const eightBitMask = 0xFF; const bitsPerCharacter = 6; var bits = _stateBits(state); var count = _stateCount(state); // String contents should be all ASCII. // Instead of checking for each character, we collect the bitwise-or of // all the characters in `charOr` and later validate that all characters // were ASCII. var charOr = 0; final inverseAlphabet = _Base64Decoder._inverseAlphabet; for (var i = start; i < end; i++) { var char = input.codeUnitAt(i); charOr |= char; var code = inverseAlphabet[char & asciiMask]; if (code >= 0) { bits = ((bits << bitsPerCharacter) | code) & 0xFFFFFF; count = (count + 1) & 3; if (count == 0) { assert(outIndex + 3 <= output.length); output[outIndex++] = (bits >> 16) & eightBitMask; output[outIndex++] = (bits >> 8) & eightBitMask; output[outIndex++] = bits & eightBitMask; bits = 0; } continue; } else if (code == _padding && count > 1) { if (charOr < 0 || charOr > asciiMax) break; if (count == 3) { if ((bits & 0x03) != 0) { throw FormatException("Invalid encoding before padding", input, i); } output[outIndex++] = bits >> 10; output[outIndex++] = bits >> 2; } else { if ((bits & 0x0F) != 0) { throw FormatException("Invalid encoding before padding", input, i); } output[outIndex++] = bits >> 4; } // 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). var expectedPadding = (3 - count) * 3; if (char == _char_percent) expectedPadding += 2; state = _encodePaddingState(expectedPadding); return _checkPadding(input, i + 1, end, state); } throw FormatException("Invalid character", input, i); } if (charOr >= 0 && charOr <= asciiMax) { return _encodeCharacterState(count, bits); } // There is an invalid (non-ASCII) character in the input. int i; for (i = start; i < end; i++) { var char = input.codeUnitAt(i); if (char < 0 || char > asciiMax) break; } throw FormatException("Invalid character", input, i); } static Uint8List _emptyBuffer = Uint8List(0); /// Allocates a buffer with room for the decoding of a substring of [input]. /// /// Includes room for the characters in [state], and handles padding correctly. static Uint8List _allocateBuffer( String input, int start, int end, int state, ) { assert(state >= 0); var paddingStart = _trimPaddingChars(input, start, end); var length = _stateCount(state) + (paddingStart - start); // Three bytes per full four bytes in the input. var bufferLength = (length >> 2) * 3; // If padding was seen, then this is the last chunk, and the final partial // chunk should be decoded too. var remainderLength = length & 3; if (remainderLength != 0 && paddingStart < end) { bufferLength += remainderLength - 1; } if (bufferLength > 0) return Uint8List(bufferLength); // If the input plus state is less than four characters, and it's not // at the end of input, no buffer is needed. return _emptyBuffer; } /// 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 as 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. var padding = 0; var index = end; var newEnd = end; while (index > start && padding < 2) { index--; var 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. /// /// Valid padding is a correct number (0, 1 or 2) of `=` 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 /// 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 expect 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; var expectedPadding = _statePadding(state); assert(expectedPadding >= 0); assert(expectedPadding < 6); while (expectedPadding > 0) { var 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. var 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); } // Expects 'D' or 'd'. if ((char | 0x20) != _char_d) break; start++; expectedPadding--; if (start == end) break; } if (start != end) { throw FormatException("Invalid padding character", input, start); } return _encodePaddingState(expectedPadding); } } class _Base64DecoderSink extends StringConversionSink { /// Output sink final Sink> _sink; final _Base64Decoder _decoder = _Base64Decoder(); _Base64DecoderSink(this._sink); void add(String string) { if (string.isEmpty) return; var buffer = _decoder.decode(string, 0, string.length); if (buffer != null) _sink.add(buffer); } void close() { _decoder.close(null, null); _sink.close(); } void addSlice(String string, int start, int end, bool isLast) { RangeError.checkValidRange(start, end, string.length); if (start == end) return; var buffer = _decoder.decode(string, start, end); if (buffer != null) _sink.add(buffer); if (isLast) { _decoder.close(string, end); _sink.close(); } } }