All files / ip-input address-utils.ts

90.62% Statements 87/96
77.46% Branches 55/71
100% Functions 8/8
90% Lines 81/90

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                                                      937x 2785x   1x               1075x 138x   937x 937x 937x 937x 708x 708x   708x 690x     18x 4x     14x     14x 14x     14x       14x       229x   222x 218x 4x         933x           1x     142x 142x     142x 142x 578x         1x     328x 328x 328x 328x       328x 2785x 2785x 1726x 1059x 1037x   184x   853x   22x 14x   2785x             328x 2113x 2113x 5x 5x 5x 30x 30x 30x 26x 26x       30x     5x 5x             328x 2113x 328x 328x 328x 328x     328x 2x         328x 96x 96x 3x   96x 96x 3x 3x 1x         328x   2112x   328x    
/**
 * Copyright (c) Siemens 2016 - 2026
 * SPDX-License-Identifier: MIT
 */
interface Section {
  value: string;
  current?: boolean;
  partNo?: number;
  /** 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');
 
const recursiveSplitIpV4 = (
  options: Ip4SplitOptions,
  input: string,
  sections: Section[] = [{ value: '', partNo: 0 }],
  index: number = 0,
  cursorDelta: number = 0
): { sections: Section[]; cursorDelta: number } => {
  // Base case: no more input to process
  if (input.length === 0) {
    return { sections, cursorDelta };
  }
  const current = sections.at(-1)!;
  const char = input[0];
  input = input.substring(1);
  if (isDigit(char)) {
    const part = `${current.value}${char}`;
    const limit = current.mask ? 32 : 255;
    // Append digits to current part until the part limit exceeds
    if (part.length <= 3 && parseInt(part, 10) <= limit) {
      current.value = part;
    } else {
      // Base case: IP completely entered
      if ((options.cidr && current.mask) || (!options.cidr && current.partNo === 3)) {
        return { sections, cursorDelta };
      }
      // Force separators since the part exceeded his limit
      Iif (current.partNo === 3) {
        input = `/${char}${input.replace('.', '').replace('/', '')}`;
      } else {
        const dotIndex = input.indexOf('.');
        Iif (dotIndex >= 0) {
          input = input.substring(0, dotIndex) + input.substring(dotIndex + 1);
        }
        input = `.${char}${input}`;
      }
      // In case the cursor position is at the the position of the exceeded digit it is necessary
      // to move the cursor one position forward since a separator will be added before the digit
      Iif (index === options.pos) {
        cursorDelta = 1;
      }
    }
  } else if (char === '.' || char === '/') {
    // Handle separators
    if ('partNo' in current && current.partNo! < 3) {
      sections.push({ value: '.' }, { value: '', partNo: current.partNo! + 1 });
    } else Iif (options.cidr && !current.mask) {
      sections.push({ value: '/' }, { value: '', mask: true });
    }
  }
 
  return recursiveSplitIpV4(options, input, sections, index + 1, cursorDelta);
};
 
/**
 * Parse IPv4 input string into IPv4 address section array.
 */
export const splitIpV4Sections = (
  options: Ip4SplitOptions
): { value: string; cursorDelta: number } => {
  const { input } = options;
  Iif (!input) {
    return { value: '', cursorDelta: 0 };
  }
  const { sections, cursorDelta } = recursiveSplitIpV4(options, input);
  return {
    value: sections.map(s => s.value).join(''),
    cursorDelta
  };
};
 
export const splitIpV6Sections = (
  options: Ip6SplitOptions
): { value: string; cursorDelta: number } => {
  const { type, input, pos, zeroCompression, cidr } = options;
  const sections: Section[] = [{ value: '' }];
  let cursorDelta = 0;
  Iif (!input) {
    return { value: '', cursorDelta: 0 };
  }
 
  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 });
    }
    Iif (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[] = [];
      let charsProcessed = 0;
      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: ':' });
          Iif (current && pos >= charsProcessed) {
            cursorDelta++;
          }
        }
        charsProcessed += 4;
      }
 
      sections.splice(i, 1, ...append);
      Iif (current) {
        sections[i + append.length - 1].current = true;
      }
    }
  }
 
  // Drop invalid zero compression indicators '::'
  const removeEnd = pos === input.length || 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);
      }
    }
  }
 
  const value = sections
    .splice(0, cidr ? 17 : 15)
    .map(s => s.value)
    .join('');
  return { value, cursorDelta };
};