From f26be5132b5c9ba41c142403aeb25dcebc2157f4 Mon Sep 17 00:00:00 2001 From: Borys Levytskyi Date: Thu, 14 Jan 2021 11:20:44 +0200 Subject: [PATCH] Add support of IP addresses (#15) --- .gitignore | 2 + .vscode/launch.json | 15 ++ src/commands.ts | 41 ++++- src/components/DisplayResultView.tsx | 18 ++- src/components/results/BinaryString.tsx | 15 +- src/components/results/IpAddressView.css | 1 + src/components/results/IpAddressView.tsx | 78 ++++++++++ src/index.css | 8 +- src/ipaddress/ip.test.ts | 104 +++++++++++++ src/ipaddress/ip.ts | 183 +++++++++++++++++++++++ src/models/ErrorResult.ts | 9 -- src/models/ErrorResults.ts | 17 +++ src/models/IpAddressResult.ts | 10 ++ todo.txt | 5 +- 14 files changed, 483 insertions(+), 23 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/components/results/IpAddressView.css create mode 100644 src/components/results/IpAddressView.tsx create mode 100644 src/ipaddress/ip.test.ts create mode 100644 src/ipaddress/ip.ts delete mode 100644 src/models/ErrorResult.ts create mode 100644 src/models/ErrorResults.ts create mode 100644 src/models/IpAddressResult.ts diff --git a/.gitignore b/.gitignore index 4d29575..b0be287 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +debug.log diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..562cc64 --- /dev/null +++ b/.vscode/launch.json @@ -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}" + } + ] +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 2392008..39958f3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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))); } } \ No newline at end of file diff --git a/src/components/DisplayResultView.tsx b/src/components/DisplayResultView.tsx index 5dfc977..de6d268 100644 --- a/src/components/DisplayResultView.tsx +++ b/src/components/DisplayResultView.tsx @@ -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 { return

{result.value}

} - if (result instanceof ErrorResult) { + if (result instanceof UnhandledErrorResult) { return
(X_X) Ooops.. Something ain' right: {result.error.message}
} + if (result instanceof ErrorResult) { + return
+
{result.errorMessage}
+
+ } + + if(result instanceof IpAddressResult) { + const ipResult = result as IpAddressResult; + + return + } + return
¯\_(ツ)_/¯ Sorry, i don′t know what {this.props.content.input} is
diff --git a/src/components/results/BinaryString.tsx b/src/components/results/BinaryString.tsx index 27de985..3826a68 100644 --- a/src/components/results/BinaryString.tsx +++ b/src/components/results/BinaryString.tsx @@ -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 { @@ -19,13 +20,19 @@ export default class BinaryStringView extends React.Component +{ + + render() { + if(this.props.ipAddresses.length === 1) + return this.renderSingleIp(this.props.ipAddresses[0]); + + return this.renderMultipleIps(); + } + + renderMultipleIps() { + return + + {this.props.ipAddresses.map((ip, i) => + + + )} + +
{ip.toString()} + {this.bin(ip.firstByte, 1, ip)}.{this.bin(ip.secondByte, 2, ip)}.{this.bin(ip.thirdByte, 3, ip)}.{this.bin(ip.fourthByte, 4, ip)} +
+ } + + renderSingleIp(ip: IpAddress) { + return + + + + + + + + + + + + + + + + + + + +
{ip.firstByte}{ip.secondByte}{ip.thirdByte}{ip.fourthByte}
{this.bin(ip.firstByte, 1, ip)}{this.bin(ip.secondByte, 2, ip)}{this.bin(ip.thirdByte, 3, ip)}{this.bin(ip.fourthByte, 4, ip)}
+ Network Class: {getNetworkClass(ip).toUpperCase()} +
; + } + + bin(value: number, octetNumber: OctetNumber, ip: IpAddress) { + return 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; \ No newline at end of file diff --git a/src/index.css b/src/index.css index dbfc2c0..1d8cf25 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } diff --git a/src/ipaddress/ip.test.ts b/src/ipaddress/ip.test.ts new file mode 100644 index 0000000..b610bdd --- /dev/null +++ b/src/ipaddress/ip.test.ts @@ -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'); + }); +}); \ No newline at end of file diff --git a/src/ipaddress/ip.ts b/src/ipaddress/ip.ts new file mode 100644 index 0000000..d835e7c --- /dev/null +++ b/src/ipaddress/ip.ts @@ -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}; \ No newline at end of file diff --git a/src/models/ErrorResult.ts b/src/models/ErrorResult.ts deleted file mode 100644 index 432e190..0000000 --- a/src/models/ErrorResult.ts +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/models/ErrorResults.ts b/src/models/ErrorResults.ts new file mode 100644 index 0000000..a9b9842 --- /dev/null +++ b/src/models/ErrorResults.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/models/IpAddressResult.ts b/src/models/IpAddressResult.ts new file mode 100644 index 0000000..06a5df2 --- /dev/null +++ b/src/models/IpAddressResult.ts @@ -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; + } +} \ No newline at end of file diff --git a/todo.txt b/todo.txt index b404ec6..9526973 100644 --- a/todo.txt +++ b/todo.txt @@ -1,2 +1,5 @@ - Bug: ~1|~2 -- add Firefox tests \ No newline at end of file +- add Firefox tests +- support for list of ip addresses +- support of subnet masks e.g. 127.0.0.1/24 +