diff options
Diffstat (limited to 'testing/web-platform/tests/resources/chromium/webusb-test.js')
-rw-r--r-- | testing/web-platform/tests/resources/chromium/webusb-test.js | 582 |
1 files changed, 582 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resources/chromium/webusb-test.js b/testing/web-platform/tests/resources/chromium/webusb-test.js new file mode 100644 index 0000000000..94ff1bcadd --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webusb-test.js @@ -0,0 +1,582 @@ +'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, + + webUsbService: null, + webUsbServiceInterceptor: null, + + messagePort: null, +}; + +let mojom = {}; + +async function loadMojomDefinitions() { + const deviceMojom = + await import('/gen/services/device/public/mojom/usb_device.mojom.m.js'); + const serviceMojom = await import( + '/gen/third_party/blink/public/mojom/usb/web_usb_service.mojom.m.js'); + return { + ...deviceMojom, + ...serviceMojom, + }; +} + +function getMessagePort(target) { + return new Promise(resolve => { + target.addEventListener('message', messageEvent => { + if (messageEvent.data.type === 'ReadyForAttachment') { + if (internal.messagePort === null) { + internal.messagePort = messageEvent.data.port; + } + resolve(); + } + }, {once: true}); + }); +} + +// 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), + selfPowered: false, + remoteWakeup: false, + maximumPower: 0, + interfaces: [], + extraData: new Uint8Array() + }; + 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: [], + extraData: new Uint8Array() + }; + alternate.endpoints.forEach(endpoint => { + var endpointInfo = { + endpointNumber: endpoint.endpointNumber, + packetSize: endpoint.packetSize, + synchronizationType: mojom.UsbSynchronizationType.NONE, + usageType: mojom.UsbUsageType.DATA, + pollingInterval: 0, + extraData: new Uint8Array() + }; + switch (endpoint.direction) { + case "in": + endpointInfo.direction = mojom.UsbTransferDirection.INBOUND; + break; + case "out": + endpointInfo.direction = mojom.UsbTransferDirection.OUTBOUND; + break; + } + switch (endpoint.type) { + case "bulk": + endpointInfo.type = mojom.UsbTransferType.BULK; + break; + case "interrupt": + endpointInfo.type = mojom.UsbTransferType.INTERRUPT; + break; + case "isochronous": + endpointInfo.type = 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({result: {success: mojom.UsbOpenDeviceSuccess.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 }); + } + + async claimInterface(interfaceNumber) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + assert_false(this.claimedInterfaces_.has(interfaceNumber), + 'interface already claimed'); + + const protectedInterfaces = new Set([ + mojom.USB_AUDIO_CLASS, + mojom.USB_HID_CLASS, + mojom.USB_MASS_STORAGE_CLASS, + mojom.USB_SMART_CARD_CLASS, + mojom.USB_VIDEO_CLASS, + mojom.USB_AUDIO_VIDEO_CLASS, + mojom.USB_WIRELESS_CLASS, + ]); + + let iface = this.currentConfiguration_.interfaces.find( + iface => iface.interfaceNumber == interfaceNumber); + // Blink should never request an invalid interface or alternate. + assert_false(iface == undefined); + if (iface.alternates.some( + alt => protectedInterfaces.has(alt.interfaceClass))) { + return {result: mojom.UsbClaimInterfaceResult.kProtectedClass}; + } + + this.claimedInterfaces_.set(interfaceNumber, 0); + return {result: mojom.UsbClaimInterfaceResult.kSuccess}; + } + + 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 }); + } + + async controlTransferIn(params, length, timeout) { + assert_true(this.opened_); + + if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE || + params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) && + this.currentConfiguration_ == null) { + return { + status: mojom.UsbTransferStatus.PERMISSION_DENIED, + }; + } + + return { + status: mojom.UsbTransferStatus.OK, + data: { + buffer: [ + length >> 8, length & 0xff, params.request, params.value >> 8, + params.value & 0xff, params.index >> 8, params.index & 0xff + ] + } + }; + } + + async controlTransferOut(params, data, timeout) { + assert_true(this.opened_); + + if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE || + params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) && + this.currentConfiguration_ == null) { + return { + status: mojom.UsbTransferStatus.PERMISSION_DENIED, + }; + } + + return {status: 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: mojom.UsbTransferStatus.OK, data: {buffer: 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: 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: mojom.UsbTransferStatus.OK + }; + } + return Promise.resolve({data: {buffer: 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: mojom.UsbTransferStatus.OK + }; + } + return Promise.resolve({ packets: packets }); + } +} + +class FakeWebUsbService { + constructor() { + this.receiver_ = new mojom.WebUsbServiceReceiver(this); + this.devices_ = new Map(); + this.devicesByGuid_ = new Map(); + this.client_ = null; + this.nextGuid_ = 0; + } + + addBinding(handle) { + this.receiver_.$.bindHandle(handle); + } + + addDevice(fakeDevice, info) { + let device = { + fakeDevice: fakeDevice, + guid: (this.nextGuid_++).toString(), + info: info, + receivers: [], + }; + this.devices_.set(fakeDevice, device); + this.devicesByGuid_.set(device.guid, device); + if (this.client_) + this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info)); + } + + async forgetDevice(guid) { + // Permissions are currently untestable through WPT. + } + + removeDevice(fakeDevice) { + let device = this.devices_.get(fakeDevice); + if (!device) + throw new Error('Cannot remove unknown device.'); + + for (const receiver of device.receivers) + receiver.$.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 (const receiver of device.receivers) + receiver.$.close(); + this.client_.onDeviceRemoved( + fakeDeviceInitToDeviceInfo(device.guid, device.info)); + }); + this.devices_.clear(); + this.devicesByGuid_.clear(); + } + + getDevices() { + 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) { + const receiver = + new mojom.UsbDeviceReceiver(new FakeDevice(retrievedDevice.info)); + receiver.$.bindHandle(request.handle); + receiver.onConnectionError.addListener(() => { + if (retrievedDevice.fakeDevice.onclose) + retrievedDevice.fakeDevice.onclose(); + }); + retrievedDevice.receivers.push(receiver); + } else { + request.handle.close(); + } + } + + getPermission(deviceFilters) { + return new Promise(resolve => { + if (navigator.usb.test.onrequestdevice) { + navigator.usb.test.onrequestdevice( + new USBDeviceRequestEvent(deviceFilters, resolve)); + } else { + resolve({ result: null }); + } + }); + } + + 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.webUsbService.devices_.get(fakeDevice); + let result = null; + if (device) { + result = fakeDeviceInitToDeviceInfo(device.guid, device.info); + } + this.resolveFunc_({ result: result }); + }, () => { + this.resolveFunc_({ result: null }); + }); + } +} + +// Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice. +class FakeUSBDevice { + constructor() { + this.onclose = null; + } + + disconnect() { + setTimeout(() => internal.webUsbService.removeDevice(this), 0); + } +} + +class USBTest { + constructor() { + this.onrequestdevice = undefined; + } + + async initialize() { + if (internal.initialized) + return; + + // Be ready to handle 'ReadyForAttachment' message from child iframes. + if ('window' in self) { + getMessagePort(window); + } + + mojom = await loadMojomDefinitions(); + internal.webUsbService = new FakeWebUsbService(); + internal.webUsbServiceInterceptor = + new MojoInterfaceInterceptor(mojom.WebUsbService.$interfaceName); + internal.webUsbServiceInterceptor.oninterfacerequest = + e => internal.webUsbService.addBinding(e.handle); + internal.webUsbServiceInterceptor.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 navigator.usb.getDevices(); + internal.initialized = true; + } + + // Returns a promise that is resolved when the implementation of |usb| in the + // global scope for |context| is controlled by the current context. + attachToContext(context) { + if (!internal.initialized) + throw new Error('Call initialize() before attachToContext()'); + + let target = context.constructor.name === 'Worker' ? context : window; + return getMessagePort(target).then(() => { + return new Promise(resolve => { + internal.messagePort.onmessage = channelEvent => { + switch (channelEvent.data.type) { + case mojom.WebUsbService.$interfaceName: + internal.webUsbService.addBinding(channelEvent.data.handle); + break; + case 'Complete': + resolve(); + break; + } + }; + internal.messagePort.postMessage({ + type: 'Attach', + interfaces: [ + mojom.WebUsbService.$interfaceName, + ] + }); + }); + }); + } + + 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.webUsbService.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(() => { + if (internal.messagePort !== null) + internal.messagePort.close(); + internal.messagePort = null; + internal.webUsbService.removeAllDevices(); + resolve(); + }, 0); + }); + } +} + +navigator.usb.test = new USBTest(); + +})(); |