From cda0b085e6f34d0940815337ab57afd0981c5b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 09:07:30 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20CIDR=20=D0=B0=D0=B3=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20(aggregateCIDRs)=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20/export=20=D0=BF=D0=BE=20=D0=A2=D0=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validators.js | 129 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/src/validators.js b/src/validators.js index 357e31a..67fc8d4 100644 --- a/src/validators.js +++ b/src/validators.js @@ -107,4 +107,131 @@ function cidrToRange(cidr) { return { start, end }; } -module.exports = { validate, overlaps, BLOCKED_RANGES }; +/** + * Конвертировать беззнаковое 32-битное число в строку IPv4. + * Используется при обратном преобразовании диапазон → CIDR. + */ +function numToIP(n) { + return [ + (n >>> 24) & 0xff, + (n >>> 16) & 0xff, + (n >>> 8) & 0xff, + n & 0xff, + ].join('.'); +} + +/** + * Преобразовать числовой диапазон [start, end] (unsigned 32-bit) в + * минимальный набор CIDR-блоков. + * + * Алгоритм: для каждой текущей позиции ищем наибольший выровненный блок, + * начинающийся в cur и не выходящий за end. «Выровненный» — значит cur + * является адресом сети (host-биты = 0) для данного префикса. + * Кол-во trailing zeros в cur определяет максимально допустимый блок. + * + * @param {number} start — uint32 начало диапазона + * @param {number} end — uint32 конец диапазона (включительно) + * @returns {string[]} массив строк вида '1.2.3.4/24' + */ +function rangeToCIDRs(start, end) { + const cidrs = []; + let cur = start >>> 0; + const endU = end >>> 0; + + while (cur <= endU) { + // Считаем кол-во trailing zeros в cur — они дают максимальный + // выровненный блок, который может начинаться с этого адреса. + let trailingZeros = 0; + if (cur === 0) { + trailingZeros = 32; // 0.0.0.0 выровнен на любой блок + } else { + let tmp = cur; + while ((tmp & 1) === 0 && trailingZeros < 32) { + tmp = tmp >>> 1; + trailingZeros++; + } + } + + // Начинаем с максимально выровненного блока и сужаем, + // пока блок не поместится в [cur, endU]. + let prefix = 32 - trailingZeros; + while (prefix <= 32) { + const hostBits = 32 - prefix; + // blockEnd = cur + 2^hostBits - 1; для /32 blockEnd = cur + const blockEnd = hostBits > 0 ? (cur + Math.pow(2, hostBits) - 1) >>> 0 : cur; + if (blockEnd <= endU) break; + prefix++; + } + + cidrs.push(numToIP(cur) + '/' + prefix); + + // Шагаем к следующему блоку + const hostBits = 32 - prefix; + if (hostBits === 0) { + // /32 — шаг ровно один адрес + if (cur === 0xFFFFFFFF) break; // последний адрес IPv4 + cur = (cur + 1) >>> 0; + } else { + const blockSize = Math.pow(2, hostBits); + cur = (cur + blockSize) >>> 0; + if (cur === 0) break; // переполнение uint32 — весь диапазон пройден + } + } + + return cidrs; +} + +/** + * Агрегировать (суммаризировать) массив CIDR-строк в минимальный + * эквивалентный набор без пересечений. + * + * Шаги: + * 1. Конвертируем каждый CIDR в числовой диапазон [start, end]. + * 2. Сортируем диапазоны по start. + * 3. Объединяем пересекающиеся и смежные диапазоны — scan-line merge. + * 4. Каждый слитый диапазон → минимальный набор CIDR (rangeToCIDRs). + * + * Используется в /export: все компании суммаризируются совместно по ТЗ. + * + * @param {string[]} cidrs — массив строк вида '1.2.3.4/24' + * @returns {string[]} минимальный набор CIDR в порядке возрастания + */ +function aggregateCIDRs(cidrs) { + if (!cidrs || cidrs.length === 0) return []; + + // Шаг 1: CIDR → числовые диапазоны + const ranges = cidrs.map(cidrToRange); + + // Шаг 2: сортировка по start (числа ≤ 2^32, безопасно сравнивать как float) + ranges.sort((a, b) => { + if (a.start < b.start) return -1; + if (a.start > b.start) return 1; + return 0; + }); + + // Шаг 3: scan-line merge — объединяем перекрывающиеся и смежные диапазоны + const merged = [{ start: ranges[0].start, end: ranges[0].end }]; + for (let i = 1; i < ranges.length; i++) { + const last = merged[merged.length - 1]; + const cur = ranges[i]; + // «Смежные» означает cur.start === last.end + 1 (без зазора) + // Защита от переполнения: last.end === 0xFFFFFFFF — следующего нет + const adjacent = last.end < 0xFFFFFFFF && cur.start === last.end + 1; + if (cur.start <= last.end || adjacent) { + // Поглощаем cur в last (расширяем если cur заходит дальше) + if (cur.end > last.end) last.end = cur.end; + } else { + // Новый несмежный диапазон + merged.push({ start: cur.start, end: cur.end }); + } + } + + // Шаг 4: каждый слитый диапазон → минимальный набор CIDR + const result = []; + for (const range of merged) { + result.push(...rangeToCIDRs(range.start, range.end)); + } + return result; +} + +module.exports = { validate, overlaps, BLOCKED_RANGES, aggregateCIDRs };