Pull request 734: + client: 1778 Add ip sort function, write unit tests

Close #1778

Squashed commit of the following:

commit ba63e147311799b17deaa97d7a12c2e0ec44a2ed
Merge: 143ba427 705a9d90
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 13 12:00:10 2020 +0300

    Merge branch 'master' into feature/1778

commit 143ba42707da3d7eece9f3e137a8b245f7f7888f
Merge: 483d2ff9 97df1989
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Thu Aug 13 11:16:46 2020 +0300

    Merge branch 'master' into feature/1778

commit 483d2ff9fa3de706d94a647701f1d26a3bcbb217
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 12 13:34:21 2020 +0300

    Always put ipv4 before ipv6 in sort, add invalid input unit tests

commit 26eb41b313785fe7ffaf98a7573cc5eef0dca14f
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Wed Aug 12 11:27:46 2020 +0300

    Rewrite tests: replace ipv4-mapped adresses

commit 4ecf287fd46945effe9ff11cfc9ae49217b9c796
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Aug 11 19:05:15 2020 +0300

    Minor fix

commit 3e5e2a6bb1f2f166619da54e5ade0904fe22a20d
Author: ArtemBaskal <a.baskal@adguard.com>
Date:   Tue Aug 11 19:01:26 2020 +0300

    + client: 1778 Add ip sort function, write unit tests
This commit is contained in:
Artem Baskal 2020-08-13 12:16:52 +03:00
parent 705a9d909d
commit 2f8e34e73b
6 changed files with 427 additions and 4 deletions

View File

@ -1,4 +1,4 @@
import { getIpMatchListStatus } from '../helpers/helpers';
import { getIpMatchListStatus, sortIp } from '../helpers/helpers';
import { IP_MATCH_LIST_STATUS } from '../helpers/constants';
describe('getIpMatchListStatus', () => {
@ -129,3 +129,356 @@ describe('getIpMatchListStatus', () => {
});
});
});
describe('sortIp', () => {
describe('ipv4', () => {
test('one octet differ', () => {
const arr = [
'127.0.2.0',
'127.0.3.0',
'127.0.1.0',
];
const sortedArr = [
'127.0.1.0',
'127.0.2.0',
'127.0.3.0',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('few octets differ', () => {
const arr = [
'192.168.11.10',
'192.168.10.0',
'192.168.11.11',
'192.168.10.10',
'192.168.1.10',
'192.168.0.1',
'192.168.1.0',
'192.168.1.1',
'192.168.11.0',
'192.168.0.10',
'192.168.10.11',
'192.168.0.11',
'192.168.1.11',
'192.168.0.0',
'192.168.10.1',
'192.168.11.1',
];
const sortedArr = [
'192.168.0.0',
'192.168.0.1',
'192.168.0.10',
'192.168.0.11',
'192.168.1.0',
'192.168.1.1',
'192.168.1.10',
'192.168.1.11',
'192.168.10.0',
'192.168.10.1',
'192.168.10.10',
'192.168.10.11',
'192.168.11.0',
'192.168.11.1',
'192.168.11.10',
'192.168.11.11',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
// Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599
const arr2 = [
'192.168.2.11',
'192.168.3.1',
'192.168.2.100',
'192.168.2.2',
'192.168.2.1',
'192.168.2.10',
'192.168.2.99',
'192.168.2.200',
'192.168.2.199',
];
const sortedArr2 = [
'192.168.2.1',
'192.168.2.2',
'192.168.2.10',
'192.168.2.11',
'192.168.2.99',
'192.168.2.100',
'192.168.2.199',
'192.168.2.200',
'192.168.3.1',
];
expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2);
});
});
describe('ipv6', () => {
test('only long form', () => {
const arr = [
'2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7:0:0:0:3',
'2001:db8:11a3:9d7:0:0:0:1',
];
const sortedArr = [
'2001:db8:11a3:9d7:0:0:0:1',
'2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7:0:0:0:3',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('only short form', () => {
const arr = [
'2001:db8::',
'2001:db7::',
'2001:db9::',
];
const sortedArr = [
'2001:db7::',
'2001:db8::',
'2001:db9::',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('long and short forms', () => {
const arr = [
'2001:db8::',
'2001:db7:11a3:9d7:0:0:0:2',
'2001:db6:11a3:9d7:0:0:0:1',
'2001:db6::',
'2001:db7:11a3:9d7:0:0:0:1',
'2001:db7::',
];
const sortedArr = [
'2001:db6::',
'2001:db6:11a3:9d7:0:0:0:1',
'2001:db7::',
'2001:db7:11a3:9d7:0:0:0:1',
'2001:db7:11a3:9d7:0:0:0:2',
'2001:db8::',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
});
describe('ipv4 and ipv6', () => {
test('ipv6 long form', () => {
const arr = [
'127.0.0.3',
'2001:db8:11a3:9d7:0:0:0:1',
'2001:db8:11a3:9d7:0:0:0:3',
'127.0.0.1',
'2001:db8:11a3:9d7:0:0:0:2',
'127.0.0.2',
];
const sortedArr = [
'127.0.0.1',
'127.0.0.2',
'127.0.0.3',
'2001:db8:11a3:9d7:0:0:0:1',
'2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7:0:0:0:3',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('ipv6 short form', () => {
const arr = [
'2001:db8:11a3:9d7::1',
'127.0.0.3',
'2001:db8:11a3:9d7::3',
'127.0.0.1',
'2001:db8:11a3:9d7::2',
'127.0.0.2',
];
const sortedArr = [
'127.0.0.1',
'127.0.0.2',
'127.0.0.3',
'2001:db8:11a3:9d7::1',
'2001:db8:11a3:9d7::2',
'2001:db8:11a3:9d7::3',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('ipv6 long and short forms', () => {
const arr = [
'2001:db8:11a3:9d7::1',
'127.0.0.3',
'2001:db8:11a3:9d7:0:0:0:2',
'127.0.0.1',
'2001:db8:11a3:9d7::3',
'127.0.0.2',
];
const sortedArr = [
'127.0.0.1',
'127.0.0.2',
'127.0.0.3',
'2001:db8:11a3:9d7::1',
'2001:db8:11a3:9d7:0:0:0:2',
'2001:db8:11a3:9d7::3',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('always put ipv4 before ipv6', () => {
const arr = [
'::1',
'0.0.0.2',
'127.0.0.1',
'::2',
'2001:db8:11a3:9d7:0:0:0:2',
'0.0.0.1',
'2001:db8:11a3:9d7::1',
];
const sortedArr = [
'0.0.0.1',
'0.0.0.2',
'127.0.0.1',
'::1',
'::2',
'2001:db8:11a3:9d7::1',
'2001:db8:11a3:9d7:0:0:0:2',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
});
describe('cidr', () => {
test('only ipv4 cidr', () => {
const arr = [
'192.168.0.1/9',
'192.168.0.1/7',
'192.168.0.1/8',
];
const sortedArr = [
'192.168.0.1/7',
'192.168.0.1/8',
'192.168.0.1/9',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('ipv4 and cidr ipv4', () => {
const arr = [
'192.168.0.1/9',
'192.168.0.1',
'192.168.0.1/32',
'192.168.0.1/7',
'192.168.0.1/8',
];
const sortedArr = [
'192.168.0.1/7',
'192.168.0.1/8',
'192.168.0.1/9',
'192.168.0.1/32',
'192.168.0.1',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('only ipv6 cidr', () => {
const arr = [
'2001:db8:11a3:9d7::1/32',
'2001:db8:11a3:9d7::1/64',
'2001:db8:11a3:9d7::1/128',
'2001:db8:11a3:9d7::1/24',
];
const sortedArr = [
'2001:db8:11a3:9d7::1/24',
'2001:db8:11a3:9d7::1/32',
'2001:db8:11a3:9d7::1/64',
'2001:db8:11a3:9d7::1/128',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
test('ipv6 and cidr ipv6', () => {
const arr = [
'2001:db8:11a3:9d7::1/32',
'2001:db8:11a3:9d7::1',
'2001:db8:11a3:9d7::1/64',
'2001:db8:11a3:9d7::1/128',
'2001:db8:11a3:9d7::1/24',
];
const sortedArr = [
'2001:db8:11a3:9d7::1/24',
'2001:db8:11a3:9d7::1/32',
'2001:db8:11a3:9d7::1/64',
'2001:db8:11a3:9d7::1/128',
'2001:db8:11a3:9d7::1',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
});
describe('invalid input', () => {
const originalError = console.error;
beforeEach(() => {
console.error = jest.fn();
});
afterEach(() => {
expect(console.error).toHaveBeenCalled();
console.error = originalError;
});
test('invalid strings', () => {
const arr = ['invalid ip', 'invalid cidr'];
expect(arr.sort(sortIp)).toStrictEqual(arr);
});
test('invalid ip', () => {
const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0'];
expect(arr.sort(sortIp)).toStrictEqual(arr);
});
test('invalid cidr', () => {
const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129'];
expect(arr.sort(sortIp)).toStrictEqual(arr);
});
test('valid and invalid ip', () => {
const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2'];
expect(arr.sort(sortIp)).toStrictEqual(arr);
});
});
describe('mixed', () => {
test('ipv4, ipv6 in short and long forms and cidr', () => {
const arr = [
'2001:db8:11a3:9d7:0:0:0:1/32',
'192.168.1.2',
'127.0.0.2',
'2001:db8:11a3:9d7::1/128',
'2001:db8:11a3:9d7:0:0:0:1',
'127.0.0.1/12',
'192.168.1.1',
'2001:db8::/32',
'2001:db8:11a3:9d7::1/24',
'192.168.1.2/12',
'2001:db7::/32',
'127.0.0.1',
'2001:db8:11a3:9d7:0:0:0:2',
'192.168.1.1/24',
'2001:db7::/64',
'2001:db7::',
'2001:db8::',
'2001:db8:11a3:9d7:0:0:0:1/128',
'192.168.1.1/12',
'127.0.0.1/32',
'::1',
];
const sortedArr = [
'127.0.0.1/12',
'127.0.0.1/32',
'127.0.0.1',
'127.0.0.2',
'192.168.1.1/12',
'192.168.1.1/24',
'192.168.1.1',
'192.168.1.2/12',
'192.168.1.2',
'::1',
'2001:db7::/32',
'2001:db7::/64',
'2001:db7::',
'2001:db8::/32',
'2001:db8::',
'2001:db8:11a3:9d7::1/24',
'2001:db8:11a3:9d7:0:0:0:1/32',
'2001:db8:11a3:9d7::1/128',
'2001:db8:11a3:9d7:0:0:0:1/128',
'2001:db8:11a3:9d7:0:0:0:1',
'2001:db8:11a3:9d7:0:0:0:2',
];
expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
});
});
});

View File

@ -6,7 +6,7 @@ import { Trans, withTranslation } from 'react-i18next';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import { getPercent, getIpMatchListStatus } from '../../helpers/helpers';
import { getPercent, getIpMatchListStatus, sortIp } from '../../helpers/helpers';
import { IP_MATCH_LIST_STATUS, STATUS_COLORS } from '../../helpers/constants';
import { formatClientCell } from '../../helpers/formatClientCell';
@ -99,7 +99,7 @@ const Clients = ({
{
Header: 'IP',
accessor: 'ip',
sortMethod: (a, b) => parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
sortMethod: sortIp,
Cell: clientCell(t, toggleClientStatus, processingAccessSet, disallowedClients),
},
{

View File

@ -8,6 +8,7 @@ import CellWrap from '../../ui/CellWrap';
import whoisCell from './whoisCell';
import LogsSearchLink from '../../ui/LogsSearchLink';
import { sortIp } from '../../../helpers/helpers';
const COLUMN_MIN_WIDTH = 200;
@ -18,6 +19,7 @@ class AutoClients extends Component {
accessor: 'ip',
minWidth: COLUMN_MIN_WIDTH,
Cell: CellWrap,
sortMethod: sortIp,
},
{
Header: this.props.t('table_name'),

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { Trans, withTranslation } from 'react-i18next';
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
import { sortIp } from '../../../helpers/helpers';
class Leases extends Component {
cellWrap = ({ value }) => (
@ -27,6 +28,7 @@ class Leases extends Component {
Header: 'IP',
accessor: 'ip',
Cell: this.cellWrap,
sortMethod: sortIp,
}, {
Header: <Trans>dhcp_table_hostname</Trans>,
accessor: 'hostname',

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { Trans, withTranslation } from 'react-i18next';
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../../helpers/constants';
import { sortIp } from '../../../../helpers/helpers';
import Modal from './Modal';
class StaticLeases extends Component {
@ -49,6 +49,7 @@ class StaticLeases extends Component {
{
Header: 'IP',
accessor: 'ip',
sortMethod: sortIp,
Cell: this.cellWrap,
},
{

View File

@ -681,3 +681,68 @@ export const processContent = (content) => (Array.isArray(content)
export const getObjectKeysSorted = (object, sortKey) => Object.entries(object)
.sort(([, { [sortKey]: order1 }], [, { [sortKey]: order2 }]) => order1 - order2)
.map(([key]) => key);
/**
* @param ip
* @returns {[IPv4|IPv6, 33|129]}
*/
const getParsedIpWithPrefixLength = (ip) => {
const MAX_PREFIX_LENGTH_V4 = 32;
const MAX_PREFIX_LENGTH_V6 = 128;
const parsedIp = ipaddr.parse(ip);
const prefixLength = parsedIp.kind() === 'ipv4' ? MAX_PREFIX_LENGTH_V4 : MAX_PREFIX_LENGTH_V6;
// Increment prefix length to always put IP after CIDR, e.g. 127.0.0.1/32, 127.0.0.1
return [parsedIp, prefixLength + 1];
};
/**
* Helper function for IP and CIDR comparison (supports both v4 and v6)
* @param item - ip or cidr
* @returns {number[]}
*/
const getAddressesComparisonBytes = (item) => {
// Sort ipv4 before ipv6
const IP_V4_COMPARISON_CODE = 0;
const IP_V6_COMPARISON_CODE = 1;
const [parsedIp, cidr] = ipaddr.isValid(item)
? getParsedIpWithPrefixLength(item)
: ipaddr.parseCIDR(item);
const [normalizedBytes, ipVersionComparisonCode] = parsedIp.kind() === 'ipv4'
? [parsedIp.toIPv4MappedAddress().parts, IP_V4_COMPARISON_CODE]
: [parsedIp.parts, IP_V6_COMPARISON_CODE];
return [ipVersionComparisonCode, ...normalizedBytes, cidr];
};
/**
* Compare function for IP and CIDR sort in ascending order (supports both v4 and v6)
* @param a
* @param b
* @returns {number} -1 | 0 | 1
*/
export const sortIp = (a, b) => {
try {
const comparisonBytesA = getAddressesComparisonBytes(a);
const comparisonBytesB = getAddressesComparisonBytes(b);
for (let i = 0; i < comparisonBytesA.length; i += 1) {
const byteA = comparisonBytesA[i];
const byteB = comparisonBytesB[i];
if (byteA === byteB) {
// eslint-disable-next-line no-continue
continue;
}
return byteA > byteB ? 1 : -1;
}
return 0;
} catch (e) {
console.error(e);
return 0;
}
};