All files / ip-input address-utils.ts

89.18% Statements 99/111
78.78% Branches 52/66
100% Functions 5/5
88.11% Lines 89/101

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197                                                      796x 2736x         1x 122x 122x 122x     122x 122x 796x 796x 609x 187x 176x 176x 11x     796x 122x         122x 122x 298x 298x     298x           122x 508x 508x 17x 17x 17x 57x 17x 17x   40x     17x 17x   17x 17x 17x             122x 511x 511x 3x         122x                           122x     1x 311x 311x 311x       311x 2736x 2736x 1668x 1068x 1048x   177x   871x   20x 10x   2736x 311x           311x 2122x 2122x 4x 4x 28x 28x 28x 25x       4x 4x 3x           311x 2122x 311x 311x 311x 311x     311x 2x         311x 80x 80x 3x   80x 80x 3x 3x 1x         311x    
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
/** */
export interface Section {
  value: string;
  current?: boolean;
  /** Indicate this is a network mask. */
  mask?: boolean;
}
 
export interface Ip4SplitOptions {
  type?: 'insert' | 'delete' | 'paste';
  input?: string | null;
  pos?: number;
  cidr?: boolean;
}
 
export interface Ip6SplitOptions {
  type?: 'insert' | 'delete' | 'paste';
  input?: string | null;
  pos?: number;
  zeroCompression?: boolean;
  cidr?: boolean;
}
 
const isDigit = (c: string): boolean => c >= '0' && c <= '9';
const isHex = (c: string): boolean => (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');
 
/**
 * Parse IPv4 input string into IPv4 address section array.
 */
export const splitIpV4Sections = (options: Ip4SplitOptions): Section[] => {
  const { input, pos, cidr } = options;
  const sections: Section[] = [{ value: '' }];
  Iif (!input) {
    return sections;
  }
  let maxDots = 3;
  for (let i = 0; i < input.length; i++) {
    const c = input.charAt(i);
    if (isDigit(c)) {
      sections.at(-1)!.value += c;
    } else if (c === '.' && maxDots > 0) {
      maxDots--;
      sections.push({ value: c }, { value: '' });
    } else Iif (cidr && c === '/') {
      sections.push({ value: c }, { value: '', mask: true });
    }
    if (pos === i) {
      sections.at(-1)!.current = true;
    }
  }
 
  // Trim empty sections for example the user entered ..
  let previousDivider = false;
  for (let i = 0; i < sections.length; i += 2) {
    const isDivider = sections.at(i)?.value === '' && sections.at(i + 1)?.value === '.';
    Iif (previousDivider && isDivider) {
      sections.splice(i, 2);
    }
    previousDivider = isDivider;
  }
 
  // Split values > 255 in multiple sections:
  // - 256 will be split into 25 and 6
  // - 255255255 will be split into 255, 255 and 255
  for (let i = 0; i < sections.length; i++) {
    const { value, current } = sections[i];
    if (value.length >= 3 && parseInt(value, 10) > 255) {
      const append: Section[] = [];
      let n = '';
      for (const c of value) {
        if (parseInt(n + c, 10) > 255) {
          append.push({ value: n }, { value: '.' });
          n = c;
        } else {
          n += c;
        }
      }
      if (n.length > 0) {
        append.push({ value: n });
      }
      sections.splice(i, 1, ...append);
      if (current) {
        sections[i + append.length - 1].current = true;
      }
    }
  }
 
  // Split leading zero sections:
  // Assume a string starting by 0 e.g. 012 will be split into 0 and 12
  for (let i = 0; i < sections.length; i++) {
    const sec = sections[i];
    if (sec.value.length > 1 && sec.value.startsWith('0')) {
      sections.splice(i, 1, { value: '0' }, { value: sec.value.substring(1) });
    }
  }
 
  // Ensure the that the CIDR divider is a slash
  Iif (cidr) {
    const startCidr = 7;
    Iif (startCidr < sections.length && sections[startCidr].value === '.') {
      sections[startCidr].value = '/';
    }
    const prefixPos = startCidr + 1;
    Iif (prefixPos < sections.length) {
      const prefixLength = sections[prefixPos].value;
      Iif (parseInt(prefixLength, 10) > 32) {
        sections[prefixPos].value = prefixLength.substring(0, 2);
      }
    }
  }
 
  return sections;
};
 
export const splitIpV6Sections = (options: Ip6SplitOptions): Section[] => {
  const { type, input, pos, zeroCompression, cidr } = options;
  const sections: Section[] = [{ value: '' }];
  Iif (!input) {
    return sections;
  }
 
  for (let i = 0; i < input.length; i++) {
    const c = input.charAt(i).toUpperCase();
    if (isHex(c)) {
      sections.at(-1)!.value += c;
    } else if (c === ':') {
      if (input.charAt(i - 1) === c) {
        // Merge :: characters
        sections.at(-2)!.value += c;
      } else {
        sections.push({ value: c }, { value: '' });
      }
    } else if (cidr && c === '/') {
      sections.push({ value: c }, { value: '', mask: true });
    }
    if (pos === i) {
      sections.at(-1)!.current = true;
    }
  }
 
  // Split values > FFFF in multiple sections:
  // - 1FFFF will be split into 1FFF and F
  for (let i = 0; i < sections.length; i++) {
    const { value, current } = sections[i];
    if (value.length > 4) {
      const append: Section[] = [];
      for (let p = 0; p < value.length; p += 4) {
        const part = value.substring(p, p + 4);
        append.push({ value: part });
        if (part.length === 4) {
          append.push({ value: ':' });
        }
      }
 
      sections.splice(i, 1, ...append);
      if (current) {
        sections[i + append.length - 1].current = true;
      }
    }
  }
 
  // Drop invalid zero compression indicators '::'
  const removeEnd = pos === input.length - 1 || type === 'paste';
  let matches = sections.filter(s => s.value.startsWith('::'));
  if (matches) {
    matches = removeEnd ? matches : matches.reverse();
    if (zeroCompression) {
      matches.shift();
    }
    // Only allow one occurrence of ::
    for (const drop of matches) {
      drop.value = drop.value.substring(1);
    }
  }
 
  // Ensure the that the CIDR divider is a slash
  if (cidr) {
    const startCidr = matches.length > 0 ? 13 : 15;
    if (startCidr < sections.length && sections[startCidr].value === ':') {
      sections[startCidr].value = '/';
    }
    const prefixPos = startCidr + 1;
    if (prefixPos < sections.length) {
      const prefixLength = sections[prefixPos].value;
      if (parseInt(prefixLength, 10) > 128) {
        sections[prefixPos].value = prefixLength.substring(0, 2);
      }
    }
  }
 
  return sections;
};