Add support of IP addresses (#15)

This commit is contained in:
Borys Levytskyi
2021-01-14 11:20:44 +02:00
committed by GitHub
parent 387555bc4c
commit f26be5132b
14 changed files with 483 additions and 23 deletions

2
.gitignore vendored
View File

@@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
debug.log

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@@ -2,7 +2,7 @@ import HelpResult from './models/HelpResult';
import AboutResult from './models/AboutResult';
import UnknownCommandResult from './models/UnknownCommandResult';
import ExpressionResult from './models/ExpressionResult';
import ErrorResult from './models/ErrorResult';
import {UnhandledErrorResult, ErrorResult} from './models/ErrorResults';
import WahtsnewResult from './models/WhatsnewResult';
import StringResult from './models/StringResult';
import * as expression from './expression/expression';
@@ -10,6 +10,9 @@ import uuid from 'uuid/v4';
import { CommandInput, CmdShell } from './core/cmd';
import { ExpressionInput } from './expression/expression-interfaces';
import AppState from './core/AppState';
import {ParsingError, IpAddress, ipAddressParser, IpAddressWithSubnetMask, ParsedIpObject} from './ipaddress/ip'
import IpAddressResult from './models/IpAddressResult';
import { isGetAccessor, isPrefixUnaryExpression } from 'typescript';
export default {
initialize (cmd: CmdShell, appState: AppState) {
@@ -32,11 +35,43 @@ export default {
appState.addCommandResult(new StringResult(c.input, `Debug Mode: ${appState.debugMode}`))
});
// Ip Addresses
cmd.command({
canHandle: (input:string) => ipAddressParser.parse(input) != null,
handle: function(c: CommandInput) {
var result = ipAddressParser.parse(c.input);
if(result == null)
return;
if(result instanceof ParsingError) {
appState.addCommandResult(new ErrorResult(c.input, result.errorMessage));
return;
}
const ipAddresses : IpAddress[] = [];
(result as ParsedIpObject[]).forEach(r => {
if(r instanceof IpAddressWithSubnetMask)
{
ipAddresses.push(r.ipAddress);
ipAddresses.push(r.createSubnetMaskIp());
}
else if(r instanceof IpAddress) {
ipAddresses.push(r);
}
});
appState.addCommandResult(new IpAddressResult(c.input, ipAddresses));
}
})
// Bitwise Expressions
cmd.command({
canHandle: (input:string) => expression.parser.canParse(input),
handle: function(c: CommandInput) {
var expr = expression.parser.parse(c.input);
appState.toggleDebugMode();
appState.addCommandResult(new ExpressionResult(c.input, expr as ExpressionInput));
}
})
@@ -47,6 +82,6 @@ export default {
handle: (c: CommandInput) => appState.addCommandResult(new UnknownCommandResult(c.input))
});
cmd.onError((input: string, err: Error) => appState.addCommandResult(new ErrorResult(input, err)));
cmd.onError((input: string, err: Error) => appState.addCommandResult(new UnhandledErrorResult(input, err)));
}
}

View File

@@ -7,11 +7,13 @@ import ExpressionResult from '../models/ExpressionResult';
import BitwiseOperationExpressionView from './results/expressions/BitwiseOperationExpressionView';
import WhatsnewResult from '../models/WhatsnewResult';
import WhatsnewResultView from './results/WhatsNewResultView';
import ErrorResult from '../models/ErrorResult';
import {UnhandledErrorResult, ErrorResult} from '../models/ErrorResults';
import StringResult from '../models/StringResult';
import IpAddressView from './results/IpAddressView';
import CommandResult from '../models/CommandResult';
import AppState from '../core/AppState';
import IpAddressResult from '../models/IpAddressResult';
type DisplayResultProps = {
content : CommandResult,
@@ -54,12 +56,24 @@ export default class DisplayResult extends React.Component<DisplayResultProps> {
return <p>{result.value}</p>
}
if (result instanceof ErrorResult) {
if (result instanceof UnhandledErrorResult) {
return <div className="result">
<div className="error">(X_X) Ooops.. Something ain' right: <strong>{result.error.message}</strong></div>
</div>
}
if (result instanceof ErrorResult) {
return <div className="result">
<div className="error">{result.errorMessage}</div>
</div>
}
if(result instanceof IpAddressResult) {
const ipResult = result as IpAddressResult;
return <IpAddressView ipAddresses={ipResult.ipAddresses} />
}
return <div className="result">
<div className="error">¯\_()_/¯ Sorry, i don&prime;t know what <strong>{this.props.content.input}</strong> is</div>
</div>

View File

@@ -3,7 +3,7 @@ import React from 'react';
export type BinaryStringViewProps = {
allowFlipBits: boolean;
binaryString: string;
onFlipBit: (input: FlipBitEventArg) => void;
onFlipBit?: (input: FlipBitEventArg) => void;
emphasizeBytes: boolean;
};
@@ -11,6 +11,7 @@ export type FlipBitEventArg = {
index: number;
binaryString: string;
$event: any;
newBinaryString: string
};
export default class BinaryStringView extends React.Component<BinaryStringViewProps> {
@@ -19,13 +20,19 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
}
onBitClick(index: number, e : any) {
if(!this.props.allowFlipBits) {
if(!this.props.allowFlipBits || !this.props.onFlipBit) {
return;
}
if(this.props.onFlipBit) {
this.props.onFlipBit({ index: index, binaryString: this.props.binaryString, $event: e });
if(!this.props.onFlipBit) {
}
const arr = this.props.binaryString.split('');
arr[index] = arr[index] == '0' ? '1' : '0';
const newBinaryString = arr.join('');
this.props.onFlipBit({ index: index, binaryString: this.props.binaryString, $event: e, newBinaryString });
}
getChildren() {

View File

@@ -0,0 +1 @@
.ip-address-info { padding-top: 1em; font-size: 0.85em; vertical-align: middle;}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { IpAddress, OctetNumber, getNetworkClass } from '../../ipaddress/ip';
import formatter from '../../core/formatter'
import BinaryStringView from './BinaryString';
import './IpAddressView.css';
type IpAddressViewProps = {
ipAddresses: IpAddress[]
};
export class IpAddressView extends React.Component<IpAddressViewProps>
{
render() {
if(this.props.ipAddresses.length === 1)
return this.renderSingleIp(this.props.ipAddresses[0]);
return this.renderMultipleIps();
}
renderMultipleIps() {
return <table className="expression">
<tbody>
{this.props.ipAddresses.map((ip, i) => <tr key={i}>
<td>{ip.toString()}</td>
<td>
{this.bin(ip.firstByte, 1, ip)}.{this.bin(ip.secondByte, 2, ip)}.{this.bin(ip.thirdByte, 3, ip)}.{this.bin(ip.fourthByte, 4, ip)}
</td>
</tr>)}
</tbody>
</table>
}
renderSingleIp(ip: IpAddress) {
return <table className="expression">
<thead>
<tr>
<th>{ip.firstByte}</th>
<th>{ip.secondByte}</th>
<th>{ip.thirdByte}</th>
<th>{ip.fourthByte}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{this.bin(ip.firstByte, 1, ip)}</td>
<td>{this.bin(ip.secondByte, 2, ip)}</td>
<td>{this.bin(ip.thirdByte, 3, ip)}</td>
<td>{this.bin(ip.fourthByte, 4, ip)}</td>
</tr>
<tr>
<td colSpan={2} className="ip-address-info">
<a href="https://www.wikiwand.com/en/Classful_network" target="_blank">Network Class: {getNetworkClass(ip).toUpperCase()}</a>
</td>
</tr>
</tbody>
</table>;
}
bin(value: number, octetNumber: OctetNumber, ip: IpAddress) {
return <BinaryStringView
binaryString={fmt(value)}
key={octetNumber}
emphasizeBytes={false}
allowFlipBits={true}
onFlipBit={e => this.onFlippedBit(e.newBinaryString, octetNumber, ip)} />;
}
onFlippedBit(binaryString: string, number: OctetNumber, ip : IpAddress) {
ip.setOctet(number, parseInt(binaryString, 2));
this.forceUpdate();
}
};
function fmt(num: number) : string {
return formatter.padLeft(formatter.formatString(num, 'bin'), 8, '0');
}
export default IpAddressView;

View File

@@ -25,17 +25,17 @@ code { font-size: 1.2em; font-weight: bold; }
.expression .label { font-weight: bold; padding-right: 5px; text-align: right; }
.expression .bin { letter-spacing: 3px; }
.expression .flipable { cursor: pointer; opacity: 1 }
.expression .flipable:hover { opacity: 0.8 }
.expression .byte { margin: 0 3px; }
.expression .flipable { cursor: pointer; opacity: 1 }
.expression .flipable:hover { opacity: 0.8 }
.expression-result td { border-top: dotted 1px gray; }
.expression { font-size: 1.5em; font-family: monospace }
.expression .prefix { font-weight: normal; display: none; font-size: 0.9em }
.expression .other { font-size: 0.9em}
.expression .sign { text-align: right}
.flipable { cursor: pointer; opacity: 1 }
.flipable { cursor: pointer; opacity: 1 }
.flipable:hover { opacity: 0.8 }
.hex .prefix { display: inline; }
.help { padding: 10px; }

104
src/ipaddress/ip.test.ts Normal file
View File

@@ -0,0 +1,104 @@
import {IpAddress, ipAddressParser, getNetworkClass, ParsingError, IpAddressWithSubnetMask, ParsedIpObject} from './ip';
describe('parser tests', () => {
it('can parse correct ip address', () => {
const actual = ipAddressParser.parse('127.1.2.3');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const obj = (actual as ParsedIpObject[])[0];
const expected = new IpAddress(127, 1, 2, 3);
expect(obj).not.toBe(null);
expect(obj.toString()).toBe(expected.toString());
});
it('cannot parse incorrect ip address', () => {
expect(ipAddressParser.parse('abc')).toBe(null);
expect(ipAddressParser.parse('')).toBe(null);
});
it('should parse invalid ip address', () => {
expect(ipAddressParser.parse('256.0.0.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.256.0.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.256.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.256')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.255 asd')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.255/99')).toBeInstanceOf(ParsingError);
});
it('parses correct ip and subnet mask', () => {
const actual = ipAddressParser.parse('127.0.0.1/24');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const obj = (actual as ParsedIpObject[])[0];
expect(obj).toBeInstanceOf(IpAddressWithSubnetMask);
expect(obj!.toString()).toBe('127.0.0.1/24');
expect((obj as IpAddressWithSubnetMask).maskBits).toBe(24);
});
it('parses list of ip addresses', () => {
const actual = ipAddressParser.parse('127.0.0.1/24 255.255.1.1');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const first = (actual as ParsedIpObject[])[0];
expect(first).toBeInstanceOf(IpAddressWithSubnetMask);
expect(first!.toString()).toBe('127.0.0.1/24');
expect((first as IpAddressWithSubnetMask).maskBits).toBe(24);
const second = (actual as ParsedIpObject[])[1];
expect(second).toBeInstanceOf(IpAddress);
expect(second!.toString()).toBe('255.255.1.1');
});
});
describe('getNetworkClass tests', () => {
it('detects class a', () => {
expect(getNetworkClass(new IpAddress(1, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(55, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(97, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(127, 0, 0, 0))).toBe('a');
});
it('detects class b', () => {
expect(getNetworkClass(new IpAddress(128, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(134, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(180, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(191, 0, 0, 0))).toBe('b');
});
it('detects class c', () => {
expect(getNetworkClass(new IpAddress(192, 0, 0, 0))).toBe('c');
expect(getNetworkClass(new IpAddress(218, 0, 0, 0))).toBe('c');
expect(getNetworkClass(new IpAddress(223, 0, 0, 0))).toBe('c');
});
it('detects class d', () => {
expect(getNetworkClass(new IpAddress(224, 0, 0, 0))).toBe('d');
expect(getNetworkClass(new IpAddress(234, 0, 0, 0))).toBe('d');
expect(getNetworkClass(new IpAddress(239, 0, 0, 0))).toBe('d');
});
it('detects class e', () => {
expect(getNetworkClass(new IpAddress(240, 0, 0, 0))).toBe('e');
expect(getNetworkClass(new IpAddress(241, 0, 0, 0))).toBe('e');
expect(getNetworkClass(new IpAddress(255, 0, 0, 0))).toBe('e');
});
});
describe('IpAddressWithSubnetMask tests', () => {
it('creates subnetmask ip', () => {
const ip = new IpAddress(127, 0, 0, 1);
expect(new IpAddressWithSubnetMask(ip, 1).createSubnetMaskIp().toString()).toBe('128.0.0.0');
expect(new IpAddressWithSubnetMask(ip, 8).createSubnetMaskIp().toString()).toBe('255.0.0.0');
expect(new IpAddressWithSubnetMask(ip, 10).createSubnetMaskIp().toString()).toBe('255.192.0.0');
expect(new IpAddressWithSubnetMask(ip, 20).createSubnetMaskIp().toString()).toBe('255.255.240.0');
expect(new IpAddressWithSubnetMask(ip, 30).createSubnetMaskIp().toString()).toBe('255.255.255.252');
expect(new IpAddressWithSubnetMask(ip, 32).createSubnetMaskIp().toString()).toBe('255.255.255.255');
});
});

183
src/ipaddress/ip.ts Normal file
View File

@@ -0,0 +1,183 @@
import formatter from '../core/formatter';
export type OctetNumber = 1 | 2 | 3 | 4;
export type NetworkClass = 'a' | 'b' | 'c' | 'd' | 'e';
export type ParsedIpObject = IpAddress | IpAddressWithSubnetMask;
const ipAddressParser = {
parse: function(input: string) : ParsedIpObject[] | ParsingError | null {
const matches = this.getMaches(input);
const correctInputs = matches.filter(m => m.matches != null);
const incorrectInputs = matches.filter(m => m.matches == null);
if(correctInputs.length == 0)
return null;
if(incorrectInputs.length > 0) {
return new ParsingError(`Value(s) ${incorrectInputs.map(v => v.input).join(',')} was not recognized as valid ip address or ip address with a subnet mask`);
}
const parsedObjects = matches.map(m => this.parseSingle(m.matches!, m.input));
const parsingErrors = parsedObjects.filter(p => p instanceof ParsingError);
if(parsingErrors.length > 0) {
return parsingErrors[0] as ParsingError;
}
return parsedObjects as ParsedIpObject[];
},
getMaches(input : string) : { matches: RegExpExecArray | null, input: string }[] {
return input.
replace(/[\t\s]+/g, ' ')
.split(' ')
.filter(s => s.length>0)
.map(s => {
const ipV4Regex = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(\/\d+)?$/;
const matches = ipV4Regex.exec(s);
if(matches == null || matches.length === 0)
return {matches: null, input: s};
return {matches, input: s};
});
},
parseSingle(matches : RegExpExecArray, input: string) : ParsedIpObject | ParsingError {
const invalid = (n: number) => n < 0 || n > 255;
const first = parseInt(matches[1]);
const second = parseInt(matches[2]);
const third = parseInt(matches[3]);
const fourth = parseInt(matches[4]);
if(invalid(first) || invalid(second) || invalid(third) || invalid(fourth))
return new ParsingError(`${input} value doesn't fall within the valid range of the IP address space`);
const ipAddress = new IpAddress(first, second, third, fourth);
if(matches[5]) {
const maskPart = matches[5].substr(1);
const maskBits = parseInt(maskPart);
if(maskBits > 32) {
return new ParsingError(`Subnet mask value in ${input} is out of range`);
}
return new IpAddressWithSubnetMask(ipAddress, maskBits);
}
return ipAddress;
}
}
export class ParsingError {
errorMessage: string;
constructor(message: string) {
this.errorMessage = message;
}
}
export class IpAddressWithSubnetMask {
maskBits: number;
ipAddress: IpAddress;
constructor(ipAddress : IpAddress, maskBits : number) {
this.ipAddress = ipAddress;
this.maskBits = maskBits;
}
toString() {
return `${this.ipAddress.toString()}/${this.maskBits}`;
}
createSubnetMaskIp() : IpAddress {
const mask = (bits: number) => 255<<(8-bits)&255;
if(this.maskBits <= 8) {
return new IpAddress(mask(this.maskBits), 0, 0, 0);
}
else if(this.maskBits <= 16) {
return new IpAddress(255, mask(this.maskBits-8), 0, 0);
}
else if(this.maskBits <= 24) {
return new IpAddress(255, 255, mask(this.maskBits-16), 0);
}
else {
return new IpAddress(255, 255, 255, mask(this.maskBits-24));
}
}
}
export class IpAddress {
firstByte : number;
secondByte: number;
thirdByte : number;
fourthByte: number
constructor(firstByte : number, secondByte: number, thirdByte : number, fourthByte: number) {
this.firstByte = firstByte;
this.secondByte = secondByte;
this.thirdByte = thirdByte;
this.fourthByte = fourthByte;
}
toString() : string {
return `${this.firstByte}.${this.secondByte}.${this.thirdByte}.${this.fourthByte}`;
}
setOctet(octet: OctetNumber, value : number) {
switch(octet) {
case 1:
this.firstByte = value;
break;
case 2:
this.secondByte = value;
break;
case 3:
this.thirdByte = value;
break;
case 4:
this.fourthByte = value;
break;
}
}
}
const getNetworkClass = function (ipAddress: IpAddress) : NetworkClass {
const byte = ipAddress.firstByte;
const bineryRep = formatter.formatString(ipAddress.firstByte, 'bin');
const firstBitOne = (byte & 128) === 128;
const firstBitZero = (byte & 128) === 0;
const secondBitOne = (byte & 64) === 64;
const thirdBitOne = (byte & 32) === 32;
const thirdBitZero = (byte & 32) === 0;
const forthBitZero = (byte & 16) === 0;
const forthBitOne = (byte & 16) === 16;
// e: 1111
if(firstBitOne && secondBitOne && thirdBitOne && forthBitOne)
return 'e';
if(firstBitOne && secondBitOne && thirdBitOne && forthBitZero) // Start bits: 1110;
return 'd';
if(firstBitOne && secondBitOne && thirdBitZero) // Start bits: 110;
return 'c';
return firstBitOne ? 'b' : 'a';
};
export {ipAddressParser, getNetworkClass};

View File

@@ -1,9 +0,0 @@
import CommandResult from './CommandResult';
export default class ErrorResult extends CommandResult {
error: Error;
constructor(input: string, error : Error) {
super(input);
this.error = error;
}
}

View File

@@ -0,0 +1,17 @@
import CommandResult from './CommandResult';
export class UnhandledErrorResult extends CommandResult {
error: Error;
constructor(input: string, error : Error) {
super(input);
this.error = error;
}
}
export class ErrorResult extends CommandResult {
errorMessage: string;
constructor(input: string, errorMessage : string) {
super(input);
this.errorMessage = errorMessage;
}
}

View File

@@ -0,0 +1,10 @@
import { IpAddress, ipAddressParser, IpAddressWithSubnetMask } from '../ipaddress/ip';
import CommandResult from './CommandResult';
export default class IpAddressResult extends CommandResult {
ipAddresses: IpAddress[];
constructor(input: string, ipAddresses: IpAddress[]) {
super(input);
this.ipAddresses = ipAddresses;
}
}

View File

@@ -1,2 +1,5 @@
- Bug: ~1|~2
- add Firefox tests
- support for list of ip addresses
- support of subnet masks e.g. 127.0.0.1/24