<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="resources/manual.js"></script>
  </head>
  <body>
    <p>
      These tests require a USB device to be connected.
    </p>
    <script>
      const kGetDescriptorRequest = 0x06;

      const kDeviceDescriptorType = 0x01;
      const kDeviceDescriptorLength = 18;

      const kConfigurationDescriptorType = 0x02;
      const kConfigurationDescriptorLength = 9;

      const kStringDescriptorType = 0x03;
      const kStringDescriptorMaxLength = 0xFF;

      const kInterfaceDescriptorType = 0x04;
      const kInterfaceDescriptorLength = 9;

      const kEndpointDescriptorType = 0x05;
      const kEndpointDescriptorLength = 7;

      let device = null;
      let pending_subtests = 0;
      const string_tests = new Set();
      const string_languages = [];

      async function subtest_complete() {
        if (--pending_subtests == 0) {
          await device.close();
        }
      }

      function manual_usb_subtest(func, name, properties) {
        pending_subtests++;
        promise_test(async (test) => {
          test.add_cleanup(subtest_complete);
          await func(test);
        }, name, properties);
      }

      function read_string(index, expected) {
        // A device may use the same string in multiple places. Don't bother
        // repeating the test.
        if (string_tests.has(index)) {
          return;
        }
        string_tests.add(index);

        const string_values = new Set();
        const decoder = new TextDecoder('utf-16le');

        for (const language of string_languages) {
          const language_string = language.toString(16).padStart(4, '0');
          manual_usb_subtest(async (t) => {
            if (expected != undefined) {
              t.add_cleanup(() => {
                if (string_values.size == string_languages.length) {
                  assert_true(string_values.has(expected));
                }
              });
            }

            const result = await device.controlTransferIn({
              requestType: 'standard',
              recipient: 'device',
              request: kGetDescriptorRequest,
              value: kStringDescriptorType << 8 | index,
              index: language
            }, kStringDescriptorMaxLength);

            assert_equals(result.status, 'ok', 'transfer status');
            const length = result.data.getUint8(0);
            assert_greater_than_equal(length, 2, 'descriptor length');
            assert_equals(result.data.byteLength, length, 'transfer length');
            assert_equals(result.data.getUint8(1), kStringDescriptorType,
                          'descriptor type');
            const string_buffer = new Uint8Array(
                result.data.buffer, result.data.byteOffset + 2, length - 2);
            string_values.add(decoder.decode(string_buffer));
          },
          `Read string descriptor ${index} in language 0x${language_string}`);
        }
      }

      function check_interface_descriptor(configuration, data) {
        assert_greater_than_equal(
            data.getUint8(0), kInterfaceDescriptorLength, 'descriptor length');

        const interface_number = data.getUint8(2);
        const iface = configuration.interfaces.find((iface) => {
          return iface.interfaceNumber == interface_number;
        });
        assert_not_equals(
            iface, undefined, `unknown interface ${interface_number}`);

        const alternate_setting = data.getUint8(3);
        const alternate = iface.alternates.find((alternate) => {
          return alternate.alternateSetting == alternate_setting;
        });
        assert_not_equals(
            alternate, undefined, `unknown alternate ${alternate_setting}`);

        assert_equals(data.getUint8(4), alternate.endpoints.length,
                      'number of endpoints');
        assert_equals(
            data.getUint8(5), alternate.interfaceClass, 'interface class');
        assert_equals(data.getUint8(6), alternate.interfaceSubclass,
                      'interface subclass');
        assert_equals(data.getUint8(7), alternate.interfaceProtocol,
                      'interface protocol');

        const interface_string = data.getUint8(8);
        if (interface_string != 0) {
          // TODO(crbug.com/727819): Check that the string descriptor matches
          // iface.interfaceName.
          read_string(interface_string);
        }

        return alternate;
      }

      function check_endpoint_descriptor(alternate, data) {
        assert_greater_than_equal(
            data.getUint8(0), kEndpointDescriptorLength, 'descriptor length');

        const endpoint_address = data.getUint8(2);
        const direction = endpoint_address & 0x80 ? 'in' : 'out';
        const endpoint_number = endpoint_address & 0x0f;
        const endpoint = alternate.endpoints.find((endpoint) => {
          return endpoint.direction == direction &&
                 endpoint.endpointNumber == endpoint_number;
        });
        assert_not_equals(
            endpoint, undefined, `unknown endpoint ${endpoint_number}`);

        const attributes = data.getUint8(3);
        switch (attributes & 0x03) {
          case 0:
            assert_equals(endpoint.type, 'control', 'endpoint type');
            break;
          case 1:
            assert_equals(endpoint.type, 'isochronous', 'endpoint type');
            break;
          case 2:
            assert_equals(endpoint.type, 'bulk', 'endpoint type');
            break;
          case 3:
            assert_equals(endpoint.type, 'interrupt', 'endpoint type');
            break;
        }

        assert_equals(data.getUint16(4, /*littleEndian=*/true),
                      endpoint.packetSize, 'packet size');
      }

      function read_config_descriptor(config_value) {
        manual_usb_subtest(async (t) => {
          const configuration = device.configurations.find((config) => {
            return config.configurationValue == config_value;
          });
          assert_not_equals(configuration, undefined);

          let result = await device.controlTransferIn({
            requestType: 'standard',
            recipient: 'device',
            request: kGetDescriptorRequest,
            value: kConfigurationDescriptorType << 8 | (config_value - 1),
            index: 0
          }, kConfigurationDescriptorLength);

          assert_equals(result.status, 'ok', 'transfer status');
          let length = result.data.getUint8(0);
          assert_greater_than_equal(
              length, kConfigurationDescriptorLength, 'descriptor length');
          assert_equals(result.data.byteLength, length, 'transfer length');
          const total_length = result.data.getUint16(2, /*littleEndian=*/true);

          result = await device.controlTransferIn({
            requestType: 'standard',
            recipient: 'device',
            request: kGetDescriptorRequest,
            value: kConfigurationDescriptorType << 8 | (config_value - 1),
            index: 0
          }, total_length);

          assert_equals(result.status, 'ok', 'transfer status');
          assert_equals(
              result.data.byteLength, total_length, 'transfer length');
          assert_equals(result.data.getUint8(0), length, 'descriptor length');
          assert_equals(result.data.getUint8(1), kConfigurationDescriptorType,
                        'descriptor type');
          assert_equals(result.data.getUint16(2, /*littleEndian=*/true),
                        total_length, 'total length');
          assert_equals(
              result.data.getUint8(4), configuration.interfaces.length,
              'number of interfaces');
          assert_equals(
              result.data.getUint8(5), config_value, 'configuration value');

          const configuration_string = result.data.getUint8(6);
          if (configuration_string != 0) {
            // TODO(crbug.com/727819): Check that the string descriptor matches
            // configuration.configurationName.
            read_string(configuration_string);
          }

          let offset = length;
          let alternate = undefined;
          while (offset < total_length) {
            length = result.data.getUint8(offset);
            assert_less_than_equal(offset + length, total_length);

            const view = new DataView(
                result.data.buffer, result.data.byteOffset + offset, length);
            switch (view.getUint8(1)) {
              case kConfigurationDescriptorType:
                assert_unreached('cannot contain multiple config descriptors');
                break;
              case kInterfaceDescriptorType:
                alternate = check_interface_descriptor(configuration, view);
                break;
              case kEndpointDescriptorType:
                assert_not_equals(alternate, undefined,
                                  'endpoint not defined after interface');
                check_endpoint_descriptor(alternate, view);
                break;
            }

            offset += length;
          }
        }, `Read config descriptor ${config_value}`);
      }

      function read_string_descriptor_languages(device_descriptor) {
        manual_usb_subtest(async (t) => {
          const result = await device.controlTransferIn({
            requestType: 'standard',
            recipient: 'device',
            request: kGetDescriptorRequest,
            value: kStringDescriptorType << 8,
            index: 0
          }, kStringDescriptorMaxLength);

          assert_equals(result.status, 'ok', 'transfer status');
          assert_equals(result.data.getUint8(1), kStringDescriptorType,
                        'descriptor type');
          const length = result.data.getUint8(0);
          assert_greater_than_equal(length, 2, 'descriptor length')
          assert_greater_than_equal(
              result.data.byteLength, length, 'transfer length');

          for (let index = 2; index < length; index += 2) {
            string_languages.push(
                result.data.getUint16(index, /*littleEndian=*/true));
          }

          const manufacturer_string = device_descriptor.getUint8(14);
          if (manufacturer_string != 0) {
            assert_not_equals(device.manufacturerName, undefined);
            read_string(manufacturer_string, device.manufacturerName);
          }

          const product_string = device_descriptor.getUint8(15);
          if (product_string != 0) {
            assert_not_equals(device.productName, undefined);
            read_string(product_string, device.productName);
          }

          const serial_number_string = device_descriptor.getUint8(16);
          if (serial_number_string != 0) {
            assert_not_equals(device.serialNumber, undefined);
            read_string(serial_number_string, device.serialNumber);
          }

          const num_configurations = device_descriptor.getUint8(17);
          for (let config_value = 1; config_value <= num_configurations;
               ++config_value) {
            read_config_descriptor(config_value);
          }
        }, `Read supported languages`);
      }

      promise_test(async (t) => {
        device = await getDeviceForManualTest();
        await device.open();

        const result = await device.controlTransferIn({
          requestType: 'standard',
          recipient: 'device',
          request: kGetDescriptorRequest,
          value: kDeviceDescriptorType << 8,
          index: 0,
        }, kDeviceDescriptorLength);

        assert_equals(result.status, 'ok', 'transfer status');
        assert_equals(
            result.data.byteLength, kDeviceDescriptorLength, 'transfer length');
        assert_greater_than_equal(
            result.data.getUint8(0),
            kDeviceDescriptorLength, 'descriptor length');
        assert_equals(result.data.getUint8(1), kDeviceDescriptorType,
                      'descriptor type');
        const bcd_usb = result.data.getUint16(2, /*littleEndian=*/true);
        assert_equals(
            bcd_usb >> 8, device.usbVersionMajor, 'USB version major');
        assert_equals(
            (bcd_usb & 0xf0) >> 4, device.usbVersionMinor, 'USB version minor');
        assert_equals(
            bcd_usb & 0xf, device.usbVersionSubminor, 'USV version subminor');
        assert_equals(
            result.data.getUint8(4), device.deviceClass, 'device class');
        assert_equals(
            result.data.getUint8(5), device.deviceSubclass, 'device subclass');
        assert_equals(
            result.data.getUint8(6), device.deviceProtocol, 'device protocol');
        assert_equals(result.data.getUint16(8, /*littleEndian=*/true),
                      device.vendorId, 'vendor id');
        assert_equals(result.data.getUint16(10, /*littleEndian=*/true),
                      device.productId, 'product id');
        const bcd_device = result.data.getUint16(12, /*littleEndian=*/true);
        assert_equals(
            bcd_device >> 8, device.deviceVersionMajor, 'device version major');
        assert_equals((bcd_device & 0xf0) >> 4, device.deviceVersionMinor,
                      'device version minor');
        assert_equals(bcd_device & 0xf, device.deviceVersionSubminor,
                      'device version subminor');
        assert_equals(result.data.getUint8(17), device.configurations.length,
                      'number of configurations');

        read_string_descriptor_languages(result.data);

        if (pending_subtests == 0) {
          await device.close();
        }
      }, 'Read device descriptor');
    </script>
  </body>
</html>
