/**
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 logger = require('../../../core/logger')('contrast:v8:propagator');
const tracker = require('../../../tracker');
const patcher = require('../../../hooks/patcher');
const { PATCH_TYPES, PROXY_TARGET } = require('../../../constants');
const moduleHook = require('../../../hooks/require');
const tagRangeUtil = require('../../models/tag-range/util');
const { PropagationEvent, Signature, CallContext } = require('../../models');
const Debraner = require('../../membrane/debraner');

const TRACKED = Symbol('csi:v8-serialized');
const signature = new Signature('v8.deserialize.serialize');

// used for:
//  native-object => membraned-object
const nativeToTracked = new WeakMap();

module.exports.handle = function handle() {
  moduleHook.resolve({ name: 'v8' }, (v8) => {
    patcher.patch(v8, 'serialize', {
      name: 'v8.serialize',
      patchType: PATCH_TYPES.ASSESS_PROPAGATOR,

      pre(data) {
        const arg = data.args[0];
        if (arg && typeof arg === 'object' && !(arg instanceof String)) {
          const braned = arg[PROXY_TARGET];
          // undefined if not behind a proxy.
          if (braned) {
            const debraner = new Debraner(arg);
            // remove any membranes present in the input.
            const debraned = debraner.remove();
            data.args[0] = debraned;
            nativeToTracked.set(debraned, debraner);
          }
        }
      },

      post(data) {
        // if it was an object or a string that was serialized,
        // the deserialize 'post' hook will determine what to do
        // based on the type of TRACKED stored on the buffer.
        //
        // tracked in this case means either "behind a membrane"
        // or "a tracked string|String"
        const tracked = nativeToTracked.get(data.args[0]);
        if (tracked) {
          // it was behind a membrane
          data.result[TRACKED] = tracked;
        } else if (tracker.getData(data.args[0])) {
          // it was a tracked string
          data.result[TRACKED] = data.args[0];
        }
      }
    });

    patcher.patch(v8, 'deserialize', {
      name: 'v8.deserialize',
      patchType: PATCH_TYPES.ASSESS_PROPAGATOR,

      post(data) {
        const tracked = data.args[0][TRACKED];
        if (!tracked || !data.result) {
          return;
        }

        const sTracking = tracker.getData(tracked);
        // if the argument was a tracked string then the result should
        // have the same tags. i don't know that the length of a string
        // can change as a result of deserialize(serialize()) but best
        // to be safe.
        if (sTracking) {
          const resultTracking = tracker.track(data.result);
          if (!resultTracking) {
            // there's nothing to do if tracking failed on the result. it should
            // only do so on a zero-length string, but node works in mysterious ways.
            return;
          }
          const { str, props: rTracking } = resultTracking;
          tagRangeUtil.addAllInPlace(rTracking.tagRanges, sTracking.tagRanges);
          if (tracked.length !== data.result.length) {
            for (const tr of rTracking.tagRanges) {
              tr.start = 0;
              tr.stop = data.result.length - 1;
            }
          }
          const context = CallContext.create(data);
          const event = new PropagationEvent({
            context,
            signature,
            tagRanges: sTracking.tagRanges,
            source: 'P',
            target: 'R'
          });
          event.parents.push(sTracking.event);
          sTracking.event = event;

          data.result = str;
          return;
        }

        // NODE-1789
        // this is where it gets complicated. getter/setters are gone. this is
        // typically done on req.body before other processing (if i can say
        // typically because it's very rarely done), so there should be few
        // complications.

        // tracked should be a Debraner instance here. it wasn't a tracked string
        // and it's only set if it's a string or a Debraner instance.
        if (!(tracked instanceof Debraner)) {
          logger.debug('expected Debraner instance');
          return;
        }

        // in theory something could have been changed by the serialize/deserialize
        // sequence that causes restore() to fail, so catch errors.
        try {
          tracked.restore();
        } catch (e) {
          logger.debug(`debraner.restore failed ${e.message}`);
        }
      }
    });
  });
};

module.exports.TRACKED = TRACKED;
