// 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<int> bytes) => base64.encode(bytes);

/// Encodes [bytes] using [base64url](https://tools.ietf.org/html/rfc4648) encoding.
///
/// Shorthand for `base64url.encode(bytes)`.
String base64UrlEncode(List<int> 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<List<int>, 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<List<int>, String> {
  final bool _urlSafe;

  const Base64Encoder() : _urlSafe = false;
  const Base64Encoder.urlSafe() : _urlSafe = true;

  String convert(List<int> 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<String> 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<int> 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<int> 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<int> source) {
    _add(source, 0, source.length, false);
  }

  void close() {
    _add(const [], 0, 0, true);
  }

  void addSlice(List<int> 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<int> source, int start, int end, bool isLast);
}

class _AsciiBase64EncoderSink extends _Base64EncoderSink {
  final Sink<String> _sink;
  final _Base64Encoder _encoder;

  _AsciiBase64EncoderSink(this._sink, bool urlSafe)
    : _encoder = _BufferCachingBase64Encoder(urlSafe);

  void _add(List<int> 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<int> 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<String, List<int>> {
  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<List<int>> 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<int> _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<List<int>> _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();
    }
  }
}
