package usb import "testing" // buildDescriptorBlob assembles a device descriptor followed by raw // configuration bytes, the way a usbdevfs file read returns them. func buildDescriptorBlob(numConfigs uint8, configs ...[]byte) []byte { dev := []byte{ 18, // bLength 0x01, // bDescriptorType = DEVICE 0x00, 0x02, // bcdUSB 2.00 0x00, // bDeviceClass (per-interface) 0x00, // bDeviceSubClass 0x00, // bDeviceProtocol 64, // bMaxPacketSize0 0x6d, 0x04, // idVendor 046d 0x1c, 0xc0, // idProduct c01c 0x10, 0x02, // bcdDevice 0210 1, 2, 3, // string indices numConfigs, } blob := dev for _, c := range configs { blob = append(blob, c...) } return blob } func ifaceDesc(number, alt, numEndpoints, class, subclass, protocol uint8) []byte { return []byte{9, 0x04, number, alt, numEndpoints, class, subclass, protocol, 0} } func endpointDesc(addr, attrs uint8, maxPacket uint16, interval uint8) []byte { return []byte{7, 0x05, addr, attrs, byte(maxPacket), byte(maxPacket >> 8), interval} } func configDesc(value uint8, body []byte) []byte { total := 9 + len(body) cfg := []byte{9, 0x02, byte(total), byte(total >> 8), 1, value, 0, 0x80, 250} return append(cfg, body...) } func TestParseDescriptorsDeviceFields(t *testing.T) { blob := buildDescriptorBlob(1, configDesc(1, ifaceDesc(0, 0, 0, 3, 1, 1))) pd, err := ParseDescriptors(blob) if err != nil { t.Fatalf("ParseDescriptors: %v", err) } if pd.VendorID != 0x046d { t.Errorf("VendorID = %04x, want 046d", pd.VendorID) } if pd.ProductID != 0xc01c { t.Errorf("ProductID = %04x, want c01c", pd.ProductID) } if pd.BcdDevice != 0x0210 { t.Errorf("BcdDevice = %04x, want 0210", pd.BcdDevice) } if pd.NumConfigs != 1 { t.Errorf("NumConfigs = %d, want 1", pd.NumConfigs) } if len(pd.Configs) != 1 { t.Fatalf("got %d configs, want 1", len(pd.Configs)) } } // A composite device where endpoint number 1 appears twice with different // directions and different transfer types. Keying the endpoint map by number // alone collapses these two into one, which is what made the server submit // interrupt URBs with the bulk type and broke HID devices. func TestAllEndpointsKeepsDirectionsSeparate(t *testing.T) { body := ifaceDesc(0, 0, 2, 0x08, 0x06, 0x50) // mass storage body = append(body, endpointDesc(0x01, 0x02, 512, 0)...) // bulk OUT, EP1 body = append(body, endpointDesc(0x82, 0x02, 512, 0)...) // bulk IN, EP2 body = append(body, ifaceDesc(1, 0, 1, 0x03, 0x01, 0x01)...) // HID keyboard body = append(body, endpointDesc(0x81, 0x03, 8, 10)...) // interrupt IN, EP1 blob := buildDescriptorBlob(1, configDesc(1, body)) pd, err := ParseDescriptors(blob) if err != nil { t.Fatalf("ParseDescriptors: %v", err) } eps := pd.Configs[0].AllEndpoints() if len(eps) != 3 { t.Fatalf("got %d endpoints, want 3: %+v", len(eps), eps) } if got := eps[0x01].TransferType; got != TransferTypeBulk { t.Errorf("EP 0x01 type = %d, want bulk (%d)", got, TransferTypeBulk) } if got := eps[0x81].TransferType; got != TransferTypeInterrupt { t.Errorf("EP 0x81 type = %d, want interrupt (%d) — direction bit must not collapse", got, TransferTypeInterrupt) } if got := eps[0x81].Interval; got != 10 { t.Errorf("EP 0x81 interval = %d, want 10", got) } if got := eps[0x82].MaxPacketSize; got != 512 { t.Errorf("EP 0x82 maxpacket = %d, want 512", got) } } // A webcam's isochronous endpoints only exist in a non-zero alternate // setting. sysfs shows only the active setting, so an endpoint map built from // it would classify these as bulk after a SET_INTERFACE. func TestAllEndpointsIncludesAlternateSettings(t *testing.T) { body := ifaceDesc(1, 0, 0, 0x0e, 0x02, 0x00) // video streaming, alt 0: no endpoints body = append(body, ifaceDesc(1, 1, 1, 0x0e, 0x02, 0x00)...) body = append(body, endpointDesc(0x81, 0x05, 1024, 1)...) // isochronous IN body = append(body, ifaceDesc(1, 2, 1, 0x0e, 0x02, 0x00)...) body = append(body, endpointDesc(0x81, 0x05, 2048, 1)...) blob := buildDescriptorBlob(1, configDesc(1, body)) pd, err := ParseDescriptors(blob) if err != nil { t.Fatalf("ParseDescriptors: %v", err) } cfg := pd.Configs[0] if len(cfg.Interfaces) != 3 { t.Fatalf("got %d interface descriptors, want 3 (alt 0,1,2)", len(cfg.Interfaces)) } eps := cfg.AllEndpoints() ep, ok := eps[0x81] if !ok { t.Fatal("EP 0x81 missing — endpoints from non-zero alternate settings were dropped") } if ep.TransferType != TransferTypeIsochronous { t.Errorf("EP 0x81 type = %d, want isochronous (%d)", ep.TransferType, TransferTypeIsochronous) } } func TestActiveInterfacesOnlyAltZero(t *testing.T) { body := ifaceDesc(0, 0, 0, 0x01, 0x01, 0x00) body = append(body, ifaceDesc(1, 0, 0, 0x01, 0x02, 0x00)...) body = append(body, ifaceDesc(1, 1, 1, 0x01, 0x02, 0x00)...) body = append(body, endpointDesc(0x81, 0x05, 192, 1)...) blob := buildDescriptorBlob(1, configDesc(1, body)) pd, _ := ParseDescriptors(blob) active := pd.Configs[0].ActiveInterfaces() if len(active) != 2 { t.Fatalf("got %d active interfaces, want 2 (one per interface number)", len(active)) } for _, iface := range active { if iface.AltSetting != 0 { t.Errorf("interface %d has alt setting %d, want 0", iface.Number, iface.AltSetting) } } } // Class-specific descriptors (HID, UVC, audio) sit between the standard ones // and must be skipped by bLength rather than confusing the walk. func TestParseDescriptorsSkipsClassSpecific(t *testing.T) { hidDesc := []byte{9, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x3f, 0x00} body := ifaceDesc(0, 0, 1, 0x03, 0x01, 0x01) body = append(body, hidDesc...) body = append(body, endpointDesc(0x81, 0x03, 8, 10)...) blob := buildDescriptorBlob(1, configDesc(1, body)) pd, err := ParseDescriptors(blob) if err != nil { t.Fatalf("ParseDescriptors: %v", err) } eps := pd.Configs[0].AllEndpoints() if _, ok := eps[0x81]; !ok { t.Fatal("endpoint after a HID descriptor was not parsed") } if len(pd.Configs[0].Interfaces[0].Endpoints) != 1 { t.Errorf("got %d endpoints on the interface, want 1", len(pd.Configs[0].Interfaces[0].Endpoints)) } } func TestFindConfigSelectsByValue(t *testing.T) { blob := buildDescriptorBlob(2, configDesc(1, ifaceDesc(0, 0, 0, 0x03, 0, 0)), configDesc(2, ifaceDesc(0, 0, 0, 0x08, 0, 0)), ) pd, err := ParseDescriptors(blob) if err != nil { t.Fatalf("ParseDescriptors: %v", err) } cfg := pd.FindConfig(2) if cfg == nil { t.Fatal("FindConfig(2) returned nil") } if cfg.Interfaces[0].Class != 0x08 { t.Errorf("got interface class %02x, want 08 — wrong configuration selected", cfg.Interfaces[0].Class) } if pd.FindConfig(9) != nil { t.Error("FindConfig(9) should return nil for a configuration that does not exist") } } func TestParseDescriptorsRejectsGarbage(t *testing.T) { tests := []struct { name string data []byte }{ {"empty", nil}, {"too short", []byte{18, 0x01, 0x00}}, {"not a device descriptor", append([]byte{9, 0x02}, make([]byte, 20)...)}, {"no configuration", buildDescriptorBlob(1)}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := ParseDescriptors(tt.data); err == nil { t.Error("expected an error, got nil") } }) } } // A truncated or zero-length descriptor must terminate the walk instead of // looping forever or reading past the buffer. func TestParseDescriptorsHandlesTruncation(t *testing.T) { blob := buildDescriptorBlob(1, configDesc(1, ifaceDesc(0, 0, 1, 3, 1, 1))) blob = append(blob, 0x00, 0x05) // zero bLength would spin forever blob = append(blob, 9, 0x04) // interface descriptor claiming 9 bytes, only 2 present done := make(chan struct{}) go func() { defer close(done) if _, err := ParseDescriptors(blob); err != nil { t.Errorf("unexpected error: %v", err) } }() <-done }