tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

parser.js (7532B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 loader.lazyRequireGetter(
      8  this,
      9  ["WebConsoleCommandsManager"],
     10  "resource://devtools/server/actors/webconsole/commands/manager.js",
     11  true
     12 );
     13 
     14 const COMMAND = "command";
     15 const KEY = "key";
     16 const ARG = "arg";
     17 
     18 const COMMAND_PREFIX = /^:/;
     19 const KEY_PREFIX = /^--/;
     20 
     21 // default value for flags
     22 const DEFAULT_VALUE = true;
     23 const COMMAND_DEFAULT_FLAG = {
     24  block: "url",
     25  screenshot: "filename",
     26  unblock: "url",
     27 };
     28 
     29 /**
     30 * When given a string that begins with `:` and a unix style string,
     31 * returns the command name and the arguments.
     32 * Throws if the command doesn't exist.
     33 * This is intended to be used by the WebConsole actor only.
     34 *
     35 * @param String string
     36 *        A string to format that begins with `:`.
     37 *
     38 * @returns Object The command name and the arguments
     39 *                 { command: String, args: Object }
     40 */
     41 function getCommandAndArgs(string) {
     42  if (!isCommand(string)) {
     43    throw Error("getCommandAndArgs was called without `:`");
     44  }
     45  string = string.trim();
     46  if (string === ":") {
     47    throw Error("Missing a command name after ':'");
     48  }
     49  const tokens = string.split(/\s+/).map(createToken);
     50  return parseCommand(tokens);
     51 }
     52 
     53 /**
     54 * creates a token object depending on a string which as a prefix,
     55 * either `:` for a command or `--` for a key, or nothing for an argument
     56 *
     57 * @param String string
     58 *               A string to use as the basis for the token
     59 *
     60 * @returns Object Token Object, with the following shape
     61 *                { type: String, value: String }
     62 */
     63 function createToken(string) {
     64  if (isCommand(string)) {
     65    const value = string.replace(COMMAND_PREFIX, "");
     66    if (!value) {
     67      throw Error("Missing a command name after ':'");
     68    }
     69    if (!WebConsoleCommandsManager.getAllColonCommandNames().includes(value)) {
     70      throw Error(`'${value}' is not a valid command`);
     71    }
     72    return { type: COMMAND, value };
     73  }
     74  if (isKey(string)) {
     75    const value = string.replace(KEY_PREFIX, "");
     76    if (!value) {
     77      throw Error("invalid flag");
     78    }
     79    return { type: KEY, value };
     80  }
     81  return { type: ARG, value: string };
     82 }
     83 
     84 /**
     85 * returns a command Tree object for a set of tokens
     86 *
     87 * @param Array Tokens tokens
     88 *                     An array of Token objects
     89 *
     90 * @returns Object Tree Object, with the following shape
     91 *                 { command: String, args: Object }
     92 */
     93 function parseCommand(tokens) {
     94  let command = null;
     95  const args = {};
     96 
     97  for (let i = 0; i < tokens.length; i++) {
     98    const token = tokens[i];
     99    if (token.type === COMMAND) {
    100      if (command) {
    101        // we are throwing here because two commands have been passed and it is unclear
    102        // what the user's intention was
    103        throw Error(
    104          "Executing multiple commands in one evaluation is not supported"
    105        );
    106      }
    107      command = token.value;
    108    }
    109 
    110    if (token.type === KEY) {
    111      const nextTokenIndex = i + 1;
    112      const nextToken = tokens[nextTokenIndex];
    113      let values = args[token.value] || DEFAULT_VALUE;
    114      if (nextToken && nextToken.type === ARG) {
    115        const { value, offset } = collectString(
    116          nextToken,
    117          tokens,
    118          nextTokenIndex
    119        );
    120        // in order for JSON.stringify to correctly output values, they must be correctly
    121        // typed
    122        // As per the old GCLI documentation, we can only have one value associated with a
    123        // flag but multiple flags with the same name can exist and should be combined
    124        // into and array.  Here we are associating only the value on the right hand
    125        // side if it is of type `arg` as a single value; the second case initializes
    126        // an array, and the final case pushes a value to an existing array
    127        const typedValue = getTypedValue(value);
    128        if (values === DEFAULT_VALUE) {
    129          values = typedValue;
    130        } else if (!Array.isArray(values)) {
    131          values = [values, typedValue];
    132        } else {
    133          values.push(typedValue);
    134        }
    135        // skip the next token since we have already consumed it
    136        i = nextTokenIndex + offset;
    137      }
    138      args[token.value] = values;
    139    }
    140 
    141    // Since this has only been implemented for screenshot, we can only have one default
    142    // value. Eventually we may have more default values. For now, ignore multiple
    143    // unflagged args
    144    const defaultFlag = COMMAND_DEFAULT_FLAG[command];
    145    if (token.type === ARG && !args[defaultFlag]) {
    146      const { value, offset } = collectString(token, tokens, i);
    147      // Throw if the command isn't registered in COMMAND_DEFAULT_FLAG
    148      // as this command may not expect any argument without an explicit argument name like "-name arg"
    149      if (!defaultFlag) {
    150        throw new Error(
    151          `:${command} command doesn't support unnamed '${value}' argument.`
    152        );
    153      }
    154      args[defaultFlag] = getTypedValue(value);
    155      i = i + offset;
    156    }
    157  }
    158  return { command, args };
    159 }
    160 
    161 const stringChars = ['"', "'", "`"];
    162 function isStringChar(testChar) {
    163  return stringChars.includes(testChar);
    164 }
    165 
    166 function checkLastChar(string, testChar) {
    167  const lastChar = string[string.length - 1];
    168  return lastChar === testChar;
    169 }
    170 
    171 function hasUnescapedChar(value, char, rightOffset, leftOffset) {
    172  const lastPos = value.length - 1;
    173  const string = value.slice(rightOffset, lastPos - leftOffset);
    174  const index = string.indexOf(char);
    175  if (index === -1) {
    176    return false;
    177  }
    178  const prevChar = index > 0 ? string[index - 1] : null;
    179  // return false if the unexpected character is escaped, true if it is not
    180  return prevChar !== "\\";
    181 }
    182 
    183 function collectString(token, tokens, index) {
    184  const firstChar = token.value[0];
    185  const isString = isStringChar(firstChar);
    186  const UNESCAPED_CHAR_ERROR = segment =>
    187    `String has unescaped \`${firstChar}\` in [${segment}...],` +
    188    " may miss a space between arguments";
    189  let value = token.value;
    190 
    191  // the test value is not a string, or it is a string but a complete one
    192  // i.e. `"test"`, as opposed to `"foo`. In either case, this we can return early
    193  if (!isString || checkLastChar(value, firstChar)) {
    194    return { value, offset: 0 };
    195  }
    196 
    197  if (hasUnescapedChar(value, firstChar, 1, 0)) {
    198    throw Error(UNESCAPED_CHAR_ERROR(value));
    199  }
    200 
    201  let offset = null;
    202  for (let i = index + 1; i <= tokens.length; i++) {
    203    if (i === tokens.length) {
    204      throw Error("String does not terminate");
    205    }
    206 
    207    const nextToken = tokens[i];
    208    if (nextToken.type !== ARG) {
    209      throw Error(`String does not terminate before flag "${nextToken.value}"`);
    210    }
    211 
    212    value = `${value} ${nextToken.value}`;
    213 
    214    if (hasUnescapedChar(nextToken.value, firstChar, 0, 1)) {
    215      throw Error(UNESCAPED_CHAR_ERROR(value));
    216    }
    217 
    218    if (checkLastChar(nextToken.value, firstChar)) {
    219      offset = i - index;
    220      break;
    221    }
    222  }
    223  return { value, offset };
    224 }
    225 
    226 function isCommand(string) {
    227  return COMMAND_PREFIX.test(string);
    228 }
    229 
    230 function isKey(string) {
    231  return KEY_PREFIX.test(string);
    232 }
    233 
    234 function getTypedValue(value) {
    235  if (!isNaN(value)) {
    236    return Number(value);
    237  }
    238  if (value === "true" || value === "false") {
    239    return Boolean(value);
    240  }
    241  if (isStringChar(value[0])) {
    242    return value.slice(1, value.length - 1);
    243  }
    244  return value;
    245 }
    246 
    247 exports.getCommandAndArgs = getCommandAndArgs;
    248 exports.isCommand = isCommand;