Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: CI

on:
push:
branches: [ "main" ]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
53 changes: 53 additions & 0 deletions _tests/parser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseDescriptor, stripComments } from '../parser.js';

const defaultOpts = { separator: ',', prefix: '0x', comment: '//' };
const simpleDescriptorBytes = [0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x81, 0x02, 0xC0];
const simpleDescriptorOutput = [
'0x05,0x01, // Usage Page (Generic Desktop Page)',
'0x09,0x02, // Usage (Mouse)',
'0xA1,0x01, // Collection (Application)',
'0x81,0x02, // Input (Data,Variable,Absolute,No Wrap,Linear,Preferred State,No Null position,Bit Field)',
'0xC0, // End Collection',
].join('\n') + '\n';

test('stripComments removes C-style, C++-style and hash comments', () => {
const input = `0x01, 0x02 // comment\n# another\n/* block\ncomment */\n0x03`;
const result = stripComments(input);

assert.ok(!result.includes('//'));
assert.ok(!result.includes('# another'));
assert.ok(!result.includes('/*'));

const tokens = result
.split(/\s+/)
.filter(Boolean);
assert.deepStrictEqual(tokens, ['0x01,', '0x02', '0x03']);
});

test('parseDescriptor formats a simple descriptor with indentation', () => {
const output = parseDescriptor(simpleDescriptorBytes, defaultOpts);
assert.strictEqual(output, simpleDescriptorOutput);
});

test('parseDescriptor falls back to default formatting options', () => {
const output = parseDescriptor(simpleDescriptorBytes);
assert.strictEqual(output, simpleDescriptorOutput);
});

test('parseDescriptor reports parsing errors in the output', () => {
const originalError = console.error;
let loggedMessage = '';
console.error = (...args) => {
loggedMessage = args.join(' ');
};

try {
const output = parseDescriptor([0xC1, 0x00], defaultOpts);
assert.strictEqual(output, '// ERROR: End Collection: (0-byte arg) expected\n');
assert.match(loggedMessage, /Error processing byte/);
} finally {
console.error = originalError;
}
});
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "hidparser",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "node --test $(find _tests -name '*.js' -print)"
}
}
2 changes: 1 addition & 1 deletion parser-long.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function handleLongItem(bytes, i, opts) {
return { text: "", comment: "ERROR: Unexpected end of data", advance: 1, error: true };
}

const line = joinHex([b, bDataSize, bLongItemTag], opts);
const line = joinHex(null, [b, bDataSize, bLongItemTag], opts);
return { text: line, comment: "Long Item", advance: 3, error: false };
}

Expand Down
8 changes: 4 additions & 4 deletions parser-short.js
Original file line number Diff line number Diff line change
Expand Up @@ -3020,7 +3020,7 @@ function handleMainItem(bTag, bSize, bytes, i, opts, indent) {
throw new Error(`End Collection: (0-byte arg) expected`);
}
const comment = "End Collection";
const line = joinHex(b, null, opts, 0) + ",";
const line = joinHex(b, null, opts, 0);
return { text: line, comment, advance: 1, indentChange: -1 };
}
case 0x8: { // Input
Expand Down Expand Up @@ -3130,15 +3130,15 @@ function handleGlobalItem(bTag, bSize, bytes, i, opts) {
throw new Error(`Push: (0-byte arg) expected`);
}
const comment = "Push";
const line = joinHex([b], null, opts, 0) + ",";
const line = joinHex(b, null, opts, 0);
return { text: line, comment, advance: 1, indentChange: 0 };
}
case 0xB0: { // Pop (0 bytes)
if (nBytes <= 0) {
throw new Error(`Pop: (0-byte arg) expected`);
}
const comment = "Pop";
const line = joinHex([b], null, opts, 0) + ",";
const line = joinHex(b, null, opts, 0);
return { text: line, comment, advance: 1, indentChange: 0 };
}
default: {
Expand Down Expand Up @@ -3232,7 +3232,7 @@ function handleShortItem(bytes, i, opts, indent, usagePage) {
return handleLocalItem(bTag, bSize, bytes, i, opts, usagePage);
default:
const comment = `Unknown (${toHex(b, opts)})`;
const line = joinHex([b], opts) + ",";
const line = joinHex(b, null, opts, 0);
return { text: line, comment, advance: 1, indentChange: 0, error: true };
}
}
Expand Down
62 changes: 42 additions & 20 deletions parser.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { handleShortItem } from "./parser-short.js";
import { handleLongItem } from "./parser-long.js";


// hid1_11.pdf 6.2.2 Report Descriptor
function parseDescriptor(bytes, opts) {
const DEFAULT_PARSE_OPTIONS = {
separator: ",",
prefix: "0x",
comment: "//",
};

export function parseDescriptor(bytes, opts = {}) {
const options = { ...DEFAULT_PARSE_OPTIONS, ...opts };
let output = "";
let indent = 0;
let usagePage = null;
Expand All @@ -15,48 +21,64 @@ function parseDescriptor(bytes, opts) {
try {
// Long item
if (b == 0xFE) {
result = handleLongItem(bytes, i, opts);
result = handleLongItem(bytes, i, options);
} else {
result = handleShortItem(bytes, i, opts, indent, usagePage);
result = handleShortItem(bytes, i, options, indent, usagePage);
}
} catch (error) {
console.error("Error processing byte:", b, error);
output += `${opts.comment} ERROR: ${error.message}\n`;
output += `${options.comment} ERROR: ${error.message}\n`;
break;
}

if (result.usagePage) {
usagePage = result.usagePage;
}

const spaces = " ".repeat(indent * 2);
const pad = " ".repeat(Math.max(0, maxLineLength - result.text.length));
output += `${result.text}${pad}${opts.comment} ${spaces}${result.comment}\n`;
output += `${result.text}${pad}${options.comment} ${spaces}${result.comment}\n`;
indent += result.indentChange;
i += result.advance;
}

return output;
}

function stripComments(input) {
export function stripComments(input) {
input = input.replace(/\/\*[\s\S]*?\*\//g, "");
input = input.replace(/\/\/.*$/gm, "");
input = input.replace(/#.*$/gm, "");
return input;
}

document.getElementById("parseBtn").addEventListener("click", () => {
let input = document.getElementById("descriptorInput").value.trim();
input = stripComments(input);
if (typeof document !== "undefined") {
const parseBtn = document.getElementById("parseBtn");
if (parseBtn) {
parseBtn.addEventListener("click", () => {
const inputField = document.getElementById("descriptorInput");
if (!inputField) {
return;
}

let input = inputField.value.trim();
input = stripComments(input);

const bytes = input.split(/[\s,]+/).map(b => parseInt(b, 16)).filter(n => !isNaN(n));
const opts = {
separator: document.getElementById("separator").value || ",",
prefix: document.getElementById("prefix").value || "0x",
comment: document.getElementById("commentSymbol").value || "//",
};
const bytes = input
.split(/[\s,]+/)
.map(b => parseInt(b, 16))
.filter(n => !isNaN(n));
const opts = {
separator: (document.getElementById("separator")?.value) || ",",
prefix: (document.getElementById("prefix")?.value) || "0x",
comment: (document.getElementById("commentSymbol")?.value) || "//",
};

const parsed = parseDescriptor(bytes, opts);
document.getElementById("output").textContent = parsed;
});
const parsed = parseDescriptor(bytes, opts);
const outputEl = document.getElementById("output");
if (outputEl) {
outputEl.textContent = parsed;
}
});
}
}
45 changes: 34 additions & 11 deletions utils.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
const DEFAULT_OPTIONS = {
prefix: "0x",
separator: " ",
};

function normalizeOptions(opts = {}) {
return {
...DEFAULT_OPTIONS,
...opts,
};
}

export function toHex(val, opts) {
const prefix = opts.prefix || "0x";
const { prefix } = normalizeOptions(opts);
return prefix + (val >>> 0).toString(16).padStart(2, "0").toUpperCase();
}

export function joinHex(tag, data, opts, nBytes) {
const sep = opts.separator || " ";
export function joinHex(tag, data, opts, nBytes = 0) {
const options = normalizeOptions(opts);
const result = [];

if(tag !== null)
result.push(toHex(tag & 0xFF, opts));

for (let i = 0; i < nBytes; i++) {
const byte = (data >>> (i * 8)) & 0xFF;
result.push(toHex(byte, opts));

if (Array.isArray(tag)) {
for (const value of tag) {
result.push(toHex(value & 0xFF, options));
}
} else if (tag !== null && tag !== undefined) {
result.push(toHex(tag & 0xFF, options));
}

if (Array.isArray(data)) {
for (const value of data) {
result.push(toHex(value & 0xFF, options));
}
} else if (nBytes && data != null) {
for (let i = 0; i < nBytes; i++) {
const byte = (data >>> (i * 8)) & 0xFF;
result.push(toHex(byte, options));
}
}

return result.join(sep) + sep;
return result.join(options.separator) + options.separator;
}

export function readIntLE(bytes, offset, n, signed = true) {
Expand Down