diff --git a/lib/util/keyutil.js b/lib/util/keyutil.js new file mode 100644 index 00000000..e580cbe4 --- /dev/null +++ b/lib/util/keyutil.js @@ -0,0 +1,456 @@ +var util = require('util') + +var adb = require('adbkit') +var Promise = require('bluebird') + +module.exports.parseKeyCharacterMap = function(stream) { + var resolver = Promise.defer() + , state = 'type_t' + , keymap = { + keys: [] + } + , lastKey + , lastRule + , lastModifier + , lastBehavior + + function fail(char, state) { + throw new Error(util.format( + 'Unexpected character "%s" in state "%s"' + , char + , state + )) + } + + function parse(char) { + switch (state) { + case 'comment_before_type_t': + if (char === '\n') { + state = 'type_t' + break + } + return true + case 'type_t': + if (char === '\n') { + return true + } + if (char === '#') { + state = 'comment_before_type_t' + return true + } + if (char === 'k') { + state = 'key_k' + return parse(char) + } + if (char === 't') { + state = 'type_y' + return true + } + return fail(char, state) + case 'type_y': + if (char === 'y') { + state = 'type_p' + return true + } + return fail(char, state) + case 'type_p': + if (char === 'p') { + state = 'type_e' + return true + } + return fail(char, state) + case 'type_e': + if (char === 'e') { + state = 'type_name_start' + keymap.type = '' + return true + } + return fail(char, state) + case 'type_name_start': + if (char === ' ') { + return true + } + if (char >= 'A' && char <= 'Z') { + keymap.type += char + state = 'type_name_continued' + return true + } + return fail(char, state) + case 'type_name_continued': + if (char === '\n') { + // Could have more of these, although it doesn't make much sense + state = 'type_t' + return true + } + if (char >= 'A' && char <= 'Z') { + keymap.type += char + return true + } + return fail(char, state) + case 'comment_before_key_k': + if (char === '\n') { + state = 'key_k' + break + } + return true + case 'key_k': + if (char === '\n') { + return true + } + if (char === '#') { + state = 'comment_before_key_k' + return true + } + if (char === 'k') { + state = 'key_e' + return true + } + return fail(char, state) + case 'key_e': + if (char === 'e') { + state = 'key_y' + return true + } + return fail(char, state) + case 'key_y': + if (char === 'y') { + state = 'key_name_start' + return true + } + return fail(char, state) + case 'key_name_start': + if (char === ' ') { + return true + } + if ((char >= '0' && char <= '9') || + (char >= 'A' && char <= 'Z')) { + keymap.keys.push(lastKey = { + key: char + , rules: [] + }) + state = 'key_name_continued' + return true + } + return fail(char, state) + case 'key_name_continued': + if (char === ' ') { + state = 'key_start_block' + return true + } + if ((char >= '0' && char <= '9') || + (char >= 'A' && char <= 'Z') || + (char === '_')) { + lastKey.key += char + return true + } + return fail(char, state) + case 'key_start_block': + if (char === ' ') { + return true + } + if (char === '{') { + state = 'filter_name_start' + return true + } + return fail(char, state) + case 'filter_name_start': + if (char === '\n' || char === '\t' || char === ' ') { + return true + } + if (char === '}') { + state = 'key_k' + return true + } + if (char >= 'a' && char <= 'z') { + lastKey.rules.push(lastRule = { + modifiers: [lastModifier = { + type: char + }] + , behaviors: [] + }) + state = 'filter_name_continued' + return true + } + return fail(char, state) + case 'filter_name_continued': + if (char === ':') { + state = 'filter_behavior_start' + return true + } + if (char === ',') { + state = 'filter_name_or_start' + return true + } + if (char === '+') { + state = 'filter_name_and_start' + return true + } + if (char >= 'a' && char <= 'z') { + lastModifier.type += char + return true + } + return fail(char, state) + case 'filter_name_or_start': + if (char === ' ') { + return true + } + if (char >= 'a' && char <= 'z') { + lastKey.rules.push(lastRule = { + modifiers: [lastModifier = { + type: char + }] + , behaviors: lastRule.behaviors + }) + state = 'filter_name_continued' + return true + } + return fail(char, state) + case 'filter_name_and_start': + if (char === ' ') { + return true + } + if (char >= 'a' && char <= 'z') { + lastRule.modifiers.push(lastModifier = { + type: char + }) + state = 'filter_name_continued' + return true + } + return fail(char, state) + case 'filter_value': + if (char === '\\') { + state = 'filter_value_escape' + return true + } + if (char !== "'") { + lastRule.behaviors.push({ + type: 'literal' + , value: char + }) + state = 'filter_value_end' + return true + } + return fail(char, state) + case 'filter_value_escape': + if (char === '\\' || char === '\'' || char === '"') { + lastRule.behaviors.push({ + type: 'literal' + , value: char + }) + state = 'filter_value_end' + return true + } + if (char === 'n') { + lastRule.behaviors.push({ + type: 'literal' + , value: '\n' + }) + state = 'filter_value_end' + return true + } + if (char === 't') { + lastRule.behaviors.push({ + type: 'literal' + , value: '\t' + }) + state = 'filter_value_end' + return true + } + if (char === 'u') { + state = 'filter_value_unicode_1' + return true + } + return fail(char, state) + case 'filter_value_end': + if (char === '\'') { + state = 'filter_behavior_start' + return true + } + return fail(char, state) + case 'filter_behavior_start': + if (char === '\n') { + state = 'filter_name_start' + return true + } + if (char === ' ') { + return true + } + if (char === "'") { + state = 'filter_value' + return true + } + if (char === 'n') { + state = 'filter_behavior_none_2' + return true + } + if (char === 'f') { + state = 'filter_behavior_fallback_2' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_2': + if (char === 'a') { + state = 'filter_behavior_fallback_3' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_3': + if (char === 'l') { + state = 'filter_behavior_fallback_4' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_4': + if (char === 'l') { + state = 'filter_behavior_fallback_5' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_5': + if (char === 'b') { + state = 'filter_behavior_fallback_6' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_6': + if (char === 'a') { + state = 'filter_behavior_fallback_7' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_7': + if (char === 'c') { + state = 'filter_behavior_fallback_8' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_8': + if (char === 'k') { + state = 'filter_behavior_fallback_key_start' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_key_start': + if (char === ' ') { + return true + } + if ((char >= '0' && char <= '9') || + (char >= 'A' && char <= 'Z')) { + lastRule.behaviors.push(lastBehavior = { + type: 'fallback' + , key: char + }) + state = 'filter_behavior_fallback_key_continued' + return true + } + return fail(char, state) + case 'filter_behavior_fallback_key_continued': + if (char === ' ') { + state = 'filter_behavior_start' + return true + } + if (char === '\n') { + state = 'filter_name_start' + return true + } + if ((char >= '0' && char <= '9') || + (char >= 'A' && char <= 'Z') || + (char === '_')) { + lastBehavior.key += char + return true + } + return fail(char, state) + case 'filter_behavior_none_2': + if (char === 'o') { + state = 'filter_behavior_none_3' + return true + } + return fail(char, state) + case 'filter_behavior_none_3': + if (char === 'n') { + state = 'filter_behavior_none_4' + return true + } + return fail(char, state) + case 'filter_behavior_none_4': + if (char === 'e') { + lastRule.behaviors.push({ + type: 'none' + }) + state = 'filter_behavior_start' + return true + } + return fail(char, state) + case 'filter_value_unicode_1': + if ((char >= '0' && char <= '9') || + (char >= 'a' && char <= 'f')) { + lastRule.behaviors.push(lastBehavior = { + type: 'literal' + , value: parseInt(char, 16) << 12 + }) + state = 'filter_value_unicode_2' + return true + } + return fail(char, state) + case 'filter_value_unicode_2': + if ((char >= '0' && char <= '9') || + (char >= 'a' && char <= 'f')) { + lastBehavior.value += parseInt(char, 16) << 8 + state = 'filter_value_unicode_3' + return true + } + return fail(char, state) + case 'filter_value_unicode_3': + if ((char >= '0' && char <= '9') || + (char >= 'a' && char <= 'f')) { + lastBehavior.value += parseInt(char, 16) << 4 + state = 'filter_value_unicode_4' + return true + } + return fail(char, state) + case 'filter_value_unicode_4': + if ((char >= '0' && char <= '9') || + (char >= 'a' && char <= 'f')) { + lastBehavior.value += parseInt(char, 16) + state = 'filter_value_end' + return true + } + return fail(char, state) + default: + throw new Error(util.format('Unexpected state "%s"', state)) + } + } + + function errorListener(err) { + resolver.reject(err) + } + + function readableListener() { + var chunk = stream.read() + , i = 0 + , l = chunk.length + + try { + while (i < l) { + parse(String.fromCharCode(chunk[i++])) + } + } + catch (err) { + resolver.reject(err) + } + } + + function endListener() { + resolver.resolve(keymap) + } + + stream.on('error', errorListener) + stream.on('readable', readableListener) + stream.on('end', endListener) + + return resolver.promise.finally(function() { + stream.removeListener('error', errorListener) + stream.removeListener('readable', readableListener) + stream.removeListener('end', endListener) + }) +}