Skip to content

Instantly share code, notes, and snippets.

@ddlsmurf
Last active August 23, 2025 22:16
Show Gist options
  • Select an option

  • Save ddlsmurf/fe23050189a93d24ad72fd9e0bb9654a to your computer and use it in GitHub Desktop.

Select an option

Save ddlsmurf/fe23050189a93d24ad72fd9e0bb9654a to your computer and use it in GitHub Desktop.
#!/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;
}
{
"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