Skip to content

feat: wire package for common parsing/serializing logic #15

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 1 commit into from
Feb 16, 2025
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
24 changes: 23 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/dhcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"default": "./dist/index.cjs"
}
},
"dependencies": {},
"dependencies": {
"@tcpip/wire": "^0.1.0"
},
"devDependencies": {
"@total-typescript/tsconfig": "^1.0.4",
"tcpip": "0.2",
Expand Down
22 changes: 0 additions & 22 deletions packages/dhcp/src/util.ts

This file was deleted.

26 changes: 13 additions & 13 deletions packages/dhcp/src/wire.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseIPv4Address, serializeIPv4Address } from '@tcpip/wire';
import { DHCPMessageTypes, DHCPOptionCodes, DHCPOptions } from './constants.js';
import type {
DHCPMessage,
Expand All @@ -6,7 +7,6 @@ import type {
DHCPOption,
DHCPServerOptions,
} from './types.js';
import { ipv4ToNumber, numberToIPv4 } from './util.js';

export function parseDHCPMessageType(type: number) {
const [key] =
Expand Down Expand Up @@ -73,8 +73,8 @@ export function parseDHCPMessage(data: Uint8Array): DHCPMessage {
type = parseDHCPMessageType(data[i + 2]!);
break;
case DHCPOptionCodes.REQUESTED_IP: {
const ip = view.getUint32(i + 2);
requestedIp = numberToIPv4(ip);
const ip = data.subarray(i + 2, i + 2 + 4);
requestedIp = parseIPv4Address(ip);
break;
}
case DHCPOptionCodes.SERVER_ID:
Expand Down Expand Up @@ -113,8 +113,8 @@ export function serializeDHCPMessage(
view.setUint32(4, params.xid);

// Set yiaddr (your IP) field
const ip = ipv4ToNumber(params.yiaddr);
view.setUint32(16, ip);
const ip = serializeIPv4Address(params.yiaddr);
message.set(ip, 16);

// Set client MAC address
const macBytes = params.mac.split(':').map((x: string) => parseInt(x, 16));
Expand All @@ -136,8 +136,8 @@ export function serializeDHCPMessage(
message[offset++] = DHCPOptions.SERVER_IDENTIFIER;
message[offset++] = 4;

const serverIP = ipv4ToNumber(options.serverIdentifier);
view.setUint32(offset, serverIP);
const serverIP = serializeIPv4Address(options.serverIdentifier);
message.set(serverIP, offset);
offset += 4;

// Lease time
Expand All @@ -149,24 +149,24 @@ export function serializeDHCPMessage(
// Subnet mask
message[offset++] = DHCPOptions.SUBNET_MASK;
message[offset++] = 4;
const mask = ipv4ToNumber(options.subnetMask);
view.setUint32(offset, mask);
const mask = serializeIPv4Address(options.subnetMask);
message.set(mask, offset);
offset += 4;

// Router
message[offset++] = DHCPOptions.ROUTER;
message[offset++] = 4;
const router = ipv4ToNumber(options.router);
view.setUint32(offset, router);
const router = serializeIPv4Address(options.router);
message.set(router, offset);
offset += 4;

// DNS Servers (if configured)
if (options.dnsServers?.length) {
message[offset++] = DHCPOptions.DNS_SERVERS;
message[offset++] = 4 * options.dnsServers.length;
for (const dnsServer of options.dnsServers) {
const dnsIP = ipv4ToNumber(dnsServer);
view.setUint32(offset, dnsIP);
const dnsIP = serializeIPv4Address(dnsServer);
message.set(dnsIP, offset);
offset += 4;
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/dns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"default": "./dist/index.cjs"
}
},
"dependencies": {},
"dependencies": {
"@tcpip/wire": "^0.1.0"
},
"devDependencies": {
"@total-typescript/tsconfig": "^1.0.4",
"tcpip": "0.2",
Expand Down
78 changes: 1 addition & 77 deletions packages/dns/src/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { compressIPv6, expandIPv6, ptrNameToIP } from './util';
import { ptrNameToIP } from './util';

describe('ptrNameToIP', () => {
test('should convert PTR name to an IPv4 address', () => {
Expand Down Expand Up @@ -32,79 +32,3 @@ describe('ptrNameToIP', () => {
expect(() => ptrNameToIP('')).toThrow('invalid PTR name: ');
});
});

describe('compressIPv6', () => {
test('compresses an IPv6 address with leading zeros', () => {
const ip = '0000:0000:0000:0000:0000:0000:0000:0001';
const compressed = compressIPv6(ip);
expect(compressed).toBe('::1');
});

test('compresses an IPv6 address with trailing zeros', () => {
const ip = '2001:0000:0000:0000:0000:0000:0000:0000';
const compressed = compressIPv6(ip);
expect(compressed).toBe('2001::');
});

test('compresses an IPv6 address with consecutive zeros', () => {
const ip = '2001:0db8:0000:0000:0000:0000:0000:0001';
const compressed = compressIPv6(ip);
expect(compressed).toBe('2001:db8::1');
});

test('compresses an IPv6 address with no zeros', () => {
const ip = '2001:db8:1234:5678:9abc:def0:1234:5678';
const compressed = compressIPv6(ip);
expect(compressed).toBe('2001:db8:1234:5678:9abc:def0:1234:5678');
});

test('compresses an IPv6 address with a single zero block', () => {
const ip = '2001:db8:0:1:0:0:0:1';
const compressed = compressIPv6(ip);
expect(compressed).toBe('2001:db8:0:1::1');
});

test('compresses an IPv6 address with multiple zero blocks', () => {
const ip = '2001:0:0:1:0:0:0:1';
const compressed = compressIPv6(ip);
expect(compressed).toBe('2001:0:0:1::1');
});
});

describe('expandIPv6', () => {
test('expands an IPv6 address with leading zeros', () => {
const ip = '::1';
const expanded = expandIPv6(ip);
expect(expanded).toBe('0000:0000:0000:0000:0000:0000:0000:0001');
});

test('expands an IPv6 address with trailing zeros', () => {
const ip = '2001::';
const expanded = expandIPv6(ip);
expect(expanded).toBe('2001:0000:0000:0000:0000:0000:0000:0000');
});

test('expands an IPv6 address with consecutive zeros', () => {
const ip = '2001:db8::1';
const expanded = expandIPv6(ip);
expect(expanded).toBe('2001:0db8:0000:0000:0000:0000:0000:0001');
});

test('expands an IPv6 address with partial zeros', () => {
const ip = '2001:db8:1234:5678:9abc:def0:1234:5678';
const expanded = expandIPv6(ip);
expect(expanded).toBe('2001:0db8:1234:5678:9abc:def0:1234:5678');
});

test('expands an IPv6 address with a single zero block', () => {
const ip = '2001:db8:0:1::1';
const expanded = expandIPv6(ip);
expect(expanded).toBe('2001:0db8:0000:0001:0000:0000:0000:0001');
});

test('expands an IPv6 address with multiple zero blocks', () => {
const ip = '2001:0:0:1::1';
const expanded = expandIPv6(ip);
expect(expanded).toBe('2001:0000:0000:0001:0000:0000:0000:0001');
});
});
94 changes: 2 additions & 92 deletions packages/dns/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { compressIPv6 } from '@tcpip/wire';

/**
* Chunk a string into parts of a given size.
*/
Expand Down Expand Up @@ -70,95 +72,3 @@ export function ptrNameToIP(name: string): PtrIP {
throw new Error(`invalid PTR name: ${name}`);
}
}

/**
* Compresses an IPv6 address by removing leading zeros.
*/
export function compressIPv6(ip: string) {
// Split into groups and normalize to lowercase
const groups = ip.toLowerCase().split(':');

// Remove leading zeros from each group
const normalizedGroups = groups.map(
(group) => group.replace(/^0+(?=\w)/, '') // Remove leading zeros, keep single 0
);

// Find longest sequence of empty groups
let longestZeroStart = -1;
let longestZeroLength = 0;
let currentZeroStart = -1;
let currentZeroLength = 0;

for (let i = 0; i < normalizedGroups.length; i++) {
if (normalizedGroups[i] === '0' || normalizedGroups[i] === '') {
if (currentZeroStart === -1) currentZeroStart = i;
currentZeroLength++;

if (currentZeroLength > longestZeroLength) {
longestZeroStart = currentZeroStart;
longestZeroLength = currentZeroLength;
}
} else {
currentZeroStart = -1;
currentZeroLength = 0;
}
}

// Replace longest zero sequence with :: if it's at least 2 groups long
if (longestZeroLength >= 2) {
// Clear out the zero sequence
normalizedGroups.splice(longestZeroStart, longestZeroLength);

// Insert empty string for :: compression
if (longestZeroStart === 0) {
// Leading zeros - ensure we have two colons at start
normalizedGroups.unshift('', '');
} else if (longestZeroStart === normalizedGroups.length) {
// Trailing zeros - ensure we have two colons at end
normalizedGroups.push('', '');
} else {
// Middle zeros - add empty string for ::
normalizedGroups.splice(longestZeroStart, 0, '');
}
}

return normalizedGroups.join(':');
}

/**
* Expands an IPv6 address by adding leading zeros.
*/
export function expandIPv6(ip: string) {
// Handle empty string edge case
if (!ip) {
throw new Error(`invalid IPv6 address: ${ip}`);
}

// Split on :: to handle compressed zeros
const doubleColonSplit = ip.split('::').map((part) => part.split(':'));

if (doubleColonSplit.length > 2) {
throw new Error(`invalid IPv6 address: ${ip}`);
}

const [left, right] = doubleColonSplit;

if (!left) {
throw new Error(`invalid IPv6 address: ${ip}`);
}

// If no :: compression, just pad each group
if (!right) {
return left.map((group) => group.padStart(4, '0')).join(':');
}

// Calculate how many zero groups we need
const totalGroups = 8;
const missingGroups = totalGroups - (left.length + right.length);
const zeros = Array(missingGroups).fill('0000');

// Combine all parts and pad each group
return [...left, ...zeros, ...right]
.map((group) => group.padStart(4, '0'))
.join(':');
}
Loading