| OLD | NEW |
| (Empty) |
| 1 'use strict'; | |
| 2 | |
| 3 // This polyfill library implements the WebUSB Test API as specified here: | |
| 4 // https://wicg.github.io/webusb/test/ | |
| 5 | |
| 6 (() => { | |
| 7 | |
| 8 // These variables are logically members of the USBTest class but are defined | |
| 9 // here to hide them from being visible as fields of navigator.usb.test. | |
| 10 let internal = { | |
| 11 intialized: false, | |
| 12 | |
| 13 deviceManager: null, | |
| 14 deviceManagerInterceptor: null, | |
| 15 deviceManagerCrossFrameProxy: null, | |
| 16 | |
| 17 chooser: null, | |
| 18 chooserInterceptor: null, | |
| 19 chooserCrossFrameProxy: null, | |
| 20 }; | |
| 21 | |
| 22 function fakeDeviceInitToDeviceInfo(guid, init) { | |
| 23 let deviceInfo = { | |
| 24 guid: guid + "", | |
| 25 usbVersionMajor: init.usbVersionMajor, | |
| 26 usbVersionMinor: init.usbVersionMinor, | |
| 27 usbVersionSubminor: init.usbVersionSubminor, | |
| 28 classCode: init.deviceClass, | |
| 29 subclassCode: init.deviceSubclass, | |
| 30 protocolCode: init.deviceProtocol, | |
| 31 vendorId: init.vendorId, | |
| 32 productId: init.productId, | |
| 33 deviceVersionMajor: init.deviceVersionMajor, | |
| 34 deviceVersionMinor: init.deviceVersionMinor, | |
| 35 deviceVersionSubminor: init.deviceVersionSubminor, | |
| 36 manufacturerName: init.manufacturerName, | |
| 37 productName: init.productName, | |
| 38 serialNumber: init.serialNumber, | |
| 39 activeConfiguration: init.activeConfigurationValue, | |
| 40 configurations: [] | |
| 41 }; | |
| 42 init.configurations.forEach(config => { | |
| 43 var configInfo = { | |
| 44 configurationValue: config.configurationValue, | |
| 45 configurationName: config.configurationName, | |
| 46 interfaces: [] | |
| 47 }; | |
| 48 config.interfaces.forEach(iface => { | |
| 49 var interfaceInfo = { | |
| 50 interfaceNumber: iface.interfaceNumber, | |
| 51 alternates: [] | |
| 52 }; | |
| 53 iface.alternates.forEach(alternate => { | |
| 54 var alternateInfo = { | |
| 55 alternateSetting: alternate.alternateSetting, | |
| 56 classCode: alternate.interfaceClass, | |
| 57 subclassCode: alternate.interfaceSubclass, | |
| 58 protocolCode: alternate.interfaceProtocol, | |
| 59 interfaceName: alternate.interfaceName, | |
| 60 endpoints: [] | |
| 61 }; | |
| 62 alternate.endpoints.forEach(endpoint => { | |
| 63 var endpointInfo = { | |
| 64 endpointNumber: endpoint.endpointNumber, | |
| 65 packetSize: endpoint.packetSize, | |
| 66 }; | |
| 67 switch (endpoint.direction) { | |
| 68 case "in": | |
| 69 endpointInfo.direction = device.mojom.UsbTransferDirection.INBOUND; | |
| 70 break; | |
| 71 case "out": | |
| 72 endpointInfo.direction = device.mojom.UsbTransferDirection.OUTBOUND; | |
| 73 break; | |
| 74 } | |
| 75 switch (endpoint.type) { | |
| 76 case "bulk": | |
| 77 endpointInfo.type = device.mojom.UsbTransferType.BULK; | |
| 78 break; | |
| 79 case "interrupt": | |
| 80 endpointInfo.type = device.mojom.UsbTransferType.INTERRUPT; | |
| 81 break; | |
| 82 case "isochronous": | |
| 83 endpointInfo.type = device.mojom.UsbTransferType.ISOCHRONOUS; | |
| 84 break; | |
| 85 } | |
| 86 alternateInfo.endpoints.push(endpointInfo); | |
| 87 }); | |
| 88 interfaceInfo.alternates.push(alternateInfo); | |
| 89 }); | |
| 90 configInfo.interfaces.push(interfaceInfo); | |
| 91 }); | |
| 92 deviceInfo.configurations.push(configInfo); | |
| 93 }); | |
| 94 return deviceInfo; | |
| 95 } | |
| 96 | |
| 97 function convertMojoDeviceFilters(input) { | |
| 98 let output = []; | |
| 99 input.forEach(filter => { | |
| 100 output.push(convertMojoDeviceFilter(filter)); | |
| 101 }); | |
| 102 return output; | |
| 103 } | |
| 104 | |
| 105 function convertMojoDeviceFilter(input) { | |
| 106 let output = {}; | |
| 107 if (input.hasVendorId) | |
| 108 output.vendorId = input.vendorId; | |
| 109 if (input.hasProductId) | |
| 110 output.productId = input.productId; | |
| 111 if (input.hasClassCode) | |
| 112 output.classCode = input.classCode; | |
| 113 if (input.hasSubclassCode) | |
| 114 output.subclassCode = input.subclassCode; | |
| 115 if (input.hasProtocolCode) | |
| 116 output.protocolCode = input.protocolCode; | |
| 117 if (input.serialNumber) | |
| 118 output.serialNumber = input.serialNumber; | |
| 119 return output; | |
| 120 } | |
| 121 | |
| 122 class FakeDevice { | |
| 123 constructor(deviceInit) { | |
| 124 this.info_ = deviceInit; | |
| 125 this.opened_ = false; | |
| 126 this.currentConfiguration_ = null; | |
| 127 this.claimedInterfaces_ = new Map(); | |
| 128 } | |
| 129 | |
| 130 getConfiguration() { | |
| 131 if (this.currentConfiguration_) { | |
| 132 return Promise.resolve({ | |
| 133 value: this.currentConfiguration_.configurationValue }); | |
| 134 } else { | |
| 135 return Promise.resolve({ value: 0 }); | |
| 136 } | |
| 137 } | |
| 138 | |
| 139 open() { | |
| 140 assert_false(this.opened_); | |
| 141 this.opened_ = true; | |
| 142 return Promise.resolve({ error: device.mojom.UsbOpenDeviceError.OK }); | |
| 143 } | |
| 144 | |
| 145 close() { | |
| 146 assert_true(this.opened_); | |
| 147 this.opened_ = false; | |
| 148 return Promise.resolve(); | |
| 149 } | |
| 150 | |
| 151 setConfiguration(value) { | |
| 152 assert_true(this.opened_); | |
| 153 | |
| 154 let selectedConfiguration = this.info_.configurations.find( | |
| 155 configuration => configuration.configurationValue == value); | |
| 156 // Blink should never request an invalid configuration. | |
| 157 assert_not_equals(selectedConfiguration, undefined); | |
| 158 this.currentConfiguration_ = selectedConfiguration; | |
| 159 return Promise.resolve({ success: true }); | |
| 160 } | |
| 161 | |
| 162 claimInterface(interfaceNumber) { | |
| 163 assert_true(this.opened_); | |
| 164 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 165 assert_false(this.claimedInterfaces_.has(interfaceNumber), | |
| 166 'interface already claimed'); | |
| 167 | |
| 168 // Blink should never request an invalid interface. | |
| 169 assert_true(this.currentConfiguration_.interfaces.some( | |
| 170 iface => iface.interfaceNumber == interfaceNumber)); | |
| 171 this.claimedInterfaces_.set(interfaceNumber, 0); | |
| 172 return Promise.resolve({ success: true }); | |
| 173 } | |
| 174 | |
| 175 releaseInterface(interfaceNumber) { | |
| 176 assert_true(this.opened_); | |
| 177 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 178 assert_true(this.claimedInterfaces_.has(interfaceNumber)); | |
| 179 this.claimedInterfaces_.delete(interfaceNumber); | |
| 180 return Promise.resolve({ success: true }); | |
| 181 } | |
| 182 | |
| 183 setInterfaceAlternateSetting(interfaceNumber, alternateSetting) { | |
| 184 assert_true(this.opened_); | |
| 185 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 186 assert_true(this.claimedInterfaces_.has(interfaceNumber)); | |
| 187 | |
| 188 let iface = this.currentConfiguration_.interfaces.find( | |
| 189 iface => iface.interfaceNumber == interfaceNumber); | |
| 190 // Blink should never request an invalid interface or alternate. | |
| 191 assert_false(iface == undefined); | |
| 192 assert_true(iface.alternates.some( | |
| 193 x => x.alternateSetting == alternateSetting)); | |
| 194 this.claimedInterfaces_.set(interfaceNumber, alternateSetting); | |
| 195 return Promise.resolve({ success: true }); | |
| 196 } | |
| 197 | |
| 198 reset() { | |
| 199 assert_true(this.opened_); | |
| 200 return Promise.resolve({ success: true }); | |
| 201 } | |
| 202 | |
| 203 clearHalt(endpoint) { | |
| 204 assert_true(this.opened_); | |
| 205 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 206 // TODO(reillyg): Assert that endpoint is valid. | |
| 207 return Promise.resolve({ success: true }); | |
| 208 } | |
| 209 | |
| 210 controlTransferIn(params, length, timeout) { | |
| 211 assert_true(this.opened_); | |
| 212 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 213 return Promise.resolve({ | |
| 214 status: device.mojom.UsbTransferStatus.OK, | |
| 215 data: [length >> 8, length & 0xff, params.request, params.value >> 8, | |
| 216 params.value & 0xff, params.index >> 8, params.index & 0xff] | |
| 217 }); | |
| 218 } | |
| 219 | |
| 220 controlTransferOut(params, data, timeout) { | |
| 221 assert_true(this.opened_); | |
| 222 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 223 return Promise.resolve({ | |
| 224 status: device.mojom.UsbTransferStatus.OK, | |
| 225 bytesWritten: data.byteLength | |
| 226 }); | |
| 227 } | |
| 228 | |
| 229 genericTransferIn(endpointNumber, length, timeout) { | |
| 230 assert_true(this.opened_); | |
| 231 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 232 // TODO(reillyg): Assert that endpoint is valid. | |
| 233 let data = new Array(length); | |
| 234 for (let i = 0; i < length; ++i) | |
| 235 data[i] = i & 0xff; | |
| 236 return Promise.resolve({ | |
| 237 status: device.mojom.UsbTransferStatus.OK, | |
| 238 data: data | |
| 239 }); | |
| 240 } | |
| 241 | |
| 242 genericTransferOut(endpointNumber, data, timeout) { | |
| 243 assert_true(this.opened_); | |
| 244 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 245 // TODO(reillyg): Assert that endpoint is valid. | |
| 246 return Promise.resolve({ | |
| 247 status: device.mojom.UsbTransferStatus.OK, | |
| 248 bytesWritten: data.byteLength | |
| 249 }); | |
| 250 } | |
| 251 | |
| 252 isochronousTransferIn(endpointNumber, packetLengths, timeout) { | |
| 253 assert_true(this.opened_); | |
| 254 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 255 // TODO(reillyg): Assert that endpoint is valid. | |
| 256 let data = new Array(packetLengths.reduce((a, b) => a + b, 0)); | |
| 257 let dataOffset = 0; | |
| 258 let packets = new Array(packetLengths.length); | |
| 259 for (let i = 0; i < packetLengths.length; ++i) { | |
| 260 for (let j = 0; j < packetLengths[i]; ++j) | |
| 261 data[dataOffset++] = j & 0xff; | |
| 262 packets[i] = { | |
| 263 length: packetLengths[i], | |
| 264 transferredLength: packetLengths[i], | |
| 265 status: device.mojom.UsbTransferStatus.OK | |
| 266 }; | |
| 267 } | |
| 268 return Promise.resolve({ data: data, packets: packets }); | |
| 269 } | |
| 270 | |
| 271 isochronousTransferOut(endpointNumber, data, packetLengths, timeout) { | |
| 272 assert_true(this.opened_); | |
| 273 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
| 274 // TODO(reillyg): Assert that endpoint is valid. | |
| 275 let packets = new Array(packetLengths.length); | |
| 276 for (let i = 0; i < packetLengths.length; ++i) { | |
| 277 packets[i] = { | |
| 278 length: packetLengths[i], | |
| 279 transferredLength: packetLengths[i], | |
| 280 status: device.mojom.UsbTransferStatus.OK | |
| 281 }; | |
| 282 } | |
| 283 return Promise.resolve({ packets: packets }); | |
| 284 } | |
| 285 } | |
| 286 | |
| 287 class FakeDeviceManager { | |
| 288 constructor() { | |
| 289 this.bindingSet_ = new mojo.BindingSet(device.mojom.UsbDeviceManager); | |
| 290 this.devices_ = new Map(); | |
| 291 this.devicesByGuid_ = new Map(); | |
| 292 this.client_ = null; | |
| 293 this.nextGuid_ = 0; | |
| 294 } | |
| 295 | |
| 296 addBinding(handle) { | |
| 297 this.bindingSet_.addBinding(this, handle); | |
| 298 } | |
| 299 | |
| 300 addDevice(fakeDevice, info) { | |
| 301 let device = { | |
| 302 fakeDevice: fakeDevice, | |
| 303 guid: (this.nextGuid_++).toString(), | |
| 304 info: info, | |
| 305 bindingArray: [] | |
| 306 }; | |
| 307 this.devices_.set(fakeDevice, device); | |
| 308 this.devicesByGuid_.set(device.guid, device); | |
| 309 if (this.client_) | |
| 310 this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info)); | |
| 311 } | |
| 312 | |
| 313 removeDevice(fakeDevice) { | |
| 314 let device = this.devices_.get(fakeDevice); | |
| 315 if (!device) | |
| 316 throw new Error('Cannot remove unknown device.'); | |
| 317 | |
| 318 for (var binding of device.bindingArray) | |
| 319 binding.close(); | |
| 320 this.devices_.delete(device.fakeDevice); | |
| 321 this.devicesByGuid_.delete(device.guid); | |
| 322 if (this.client_) { | |
| 323 this.client_.onDeviceRemoved( | |
| 324 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
| 325 } | |
| 326 } | |
| 327 | |
| 328 removeAllDevices() { | |
| 329 this.devices_.forEach(device => { | |
| 330 for (var binding of device.bindingArray) | |
| 331 binding.close(); | |
| 332 this.client_.onDeviceRemoved( | |
| 333 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
| 334 }); | |
| 335 this.devices_.clear(); | |
| 336 this.devicesByGuid_.clear(); | |
| 337 } | |
| 338 | |
| 339 getDevices(options) { | |
| 340 let devices = []; | |
| 341 this.devices_.forEach(device => { | |
| 342 devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
| 343 }); | |
| 344 return Promise.resolve({ results: devices }); | |
| 345 } | |
| 346 | |
| 347 getDevice(guid, request) { | |
| 348 let device = this.devicesByGuid_.get(guid); | |
| 349 if (device) { | |
| 350 let binding = new mojo.Binding( | |
| 351 window.device.mojom.UsbDevice, new FakeDevice(device.info), request); | |
| 352 binding.setConnectionErrorHandler(() => { | |
| 353 if (device.fakeDevice.onclose) | |
| 354 device.fakeDevice.onclose(); | |
| 355 }); | |
| 356 device.bindingArray.push(binding); | |
| 357 } else { | |
| 358 request.close(); | |
| 359 } | |
| 360 } | |
| 361 | |
| 362 setClient(client) { | |
| 363 this.client_ = client; | |
| 364 } | |
| 365 } | |
| 366 | |
| 367 class FakeChooserService { | |
| 368 constructor() { | |
| 369 this.bindingSet_ = new mojo.BindingSet(device.mojom.UsbChooserService); | |
| 370 this.chosenDevice_ = null; | |
| 371 this.lastFilters_ = null; | |
| 372 } | |
| 373 | |
| 374 addBinding(handle) { | |
| 375 this.bindingSet_.addBinding(this, handle); | |
| 376 } | |
| 377 | |
| 378 setChosenDevice(fakeDevice) { | |
| 379 this.chosenDevice_ = fakeDevice; | |
| 380 } | |
| 381 | |
| 382 getPermission(deviceFilters) { | |
| 383 this.lastFilters_ = convertMojoDeviceFilters(deviceFilters); | |
| 384 let device = internal.deviceManager.devices_.get(this.chosenDevice_); | |
| 385 if (device) { | |
| 386 return Promise.resolve({ | |
| 387 result: fakeDeviceInitToDeviceInfo(device.guid, device.info) | |
| 388 }); | |
| 389 } else { | |
| 390 return Promise.resolve({ result: null }); | |
| 391 } | |
| 392 } | |
| 393 } | |
| 394 | |
| 395 // Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice. | |
| 396 class FakeUSBDevice { | |
| 397 constructor() { | |
| 398 this.onclose = null; | |
| 399 } | |
| 400 | |
| 401 disconnect() { | |
| 402 setTimeout(() => internal.deviceManager.removeDevice(this), 0); | |
| 403 } | |
| 404 } | |
| 405 | |
| 406 // A helper for forwarding MojoHandle instances from one frame to another. | |
| 407 class CrossFrameHandleProxy { | |
| 408 constructor(callback) { | |
| 409 let {handle0, handle1} = Mojo.createMessagePipe(); | |
| 410 this.sender_ = handle0; | |
| 411 this.receiver_ = handle1; | |
| 412 this.receiver_.watch({readable: true}, () => { | |
| 413 let message = this.receiver_.readMessage(); | |
| 414 assert_equals(message.buffer.byteLength, 0); | |
| 415 assert_equals(message.handles.length, 1); | |
| 416 callback(message.handles[0]); | |
| 417 }); | |
| 418 } | |
| 419 | |
| 420 forwardHandle(handle) { | |
| 421 this.sender_.writeMessage(new ArrayBuffer, [handle]); | |
| 422 } | |
| 423 } | |
| 424 | |
| 425 class USBTest { | |
| 426 constructor() {} | |
| 427 | |
| 428 initialize() { | |
| 429 if (internal.initialized) | |
| 430 return Promise.resolve(); | |
| 431 | |
| 432 internal.deviceManager = new FakeDeviceManager(); | |
| 433 internal.deviceManagerInterceptor = | |
| 434 new MojoInterfaceInterceptor(device.mojom.UsbDeviceManager.name); | |
| 435 internal.deviceManagerInterceptor.oninterfacerequest = | |
| 436 e => internal.deviceManager.addBinding(e.handle); | |
| 437 internal.deviceManagerInterceptor.start(); | |
| 438 internal.deviceManagerCrossFrameProxy = new CrossFrameHandleProxy( | |
| 439 handle => internal.deviceManager.addBinding(handle)); | |
| 440 | |
| 441 internal.chooser = new FakeChooserService(); | |
| 442 internal.chooserInterceptor = | |
| 443 new MojoInterfaceInterceptor(device.mojom.UsbChooserService.name); | |
| 444 internal.chooserInterceptor.oninterfacerequest = | |
| 445 e => internal.chooser.addBinding(e.handle); | |
| 446 internal.chooserInterceptor.start(); | |
| 447 internal.chooserCrossFrameProxy = new CrossFrameHandleProxy( | |
| 448 handle => internal.chooser.addBinding(handle)); | |
| 449 | |
| 450 internal.initialized = true; | |
| 451 return Promise.resolve(); | |
| 452 } | |
| 453 | |
| 454 attachToWindow(otherWindow) { | |
| 455 if (!internal.initialized) | |
| 456 throw new Error('Call initialize() before attachToWindow().'); | |
| 457 | |
| 458 otherWindow.deviceManagerInterceptor = | |
| 459 new otherWindow.MojoInterfaceInterceptor( | |
| 460 device.mojom.UsbDeviceManager.name); | |
| 461 otherWindow.deviceManagerInterceptor.oninterfacerequest = | |
| 462 e => internal.deviceManagerCrossFrameProxy.forwardHandle(e.handle); | |
| 463 otherWindow.deviceManagerInterceptor.start(); | |
| 464 | |
| 465 otherWindow.chooserInterceptor = | |
| 466 new otherWindow.MojoInterfaceInterceptor( | |
| 467 device.mojom.UsbChooserService.name); | |
| 468 otherWindow.chooserInterceptor.oninterfacerequest = | |
| 469 e => internal.chooserCrossFrameProxy.forwardHandle(e.handle); | |
| 470 otherWindow.chooserInterceptor.start(); | |
| 471 return Promise.resolve(); | |
| 472 } | |
| 473 | |
| 474 addFakeDevice(deviceInit) { | |
| 475 if (!internal.initialized) | |
| 476 throw new Error('Call initialize() before addFakeDevice().'); | |
| 477 | |
| 478 // |addDevice| and |removeDevice| are called in a setTimeout callback so | |
| 479 // that tests do not rely on the device being immediately available which | |
| 480 // may not be true for all implementations of this test API. | |
| 481 let fakeDevice = new FakeUSBDevice(); | |
| 482 setTimeout( | |
| 483 () => internal.deviceManager.addDevice(fakeDevice, deviceInit), 0); | |
| 484 return fakeDevice; | |
| 485 } | |
| 486 | |
| 487 set chosenDevice(fakeDevice) { | |
| 488 if (!internal.initialized) | |
| 489 throw new Error('Call initialize() before setting chosenDevice.'); | |
| 490 | |
| 491 internal.chooser.setChosenDevice(fakeDevice); | |
| 492 } | |
| 493 | |
| 494 get lastFilters() { | |
| 495 if (!internal.initialized) | |
| 496 throw new Error('Call initialize() before getting lastFilters.'); | |
| 497 | |
| 498 return internal.chooser.lastFilters_; | |
| 499 } | |
| 500 | |
| 501 reset() { | |
| 502 if (!internal.initialized) | |
| 503 throw new Error('Call initialize() before reset().'); | |
| 504 | |
| 505 // Reset the mocks in a setTimeout callback so that tests do not rely on | |
| 506 // the fact that this polyfill can do this synchronously. | |
| 507 return new Promise(resolve => { | |
| 508 setTimeout(() => { | |
| 509 internal.deviceManager.removeAllDevices(); | |
| 510 internal.chooser.setChosenDevice(null); | |
| 511 resolve(); | |
| 512 }, 0); | |
| 513 }); | |
| 514 } | |
| 515 } | |
| 516 | |
| 517 navigator.usb.test = new USBTest(); | |
| 518 | |
| 519 })(); | |
| OLD | NEW |