Minor improvements and bug fixes (#48)

This commit is contained in:
Borys Levytskyi
2023-05-12 14:26:18 +03:00
committed by GitHub
parent 0f6e7c8092
commit 585d3f342f
8 changed files with 93 additions and 58 deletions

View File

@@ -47,6 +47,10 @@ export class Integer {
return new Integer(value, 8); return new Integer(value, 8);
} }
isTheSame (other : Integer) : boolean {
return this.value == other.value && this.signed == other.signed && this.maxBitSize == other.maxBitSize;
}
toUnsigned() { toUnsigned() {
return this.signed return this.signed
? new Integer(BigInt("0b" + this.toString(2)), this.maxBitSize, false) ? new Integer(BigInt("0b" + this.toString(2)), this.maxBitSize, false)

View File

@@ -62,6 +62,13 @@ describe('calc.addSpace', () => {
expect(calc.addSpace(n16, 1).maxBitSize).toBe(32); expect(calc.addSpace(n16, 1).maxBitSize).toBe(32);
expect(calc.addSpace(n16, 32).maxBitSize).toBe(64); expect(calc.addSpace(n16, 32).maxBitSize).toBe(64);
}); });
it('preserves the sign when extending number', () => {
const byte = Integer.byte(-1);
const actual = calc.addSpace(byte, 1);
expect(actual.maxBitSize).toBe(16);
expect(actual.num()).toBe(-1);
})
}); });
describe('calc.numberOfBitsDisplayed', () => { describe('calc.numberOfBitsDisplayed', () => {

View File

@@ -1,5 +1,5 @@
import { Integer, JsNumber, asInteger } from "./Integer"; import { Integer, JsNumber, asInteger } from "./Integer";
import { asIntN } from "./utils"; import { asIntN, logLines } from "./utils";
export default { export default {
abs (num : Integer) : Integer { abs (num : Integer) : Integer {
@@ -20,9 +20,8 @@ export default {
}, },
addSpace(number: Integer, requiredSpace: number) : Integer { addSpace(number: Integer, requiredSpace: number) : Integer {
const bin = this.toBinaryString(number);
const totalSpaceRequired = number.maxBitSize + requiredSpace; const totalSpaceRequired = number.maxBitSize + requiredSpace;
return new Integer(BigInt("0b" + bin), nextPowOfTwo(totalSpaceRequired)); return new Integer(number.value, nextPowOfTwo(totalSpaceRequired));
}, },
operation (op1: Integer, operator: string, op2 : Integer) : Integer { operation (op1: Integer, operator: string, op2 : Integer) : Integer {

View File

@@ -4,10 +4,10 @@ import './BinaryString.css';
export type BinaryStringViewProps = { export type BinaryStringViewProps = {
allowFlipBits?: boolean; allowFlipBits?: boolean;
binaryString: string; binaryString: string;
onFlipBit?: (input: FlipBitEventArg) => void; onBitClicked?: (input: FlipBitEventArg) => void;
emphasizeBytes?: boolean; emphasizeBytes?: boolean;
className?:string; className?: string;
disableHighlight?:boolean, disableHighlight?: boolean,
signBitIndex?: number, signBitIndex?: number,
}; };
@@ -15,7 +15,8 @@ export type FlipBitEventArg = {
bitIndex: number; bitIndex: number;
binaryStringLength: number; binaryStringLength: number;
$event: any; $event: any;
newBinaryString: string newBinaryString: any;
isTypeExtend: boolean
}; };
export default class BinaryStringView extends React.Component<BinaryStringViewProps> { export default class BinaryStringView extends React.Component<BinaryStringViewProps> {
@@ -23,8 +24,8 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
return <span className={this.props.className}>{this.getChildren()}</span> return <span className={this.props.className}>{this.getChildren()}</span>
} }
onBitClick(index: number, e : any) { onBitClick(index: number, isExtra: boolean, e: any) {
if(!this.props.allowFlipBits || !this.props.onFlipBit) { if (!this.props.allowFlipBits || !this.props.onBitClicked) {
return; return;
} }
@@ -32,20 +33,26 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
arr[index] = arr[index] == '0' ? '1' : '0'; arr[index] = arr[index] == '0' ? '1' : '0';
const newBinaryString = arr.join(''); const newBinaryString = arr.join('');
this.props.onFlipBit({ bitIndex: index, binaryStringLength: this.props.binaryString.length, $event: e, newBinaryString }); this.props.onBitClicked({
bitIndex: index,
binaryStringLength: this.props.binaryString.length,
newBinaryString: newBinaryString,
$event: e,
isTypeExtend: isExtra
});
} }
getChildren() { getChildren() {
var bits = this.createBits(this.props.binaryString.split('')); var bits = this.createBits(this.props.binaryString.split(''));
if(this.props.emphasizeBytes) { if (this.props.emphasizeBytes) {
return this.splitIntoBytes(bits); return this.splitIntoBytes(bits);
} }
return bits; return bits;
} }
createBits(bitChars:string[]) : JSX.Element[] { createBits(bitChars: string[]): JSX.Element[] {
const allowFlipBits = this.props.allowFlipBits || false; const allowFlipBits = this.props.allowFlipBits || false;
const css = allowFlipBits ? ' flipable' : '' const css = allowFlipBits ? ' flipable' : ''
@@ -56,18 +63,19 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
var className = c == '1' ? `one${css}` : `zero${css}`; var className = c == '1' ? `one${css}` : `zero${css}`;
var tooltip = ''; var tooltip = '';
if(i < (this.props.signBitIndex || 0)) const isExtra = i < (this.props.signBitIndex || 0);
if (isExtra)
className += ' extra-bit'; className += ' extra-bit';
if(i === this.props.signBitIndex) { if (i === this.props.signBitIndex) {
className += ' accent1'; className += ' accent1';
tooltip = 'Signature bit. 0 means a positive number and 1 means a negative.' tooltip = 'Signature bit. 0 means a positive number and 1 means a negative.'
} }
if(disableHighlight) if (disableHighlight)
className = css; className = css;
return <span className={className} title={tooltip} key={i} onClick={e => this.onBitClick(i, e)}>{c}</span> return <span className={className} title={tooltip} key={i} onClick={e => this.onBitClick(i, isExtra, e)}>{c}</span>
}); });
} }
@@ -75,10 +83,10 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
const bytes = []; const bytes = [];
var key = 0; var key = 0;
while(bits.length > 0) { while (bits.length > 0) {
bytes.push(<span key={key++} className="byte">{bits.splice(0, 8)}</span>); bytes.push(<span key={key++} className="byte">{bits.splice(0, 8)}</span>);
} }
return bytes; return bytes;
} }
} }

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import formatter from '../../core/formatter'; import formatter from '../../core/formatter';
import BinaryStringView, { FlipBitEventArg } from '../../core/components/BinaryString'; import BinaryStringView, { FlipBitEventArg as BitClickedEventArg } from '../../core/components/BinaryString';
import BitwiseResultViewModel from './BitwiseResultViewModel'; import BitwiseResultViewModel from './BitwiseResultViewModel';
import { Expression, ExpressionElement } from '../expression-interfaces'; import { Expression, ExpressionElement } from '../expression-interfaces';
import { Operator, Operand, ListOfNumbers } from '../expression'; import { Operator, Operand, ListOfNumbers } from '../expression';
@@ -8,6 +8,7 @@ import calc from '../../core/calc';
import { Integer } from '../../core/Integer'; import { Integer } from '../../core/Integer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUndo } from '@fortawesome/free-solid-svg-icons'; import { faUndo } from '@fortawesome/free-solid-svg-icons';
import loglevel from 'loglevel';
type BitwiseResultViewProps = { type BitwiseResultViewProps = {
expression: Expression; expression: Expression;
@@ -72,10 +73,11 @@ export default class BitwiseResultView extends React.Component<BitwiseResultView
emphasizeBytes={this.props.emphasizeBytes} emphasizeBytes={this.props.emphasizeBytes}
maxNumberOfBits={this.maxSeenLengthNumberOfBits} maxNumberOfBits={this.maxSeenLengthNumberOfBits}
showInfoColumn={showInfoColumn} showInfoColumn={showInfoColumn}
onBitFlipped={() => this.onBitFlipped()} />); onValueChanged={() => this.onValueChanged()} />);
} }
onBitFlipped() { onValueChanged() {
loglevel.debug("onValueChanged()");
this.forceUpdate(); this.forceUpdate();
} }
} }
@@ -89,45 +91,58 @@ type ExpressionElementRowProps = {
allowFlipBits: boolean, allowFlipBits: boolean,
allowSignChange: boolean, allowSignChange: boolean,
expressionItem: ExpressionElement, expressionItem: ExpressionElement,
onBitFlipped: any, onValueChanged: any,
showInfoColumn: boolean showInfoColumn: boolean
} }
class ExpressionElementTableRow extends React.Component<ExpressionElementRowProps> { class ExpressionElementTableRow extends React.Component<ExpressionElementRowProps> {
infoWasShown: boolean = false; infoWasShown: boolean = false;
originalValue: Integer | null = null; originalValue: Integer;
scalar: Operand;
constructor(props: ExpressionElementRowProps) { constructor(props: ExpressionElementRowProps) {
super(props); super(props);
this.state = { operand: null }; this.state = { operand: null };
this.scalar = this.props.expressionItem.getUnderlyingOperand();
this.originalValue = this.scalar.value;
} }
render() { render() {
const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props; const { sign, css, maxNumberOfBits, emphasizeBytes, allowFlipBits } = this.props;
const scalar = this.props.expressionItem.evaluate(); const scalar = this.props.expressionItem.evaluate();
const bin = formatter.numberToString(scalar.value, 'bin').padStart(maxNumberOfBits, '0'); const padChar = scalar.value.value >= 0 ? '0' : '1';
const bin = formatter.numberToString(scalar.value, 'bin').padStart(maxNumberOfBits, padChar);
const signBitIndex = scalar.value.signed && bin.length >= scalar.value.maxBitSize ? bin.length - scalar.value.maxBitSize : -1; const signBitIndex = scalar.value.signed && bin.length >= scalar.value.maxBitSize ? bin.length - scalar.value.maxBitSize : -1;
return <tr className={"row-with-bits " + css}> return <tr className={"row-with-bits " + css}>
<td className="sign">{sign}</td> <td className="sign">{sign}</td>
<td className="label">{this.getLabel()}</td> <td className="label">
<span>{this.getLabel()}</span>
</td>
<td className="bin"> <td className="bin">
<BinaryStringView <BinaryStringView
emphasizeBytes={emphasizeBytes} emphasizeBytes={emphasizeBytes}
binaryString={bin} binaryString={bin}
allowFlipBits={allowFlipBits} allowFlipBits={allowFlipBits}
signBitIndex={signBitIndex} signBitIndex={signBitIndex}
onFlipBit={args => this.flipBit(args)} /> onBitClicked={args => this.onBitClicked(args)} />
</td> </td>
<td className="other">{this.getAlternative()}</td> <td className="other">{this.getAlternative()}</td>
<td className="info accent1" data-test-name='ignore'>{this.props.showInfoColumn ? this.getInfo(maxNumberOfBits) : null}</td> <td className="info accent1" data-test-name='ignore'>{this.props.showInfoColumn ? this.getInfo() : null}</td>
<td className='undo' data-test-name='ignore'> <td className='undo' data-test-name='ignore'>
{this.originalValue != null ? <button title='Undo all changes' onClick={() => this.undo()}><FontAwesomeIcon icon={faUndo}/></button> : null} {this.getUndoButton()}
</td> </td>
</tr>; </tr>;
} }
getUndoButton(): React.ReactNode {
return !this.originalValue.isTheSame(this.scalar.value)
? <button title='Undo all changes' className='undo' data-control="undo" onClick={() => this.undo()}><FontAwesomeIcon icon={faUndo}/></button>
: null;
}
getLabel(): string { getLabel(): string {
// For expressions like |~2 // For expressions like |~2
@@ -159,46 +174,43 @@ class ExpressionElementTableRow extends React.Component<ExpressionElementRowProp
} }
undo() { undo() {
if(this.originalValue == null) this.changeValue(this.originalValue);
return; this.props.onValueChanged();
this.props.expressionItem.getUnderlyingOperand().setValue(this.originalValue);
this.originalValue = null;
this.forceUpdate();
} }
flipBit(args: FlipBitEventArg) { onBitClicked(args: BitClickedEventArg) {
const op = this.props.expressionItem.getUnderlyingOperand();
const { bitIndex: index, binaryStringLength: totalLength } = args; const { bitIndex: index, binaryStringLength: totalLength } = args;
const maxBitSize = op.value.maxBitSize; const maxBitSize = this.scalar.value.maxBitSize;
const space = (totalLength - index - maxBitSize);
if(this.originalValue == null)
this.originalValue = op.value;
if(totalLength > op.value.maxBitSize && space > 0) { if(!args.isTypeExtend)
op.setValue(calc.addSpace(op.value, space)); {
const pad = this.scalar.value.maxBitSize - totalLength;
const newValue = calc.flipBit(this.scalar.value, pad + index);
this.changeValue(newValue);
return;
} }
const pad = op.value.maxBitSize - totalLength; const space = (totalLength - index - maxBitSize);
const newValue = calc.flipBit(op.value, pad + index); this.changeValue(calc.addSpace(this.scalar.value, space));
op.setValue(newValue);
this.props.onBitFlipped();
} }
onChangeSign () { onChangeSign () {
var op = this.props.expressionItem.getUnderlyingOperand(); var op = this.props.expressionItem.getUnderlyingOperand();
if(this.originalValue == null)
this.originalValue = op.value; this.changeValue(op.value.signed ? op.value.toUnsigned() : op.value.toSigned());
op.setValue(op.value.signed ? op.value.toUnsigned() : op.value.toSigned());
this.forceUpdate(); this.forceUpdate();
} }
getInfo(maxNumberOfBits:number) { changeValue(newValue: Integer) {
this.scalar.setValue(newValue);
this.props.onValueChanged();
}
getInfo() {
const op = this.props.expressionItem.getUnderlyingOperand(); const op = this.props.expressionItem.getUnderlyingOperand();
const { allowSignChange } = this.props; const { allowSignChange } = this.props;

View File

@@ -2,6 +2,7 @@ import { Operand, ListOfNumbers, BitwiseOperation, Operator } from '../expressio
import { ExpressionElement, Expression } from '../expression-interfaces'; import { ExpressionElement, Expression } from '../expression-interfaces';
import calc from '../../core/calc'; import calc from '../../core/calc';
import formatter from '../../core/formatter'; import formatter from '../../core/formatter';
import exp from 'constants';
type Config = { type Config = {
emphasizeBytes: boolean; emphasizeBytes: boolean;
@@ -156,6 +157,8 @@ export default class BitwiseResultViewModel {
static createModel(expr : Expression, emphasizeBytes: boolean) : BitwiseResultViewModel { static createModel(expr : Expression, emphasizeBytes: boolean) : BitwiseResultViewModel {
console.log(expr);
if(expr instanceof ListOfNumbers) { if(expr instanceof ListOfNumbers) {
return BitwiseResultViewModel.buildListOfNumbers(expr, { return BitwiseResultViewModel.buildListOfNumbers(expr, {
emphasizeBytes: emphasizeBytes, emphasizeBytes: emphasizeBytes,

View File

@@ -30,6 +30,8 @@ code { font-size: 1.2em; font-weight: bold; }
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
} }
.label { width: 5em; overflow: visible;}
.label span {position: global;}
.hidden { display: none;} .hidden { display: none;}
.result { margin: 10px 10px 30px; } .result { margin: 10px 10px 30px; }
@@ -71,7 +73,7 @@ a.hashLink { font-size: 1.1em;}
.cur { color: lightgray; } .cur { color: lightgray; }
.extra-bit { opacity: 0.4;} .extra-bit { opacity: 0.2;}
button { border: none; text-decoration: underline;} button { border: none; text-decoration: underline;}

View File

@@ -34,7 +34,7 @@ export class IpAddressView extends React.Component<IpAddressViewProps>
emphasizeBytes={false} emphasizeBytes={false}
allowFlipBits={true} allowFlipBits={true}
className={`octet-${octetNumber}`} className={`octet-${octetNumber}`}
onFlipBit={e => this.onFlippedBit(e.newBinaryString, octetNumber, ip)} />; onBitClicked={e => this.onFlippedBit(e.newBinaryString, octetNumber, ip)} />;
} }
onFlippedBit(binaryString: string, number: OctetNumber, ip : IpAddress) { onFlippedBit(binaryString: string, number: OctetNumber, ip : IpAddress) {