layers_ICMPv6.js

const { compile } = require('struct-compile');
const { OsiModelLayers } = require('./osi');
const { ICMPv6Types } = require('./enums');
const child = require('./child');
const mixins = require('./mixins');

const { ICMPv6Header } = compile(`
  //@NE
  struct ICMPv6Header {
    uint8_t type;
    uint8_t code;
    uint16_t checksum;
  } __attribute__(packed);
`);

const { ICMPv6EchoHeader } = compile(`
  //@NE
  struct ICMPv6EchoHeader {
    uint16_t id;
    uint16_t sequence;
  } __attribute__(packed);
`);

const { ICMPv6DestUnreachableHeader } = compile(`
  //@NE
  struct ICMPv6DestUnreachableHeader {
    uint32_t unused;
  } __attribute__(packed);
`);

const { ICMPv6TimeExceededHeader } = compile(`
  //@NE
  struct ICMPv6TimeExceededHeader {
    uint32_t unused;
  } __attribute__(packed);
`);

const { ICMPv6ParamProblemHeader } = compile(`
  //@NE
  struct ICMPv6ParamProblemHeader {
    uint32_t pointer;
  } __attribute__(packed);
`);

const childProto = {};
const lookupChild = child.lookupChild(childProto);
const lookupKey = child.lookupKey(childProto);

/**
 * ICMPv6 protocol layer
 * @class
 * @implements {Layer}
 * @property {number} type - ICMPv6 message type (see ICMPv6Types)
 * @property {number} code - ICMPv6 message code
 * @property {number} checksum - ICMPv6 checksum
 */
class ICMPv6 extends ICMPv6Header {
  name = 'ICMPv6';
  static osi = OsiModelLayers.Network;
  osi = OsiModelLayers.Network;

  /**
   * @param {Buffer|Object} data - Input buffer or object with protocol fields
   * @param {Object} opts - Options for the layer
   */
  constructor(data = {}, opts = {}) {
    const isObj = typeof data == 'object' && !Buffer.isBuffer(data);
    if (isObj) {
      super(data, { toAlloc: ICMPv6.toAlloc(data) });
    }
    else {
      super(data);
    }

    mixins.ctor(this, data, opts);

    if (isObj) {
      this.merge(data);
    }
  }

  _customPayload() {
    return this._buf.subarray(ICMPv6Header.prototype.config.length);
  }

  merge(data) {
    super.merge(data);

    const tailBuffer = this._customPayload();

    if (this.isEchoRequest || this.isEchoReply) {
      const echo = new ICMPv6EchoHeader(tailBuffer);
      echo.merge(data);
    } else if (this.isDestinationUnreachable) {
      const unreachable = new ICMPv6DestUnreachableHeader(tailBuffer);
      unreachable.merge(data);
    } else if (this.isTimeExceeded) {
      const timeExceeded = new ICMPv6TimeExceededHeader(tailBuffer);
      timeExceeded.merge(data);
    } else if (this.isParameterProblem) {
      const paramProblem = new ICMPv6ParamProblemHeader(tailBuffer);
      paramProblem.merge(data);
    }
  }

  static _toAllocForType(type) {
    let customPayloadLength = 0;
    switch (type) {
      case ICMPv6Types.EchoRequest:
      case ICMPv6Types.EchoReply:
        customPayloadLength = ICMPv6EchoHeader.prototype.config.length;
        break;
      case ICMPv6Types.DestinationUnreachable:
        customPayloadLength = ICMPv6DestUnreachableHeader.prototype.config.length;
        break;
      case ICMPv6Types.TimeExceeded:
        customPayloadLength = ICMPv6TimeExceededHeader.prototype.config.length;
        break;
      case ICMPv6Types.ParameterProblem:
        customPayloadLength = ICMPv6ParamProblemHeader.prototype.config.length;
        break;
    }

    return customPayloadLength + ICMPv6Header.prototype.config.length;
  }

  /**
   * Get the required allocation size based on ICMPv6 message type
   * @param {Object} data - The data object containing type and other fields
   * @returns {number} Required allocation size
   */
  static toAlloc(data) {
    return ICMPv6._toAllocForType(data.type);
  }

  get length() {
    try {
      return ICMPv6._toAllocForType(this.type);
    } catch(err) {
      return super.length;
    }
  }

  /**
   * Get the name of the ICMPv6 message type
   * @returns {string} Name of the ICMPv6 message type
   */
  get typeName() {
    return Object.entries(ICMPv6Types).find(([_, value]) => value === this.type)?.[0] || 'Unknown';
  }

  /**
   * Check if this is an Echo Request message
   * @returns {boolean}
   */
  get isEchoRequest() {
    return this.type === ICMPv6Types.EchoRequest;
  }

  /**
   * Check if this is an Echo Reply message
   * @returns {boolean}
   */
  get isEchoReply() {
    return this.type === ICMPv6Types.EchoReply;
  }

  /**
   * Check if this is a Destination Unreachable message
   * @returns {boolean}
   */
  get isDestinationUnreachable() {
    return this.type === ICMPv6Types.DestinationUnreachable;
  }

  /**
   * Check if this is a Time Exceeded message
   * @returns {boolean}
   */
  get isTimeExceeded() {
    return this.type === ICMPv6Types.TimeExceeded;
  }

  /**
   * Check if this is a Parameter Problem message
   * @returns {boolean}
   */
  get isParameterProblem() {
    return this.type === ICMPv6Types.ParameterProblem;
  }

  toObject() {
    const base = {
      ...super.toObject(),
      typeName: this.typeName,
      isEchoRequest: this.isEchoRequest,
      isEchoReply: this.isEchoReply,
      isDestinationUnreachable: this.isDestinationUnreachable,
      isTimeExceeded: this.isTimeExceeded,
      isParameterProblem: this.isParameterProblem
    };

    const tailBuffer = this._customPayload();

    if (this.isEchoRequest || this.isEchoReply) {
      const echo = new ICMPv6EchoHeader(tailBuffer);
      return {
        ...base,
        id: echo.id,
        sequence: echo.sequence
      };
    }

    if (this.isDestinationUnreachable) {
      const unreachable = new ICMPv6DestUnreachableHeader(tailBuffer);
      return {
        ...base,
        unused: unreachable.unused
      };
    }

    if (this.isTimeExceeded) {
      const timeExceeded = new ICMPv6TimeExceededHeader(tailBuffer);
      return {
        ...base,
        unused: timeExceeded.unused
      };
    }

    if (this.isParameterProblem) {
      const paramProblem = new ICMPv6ParamProblemHeader(tailBuffer);
      return {
        ...base,
        pointer: paramProblem.pointer
      };
    }

    return base;
  }

  defaults(obj, layers) {
    if (!obj.type) {
      this.type = ICMPv6Types.EchoRequest;
    }
    if (!obj.code) {
      this.code = 0;
    }
  }

  nextProto(layers) {
    return lookupChild(layers, this.type, this);
  }
}

module.exports = { ICMPv6 };