Index: work/remote/web/protocol/.jshintrc =================================================================== --- work/remote/web/protocol/.jshintrc (nonexistent) +++ work/remote/web/protocol/.jshintrc (revision 4719) @@ -0,0 +1,14 @@ +{ + "curly": true, + "eqeqeq": true, + "eqnull": true, + "esversion": 6, + "funcscope": true, + "futurehostile": true, + "node": true, + "notypeof": true, + "shadow": true, + "singleGroups": true, + "strict": true, + "undef": true +} Index: work/remote/web/protocol/README =================================================================== --- work/remote/web/protocol/README (revision 4718) +++ work/remote/web/protocol/README (revision 4719) @@ -1,3 +1,28 @@ -# protocol + +Running unit tests manually +--------------------------- +Before you can run the tests you need to install node.js v4.x.x or higher. +node v6.x.x is recommended. +The next step is the installation of package dependencies. +Go to the protocol directory and execute the following command: + +npm install + +This will download and install all normal and dev dependencies as they are +defined in package.json. +All dependencies are installed into the node_modules subdirectory. +After successful installation you can run unit tests any time by typing: + +npm test + +This will perform some checking on the source code and if everything is ok +test/test.js will be run. + + +Manual clean up +--------------- + +The node_modules subdirectory can be deleted manually. +No other artifects are created. Index: work/remote/web/protocol/package.json =================================================================== --- work/remote/web/protocol/package.json (revision 4718) +++ work/remote/web/protocol/package.json (revision 4719) @@ -5,7 +5,7 @@ "homepage": "http://repo.hu/projects/pcb-rnd/", "repository": { "type": "svn", - "url": "svn://repo.hu/pcb-rnd" + "url": "svn://repo.hu/pcb-rnd/work/remote/web/protocol" }, "license": "MIT", "author": { @@ -17,9 +17,9 @@ }, "devDependencies": { "jshint": "^2.9.3", - "tape": "^4.6.0", + "tape": "^4.6.0" }, "scripts": { - "test": "jshint protocol.js test/test.js && node ./test/test.js", + "test": "jshint protocol.js test/test.js && node ./test/test.js" } } Index: work/remote/web/protocol/protocol.js =================================================================== --- work/remote/web/protocol/protocol.js (revision 4718) +++ work/remote/web/protocol/protocol.js (revision 4719) @@ -20,7 +20,7 @@ /** * Constructs a HID protocol parser. */ -function ProtocolParser() { +function Parser() { this.state = MSG; } /** @@ -28,10 +28,9 @@ * * @param buffer - An array-like indexable object whose elements are octets. */ -ProtocolParser.prototype.onreceive = function (buffer) { +Parser.prototype.parse = function (buffer) { for (let i = 0; i < buffer.length; ++i) { let b = buffer[i]; - while (true) { switch (this.state) { case MSG: @@ -38,37 +37,50 @@ if (b === 9 || b === 10 || b === 13 || b === 32) { // empty message or leading space } else if (b === 35) { // '#' - this.state = STATE.COMMENT; + this.state = COMMENT; } else { - this.state = STATE.COMMAND; + this.state = COMMAND; this.command = ''; - break; // pass the same character to COMMAND + --i; // pass the same character to COMMAND } - continue; + break; case COMMAND: - if (this.command.length >= 16) { - this.onError("command too long"); - this.state = COMMENT; - break; // process the same character again so we won't miss EOL - } else if (b >= 65 && b <= 90 || b >= 97 && b <= 122 || b === 95 - || b === 43 || b === 45 || b === 46 || b === 35) { + if (b >= 65 && b <= 90 || b >= 97 && b <= 122 || + b >= 48 && b <= 57 || + b === 95 || b === 43 || b === 45 || b === 46 || b === 35) { this.command += String.fromCharCode(b); + if (this.command.length > 16) { + this.onError(i, "command too long"); + this.state = COMMENT; + --i; // process the same character again so we won't miss EOL + } } else { - this.STATE = ARGUMENT; - this.list = null; - this.args = null; + if (this.command.length > 0) { + this.state = ARGUMENT; + this.list = null; + this.args = null; + --i; + } else { + this.onError(i, "missing command"); + this.state = COMMENT; + --i; // process the same character again so we won't miss EOL + } } - continue; + break; case ARGUMENT: if (b === 9 || b === 32) { // skip whitespace + } else if (b === 10 || b === 13) { + this.onError(i, "missing or unfinished arguments"); + this.state = EOL; + --i; } else if (b === 40) { // '(' if (this.list && this.list.type === BINARY) { - this.onError("generic list inside binary list"); + this.onError(i, "generic list inside binary list"); this.state = COMMENT; - break; + --i; } else { let list = { elements: [], @@ -75,7 +87,9 @@ type: GENERIC, parent: this.list }; - this.list.elements.push(list); + if (this.list) { + this.list.elements.push(list); + } this.list = list; } } else if (b === 123) { // '{' @@ -84,70 +98,73 @@ type: BINARY, parent: this.list }; - this.list.elements.push(list); + if (this.list) { + this.list.elements.push(list); + } this.list = list; } else if (b === 41 || b === 125) { // ')' || '}' if (!this.list) { - this.onError("list terminator outside of list"); + this.onError(i, "list terminator outside of list"); this.state = COMMENT; - break; + --i; } else if (b === 41 && this.list.type === BINARY) { - this.onError("generic list terminator in binary list"); + this.onError(i, "generic list terminator in binary list"); this.state = COMMENT; - break; + --i; } else if (b === 125 && this.list.type === GENERIC) { - this.onError("binary list terminator in generic list"); + this.onError(i, "binary list terminator in generic list"); this.state = COMMENT; - break; + --i; } else { if (this.list.parent) { this.list = this.list.parent; } else { - this.args = compactArguments(this.list); + // the root list has been closed + this.args = Parser.compactTree(this.list); this.list = null; this.state = EOL; } } } else { // string - if (this.list.type === BINARY) { + if (!this.list) { + this.onError(i, "string outside of list"); + this.state = COMMENT; + --i; + } else if (this.list.type === BINARY) { this.state = BINARY_LENGTH; this.strLen = 0; this.strLenSize = 0; this.str = ''; - break; + --i; } else { this.state = TEXT_STRING; this.str = ''; - break; + --i; } } - continue; + break; case TEXT_STRING: - if (this.str.length >= 16) { - this.onError("text string too long"); - this.state = COMMENT; - break; - } else if (b >= 65 && b <= 90 || b >= 97 && b <= 122 || b === 95 - || b === 43 || b === 45 || b === 46 || b === 35) { + if (b >= 65 && b <= 90 || b >= 97 && b <= 122 || + b >= 48 && b <= 57 || + b === 95 || b === 43 || b === 45 || b === 46 || b === 35) { this.str += String.fromCharCode(b); - } else { - if (this.list) { - this.list.elements.push(this.str); - this.STATE = ARGUMENT; - } else { - this.onError("string outside of list"); + if (this.str.length > 16) { + this.onError(i, "text string too long"); this.state = COMMENT; - break; + --i; } + } else { + this.list.elements.push(this.str); + this.state = ARGUMENT; } - continue; + break; case BINARY_LENGTH: if (this.strLenSize >= 5) { - this.onError("binary string length is too long"); + this.onError(i, "binary string length is too long"); this.state = COMMENT; - break; + --i; } else if (b >= 65 && b <= 90) { // A-Z this.strLenSize++; let code = b - 65; @@ -169,58 +186,67 @@ } else if (b === 61) { // '=' this.state = BINARY_STRING; } else { - this.onError("invalid character in binary string length"); + this.onError(i, "invalid character in binary string length"); this.state = COMMENT; - break; + --i; } - continue; + break; case BINARY_STRING: if (this.strLen <= 0) { - if (this.list) { - this.list.elements.push(this.str); - this.STATE = ARGUMENT; - } else { - this.onError("string outside of list"); - this.state = COMMENT; - break; - } + this.list.elements.push(this.str); + this.state = ARGUMENT; } else { this.str += String.fromCharCode(b); this.strLen--; } - continue; + break; case COMMENT: if (b === 10 || b === 13) { this.state = MSG; } - continue; + break; case EOL: if (b === 9 || b === 32) { // skip whitespace } else if (b === 10 || b === 13) { - this.onCommand(this.args); + this.onCommand(this.command, this.args); this.state = MSG; } else { - this.onError("extra characters after argument list"); + this.onError(i, "extra characters after argument list"); this.state = COMMENT; - break; + --i; } - continue; + break; } // end switch - } // end while } // end for -} +}; /** * Override this to execute a command. */ -ProtocolParser.prototype.onCommand = function (command) { -} +Parser.prototype.onCommand = function (command, args) { +}; /** * Override this to respond to errors. */ -ProtocolParser.prototype.onError = function () { -} +Parser.prototype.onError = function () { +}; +/** + * Converts the argument tree to array of arrays by removing object levels. + */ +Parser.compactTree = function (tree) { + let list = tree.elements; + for (let i = 0; i < list.length; ++i) { + let elem = list[i]; + if (Array.isArray(elem)) { + list[i] = Parser.compactTree(elem); + } + } + return list; +}; + + +exports.Parser = Parser; Index: work/remote/web/protocol/test/test.js =================================================================== --- work/remote/web/protocol/test/test.js (nonexistent) +++ work/remote/web/protocol/test/test.js (revision 4719) @@ -0,0 +1,95 @@ +"use strict"; + + +const test = require("tape"); +const Parser = require("..").Parser; + + +test("cmd", (t) => { + t.deepEqual(parse("a()\n"), { cmd: "a", args: [] }); + t.deepEqual(parse("abc{}\n"), { cmd: "abc", args: [] }); + t.deepEqual(parse("azAZ09_+-.#()\n"), { cmd: "azAZ09_+-.#", args: [] }); + t.deepEqual(parse("abcdefghijklmnop{}\n"), { cmd: "abcdefghijklmnop", args: [] }); + + t.end(); +}); + + +test("badcmd", (t) => { + t.notOk(parse("()\n")); + t.notOk(parse(" ()\n")); + t.notOk(parse("abcdefghijklmnopq()\n")); + t.notOk(parse(":()\n")); + t.notOk(parse("ö()\n")); + t.notOk(parse("abc>()\n")); + + t.end(); +}); + + +test("badargs", (t) => { + t.notOk(parse("a\n")); + t.notOk(parse("a \n")); + t.notOk(parse("a(\n")); + t.notOk(parse("a(}\n")); + t.notOk(parse("a{)\n")); + t.notOk(parse("a(((((((((\n")); + t.notOk(parse("a{{{{{{{{{\n")); + + t.end(); +}); + + +test("whitespace", (t) => { + t.deepEqual(parse(" \t b \t { \t } \t \n"), { + cmd: "b", + args: [] + }); + + t.end(); +}); + + +test("comment", (t) => { + t.equal(parse("#a()\n"), "pending"); + t.equal(parse("# \t b \t { \t } \t \n"), "pending"); + t.equal(parse("#\n"), "pending"); + + t.end(); +}); + + +test("unfinished", (t) => { + t.equal(parse("a()"), "pending"); + t.equal(parse(" \t b \t { "), "pending"); + t.equal(parse("fdfds"), "pending"); + + t.end(); +}); + + +function parse(str) { + var cmd; + var args; + var err; + let buffer = Buffer.from(str); + let parser = new Parser(); + + parser.onCommand = function (_cmd, _args) { + cmd = _cmd; + args = _args; + }; + parser.onError = function (pos, _err) { + console.log(" msg[%d]: ", pos, _err); + err = _err; + }; + parser.parse(buffer); + + if (err) { + return false; + } else if (cmd) { + return { cmd: cmd, args: args }; + } else { + return "pending"; + } +}