tor-browser

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

Function.ts (2650B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 const createdFunctions = new Map<string, (...args: unknown[]) => unknown>();
      7 
      8 /**
      9 * Creates a function from a string.
     10 *
     11 * @internal
     12 */
     13 export const createFunction = (
     14  functionValue: string,
     15 ): ((...args: unknown[]) => unknown) => {
     16  let fn = createdFunctions.get(functionValue);
     17  if (fn) {
     18    return fn;
     19  }
     20  fn = new Function(`return ${functionValue}`)() as (
     21    ...args: unknown[]
     22  ) => unknown;
     23  createdFunctions.set(functionValue, fn);
     24  return fn;
     25 };
     26 
     27 /**
     28 * @internal
     29 */
     30 export function stringifyFunction(fn: (...args: never) => unknown): string {
     31  let value = fn.toString();
     32  try {
     33    new Function(`(${value})`);
     34  } catch (err) {
     35    if (
     36      (err as Error).message.includes(
     37        `Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive`,
     38      )
     39    ) {
     40      // The content security policy does not allow Function eval. Let's
     41      // assume the value might be valid as is.
     42      return value;
     43    }
     44    // This means we might have a function shorthand (e.g. `test(){}`). Let's
     45    // try prefixing.
     46    let prefix = 'function ';
     47    if (value.startsWith('async ')) {
     48      prefix = `async ${prefix}`;
     49      value = value.substring('async '.length);
     50    }
     51    value = `${prefix}${value}`;
     52    try {
     53      new Function(`(${value})`);
     54    } catch {
     55      // We tried hard to serialize, but there's a weird beast here.
     56      throw new Error('Passed function cannot be serialized!');
     57    }
     58  }
     59  return value;
     60 }
     61 
     62 /**
     63 * Replaces `PLACEHOLDER`s with the given replacements.
     64 *
     65 * All replacements must be valid JS code.
     66 *
     67 * @example
     68 *
     69 * ```ts
     70 * interpolateFunction(() => PLACEHOLDER('test'), {test: 'void 0'});
     71 * // Equivalent to () => void 0
     72 * ```
     73 *
     74 * @internal
     75 */
     76 export const interpolateFunction = <T extends (...args: never[]) => unknown>(
     77  fn: T,
     78  replacements: Record<string, string>,
     79 ): T => {
     80  let value = stringifyFunction(fn);
     81  for (const [name, jsValue] of Object.entries(replacements)) {
     82    value = value.replace(
     83      new RegExp(`PLACEHOLDER\\(\\s*(?:'${name}'|"${name}")\\s*\\)`, 'g'),
     84      // Wrapping this ensures tersers that accidentally inline PLACEHOLDER calls
     85      // are still valid. Without, we may get calls like ()=>{...}() which is
     86      // not valid.
     87      `(${jsValue})`,
     88    );
     89  }
     90  return createFunction(value) as unknown as T;
     91 };
     92 
     93 declare global {
     94  /**
     95   * Used for interpolation with {@link interpolateFunction}.
     96   *
     97   * @internal
     98   */
     99  function PLACEHOLDER<T>(name: string): T;
    100 }