Remove _HttpUtils from dart:io.

The Uri rewrite had all but one of these functions public available, so
they are used insted. The decodeHttpEntityString was only used by
HttpMultipartFormData, so moved to that class.

The function parseCookieDate was moved to HttpDate as a private
function. Renamed the file http_utils.dart to http_date.dart to match the content.

BUG=
R=lrn@google.com, whesse@google.com

Review URL: https://codereview.chromium.org//18576006

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@24783 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
ajohnsen@google.com 2013-07-05 09:51:39 +00:00
parent 658ea93cec
commit 10996fd256
12 changed files with 99 additions and 263 deletions

View file

@ -769,8 +769,17 @@ class Uri {
return _uriDecode(encodedComponent);
}
static String decodeQueryComponent(String encodedComponent) {
return _uriDecode(encodedComponent, plusToSpace: true);
/**
* Decodes the percent-encoding in [encodedComponent], converting
* pluses to spaces.
*
* It will create a byte-list of the decoded characters, and then use
* [decode] to decode the byte-list to a String. Default is a UTF_8 decoder.
*/
static String decodeQueryComponent(
String encodedComponent,
{String decode(List<int> bytes): decodeUtf8}) {
return _uriDecode(encodedComponent, plusToSpace: true, decode: decode);
}
/**
@ -809,8 +818,13 @@ class Uri {
*
* Keys in the query string that have no value are mapped to the
* empty string.
*
* Each query component will be decoded using [decode]. Default is a UTF_8
* decoder.
*/
static Map<String, String> splitQueryString(String query) {
static Map<String, String> splitQueryString(
String query,
{String decode(List<int> bytes): decodeUtf8}) {
return query.split("&").fold({}, (map, element) {
int index = element.indexOf("=");
if (index == -1) {
@ -818,7 +832,8 @@ class Uri {
} else if (index != 0) {
var key = element.substring(0, index);
var value = element.substring(index + 1);
map[Uri.decodeQueryComponent(key)] = decodeQueryComponent(value);
map[Uri.decodeQueryComponent(key, decode: decode)] =
decodeQueryComponent(value, decode: decode);
}
return map;
});
@ -901,10 +916,20 @@ class Uri {
}
/**
* A JavaScript-like decodeURI function. It unescapes the string [text] and
* returns the unescaped string.
* Uri-decode a percent-encoded string.
*
* It unescapes the string [text] and returns the unescaped string.
*
* This function is similar to the JavaScript-function `decodeURI`.
*
* If [plusToSpace] is `true`, plus characters will be converted to spaces.
*
* The decoder will create a byte-list of the percent-encoded parts, and then
* decode the byte-list using [decode]. Default is a UTF_8 decoder.
*/
static String _uriDecode(String text, {bool plusToSpace: false}) {
static String _uriDecode(String text,
{bool plusToSpace: false,
String decode(List<int> bytes): decodeUtf8}) {
StringBuffer result = new StringBuffer();
List<int> codepoints = new List<int>();
for (int i = 0; i < text.length;) {
@ -927,7 +952,7 @@ class Uri {
if (i == text.length) break;
ch = text.codeUnitAt(i);
}
result.write(decodeUtf8(codepoints));
result.write(decode(codepoints));
}
}
return result.toString();

View file

@ -120,8 +120,8 @@ class _HttpBodyHandler {
case "x-www-form-urlencoded":
return asText(Encoding.ASCII)
.then((body) {
var map = _HttpUtils.splitQueryString(
body.body, encoding: defaultEncoding);
var map = Uri.splitQueryString(body.body,
decode: (s) => _decodeString(s, defaultEncoding));
var result = {};
for (var key in map.keys) {
result[key] = map[key];

View file

@ -202,114 +202,9 @@ class HttpDate {
expectEnd();
return new DateTime.utc(year, month + 1, day, hours, minutes, seconds, 0);
}
}
class _HttpUtils {
static String decodeUrlEncodedString(
String urlEncoded,
{Encoding encoding: Encoding.UTF_8}) {
// First check the string for any encoding.
int index = 0;
bool encoded = false;
while (!encoded && index < urlEncoded.length) {
encoded = urlEncoded[index] == "+" || urlEncoded[index] == "%";
index++;
}
if (!encoded) return urlEncoded;
index--;
// Start decoding from the first encoded character.
List<int> bytes = new List<int>();
for (int i = 0; i < index; i++) bytes.add(urlEncoded.codeUnitAt(i));
for (int i = index; i < urlEncoded.length; i++) {
if (urlEncoded[i] == "+") {
bytes.add(32);
} else if (urlEncoded[i] == "%") {
if (urlEncoded.length - i < 2) {
throw new HttpException("Invalid URL encoding");
}
int byte = 0;
for (int j = 0; j < 2; j++) {
var charCode = urlEncoded.codeUnitAt(i + j + 1);
if (0x30 <= charCode && charCode <= 0x39) {
byte = byte * 16 + charCode - 0x30;
} else {
// Check ranges A-F (0x41-0x46) and a-f (0x61-0x66).
charCode |= 0x20;
if (0x61 <= charCode && charCode <= 0x66) {
byte = byte * 16 + charCode - 0x57;
} else {
throw new ArgumentError("Invalid URL encoding");
}
}
}
bytes.add(byte);
i += 2;
} else {
bytes.add(urlEncoded.codeUnitAt(i));
}
}
return _decodeString(bytes, encoding);
}
static Map<String, String> splitQueryString(
String queryString,
{Encoding encoding: Encoding.UTF_8}) {
Map<String, String> result = new Map<String, String>();
int currentPosition = 0;
int length = queryString.length;
while (currentPosition < length) {
// Find the first equals character between current position and
// the provided end.
int indexOfEquals(int end) {
int index = currentPosition;
while (index < end) {
if (queryString.codeUnitAt(index) == _CharCode.EQUAL) return index;
index++;
}
return -1;
}
// Find the next separator (either & or ;), see
// http://www.w3.org/TR/REC-html40/appendix/notes.html#ampersands-in-uris
// relating the ; separator. If no separator is found returns
// the length of the query string.
int indexOfSeparator() {
int end = length;
int index = currentPosition;
while (index < end) {
int codeUnit = queryString.codeUnitAt(index);
if (codeUnit == _CharCode.AMPERSAND ||
codeUnit == _CharCode.SEMI_COLON) {
return index;
}
index++;
}
return end;
}
int seppos = indexOfSeparator();
int equalspos = indexOfEquals(seppos);
String name;
String value;
if (equalspos == -1) {
name = queryString.substring(currentPosition, seppos);
value = '';
} else {
name = queryString.substring(currentPosition, equalspos);
value = queryString.substring(equalspos + 1, seppos);
}
currentPosition = seppos + 1; // This also works when seppos == length.
if (name == '') continue;
result[_HttpUtils.decodeUrlEncodedString(name, encoding: encoding)] =
_HttpUtils.decodeUrlEncodedString(value, encoding: encoding);
}
return result;
}
static DateTime parseCookieDate(String date) {
// Parse a cookie date string.
static DateTime _parseCookieDate(String date) {
const List monthsLowerCase = const ["jan", "feb", "mar", "apr", "may",
"jun", "jul", "aug", "sep", "oct",
"nov", "dec"];
@ -418,41 +313,4 @@ class _HttpUtils {
return new DateTime.utc(year, month, dayOfMonth, hour, minute, second, 0);
}
// Decode a string with HTTP entities. Returns null if the string ends in the
// middle of a http entity.
static String decodeHttpEntityString(String input) {
int amp = input.lastIndexOf('&');
if (amp < 0) return input;
int end = input.lastIndexOf(';');
if (end < amp) return null;
var buffer = new StringBuffer();
int offset = 0;
parse(amp, end) {
switch (input[amp + 1]) {
case '#':
if (input[amp + 2] == 'x') {
buffer.writeCharCode(
int.parse(input.substring(amp + 3, end), radix: 16));
} else {
buffer.writeCharCode(int.parse(input.substring(amp + 2, end)));
}
break;
default:
throw new HttpException('Unhandled HTTP entity token');
}
}
while ((amp = input.indexOf('&', offset)) >= 0) {
buffer.write(input.substring(offset, amp));
int end = input.indexOf(';', amp);
parse(amp, end);
offset = end + 1;
}
buffer.write(input.substring(offset));
return buffer.toString();
}
}

View file

@ -716,7 +716,7 @@ class _Cookie implements Cookie {
value = parseAttributeValue();
}
if (name == "expires") {
expires = _HttpUtils.parseCookieDate(value);
expires = HttpDate._parseCookieDate(value);
} else if (name == "max-age") {
maxAge = int.parse(value);
} else if (name == "domain") {

View file

@ -41,9 +41,9 @@ class _HttpMultipartFormData extends Stream implements HttpMultipartFormData {
.transform(new StringDecoder(encoding))
.expand((data) {
buffer.write(data);
var out = _HttpUtils.decodeHttpEntityString(buffer.toString());
var out = _decodeHttpEntityString(buffer.toString());
if (out != null) {
buffer = new StringBuffer();
buffer.clear();
return [out];
}
return const [];
@ -98,5 +98,42 @@ class _HttpMultipartFormData extends Stream implements HttpMultipartFormData {
String value(String name) {
return _mimeMultipart.headers[name];
}
// Decode a string with HTTP entities. Returns null if the string ends in the
// middle of a http entity.
static String _decodeHttpEntityString(String input) {
int amp = input.lastIndexOf('&');
if (amp < 0) return input;
int end = input.lastIndexOf(';');
if (end < amp) return null;
var buffer = new StringBuffer();
int offset = 0;
parse(amp, end) {
switch (input[amp + 1]) {
case '#':
if (input[amp + 2] == 'x') {
buffer.writeCharCode(
int.parse(input.substring(amp + 3, end), radix: 16));
} else {
buffer.writeCharCode(int.parse(input.substring(amp + 2, end)));
}
break;
default:
throw new HttpException('Unhandled HTTP entity token');
}
}
while ((amp = input.indexOf('&', offset)) >= 0) {
buffer.write(input.substring(offset, amp));
int end = input.indexOf(';', amp);
parse(amp, end);
offset = end + 1;
}
buffer.write(input.substring(offset));
return buffer.toString();
}
}

View file

@ -36,13 +36,13 @@ part 'file_system_entity.dart';
part 'http.dart';
part 'http_body.dart';
part 'http_body_impl.dart';
part 'http_date.dart';
part 'http_headers.dart';
part 'http_impl.dart';
part 'http_multipart_form_data.dart';
part 'http_multipart_form_data_impl.dart';
part 'http_parser.dart';
part 'http_session.dart';
part 'http_utils.dart';
part 'io_sink.dart';
part 'link.dart';
part 'mime_multipart_parser.dart';

View file

@ -17,13 +17,13 @@
'http.dart',
'http_body.dart',
'http_body_impl.dart',
'http_date.dart',
'http_headers.dart',
'http_impl.dart',
'http_multipart_form_data.dart',
'http_multipart_form_data_impl.dart',
'http_parser.dart',
'http_session.dart',
'http_utils.dart',
'io_sink.dart',
'link.dart',
'mime_multipart_parser.dart',

View file

@ -255,7 +255,22 @@ File content\r
test('application/x-www-form-urlencoded',
'a=%F8+%26%23548%3B'.codeUnits,
{ 'a' : '\u{FFFD}&#548;' },
{ 'a' : '\u{FFFD} &#548;' },
"form");
test('application/x-www-form-urlencoded',
'a=%C0%A0'.codeUnits,
{ 'a' : '\u{FFFD}' },
"form");
test('application/x-www-form-urlencoded',
'a=x%A0x'.codeUnits,
{ 'a' : 'x\u{FFFD}x' },
"form");
test('application/x-www-form-urlencoded',
'a=x%C0x'.codeUnits,
{ 'a' : 'x\u{FFFD}x' },
"form");
test('application/x-www-form-urlencoded',

View file

@ -12,11 +12,11 @@ part "../../../sdk/lib/io/io_sink.dart";
part "../../../sdk/lib/io/http.dart";
part "../../../sdk/lib/io/http_impl.dart";
part "../../../sdk/lib/io/http_parser.dart";
part "../../../sdk/lib/io/http_utils.dart";
part "../../../sdk/lib/io/http_date.dart";
part "../../../sdk/lib/io/socket.dart";
void testParseHttpCookieDate() {
Expect.throws(() => _HttpUtils.parseCookieDate(""));
Expect.throws(() => HttpDate._parseCookieDate(""));
test(int year,
int month,
@ -26,7 +26,7 @@ void testParseHttpCookieDate() {
int seconds,
String formatted) {
DateTime date = new DateTime.utc(year, month, day, hours, minutes, seconds, 0);
Expect.equals(date, _HttpUtils.parseCookieDate(formatted));
Expect.equals(date, HttpDate._parseCookieDate(formatted));
}
test(2012, DateTime.JUNE, 19, 14, 15, 01, "tue, 19-jun-12 14:15:01 gmt");

View file

@ -10,10 +10,10 @@ import 'dart:collection';
part '../../../sdk/lib/io/common.dart';
part "../../../sdk/lib/io/io_sink.dart";
part "../../../sdk/lib/io/http.dart";
part "../../../sdk/lib/io/http_date.dart";
part "../../../sdk/lib/io/http_headers.dart";
part "../../../sdk/lib/io/http_impl.dart";
part "../../../sdk/lib/io/http_parser.dart";
part "../../../sdk/lib/io/http_utils.dart";
part "../../../sdk/lib/io/socket.dart";
void testMultiValue() {

View file

@ -1,96 +0,0 @@
// Copyright (c) 2013, 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 "package:expect/expect.dart";
import "dart:async";
import "dart:utf";
import 'dart:collection';
part '../../../sdk/lib/io/common.dart';
part '../../../sdk/lib/io/io_sink.dart';
part "../../../sdk/lib/io/http.dart";
part "../../../sdk/lib/io/http_impl.dart";
part "../../../sdk/lib/io/http_parser.dart";
part "../../../sdk/lib/io/http_utils.dart";
part "../../../sdk/lib/io/socket.dart";
part "../../../sdk/lib/io/string_transformer.dart";
void testParseEncodedString() {
String encodedString = 'foo+bar%20foobar%25%26';
Expect.equals(_HttpUtils.decodeUrlEncodedString(encodedString),
'foo bar foobar%&');
encodedString = 'A+%2B+B';
Expect.equals(_HttpUtils.decodeUrlEncodedString(encodedString),
'A + B');
}
void testParseQueryString() {
test(String queryString, Map<String, String> expected) {
Map<String, String> map = _HttpUtils.splitQueryString(queryString);
for (String key in map.keys) {
Expect.equals(expected[key], map[key]);
}
Expect.setEquals(expected.keys.toSet(), map.keys.toSet());
}
// The query string includes escaped "?"s, "&"s, "%"s and "="s.
// These should not affect the splitting of the string.
test('%3F=%3D&foo=bar&%26=%25&sqrt2=%E2%88%9A2&name=Franti%C5%A1ek',
{ '&' : '%',
'foo' : 'bar',
'?' : '=',
'sqrt2' : '\u221A2',
'name' : 'Franti\u0161ek'});
// Same query string with ; as separator.
test('%3F=%3D;foo=bar;%26=%25;sqrt2=%E2%88%9A2;name=Franti%C5%A1ek',
{ '&' : '%',
'foo' : 'bar',
'?' : '=',
'sqrt2' : '\u221A2',
'name' : 'Franti\u0161ek'});
// Same query string with alternating ; and & separators.
test('%3F=%3D&foo=bar;%26=%25&sqrt2=%E2%88%9A2;name=Franti%C5%A1ek',
{ '&' : '%',
'foo' : 'bar',
'?' : '=',
'sqrt2' : '\u221A2',
'name' : 'Franti\u0161ek'});
test('%3F=%3D;foo=bar&%26=%25;sqrt2=%E2%88%9A2&name=Franti%C5%A1ek',
{ '&' : '%',
'foo' : 'bar',
'?' : '=',
'sqrt2' : '\u221A2',
'name' : 'Franti\u0161ek'});
// Corner case tests.
test('', { });
test('&', { });
test(';', { });
test('&;', { });
test(';&', { });
test('&&&&', { });
test(';;;;', { });
test('a', { 'a' : '' });
test('&a&', { 'a' : '' });
test(';a;', { 'a' : '' });
test('a=', { 'a' : '' });
test('a=&', { 'a' : '' });
test('a=;', { 'a' : '' });
test('a=&b', { 'a' : '', 'b' : '' });
test('a=;b', { 'a' : '', 'b' : '' });
test('a=&b', { 'a' : '', 'b' : '' });
test('a=&b=', { 'a' : '', 'b' : '' });
// These are not really a legal query string.
test('=', { });
test('=x', { });
test('a==&b===', { 'a' : '=', 'b' : '==' });
}
void main() {
testParseEncodedString();
testParseQueryString();
}

View file

@ -67,7 +67,6 @@ io/file_constructor_test: fail
# Dart analyzer spots the misuse of 'part' directives in these unit tests.
io/http_headers_test: Skip
io/http_cookie_date_test: Skip
io/url_encoding_test: Skip
io/http_parser_test: Skip
io/web_socket_protocol_processor_test: Skip
@ -86,7 +85,6 @@ io/file_constructor_test: fail
# Dart analyzer spots the misuse of 'part' directives in these unit tests.
io/http_headers_test: Skip
io/http_cookie_date_test: Skip
io/url_encoding_test: Skip
io/http_parser_test: Skip
io/web_socket_protocol_processor_test: Skip
@ -103,7 +101,6 @@ typed_array_int64_uint64_test: Skip # dart:typed_data on dart2js does not suppor
float_array_test: Skip # dart:typed_data support needed.
int_array_test: Skip # dart:typed_data support needed.
io/web_socket_protocol_processor_test: Skip # Importing code with external keyword
io/url_encoding_test: Skip # Importing code with external keyword
int_array_load_elimination_test: Skip # This is a VM test
medium_integer_test: Fail, OK # Test fails with JS number semantics: issue 1533.
io/process_exit_negative_test: Fail, OK # relies on a static error that is a warning now.