// 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. import 'dart:convert'; import '../charcodes.dart'; /// The canonical instance of [PercentEncoder]. const percentEncoder = PercentEncoder._(); /// A converter that encodes byte arrays into percent-encoded strings. /// /// Encodes all bytes other than ASCII letters, decimal digits, or one /// of `-._~`. This matches the behavior of [Uri.encodeQueryComponent] except /// that it doesn't encode `0x20` bytes to the `+` character. /// /// This will throw a [RangeError] if the byte array has any digits that don't /// fit in the gamut of a byte. class PercentEncoder extends Converter, String> { const PercentEncoder._(); @override String convert(List input) => _convert(input, 0, input.length); @override ByteConversionSink startChunkedConversion(Sink sink) => _PercentEncoderSink(sink); } /// A conversion sink for chunked percentadecimal encoding. class _PercentEncoderSink extends ByteConversionSinkBase { /// The underlying sink to which decoded byte arrays will be passed. final Sink _sink; _PercentEncoderSink(this._sink); @override void add(List chunk) { _sink.add(_convert(chunk, 0, chunk.length)); } @override void addSlice(List chunk, int start, int end, bool isLast) { RangeError.checkValidRange(start, end, chunk.length); _sink.add(_convert(chunk, start, end)); if (isLast) _sink.close(); } @override void close() { _sink.close(); } } String _convert(List bytes, int start, int end) { var buffer = StringBuffer(); // A bitwise OR of all bytes in [bytes]. This allows us to check for // out-of-range bytes without adding more branches than necessary to the // core loop. var byteOr = 0; for (var i = start; i < end; i++) { var byte = bytes[i]; byteOr |= byte; // If the byte is an uppercase letter, convert it to lowercase to check if // it's unreserved. This works because uppercase letters in ASCII are // exactly `0b100000 = 0x20` less than lowercase letters, so if we ensure // that that bit is 1 we ensure that the letter is lowercase. var letter = 0x20 | byte; if ((letter >= $a && letter <= $z) || (byte >= $0 && byte <= $9) || byte == $dash || byte == $dot || byte == $underscore || byte == $tilde) { // Unreserved characters are safe to write as-is. buffer.writeCharCode(byte); continue; } buffer.writeCharCode($percent); // The bitwise arithmetic here is equivalent to `byte ~/ 16` and `byte % 16` // for valid byte values, but is easier for dart2js to optimize given that // it can't prove that [byte] will always be positive. buffer.writeCharCode(_codeUnitForDigit((byte & 0xF0) >> 4)); buffer.writeCharCode(_codeUnitForDigit(byte & 0x0F)); } if (byteOr >= 0 && byteOr <= 255) return buffer.toString(); // If there was an invalid byte, find it and throw an exception. for (var i = start; i < end; i++) { var byte = bytes[i]; if (byte >= 0 && byte <= 0xff) continue; throw FormatException( "Invalid byte ${byte < 0 ? "-" : ""}0x${byte.abs().toRadixString(16)}.", bytes, i); } throw StateError('unreachable'); } /// Returns the ASCII/Unicode code unit corresponding to the hexadecimal digit /// [digit]. int _codeUnitForDigit(int digit) => digit < 10 ? digit + $0 : digit + $A - 10;