| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * Javascript for DescriptorPanel UI, served from |
| * chrome://usb-internals/. |
| */ |
| |
| import 'chrome://resources/cr_elements/cr_tree/cr_tree.js'; |
| import 'chrome://resources/cr_elements/cr_tree/cr_tree_item.js'; |
| |
| import type {CrTreeElement} from 'chrome://resources/cr_elements/cr_tree/cr_tree.js'; |
| import type {CrTreeItemElement} from 'chrome://resources/cr_elements/cr_tree/cr_tree_item.js'; |
| import {assert} from 'chrome://resources/js/assert.js'; |
| |
| import type {UsbControlTransferParams, UsbDeviceInterface} from './usb_device.mojom-webui.js'; |
| import {UsbControlTransferRecipient, UsbControlTransferType, UsbTransferStatus} from './usb_device.mojom-webui.js'; |
| |
| const INPUT_TYPE_DECIMAL_WITH_DROPDOWN = 0; |
| const INPUT_TYPE_HEX_BYTE = 1; |
| |
| // Standard USB requests and descriptor types: |
| const GET_DESCRIPTOR_REQUEST = 0x06; |
| |
| const CONTROL_TRANSFER_DIRECTION_HOST_TO_DEVICE = 0; |
| const CONTROL_TRANSFER_DIRECTION_DEVICE_TO_HOST = 1; |
| |
| const DEVICE_DESCRIPTOR_TYPE = 0x01; |
| const CONFIGURATION_DESCRIPTOR_TYPE = 0x02; |
| const STRING_DESCRIPTOR_TYPE = 0x03; |
| const INTERFACE_DESCRIPTOR_TYPE = 0x04; |
| const ENDPOINT_DESCRIPTOR_TYPE = 0x05; |
| const BOS_DESCRIPTOR_TYPE = 0x0F; |
| const DEVICE_CAPABILITY_DESCRIPTOR_TYPE = 0x10; |
| |
| const DEVICE_CAPABILITY_DESCRIPTOR_TYPE_PLATFORM_TYPE = 0x05; |
| |
| const DEVICE_DESCRIPTOR_LENGTH = 18; |
| const CONFIGURATION_DESCRIPTOR_LENGTH = 9; |
| const MAX_STRING_DESCRIPTOR_LENGTH = 0xFF; |
| const INTERFACE_DESCRIPTOR_LENGTH = 9; |
| const ENDPOINT_DESCRIPTOR_LENGTH = 7; |
| const BOS_DESCRIPTOR_HEADER_LENGTH = 5; |
| const MAX_URL_DESCRIPTOR_LENGTH = 0xFF; |
| |
| const CONTROL_TRANSFER_TIMEOUT_MS = 2000; // 2 seconds |
| |
| const STANDARD_DESCRIPTOR_LENGTH_OFFSET = 0; |
| const STANDARD_DESCRIPTOR_TYPE_OFFSET = 1; |
| const CONFIGURATION_DESCRIPTOR_TOTAL_LENGTH_OFFSET = 2; |
| const CONFIGURATION_DESCRIPTOR_NUM_INTERFACES_OFFSET = 4; |
| const INTERFACE_DESCRIPTOR_NUM_ENDPOINTS_OFFSET = 4; |
| const BOS_DESCRIPTOR_TOTAL_LENGTH_OFFSET = 2; |
| const BOS_DESCRIPTOR_NUM_DEVICE_CAPABILITIES_OFFSET = 4; |
| const BOS_DESCRIPTOR_DEVICE_CAPABILITY_TYPE_OFFSET = 2; |
| |
| // Language codes are defined in: |
| // https://docs.microsoft.com/en-us/windows/desktop/intl/language-identifier-constants-and-strings |
| const LANGUAGE_CODE_EN_US = 0x0409; |
| |
| // Windows headers defined in: |
| // https://docs.microsoft.com/en-us/windows/desktop/winprog/using-the-windows-headers |
| const WIN_81_HEADER = 0x06030000; |
| |
| // These constants are defined by the WebUSB specification: |
| // http://wicg.github.io/webusb/ |
| |
| const GET_URL_REQUEST = 0x02; |
| |
| const WEB_USB_VENDOR_CODE_OFFSET = 22; |
| const WEB_USB_URL_DESCRIPTOR_INDEX_OFFSET = 23; |
| |
| const WEB_USB_CAPABILITY_UUID = [ |
| // Little-endian encoding of {3408b638-09a9-47a0-8bfd-a0768815b665}. |
| 0x38, |
| 0xB6, |
| 0x08, |
| 0x34, |
| 0xA9, |
| 0x09, |
| 0xA0, |
| 0x47, |
| 0x8B, |
| 0xFD, |
| 0xA0, |
| 0x76, |
| 0x88, |
| 0x15, |
| 0xB6, |
| 0x65, |
| ]; |
| |
| // These constants are defined by Microsoft OS 2.0 Descriptors Specification |
| // (July, 2018). |
| |
| const MS_OS_20_DESCRIPTOR_INDEX = 0x07; |
| const MS_OS_20_SET_ALT_ENUMERATION = 0x08; |
| |
| const MS_OS_20_SET_TOTAL_LENGTH_OFFSET = 4; |
| const MS_OS_20_VENDOR_CODE_ITEM_OFFSET = 6; |
| const MS_OS_20_ALT_ENUM_CODE_ITEM_OFFSET = 7; |
| const MS_OS_20_DESCRIPTOR_LENGTH_OFFSET = 0; |
| const MS_OS_20_DESCRIPTOR_TYPE_OFFSET = 2; |
| const MS_OS_20_REGISTRY_PROPERTY_DESCRIPTOR_PROPERTY_DATA_TYPE_OFFSET = 4; |
| const MS_OS_20_REGISTRY_PROPERTY_DESCRIPTOR_NAME_LENGTH_OFFSET = 6; |
| |
| const MS_OS_20_SET_HEADER_DESCRIPTOR = 0x00; |
| const MS_OS_20_SUBSET_HEADER_CONFIGURATION = 0x01; |
| const MS_OS_20_SUBSET_HEADER_FUNCTION = 0x02; |
| const MS_OS_20_FEATURE_COMPATIBLE_ID = 0x03; |
| const MS_OS_20_FEATURE_REG_PROPERTY = 0x04; |
| const MS_OS_20_FEATURE_MIN_RESUME_TIME = 0x05; |
| const MS_OS_20_FEATURE_MODEL_ID = 0x06; |
| const MS_OS_20_FEATURE_CCGP_DEVICE = 0x07; |
| const MS_OS_20_FEATURE_VENDOR_REVISION = 0x08; |
| |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_SZ = 0x01; |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_EXPAND_SZ = 0x02; |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_BINARY = 0x03; |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_DWORD_LITTLE_ENDIAN = 0x04; |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_DWORD_BIG_ENDIAN = 0x05; |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_LINK = 0x06; |
| const MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_MULTI_SZ = 0x07; |
| |
| const MS_OS_20_PLATFORM_CAPABILITY_UUID = [ |
| // Little-endian encoding of {D8DD60DF-4589-4CC7-9CD2-659D9E648A9F}. |
| 0xDF, |
| 0x60, |
| 0xDD, |
| 0xD8, |
| 0x89, |
| 0x45, |
| 0xC7, |
| 0x4C, |
| 0x9C, |
| 0xD2, |
| 0x65, |
| 0x9D, |
| 0x9E, |
| 0x64, |
| 0x8A, |
| 0x9F, |
| ]; |
| |
| export class DescriptorPanel { |
| private usbDeviceProxy_: UsbDeviceInterface; |
| private rootElement_: HTMLElement; |
| private stringDescriptorPanel_: DescriptorPanel|null = null; |
| private languageCodesListElement_: HTMLElement|null = null; |
| private indexInput_: HTMLInputElement|null = null; |
| stringDescriptorIndexes: Set<number> = new Set(); |
| indexesListElement: HTMLElement|null = null; |
| |
| constructor(usbDeviceProxy: UsbDeviceInterface, rootElement: HTMLElement) { |
| this.usbDeviceProxy_ = usbDeviceProxy; |
| this.rootElement_ = rootElement; |
| } |
| |
| /** |
| * Adds the reference of the string descriptor panel of the device for |
| * string descriptor functionality. |
| */ |
| setStringDescriptorPanel(stringDescriptorPanel: DescriptorPanel) { |
| this.stringDescriptorPanel_ = stringDescriptorPanel; |
| } |
| |
| /** |
| * Clears the data first before populating it with the new content. |
| */ |
| clearView() { |
| this.rootElement_.querySelectorAll('descriptorpanel') |
| .forEach(el => el.remove()); |
| this.rootElement_.querySelectorAll('error').forEach(el => el.remove()); |
| this.rootElement_.querySelectorAll('descriptorpaneltitle') |
| .forEach(el => el.remove()); |
| } |
| |
| private getButtonElementFromTemplate_(): HTMLButtonElement { |
| const buttonTemplate = |
| (this.rootElement_.getRootNode() as DocumentFragment | HTMLElement) |
| .querySelector<HTMLTemplateElement>('#raw-data-tree-button'); |
| assert(buttonTemplate); |
| const button = document.importNode(buttonTemplate.content, true) |
| .querySelector('button'); |
| assert(button); |
| return button; |
| } |
| |
| /** |
| * Adds a button for getting string descriptor to the string descriptor |
| * index item, and adds an autocomplete value to the index input area in |
| * the string descriptor panel. |
| * @param offset The offset of the string descriptor index field. |
| */ |
| private renderIndexItem_( |
| rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| fieldLabel: string) { |
| const index = rawData[offset]!; |
| if (index > 0) { |
| assert(this.stringDescriptorPanel_); |
| if (!this.stringDescriptorPanel_.stringDescriptorIndexes.has(index)) { |
| const optionElement = document.createElement('option'); |
| optionElement.label = index.toString(); |
| optionElement.value = index.toString(); |
| assert(this.stringDescriptorPanel_.indexesListElement); |
| this.stringDescriptorPanel_.indexesListElement.appendChild( |
| optionElement); |
| |
| this.stringDescriptorPanel_.stringDescriptorIndexes.add(index); |
| } |
| |
| const button = this.getButtonElementFromTemplate_(); |
| item.labelElement.appendChild(button); |
| button.style.marginInlineStart = '16px'; |
| button.addEventListener('click', (event: MouseEvent) => { |
| event.stopPropagation(); |
| // Clear the previous string descriptors. |
| const children = |
| item.shadowRoot!.querySelector<HTMLElement>('.tree-children'); |
| assert(children); |
| children.textContent = ''; |
| assert(this.stringDescriptorPanel_); |
| this.stringDescriptorPanel_.clearView(); |
| this.stringDescriptorPanel_.getStringDescriptorForAllLanguages_( |
| index, item); |
| }); |
| } else if (index < 0) { |
| // Delete the ': ' in fieldLabel. |
| const fieldName = fieldLabel.slice(0, -2); |
| showError( |
| `Invalid String Descriptor occurs in field ${ |
| fieldName} of this descriptor.`, |
| this.rootElement_); |
| } |
| } |
| |
| /** |
| * Adds a button for getting URL descriptor. |
| * @param offset The offset of the URL descriptor index. |
| */ |
| private renderUrlDescriptorIndexItem_( |
| rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| _fieldLabel: string) { |
| const index = rawData[offset]!; |
| if (index > 0) { |
| const button = this.getButtonElementFromTemplate_(); |
| item.labelElement.appendChild(button); |
| button.addEventListener('click', (event: MouseEvent) => { |
| event.stopPropagation(); |
| // Clear the previous URL descriptors. |
| const children = |
| item.shadowRoot!.querySelector<HTMLElement>('.tree-children'); |
| assert(children); |
| children.textContent = ''; |
| this.getUrlDescriptor_( |
| rawData, offset - WEB_USB_URL_DESCRIPTOR_INDEX_OFFSET, item); |
| }); |
| } |
| } |
| |
| /** |
| * Adds a button for getting the Microsoft OS 2.0 vendor-specific descriptor |
| * to the Microsoft OS 2.0 descriptor set information vendor-specific code |
| * item. |
| * @param offset The start offset of the Microsoft OS 2.0 |
| * descriptor set information. |
| */ |
| private renderMsOs20DescriptorVendorSpecific_( |
| rawData: Uint8Array, offset: number, item: CrTreeItemElement) { |
| // Use the vendor specified code and the length of Microsoft OS 2.0 |
| // descriptor Set that contained in Microsoft OS 2.0 descriptor Set Info |
| // to get Microsoft OS 2.0 Descriptor Set. |
| // This is defined by Microsoft OS 2.0 Descriptors Specification (July, |
| // 2018). |
| const vendorCode = rawData[offset + MS_OS_20_VENDOR_CODE_ITEM_OFFSET]!; |
| const data = new DataView(rawData.buffer, offset); |
| const msOs20DescriptorSetLength = |
| data.getUint16(MS_OS_20_SET_TOTAL_LENGTH_OFFSET, true); |
| |
| const button = this.getButtonElementFromTemplate_(); |
| item.labelElement.appendChild(button); |
| button.addEventListener('click', async (event: MouseEvent) => { |
| event.stopPropagation(); |
| // Clear all the descriptor display elements except the first one, which |
| // displays the original BOS descriptor. |
| Array.from(this.rootElement_.querySelectorAll('descriptorpanel')) |
| .slice(1) |
| .forEach(el => el.remove()); |
| this.rootElement_.querySelectorAll('descriptorpaneltitle') |
| .forEach(el => el.remove()); |
| const msOs20RawData = await this.getMsOs20DescriptorSet_( |
| vendorCode, msOs20DescriptorSetLength); |
| this.renderMsOs20DescriptorSet_(msOs20RawData); |
| }); |
| } |
| |
| /** |
| * Adds a button for sending a Microsoft OS 2.0 descriptor set alternate |
| * enumeration command to the USB device. |
| * @param offset The start offset of the Microsoft OS 2.0 |
| * descriptor set information. |
| */ |
| private renderMsOs20DescriptorSetAltEnum_( |
| rawData: Uint8Array, offset: number, item: CrTreeItemElement) { |
| // Use the vendor specified code, alternate enumeration code to send a |
| // Microsoft OS 2.0 set alternate enumeration command. |
| // This is defined by Microsoft OS 2.0 Descriptors Specification (July, |
| // 2018). |
| const altEnumCode = rawData[offset + MS_OS_20_ALT_ENUM_CODE_ITEM_OFFSET]!; |
| if (altEnumCode !== 0) { |
| const vendorCode = rawData[offset + MS_OS_20_VENDOR_CODE_ITEM_OFFSET]!; |
| |
| const button = this.getButtonElementFromTemplate_(); |
| item.labelElement.appendChild(button); |
| button.addEventListener('click', async (event: MouseEvent) => { |
| event.stopPropagation(); |
| await this.sendMsOs20DescriptorSetAltEnumCommand_( |
| vendorCode, altEnumCode); |
| }); |
| } |
| } |
| |
| /** |
| * Changes the display text in tree item for the Microsoft OS 2.0 registry |
| * property descriptor. |
| * @param offset The start offset of the registry Property |
| * descriptor. |
| */ |
| private renderFeatureRegistryPropertyDataItem_( |
| rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| _fieldLabel: string, featureRegistryPropertyDataType: number, |
| length: number) { |
| let data: DataView; |
| switch (featureRegistryPropertyDataType) { |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_BINARY: |
| break; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_DWORD_LITTLE_ENDIAN: |
| data = new DataView(rawData.buffer, offset); |
| item.label += data.getUint32(0, true); |
| break; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_DWORD_BIG_ENDIAN: |
| data = new DataView(rawData.buffer, offset); |
| item.label += data.getUint32(0, false); |
| break; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_SZ: |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_EXPAND_SZ: |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_LINK: |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_MULTI_SZ: |
| item.label += |
| decodeUtf16Array(rawData.slice(offset, offset + length), true); |
| break; |
| case 0: |
| default: |
| item.label += `Illegal Data Type. (${ |
| featureRegistryPropertyDataType} should be reserved.)`; |
| break; |
| } |
| } |
| |
| /** |
| * Renders a view to display standard descriptor hex data in both tree view |
| * and raw form view. |
| */ |
| private async renderStandardDescriptor_( |
| data: Uint8Array, languageCode: number = 0, |
| treeItem?: CrTreeItemElement) { |
| const displayElement = addNewDescriptorDisplayElement(this.rootElement_); |
| const rawDataTreeRoot = displayElement.rawDataTreeRoot; |
| const rawDataByteElement = displayElement.rawDataByteElement; |
| |
| renderRawDataBytes(rawDataByteElement, data); |
| |
| let offset = 0; |
| let indexInterface = 0; |
| let indexEndpoint = 0; |
| let indexUnknown = 0; |
| let indexDevCapability = 0; |
| |
| let expectNumInterfaces = 0; |
| let expectNumEndpoints = 0; |
| let expectNumDevCapabilities = 0; |
| |
| let lastInterfaceItem; |
| // Continue parsing while there are still unparsed standard descriptor. |
| // Stop if accessing the descriptor type would cause us to read past the |
| // end of the buffer. |
| while (offset + STANDARD_DESCRIPTOR_TYPE_OFFSET < data.length) { |
| const length = data[offset + STANDARD_DESCRIPTOR_LENGTH_OFFSET]!; |
| const descriptorType = data[offset + STANDARD_DESCRIPTOR_TYPE_OFFSET]!; |
| switch (descriptorType) { |
| case DEVICE_DESCRIPTOR_TYPE: |
| this.renderDeviceDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, data, offset); |
| break; |
| case CONFIGURATION_DESCRIPTOR_TYPE: |
| if (CONFIGURATION_DESCRIPTOR_NUM_INTERFACES_OFFSET < length) { |
| expectNumInterfaces = |
| data[offset + CONFIGURATION_DESCRIPTOR_NUM_INTERFACES_OFFSET]!; |
| } |
| this.renderConfigurationDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, data, offset); |
| break; |
| case STRING_DESCRIPTOR_TYPE: |
| this.renderStringDescriptorForLanguageCode_( |
| rawDataTreeRoot, rawDataByteElement, data, offset, languageCode, |
| treeItem); |
| break; |
| case INTERFACE_DESCRIPTOR_TYPE: |
| if (INTERFACE_DESCRIPTOR_NUM_ENDPOINTS_OFFSET < length) { |
| expectNumEndpoints += |
| data[offset + INTERFACE_DESCRIPTOR_NUM_ENDPOINTS_OFFSET]!; |
| } |
| lastInterfaceItem = this.renderInterfaceDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, data, offset, |
| indexInterface); |
| indexInterface++; |
| break; |
| case ENDPOINT_DESCRIPTOR_TYPE: |
| const treeRoot = lastInterfaceItem || rawDataTreeRoot; |
| this.renderEndpointDescriptor_( |
| treeRoot, rawDataByteElement, data, offset, indexEndpoint); |
| indexEndpoint++; |
| break; |
| case BOS_DESCRIPTOR_TYPE: |
| this.renderBosDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, data, offset); |
| expectNumDevCapabilities = |
| data[BOS_DESCRIPTOR_NUM_DEVICE_CAPABILITIES_OFFSET]!; |
| break; |
| case DEVICE_CAPABILITY_DESCRIPTOR_TYPE: |
| await this.renderDeviceCapabilityDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, data, offset, |
| indexDevCapability); |
| indexDevCapability++; |
| break; |
| default: |
| this.renderUnknownDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, data, offset, indexUnknown); |
| indexUnknown++; |
| break; |
| } |
| offset += length; |
| } |
| |
| if (expectNumInterfaces !== indexInterface) { |
| showError( |
| `Expected to find ${expectNumInterfaces} interface descriptors ` + |
| `but only encountered ${indexInterface}.`, |
| this.rootElement_); |
| } |
| |
| if (expectNumEndpoints !== indexEndpoint) { |
| showError( |
| `Expected to find ${expectNumEndpoints} interface descriptors ` + |
| `but only encountered ${indexEndpoint}.`, |
| this.rootElement_); |
| } |
| |
| if (expectNumDevCapabilities !== indexDevCapability) { |
| showError( |
| `Expected to find ${expectNumDevCapabilities} ` + |
| `device capability descriptors but only encountered ${ |
| indexDevCapability}.`, |
| this.rootElement_); |
| } |
| addMappingAction(rawDataTreeRoot, rawDataByteElement); |
| } |
| |
| /** |
| * Gets device descriptor of current device, and display it. |
| */ |
| async getDeviceDescriptor() { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| type: UsbControlTransferType.STANDARD, |
| recipient: UsbControlTransferRecipient.DEVICE, |
| request: GET_DESCRIPTOR_REQUEST, |
| value: DEVICE_DESCRIPTOR_TYPE << 8, |
| index: 0, |
| }; |
| |
| try { |
| await this.usbDeviceProxy_.open(); |
| const response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, DEVICE_DESCRIPTOR_LENGTH, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, 'Failed to read the device descriptor.', |
| this.rootElement_); |
| this.renderStandardDescriptor_(new Uint8Array(response.data.buffer)); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Renders a view to display device descriptor hex data in both tree view |
| * and raw form. |
| * @param offset The start offset of the device descriptor. |
| */ |
| private renderDeviceDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number) { |
| const fields = [ |
| { |
| label: `Length (should be ${DEVICE_DESCRIPTOR_LENGTH}): `, |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type (should be 0x01): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'USB Version: ', |
| size: 2, |
| formatter: formatUsbVersion, |
| }, |
| { |
| label: 'Class Code: ', |
| size: 1, |
| formatter: formatClassCode, |
| }, |
| { |
| label: 'Subclass Code: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Protocol Code: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Control Pipe Maximum Packet Size: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Vendor ID: ', |
| size: 2, |
| formatter: formatTwoBytesToHex, |
| }, |
| { |
| label: 'Product ID: ', |
| size: 2, |
| formatter: formatTwoBytesToHex, |
| }, |
| { |
| label: 'Device Version: ', |
| size: 2, |
| formatter: formatUsbVersion, |
| }, |
| { |
| label: 'Manufacturer String Index: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: this.renderIndexItem_.bind(this), |
| }, |
| { |
| label: 'Product String Index: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: this.renderIndexItem_.bind(this), |
| }, |
| { |
| label: 'Serial Number Index: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: this.renderIndexItem_.bind(this), |
| }, |
| { |
| label: 'Number of Configurations: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| ]; |
| |
| renderRawDataTree( |
| rawDataTreeRoot, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_); |
| |
| document.body.dispatchEvent(new CustomEvent( |
| 'device-descriptor-complete-for-test', |
| {bubbles: true, composed: true})); |
| } |
| |
| /** |
| * Gets configuration descriptor of current device, and display it. |
| */ |
| async getConfigurationDescriptor() { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| type: UsbControlTransferType.STANDARD, |
| recipient: UsbControlTransferRecipient.DEVICE, |
| request: GET_DESCRIPTOR_REQUEST, |
| value: CONFIGURATION_DESCRIPTOR_TYPE << 8, |
| index: 0, |
| }; |
| |
| try { |
| await this.usbDeviceProxy_.open(); |
| let response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, CONFIGURATION_DESCRIPTOR_LENGTH, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, |
| 'Failed to read the device configuration descriptor to determine ' + |
| 'the total descriptor length.', |
| this.rootElement_); |
| const dataView = |
| new DataView(new Uint8Array(response.data.buffer).buffer); |
| const length = dataView.getUint16( |
| CONFIGURATION_DESCRIPTOR_TOTAL_LENGTH_OFFSET, true); |
| // Re-gets the data using the full length. |
| response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, length, CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, |
| 'Failed to read the complete configuration descriptor.', |
| this.rootElement_); |
| this.renderStandardDescriptor_(new Uint8Array(response.data.buffer)); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Renders a view to display configuration descriptor hex data in both tree |
| * view and raw form. |
| * @param offset The start offset of the configuration descriptor. |
| */ |
| private renderConfigurationDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number) { |
| const fields = [ |
| { |
| label: `Length (should be ${CONFIGURATION_DESCRIPTOR_LENGTH}): `, |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type (should be 0x02): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Total Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Number of Interfaces: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Configuration Value: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Configuration String Index: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: this.renderIndexItem_.bind(this), |
| }, |
| { |
| label: 'Attribute Bitmap: ', |
| size: 1, |
| formatter: formatBitmap, |
| }, |
| { |
| label: 'Max Power (2mA increments): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| ]; |
| |
| renderRawDataTree( |
| rawDataTreeRoot, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_); |
| } |
| |
| /** |
| * Renders a tree item to display interface descriptor at index |
| * indexInterface. |
| * @param offset The start offset of the interface descriptor. |
| */ |
| private renderInterfaceDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number, |
| indexInterface: number): CrTreeItemElement { |
| const parentClassName = `descriptor-interface-${indexInterface}`; |
| const interfaceItem = |
| customTreeItem(`Interface ${indexInterface}`, parentClassName); |
| rawDataTreeRoot.add(interfaceItem); |
| |
| const fields = [ |
| { |
| label: `Length (should be ${INTERFACE_DESCRIPTOR_LENGTH}): `, |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type (should be 0x04): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Interface Number: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Alternate String: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Number of Endpoint: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Interface Class Code: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Interface Subclass Code: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Interface Protocol Code: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Interface String Index: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: this.renderIndexItem_.bind(this), |
| }, |
| ]; |
| |
| renderRawDataTree( |
| interfaceItem, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_, parentClassName); |
| |
| return interfaceItem; |
| } |
| |
| /** |
| * Renders a tree item to display endpoint descriptor at index |
| * indexEndpoint. |
| * @param offset The start offset of the endpoint |
| * descriptor. |
| */ |
| private renderEndpointDescriptor_( |
| rawDataTreeRoot: CrTreeElement|CrTreeItemElement, |
| rawDataByteElement: HTMLElement, rawData: Uint8Array, offset: number, |
| indexEndpoint: number) { |
| const parentClassName = `descriptor-endpoint-${indexEndpoint}`; |
| const endpointItem = |
| customTreeItem(`Endpoint ${indexEndpoint}`, parentClassName); |
| rawDataTreeRoot.add(endpointItem); |
| |
| const fields = [ |
| { |
| label: `Length (should be ${ENDPOINT_DESCRIPTOR_LENGTH}): `, |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type (should be 0x05): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'EndPoint Address: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Attribute Bitmap: ', |
| size: 1, |
| formatter: formatBitmap, |
| }, |
| { |
| label: 'Max Packet Size: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Interval: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| ]; |
| |
| renderRawDataTree( |
| endpointItem, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_, parentClassName); |
| } |
| |
| /** |
| * Renders a tree item to display length and type of unknown descriptor at |
| * index indexUnknown. |
| * @param originalOffset The start offset of the this descriptor. |
| */ |
| private renderUnknownDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, indexUnknown: number) { |
| const length = rawData[originalOffset + STANDARD_DESCRIPTOR_LENGTH_OFFSET]!; |
| const parentClassName = `descriptor-unknown-${indexUnknown}`; |
| const unknownItem = |
| customTreeItem(`Unknown Descriptor ${indexUnknown}`, parentClassName); |
| rawDataTreeRoot.add(unknownItem); |
| |
| const fields = [ |
| { |
| label: 'Length: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type: ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| ]; |
| |
| let offset = renderRawDataTree( |
| unknownItem, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| const rawDataByteElements = rawDataByteElement.querySelectorAll('span'); |
| |
| for (; offset < originalOffset + length; offset++) { |
| rawDataByteElements[offset]!.classList.add(`field-offset-${offset}`); |
| rawDataByteElements[offset]!.classList.add(parentClassName); |
| } |
| } |
| |
| /** |
| * Gets all the supported language codes of this device, and adds them as |
| * autocompletions for the language code input area in the string descriptor |
| * panel. |
| */ |
| async getAllLanguageCodes(): Promise<number[]> { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| type: UsbControlTransferType.STANDARD, |
| recipient: UsbControlTransferRecipient.DEVICE, |
| request: GET_DESCRIPTOR_REQUEST, |
| value: STRING_DESCRIPTOR_TYPE << 8, |
| index: 0, |
| }; |
| |
| let response; |
| try { |
| await this.usbDeviceProxy_.open(); |
| |
| response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, MAX_STRING_DESCRIPTOR_LENGTH, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| |
| checkTransferSuccess( |
| response.status, |
| 'Failed to read the device string descriptor to determine ' + |
| 'all supported languages.', |
| this.rootElement_); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| // Stop rendering autocomplete datalist if failed to read the string |
| // descriptor. |
| return []; |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| |
| const responseData = new Uint8Array(response.data.buffer); |
| assert(this.languageCodesListElement_); |
| this.languageCodesListElement_.innerText = ''; |
| |
| const optionAllElement = document.createElement('option'); |
| optionAllElement.value = 'All'; |
| this.languageCodesListElement_.appendChild(optionAllElement); |
| |
| const languageCodesList = []; |
| // First two bytes are length and descriptor type(0x03); |
| for (let i = 2; i < responseData.length; i += 2) { |
| const languageCode = parseShort(responseData, i); |
| |
| const optionElement = document.createElement('option'); |
| optionElement.label = parseLanguageCode(languageCode); |
| optionElement.value = `0x${toHex(languageCode, 4)}`; |
| |
| this.languageCodesListElement_.appendChild(optionElement); |
| |
| languageCodesList.push(languageCode); |
| } |
| return languageCodesList; |
| } |
| |
| /** |
| * Gets the string descriptor for the current device with the given index |
| * and language code, and display it. |
| */ |
| private async getStringDescriptorForLanguageCode_( |
| index: number, languageCode: number, treeItem?: CrTreeItemElement) { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| type: UsbControlTransferType.STANDARD, |
| recipient: UsbControlTransferRecipient.DEVICE, |
| request: GET_DESCRIPTOR_REQUEST, |
| index: languageCode, |
| value: (STRING_DESCRIPTOR_TYPE << 8) | index, |
| }; |
| |
| try { |
| await this.usbDeviceProxy_.open(); |
| const response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, MAX_STRING_DESCRIPTOR_LENGTH, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, |
| `Failed to read the device string descriptor of index: ${ |
| index}, language: ${parseLanguageCode(languageCode)}.`, |
| this.rootElement_); |
| |
| assert(this.indexInput_); |
| this.indexInput_.value = index.toString(); |
| this.renderStandardDescriptor_( |
| new Uint8Array(response.data.buffer), languageCode, treeItem); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Renders string descriptor of current device with given index and language |
| * code. |
| * @param offset The start offset of the string descriptor. |
| */ |
| private renderStringDescriptorForLanguageCode_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number, languageCode: number = 0, |
| treeItem?: CrTreeItemElement) { |
| this.rootElement_.hidden = false; |
| |
| const languageStr = parseLanguageCode(languageCode); |
| |
| const fields = [ |
| { |
| 'label': 'Length: ', |
| 'size': 1, |
| 'formatter': formatByte, |
| }, |
| { |
| 'label': 'Descriptor Type (should be 0x03): ', |
| 'size': 1, |
| 'formatter': formatDescriptorType, |
| }, |
| ]; |
| |
| // The first two elements are length and descriptor type. |
| for (let i = 2; i < rawData.length; i += 2) { |
| const field = { |
| 'label': '', |
| 'size': 2, |
| 'formatter': formatLetter, |
| }; |
| fields.push(field); |
| } |
| |
| // The first two elements of rawData are length and descriptor type. |
| const stringDescriptor = decodeUtf16Array(rawData.slice(2), true); |
| const parentClassName = `descriptor-string-language-${languageStr}`; |
| const stringDescriptorItem = |
| customTreeItem(`${languageStr}: ${stringDescriptor}`, parentClassName); |
| rawDataTreeRoot.add(stringDescriptorItem); |
| if (treeItem) { |
| treeItem.add(customTreeItem(`${languageStr}: ${stringDescriptor}`)); |
| treeItem.toggleAttribute('expanded', true); |
| } |
| |
| renderRawDataTree( |
| stringDescriptorItem, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_, parentClassName); |
| } |
| |
| /** |
| * Gets string descriptor in all supported languages of current device with |
| * given index. |
| */ |
| private async getStringDescriptorForAllLanguages_( |
| index: number, treeItem?: CrTreeItemElement) { |
| this.rootElement_.hidden = false; |
| |
| assert(this.indexInput_); |
| this.indexInput_.value = index.toString(); |
| |
| const languageCodesList = await this.getAllLanguageCodes(); |
| |
| assert(treeItem); |
| for (const languageCode of languageCodesList) { |
| await this.getStringDescriptorForLanguageCode_( |
| index, languageCode, treeItem); |
| } |
| } |
| |
| /** |
| * Initializes the string descriptor panel for autocomplete functionality. |
| */ |
| initialStringDescriptorPanel(tabId: string) { |
| // Binds the input area and datalist use each tab's unique id. |
| this.rootElement_.querySelectorAll('input').forEach( |
| el => el.setAttribute('list', `${el.getAttribute('list')}-${tabId}`)); |
| this.rootElement_.querySelectorAll('datalist') |
| .forEach(el => el.id = `${el.id}-${tabId}`); |
| |
| const button = this.rootElement_.querySelector('button'); |
| assert(button); |
| const indexInput = |
| this.rootElement_.querySelector<HTMLInputElement>('#index-input'); |
| this.indexInput_ = indexInput; |
| const languageCodeInput = this.rootElement_.querySelector<HTMLInputElement>( |
| '#language-code-input'); |
| assert(languageCodeInput); |
| |
| button.addEventListener('click', async () => { |
| this.clearView(); |
| assert(this.indexInput_); |
| const index = Number.parseInt(this.indexInput_.value, 10); |
| if (this.checkParamValid_(index, 'Index', 1, 255)) { |
| if (languageCodeInput.value === 'All') { |
| await this.getStringDescriptorForAllLanguages_(index); |
| } else { |
| const languageCode = Number.parseInt(languageCodeInput.value, 10); |
| if (this.checkParamValid_(languageCode, 'Language Code', 0, 65535)) { |
| await this.getStringDescriptorForLanguageCode_(index, languageCode); |
| } |
| } |
| } |
| }); |
| |
| this.stringDescriptorIndexes = new Set<number>(); |
| this.indexesListElement = |
| this.rootElement_.querySelector<HTMLElement>(`#indexes-${tabId}`); |
| assert(this.indexesListElement); |
| this.languageCodesListElement_ = |
| this.rootElement_.querySelector<HTMLElement>(`#languages-${tabId}`); |
| assert(this.languageCodesListElement_); |
| } |
| |
| /** |
| * Gets the Binary device Object Store (BOS) descriptor of the current |
| * device, which contains the WebUSB descriptor and Microsoft OS 2.0 |
| * descriptor, and display it. |
| */ |
| async getBosDescriptor() { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| type: UsbControlTransferType.STANDARD, |
| recipient: UsbControlTransferRecipient.DEVICE, |
| request: GET_DESCRIPTOR_REQUEST, |
| value: BOS_DESCRIPTOR_TYPE << 8, |
| index: 0, |
| }; |
| |
| try { |
| await this.usbDeviceProxy_.open(); |
| let response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, BOS_DESCRIPTOR_HEADER_LENGTH, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, |
| 'Failed to read the device BOS descriptor to determine ' + |
| 'the total descriptor length.', |
| this.rootElement_); |
| const dataView = |
| new DataView(new Uint8Array(response.data.buffer).buffer); |
| const length = |
| dataView.getUint16(BOS_DESCRIPTOR_TOTAL_LENGTH_OFFSET, true); |
| // Re-gets the data using the full length. |
| response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, length, CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, 'Failed to read the complete BOS descriptor.', |
| this.rootElement_); |
| await this.renderStandardDescriptor_( |
| new Uint8Array(response.data.buffer)); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Renders a view to display Binary device Object Store (BOS) descriptor hex |
| * data in both tree view and raw form. |
| * @param offset The start offset of the BOS descriptor. |
| */ |
| private renderBosDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number) { |
| const fields = [ |
| { |
| 'label': 'Length (should be 5): ', |
| 'size': 1, |
| 'formatter': formatByte, |
| }, |
| { |
| 'label': 'Descriptor Type (should be 0x0F): ', |
| 'size': 1, |
| 'formatter': formatDescriptorType, |
| }, |
| { |
| 'label': 'Total Length: ', |
| 'size': 2, |
| 'formatter': formatShort, |
| }, |
| { |
| 'label': 'Number of Device Capability Descriptors: ', |
| 'size': 1, |
| 'formatter': formatByte, |
| }, |
| ]; |
| |
| renderRawDataTree( |
| rawDataTreeRoot, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_); |
| } |
| |
| /** |
| * Renders a view to display device capability descriptor hex data in both |
| * tree view and raw form. |
| * @param offset The start offset of the BOS descriptor. |
| */ |
| private renderDeviceCapabilityDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number, indexDevCapability: number) { |
| switch (rawData[offset + BOS_DESCRIPTOR_DEVICE_CAPABILITY_TYPE_OFFSET]) { |
| case DEVICE_CAPABILITY_DESCRIPTOR_TYPE_PLATFORM_TYPE: |
| if (isSameUuid(rawData, offset, WEB_USB_CAPABILITY_UUID)) { |
| this.renderWebUsbPlatformDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, rawData, offset, |
| indexDevCapability); |
| break; |
| } else if (isSameUuid( |
| rawData, offset, MS_OS_20_PLATFORM_CAPABILITY_UUID)) { |
| this.renderMsOs20PlatformDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, rawData, offset, |
| indexDevCapability); |
| break; |
| } else { |
| this.renderUnknownBosDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, rawData, offset, |
| indexDevCapability); |
| break; |
| } |
| default: |
| this.renderUnknownBosDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, rawData, offset, |
| indexDevCapability); |
| } |
| } |
| |
| /** |
| * Renders a tree item to display WebUSB platform capability descriptor at |
| * index indexWebUsb. |
| * @param offset The start offset of the WebUSB platform |
| * capability descriptor. |
| */ |
| private renderWebUsbPlatformDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number, indexWebUsb: number) { |
| const parentClassName = `descriptor-webusb-${indexWebUsb}`; |
| const webUsbItem = customTreeItem('WebUSB Descriptor', parentClassName); |
| rawDataTreeRoot.add(webUsbItem); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 24): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type (should be 0x10): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Device Capability Descriptor Type (should be 0x05): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Reserved (should be 0): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Platform Capability UUID: ', |
| size: 16, |
| formatter: formatUuid, |
| }, |
| { |
| label: 'Protocol Version Supported (should be 1.0.0): ', |
| size: 2, |
| formatter: formatUsbVersion, |
| }, |
| { |
| label: 'Vendor Code: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Landing Page: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: this.renderUrlDescriptorIndexItem_.bind(this), |
| }, |
| ]; |
| renderRawDataTree( |
| webUsbItem, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_, parentClassName); |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 platform capability |
| * descriptor at index indexMsOs20. |
| * @param offset The start offset of the Microsoft OS 2.0 platform |
| * capability descriptor. |
| */ |
| private renderMsOs20PlatformDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number, indexMsOs20: number) { |
| const parentClassName = `descriptor-ms-os-20-${indexMsOs20}`; |
| const msOs20Item = |
| customTreeItem(`Microsoft OS 2.0 Descriptor`, parentClassName); |
| rawDataTreeRoot.add(msOs20Item); |
| |
| const fields = [ |
| { |
| label: 'Length: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type (should be 0x10): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Device Capability Descriptor Type (should be 0x05): ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Reserved (should be 0): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Platform Capability UUID: ', |
| size: 16, |
| formatter: formatUuid, |
| }, |
| ]; |
| |
| offset = renderRawDataTree( |
| msOs20Item, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_, parentClassName); |
| |
| let indexMsOs20DescriptorSetInfo = 0; |
| // Continue parsing while there are still unparsed Microsoft OS 2.0 |
| // descriptor set information structures. Stop if accessing the descriptor |
| // set information structure would cause us to read past the end of the |
| // buffer. |
| while (offset < rawData.length) { |
| offset = this.renderMsOs20DescriptorSetInfo_( |
| msOs20Item, rawDataByteElement, rawData, offset, |
| indexMsOs20DescriptorSetInfo, indexMsOs20); |
| indexMsOs20DescriptorSetInfo++; |
| } |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 descriptor set |
| * information at index indexMsOs20DescriptorSetInfo. |
| * @param offset The start offset of the Microsoft OS 2.0 |
| * set information structure. |
| */ |
| private renderMsOs20DescriptorSetInfo_( |
| rawDataTreeRoot: CrTreeItemElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, offset: number, indexMsOs20DescriptorSetInfo: number, |
| indexMsOs20: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-set-info-${indexMsOs20DescriptorSetInfo}`; |
| const msOs20SetInfoItem = customTreeItem( |
| `Microsoft OS 2.0 Descriptor Set Information`, parentClassName); |
| rawDataTreeRoot.add(msOs20SetInfoItem); |
| |
| const fields = [ |
| { |
| label: 'Windows Version: ', |
| size: 4, |
| formatter: formatWindowsVersion, |
| }, |
| { |
| label: 'Total Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Vendor Code: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: |
| (rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| _fieldLabel: string) => |
| this.renderMsOs20DescriptorVendorSpecific_( |
| rawData, offset - MS_OS_20_VENDOR_CODE_ITEM_OFFSET, item), |
| }, |
| { |
| label: 'Alternate Enumeration Code: ', |
| size: 1, |
| formatter: formatByte, |
| extraTreeItemFormatter: |
| (rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| _fieldLabel: string) => |
| this.renderMsOs20DescriptorSetAltEnum_( |
| rawData, offset - MS_OS_20_ALT_ENUM_CODE_ITEM_OFFSET, item), |
| }, |
| ]; |
| |
| return renderRawDataTree( |
| msOs20SetInfoItem, rawDataByteElement, fields, rawData, offset, |
| this.rootElement_, parentClassName, |
| `descriptor-ms-os-20-${indexMsOs20}`); |
| } |
| |
| /** |
| * Renders a tree item to display unknown device capability descriptor at |
| * indexUnknownDevCapability |
| * @param originalOffset The start offset of the unknown device |
| * capability descriptor. |
| */ |
| private renderUnknownBosDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexUnknownDevCapability: number) { |
| const length = rawData[originalOffset + STANDARD_DESCRIPTOR_LENGTH_OFFSET]!; |
| |
| const parentClassName = |
| `descriptor-unknownbos-${indexUnknownDevCapability}`; |
| const unknownBosItem = |
| customTreeItem(`Unknown BOS Descriptor`, parentClassName); |
| rawDataTreeRoot.add(unknownBosItem); |
| |
| const fields = [ |
| { |
| label: 'Length: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Descriptor Type: ', |
| size: 1, |
| formatter: formatDescriptorType, |
| }, |
| { |
| label: 'Device Capability Descriptor Type: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| ]; |
| |
| let offset = renderRawDataTree( |
| unknownBosItem, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| const rawDataByteElements = rawDataByteElement.querySelectorAll('span'); |
| |
| for (; offset < originalOffset + length; offset++) { |
| rawDataByteElements[offset]!.classList.add(`field-offset-${offset}`); |
| rawDataByteElements[offset]!.classList.add(parentClassName); |
| } |
| } |
| |
| /** |
| * Gets the URL Descriptor, renders a URL descriptor item and adds it to |
| * the URL descriptor index item. |
| * @param offset The offset of the WebUSB descriptor. |
| * @param item The URL descriptor index item. |
| */ |
| private async getUrlDescriptor_( |
| rawData: Uint8Array, offset: number, item: CrTreeItemElement) { |
| // The second to last byte is the vendor code used to query URL |
| // descriptor. Last byte is index of url descriptor. These are defined by |
| // the WebUSB specification: http://wicg.github.io/webusb/ |
| const vendorCode = rawData[offset + WEB_USB_VENDOR_CODE_OFFSET]!; |
| const urlIndex = rawData[offset + WEB_USB_URL_DESCRIPTOR_INDEX_OFFSET]!; |
| |
| const usbControlTransferParams: UsbControlTransferParams = { |
| recipient: UsbControlTransferRecipient.DEVICE, |
| // These constants are defined by the WebUSB specification: |
| // http://wicg.github.io/webusb/ |
| type: UsbControlTransferType.VENDOR, |
| request: vendorCode, |
| value: urlIndex, |
| index: GET_URL_REQUEST, |
| }; |
| |
| try { |
| await this.usbDeviceProxy_.open(); |
| // Gets the URL descriptor. |
| const urlResponse = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, MAX_URL_DESCRIPTOR_LENGTH, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| |
| checkTransferSuccess( |
| urlResponse.status, 'Failed to read the device URL descriptor.', |
| this.rootElement_); |
| |
| let url: string = ''; |
| // URL Prefixes are defined by Chapter 4.3.1 of the WebUSB |
| // specification: http://wicg.github.io/webusb/ |
| switch (urlResponse.data.buffer[2]) { |
| case 0: |
| url = 'http://'; |
| break; |
| case 1: |
| url = 'https://'; |
| break; |
| case 255: |
| default: |
| url = ''; |
| } |
| // The first three elements of urlResponse.data are length, descriptor |
| // type and URL scheme prefix. |
| url += decodeUtf8Array(new Uint8Array(urlResponse.data.buffer.slice(3))); |
| |
| const landingPageItem = customTreeItem(url, 'descriptor-url'); |
| landingPageItem.labelElement.addEventListener( |
| 'click', () => window.open(url, '_blank')); |
| item.add(landingPageItem); |
| item.expanded = true; |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| // Stops parsing to string format URL if failed to read the URL |
| // descriptor. |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Gets the Microsoft OS 2.0 Descriptor vendor-specific descriptor. |
| */ |
| private async getMsOs20DescriptorSet_( |
| vendorCode: number, |
| msOs20DescriptorSetLength: number): Promise<Uint8Array> { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| recipient: UsbControlTransferRecipient.DEVICE, |
| // These constants are defined by Microsoft OS 2.0 Descriptors |
| // Specification (July, 2018). |
| type: UsbControlTransferType.VENDOR, |
| request: vendorCode, |
| value: 0, |
| index: MS_OS_20_DESCRIPTOR_INDEX, |
| }; |
| |
| let response; |
| try { |
| await this.usbDeviceProxy_.open(); |
| // Gets the Microsoft OS 2.0 descriptor set. |
| response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, msOs20DescriptorSetLength, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| |
| checkTransferSuccess( |
| response.status, |
| 'Failed to read the Microsoft OS 2.0 descriptor set.', |
| this.rootElement_); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| // Returns an empty array if failed to read the Microsoft OS 2.0 |
| // descriptor set. |
| return new Uint8Array(0); |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| |
| return new Uint8Array(response.data.buffer); |
| } |
| |
| /** |
| * Sends the Microsoft OS 2.0 Descriptor set alternate enumeration command. |
| */ |
| private async sendMsOs20DescriptorSetAltEnumCommand_( |
| vendorCode: number, altEnumCode: number) { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| recipient: UsbControlTransferRecipient.DEVICE, |
| // These constants are defined by Microsoft OS 2.0 Descriptors |
| // Specification (July, 2018). |
| type: UsbControlTransferType.VENDOR, |
| request: vendorCode, |
| value: altEnumCode, |
| index: MS_OS_20_SET_ALT_ENUMERATION, |
| }; |
| |
| try { |
| await this.usbDeviceProxy_.open(); |
| // Sends the Microsoft OS 2.0 descriptor set alternate enumeration |
| // command. It doesn't need extra bytes to send the device in the body |
| // of the request. |
| const response = await this.usbDeviceProxy_.controlTransferOut( |
| usbControlTransferParams, {buffer: []}, CONTROL_TRANSFER_TIMEOUT_MS); |
| |
| checkTransferSuccess( |
| response.status, |
| 'Failed to read the Microsoft OS 2.0 descriptor ' + |
| 'alternate enumeration set.', |
| this.rootElement_); |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Renders a view to display Microsoft OS 2.0 Descriptor Set hex data in |
| * both tree view and raw form. |
| */ |
| private renderMsOs20DescriptorSet_(msOs20RawData: Uint8Array) { |
| const displayElement = addNewDescriptorDisplayElement( |
| this.rootElement_, 'Microsoft OS 2.0 Descriptor Set'); |
| const rawDataTreeRoot = displayElement.rawDataTreeRoot; |
| const rawDataByteElement = displayElement.rawDataByteElement; |
| renderRawDataBytes(rawDataByteElement, msOs20RawData); |
| |
| let msOs20DescriptorOffset = 0; |
| let indexMsOs20Descriptor = 0; |
| const data = new DataView(msOs20RawData.buffer); |
| // Continue parsing while there are still unparsed Microsoft OS 2.0 |
| // Descriptor Set. Stop if accessing the descriptor type (two bytes) |
| // would cause us to read past the end of the buffer. |
| while (msOs20DescriptorOffset + MS_OS_20_DESCRIPTOR_TYPE_OFFSET + 1 < |
| msOs20RawData.length) { |
| const msOs20DescriptorType = data.getUint16( |
| msOs20DescriptorOffset + MS_OS_20_DESCRIPTOR_TYPE_OFFSET, true); |
| switch (msOs20DescriptorType) { |
| case MS_OS_20_SET_HEADER_DESCRIPTOR: |
| msOs20DescriptorOffset = this.renderMsOs20SetHeader_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset); |
| break; |
| case MS_OS_20_SUBSET_HEADER_CONFIGURATION: |
| msOs20DescriptorOffset = this.renderMsOs20ConfigurationSubsetHeader_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_SUBSET_HEADER_FUNCTION: |
| msOs20DescriptorOffset = this.renderMsOs20FunctionSubsetHeader_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_FEATURE_COMPATIBLE_ID: |
| msOs20DescriptorOffset = this.renderMsOs20FeatureCompatibleId_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_FEATURE_REG_PROPERTY: |
| msOs20DescriptorOffset = this.renderMsOs20FeatureRegistryProperty_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_FEATURE_MIN_RESUME_TIME: |
| msOs20DescriptorOffset = this.renderMsOs20FeatureMinResumeTime_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_FEATURE_MODEL_ID: |
| msOs20DescriptorOffset = this.renderMsOs20FeatureModelId_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_FEATURE_CCGP_DEVICE: |
| msOs20DescriptorOffset = this.renderMsOs20FeatureCcgpDevice_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| case MS_OS_20_FEATURE_VENDOR_REVISION: |
| msOs20DescriptorOffset = this.renderMsOs20FeatureVendorRevision_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| break; |
| default: |
| msOs20DescriptorOffset = |
| this.renderUnknownMsOs20DescriptorDescriptor_( |
| rawDataTreeRoot, rawDataByteElement, msOs20RawData, |
| msOs20DescriptorOffset, indexMsOs20Descriptor); |
| indexMsOs20Descriptor++; |
| } |
| } |
| addMappingAction(rawDataTreeRoot, rawDataByteElement); |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 descriptor set header. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * descriptor set header. |
| */ |
| private renderMsOs20SetHeader_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number): number { |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 10): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 0): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Windows Version: ', |
| size: 4, |
| formatter: formatWindowsVersion, |
| }, |
| { |
| label: 'Total Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| rawDataTreeRoot, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 Descriptor ' + |
| 'Set header.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 configuration subset |
| * header. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * configuration subset header. |
| */ |
| private renderMsOs20ConfigurationSubsetHeader_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Configuration Subset Header', parentClassName); |
| rawDataTreeRoot.add(item); |
| |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 8): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 1): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Configuration Value: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Reserved (should be 0): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Total Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Configuration Subset Header.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 function subset header. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * function subset header. |
| */ |
| private renderMsOs20FunctionSubsetHeader_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Function Subset Header', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 8): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 2): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'First Interface Number: ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Reserved (should be 0): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Total Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Function Subset Header.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 compatible ID Descriptor. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * compatible ID descriptor. |
| */ |
| private renderMsOs20FeatureCompatibleId_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Compatible ID Descriptor', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 20): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 3): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Compatible ID String: ', |
| size: 8, |
| formatter: formatCompatibleIdString, |
| }, |
| { |
| label: 'Sub-compatible ID String: ', |
| size: 8, |
| formatter: formatCompatibleIdString, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Compatible ID Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 registry property |
| * descriptor. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * registry property descriptor. |
| */ |
| private renderMsOs20FeatureRegistryProperty_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Registry Property Descriptor', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| const featureRegistryPropertyDataType = data.getUint16( |
| originalOffset + |
| MS_OS_20_REGISTRY_PROPERTY_DESCRIPTOR_PROPERTY_DATA_TYPE_OFFSET, |
| true); |
| const propertyNameLength = data.getUint16( |
| originalOffset + |
| MS_OS_20_REGISTRY_PROPERTY_DESCRIPTOR_NAME_LENGTH_OFFSET, |
| true); |
| const fields = [ |
| { |
| label: 'Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 4): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Property Data Type: ', |
| size: 2, |
| formatter: formatFeatureRegistryPropertyDataType, |
| }, |
| { |
| label: 'Property Name Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Property Name: ', |
| size: propertyNameLength, |
| formatter: formatUnknown, |
| extraTreeItemFormatter: |
| (rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| fieldLabel: string) => |
| this.renderFeatureRegistryPropertyDataItem_( |
| rawData, offset, item, fieldLabel, |
| featureRegistryPropertyDataType, propertyNameLength), |
| }, |
| ]; |
| |
| let offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| while (offset < originalOffset + length) { |
| const propertyDataLength = data.getUint16(offset, true); |
| const propertyDataFields = [ |
| { |
| label: 'Property Data Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Property Data: ', |
| size: propertyDataLength, |
| formatter: formatUnknown, |
| extraTreeItemFormatter: |
| (rawData: Uint8Array, offset: number, item: CrTreeItemElement, |
| fieldLabel: string) => |
| this.renderFeatureRegistryPropertyDataItem_( |
| rawData, offset, item, fieldLabel, |
| featureRegistryPropertyDataType, propertyDataLength), |
| }, |
| ]; |
| offset = renderRawDataTree( |
| item, rawDataByteElement, propertyDataFields, rawData, offset, |
| this.rootElement_, parentClassName); |
| } |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Registry Property Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 minimum USB resume time |
| * descriptor. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * minimum USB resume time descriptor. |
| */ |
| private renderMsOs20FeatureMinResumeTime_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Minimum USB Resume Time Descriptor', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 6): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 5): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Resume Recovery Time (milliseconds): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| { |
| label: 'Resume Signaling Time (milliseconds): ', |
| size: 1, |
| formatter: formatByte, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Minimum USB Resume Time Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 model ID descriptor. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * model ID descriptor. |
| */ |
| private renderMsOs20FeatureModelId_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = |
| customTreeItem('Microsoft OS 2.0 Model ID Descriptor', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 20): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 6): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Model ID: ', |
| size: 16, |
| formatter: formatUuid, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Model ID Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 Common Class Generic |
| * Parent (CCGP) device descriptor. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * CCGP device descriptor. |
| */ |
| private renderMsOs20FeatureCcgpDevice_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Common Class Generic Parent (CCGP) Device ' + |
| 'Descriptor', |
| parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 4): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 7): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'CCGP Device Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display Microsoft OS 2.0 vendor revision |
| * descriptor. |
| * @param originalOffset The start offset of the Microsoft OS 2.0 |
| * vendor revision descriptor. |
| */ |
| private renderMsOs20FeatureVendorRevision_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexMsOs20Descriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexMsOs20Descriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Vendor Revision Descriptor', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length (should be 6): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type (should be 8): ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'Vendor Revision: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| ]; |
| |
| const offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Vendor Revision Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Renders a tree item to display an unknown Microsoft OS 2.0 descriptor. |
| * @param originalOffset The start offset of the unknown Microsoft |
| * OS 2.0 descriptor. |
| */ |
| private renderUnknownMsOs20DescriptorDescriptor_( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement, |
| rawData: Uint8Array, originalOffset: number, |
| indexDescriptor: number): number { |
| const parentClassName = |
| `descriptor-ms-os-20-subdescriptor-${indexDescriptor}`; |
| const item = customTreeItem( |
| 'Microsoft OS 2.0 Descriptor Unknown Descriptor', parentClassName); |
| rawDataTreeRoot.add(item); |
| const data = new DataView(rawData.buffer); |
| const length = data.getUint16( |
| originalOffset + MS_OS_20_DESCRIPTOR_LENGTH_OFFSET, true); |
| |
| const fields = [ |
| { |
| label: 'Length: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| { |
| label: 'MS OS 2.0 Descriptor Type: ', |
| size: 2, |
| formatter: formatShort, |
| }, |
| ]; |
| |
| let offset = renderRawDataTree( |
| item, rawDataByteElement, fields, rawData, originalOffset, |
| this.rootElement_, parentClassName); |
| |
| const rawDataByteElements = rawDataByteElement.querySelectorAll('span'); |
| |
| for (; offset < originalOffset + length; offset++) { |
| rawDataByteElements[offset]!.classList.add( |
| `field-offset-${offset}`, parentClassName); |
| } |
| |
| if (offset !== originalOffset + length) { |
| showError( |
| 'An error occurred while rendering Microsoft OS 2.0 ' + |
| 'Unknown Descriptor.', |
| this.rootElement_); |
| } |
| |
| return offset; |
| } |
| |
| /** |
| * Gets response of the given request. |
| */ |
| private async sendTestingRequest_( |
| usbControlTransferParams: UsbControlTransferParams, length: number, |
| direction: string) { |
| try { |
| await this.usbDeviceProxy_.open(); |
| |
| if (direction === 'Device-to-Host') { |
| const response = await this.usbDeviceProxy_.controlTransferIn( |
| usbControlTransferParams, length, CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, 'Failed to send request.', this.rootElement_); |
| this.renderTestingData_(new Uint8Array(response.data.buffer)); |
| } else if (direction === 'Host-to-Device') { |
| const textarea = this.rootElement_.querySelector('textarea'); |
| assert(textarea); |
| const dataString = textarea.value; |
| |
| const data = []; |
| for (let i = 0; i < dataString.length; i += 2) { |
| data.push(Number.parseInt(dataString.substring(i, i + 2), 16)); |
| } |
| |
| const response = await this.usbDeviceProxy_.controlTransferOut( |
| usbControlTransferParams, {buffer: data}, |
| CONTROL_TRANSFER_TIMEOUT_MS); |
| checkTransferSuccess( |
| response.status, 'Failed to send request.', this.rootElement_); |
| } |
| } catch (e) { |
| showError((e as Error).message, this.rootElement_); |
| return; |
| } finally { |
| await this.usbDeviceProxy_.close(); |
| } |
| } |
| |
| /** |
| * Renders a view to display response data in hex format. |
| */ |
| private renderTestingData_(rawData: Uint8Array) { |
| const displayElement = addNewDescriptorDisplayElement(this.rootElement_); |
| const rawDataTreeRoot = displayElement.rawDataTreeRoot; |
| rawDataTreeRoot.style.display = 'none'; |
| const rawDataByteElement = displayElement.rawDataByteElement; |
| renderRawDataBytes(rawDataByteElement, rawData); |
| } |
| |
| /** |
| * Initializes the testing tool panel for input and query functionality. |
| */ |
| initialTestingToolPanel() { |
| showWarn( |
| 'Warning: This tool can send arbitrary commands to the device. ' + |
| 'Invalid commands may cause unexpected results.', |
| this.rootElement_); |
| const tbody = this.rootElement_.querySelector('tbody'); |
| assert(tbody); |
| const inputTableRows = tbody.querySelectorAll('tr'); |
| const buttons = tbody.querySelectorAll('button'); |
| const dataInputArea = this.rootElement_.querySelector('textarea'); |
| assert(dataInputArea); |
| dataInputArea.addEventListener('keypress', () => { |
| const index = dataInputArea.selectionStart; |
| dataInputArea.value = dataInputArea.value.substring(0, index) + |
| dataInputArea.value.substring(index + 1); |
| dataInputArea.selectionEnd = index; |
| }); |
| |
| const testingToolPanelInputTypeSelector = |
| this.rootElement_.querySelector<HTMLSelectElement>('#input-type'); |
| assert(testingToolPanelInputTypeSelector); |
| testingToolPanelInputTypeSelector.addEventListener('change', () => { |
| this.clearView(); |
| const index = testingToolPanelInputTypeSelector.selectedIndex; |
| inputTableRows.forEach(row => row.hidden = true); |
| const rowAtIndex = inputTableRows[index]; |
| assert(rowAtIndex); |
| rowAtIndex.hidden = false; |
| |
| const direction = getRequestTypeDirection(rowAtIndex, index); |
| const length = getRequestLength(rowAtIndex, index); |
| const area = |
| this.rootElement_.querySelector<HTMLElement>('#data-input-area'); |
| assert(area); |
| area.hidden = (direction !== 'Host-to-Device'); |
| dataInputArea.value = '00'.repeat(length); |
| dataInputArea.maxLength = length * 2; |
| }); |
| |
| |
| inputTableRows.forEach((el, i) => { |
| const inputTableRow = el; |
| let directionInputElement: HTMLElement|null = null; |
| switch (i) { |
| case INPUT_TYPE_DECIMAL_WITH_DROPDOWN: |
| directionInputElement = |
| inputTableRow.querySelector('#transfer-direction'); |
| break; |
| case INPUT_TYPE_HEX_BYTE: |
| directionInputElement = |
| inputTableRow.querySelector('#query-request-type'); |
| break; |
| } |
| assert(directionInputElement); |
| directionInputElement.addEventListener('change', () => { |
| const area = |
| this.rootElement_.querySelector<HTMLElement>('#data-input-area'); |
| assert(area); |
| area.hidden = |
| (getRequestTypeDirection(inputTableRow, i) !== 'Host-to-Device'); |
| }); |
| |
| inputTableRow.querySelector('#query-length')!.addEventListener( |
| 'blur', () => { |
| const length = getRequestLength(inputTableRow, i); |
| dataInputArea.value = '00'.repeat(length); |
| dataInputArea.maxLength = length * 2; |
| }); |
| }); |
| |
| buttons.forEach((button, i) => { |
| button.addEventListener('click', () => { |
| this.clearView(); |
| |
| const row = inputTableRows[i]; |
| assert(row); |
| const direction = getRequestTypeDirection(row, i); |
| const type = getRequestType(row, i); |
| const recipient = getRequestTypeRecipient(row, i); |
| const request = getRequestCode(row, i); |
| const value = getRequestValue(row, i); |
| const index = getRequestIndex(row, i); |
| const dataLength = getRequestLength(row, i); |
| |
| const transferType = this.convertStringToTransferType_(type); |
| const transferRecipient = |
| this.convertStringToTransferRecipient_(recipient); |
| |
| if (transferType !== null && transferRecipient !== null && |
| this.checkParamValid_(request, 'Transfer Request', 0, 255) && |
| this.checkParamValid_(value, 'wValue', 0, 65535) && |
| this.checkParamValid_(index, 'wIndex', 0, 65535) && |
| this.checkParamValid_(dataLength, 'Length', 0, 65535)) { |
| const usbControlTransferParams: UsbControlTransferParams = { |
| type: transferType, |
| recipient: transferRecipient, |
| request, |
| value, |
| index, |
| }; |
| this.sendTestingRequest_( |
| usbControlTransferParams, dataLength, direction); |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * Checks if the user input is a valid number. |
| */ |
| private checkParamValid_( |
| paramValue: number, paramName: string, min: number, |
| max: number): boolean { |
| if (Number.isNaN(paramValue) || paramValue < min || paramValue > max) { |
| showError(`Invalid ${paramName}.`, this.rootElement_); |
| return false; |
| } |
| return true; |
| } |
| |
| private convertStringToTransferType_(enumString: string): |
| UsbControlTransferType|null { |
| if (enumString === 'STANDARD') { |
| return UsbControlTransferType.STANDARD; |
| } |
| if (enumString === 'CLASS') { |
| return UsbControlTransferType.CLASS; |
| } |
| if (enumString === 'VENDOR') { |
| return UsbControlTransferType.VENDOR; |
| } |
| if (enumString === 'RESERVED') { |
| return UsbControlTransferType.RESERVED; |
| } |
| showError('Invalid Transfer Type', this.rootElement_); |
| return null; |
| } |
| |
| private convertStringToTransferRecipient_(enumString: string): |
| UsbControlTransferRecipient|null { |
| if (enumString === 'DEVICE') { |
| return UsbControlTransferRecipient.DEVICE; |
| } |
| if (enumString === 'INTERFACE') { |
| return UsbControlTransferRecipient.INTERFACE; |
| } |
| if (enumString === 'ENDPOINT') { |
| return UsbControlTransferRecipient.ENDPOINT; |
| } |
| if (enumString === 'OTHER') { |
| return UsbControlTransferRecipient.OTHER; |
| } |
| showError('Invalid Transfer Recipient', this.rootElement_); |
| return null; |
| } |
| } |
| |
| /** |
| * Get the USB control transfer type. |
| */ |
| function getRequestType(inputRow: HTMLElement, inputType: number): string { |
| switch (inputType) { |
| case INPUT_TYPE_DECIMAL_WITH_DROPDOWN: |
| const select = |
| inputRow.querySelector<HTMLSelectElement>('#transfer-type'); |
| assert(select); |
| return select.value; |
| case INPUT_TYPE_HEX_BYTE: |
| const input = |
| inputRow.querySelector<HTMLInputElement>('#query-request-type'); |
| assert(input); |
| const value = Number.parseInt(input.value, 16); |
| switch (value >> 5 & 0x03) { |
| case 0: |
| return 'STANDARD'; |
| case 1: |
| return 'CLASS'; |
| case 2: |
| return 'VENDOR'; |
| } |
| return ''; |
| default: |
| return ''; |
| } |
| } |
| |
| /** |
| * Get the USB control transfer recipient. |
| */ |
| function getRequestTypeRecipient( |
| inputRow: HTMLElement, inputType: number): string { |
| switch (inputType) { |
| case INPUT_TYPE_DECIMAL_WITH_DROPDOWN: |
| const select = |
| inputRow.querySelector<HTMLSelectElement>('#transfer-recipient'); |
| assert(select); |
| return select.value; |
| case INPUT_TYPE_HEX_BYTE: |
| const input = |
| inputRow.querySelector<HTMLInputElement>('#query-request-type'); |
| assert(input); |
| const value = Number.parseInt(input.value, 16); |
| switch (value & 0x1F) { |
| case 0: |
| return 'DEVICE'; |
| case 1: |
| return 'INTERFACE'; |
| case 2: |
| return 'ENDPOINT'; |
| case 3: |
| return 'OTHER'; |
| } |
| return ''; |
| default: |
| return ''; |
| } |
| } |
| |
| /** |
| * Get the USB control transfer direction. 0 for device-to-host, 1 for |
| * host-to-device. |
| */ |
| function getRequestTypeDirection( |
| inputRow: HTMLElement, inputType: number): string { |
| switch (inputType) { |
| case INPUT_TYPE_DECIMAL_WITH_DROPDOWN: |
| const select = |
| inputRow.querySelector<HTMLSelectElement>('#transfer-direction'); |
| assert(select); |
| return select.value; |
| case INPUT_TYPE_HEX_BYTE: |
| const input = |
| inputRow.querySelector<HTMLInputElement>('#query-request-type'); |
| assert(input); |
| const value = Number.parseInt(input.value, 16); |
| switch (value >> 7) { |
| case CONTROL_TRANSFER_DIRECTION_HOST_TO_DEVICE: |
| return 'Host-to-Device'; |
| case CONTROL_TRANSFER_DIRECTION_DEVICE_TO_HOST: |
| return 'Device-to-Host'; |
| } |
| return 'Device-to-Host'; |
| default: |
| return 'Device-to-Host'; |
| } |
| } |
| |
| function getDecimalOrHex( |
| inputRow: HTMLElement, inputType: number, selector: string): number { |
| const input = inputRow.querySelector<HTMLInputElement>(selector); |
| switch (inputType) { |
| case INPUT_TYPE_DECIMAL_WITH_DROPDOWN: |
| assert(input); |
| return Number.parseInt(input.value, 10); |
| case INPUT_TYPE_HEX_BYTE: |
| assert(input); |
| return Number.parseInt(input.value, 16); |
| default: |
| return Number.NaN; |
| } |
| } |
| |
| /** |
| * Get the USB control transfer request code. |
| */ |
| function getRequestCode(inputRow: HTMLElement, inputType: number): number { |
| return getDecimalOrHex(inputRow, inputType, '#query-request'); |
| } |
| |
| /** |
| * Get the value of USB control transfer request wValue field. |
| */ |
| function getRequestValue(inputRow: HTMLElement, inputType: number): number { |
| return getDecimalOrHex(inputRow, inputType, '#query-value'); |
| } |
| |
| /** |
| * Get the value of USB control transfer request wIndex field. |
| */ |
| function getRequestIndex(inputRow: HTMLElement, inputType: number): number { |
| return getDecimalOrHex(inputRow, inputType, '#query-index'); |
| } |
| |
| /** |
| * Get the length of the data transferred during USB control transfer. |
| */ |
| function getRequestLength(inputRow: HTMLElement, inputType: number): number { |
| return getDecimalOrHex(inputRow, inputType, '#query-length'); |
| } |
| |
| /** |
| * Adds a display area which contains a tree view and a byte view. |
| */ |
| function addNewDescriptorDisplayElement( |
| rootElement: HTMLElement, descriptorPanelTitle?: string): |
| {rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement} { |
| const descriptorPanelTemplate = |
| (rootElement.getRootNode() as DocumentFragment | HTMLElement) |
| .querySelector<HTMLTemplateElement>('#descriptor-panel-template'); |
| assert(descriptorPanelTemplate); |
| |
| const descriptorPanelClone = |
| document.importNode(descriptorPanelTemplate.content, true); |
| |
| const rawDataTreeRoot = |
| descriptorPanelClone.querySelector<CrTreeElement>('.raw-data-tree-view'); |
| assert(rawDataTreeRoot); |
| const rawDataByteElement = |
| descriptorPanelClone.querySelector<HTMLElement>('.raw-data-byte-view'); |
| assert(rawDataByteElement); |
| |
| rawDataTreeRoot.detail = {payload: {}, children: {}}; |
| |
| if (descriptorPanelTitle) { |
| const descriptorPanelTitleTemplate = |
| (rootElement.getRootNode() as DocumentFragment | HTMLElement) |
| .querySelector<HTMLTemplateElement>('#descriptor-panel-title'); |
| assert(descriptorPanelTitleTemplate); |
| const clone = |
| document.importNode(descriptorPanelTitleTemplate.content, true) |
| .querySelector('descriptorpaneltitle'); |
| assert(clone); |
| clone.textContent = descriptorPanelTitle; |
| rootElement.appendChild(clone); |
| } |
| rootElement.appendChild(descriptorPanelClone); |
| return {rawDataTreeRoot, rawDataByteElement}; |
| } |
| |
| /** |
| * Shows an error message. |
| */ |
| function showError(message: string, rootElement: HTMLElement) { |
| const errorElement = document.createElement('error'); |
| errorElement.textContent = message; |
| rootElement.prepend(errorElement); |
| } |
| |
| /** |
| * Shows a warning message. |
| */ |
| function showWarn(message: string, rootElement: HTMLElement) { |
| const warnElement = document.createElement('warn'); |
| warnElement.textContent = message; |
| rootElement.prepend(warnElement); |
| } |
| |
| /** |
| * Renders a customized TreeItem with the given content and class name. |
| */ |
| function customTreeItem( |
| itemLabel: string, className?: string): CrTreeItemElement { |
| const item = document.createElement('cr-tree-item'); |
| item.label = itemLabel; |
| if (className) { |
| item.classList.add(className); |
| } |
| return item; |
| } |
| |
| /** |
| * Adds function for mapping between two views. |
| */ |
| function addMappingAction( |
| rawDataTreeRoot: CrTreeElement, rawDataByteElement: HTMLElement) { |
| // Highlights the byte(s) that hovered in the tree. |
| function mapElement(el: CrTreeItemElement) { |
| const classList = el.classList; |
| // classList[0] is 'tree-row'. classList[1] of tree item for fields |
| // starts with 'field-offset-', and classList[1] of tree item for |
| // descriptors (ie. endpoint descriptor) is descriptor type and index. |
| const fieldOffsetOrDescriptorClass = classList[0]!; |
| assert( |
| fieldOffsetOrDescriptorClass.startsWith('field-offset-') || |
| fieldOffsetOrDescriptorClass.startsWith('descriptor-')); |
| |
| el.rowElement.addEventListener('pointerenter', (event: MouseEvent) => { |
| rawDataByteElement.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`) |
| .forEach((el) => el.classList.add('hovered-field')); |
| event.stopPropagation(); |
| }); |
| |
| el.rowElement.addEventListener('pointerleave', () => { |
| rawDataByteElement.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`) |
| .forEach((el) => el.classList.remove('hovered-field')); |
| }); |
| |
| el.rowElement.addEventListener('click', (event: MouseEvent) => { |
| if ((event.target as HTMLElement).className !== 'expand-icon') { |
| // Clears all the selected elements before select another. |
| rawDataByteElement.querySelectorAll('.raw-data-byte-view span') |
| .forEach((el) => el.classList.remove('selected-field')); |
| |
| rawDataByteElement.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`) |
| .forEach((el) => el.classList.add('selected-field')); |
| } |
| }); |
| |
| el.items.forEach(item => mapElement(item as CrTreeItemElement)); |
| } |
| |
| rawDataTreeRoot.items.forEach(item => mapElement(item as CrTreeItemElement)); |
| |
| // Selects the tree item that displays the byte hovered in the raw view. |
| const rawDataByteElements = rawDataByteElement.querySelectorAll('span'); |
| rawDataByteElements.forEach((el) => { |
| const classList = el.classList; |
| if (!classList[0]) { |
| // For a field that has failed to render there might be some leftover |
| // bytes. Just skip them. |
| return; |
| } |
| const fieldOffsetClass = classList[0]; |
| assert(fieldOffsetClass.startsWith('field-offset-')); |
| |
| function configureMatchingItem( |
| className: string, callback: (e: CrTreeItemElement) => void, |
| root: CrTreeElement|CrTreeItemElement) { |
| if (root.tagName === 'CR-TREE-ITEM' && |
| root.classList.contains(className)) { |
| callback(root as CrTreeItemElement); |
| return true; |
| } |
| for (const item of root.items) { |
| if (configureMatchingItem( |
| className, callback, item as CrTreeItemElement)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| el.addEventListener('pointerenter', () => { |
| rawDataByteElement.querySelectorAll(`.${fieldOffsetClass}`) |
| .forEach((el) => el.classList.add('hovered-field')); |
| configureMatchingItem( |
| fieldOffsetClass, el => el.forceHoverStyle(true), rawDataTreeRoot); |
| }); |
| |
| el.addEventListener('pointerleave', () => { |
| rawDataByteElement.querySelectorAll(`.${fieldOffsetClass}`) |
| .forEach((el) => el.classList.remove('hovered-field')); |
| configureMatchingItem( |
| fieldOffsetClass, el => el.forceHoverStyle(false), rawDataTreeRoot); |
| }); |
| |
| el.addEventListener('click', () => { |
| configureMatchingItem( |
| fieldOffsetClass, el => el.rowElement.click(), rawDataTreeRoot); |
| }); |
| }); |
| } |
| |
| interface Field { |
| size: number; |
| label: string; |
| formatter: (data: Uint8Array, offset: number) => string; |
| extraTreeItemFormatter?: |
| (data: Uint8Array, offset: number, item: CrTreeItemElement, |
| label: string) => void; |
| } |
| |
| /** |
| * Renders a tree view to display the raw data in readable text. |
| * @param offset The start offset of the descriptor structure that |
| * want to be rendered. |
| * @return The end offset of descriptor structure that want to be |
| * rendered. |
| */ |
| function renderRawDataTree( |
| root: CrTreeElement|CrTreeItemElement, rawDataByteElement: HTMLElement, |
| fields: Field[], rawData: Uint8Array, offset: number, |
| rootElement: HTMLElement, ...parentClassNames: string[]): number { |
| const rawDataByteElements = rawDataByteElement.querySelectorAll('span'); |
| |
| for (const field of fields) { |
| const className = `field-offset-${offset}`; |
| let item; |
| try { |
| item = customTreeItem( |
| `${field.label}${field.formatter(rawData, offset)}`, className); |
| |
| for (let i = 0; i < field.size; i++) { |
| rawDataByteElements[offset + i]!.classList.add(className); |
| for (const parentClassName of parentClassNames) { |
| rawDataByteElements[offset + i]!.classList.add(parentClassName); |
| } |
| } |
| } catch (e) { |
| showError(`Field at offset ${offset} is invalid.`, rootElement); |
| break; |
| } |
| |
| root.add(item); |
| |
| try { |
| if (field.extraTreeItemFormatter) { |
| field.extraTreeItemFormatter(rawData, offset, item, field.label); |
| } |
| } catch (e) { |
| const message = (e as Error).message; |
| showError( |
| `Error at rendering field at index ${offset}: ${message}`, |
| rootElement); |
| } |
| offset += field.size; |
| } |
| return offset; |
| } |
| |
| /** |
| * Renders an element to display the raw data in hex, byte by byte. |
| */ |
| function renderRawDataBytes( |
| rawDataByteElement: HTMLElement, rawData: Uint8Array) { |
| const rawDataByteContainerTemplate = |
| (rawDataByteElement.getRootNode() as HTMLElement) |
| .querySelector<HTMLTemplateElement>( |
| '#raw-data-byte-container-template'); |
| assert(rawDataByteContainerTemplate); |
| const rawDataByteContainerClone: DocumentFragment = |
| document.importNode(rawDataByteContainerTemplate.content, true); |
| const rawDataByteContainerElement = |
| rawDataByteContainerClone.querySelector('div'); |
| assert(rawDataByteContainerElement); |
| |
| const rawDataByteTemplate = |
| (rawDataByteElement.getRootNode() as HTMLElement) |
| .querySelector<HTMLTemplateElement>('#raw-data-byte-template'); |
| assert(rawDataByteTemplate); |
| for (const value of rawData) { |
| const rawDataByteClone: DocumentFragment = |
| document.importNode(rawDataByteTemplate.content, true); |
| const rawDataByteSpan = rawDataByteClone.querySelector('span'); |
| assert(rawDataByteSpan); |
| rawDataByteSpan.textContent = toHex(value, 2); |
| rawDataByteContainerElement.appendChild(rawDataByteSpan); |
| } |
| rawDataByteElement.appendChild(rawDataByteContainerElement); |
| } |
| |
| /** |
| * Checks if the status of a control transfer indicates success. |
| */ |
| function checkTransferSuccess( |
| status: number, defaultMessage: string, rootElement: HTMLElement) { |
| let failReason = ''; |
| switch (status) { |
| case UsbTransferStatus.COMPLETED: |
| return; |
| case UsbTransferStatus.SHORT_PACKET: |
| showError('Descriptor is too short.', rootElement); |
| return; |
| case UsbTransferStatus.BABBLE: |
| showError('Descriptor is too long.', rootElement); |
| return; |
| case UsbTransferStatus.TRANSFER_ERROR: |
| failReason = 'Transfer Error'; |
| break; |
| case UsbTransferStatus.TIMEOUT: |
| failReason = 'Timeout'; |
| break; |
| case UsbTransferStatus.CANCELLED: |
| failReason = 'Transfer was cancelled'; |
| break; |
| case UsbTransferStatus.STALLED: |
| failReason = 'Transfer Error'; |
| break; |
| case UsbTransferStatus.DISCONNECT: |
| failReason = 'Transfer stalled'; |
| break; |
| case UsbTransferStatus.PERMISSION_DENIED: |
| failReason = 'Permission denied'; |
| break; |
| } |
| // Response data will be null if |status| is neither COMPLETED, BABBLE, or |
| // SHORT_PACKET. Throws an error to stop rendering response data. |
| throw new Error(`${defaultMessage} (Reason: ${failReason})`); |
| } |
| |
| /** |
| * Converts a number to a hexadecimal string padded with zeros to the given |
| * number of digits. |
| */ |
| function toHex(number: number, numOfDigits: number): string { |
| return number.toString(16).padStart(numOfDigits, '0').toUpperCase(); |
| } |
| |
| /** |
| * Parses UTF-16 array to string. |
| */ |
| function decodeUtf16Array( |
| arr: Uint8Array, isLittleEndian: boolean = false): string { |
| let str = ''; |
| const data = new DataView(arr.buffer); |
| for (let i = 0; i < arr.length; i += 2) { |
| str += String.fromCodePoint(data.getUint16(i, isLittleEndian)); |
| } |
| return str; |
| } |
| |
| /** |
| * Parses UTF-8 array to string. |
| */ |
| function decodeUtf8Array(arr: Uint8Array): string { |
| return String.fromCodePoint(...arr); |
| } |
| |
| /** |
| * Parses one byte to decimal number string. |
| */ |
| function formatByte(rawData: Uint8Array, offset: number): string { |
| return rawData[offset]!.toString(); |
| } |
| |
| /** |
| * Parses two bytes to decimal number. |
| */ |
| function parseShort(rawData: Uint8Array, offset: number): number { |
| const data = new DataView(rawData.buffer); |
| return data.getUint16(offset, true); |
| } |
| |
| /** |
| * Parses two bytes to decimal number string. |
| : |
| */ |
| function formatShort(rawData: Uint8Array, offset: number): string { |
| return parseShort(rawData, offset).toString(); |
| } |
| |
| /** |
| * Parses two bytes to decimal number string. |
| */ |
| function formatLetter(rawData: Uint8Array, offset: number): string { |
| const num = parseShort(rawData, offset); |
| return String.fromCodePoint(num); |
| } |
| |
| /** |
| * Parses two bytes to a hex string. |
| */ |
| function formatTwoBytesToHex(rawData: Uint8Array, offset: number): string { |
| const num = parseShort(rawData, offset); |
| return `0x${toHex(num, 4)}`; |
| } |
| |
| /** |
| * Parses two bytes to USB version format. |
| */ |
| function formatUsbVersion(rawData: Uint8Array, offset: number): string { |
| return `${rawData[offset + 1]}.${rawData[offset]! >> 4}.${ |
| rawData[offset]! & 0x0F}`; |
| } |
| |
| /** |
| * Parses one byte to a bitmap. |
| */ |
| function formatBitmap(rawData: Uint8Array, offset: number): string { |
| return rawData[offset]!.toString(2).padStart(8, '0'); |
| } |
| |
| /** |
| * Parses descriptor type to a hex string. |
| */ |
| function formatDescriptorType(rawData: Uint8Array, offset: number): string { |
| return `0x${toHex(rawData[offset]!, 2)}`; |
| } |
| |
| /** |
| * Parses UUID field. |
| */ |
| function formatUuid(rawData: Uint8Array, offset: number): string { |
| // UUID is 16 bytes (Section 9.6.2.4 of Universal Serial Bus 3.1 |
| // Specification). |
| // Additional reference: IETF RFC 4122. https://tools.ietf.org/html/rfc4122 |
| let uuidStr = ''; |
| const data = new DataView(rawData.buffer); |
| |
| uuidStr += toHex(data.getUint32(offset, true), 8); |
| uuidStr += '-'; |
| uuidStr += toHex(data.getUint16(offset + 4, true), 4); |
| uuidStr += '-'; |
| uuidStr += toHex(data.getUint16(offset + 6, true), 4); |
| uuidStr += '-'; |
| uuidStr += toHex(data.getUint8(offset + 8), 2); |
| uuidStr += toHex(data.getUint8(offset + 9), 2); |
| uuidStr += '-'; |
| uuidStr += toHex(data.getUint8(offset + 10), 2); |
| uuidStr += toHex(data.getUint8(offset + 11), 2); |
| uuidStr += toHex(data.getUint8(offset + 12), 2); |
| uuidStr += toHex(data.getUint8(offset + 13), 2); |
| uuidStr += toHex(data.getUint8(offset + 14), 2); |
| uuidStr += toHex(data.getUint8(offset + 15), 2); |
| |
| return uuidStr; |
| } |
| |
| /** |
| * Parses Compatible ID String field. |
| */ |
| function formatCompatibleIdString(rawData: Uint8Array, offset: number): string { |
| // Compatible ID String is 8 bytes, which is defined by Microsoft OS 2.0 |
| // Descriptors Specification (July, 2018). |
| return decodeUtf8Array(rawData.slice(offset, offset + 8)); |
| } |
| |
| /** |
| * Parses Windows Version field. |
| */ |
| function formatWindowsVersion(rawData: Uint8Array, offset: number): string { |
| const data = new DataView(rawData.buffer); |
| const windowsVersion = data.getUint32(offset, true); |
| switch (windowsVersion) { |
| case WIN_81_HEADER: |
| return 'Windows 8.1'; |
| default: |
| return `0x${toHex(windowsVersion, 8)}`; |
| } |
| } |
| |
| /** |
| * Parses Feature Registry Property Data Type. |
| */ |
| function formatFeatureRegistryPropertyDataType( |
| rawData: Uint8Array, offset: number): string { |
| const data = new DataView(rawData.buffer); |
| const propertyDataType = data.getUint16(offset, true); |
| switch (propertyDataType) { |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_SZ: |
| return 'A NULL-terminated Unicode String (REG_SZ)'; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_EXPAND_SZ: |
| return 'A NULL-terminated Unicode String that includes environment ' + |
| 'variables (REG_EXPAND_SZ)'; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_BINARY: |
| return 'Free-form binary (REG_BINARY)'; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_DWORD_LITTLE_ENDIAN: |
| return 'A little-endian 32-bit integer (REG_DWORD_LITTLE_ENDIAN)'; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_DWORD_BIG_ENDIAN: |
| return 'A big-endian 32-bit integer (REG_DWORD_BIG_ENDIAN)'; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_LINK: |
| return 'A NULL-terminated Unicode string that contains a symbolic ' + |
| 'link (REG_LINK)'; |
| case MS_OS_20_FEATURE_REG_PROPERTY_DATA_TYPE_REG_MULTI_SZ: |
| return 'Multiple NULL-terminated Unicode strings (REG_MULTI_SZ)'; |
| default: |
| return 'Reserved'; |
| } |
| } |
| |
| /** |
| * Returns empty string for a field that can't or doesn't need to be parsed. |
| */ |
| function formatUnknown(_rawData: Uint8Array, _offset: number): string { |
| return ''; |
| } |
| |
| /** |
| * Returns a class code string with a description. |
| */ |
| function formatClassCode(rawData: Uint8Array, offset: number): string { |
| return renderClassCodeWithDescription(rawData[offset]!); |
| } |
| |
| /** |
| * Returns a class code string with a description. |
| */ |
| export function renderClassCodeWithDescription(classCode: number): string { |
| const blockedByWebUsb = '(blocked by WebUSB)'; |
| // USB Class Codes are defined by the USB-IF: |
| // https://www.usb.org/defined-class-codes |
| switch (classCode) { |
| case 0x00: |
| return `${classCode} (Device)`; |
| case 0x01: |
| return `${classCode} (Audio) ${blockedByWebUsb}`; |
| case 0x02: |
| return `${classCode} (Communications and CDC Control)`; |
| case 0x03: |
| return `${classCode} (HID) ${blockedByWebUsb}`; |
| case 0x05: |
| return `${classCode} (Physical)`; |
| case 0x06: |
| return `${classCode} (Still Imaging)`; |
| case 0x07: |
| return `${classCode} (Printer)`; |
| case 0x08: |
| return `${classCode} (Mass Storage) ${blockedByWebUsb}`; |
| case 0x09: |
| return `${classCode} (Hub)`; |
| case 0x0A: |
| return `${classCode} (CDC-Data)`; |
| case 0x0B: |
| return `${classCode} (Smart Card) ${blockedByWebUsb}`; |
| case 0x0D: |
| return `${classCode} (Content Security)`; |
| case 0x0E: |
| return `${classCode} (Video) ${blockedByWebUsb}`; |
| case 0x0F: |
| return `${classCode} (Personal Healthcare)`; |
| case 0x10: |
| return `${classCode} (Audio/Video Devices) ${blockedByWebUsb}`; |
| case 0x11: |
| return `${classCode} (Billboard Device)`; |
| case 0x12: |
| return `${classCode} (USB Type-C Bridge Device)`; |
| case 0xDC: |
| return `${classCode} (Diagnostic Device)`; |
| case 0xE0: |
| return `${classCode} (Wireless Controller) ${blockedByWebUsb}`; |
| case 0xEF: |
| return `${classCode} (Miscellaneous)`; |
| case 0xFE: |
| return `${classCode} (Application Specific)`; |
| case 0xFF: |
| return `${classCode} (Vendor Specific)`; |
| } |
| return `${classCode}`; |
| } |
| |
| /** |
| * Parses language code to readable language name. |
| */ |
| function parseLanguageCode(languageCode: number): string { |
| switch (languageCode) { |
| case LANGUAGE_CODE_EN_US: |
| return 'en-US'; |
| default: |
| return `0x${toHex(languageCode, 4)}`; |
| } |
| } |
| |
| /** |
| * Checks if two UUIDs are same. |
| */ |
| function isSameUuid( |
| rawData: Uint8Array, offset: number, uuidArr: number[]): boolean { |
| // Validate the Platform Capability Descriptor |
| if (offset + 20 > rawData.length) { |
| return false; |
| } |
| // UUID is from index 4 to index 19 (Section 9.6.2.4 of Universal Serial |
| // Bus 3.1 Specification). |
| for (const [i, num] of rawData.slice(offset + 4, offset + 20).entries()) { |
| if (num !== uuidArr[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |