sort IPv6 subnets and addresses numerically instead of randomly
This commit is contained in:
3 files changed
+147
-6
No files matched your search
+66
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { ipToNumber } from '../src/lib/utils/ip';
|
||||
import { ipToNumber, ipv6ToBigInt, compareIps } from '../src/lib/utils/ip';
|
||||
|
||||
describe('ipToNumber', () => {
|
||||
test('converts standard IPv4 addresses', () => {
|
||||
@@ -31,3 +31,68 @@ describe('ipToNumber', () => {
|
||||
expect(sorted).toEqual(['10.0.0.1', '172.16.0.1', '192.168.1.2', '192.168.1.10']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipv6ToBigInt', () => {
|
||||
test('parses full and compressed forms to the same value', () => {
|
||||
const full = ipv6ToBigInt('2001:0db8:0000:0000:0000:0000:0000:0001');
|
||||
expect(ipv6ToBigInt('2001:db8::1')).toBe(full);
|
||||
expect(ipv6ToBigInt('2001:db8::1/64')).toBe(full); // CIDR stripped
|
||||
});
|
||||
|
||||
test('handles :: at the start and end', () => {
|
||||
expect(ipv6ToBigInt('::1')).toBe(1n);
|
||||
expect(ipv6ToBigInt('::')).toBe(0n);
|
||||
expect(ipv6ToBigInt('fe80::')).toBe(ipv6ToBigInt('fe80:0:0:0:0:0:0:0'));
|
||||
});
|
||||
|
||||
test('handles an IPv4 tail (IPv4-mapped)', () => {
|
||||
expect(ipv6ToBigInt('::ffff:192.168.0.1')).toBe(ipv6ToBigInt('::ffff:c0a8:1'));
|
||||
});
|
||||
|
||||
test('orders IPv6 numerically, not lexically', () => {
|
||||
// The #1453 case: the last hextet differs; string sort mis-orders 0x9 vs 0x10.
|
||||
expect(ipv6ToBigInt('2001:db8::9')! < ipv6ToBigInt('2001:db8::10')!).toBe(true);
|
||||
expect(ipv6ToBigInt('2001:db8::2')! < ipv6ToBigInt('2001:db8::a')!).toBe(true);
|
||||
});
|
||||
|
||||
test('rejects invalid input', () => {
|
||||
expect(ipv6ToBigInt('192.168.0.1')).toBeNull(); // no colon
|
||||
expect(ipv6ToBigInt('2001::db8::1')).toBeNull(); // two '::'
|
||||
expect(ipv6ToBigInt('gggg::1')).toBeNull(); // non-hex
|
||||
expect(ipv6ToBigInt('1:2:3:4:5:6:7:8:9')).toBeNull(); // too many groups
|
||||
expect(ipv6ToBigInt('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareIps (#1453 mixed v4/v6 ordering)', () => {
|
||||
test('orders IPv6 subnets numerically (the reported bug)', () => {
|
||||
const subnets = ['2001:db8::10/64', '2001:db8::2/64', '2001:db8::a/64', '2001:db8::1/64'];
|
||||
const sorted = [...subnets].sort(compareIps);
|
||||
expect(sorted).toEqual([
|
||||
'2001:db8::1/64',
|
||||
'2001:db8::2/64',
|
||||
'2001:db8::a/64',
|
||||
'2001:db8::10/64'
|
||||
]);
|
||||
});
|
||||
|
||||
test('IPv4 sorts before IPv6, each family numerically', () => {
|
||||
const mixed = ['fd00::1', '10.0.0.5', 'fd00::2', '10.0.0.1', '2001:db8::1'];
|
||||
const sorted = [...mixed].sort(compareIps);
|
||||
expect(sorted).toEqual(['10.0.0.1', '10.0.0.5', '2001:db8::1', 'fd00::1', 'fd00::2']);
|
||||
});
|
||||
|
||||
test('empty / invalid values sort last', () => {
|
||||
const list = ['fd00::1', '-', '10.0.0.1', undefined, null, 'garbage'];
|
||||
const sorted = [...list].sort(compareIps);
|
||||
expect(sorted.slice(0, 2)).toEqual(['10.0.0.1', 'fd00::1']);
|
||||
// the three empty/invalid entries trail, order among them not significant
|
||||
expect(new Set(sorted.slice(2))).toEqual(new Set(['-', undefined, null, 'garbage']));
|
||||
});
|
||||
|
||||
test('is a stable transitive comparator (returns -1/0/1 semantics)', () => {
|
||||
expect(compareIps('10.0.0.1', '10.0.0.1')).toBe(0);
|
||||
expect(compareIps('10.0.0.1', '10.0.0.2')).toBeLessThan(0);
|
||||
expect(compareIps('2001:db8::2', '2001:db8::1')).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user