Last active
August 23, 2025 22:16
-
-
Save ddlsmurf/fe23050189a93d24ad72fd9e0bb9654a to your computer and use it in GitHub Desktop.
Parse USB definition from http://www.linux-usb.org/usb.ids into json. Example output at https://gist.github.com/ddlsmurf/5419155b9ae145cefe2426e94045982e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /* Parse http://www.linux-usb.org/usb.ids */ | |
| let source = process.argv[2] | |
| ? require('fs/promises').readFile(process.argv[2], 'utf8') | |
| : fetch('http://www.linux-usb.org/usb.ids').then(res => res.text()); | |
| source = source.then(text => parseUSBIDsFile(text)); | |
| // source = source.then(makeExampleSchema); | |
| source.then(result => console.log(JSON.stringify(result, null, 2))); | |
| // // Output in the format of https://github.com/vampirecat35/USBProberV2/blob/master/USBVendors.txt | |
| // source | |
| // .then(({vendor}) => Object.keys(vendor).sort().filter(x => x !== "").map(hexId => parseInt(hexId, 16) + "|" + vendor[hexId].vendor_name)) | |
| // .then(lines => console.log(lines.join("\n"))) | |
| function parseHeaderComment(headerText) { | |
| return { | |
| raw: headerText, | |
| version: /^Version:\s+(.*)$/m.exec(headerText)[1], | |
| date: /^Date:\s+(.*)$/m.exec(headerText)[1], | |
| } | |
| } | |
| function parseSectionHeaderComment(lines) { | |
| let sectionPrefix = null; | |
| const syntaxDefinitionRow = lines.indexOf("Syntax:"); | |
| if (syntaxDefinitionRow < 0) | |
| return false; | |
| const label = lines.slice(0, syntaxDefinitionRow).join("\n").trim(); | |
| const syntax = lines.slice(syntaxDefinitionRow + 1).map(line => { | |
| // Special case because of typo in source file | |
| if (line === "HUT hi _usage_page hid_usage_page_name") | |
| line = "HUT hid_usage_page hid_usage_page_name"; | |
| // There are no interfaces defined under vendor/device it's only in the syntax IGNORE_INTERFACE_SCHEMA | |
| if (line === "interface interface_name\t\t<-- two tabs") | |
| return null; | |
| const [, prefix, definition, name] = /^(?:([A-Z]+)\s+)?(([a-z_]+)[^\t]*)/.exec(line); | |
| if (!sectionPrefix) | |
| sectionPrefix = prefix; | |
| else if (prefix) | |
| throw new Error(`Unexpected multiple prefixes (${prefix}, ${sectionPrefix})"`); | |
| const fields = definition.split(/\s+/g); | |
| if (fields.length != 2) | |
| throw new Error(`Expected only 2 fields but got: ${JSON.stringify(fields)}`); | |
| return fields; | |
| }).filter(x => x); | |
| return {label, prefix: sectionPrefix, syntax}; | |
| } | |
| function parseUSBIDsFile(text) { | |
| const lines = text.split(/\n/g); | |
| const result = { }; | |
| let consecutiveComments = []; | |
| let currentSection = undefined; | |
| let parentStack = []; | |
| lines.forEach((line, lineNumber) => { | |
| try { | |
| if (line.match(/^#/)) { | |
| consecutiveComments.push(line.replace(/^#\s*/, '')); | |
| } else { | |
| if (consecutiveComments.length) { | |
| if (!result.header) { | |
| result.header = { | |
| schema: {}, | |
| ...parseHeaderComment(consecutiveComments.join("\n").trim()) | |
| }; | |
| consecutiveComments = []; | |
| } else { | |
| const section = parseSectionHeaderComment(consecutiveComments); | |
| if (section) { | |
| consecutiveComments = []; | |
| currentSection = section; | |
| parentStack = [ result[currentSection.syntax[0][0]] = { "": { | |
| label: section.label, | |
| fields: section.syntax.map(([ key, value ]) => ({ key, value, width: undefined })), | |
| } } ]; | |
| } | |
| } | |
| } | |
| if (line.trim().length > 0) { | |
| consecutiveComments = []; // ignore comments in the middle of data | |
| const [, prefix, indent, id, name] = /^(?:(?:([A-Z]+)\s+)|(\t{1,2}))?([0-9a-fA-F]+)\s+(.+)$/.exec(line); | |
| const depth = prefix ? 0 : (indent ? indent.length : 0); | |
| if (depth === 0 && prefix !== currentSection.prefix) | |
| throw new Error(`Expected prefix ${currentSection.prefix}`); | |
| if (depth === 2 && currentSection.prefix === undefined) | |
| throw new Error(`Interface defined in file but special case is removing it, search this code for IGNORE_INTERFACE_SCHEMA`); | |
| if (parentStack[0][""].fields[depth].width === undefined) // Set or check width of id field in hex chars | |
| parentStack[0][""].fields[depth].width = id.length; | |
| else if (parentStack[0][""].fields[depth].width != id.length) | |
| throw new Error(`Unexpected id length ${JSON.stringify(parentStack[0][""])}`); | |
| const [ keyInParent, valueKeyName ] = currentSection.syntax[depth]; | |
| const parent = parentStack[depth]; | |
| parentStack.length = depth + 2; | |
| const container = depth ? (parent[keyInParent] ||= {}) : parent; | |
| parentStack[depth + 1] = container[id] = (depth == currentSection.syntax.length - 1 ? name : { [valueKeyName]: name }); | |
| } | |
| } | |
| } catch (e) { | |
| console.error(`\nError on line ${lineNumber + 1}: ${JSON.stringify(line)}\n`); | |
| console.error(e); | |
| process.exit(1); | |
| } | |
| }); | |
| Object.keys(result).filter(k => k !== "header").forEach(sectionName => { | |
| result.header.schema[sectionName] = result[sectionName][""]; | |
| delete result[sectionName][""]; | |
| }); | |
| return result; | |
| } | |
| function makeExampleSchema(parsedUSBInfo) { | |
| const result = { | |
| header: { | |
| ...parsedUSBInfo.header, | |
| raw: 'The whole text in the comment at the start of the file', | |
| } | |
| }; | |
| Object.keys(parsedUSBInfo).sort().forEach(sectionKey => { | |
| if (sectionKey === "header") return; | |
| const exampleNumber = w => new Array(w + 1).join("x"); | |
| const schema = parsedUSBInfo.header.schema[sectionKey]; | |
| let parent = result[sectionKey] = { }; | |
| schema.fields.forEach(({ key, value, width }, fieldIndex) => { | |
| if (fieldIndex < schema.fields.length - 1) { | |
| parent = parent[exampleNumber(width)] = {}; | |
| parent[value] = `Label for ${key}`; | |
| } else { | |
| if (fieldIndex > 0) | |
| parent = (parent[key] ||= {}); | |
| parent = parent[exampleNumber(width)] = value; | |
| } | |
| }); | |
| }); | |
| return result; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "header": { | |
| "schema": { | |
| "vendor": { | |
| "label": "Vendors, devices and interfaces. Please keep sorted.", | |
| "fields": [ | |
| { | |
| "key": "vendor", | |
| "value": "vendor_name", | |
| "width": 4 | |
| }, | |
| { | |
| "key": "device", | |
| "value": "device_name", | |
| "width": 4 | |
| } | |
| ] | |
| }, | |
| "class": { | |
| "label": "List of known device classes, subclasses and protocols", | |
| "fields": [ | |
| { | |
| "key": "class", | |
| "value": "class_name", | |
| "width": 2 | |
| }, | |
| { | |
| "key": "subclass", | |
| "value": "subclass_name", | |
| "width": 2 | |
| }, | |
| { | |
| "key": "protocol", | |
| "value": "protocol_name", | |
| "width": 2 | |
| } | |
| ] | |
| }, | |
| "terminal_type": { | |
| "label": "List of Video Class Terminal Types", | |
| "fields": [ | |
| { | |
| "key": "terminal_type", | |
| "value": "terminal_type_name", | |
| "width": 4 | |
| } | |
| ] | |
| }, | |
| "descriptor_type": { | |
| "label": "List of HID Descriptor Types", | |
| "fields": [ | |
| { | |
| "key": "descriptor_type", | |
| "value": "descriptor_type_name", | |
| "width": 2 | |
| } | |
| ] | |
| }, | |
| "item_type": { | |
| "label": "List of Physical Descriptor Item Types", | |
| "fields": [ | |
| { | |
| "key": "item_type", | |
| "value": "item_type_name", | |
| "width": 2 | |
| } | |
| ] | |
| }, | |
| "hid_usage_page": { | |
| "label": "List of HID Usages", | |
| "fields": [ | |
| { | |
| "key": "hid_usage_page", | |
| "value": "hid_usage_page_name", | |
| "width": 2 | |
| }, | |
| { | |
| "key": "hid_usage", | |
| "value": "hid_usage_name", | |
| "width": 3 | |
| } | |
| ] | |
| }, | |
| "language_id": { | |
| "label": "List of Languages", | |
| "fields": [ | |
| { | |
| "key": "language_id", | |
| "value": "language_name", | |
| "width": 4 | |
| }, | |
| { | |
| "key": "dialect_id", | |
| "value": "dialect_name", | |
| "width": 2 | |
| } | |
| ] | |
| }, | |
| "country_code": { | |
| "label": "HID Descriptor bCountryCode\nHID Specification 1.11 (2001-06-27) page 23", | |
| "fields": [ | |
| { | |
| "key": "country_code", | |
| "value": "keymap_type", | |
| "width": 2 | |
| } | |
| ] | |
| } | |
| }, | |
| "raw": "The whole text in the comment at the start of the file", | |
| "version": "2025.07.26", | |
| "date": "2025-07-26 20:34:01" | |
| }, | |
| "class": { | |
| "xx": { | |
| "class_name": "Label for class", | |
| "xx": { | |
| "subclass_name": "Label for subclass", | |
| "protocol": { | |
| "xx": "protocol_name" | |
| } | |
| } | |
| } | |
| }, | |
| "country_code": { | |
| "xx": "keymap_type" | |
| }, | |
| "descriptor_type": { | |
| "xx": "descriptor_type_name" | |
| }, | |
| "hid_usage_page": { | |
| "xx": { | |
| "hid_usage_page_name": "Label for hid_usage_page", | |
| "hid_usage": { | |
| "xxx": "hid_usage_name" | |
| } | |
| } | |
| }, | |
| "item_type": { | |
| "xx": "item_type_name" | |
| }, | |
| "language_id": { | |
| "xxxx": { | |
| "language_name": "Label for language_id", | |
| "dialect_id": { | |
| "xx": "dialect_name" | |
| } | |
| } | |
| }, | |
| "terminal_type": { | |
| "xxxx": "terminal_type_name" | |
| }, | |
| "vendor": { | |
| "xxxx": { | |
| "vendor_name": "Label for vendor", | |
| "device": { | |
| "xxxx": "device_name" | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment