/**
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 tracker = require('../../../tracker');
const moduleHook = require('../../../hooks/require');
const patcher = require('../../../hooks/patcher');
const { PATCH_TYPES } = require('../../../constants');
const TagRange = require('../../models/tag-range');
const { createEvent } = require('../common');
const {
  SCOPE_NAMES: { NO_PROPAGATION }
} = require('../../../core/async-storage/scopes');

const provider = {
  method: 'path.relative',
  /**
   * Performs the dataflow propagation through the method call.
   * @param {Object} data the obj, args, and result
   * @param {Boolean} win32 whether we're on a Windows OS
   */
  propagate({ data, win32 }) {
    const { args, result } = data;
    const trackData = tracker.getData(args[1]);

    if (!trackData) {
      return;
    }

    const { tagRanges } = trackData;
    const shiftedRanges = [];
    const segments = SegmentIterator({ to: args[1], result, win32 });

    for (const { argRange, resultRange } of segments) {
      for (const tagRange of tagRanges) {
        if (tagRange.overlaps(argRange)) {
          const { start, stop } = adjustRange(argRange, resultRange, tagRange);
          shiftedRanges.unshift(new TagRange(start, stop, tagRange.tag));
        }
      }
    }

    if (shiftedRanges.length) {
      const event = createEvent(
        {
          method: provider.method,
          tagRanges: shiftedRanges,
          parents: [trackData.event]
        },
        data
      );
      const tracked = tracker.track(data.result);
      if (tracked) {
        data.result = tracked.str;
        tracked.props.tagRanges = shiftedRanges;
        tracked.props.event = event;
      }
    }
  },
  /**
   * Called to install propagator into the relevant methods.
   */
  handle() {
    moduleHook.resolve({ name: 'path' }, (path) => {
      for (const os of ['posix', 'win32']) {
        patcher.patch(path[os], 'relative', {
          name: `path.${os}.relative`,
          patchType: PATCH_TYPES.ASSESS_PROPAGATOR,
          scope: NO_PROPAGATION,
          post: (data) => {
            provider.propagate({ data, win32: os === 'win32' });
          }
        });
      }
    });
  }
};

/**
 * Given a TagRange and a segment's arg/result range info, will calculate and
 * return new start/stop values for the new tag range needing the adjustments.
 * @param {Object} argRange start/stop of segment w/in argument
 * @param {Object} resultRange start/stop of segment w/in result
 * @param {TagRange} tagRange TagRange instance needing adjusting
 * @returns {Object}
 */
function adjustRange(argRange, resultRange, tagRange) {
  let offset;
  offset = tagRange.start - argRange.start;
  const start = resultRange.start + Math.max(0, offset);

  offset = argRange.stop - tagRange.stop;
  const stop = resultRange.stop - Math.max(0, offset);

  return { start, stop };
}

/**
 * Provided with <code>path.resolve</code>'s <code>to</code> argument and its
 * result, will produce an iterable interface where each item in an iteration
 * will be an object with the next segment appearing in both the argument and
 * result (potentially needing tagRange adjustments), as well as the start and
 * stop indices of the segment in each.
 */
function* SegmentIterator({ to, result, win32 }) {
  const argSegments = to.split(win32 ? '\\' : '/');
  let argIdx = to.length;
  let argTruncated = to;
  let resultIdx = result.length;
  let resultTruncated = result;
  let segment = null;
  let ups = 0;

  for (let idx = argSegments.length - 1; idx >= 0; idx--) {
    segment = argSegments[idx];

    if (segment === '' || segment === '.') {
      continue;
    }
    if (segment === '..') {
      ups++;
      continue;
    }
    if (ups) {
      ups--;
      continue;
    }

    argIdx = argTruncated.lastIndexOf(segment);
    argTruncated = argTruncated.substring(0, argIdx - 1);
    resultIdx = resultTruncated.lastIndexOf(segment);
    resultTruncated = resultTruncated.substring(0, resultIdx);

    yield {
      segment,
      argRange: { start: argIdx, stop: argIdx + segment.length - 1 },
      resultRange: { start: resultIdx, stop: resultIdx + segment.length - 1 }
    };
  }
}

module.exports = provider;
