Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 126 additions & 27 deletions Libraries/Network/XMLHttpRequestBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,28 @@

var RCTNetworking = require('RCTNetworking');
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
var invariant = require('fbjs/lib/invariant');
const invariant = require('fbjs/lib/invariant');
const utf8 = require('utf8');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How big is this library?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind -_- you implemented it below.

const warning = require('fbjs/lib/warning');

type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
type Response = ?Object | string;

const UNSENT = 0;
const OPENED = 1;
const HEADERS_RECEIVED = 2;
const LOADING = 3;
const DONE = 4;

const SUPPORTED_RESPONSE_TYPES = {
arraybuffer: typeof global.ArrayBuffer === 'function',
blob: typeof global.Blob === 'function',
document: false,
json: true,
text: true,
'': true,
};

/**
* Shared base for platform-specific XMLHttpRequest implementations.
*/
Expand All @@ -43,9 +57,7 @@ class XMLHttpRequestBase {
upload: any;
readyState: number;
responseHeaders: ?Object;
responseText: ?string;
response: ?string;
responseType: '' | 'text';
responseText: string;
status: number;
timeout: number;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get/set properties not yet supported

responseURL: ?string;
Expand All @@ -57,12 +69,16 @@ class XMLHttpRequestBase {
_requestId: ?number;
_subscriptions: [any];

_method: ?string;
_url: ?string;
_headers: Object;
_sent: boolean;
_aborted: boolean;
_cachedResponse: Response;
_hasError: boolean;
_headers: Object;
_lowerCaseResponseHeaders: Object;
_method: ?string;
_response: string | ?Object;
_responseType: ResponseType;
_sent: boolean;
_url: ?string;

constructor() {
this.UNSENT = UNSENT;
Expand All @@ -82,24 +98,101 @@ class XMLHttpRequestBase {
this._aborted = false;
}

_reset() {
_reset(): void {
this.readyState = this.UNSENT;
this.responseHeaders = undefined;
this.responseText = '';
this.response = null;
this.responseType = '';
this.status = 0;
delete this.responseURL;

this._requestId = null;

this._cachedResponse = undefined;
this._hasError = false;
this._headers = {};
this._responseType = '';
this._sent = false;
this._lowerCaseResponseHeaders = {};

this._clearSubscriptions();
}

// $FlowIssue #10784535
get responseType(): ResponseType {
return this._responseType;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get/set properties not yet supported

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pity

}

// $FlowIssue #10784535
set responseType(responseType: ResponseType): void {
if (this.readyState > HEADERS_RECEIVED) {
throw new Error(
"Failed to set the 'responseType' property on 'XMLHttpRequest': The " +
"response type cannot be set if the object's state is LOADING or DONE"
);
}
if (!SUPPORTED_RESPONSE_TYPES.hasOwnProperty(responseType)) {
warning(
`The provided value '${responseType}' is not a valid 'responseType'.`);
return;
}

// redboxes early, e.g. for 'arraybuffer' on ios 7
invariant(
SUPPORTED_RESPONSE_TYPES[responseType] || responseType === 'document',
`The provided value '${responseType}' is unsupported in this environment.`
);
this._responseType = responseType;
}

// $FlowIssue #10784535
get response(): Response {
const {responseType} = this;
if (responseType === '' || responseType === 'text') {
return this.readyState < LOADING || this._hasError
? ''
: this.responseText;
}

if (this.readyState !== DONE) {
return null;
}

if (this._cachedResponse !== undefined) {
return this._cachedResponse;
}

switch (this.responseType) {
case 'document':
this._cachedResponse = null;
break;

case 'arraybuffer':
this._cachedResponse = toArrayBuffer(
this.responseText, this.getResponseHeader('content-type') || '');
break;

case 'blob':
this._cachedResponse = new global.Blob(
[this.responseText],
{type: this.getResponseHeader('content-type') || ''}
);
break;

case 'json':
try {
this._cachedResponse = JSON.parse(this.responseText);
} catch (_) {
this._cachedResponse = null;
}
break;

default:
this._cachedResponse = null;
}

return this._cachedResponse;
}

didCreateRequest(requestId: number): void {
this._requestId = requestId;
this._subscriptions.push(RCTDeviceEventEmitter.addListener(
Expand Down Expand Up @@ -151,22 +244,7 @@ class XMLHttpRequestBase {
} else {
this.responseText += responseText;
}
switch(this.responseType) {
case '':
case 'text':
this.response = this.responseText;
break;
case 'blob': // whatwg-fetch sets this in Chrome
/* global Blob: true */
invariant(
typeof Blob === 'function',
`responseType "blob" is only supported on platforms with native Blob support`
);
this.response = new Blob([this.responseText]);
break;
default: //TODO: Support other types, eg: document, arraybuffer, json
invariant(false, `responseType "${this.responseType}" is unsupported`);
}
this._cachedResponse = undefined; // force lazy recomputation
this.setReadyState(this.LOADING);
}
}
Expand All @@ -175,6 +253,7 @@ class XMLHttpRequestBase {
if (requestId === this._requestId) {
if (error) {
this.responseText = error;
this._hasError = true;
}
this._clearSubscriptions();
this._requestId = null;
Expand Down Expand Up @@ -304,4 +383,24 @@ XMLHttpRequestBase.HEADERS_RECEIVED = HEADERS_RECEIVED;
XMLHttpRequestBase.LOADING = LOADING;
XMLHttpRequestBase.DONE = DONE;

function toArrayBuffer(text: string, contentType: string): ArrayBuffer {
const {length} = text;
if (length === 0) {
return new ArrayBuffer(0);
}

const charsetMatch = contentType.match(/;\s*charset=([^;]*)/i);
const charset = charsetMatch ? charsetMatch[1].trim() : 'utf-8';

if (/^utf-?8$/i.test(charset)) {
return utf8.encode(text);
} else { //TODO: utf16 / ucs2 / utf32
const array = new Uint8Array(length);
for (let i = 0; i < length; i++) {
array[i] = text.charCodeAt(i); // Uint8Array automatically masks with 0xff
}
return array.buffer;
}
}

module.exports = XMLHttpRequestBase;
63 changes: 63 additions & 0 deletions Libraries/Utilities/__tests__/utf8-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

'use strict';

jest.autoMockOff();

const {encode} = require('../utf8');

describe('UTF-8 encoding:', () => {
it('can encode code points < U+80', () => {
const arrayBuffer = encode('\u0000abcDEF\u007f');
expect(new Uint8Array(arrayBuffer)).toEqual(
new Uint8Array([0x00, 0x61, 0x62, 0x63, 0x44, 0x45, 0x46, 0x7f]));
});

it('can encode code points < U+800', () => {
const arrayBuffer = encode('\u0080\u0548\u07ff');
expect(new Uint8Array(arrayBuffer)).toEqual(
new Uint8Array([0xc2, 0x80, 0xd5, 0x88, 0xdf, 0xbf]));
});

it('can encode code points < U+10000', () => {
const arrayBuffer = encode('\u0800\uac48\uffff');
expect(new Uint8Array(arrayBuffer)).toEqual(
new Uint8Array([0xe0, 0xa0, 0x80, 0xea, 0xb1, 0x88, 0xef, 0xbf, 0xbf]));
});

it('can encode code points in the Supplementary Planes (surrogate pairs)', () => {
const arrayBuffer = encode([
'\ud800\udc00',
'\ud800\ude89',
'\ud83d\ude3b',
'\udbff\udfff'
].join(''));
expect(new Uint8Array(arrayBuffer)).toEqual(
new Uint8Array([
0xf0, 0x90, 0x80, 0x80,
0xf0, 0x90, 0x8a, 0x89,
0xf0, 0x9f, 0x98, 0xbb,
0xf4, 0x8f, 0xbf, 0xbf,
])
);
});

it('allows for stray high surrogates', () => {
const arrayBuffer = encode('a\ud8c6b');
expect(new Uint8Array(arrayBuffer)).toEqual(
new Uint8Array([0x61, 0xed, 0xa3, 0x86, 0x62]));
});

it('allows for stray low surrogates', () => {
const arrayBuffer = encode('a\ude19b');
expect(new Uint8Array(arrayBuffer)).toEqual(
new Uint8Array([0x61, 0xed, 0xb8, 0x99, 0x62]));
});
});
91 changes: 91 additions & 0 deletions Libraries/Utilities/utf8.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule utf8
* @flow
*/
'use strict';

class ByteVector {
_storage: Uint8Array;
_sizeWritten: number;

constructor(size) {
this._storage = new Uint8Array(size);
this._sizeWritten = 0;
}

push(value: number): ByteVector {
const i = this._sizeWritten;
if (i === this._storage.length) {
this._realloc();
}
this._storage[i] = value;
this._sizeWritten = i + 1;
return this;
}

getBuffer(): ArrayBuffer {
return this._storage.buffer.slice(0, this._sizeWritten);
}

_realloc() {
const storage = this._storage;
this._storage = new Uint8Array(align(storage.length * 1.5));
this._storage.set(storage);
}
}

/*eslint-disable no-bitwise */
exports.encode = (string: string): ArrayBuffer => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something here, but doing utf8 ourself doesn't seem like something we should do.

Like the #1 rule of crypto "don't roll your own", I think this applies to string encoding too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the complexity of crypto and character encodings is very different. I have been looking for a compact utf-8 encoding lib, but didn’t find anything that was compact and solid at the same time. My implementation adds ~1KB when minified.

Do you have any specific concerns? We wouldn’t need to do this if we could pass the binary response data from native to JS wrapped into a host object we’d implement. That host object could pass of character encoding to a system service. I have been looking into JSC’s API for that, but as long as we pass messages as JSON, we can’t do it.

const {length} = string;
const bytes = new ByteVector(length);

// each character / char code is assumed to represent an UTF-16 wchar.
// With the notable exception of surrogate pairs, each wchar represents the
// corresponding unicode code point.
// For an explanation of UTF-8 encoding, read [1]
// For an explanation of UTF-16 surrogate pairs, read [2]
//
// [1] https://en.wikipedia.org/wiki/UTF-8#Description
// [2] https://en.wikipedia.org/wiki/UTF-16#U.2B10000_to_U.2B10FFFF
let nextCodePoint = string.charCodeAt(0);
for (let i = 0; i < length; i++) {
let codePoint = nextCodePoint;
nextCodePoint = string.charCodeAt(i + 1);

if (codePoint < 0x80) {
bytes.push(codePoint);
} else if (codePoint < 0x800) {
bytes
.push(0xc0 | codePoint >>> 6)
.push(0x80 | codePoint & 0x3f);
} else if (codePoint >>> 10 === 0x36 && nextCodePoint >>> 10 === 0x37) { // high surrogate & low surrogate
codePoint = 0x10000 + (((codePoint & 0x3ff) << 10) | (nextCodePoint & 0x3ff));
bytes
.push(0xf0 | codePoint >>> 18 & 0x7)
.push(0x80 | codePoint >>> 12 & 0x3f)
.push(0x80 | codePoint >>> 6 & 0x3f)
.push(0x80 | codePoint & 0x3f);

i += 1;
nextCodePoint = string.charCodeAt(i + 1);
} else {
bytes
.push(0xe0 | codePoint >>> 12)
.push(0x80 | codePoint >>> 6 & 0x3f)
.push(0x80 | codePoint & 0x3f);
}
}
return bytes.getBuffer();
};

// align to multiples of 8 bytes
function align(size: number): number {
return size % 8 ? (Math.floor(size / 8) + 1) << 3 : size;
}