/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

const distringuish = require('@contrast/distringuish');
const logger = require('../core/logger')('contrast:hooks');

// some functions in the standard library allow users to specify a string
// encoding for their strings to be interpreted as. By default, all strings
// in v8 are created using UTF8; however, once Distringuish is added to the
// mix, strings that would previously be interpreted as UTF8 are now encoded
// using UTF16. As such, we need to do a series of patches over standard
// library functions.
// It should be noted that not all functions handle encoding in the same way.
// Some functions when asked for a specific encoding will perform encoding
// operations to make sure its in the right encoding, other's will not.
module.exports = function() {
  hardPatchEncoding(Buffer.prototype, 'Buffer', 'indexOf', 0);
  hardPatchEncoding(Buffer.prototype, 'Buffer', 'lastIndexOf', 0);
  hardPatchEncoding(Buffer.prototype, 'Buffer', 'includes', 0);

  hardPatchEncoding(Buffer, 'Buffer', 'alloc', 0);
  hardPatchEncoding(Buffer.prototype, 'Buffer', 'fill', 0);
};

/**
 * Force JavaScript to create another copy of a given string
 * @param {String} str   string you want to copy
 */
function copy(str) {
  return ` ${str}`.slice(1);
}

/**
 * Patch a function that takes an encoding argument for externalized strings.
 * This is necessary because of the `binary' / `latin1' encodings which are
 * UTF-16 incompatible (unlike UTF-8, which is identical up to 0xFF).
 * This does a forced string copy to ensure it's in the default string encoding.
 *
 * @param {Object}    obj              Object prototype that is being patched.
 * @param {String}    objName          module name (using a param to be less hacky)
 * @param {String}    methodName       the function to patch
 * @param {Number}    strIndex         The index of the string argument thats being patched.
 *                                     Since there are multiple of these locations
 *                                     possible, provide a list of all of them.
 */
function hardPatchEncoding(obj, objName, methodName, strIndex) {
  // in the event that the Node API changes in some deprecating way that we don't
  // catch, we just wont apply these patches. Given these encodings are still for
  // edge-cases
  if (!obj) {
    logger.error(
      `Failed to load encoding hook for ${objName}:${methodName}:${strIndex}`
    );
    return;
  }
  const origFn = obj[methodName];
  // export the original function for testing.
  module.exports[`${objName}.${methodName}`] = origFn;
  obj[methodName] = function(...args) {
    try {
      // check each possible index (encoding can be a number of arguments and)
      // it depends on the function.
      // force to desired encoding by decoding to a buffer and then re-encoding.
      if (typeof args[strIndex] === 'string' && distringuish.isExternal(args[strIndex])) {
        args[strIndex] = copy(args[strIndex]);
      }
    } catch (err) {
      logger.info(`Encoding patch for ${objName}.${methodName} failed. ${err}`);
    }
    return origFn.apply(this, args);
  };
}

module.exports.copy = copy;
module.exports.hardPatchEncoding = hardPatchEncoding;
