normalize method

String normalize (String source, [ int start = 0, int end ])

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).

Implementation

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();
        buffer.write(source.substring(sliceStart, sliceEnd));
        buffer.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;
}