#!/bin/sh
# ABOUTME: Lists cameras as `id|vid:pid|label|on/off|video[+audio]`, one line each, for the camera module.
# ABOUTME: Identifies a camera by USB interface class 0x0e in the kernel's descriptor blob.

# The interface *directories* only exist while the device is authorized, so they cannot answer
# "is this a camera" for a switched-off one. The raw descriptor blob can: it is allocated at
# enumeration, stays world-readable, and carries every interface class.
# Prints "video" plus every other notable function the same physical device carries. Switching a
# camera on authorizes the *whole* device, so anything else on it becomes active too — the popout
# shows this so an innocuous-looking "camera" that also brings a keyboard is visible as such.
classes_of() {
    od -An -v -tu1 "$1/descriptors" 2>/dev/null | awk '
        { for (k = 1; k <= NF; k++) b[n++] = $k }
        END {
            p = 0
            while (p < n) {
                l = b[p] + 0
                if (l == 0)          # malformed or short read — stop rather than walk off
                    break
                if (b[p + 1] == 4) { # interface descriptor
                    c = b[p + 5] + 0
                    if (c == 14)
                        seen["video"] = 1
                    else if (c == 1)
                        seen["audio"] = 1
                    else if (c == 3)
                        seen["input"] = 1
                    else if (c == 8)
                        seen["storage"] = 1
                    else if (c == 2 || c == 10)
                        seen["network"] = 1
                    else if (c == 11)
                        seen["smartcard"] = 1
                    else if (c == 224)
                        seen["wireless"] = 1
                    else if (c == 7)
                        seen["printer"] = 1
                }
                p += l
            }
            if (!seen["video"])
                exit
            out = "video"
            # Fixed order so the field is stable across runs.
            split("audio input storage network smartcard wireless printer", order, " ")
            for (i = 1; i <= 7; i++)
                if (seen[order[i]])
                    out = out "+" order[i]
            printf "%s", out
        }'
}

# Product strings come from the device, i.e. from whoever plugs it in. Strip the field separator and
# control characters so a hostile string cannot forge extra columns, and the invisible/directional
# Unicode range (U+200B..U+200F, U+202A..U+202E, U+2066..U+2069) so a name cannot be made to *look*
# like a different device. LC_ALL=C keeps this byte-oriented, which is what the \x patterns need.
sanitize() {
    LC_ALL=C sed 's/[|[:cntrl:]]//g
        s/\xe2\x80[\x8b-\x8f\xaa-\xae]//g
        s/\xe2\x81[\xa6-\xa9]//g
        s/   */ /g
        s/^ //
        s/ $//' | cut -c1-64
}

for dev in /sys/bus/usb/devices/*-*; do
    [ -d "$dev" ] || continue
    id=${dev##*/}
    case "$id" in
        *:*) continue ;; # interface, not a device
    esac

    kind=$(classes_of "$dev")
    [ -n "$kind" ] || continue

    label=$(cat "$dev/product" 2>/dev/null | sanitize)
    [ -n "$label" ] || label=$(cat "$dev/manufacturer" 2>/dev/null | sanitize)
    [ -n "$label" ] || label="USB camera"

    if [ "$(cat "$dev/authorized" 2>/dev/null)" = "0" ]; then
        state=off
    else
        state=on
    fi

    printf '%s|%s:%s|%s|%s|%s\n' \
        "$id" \
        "$(cat "$dev/idVendor" 2>/dev/null)" \
        "$(cat "$dev/idProduct" 2>/dev/null)" \
        "$label" \
        "$state" \
        "$kind"
done | sort -t'|' -k1,1
