ajhahn.de
← FlashOS
Flash 419 lines
// usb_descriptors: byte-exact USB descriptor set + SETUP-packet decode
// for the DWC2 gadget. Pure data + pure functions — no MMIO, no extern —
// so it host-unit-tests with no hardware (mirrors src/mailbox.zig and
// src/sdhci_cmd.zig). The rpi4b driver (src/board/rpi4b/usb.zig) imports
// this as the named module "usb_descriptors"; the host-test build runs
// the tests at the bottom.
//
// CDC-ACM: the descriptor set below describes a CDC-ACM serial
// function — a Communications interface (class 2 / subclass ACM 2) carrying
// the Header / Call-Management / ACM / Union functional descriptors + an
// interrupt-IN notification endpoint, and a Data interface (class 0x0A) with
// bulk IN + bulk OUT. macOS binds AppleUSBCDCACM by interface class and
// creates /dev/tty.usbmodem<serial>. The bulk/interrupt endpoints are
// declared here but only hardware-configured by the data-path code in
// usb.zig; this module covers enumeration + the EP0 class requests.

const std = #import("std")

// --- Descriptor type codes (USB 2.0 §9.4, table 9-5). ---
pub const DESC_DEVICE u8 = 1
pub const DESC_CONFIG u8 = 2
pub const DESC_STRING u8 = 3
pub const DESC_INTERFACE u8 = 4
pub const DESC_ENDPOINT u8 = 5
pub const DESC_DEVICE_QUALIFIER u8 = 6
pub const DESC_OTHER_SPEED u8 = 7
// CDC class-specific descriptor type (USB CDC 1.1 §5.2.3): the functional
// descriptors carried inside the Communications interface.
pub const CS_INTERFACE u8 = 0x24

// --- Standard device requests (USB 2.0 §9.4, table 9-4). ---
pub const REQ_GET_STATUS u8 = 0x00
pub const REQ_CLEAR_FEATURE u8 = 0x01
pub const REQ_SET_FEATURE u8 = 0x03
pub const REQ_SET_ADDRESS u8 = 0x05
pub const REQ_GET_DESCRIPTOR u8 = 0x06
pub const REQ_SET_DESCRIPTOR u8 = 0x07
pub const REQ_GET_CONFIGURATION u8 = 0x08
pub const REQ_SET_CONFIGURATION u8 = 0x09

// --- CDC-ACM class-specific requests (USB CDC PSTN 1.2 §6.3). macOS issues
// these when an app opens the tty; the device must ACK them. ---
pub const REQ_SET_LINE_CODING u8 = 0x20
pub const REQ_GET_LINE_CODING u8 = 0x21
pub const REQ_SET_CONTROL_LINE_STATE u8 = 0x22
pub const REQ_SEND_BREAK u8 = 0x23

// Pinned identity: pid.codes vendor 0x1209, a throwaway test product id.
// macOS binds by class, so the exact pair is irrelevant for enumeration;
// any non-colliding pair works for the spike.
pub const VID u16 = 0x1209
pub const PID u16 = 0x0001

// Full-Speed EP0 max packet.
pub const EP0_MPS u16 = 64

// CDC line coding (USB CDC PSTN §6.3.11): dwDTERate (LE u32) + bCharFormat
// (stop bits) + bParityType + bDataBits. Default 115200 8N1 — cosmetic over
// USB (there is no real UART), but macOS reads it back via GET_LINE_CODING.
pub const line_coding_default = [7]u8{ 0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x08 }

// Device descriptor (18 bytes). bcdUSB 0x0200; CDC (Communications) class at
// the device level so macOS binds AppleUSBCDCACM via the comms interface;
// FS EP0 = 64.
pub const device_descriptor = [18]u8{
    0x12, DESC_DEVICE,
    0x00, 0x02, // bcdUSB = 0x0200
    0x02, 0x00, // bDeviceClass = CDC (Communications); bDeviceSubClass = 0
    0x00, 0x40, // bDeviceProtocol = 0; bMaxPacketSize0 = 64
    0x09, 0x12, // idVendor  = 0x1209 (little-endian)
    0x01, 0x00, // idProduct = 0x0001 (little-endian)
    0x00, 0x01, // bcdDevice = 0x0100
    0x01, 0x02,
    0x03, 0x01, // iManufacturer/iProduct/iSerial = 1/2/3; bNumConfigurations = 1
}

// CDC-ACM configuration block (67 bytes): config (9) + comms interface (9) +
// Header/Call-Mgmt/ACM/Union functional descriptors (5+5+4+5) + notify EP (7)
// + data interface (9) + bulk OUT (7) + bulk IN (7). Two interfaces; bus-
// powered, 100 mA. Endpoint map: EP1 IN = interrupt notify, EP2 IN/OUT = bulk.
pub const config_descriptor = [67]u8{
    // configuration descriptor (9)
    0x09, DESC_CONFIG,
    0x43, 0x00, // wTotalLength = 67 (little-endian)
    0x02, // bNumInterfaces = comms + data
    0x01, // bConfigurationValue
    0x00, // iConfiguration
    0x80, // bmAttributes = bus-powered, no remote wakeup
    0x32, // bMaxPower = 0x32 * 2 mA = 100 mA

    // interface 0: Communications class (9)
    0x09, DESC_INTERFACE,
    0x00, // bInterfaceNumber = 0
    0x00, // bAlternateSetting
    0x01, // bNumEndpoints = 1 (interrupt-IN notification)
    0x02, // bInterfaceClass = CDC (Communications)
    0x02, // bInterfaceSubClass = Abstract Control Model
    0x01, // bInterfaceProtocol = AT/V.25ter (0x00 also binds on macOS)
    0x00, // iInterface

    // CDC Header functional descriptor (5)
    0x05, CS_INTERFACE,
    0x00, // bDescriptorSubtype = Header
    0x10, 0x01, // bcdCDC = 0x0110 (little-endian)

    // CDC Call-Management functional descriptor (5)
    0x05, CS_INTERFACE,
    0x01, // bDescriptorSubtype = Call Management
    0x00, // bmCapabilities = device does not handle call management
    0x01, // bDataInterface = 1

    // CDC Abstract-Control-Management functional descriptor (4)
    0x04, CS_INTERFACE,
    0x02, // bDescriptorSubtype = ACM
    0x02, // bmCapabilities = Set/Get_Line_Coding + Set_Control_Line_State

    // CDC Union functional descriptor (5)
    0x05, CS_INTERFACE,
    0x06, // bDescriptorSubtype = Union
    0x00, // bControlInterface = 0 (comms)
    0x01, // bSubordinateInterface0 = 1 (data)

    // endpoint: notification (interrupt IN, EP1) (7)
    0x07, DESC_ENDPOINT,
    0x81, // bEndpointAddress = EP1 IN
    0x03, // bmAttributes = interrupt
    0x10, 0x00, // wMaxPacketSize = 16 (little-endian)
    0x10, // bInterval = 16 (FS frames)

    // interface 1: Data class (9)
    0x09, DESC_INTERFACE,
    0x01, // bInterfaceNumber = 1
    0x00, // bAlternateSetting
    0x02, // bNumEndpoints = 2 (bulk IN + bulk OUT)
    0x0A, // bInterfaceClass = CDC Data
    0x00, // bInterfaceSubClass
    0x00, // bInterfaceProtocol
    0x00, // iInterface

    // endpoint: bulk OUT (EP2) (7)
    0x07, DESC_ENDPOINT,
    0x02, // bEndpointAddress = EP2 OUT
    0x02, // bmAttributes = bulk
    0x40, 0x00, // wMaxPacketSize = 64 (little-endian)
    0x00, // bInterval

    // endpoint: bulk IN (EP2) (7)
    0x07, DESC_ENDPOINT,
    0x82, // bEndpointAddress = EP2 IN
    0x02, // bmAttributes = bulk
    0x40, 0x00, // wMaxPacketSize = 64 (little-endian)
    0x00, // bInterval
}

// String index 0 = supported-LANGID list. macOS requests index 0 during
// enumeration; a missing index 0 trips the whole enum. Single LANGID =
// 0x0409 (en-US).
pub const str_langid = [4]u8{ 0x04, DESC_STRING, 0x09, 0x04 }

// Build a UTF-16LE string descriptor at comptime from an ASCII literal.
fn utf16leStringDesc(comptime s []u8) [2 + 2 * s.len]u8 {
    var buf [2 + 2 * s.len]u8 = undefined
    buf[0] = #intCast(buf.len) // bLength
    buf[1] = DESC_STRING
    inline for c, i in s {
        buf[2 + i * 2] = c
        buf[2 + i * 2 + 1] = 0x00
    }
    return buf
}

pub const str_manufacturer = utf16leStringDesc("FlashOS")
pub const str_product = utf16leStringDesc("FlashOS Serial")
// Fixed serial → a deterministic /dev/tty.usbmodem node once CDC lands.
pub const str_serial = utf16leStringDesc("0001")

// Resolve a GET_DESCRIPTOR request to its byte slice, or null when the
// device should STALL EP0 (unknown type, unknown string index, and the
// HS-only DEVICE_QUALIFIER / OTHER_SPEED descriptors a Full-Speed-only
// device must reject).
pub fn getDescriptor(desc_type u8, index u8) ?[]u8 {
    return switch desc_type {
        DESC_DEVICE => &device_descriptor,
        DESC_CONFIG => &config_descriptor,
        DESC_STRING => switch index {
            0 => &str_langid,
            1 => &str_manufacturer,
            2 => &str_product,
            3 => &str_serial,
            else => null,
        },
        else => null,
    }
}

// Decoded 8-byte SETUP packet (USB 2.0 §9.3, all multi-byte fields LE).
pub const Setup = struct {
    bmRequestType u8,
    bRequest u8,
    wValue u16,
    wIndex u16,
    wLength u16,

    // GET_DESCRIPTOR packs type in the high byte, index in the low byte.
    pub fn descType(self Setup) u8 {
        return #intCast(self.wValue >> 8)
    }
    pub fn descIndex(self Setup) u8 {
        return #intCast(self.wValue & 0x00FF)
    }
    // SET_ADDRESS carries a 7-bit address in wValue.
    pub fn address(self Setup) u8 {
        return #intCast(self.wValue & 0x007F)
    }
}

pub fn decodeSetup(raw *[8]u8) Setup {
    return .{
        .bmRequestType = raw[0],
        .bRequest = raw[1],
        .wValue = #as(u16, raw[2]) | (#as(u16, raw[3]) << 8),
        .wIndex = #as(u16, raw[4]) | (#as(u16, raw[5]) << 8),
        .wLength = #as(u16, raw[6]) | (#as(u16, raw[7]) << 8),
    }
}

// --- Host tests ---

const testing = std.testing

test "device descriptor is byte-exact (CDC class at device level)" {
    try testing.expectEqual(#as(usize, 18), device_descriptor.len)
    try testing.expectEqual(#as(u8, 18), device_descriptor[0]) // bLength
    try testing.expectEqual(DESC_DEVICE, device_descriptor[1])
    try testing.expectEqual(#as(u8, 0x02), device_descriptor[4]) // bDeviceClass = CDC
    try testing.expectEqual(#as(u8, 0x00), device_descriptor[5]) // bDeviceSubClass
    try testing.expectEqual(#as(u8, 0x00), device_descriptor[6]) // bDeviceProtocol
    try testing.expectEqual(#as(u8, 0x40), device_descriptor[7]) // bMaxPacketSize0 = 64
    // idVendor 0x1209 / idProduct 0x0001, little-endian.
    try testing.expectEqual(#as(u8, 0x09), device_descriptor[8])
    try testing.expectEqual(#as(u8, 0x12), device_descriptor[9])
    try testing.expectEqual(#as(u8, 0x01), device_descriptor[10])
    try testing.expectEqual(#as(u8, 0x00), device_descriptor[11])
    try testing.expectEqual(#as(u8, 1), device_descriptor[17]) // bNumConfigurations
}

test "config descriptor: CDC-ACM two-interface layout is byte-exact" {
    try testing.expectEqual(#as(usize, 67), config_descriptor.len)
    // configuration descriptor
    try testing.expectEqual(#as(u8, 9), config_descriptor[0])
    try testing.expectEqual(DESC_CONFIG, config_descriptor[1])
    try testing.expectEqual(#as(u8, 0x43), config_descriptor[2]) // wTotalLength = 67 lo
    try testing.expectEqual(#as(u8, 0x00), config_descriptor[3]) // wTotalLength hi
    try testing.expectEqual(#as(u8, 2), config_descriptor[4]) // bNumInterfaces
    try testing.expectEqual(#as(u8, 1), config_descriptor[5]) // bConfigurationValue
    // interface 0: Communications class (subclass ACM, 1 notify EP)
    try testing.expectEqual(DESC_INTERFACE, config_descriptor[10])
    try testing.expectEqual(#as(u8, 0), config_descriptor[11]) // bInterfaceNumber
    try testing.expectEqual(#as(u8, 1), config_descriptor[13]) // bNumEndpoints
    try testing.expectEqual(#as(u8, 0x02), config_descriptor[14]) // CDC
    try testing.expectEqual(#as(u8, 0x02), config_descriptor[15]) // ACM
    // notification endpoint (interrupt IN, EP1)
    try testing.expectEqual(DESC_ENDPOINT, config_descriptor[38])
    try testing.expectEqual(#as(u8, 0x81), config_descriptor[39])
    try testing.expectEqual(#as(u8, 0x03), config_descriptor[40]) // interrupt
    // interface 1: Data class + bulk endpoints
    try testing.expectEqual(#as(u8, 0x0A), config_descriptor[49]) // CDC Data
    try testing.expectEqual(#as(u8, 0x02), config_descriptor[55]) // bulk OUT addr
    try testing.expectEqual(#as(u8, 0x40), config_descriptor[57]) // bulk OUT MPS = 64
    try testing.expectEqual(#as(u8, 0x82), config_descriptor[62]) // bulk IN addr
    try testing.expectEqual(#as(u8, 0x40), config_descriptor[64]) // bulk IN MPS = 64
}

test "CDC Union functional descriptor links comms->data (macOS binds on this)" {
    try testing.expectEqual(#as(u8, 5), config_descriptor[32]) // bFunctionLength
    try testing.expectEqual(CS_INTERFACE, config_descriptor[33])
    try testing.expectEqual(#as(u8, 0x06), config_descriptor[34]) // Union subtype
    try testing.expectEqual(#as(u8, 0), config_descriptor[35]) // bControlInterface = comms
    try testing.expectEqual(#as(u8, 1), config_descriptor[36]) // bSubordinateInterface0 = data
    // Call-Management functional descriptor points at the data interface.
    try testing.expectEqual(#as(u8, 0x01), config_descriptor[25]) // Call-Mgmt subtype
    try testing.expectEqual(#as(u8, 1), config_descriptor[27]) // bDataInterface = 1
}

test "LANGID string descriptor (index 0) is mandatory and exact" {
    // A missing/wrong index-0 LANGID is the classic macOS enum-abort cause.
    try testing.expectEqualSlices(u8, &[_]u8{ 0x04, 0x03, 0x09, 0x04 }, &str_langid)
}

test "getDescriptor resolves the known descriptors and STALLs the rest" {
    try testing.expect(getDescriptor(DESC_DEVICE, 0) != null)
    try testing.expectEqual(#as(usize, 67), getDescriptor(DESC_CONFIG, 0).?.len)
    try testing.expectEqual(#as(usize, 4), getDescriptor(DESC_STRING, 0).?.len)
    try testing.expect(getDescriptor(DESC_STRING, 1) != null)
    try testing.expect(getDescriptor(DESC_STRING, 9) == null) // unknown string → STALL
    try testing.expect(getDescriptor(DESC_DEVICE_QUALIFIER, 0) == null) // FS-only → STALL
    try testing.expect(getDescriptor(DESC_OTHER_SPEED, 0) == null) // FS-only → STALL
}

test "string descriptors carry UTF-16LE bodies with correct bLength" {
    // "FlashOS" = 7 chars → bLength = 2 + 2*7 = 16.
    try testing.expectEqual(#as(u8, 16), str_manufacturer[0])
    try testing.expectEqual(DESC_STRING, str_manufacturer[1])
    try testing.expectEqual(#as(u8, 'F'), str_manufacturer[2])
    try testing.expectEqual(#as(u8, 0x00), str_manufacturer[3])
    try testing.expectEqual(#as(u8, 'l'), str_manufacturer[4])
    // "0001" = 4 chars → bLength = 10.
    try testing.expectEqual(#as(u8, 10), str_serial[0])
}

test "line coding default is 115200 8N1" {
    // dwDTERate = 115200 = 0x0001C200, little-endian.
    try testing.expectEqual(#as(u8, 0x00), line_coding_default[0])
    try testing.expectEqual(#as(u8, 0xC2), line_coding_default[1])
    try testing.expectEqual(#as(u8, 0x01), line_coding_default[2])
    try testing.expectEqual(#as(u8, 0x00), line_coding_default[3])
    try testing.expectEqual(#as(u8, 0x00), line_coding_default[4]) // 1 stop bit
    try testing.expectEqual(#as(u8, 0x00), line_coding_default[5]) // no parity
    try testing.expectEqual(#as(u8, 0x08), line_coding_default[6]) // 8 data bits
}

test "decodeSetup unpacks a GET_DESCRIPTOR(device) request" {
    // bmRT=0x80 (D2H,std,device) bReq=0x06 wValue=0x0100 (DEVICE,idx0) wLen=64.
    const raw = [8]u8{ 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x40, 0x00 }
    const s = decodeSetup(&raw)
    try testing.expectEqual(#as(u8, 0x80), s.bmRequestType)
    try testing.expectEqual(REQ_GET_DESCRIPTOR, s.bRequest)
    try testing.expectEqual(#as(u16, 0x0100), s.wValue)
    try testing.expectEqual(DESC_DEVICE, s.descType())
    try testing.expectEqual(#as(u8, 0), s.descIndex())
    try testing.expectEqual(#as(u16, 64), s.wLength)
}

test "decodeSetup masks the SET_ADDRESS 7-bit address" {
    // SET_ADDRESS to 0x6B; high bit of wValue must be masked off.
    const raw = [8]u8{ 0x00, 0x05, 0xEB, 0x00, 0x00, 0x00, 0x00, 0x00 }
    const s = decodeSetup(&raw)
    try testing.expectEqual(REQ_SET_ADDRESS, s.bRequest)
    try testing.expectEqual(#as(u8, 0x6B), s.address()) // 0xEB & 0x7F
}

test "decodeSetup unpacks a SET_CONFIGURATION request" {
    const raw = [8]u8{ 0x00, 0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 }
    const s = decodeSetup(&raw)
    try testing.expectEqual(REQ_SET_CONFIGURATION, s.bRequest)
    try testing.expectEqual(#as(u16, 1), s.wValue)
}

test "decodeSetup unpacks a CDC SET_LINE_CODING request" {
    // bmRT=0x21 (H2D, class, interface) bReq=0x20 wIndex=0 wLength=7.
    const raw = [8]u8{ 0x21, 0x20, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00 }
    const s = decodeSetup(&raw)
    try testing.expectEqual(#as(u8, 0x21), s.bmRequestType)
    try testing.expectEqual(REQ_SET_LINE_CODING, s.bRequest)
    try testing.expectEqual(#as(u16, 7), s.wLength)
}

test "config descriptor sub-descriptor bLength chain sums to wTotalLength" {
    // Walk the descriptor chain by bLength; it must land exactly on the
    // buffer end. Catches an edit that resizes a functional descriptor but
    // forgets wTotalLength — a silent enumeration-corrupting bug macOS
    // surfaces only as an unhelpful "device not configured".
    var i usize = 0
    while i < config_descriptor.len {
        try testing.expect(config_descriptor[i] != 0) // bLength 0 = malformed (would never terminate on hw)
        i += config_descriptor[i]
    }
    try testing.expectEqual(#as(usize, 67), i) // chain ends exactly at the buffer end
    const wtotal = #as(u16, config_descriptor[2]) | (#as(u16, config_descriptor[3]) << 8)
    try testing.expectEqual(#as(u16, config_descriptor.len), wtotal)
}

test "getDescriptor(CONFIG) serves exactly wTotalLength bytes" {
    // The served slice length must match the descriptor's own wTotalLength
    // field, or the host's two-stage config fetch (9-byte header, then full)
    // reads a truncated/over-long body.
    const d = getDescriptor(DESC_CONFIG, 0).?
    const wtotal = #as(u16, d[2]) | (#as(u16, d[3]) << 8)
    try testing.expectEqual(#as(usize, 67), d.len)
    try testing.expectEqual(#as(u16, #intCast(d.len)), wtotal)
}

test "descType/descIndex split CONFIG and STRING wValue correctly" {
    // GET_DESCRIPTOR(CONFIG, 0) → wValue 0x0200; (STRING, 3 = serial) → 0x0303.
    const cfg_raw = [8]u8{ 0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 0xFF, 0x00 }
    const cfg = decodeSetup(&cfg_raw)
    try testing.expectEqual(DESC_CONFIG, cfg.descType())
    try testing.expectEqual(#as(u8, 0), cfg.descIndex())
    const str_raw = [8]u8{ 0x80, 0x06, 0x03, 0x03, 0x09, 0x04, 0xFF, 0x00 }
    const str = decodeSetup(&str_raw)
    try testing.expectEqual(DESC_STRING, str.descType())
    try testing.expectEqual(#as(u8, 3), str.descIndex())
}

test "decodeSetup unpacks the CDC SET_CONTROL_LINE_STATE request" {
    // macOS sends this on tty open: bmRT=0x21, bReq=0x22, wValue bit0=DTR
    // bit1=RTS, wLength=0 (no data stage — dispatchSetup ZLP-acks it).
    const raw = [8]u8{ 0x21, 0x22, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }
    const s = decodeSetup(&raw)
    try testing.expectEqual(#as(u8, 0x21), s.bmRequestType)
    try testing.expectEqual(REQ_SET_CONTROL_LINE_STATE, s.bRequest)
    try testing.expectEqual(#as(u16, 0x0003), s.wValue) // DTR|RTS asserted
    try testing.expectEqual(#as(u16, 0), s.wLength)
}

test "decodeSetup unpacks GET_CONFIGURATION / GET_STATUS / CLEAR_FEATURE" {
    // The remaining standard requests dispatchSetup answers — round-trip
    // each so every arm of that switch has decode coverage.
    const getcfg = [8]u8{ 0x80, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 }
    try testing.expectEqual(REQ_GET_CONFIGURATION, decodeSetup(&getcfg).bRequest)
    const getsts = [8]u8{ 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00 }
    try testing.expectEqual(REQ_GET_STATUS, decodeSetup(&getsts).bRequest)
    // CLEAR_FEATURE(ENDPOINT_HALT) on EP2 IN: bmRT=0x02 (endpoint), wIndex=0x82.
    const clrf = [8]u8{ 0x02, 0x01, 0x00, 0x00, 0x82, 0x00, 0x00, 0x00 }
    const s = decodeSetup(&clrf)
    try testing.expectEqual(REQ_CLEAR_FEATURE, s.bRequest)
    try testing.expectEqual(#as(u16, 0x0082), s.wIndex)
}