From fc919c4625a571b09a80410cb2e5300a09d8848e Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 1 Nov 2026 17:54:13 -0800 Subject: [PATCH] Accept USB descriptor extensions and verify endpoint reloads. Standard descriptors with legal trailing bytes were rejected, as were zero-bandwidth isochronous endpoints. Parse each defined prefix and advance by the returned length, retaining short-prefix rejection and consistent configuration lengths across reads. Keep bulk payload requirements while allowing zero-bandwidth periodic descriptors. Exercise Linux endpoint reload after alternate selection, including a previously cached alternate and the known-selection path. Describe the existing lexicographic USB location ordering without changing its representation or runtime comparison. --- docs/architecture.md | 8 +- docs/linux-usb.md | 7 ++ usb/configuration.go | 29 +++--- usb/configuration_linux.go | 2 +- usb/configuration_test.go | 12 +-- usb/discovery/discovery.go | 7 +- usb/discovery/discovery_test.go | 28 ++++++ usb/endpoint_reload_linux_test.go | 150 ++++++++++++++++++++++++++++++ usb/extensions_test.go | 106 +++++++++++++++++++++ 9 files changed, 320 insertions(+), 29 deletions(-) create mode 100644 usb/endpoint_reload_linux_test.go create mode 100644 usb/extensions_test.go diff --git a/docs/architecture.md b/docs/architecture.md index 236f4cb..6a9c058 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -88,9 +88,11 @@ explicitly or use the package registry. Each call enumerates each provider once. Import `usb/discovery` to register all-device USB enumeration, or call its `Register` with an explicit registry. Its detached `Attachment` retains the -exact `usb.DeviceInfo` snapshot for later opening. USB identity breaks final -ordering ties by numeric bus/address and VID/PID; missing or duplicate serials -cannot keep physical-device ordering stable when replugging changes addresses. +exact `usb.DeviceInfo` snapshot for later opening. For equal serials, USB +ordering compares the unpadded `bus:address` location lexicographically, then +product and the fixed-width identity key; `10:2` therefore precedes `2:1`. +Missing or duplicate serials cannot keep physical-device ordering stable when +replugging changes addresses. Each concrete driver exposes `OpenProbe` to acquire an exact USB attachment under a generic `probe.Probe`. Interface claim and protocol configuration happen diff --git a/docs/linux-usb.md b/docs/linux-usb.md index 86844cb..101a212 100644 --- a/docs/linux-usb.md +++ b/docs/linux-usb.md @@ -19,6 +19,13 @@ without opening another attachment or falling back to a control transfer. An `ENODEV` during either cached descriptor read matches `usb.ErrDeviceGone` and preserves the native cause; other read errors retain their identity. +Standard device, configuration, interface and endpoint descriptors retain their +defined fields while ignoring legal trailing bytes. Parsing advances by the +returned descriptor length. Zero-bandwidth isochronous alternates may report a +zero maximum packet size; bulk endpoints still require a nonzero payload size. +After a successful alternate selection, endpoint lookup reloads its descriptors +using the known selection without another `GET_INTERFACE` request. + The control API accepts buffers up to the USB limit of 65,535 bytes; Linux usbfs can reject a request larger than the host page size. The host timeout defaults to five seconds; a context deadline rounds up to the next millisecond and clamps diff --git a/usb/configuration.go b/usb/configuration.go index 076b54b..4ff051d 100644 --- a/usb/configuration.go +++ b/usb/configuration.go @@ -18,7 +18,6 @@ const ( descriptorConfiguration = 0x02 descriptorInterface = 0x04 descriptorEndpoint = 0x05 - classAudio = 0x01 ) // TransferType identifies a USB endpoint transfer type. @@ -58,7 +57,8 @@ type AlternateSetting struct { // Endpoint describes one endpoint in an interface alternate setting. // MaxPacketSize is the descriptor's packet payload size, excluding periodic -// transaction multipliers and burst capacity. +// transaction multipliers and burst capacity. Zero payload is retained for +// zero-bandwidth periodic endpoints. type Endpoint struct { Address uint8 TransferType TransferType @@ -175,7 +175,7 @@ func configurationCount(ctx context.Context, device controlTransferer) (int, err if err := readStandard(ctx, device, requestGetDescriptor, descriptorDevice<<8, deviceDescriptor); err != nil { return 0, fmt.Errorf("usb: read device descriptor: %w", err) } - if int(deviceDescriptor[0]) != len(deviceDescriptor) || deviceDescriptor[1] != descriptorDevice || deviceDescriptor[17] == 0 { + if int(deviceDescriptor[0]) < len(deviceDescriptor) || deviceDescriptor[1] != descriptorDevice || deviceDescriptor[17] == 0 { return 0, errors.New("usb: malformed device descriptor") } return int(deviceDescriptor[17]), nil @@ -185,6 +185,7 @@ type configurationHeader struct { index int value uint8 total int + size uint8 } func configurationHeaderAt(ctx context.Context, device controlTransferer, index int) (configurationHeader, error) { @@ -193,10 +194,10 @@ func configurationHeaderAt(ctx context.Context, device controlTransferer, index if err := readStandard(ctx, device, requestGetDescriptor, descriptorValue, header); err != nil { return configurationHeader{}, fmt.Errorf("usb: read configuration %d header: %w", index, err) } - if int(header[0]) != len(header) || header[1] != descriptorConfiguration { + if int(header[0]) < len(header) || header[1] != descriptorConfiguration { return configurationHeader{}, fmt.Errorf("usb: malformed configuration %d header", index) } - return configurationHeader{index: index, value: header[5], total: int(binary.LittleEndian.Uint16(header[2:4]))}, nil + return configurationHeader{index: index, value: header[5], total: int(binary.LittleEndian.Uint16(header[2:4])), size: header[0]}, nil } func configurationAt(ctx context.Context, device controlTransferer, header configurationHeader, active uint8) (Configuration, error) { @@ -207,6 +208,9 @@ func configurationAt(ctx context.Context, device controlTransferer, header confi if err != nil { return Configuration{}, fmt.Errorf("usb: read configuration %d: %w", header.index, err) } + if raw[0] != header.size { + return Configuration{}, fmt.Errorf("usb: configuration %d descriptor length changed from %d to %d", header.index, header.size, raw[0]) + } if raw[5] != active { return Configuration{}, fmt.Errorf("usb: configuration %d value changed from %d to %d", header.index, active, raw[5]) } @@ -286,7 +290,7 @@ func validConfigurationHeader(raw []byte) bool { if len(raw) < 9 { return false } - return raw[0] == 9 && raw[1] == descriptorConfiguration && int(binary.LittleEndian.Uint16(raw[2:4])) == len(raw) && raw[7]&0x80 != 0 && raw[7]&0x1f == 0 + return raw[0] >= 9 && int(raw[0]) <= len(raw) && raw[1] == descriptorConfiguration && int(binary.LittleEndian.Uint16(raw[2:4])) == len(raw) && raw[7]&0x80 != 0 && raw[7]&0x1f == 0 } type configurationParser struct { @@ -317,7 +321,7 @@ func configurationDescriptor(raw []byte, position int) ([]byte, int, error) { } func (p *configurationParser) addInterface(descriptor []byte, position int) error { - if len(descriptor) != 9 { + if len(descriptor) < 9 { return fmt.Errorf("usb: invalid interface descriptor length %d at offset %d", len(descriptor), position) } if err := p.finishAlternate(); err != nil { @@ -357,7 +361,7 @@ func (p *configurationParser) addEndpoint(descriptor []byte, position int) error if p.alternate == nil { return fmt.Errorf("usb: invalid endpoint descriptor at offset %d", position) } - endpoint, err := endpointFromDescriptor(descriptor, position, p.alternate.Class) + endpoint, err := endpointFromDescriptor(descriptor, position) if err != nil { return err } @@ -369,13 +373,10 @@ func (p *configurationParser) addEndpoint(descriptor []byte, position int) error return nil } -func endpointFromDescriptor(descriptor []byte, position int, class uint8) (Endpoint, error) { - if len(descriptor) != 7 && len(descriptor) != 9 { +func endpointFromDescriptor(descriptor []byte, position int) (Endpoint, error) { + if len(descriptor) < 7 { return Endpoint{}, fmt.Errorf("usb: invalid endpoint descriptor length %d at offset %d", len(descriptor), position) } - if len(descriptor) == 9 && class != classAudio { - return Endpoint{}, fmt.Errorf("usb: nine-byte endpoint descriptor outside audio interface at offset %d", position) - } address := descriptor[2] if address&0x0f == 0 || address&0x70 != 0 { return Endpoint{}, fmt.Errorf("usb: invalid endpoint address %#02x at offset %d", address, position) @@ -424,7 +425,7 @@ func endpointMaxPacketSize(encoded uint16, transferType TransferType) (uint16, b } size := encoded & 0x07ff minimum := endpointPacketSizeMinimum(transactions) - return size, size <= 1024 && (size >= minimum || transactions == 0 && size == 0 && transferType == TransferInterrupt) + return size, size <= 1024 && (size >= minimum || transactions == 0 && size == 0 && (transferType == TransferInterrupt || transferType == TransferIsochronous)) } func validEndpointTransactions(transactions uint16, transferType TransferType) bool { diff --git a/usb/configuration_linux.go b/usb/configuration_linux.go index b0e0231..ef249c9 100644 --- a/usb/configuration_linux.go +++ b/usb/configuration_linux.go @@ -60,7 +60,7 @@ func attachmentConfigurationLength(reader io.ReaderAt, index int, offset int64) return 0, fmt.Errorf("usb: read attachment configuration %d header: %w", index, linuxUSBError(err)) } length := int(binary.LittleEndian.Uint16(header[2:4])) - if header[0] != 9 || header[1] != descriptorConfiguration || length < 9 { + if header[0] < 9 || int(header[0]) > length || header[1] != descriptorConfiguration || length < 9 { return 0, fmt.Errorf("usb: malformed attachment configuration %d header", index) } return length, nil diff --git a/usb/configuration_test.go b/usb/configuration_test.go index 9d813ff..9599d38 100644 --- a/usb/configuration_test.go +++ b/usb/configuration_test.go @@ -170,7 +170,7 @@ func TestActiveConfigurationRejectsChangedConfigurationLength(t *testing.T) { {request: 6, value: 0x0200, data: full}, {request: 8, data: []byte{7}}, }} - if _, err := activeConfiguration(context.Background(), device); err == nil || !strings.Contains(err.Error(), "malformed configuration descriptor") { + if _, err := activeConfiguration(context.Background(), device); err == nil || !strings.Contains(err.Error(), "descriptor length changed") { t.Fatalf("ActiveConfiguration() error = %v", err) } } @@ -269,15 +269,12 @@ func TestConfigurationValidatesAlternateSettings(t *testing.T) { } func TestConfigurationRejectsInvalidInterfaceHierarchy(t *testing.T) { - long := append(interfaceDescriptor(0, 0, 0), 0) - long[0] = 10 tests := []struct { name string raw []byte want string }{ {name: "zero interfaces", raw: rawConfiguration(1, 0), want: "configuration descriptor has no interfaces"}, - {name: "long interface descriptor", raw: rawConfiguration(1, 1, long), want: "invalid interface descriptor length 10"}, {name: "noncontiguous interface numbers", raw: rawConfiguration(1, 2, interfaceDescriptor(0, 0, 0), interfaceDescriptor(2, 0, 0)), want: "interface 2 is outside configuration interface count 2"}, } for _, test := range tests { @@ -302,12 +299,12 @@ func TestConfigurationValidatesConfigurationAttributes(t *testing.T) { func TestConfigurationValidatesEndpointDescriptorLengths(t *testing.T) { long := append(endpointDescriptor(0x81), 0) long[0] = 8 - if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), long)); err == nil || !strings.Contains(err.Error(), "invalid endpoint descriptor length 8") { + if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), long)); err != nil { t.Fatalf("parseConfiguration() error = %v", err) } vendor := append(endpointDescriptor(0x81), 0, 0) vendor[0] = 9 - if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), vendor)); err == nil || !strings.Contains(err.Error(), "outside audio interface") { + if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), vendor)); err != nil { t.Fatalf("parseConfiguration() error = %v", err) } audioInterface := interfaceDescriptor(0, 0, 1) @@ -426,7 +423,7 @@ func TestActiveConfigurationRejectsUnavailableMetadata(t *testing.T) { for _, test := range []struct { name string length byte - }{{name: "short device descriptor header", length: 17}, {name: "long device descriptor header", length: 19}} { + }{{name: "short device descriptor header", length: 17}, {name: "zero device descriptor header", length: 0}} { t.Run(test.name, func(t *testing.T) { deviceDescriptor := make([]byte, 18) deviceDescriptor[0], deviceDescriptor[1], deviceDescriptor[17] = test.length, 1, 1 @@ -452,7 +449,6 @@ func TestActiveConfigurationRejectsShortAndMalformedDescriptors(t *testing.T) { }) tests := [][]byte{ {10, 2, 9, 0, 0, 1, 0, 0x80, 50}, - {10, 2, 19, 0, 1, 1, 0, 0x80, 50, 0, 9, 4, 0, 0, 0, 0xff, 0xff, 0xff, 0}, {9, 2, 11, 0, 0, 1, 0, 0x80, 50, 0, 4}, {9, 2, 16, 0, 0, 1, 0, 0x80, 50, 7, 5, 0x81, 2, 64, 0, 0}, {9, 2, 18, 0, 1, 1, 0, 0x80, 50, 9, 4, 0, 0, 1, 0xff, 0xff, 0xff, 0}, diff --git a/usb/discovery/discovery.go b/usb/discovery/discovery.go index b886071..115e58f 100644 --- a/usb/discovery/discovery.go +++ b/usb/discovery/discovery.go @@ -22,9 +22,10 @@ func NewAttachment(identity usb.DeviceInfo) Attachment { return Attachment{ident // Identity returns the exact snapshot needed by usb.Enumerator.Open. func (a Attachment) Identity() usb.DeviceInfo { return a.identity } -// Info returns common metadata. The fixed-width key sorts bus, address, VID, -// and PID numerically. Missing or duplicate serials cannot guarantee physical -// ordering across replugging because the host may assign another address. +// Info returns common metadata. For equal serials, registry ordering compares +// the unpadded bus:address location lexicographically, then product and the +// fixed-width identity key. Missing or duplicate serials cannot guarantee +// physical ordering across replugging because the host may assign another address. func (a Attachment) Info() discover.AttachmentInfo { i := a.identity return discover.AttachmentInfo{ diff --git a/usb/discovery/discovery_test.go b/usb/discovery/discovery_test.go index 680ff6a..206101f 100644 --- a/usb/discovery/discovery_test.go +++ b/usb/discovery/discovery_test.go @@ -1,7 +1,9 @@ package discovery import ( + "context" "errors" + "reflect" "testing" "github.com/jon/ostiole/discover" @@ -29,3 +31,29 @@ func TestRegistrationAndDependencyInstallation(t *testing.T) { t.Fatal(err) } } + +func TestEqualSerialUSBLocationsSortLexicographically(t *testing.T) { + attachments := []discover.Attachment{ + NewAttachment(usb.DeviceInfo{Bus: 2, Address: 2, Serial: "same"}), + NewAttachment(usb.DeviceInfo{Bus: 10, Address: 2, Serial: "same"}), + NewAttachment(usb.DeviceInfo{Bus: 2, Address: 10, Serial: "same"}), + } + provider := discover.NewTransportProvider("fixture", func(context.Context) ([]discover.Attachment, error) { + return attachments, nil + }) + var registry discover.Registry + if err := registry.RegisterTransport(provider); err != nil { + t.Fatal(err) + } + inventory, err := registry.Transports(t.Context()) + if err != nil { + t.Fatal(err) + } + var locations []string + for transport := range inventory { + locations = append(locations, transport.Info().Location) + } + if !reflect.DeepEqual(locations, []string{"10:2", "2:10", "2:2"}) { + t.Fatalf("USB location ordering: %v", locations) + } +} diff --git a/usb/endpoint_reload_linux_test.go b/usb/endpoint_reload_linux_test.go new file mode 100644 index 0000000..9b97e75 --- /dev/null +++ b/usb/endpoint_reload_linux_test.go @@ -0,0 +1,150 @@ +//go:build linux + +package usb + +import ( + "encoding/binary" + "fmt" + "os" + "syscall" + "testing" +) + +func TestLinuxKnownAlternateReloadsEndpointDescriptors(t *testing.T) { + for _, loaded := range []bool{false, true} { + t.Run(fmt.Sprintf("previouslyLoaded=%v", loaded), func(t *testing.T) { checkLinuxAlternateReload(t, loaded) }) + } +} + +func checkLinuxAlternateReload(t *testing.T, loaded bool) { + t.Helper() + first, second := endpointDescriptor(0x81), endpointDescriptor(0x82) + binary.LittleEndian.PutUint16(second[4:6], 32) + raw := rawConfiguration(7, 1, interfaceDescriptor(0, 0, 1), first, interfaceDescriptor(0, 1, 1), second) + fixture := configurationFixture(t, [][]byte{raw}, 7) + d := fixture.Device + reply, err := os.CreateTemp(t.TempDir(), "control-reply") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := reply.Close(); err != nil { + t.Error(err) + } + }) + host := &endpointReloadHost{t: t, reply: reply, raw: raw} + d.ioctl = host.ioctl + claim, err := d.ClaimInterface(0) + if err != nil { + t.Fatal(err) + } + if loaded { + if _, err := claim.Endpoint(t.Context(), 0x81); err != nil { + t.Fatal(err) + } + } + before := host.gets + if err := claim.SetAltSetting(1); err != nil { + t.Fatal(err) + } + got, err := claim.Endpoint(t.Context(), 0x82) + if err != nil || got != (Endpoint{Address: 0x82, TransferType: TransferBulk, MaxPacketSize: 32}) { + t.Fatalf("alternate endpoint: %+v %v", got, err) + } + if _, err := claim.Endpoint(t.Context(), 0x81); err == nil { + t.Fatal("old alternate endpoint remained active") + } + if host.gets != before || host.sets != 1 { + t.Fatalf("known alternate reread or repeated: gets%d before%d sets%d", host.gets, before, host.sets) + } + if err := d.Close(); err != nil { + t.Fatal(err) + } + if host.releases != 1 || d.CleanupPending() { + t.Fatal("interface/device cleanup not complete") + } +} + +type endpointReloadHost struct { + t *testing.T + reply *os.File + raw []byte + alternate byte + gets, sets, releases int +} + +func (h *endpointReloadHost) ioctl(_ uintptr, request uintptr, arg any) (uintptr, error) { + switch request { + case usbfsClaimInterface: + if *arg.(*uint32) != 0 { + h.t.Fatal("claimed wrong interface") + } + return 0, nil + case usbfsSetInterface: + if *arg.(*[2]uint32) != [2]uint32{0, 1} { + h.t.Fatal("selected wrong alternate") + } + h.alternate, h.sets = 1, h.sets+1 + return 0, nil + case usbfsReleaseInterface: + h.releases++ + return 0, nil + case usbfsControl: + control := arg.(*usbControlTransfer) + data := endpointReloadReply(h.t, control, h.raw, h.alternate, &h.gets) + if _, err := h.reply.WriteAt(data, 0); err != nil { + h.t.Fatal(err) + } + if _, err := h.reply.Seek(0, 0); err != nil { + h.t.Fatal(err) + } + count, _, errno := syscall.Syscall(syscall.SYS_READ, h.reply.Fd(), control.Data, uintptr(len(data))) + if errno != 0 { + return count, errno + } + return count, nil + default: + h.t.Fatalf("unexpected ioctl %#x", request) + return 0, syscall.EINVAL + } +} + +func endpointReloadReply(t *testing.T, control *usbControlTransfer, raw []byte, alternate byte, gets *int) []byte { + t.Helper() + switch { + case control.RequestType == 0x81 && control.Request == requestGetInterface && control.Length == 1: + *gets++ + return []byte{alternate} + case control.RequestType == 0x80 && control.Request == requestGetConfiguration && control.Length == 1: + return []byte{7} + case control.RequestType == 0x80 && control.Request == requestGetDescriptor && control.Value == 0x100 && control.Length == 18: + data := make([]byte, 18) + data[0], data[1], data[17] = 18, 1, 1 + return data + case control.RequestType == 0x80 && control.Request == requestGetDescriptor && control.Value == 0x200 && control.Length == 9: + return raw[:9] + default: + t.Fatalf("unexpected endpoint metadata request: %+v", control) + return nil + } +} + +func TestLinuxAttachmentCacheAcceptsExtendedHeaders(t *testing.T) { + first, second := extendedConfiguration(2, 0, 0), extendedConfiguration(3, 2, 4) + first[5] = 1 + fixture := configurationFixture(t, [][]byte{first, second}, 7) + got, err := activeConfiguration(t.Context(), fixture) + if err != nil || got.Value != 7 || len(got.Interfaces) != 1 { + t.Fatalf("extended attachment descriptors: %+v %v", got, err) + } +} + +func TestLinuxCacheRejectsHeaderPastConfigurationTotal(t *testing.T) { + fixture := configurationFixture(t, [][]byte{singleInterfaceConfiguration(1), singleInterfaceConfiguration(7)}, 7) + if _, err := fixture.file.WriteAt([]byte{255}, 18); err != nil { + t.Fatal(err) + } + if _, err := activeConfiguration(t.Context(), fixture); err == nil { + t.Fatal("accepted cached header longer than its configuration") + } +} diff --git a/usb/extensions_test.go b/usb/extensions_test.go new file mode 100644 index 0000000..879636b --- /dev/null +++ b/usb/extensions_test.go @@ -0,0 +1,106 @@ +package usb + +import ( + "encoding/binary" + "testing" +) + +func extendedConfiguration(configExtra, interfaceExtra, endpointExtra int) []byte { + header := []byte{9, 2, 0, 0, 1, 7, 0, 0x80, 50} + iface := interfaceDescriptor(0, 0, 1) + endpoint := endpointDescriptor(0x81) + for range configExtra { + header = append(header, 0xde) + } + for range interfaceExtra { + iface = append(iface, 0xad) + } + for range endpointExtra { + endpoint = append(endpoint, 0xbe) + } + header[0], iface[0], endpoint[0] = byte(len(header)), byte(len(iface)), byte(len(endpoint)) + raw := append(header, iface...) + raw = append(raw, endpoint...) + binary.LittleEndian.PutUint16(raw[2:4], uint16(len(raw))) + return raw +} + +func TestDescriptorExtensionsUseDefinedPrefixes(t *testing.T) { + for _, row := range []struct { + name string + configuration, iface, endpoint int + }{{"configuration", 3, 0, 0}, {"interface", 0, 2, 0}, {"endpoint8", 0, 0, 1}, {"endpoint9", 0, 0, 2}, {"all", 3, 2, 4}} { + t.Run(row.name, func(t *testing.T) { + raw := extendedConfiguration(row.configuration, row.iface, row.endpoint) + got, err := parseConfiguration(raw) + if err != nil || got.Value != 7 || len(got.Interfaces) != 1 { + t.Fatalf("extended descriptors: %+v %v", got, err) + } + setting := got.Interfaces[0].Alternates[0] + if setting.Class != 0xff || len(setting.Endpoints) != 1 || setting.Endpoints[0] != (Endpoint{Address: 0x81, TransferType: TransferBulk, MaxPacketSize: 64}) { + t.Fatalf("defined prefix changed: %+v", setting) + } + }) + } +} + +func TestActiveConfigurationAcceptsExtendedHeaderPrefixes(t *testing.T) { + raw := extendedConfiguration(3, 2, 4) + device := make([]byte, 18) + device[0], device[1], device[17] = 20, 1, 1 + f := &scriptedDescriptorDevice{t: t, replies: []descriptorReply{{request: 8, data: []byte{7}}, {request: 6, value: 0x100, data: device}, {request: 6, value: 0x200, data: raw[:9]}, {request: 6, value: 0x200, data: raw}, {request: 8, data: []byte{7}}}} + if got, err := activeConfiguration(t.Context(), f); err != nil || got.Value != 7 { + t.Fatalf("extended header prefixes: %+v %v", got, err) + } +} + +func TestZeroBandwidthIsochronousAlternate(t *testing.T) { + zero := endpointDescriptorWithType(0x81, TransferIsochronous) + zero[4], zero[5], zero[6] = 0, 0, 1 + active := append([]byte(nil), zero...) + binary.LittleEndian.PutUint16(active[4:6], 256) + raw := rawConfiguration(7, 1, interfaceDescriptor(0, 0, 1), zero, interfaceDescriptor(0, 1, 1), active) + got, err := parseConfiguration(raw) + if err != nil { + t.Fatal(err) + } + settings := got.Interfaces[0].Alternates + if len(settings) != 2 || settings[0].Endpoints[0].MaxPacketSize != 0 || settings[1].Endpoints[0].MaxPacketSize != 256 { + t.Fatalf("zero bandwidth alternates: %+v", settings) + } +} + +func TestDescriptorPrefixesStillRequireDefinedBytes(t *testing.T) { + iface := interfaceDescriptor(0, 0, 0)[:8] + iface[0] = 8 + endpoint := endpointDescriptor(0x81)[:6] + endpoint[0] = 6 + for _, raw := range [][]byte{rawConfiguration(1, 1, iface), rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), endpoint)} { + if _, err := parseConfiguration(raw); err == nil { + t.Fatal("accepted short defined prefix") + } + } + raw := extendedConfiguration(0, 0, 0) + raw[0] = 8 + if _, err := parseConfiguration(raw); err == nil { + t.Fatal("accepted short configuration prefix") + } +} + +func TestZeroBandwidthRetainsEndpointBounds(t *testing.T) { + for _, row := range []struct { + kind TransferType + size uint16 + interval byte + }{ + {TransferBulk, 0, 0}, {TransferControl, 0, 0}, + {TransferIsochronous, 0x800, 1}, {TransferIsochronous, 0, 0}, + } { + endpoint := endpointDescriptorWithType(0x81, row.kind) + binary.LittleEndian.PutUint16(endpoint[4:6], row.size) + endpoint[6] = row.interval + if _, err := parseConfiguration(rawConfiguration(1, 1, interfaceDescriptor(0, 0, 1), endpoint)); err == nil { + t.Fatalf("accepted invalid endpoint kind%d size%#x interval%d", row.kind, row.size, row.interval) + } + } +}