packet.js

const { LinkLayerType } = require('#lib/enums');
const defaults = require('#lib/defaults');
const { layers, linktype } = require('#lib/layers/index');
const { shrinkAt, extendAt } = require('#lib/buffer');
const { TimeStamp } = require('#lib/timestamp');
const { LayersList } = require('#lib/layersList');


/**
 * Represents a network packet with protocol layer support
 * Provides fluent interface for packet crafting and parsing
 *
 * @class
 * @extends LayersList
 *
 * @param {Object} [data] - Packet data, options object, or existing packet
 * @param {Buffer} [data.buffer] - Raw packet buffer for parsing existing packets
 * @param {Object} [data.iface] - Network interface configuration
 *
 * @example
 *
 * // Create an ICMP echo request packet
 * const packet = new Packet({ iface: 'eth0' })
 *   .Ethernet({ dst: 'ff:ff:ff:ff:ff:ff', src: '00:11:22:33:44:55' })
 *   .IPv4({ src: '192.168.1.1', dst: '8.8.8.8', timeToLive: 64 })
 *   .ICMP({ type: 8, code: 0, id: 12345, sequence: 1 });
 *
 * @example
 * // Parse existing packet
 * const packet = new Packet({ buffer: rawBuffer });
 * console.log(packet.layers.IPv4.src);
 */
class Packet extends LayersList {
  constructor(data, opts = {}) {
    super({});
    this._toBuild = [];

    this.shrinkAt = this.shrinkAt.bind(this._buffer);
    this.extendAt = this.shrinkAt.bind(this._buffer);

    this._defaultOpts = {
      shrinkAt: this.shrinkAt,
      extendAt: this.shrinkAt,
    };

    if (data instanceof Packet) {
      this._buffer = Buffer.from(data.buffer);
      this._origLength = data._origLength;
      this._layersCount = data._layersCount;

      this.iface = { ...data.iface };
      this.linktype = data.linktype;

      if (opts.copy) {
        this.timestamp = TimeStamp.now();
      }
      else {
        this.timestamp = data.timestamp.clone();
      }

      this._toBuild = [...data._toBuild];

      data._eachLayer(l => {
        this._createLayer(layers[l.name], Buffer.from(l.buffer));
      });
    }
    else if (typeof data == 'object') {
      const { buffer = null, iface = { ...defaults }, timestamp = TimeStamp.now(), origLength = null } = data;

      this._buffer = buffer;
      this._origLength = origLength;
      this._layersCount = 0;

      this.iface = iface;
      this.linktype = this.iface?.linktype ?? LinkLayerType.LINKTYPE_ETHERNET;

      this.timestamp = timestamp;

      this._needsParse = this._buffer?.length > 0;
    }

    if (data?.comment) {
      this.comment = data.comment;
    }
  }

  /**
   * Compares this packet with another packet for equality
   *
   * @param {Packet} pkt - The packet to compare with
   * @returns {boolean} True if packets are identical (same interface, timestamp, and buffer content)
   *
   * @example
   * const packet1 = new Packet({ buffer: data });
   * const packet2 = new Packet({ buffer: data });
   * if (packet1.equals(packet2)) {
   *   console.log('Packets are identical');
   * }
   */
  equals(pkt) {
    if (!pkt instanceof Packet) {
      return false;
    }

    if (this.iface?.linktype !== pkt.iface?.linktype) {
      return false;
    }

    if (this.iface?.name !== pkt.iface?.name) {
      return false;
    }

    if (this.iface?.mtu !== pkt.iface?.mtu) {
      return false;
    }

    if (this.comment !== pkt.comment) {
      return false;
    }

    if (this.timestamp.compare(pkt.timestamp) !== 0) {
      return false;
    }

    if (Buffer.compare(this.buffer, pkt.buffer) !== 0) {
      return false;
    }

    return true;
  }

  get _needsBuild() {
    return this._toBuild.length > 0;
  }

  [Symbol.for('nodejs.util.inspect.custom')]() {
    return `<Packet iface=${this.iface.name ?? this.iface.linktype}` +
            (this._buffer ? ' | ' + this._buffer.length + ' bytes' : '') +
            (this.comment ? '| ' + this.comment : '') +
            '>';
  }

  _build() {
    if (!this._needsBuild) {
      return;
    }

    let toAlloc = 0;
    const allocArray = [];

    for (const { Layer, data } of this._toBuild) {
      const allocUnit = Layer.toAlloc(data);
      toAlloc += allocUnit;
      allocArray.push(allocUnit);
    }

    let newBuffer = Buffer.alloc(toAlloc);
    let curBuffer;

    if (Buffer.isBuffer(this._buffer)) {
      this._buffer = Buffer.concat([this._buffer, newBuffer]);
      curBuffer = this._buffer;

      this._eachLayer(l => {
        l.buffer = curBuffer;
        curBuffer = curBuffer.subarray(l.length);
      });
    }
    else {
      curBuffer = newBuffer;
      this._buffer = curBuffer;
    }

    const initCount = this._layersCount;

    this._genLayers((prev, i) => {
      const obj = this._toBuild[i];

      if (!obj) {
        return null;
      }

      const { Layer, data } = obj;

      const res = this._createLayer(Layer, curBuffer, { allocated: allocArray[i] });

      res.merge(data);

      curBuffer = curBuffer.subarray(res.length);

      return res;
    });

    this._eachLayer((l, i) => {
      if (i < initCount) return;
      l.defaults(this._toBuild[i - initCount].data);
    });

    this._eachLayer((l, i) => {
      if (i < initCount) return;
      if (typeof l.checksums == 'function') {
        l.checksums(this._toBuild[i - initCount]);
      }
    });

    this._toBuild = [];
    this._origLength = null;
  }

  get origLength() {
    return this._origLength ?? this.length;
  }

  get length() {
    return this.buffer.length;
  }

  _parse() {
    let buf = this._buffer;
    let prev = null;
    let Layer = linktype[this.linktype] ?? layers.Payload;

    let newLayer = new Layer(this._buffer, {
      shrinkAt: this.shrinkAt,
      extendAt: this.extendAt,
      prev,
    });

    this._layersHead = newLayer;

    while (newLayer !== null && buf.length > 0) {
      this._layersCount++;
      this._layers[newLayer.name] = newLayer;
      buf = buf.subarray(newLayer.length);
      prev = newLayer;

      newLayer = newLayer.nextProto(layers);
    }

    this._layersTail = newLayer;

    this._needsParse = false;
  }

  get layers() {
    if (this._needsParse) {
      this._parse();
    }
    if (this._needsBuild) {
      this._build();
    }
    return this._layers;
  }

  /**
   * Converts the packet to a plain JavaScript object
   *
   * @returns {Object} Plain object representation
   * @returns {Object} returns.iface - Interface configuration
   * @returns {Object} returns.layers - Layer objects with their fields
   *
   * @example
   * const obj = packet.toObject();
   * console.log(obj.layers.IPv4.src);
   */
  toObject() {
    this.layers;

    const layers = {};

    this._eachLayer(l => {
      layers[l.name] = l.toObject();
    });

    return {
      iface: { ...this.iface },
      layers,
    };
  }

  shrinkAt(...args) {
    throw new Error('Buffer shrinking not yet implemented, try creating new packet');
    //return shrinkAt(this.buffer, ...args);
  }

  extendAt(...args) {
    throw new Error('Buffer extending not yet implemented, try creating new packet');
    //return extendAt(this.buffer, ...args);
  }

  /**
   * Raw buffer containing the complete packet data
   * Automatically builds packet if layers were added
   *
   * @returns {Buffer} Complete packet buffer
   */
  get buffer() {
    if (this._needsBuild) {
      this._build();
    }
    return this._buffer;
  }

  /**
   * Creates a deep clone of the packet
   * Preserves all layers, timestamps, and interface settings
   *
   * @returns {Packet} New packet instance identical to this one
   */
  clone() {
    return new Packet(this);
  }

  copy() {
    return new Packet(this, { copy: true });
  }
}

for (const [name, Layer] of Object.entries(layers)) {
  Packet.prototype[name] = function(data) {
    this._toBuild.push({ Layer, data });
    this.needsBuild = true;
    return this;
  }
}

module.exports = { Packet };