+
Skip to content

feat(ipv4-range-expander): expands a given IPv4 start and end address to a valid IPv4 subnet #366

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ declare module '@vue/runtime-core' {
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<span class="ip" @click="handleClick">{{ ip }}</span>
<span class="value" @click="handleClick">{{ value }}</span>
</template>
{{ tooltipText }}
</n-tooltip>
Expand All @@ -11,13 +11,13 @@
import { useClipboard } from '@vueuse/core';
import { ref, toRefs } from 'vue';

const props = withDefaults(defineProps<{ ip?: string }>(), { ip: '' });
const { ip } = toRefs(props);
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
const { value } = toRefs(props);

const initialText = 'Copy to clipboard';
const tooltipText = ref(initialText);

const { copy } = useClipboard({ source: ip });
const { copy } = useClipboard({ source: value });

function handleClick() {
copy();
Expand All @@ -28,8 +28,8 @@ function handleClick() {
</script>

<style scoped lang="less">
.ip {
font-family: monospace;
.value {
cursor: pointer;
font-family: monospace;
}
</style>
3 changes: 2 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as ipv4RangeExpander } from './ipv4-range-expander';
import { tool as httpStatusCodes } from './http-status-codes';
import { tool as yamlToJson } from './yaml-to-json-converter';
import { tool as jsonToYaml } from './json-to-yaml-converter';
Expand Down Expand Up @@ -111,7 +112,7 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Network',
components: [ipv4SubnetCalculator, ipv4AddressConverter, macAddressLookup, ipv6UlaGenerator],
components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator],
},
{
name: 'Math',
Expand Down
13 changes: 13 additions & 0 deletions src/tools/ipv4-range-expander/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { UnfoldMoreOutlined } from '@vicons/material';
import { defineTool } from '../tool';

export const tool = defineTool({
name: 'IPv4 range expander',
path: '/ipv4-range-expander',
description:
'Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.',
keywords: ['ipv4', 'range', 'expander', 'subnet', 'creator', 'cidr'],
component: () => import('./ipv4-range-expander.vue'),
icon: UnfoldMoreOutlined,
createdAt: new Date('2023-04-19'),
});
32 changes: 32 additions & 0 deletions src/tools/ipv4-range-expander/ipv4-range-expander.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '@playwright/test';

test.describe('Tool - IPv4 range expander', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ipv4-range-expander');
});

test('Has correct title', async ({ page }) => {
await expect(page).toHaveTitle('IPv4 range expander - IT Tools');
});

test('Calculates correct for valid input', async ({ page }) => {
await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1');
await page.getByPlaceholder('End IPv4 address...').fill('192.168.7.255');

expect(await page.getByTestId('start-address.old').textContent()).toEqual('192.168.1.1');
expect(await page.getByTestId('start-address.new').textContent()).toEqual('192.168.0.0');
expect(await page.getByTestId('end-address.old').textContent()).toEqual('192.168.7.255');
expect(await page.getByTestId('end-address.new').textContent()).toEqual('192.168.7.255');
expect(await page.getByTestId('addresses-in-range.old').textContent()).toEqual('1,791');
expect(await page.getByTestId('addresses-in-range.new').textContent()).toEqual('2,048');
expect(await page.getByTestId('cidr.old').textContent()).toEqual('');
expect(await page.getByTestId('cidr.new').textContent()).toEqual('192.168.0.0/21');
});

test('Hides result for invalid input', async ({ page }) => {
await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1');
await page.getByPlaceholder('End IPv4 address...').fill('192.168.0.255');

await expect(page.getByTestId('result')).not.toBeVisible();
});
});
Comment on lines +1 to +32
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for those tests 🙏🏻

21 changes: 21 additions & 0 deletions src/tools/ipv4-range-expander/ipv4-range-expander.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, describe, it } from 'vitest';
import { calculateCidr } from './ipv4-range-expander.service';

describe('ipv4RangeExpander', () => {
describe('when there are two valid ipv4 addresses given', () => {
it('should calculate valid cidr for given addresses', () => {
const result = calculateCidr({ startIp: '192.168.1.1', endIp: '192.168.7.255' });

expect(result).toBeDefined();
expect(result?.oldSize).toEqual(1791);
expect(result?.newSize).toEqual(2048);
expect(result?.newStart).toEqual('192.168.0.0');
expect(result?.newEnd).toEqual('192.168.7.255');
expect(result?.newCidr).toEqual('192.168.0.0/21');
});

it('should return empty result for invalid input', () => {
expect(calculateCidr({ startIp: '192.168.7.1', endIp: '192.168.6.255' })).not.toBeDefined();
});
});
});
63 changes: 63 additions & 0 deletions src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
import { convertBase } from '../integer-base-converter/integer-base-converter.model';
import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service';
export { calculateCidr };

function bits2ip(ipInt: number) {
return (ipInt >>> 24) + '.' + ((ipInt >> 16) & 255) + '.' + ((ipInt >> 8) & 255) + '.' + (ipInt & 255);
}

function getRangesize(start: string, end: string) {
if (start == null || end == null) return -1;

return 1 + parseInt(end, 2) - parseInt(start, 2);
}

function getCidr(start: string, end: string) {
if (start == null || end == null) return null;

const range = getRangesize(start, end);
if (range < 1) {
return null;
}

let mask = 32;
for (let i = 0; i < 32; i++) {
if (start[i] !== end[i]) {
mask = i;
break;
}
}

const newStart = start.substring(0, mask) + '0'.repeat(32 - mask);
const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask);

return { start: newStart, end: newEnd, mask: mask };
}

function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) {
const start = convertBase({
value: ipv4ToInt({ ip: startIp }).toString(),
fromBase: 10,
toBase: 2,
});
const end = convertBase({
value: ipv4ToInt({ ip: endIp }).toString(),
fromBase: 10,
toBase: 2,
});

const cidr = getCidr(start, end);
if (cidr != null) {
const result: Ipv4RangeExpanderResult = {};
result.newEnd = bits2ip(parseInt(cidr.end, 2));
result.newStart = bits2ip(parseInt(cidr.start, 2));
result.newCidr = result.newStart + '/' + cidr.mask;
result.newSize = getRangesize(cidr.start, cidr.end);

result.oldSize = getRangesize(start, end);
return result;
}

return undefined;
}
7 changes: 7 additions & 0 deletions src/tools/ipv4-range-expander/ipv4-range-expander.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type Ipv4RangeExpanderResult = {
oldSize?: number;
newStart?: string;
newEnd?: string;
newCidr?: string;
newSize?: number;
};
110 changes: 110 additions & 0 deletions src/tools/ipv4-range-expander/ipv4-range-expander.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<template>
<div>
<n-space item-style="flex:1 1 0">
<div>
<n-space item-style="flex:1 1 0">
<n-form-item label="Start address" v-bind="startIpValidation.attrs">
<n-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." />
</n-form-item>
<n-form-item label="End address" v-bind="endIpValidation.attrs">
<n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." />
</n-form-item>
</n-space>

<n-table v-if="showResult" data-test-id="result">
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a feedback when ip are valid but their is no result since range is invalid please

image

<thead>
<tr>
<th scope="col">&nbsp;</th>
<th scope="col">old value</th>
<th scope="col">new value</th>
</tr>
</thead>
<tbody>
<result-row
v-for="{ label, getOldValue, getNewValue } in calculatedValues"
:key="label"
:label="label"
:old-value="getOldValue(result)"
:new-value="getNewValue(result)"
/>
</tbody>
</n-table>
<n-alert
v-else-if="startIpValidation.isValid && endIpValidation.isValid"
title="Invalid combination of start and end IPv4 address"
type="error"
>
<n-space vertical>
<n-text depth="3">
The end IPv4 address is lower than the start IPv4 address. This is not valid and no result could be
calculated. In the most cases the solution to solve this problem is to change start and end address.
</n-text>
<n-button quaternary @click="onSwitchStartEndClicked">
<n-icon :component="ChangeCircleOutlined" />
&nbsp;&nbsp;Switch start and end IPv4 address
</n-button>
</n-space>
</n-alert>
</div>
</n-space>
</div>
</template>

<script setup lang="ts">
import { useValidation } from '@/composable/validation';
import { ChangeCircleOutlined } from '@vicons/material';
import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service';
import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types';
import { calculateCidr } from './ipv4-range-expander.service';
import ResultRow from './result-row.vue';

const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');

const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value }));

const calculatedValues: {
label: string;
getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined;
}[] = [
{
label: 'Start address',
getOldValue: () => rawStartAddress.value,
getNewValue: (result) => result?.newStart,
},
{
label: 'End address',
getOldValue: () => rawEndAddress.value,
getNewValue: (result) => result?.newEnd,
},
{
label: 'Addresses in range',
getOldValue: (result) => result?.oldSize?.toLocaleString(),
getNewValue: (result) => result?.newSize?.toLocaleString(),
},
{
label: 'CIDR',
getOldValue: () => '',
getNewValue: (result) => result?.newCidr,
},
];

const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
const startIpValidation = useValidation({
source: rawStartAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});
const endIpValidation = useValidation({
source: rawEndAddress,
rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }],
});

function onSwitchStartEndClicked() {
const tmpStart = rawStartAddress.value;
rawStartAddress.value = rawEndAddress.value;
rawEndAddress.value = tmpStart;
}
</script>

<style lang="less" scoped></style>
27 changes: 27 additions & 0 deletions src/tools/ipv4-range-expander/result-row.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<tr>
<td>
<n-text strong>{{ label }}</n-text>
</td>
<td :data-test-id="testId + '.old'"><span-copyable :value="oldValue" class="monospace" /></td>
<td :data-test-id="testId + '.new'">
<span-copyable :value="newValue"></span-copyable>
</td>
</tr>
</template>

<script setup lang="ts">
import SpanCopyable from '@/components/SpanCopyable.vue';
import _ from 'lodash';

const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), {
label: '',
oldValue: '',
newValue: '',
});
const { label, oldValue, newValue } = toRefs(props);

const testId = computed(() => _.kebabCase(label.value));
</script>

<style scoped lang="less"></style>
4 changes: 2 additions & 2 deletions src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<n-text strong>{{ label }}</n-text>
</td>
<td>
<copyable-ip-like v-if="getValue(networkInfo)" :ip="getValue(networkInfo)"></copyable-ip-like>
<span-copyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)"></span-copyable>
<n-text v-else depth="3">{{ undefinedFallback }}</n-text>
</td>
</tr>
Expand Down Expand Up @@ -41,8 +41,8 @@ import { useValidation } from '@/composable/validation';
import { isNotThrowing } from '@/utils/boolean';
import { useStorage } from '@vueuse/core';
import { ArrowLeft, ArrowRight } from '@vicons/tabler';
import SpanCopyable from '@/components/SpanCopyable.vue';
import { getIPClass } from './ipv4-subnet-calculator.models';
import CopyableIpLike from './copyable-ip-like.vue';

const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');

Expand Down
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载