diff --git a/lib/base64.js b/lib/base64.js index f65ae5ca..db73b0f2 100644 --- a/lib/base64.js +++ b/lib/base64.js @@ -43,8 +43,20 @@ function decode(base64) { } function encode(data) { - let bytes = new Uint8Array(data); - let byteCount = bytes.length; + var byteOffset = 0; + var buffer; + + if (data instanceof ArrayBuffer) { + buffer = data; + } else if (ArrayBuffer.isView(data)) { + buffer = data.buffer; + byteOffset = data.byteOffset; + } else { + throw new TypeError('Can only base64 encode ArrayBuffer and ArrayBufferView objects'); + } + + let byteCount = data.byteLength; + let bytes = new Uint8Array(buffer, byteOffset, byteCount); let base64 = ''; for (let i = 0; i < byteCount; i += 3) { @@ -59,7 +71,7 @@ function encode(data) { return base64.slice(0, -2) + '=='; case 2: return base64.slice(0, -1) + '='; + default: + return base64; } - - return base64; } diff --git a/lib/rpc.js b/lib/rpc.js index 4098e7ab..5d8380c3 100644 --- a/lib/rpc.js +++ b/lib/rpc.js @@ -125,9 +125,6 @@ function serialize(realmId, value) { } if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - if (value instanceof DataView) { - throw new Error('Unable to serialize a DataView object'); - } return {type: propTypes.DATA, value: base64.encode(value)}; } diff --git a/src/RJSObject.mm b/src/RJSObject.mm index 12fef342..84075f87 100644 --- a/src/RJSObject.mm +++ b/src/RJSObject.mm @@ -132,34 +132,41 @@ template<> JSValueRef RJSAccessor::from_string(JSContextRef ctx, StringData s) { template<> std::string RJSAccessor::to_binary(JSContextRef ctx, JSValueRef &val) { static JSStringRef arrayBufferString = JSStringCreateWithUTF8CString("ArrayBuffer"); - static JSStringRef dataViewString = JSStringCreateWithUTF8CString("DataView"); + static JSStringRef bufferString = JSStringCreateWithUTF8CString("buffer"); + static JSStringRef byteLengthString = JSStringCreateWithUTF8CString("byteLength"); + static JSStringRef byteOffsetString = JSStringCreateWithUTF8CString("byteOffset"); static JSStringRef isViewString = JSStringCreateWithUTF8CString("isView"); static JSStringRef uint8ArrayString = JSStringCreateWithUTF8CString("Uint8Array"); - switch (JSValueGetType(ctx, val)) { - case kJSTypeObject: { - // Allow value to be an ArrayBuffer instance. - JSObjectRef arrayBufferConstructor = RJSValidatedObjectProperty(ctx, JSContextGetGlobalObject(ctx), arrayBufferString); - if (JSValueIsInstanceOfConstructor(ctx, val, arrayBufferConstructor, NULL)) { - break; - } + JSObjectRef arrayBufferConstructor = RJSValidatedObjectProperty(ctx, JSContextGetGlobalObject(ctx), arrayBufferString); + JSObjectRef uint8ArrayContructor = RJSValidatedObjectProperty(ctx, JSContextGetGlobalObject(ctx), uint8ArrayString); + JSValueRef uint8ArrayArguments[3]; + size_t uint8ArrayArgumentsCount = 0; - // Check if value is a TypedArray by calling ArrayBuffer.isView(val), but *not* a DataView object. - JSObjectRef isViewMethod = RJSValidatedObjectProperty(ctx, arrayBufferConstructor, isViewString); - JSValueRef isView = JSObjectCallAsFunction(ctx, isViewMethod, arrayBufferConstructor, 1, &val, NULL); - if (isView && JSValueToBoolean(ctx, isView) && !RJSIsValueObjectOfType(ctx, val, dataViewString)) { - break; - } - // Fall through + // Value should either be an ArrayBuffer or ArrayBufferView (i.e. TypedArray or DataView). + if (JSValueIsInstanceOfConstructor(ctx, val, arrayBufferConstructor, NULL)) { + uint8ArrayArguments[0] = val; + uint8ArrayArgumentsCount = 1; + } + else if (JSObjectRef object = JSValueToObject(ctx, val, NULL)) { + // Check if value is an ArrayBufferView by calling ArrayBuffer.isView(val). + JSObjectRef isViewMethod = RJSValidatedObjectProperty(ctx, arrayBufferConstructor, isViewString); + JSValueRef isView = JSObjectCallAsFunction(ctx, isViewMethod, arrayBufferConstructor, 1, &val, NULL); + + if (isView && JSValueToBoolean(ctx, isView)) { + uint8ArrayArguments[0] = RJSValidatedObjectProperty(ctx, object, bufferString); + uint8ArrayArguments[1] = RJSValidatedPropertyValue(ctx, object, byteOffsetString); + uint8ArrayArguments[2] = RJSValidatedPropertyValue(ctx, object, byteLengthString); + uint8ArrayArgumentsCount = 3; } - default: - throw std::runtime_error("Can only convert ArrayBuffer and TypedArray objects to binary"); + } + + if (!uint8ArrayArgumentsCount) { + throw std::runtime_error("Can only convert ArrayBuffer and TypedArray objects to binary"); } JSValueRef exception = NULL; - JSObjectRef uint8ArrayContructor = RJSValidatedObjectProperty(ctx, JSContextGetGlobalObject(ctx), uint8ArrayString); - JSObjectRef uint8Array = JSObjectCallAsConstructor(ctx, uint8ArrayContructor, 1, &val, &exception); - + JSObjectRef uint8Array = JSObjectCallAsConstructor(ctx, uint8ArrayContructor, uint8ArrayArgumentsCount, uint8ArrayArguments, &exception); if (exception) { throw RJSException(ctx, exception); } diff --git a/tests/ObjectTests.js b/tests/ObjectTests.js index 8380a8e3..3b132a73 100644 --- a/tests/ObjectTests.js +++ b/tests/ObjectTests.js @@ -9,10 +9,9 @@ var BaseTest = require('./base-test'); var TestCase = require('./asserts'); var schemas = require('./schemas'); -// 31 random bytes (purposefully an odd number). var RANDOM_DATA = new Uint8Array([ - 0xd8, 0x21, 0xd6, 0xe8, 0x0c, 0x57, 0xbc, 0xb2, 0x6a, 0x15, 0x77, 0x30, 0xac, 0x77, 0x96, 0xd9, - 0x67, 0x1e, 0x40, 0xa7, 0x6d, 0x52, 0x83, 0xda, 0x07, 0x29, 0x9c, 0x70, 0x38, 0x48, 0x4e, + 0xd8, 0x21, 0xd6, 0xe8, 0x00, 0x57, 0xbc, 0xb2, 0x6a, 0x15, 0x77, 0x30, 0xac, 0x77, 0x96, 0xd9, + 0x67, 0x1e, 0x40, 0xa7, 0x6d, 0x52, 0x83, 0xda, 0x07, 0x29, 0x9c, 0x70, 0x38, 0x48, 0x4e, 0xff, ]); module.exports = BaseTest.extend({ @@ -273,6 +272,49 @@ module.exports = BaseTest.extend({ }); TestCase.assertArraysEqual(new Uint8Array(object.dataCol), RANDOM_DATA); + // Should be to set a data property to a DataView. + realm.write(function() { + object.dataCol = new DataView(RANDOM_DATA.buffer); + }); + TestCase.assertArraysEqual(new Uint8Array(object.dataCol), RANDOM_DATA); + + // Test that a variety of size and slices of data still work. + [ + [0, -1], + [0, -2], + [1, 0], + [1, -1], + [1, -2], + [2, 0], + [2, -1], + [2, -2], + ].forEach(function(range) { + var array = RANDOM_DATA.subarray(range[0], range[1]); + realm.write(function() { + // Use a partial "view" of the underlying ArrayBuffer. + object.dataCol = new Uint8Array(RANDOM_DATA.buffer, range[0], array.length); + }); + TestCase.assertArraysEqual(new Uint8Array(object.dataCol), array, range.join('...')); + }); + + // Test other TypedArrays to make sure they all work for setting data properties. + [ + Int8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + ].forEach(function(TypedArray) { + var array = new TypedArray(RANDOM_DATA.buffer); + realm.write(function() { + object.dataCol = array; + }); + TestCase.assertArraysEqual(new TypedArray(object.dataCol), array, TypedArray.name); + }); + realm.write(function() { TestCase.assertThrows(function() { object.dataCol = true; @@ -286,9 +328,6 @@ module.exports = BaseTest.extend({ TestCase.assertThrows(function() { object.dataCol = [1]; }); - TestCase.assertThrows(function() { - object.dataCol = new DataView(RANDOM_DATA.buffer); - }); }); } });