mirror of
https://github.com/servo/servo.git
synced 2025-07-01 12:33:40 +01:00
543 lines
17 KiB
JavaScript
543 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
// This polyfill library implements the WebUSB Test API as specified here:
|
|
// https://wicg.github.io/webusb/test/
|
|
|
|
(() => {
|
|
|
|
// These variables are logically members of the USBTest class but are defined
|
|
// here to hide them from being visible as fields of navigator.usb.test.
|
|
let internal = {
|
|
intialized: false,
|
|
|
|
deviceManager: null,
|
|
deviceManagerInterceptor: null,
|
|
deviceManagerCrossFrameProxy: null,
|
|
|
|
chooser: null,
|
|
chooserInterceptor: null,
|
|
chooserCrossFrameProxy: null,
|
|
};
|
|
|
|
// Converts an ECMAScript String object to an instance of
|
|
// mojo_base.mojom.String16.
|
|
function mojoString16ToString(string16) {
|
|
return String.fromCharCode.apply(null, string16.data);
|
|
}
|
|
|
|
// Converts an instance of mojo_base.mojom.String16 to an ECMAScript String.
|
|
function stringToMojoString16(string) {
|
|
let array = new Array(string.length);
|
|
for (var i = 0; i < string.length; ++i) {
|
|
array[i] = string.charCodeAt(i);
|
|
}
|
|
return { data: array }
|
|
}
|
|
|
|
function fakeDeviceInitToDeviceInfo(guid, init) {
|
|
let deviceInfo = {
|
|
guid: guid + "",
|
|
usbVersionMajor: init.usbVersionMajor,
|
|
usbVersionMinor: init.usbVersionMinor,
|
|
usbVersionSubminor: init.usbVersionSubminor,
|
|
classCode: init.deviceClass,
|
|
subclassCode: init.deviceSubclass,
|
|
protocolCode: init.deviceProtocol,
|
|
vendorId: init.vendorId,
|
|
productId: init.productId,
|
|
deviceVersionMajor: init.deviceVersionMajor,
|
|
deviceVersionMinor: init.deviceVersionMinor,
|
|
deviceVersionSubminor: init.deviceVersionSubminor,
|
|
manufacturerName: stringToMojoString16(init.manufacturerName),
|
|
productName: stringToMojoString16(init.productName),
|
|
serialNumber: stringToMojoString16(init.serialNumber),
|
|
activeConfiguration: init.activeConfigurationValue,
|
|
configurations: []
|
|
};
|
|
init.configurations.forEach(config => {
|
|
var configInfo = {
|
|
configurationValue: config.configurationValue,
|
|
configurationName: stringToMojoString16(config.configurationName),
|
|
interfaces: []
|
|
};
|
|
config.interfaces.forEach(iface => {
|
|
var interfaceInfo = {
|
|
interfaceNumber: iface.interfaceNumber,
|
|
alternates: []
|
|
};
|
|
iface.alternates.forEach(alternate => {
|
|
var alternateInfo = {
|
|
alternateSetting: alternate.alternateSetting,
|
|
classCode: alternate.interfaceClass,
|
|
subclassCode: alternate.interfaceSubclass,
|
|
protocolCode: alternate.interfaceProtocol,
|
|
interfaceName: stringToMojoString16(alternate.interfaceName),
|
|
endpoints: []
|
|
};
|
|
alternate.endpoints.forEach(endpoint => {
|
|
var endpointInfo = {
|
|
endpointNumber: endpoint.endpointNumber,
|
|
packetSize: endpoint.packetSize,
|
|
};
|
|
switch (endpoint.direction) {
|
|
case "in":
|
|
endpointInfo.direction = device.mojom.UsbTransferDirection.INBOUND;
|
|
break;
|
|
case "out":
|
|
endpointInfo.direction = device.mojom.UsbTransferDirection.OUTBOUND;
|
|
break;
|
|
}
|
|
switch (endpoint.type) {
|
|
case "bulk":
|
|
endpointInfo.type = device.mojom.UsbTransferType.BULK;
|
|
break;
|
|
case "interrupt":
|
|
endpointInfo.type = device.mojom.UsbTransferType.INTERRUPT;
|
|
break;
|
|
case "isochronous":
|
|
endpointInfo.type = device.mojom.UsbTransferType.ISOCHRONOUS;
|
|
break;
|
|
}
|
|
alternateInfo.endpoints.push(endpointInfo);
|
|
});
|
|
interfaceInfo.alternates.push(alternateInfo);
|
|
});
|
|
configInfo.interfaces.push(interfaceInfo);
|
|
});
|
|
deviceInfo.configurations.push(configInfo);
|
|
});
|
|
return deviceInfo;
|
|
}
|
|
|
|
function convertMojoDeviceFilters(input) {
|
|
let output = [];
|
|
input.forEach(filter => {
|
|
output.push(convertMojoDeviceFilter(filter));
|
|
});
|
|
return output;
|
|
}
|
|
|
|
function convertMojoDeviceFilter(input) {
|
|
let output = {};
|
|
if (input.hasVendorId)
|
|
output.vendorId = input.vendorId;
|
|
if (input.hasProductId)
|
|
output.productId = input.productId;
|
|
if (input.hasClassCode)
|
|
output.classCode = input.classCode;
|
|
if (input.hasSubclassCode)
|
|
output.subclassCode = input.subclassCode;
|
|
if (input.hasProtocolCode)
|
|
output.protocolCode = input.protocolCode;
|
|
if (input.serialNumber)
|
|
output.serialNumber = mojoString16ToString(input.serialNumber);
|
|
return output;
|
|
}
|
|
|
|
class FakeDevice {
|
|
constructor(deviceInit) {
|
|
this.info_ = deviceInit;
|
|
this.opened_ = false;
|
|
this.currentConfiguration_ = null;
|
|
this.claimedInterfaces_ = new Map();
|
|
}
|
|
|
|
getConfiguration() {
|
|
if (this.currentConfiguration_) {
|
|
return Promise.resolve({
|
|
value: this.currentConfiguration_.configurationValue });
|
|
} else {
|
|
return Promise.resolve({ value: 0 });
|
|
}
|
|
}
|
|
|
|
open() {
|
|
assert_false(this.opened_);
|
|
this.opened_ = true;
|
|
return Promise.resolve({ error: device.mojom.UsbOpenDeviceError.OK });
|
|
}
|
|
|
|
close() {
|
|
assert_true(this.opened_);
|
|
this.opened_ = false;
|
|
return Promise.resolve();
|
|
}
|
|
|
|
setConfiguration(value) {
|
|
assert_true(this.opened_);
|
|
|
|
let selectedConfiguration = this.info_.configurations.find(
|
|
configuration => configuration.configurationValue == value);
|
|
// Blink should never request an invalid configuration.
|
|
assert_not_equals(selectedConfiguration, undefined);
|
|
this.currentConfiguration_ = selectedConfiguration;
|
|
return Promise.resolve({ success: true });
|
|
}
|
|
|
|
claimInterface(interfaceNumber) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
assert_false(this.claimedInterfaces_.has(interfaceNumber),
|
|
'interface already claimed');
|
|
|
|
// Blink should never request an invalid interface.
|
|
assert_true(this.currentConfiguration_.interfaces.some(
|
|
iface => iface.interfaceNumber == interfaceNumber));
|
|
this.claimedInterfaces_.set(interfaceNumber, 0);
|
|
return Promise.resolve({ success: true });
|
|
}
|
|
|
|
releaseInterface(interfaceNumber) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
assert_true(this.claimedInterfaces_.has(interfaceNumber));
|
|
this.claimedInterfaces_.delete(interfaceNumber);
|
|
return Promise.resolve({ success: true });
|
|
}
|
|
|
|
setInterfaceAlternateSetting(interfaceNumber, alternateSetting) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
assert_true(this.claimedInterfaces_.has(interfaceNumber));
|
|
|
|
let iface = this.currentConfiguration_.interfaces.find(
|
|
iface => iface.interfaceNumber == interfaceNumber);
|
|
// Blink should never request an invalid interface or alternate.
|
|
assert_false(iface == undefined);
|
|
assert_true(iface.alternates.some(
|
|
x => x.alternateSetting == alternateSetting));
|
|
this.claimedInterfaces_.set(interfaceNumber, alternateSetting);
|
|
return Promise.resolve({ success: true });
|
|
}
|
|
|
|
reset() {
|
|
assert_true(this.opened_);
|
|
return Promise.resolve({ success: true });
|
|
}
|
|
|
|
clearHalt(endpoint) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
// TODO(reillyg): Assert that endpoint is valid.
|
|
return Promise.resolve({ success: true });
|
|
}
|
|
|
|
controlTransferIn(params, length, timeout) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
return Promise.resolve({
|
|
status: device.mojom.UsbTransferStatus.OK,
|
|
data: [length >> 8, length & 0xff, params.request, params.value >> 8,
|
|
params.value & 0xff, params.index >> 8, params.index & 0xff]
|
|
});
|
|
}
|
|
|
|
controlTransferOut(params, data, timeout) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
return Promise.resolve({
|
|
status: device.mojom.UsbTransferStatus.OK,
|
|
bytesWritten: data.byteLength
|
|
});
|
|
}
|
|
|
|
genericTransferIn(endpointNumber, length, timeout) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
// TODO(reillyg): Assert that endpoint is valid.
|
|
let data = new Array(length);
|
|
for (let i = 0; i < length; ++i)
|
|
data[i] = i & 0xff;
|
|
return Promise.resolve({
|
|
status: device.mojom.UsbTransferStatus.OK,
|
|
data: data
|
|
});
|
|
}
|
|
|
|
genericTransferOut(endpointNumber, data, timeout) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
// TODO(reillyg): Assert that endpoint is valid.
|
|
return Promise.resolve({
|
|
status: device.mojom.UsbTransferStatus.OK,
|
|
bytesWritten: data.byteLength
|
|
});
|
|
}
|
|
|
|
isochronousTransferIn(endpointNumber, packetLengths, timeout) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
// TODO(reillyg): Assert that endpoint is valid.
|
|
let data = new Array(packetLengths.reduce((a, b) => a + b, 0));
|
|
let dataOffset = 0;
|
|
let packets = new Array(packetLengths.length);
|
|
for (let i = 0; i < packetLengths.length; ++i) {
|
|
for (let j = 0; j < packetLengths[i]; ++j)
|
|
data[dataOffset++] = j & 0xff;
|
|
packets[i] = {
|
|
length: packetLengths[i],
|
|
transferredLength: packetLengths[i],
|
|
status: device.mojom.UsbTransferStatus.OK
|
|
};
|
|
}
|
|
return Promise.resolve({ data: data, packets: packets });
|
|
}
|
|
|
|
isochronousTransferOut(endpointNumber, data, packetLengths, timeout) {
|
|
assert_true(this.opened_);
|
|
assert_false(this.currentConfiguration_ == null, 'device configured');
|
|
// TODO(reillyg): Assert that endpoint is valid.
|
|
let packets = new Array(packetLengths.length);
|
|
for (let i = 0; i < packetLengths.length; ++i) {
|
|
packets[i] = {
|
|
length: packetLengths[i],
|
|
transferredLength: packetLengths[i],
|
|
status: device.mojom.UsbTransferStatus.OK
|
|
};
|
|
}
|
|
return Promise.resolve({ packets: packets });
|
|
}
|
|
}
|
|
|
|
class FakeDeviceManager {
|
|
constructor() {
|
|
this.bindingSet_ = new mojo.BindingSet(device.mojom.UsbDeviceManager);
|
|
this.devices_ = new Map();
|
|
this.devicesByGuid_ = new Map();
|
|
this.client_ = null;
|
|
this.nextGuid_ = 0;
|
|
}
|
|
|
|
addBinding(handle) {
|
|
this.bindingSet_.addBinding(this, handle);
|
|
}
|
|
|
|
addDevice(fakeDevice, info) {
|
|
let device = {
|
|
fakeDevice: fakeDevice,
|
|
guid: (this.nextGuid_++).toString(),
|
|
info: info,
|
|
bindingArray: []
|
|
};
|
|
this.devices_.set(fakeDevice, device);
|
|
this.devicesByGuid_.set(device.guid, device);
|
|
if (this.client_)
|
|
this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info));
|
|
}
|
|
|
|
removeDevice(fakeDevice) {
|
|
let device = this.devices_.get(fakeDevice);
|
|
if (!device)
|
|
throw new Error('Cannot remove unknown device.');
|
|
|
|
for (var binding of device.bindingArray)
|
|
binding.close();
|
|
this.devices_.delete(device.fakeDevice);
|
|
this.devicesByGuid_.delete(device.guid);
|
|
if (this.client_) {
|
|
this.client_.onDeviceRemoved(
|
|
fakeDeviceInitToDeviceInfo(device.guid, device.info));
|
|
}
|
|
}
|
|
|
|
removeAllDevices() {
|
|
this.devices_.forEach(device => {
|
|
for (var binding of device.bindingArray)
|
|
binding.close();
|
|
this.client_.onDeviceRemoved(
|
|
fakeDeviceInitToDeviceInfo(device.guid, device.info));
|
|
});
|
|
this.devices_.clear();
|
|
this.devicesByGuid_.clear();
|
|
}
|
|
|
|
getDevices(options) {
|
|
let devices = [];
|
|
this.devices_.forEach(device => {
|
|
devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info));
|
|
});
|
|
return Promise.resolve({ results: devices });
|
|
}
|
|
|
|
getDevice(guid, request) {
|
|
let retrievedDevice = this.devicesByGuid_.get(guid);
|
|
if (retrievedDevice) {
|
|
let binding = new mojo.Binding(
|
|
device.mojom.UsbDevice,
|
|
new FakeDevice(retrievedDevice.info),
|
|
request);
|
|
binding.setConnectionErrorHandler(() => {
|
|
if (retrievedDevice.fakeDevice.onclose)
|
|
retrievedDevice.fakeDevice.onclose();
|
|
});
|
|
retrievedDevice.bindingArray.push(binding);
|
|
} else {
|
|
request.close();
|
|
}
|
|
}
|
|
|
|
setClient(client) {
|
|
this.client_ = client;
|
|
}
|
|
}
|
|
|
|
class USBDeviceRequestEvent {
|
|
constructor(deviceFilters, resolve) {
|
|
this.filters = convertMojoDeviceFilters(deviceFilters);
|
|
this.resolveFunc_ = resolve;
|
|
}
|
|
|
|
respondWith(value) {
|
|
// Wait until |value| resolves (if it is a Promise). This function returns
|
|
// no value.
|
|
Promise.resolve(value).then(fakeDevice => {
|
|
let device = internal.deviceManager.devices_.get(fakeDevice);
|
|
let result = null;
|
|
if (device) {
|
|
result = fakeDeviceInitToDeviceInfo(device.guid, device.info);
|
|
}
|
|
this.resolveFunc_({ result: result });
|
|
}, () => {
|
|
this.resolveFunc_({ result: null });
|
|
});
|
|
}
|
|
}
|
|
|
|
class FakeChooserService {
|
|
constructor() {
|
|
this.bindingSet_ = new mojo.BindingSet(device.mojom.UsbChooserService);
|
|
}
|
|
|
|
addBinding(handle) {
|
|
this.bindingSet_.addBinding(this, handle);
|
|
}
|
|
|
|
getPermission(deviceFilters) {
|
|
return new Promise(resolve => {
|
|
if (navigator.usb.test.onrequestdevice) {
|
|
navigator.usb.test.onrequestdevice(
|
|
new USBDeviceRequestEvent(deviceFilters, resolve));
|
|
} else {
|
|
resolve({ result: null });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice.
|
|
class FakeUSBDevice {
|
|
constructor() {
|
|
this.onclose = null;
|
|
}
|
|
|
|
disconnect() {
|
|
setTimeout(() => internal.deviceManager.removeDevice(this), 0);
|
|
}
|
|
}
|
|
|
|
// A helper for forwarding MojoHandle instances from one frame to another.
|
|
class CrossFrameHandleProxy {
|
|
constructor(callback) {
|
|
let {handle0, handle1} = Mojo.createMessagePipe();
|
|
this.sender_ = handle0;
|
|
this.receiver_ = handle1;
|
|
this.receiver_.watch({readable: true}, () => {
|
|
let message = this.receiver_.readMessage();
|
|
assert_equals(message.buffer.byteLength, 0);
|
|
assert_equals(message.handles.length, 1);
|
|
callback(message.handles[0]);
|
|
});
|
|
}
|
|
|
|
forwardHandle(handle) {
|
|
this.sender_.writeMessage(new ArrayBuffer, [handle]);
|
|
}
|
|
}
|
|
|
|
class USBTest {
|
|
constructor() {
|
|
this.onrequestdevice = undefined;
|
|
}
|
|
|
|
async initialize() {
|
|
if (internal.initialized)
|
|
return;
|
|
|
|
internal.deviceManager = new FakeDeviceManager();
|
|
internal.deviceManagerInterceptor =
|
|
new MojoInterfaceInterceptor(device.mojom.UsbDeviceManager.name);
|
|
internal.deviceManagerInterceptor.oninterfacerequest =
|
|
e => internal.deviceManager.addBinding(e.handle);
|
|
internal.deviceManagerInterceptor.start();
|
|
internal.deviceManagerCrossFrameProxy = new CrossFrameHandleProxy(
|
|
handle => internal.deviceManager.addBinding(handle));
|
|
|
|
internal.chooser = new FakeChooserService();
|
|
internal.chooserInterceptor =
|
|
new MojoInterfaceInterceptor(device.mojom.UsbChooserService.name);
|
|
internal.chooserInterceptor.oninterfacerequest =
|
|
e => internal.chooser.addBinding(e.handle);
|
|
internal.chooserInterceptor.start();
|
|
internal.chooserCrossFrameProxy = new CrossFrameHandleProxy(
|
|
handle => internal.chooser.addBinding(handle));
|
|
|
|
// Wait for a call to GetDevices() to pass between the renderer and the
|
|
// mock in order to establish that everything is set up.
|
|
await navigator.usb.getDevices();
|
|
internal.initialized = true;
|
|
}
|
|
|
|
async attachToWindow(otherWindow) {
|
|
if (!internal.initialized)
|
|
throw new Error('Call initialize() before attachToWindow().');
|
|
|
|
otherWindow.deviceManagerInterceptor =
|
|
new otherWindow.MojoInterfaceInterceptor(
|
|
device.mojom.UsbDeviceManager.name);
|
|
otherWindow.deviceManagerInterceptor.oninterfacerequest =
|
|
e => internal.deviceManagerCrossFrameProxy.forwardHandle(e.handle);
|
|
otherWindow.deviceManagerInterceptor.start();
|
|
|
|
otherWindow.chooserInterceptor =
|
|
new otherWindow.MojoInterfaceInterceptor(
|
|
device.mojom.UsbChooserService.name);
|
|
otherWindow.chooserInterceptor.oninterfacerequest =
|
|
e => internal.chooserCrossFrameProxy.forwardHandle(e.handle);
|
|
otherWindow.chooserInterceptor.start();
|
|
|
|
// Wait for a call to GetDevices() to pass between the renderer and the
|
|
// mock in order to establish that everything is set up.
|
|
await otherWindow.navigator.usb.getDevices();
|
|
}
|
|
|
|
addFakeDevice(deviceInit) {
|
|
if (!internal.initialized)
|
|
throw new Error('Call initialize() before addFakeDevice().');
|
|
|
|
// |addDevice| and |removeDevice| are called in a setTimeout callback so
|
|
// that tests do not rely on the device being immediately available which
|
|
// may not be true for all implementations of this test API.
|
|
let fakeDevice = new FakeUSBDevice();
|
|
setTimeout(
|
|
() => internal.deviceManager.addDevice(fakeDevice, deviceInit), 0);
|
|
return fakeDevice;
|
|
}
|
|
|
|
reset() {
|
|
if (!internal.initialized)
|
|
throw new Error('Call initialize() before reset().');
|
|
|
|
// Reset the mocks in a setTimeout callback so that tests do not rely on
|
|
// the fact that this polyfill can do this synchronously.
|
|
return new Promise(resolve => {
|
|
setTimeout(() => {
|
|
internal.deviceManager.removeAllDevices();
|
|
resolve();
|
|
}, 0);
|
|
});
|
|
}
|
|
}
|
|
|
|
navigator.usb.test = new USBTest();
|
|
|
|
})();
|