"use strict";
const utils = require("../utils");
const { napiErrorHandler } = require("../new-utils");
const rust = require("../../index");
/** @module types */
/**
* Represents an IP address.
*/
class InetAddress {
/**
* Represents a Rust object created by napi-rs
* @type {rust.InetAddressWrapper}
* @private
*/
#internal;
#unwrappedConstructor(buffer) {
this.#internal = rust.InetAddressWrapper.new(buffer);
}
/**
* Creates an instance of InetAddress.
*
* @param {Buffer} buffer The buffer containing the IPv4 or IPv6 address.
* @throws {Error} If the buffer is not a valid IPv4 or IPv6 address.
*/
constructor(buffer) {
if (!buffer) {
throw new TypeError("Buffer cannot be null or undefined");
}
napiErrorHandler(this.#unwrappedConstructor.bind(this))(buffer);
}
/**
* Returns the length of the underlying buffer
* @readonly
* @type {Number}
*/
get length() {
return this.#internal.getLength();
}
set length(_) {
throw new SyntaxError("InetAddress length is read-only");
}
/**
* Returns the Ip version (4 or 6)
* @readonly
* @type {Number}
*/
get version() {
return this.#internal.getVersion();
}
set version(_) {
throw new SyntaxError("InetAddress version is read-only");
}
/**
* Immutable buffer that represents the IP address
* @readonly
* @type {Array}
*/
get buffer() {
return this.#internal.getBuffer();
}
set buffer(_) {
throw new SyntaxError("InetAddress buffer is read-only");
}
/**
* Converts a string representation of an IP address to an InetAddress instance.
*
* This function accepts both IPv4 and IPv6 addresses, which may include
* an embedded IPv4 address.
*
* @param {string} value
* @returns {InetAddress}
* @throws {TypeError} - If the input string is not a valid IPv4 or IPv6 address.
*/
static fromString(value) {
if (!value) {
return new InetAddress(utils.allocBufferFromArray([0, 0, 0, 0]));
}
// IPv4 pattern from https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
const ipv4Pattern = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
const ipv6Pattern = /^[\da-f:.]+$/i;
let parts;
if (ipv4Pattern.test(value)) {
parts = value.split(".");
return new InetAddress(utils.allocBufferFromArray(parts));
}
if (!ipv6Pattern.test(value)) {
throw new TypeError(
`Value could not be parsed as InetAddress: ${value}`,
);
}
parts = value.split(":");
if (parts.length < 3) {
throw new TypeError(
`Value could not be parsed as InetAddress: ${value}`,
);
}
const buffer = utils.allocBufferUnsafe(16);
let filling = 8 - parts.length + 1;
let applied = false;
let offset = 0;
const embeddedIp4 = ipv4Pattern.test(parts[parts.length - 1]);
if (embeddedIp4) {
// Its IPv6 address with an embedded IPv4 address:
// subtract 1 from the potential empty filling as ip4 contains 4 bytes instead of 2 of a ipv6 section
filling -= 1;
}
function writeItem(uIntValue) {
buffer.writeUInt8(+uIntValue, offset++);
}
for (let i = 0; i < parts.length; i++) {
const item = parts[i];
if (item) {
if (embeddedIp4 && i === parts.length - 1) {
item.split(".").forEach(writeItem);
break;
}
buffer.writeUInt16BE(parseInt(item, 16), offset);
offset = offset + 2;
continue;
}
// its an empty string
if (applied) {
// there could be 2 occurrences of empty string
filling = 1;
}
applied = true;
for (let j = 0; j < filling; j++) {
buffer[offset++] = 0;
buffer[offset++] = 0;
}
}
if (embeddedIp4 && !InetAddress.#isValidIPv4Mapped(buffer)) {
throw new TypeError(
"Only IPv4-Mapped IPv6 addresses are allowed as IPv6 address with embedded IPv4 address",
);
}
return new InetAddress(buffer);
}
/**
* Compares 2 addresses and returns true if the underlying bytes are the same
* @param {InetAddress} other
* @returns {Boolean}
*/
equals(other) {
if (!(other instanceof InetAddress)) {
return false;
}
return (
this.buffer.length === other.buffer.length &&
this.buffer.equals(other.buffer)
);
}
/**
* Returns the underlying buffer
* @returns {Buffer}
*/
getBuffer() {
return this.buffer;
}
/**
* Provide the name of the constructor and the string representation
* @returns {string}
*/
inspect() {
return `${this.constructor.name}: ${this.toString()}`;
}
/**
* Returns the string representation of the IP address.
*
* For v4 IP addresses, a string in the form of d.d.d.d is returned.
*
* For v6 IP addresses, a string in the form of x:x:x:x:x:x:x:x is returned, where the 'x's are the hexadecimal
* values of the eight 16-bit pieces of the address, according to rfc5952.
* In cases where there is more than one field of only zeros, it can be shortened. For example, 2001:0db8:0:0:0:1:0:1
* will be expressed as 2001:0db8::1:0:1.
*
* @param {String} [encoding]
* @returns {String}
*/
toString(encoding) {
if (encoding === "hex") {
// backward compatibility: behave in the same way as the buffer
return this.buffer.toString("hex");
}
if (this.buffer.length === 4) {
return this.buffer.join(".");
}
let start = -1;
const longest = { length: 0, start: -1 };
function checkLongest(i) {
if (start >= 0) {
// close the group
const length = i - start;
if (length > longest.length) {
longest.length = length;
longest.start = start;
start = -1;
}
}
}
// get the longest 16-bit group of zeros
for (let i = 0; i < this.buffer.length; i = i + 2) {
if (this.buffer[i] === 0 && this.buffer[i + 1] === 0) {
// its a group of zeros
if (start < 0) {
start = i;
}
// at the end of the buffer, make a final call to checkLongest.
if (i === this.buffer.length - 2) {
checkLongest(i + 2);
}
continue;
}
// its a group of non-zeros
checkLongest(i);
}
let address = "";
for (let h = 0; h < this.buffer.length; h = h + 2) {
if (h === longest.start) {
address += ":";
continue;
}
if (h < longest.start + longest.length && h > longest.start) {
// its a group of zeros
continue;
}
if (address.length > 0) {
address += ":";
}
address += ((this.buffer[h] << 8) | this.buffer[h + 1]).toString(
16,
);
}
if (address.charAt(address.length - 1) === ":") {
address += ":";
}
return address;
}
/**
* Returns the string representation.
* Method used by the native JSON.stringify() to serialize this instance.
* @returns {String}
*/
toJSON() {
return this.toString();
}
/**
* Validates for a IPv4-Mapped IPv6 according to https://tools.ietf.org/html/rfc4291#section-2.5.5
* @private
* @param {Buffer} buffer
*/
static #isValidIPv4Mapped(buffer) {
// check the form
// | 80 bits | 16 | 32 bits
// +----------------+----+-------------
// |0000........0000|FFFF| IPv4 address
for (let i = 0; i < buffer.length - 6; i++) {
if (buffer[i] !== 0) {
return false;
}
}
return !(buffer[10] !== 255 || buffer[11] !== 255);
}
/**
* Get duration from rust object. Not intended to be exposed in the API
* @package
* @param {rust.InetAddressWrapper} rustInetAddress
* @returns {InetAddress}
*/
static fromRust(rustInetAddress) {
return new InetAddress(rustInetAddress.getBuffer());
}
/**
* Get the rust object.
* @package
* @returns {rust.InetAddressWrapper}
*/
getInternal() {
return this.#internal;
}
}
module.exports = InetAddress;