Skip to content

Commit b3192bd

Browse files
committed
Fix b64/hex setting handling in web client
Closes #1233
1 parent 3c9d601 commit b3192bd

5 files changed

Lines changed: 185 additions & 25 deletions

File tree

OpenBullet2.Web.Tests/Unit/Utils/WebMappingTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ public void BlockSettingMapper_MapsFixedAndInterpolatedValues()
424424
Assert.Equal(BlockSettingType.String, stringDto.Type);
425425
Assert.Equal("hello", stringDto.Value);
426426

427+
var byteArraySetting = BlockSettingFactory.CreateByteArraySetting("bytes", [1, 2, 3]);
428+
var byteArrayDto = BlockSettingMapper.ToDto(byteArraySetting);
429+
430+
Assert.Equal(BlockSettingType.ByteArray, byteArrayDto.Type);
431+
Assert.Equal([1, 2, 3], Assert.IsType<byte[]>(byteArrayDto.Value));
432+
427433
var listSetting = BlockSettingFactory.CreateListOfStringsSetting(
428434
"items", ["one", "two"], SettingInputMode.Interpolated);
429435
var listDto = BlockSettingMapper.ToDto(listSetting);
@@ -444,6 +450,27 @@ public void BlockSettingMapper_MapsFixedAndInterpolatedValues()
444450

445451
var value = Assert.IsType<DictionaryOfStringsSetting>(target.FixedSetting).Value;
446452
Assert.Equal("1", value["a"]);
453+
454+
var bytesTarget = BlockSettingFactory.CreateByteArraySetting("bytes");
455+
BlockSettingMapper.Apply(new BlockSettingDto
456+
{
457+
Name = "bytes",
458+
Type = BlockSettingType.ByteArray,
459+
InputMode = SettingInputMode.Fixed,
460+
Value = JsonSerializer.SerializeToElement("AQID", Globals.JsonOptions)
461+
}, bytesTarget);
462+
463+
Assert.Equal([1, 2, 3], Assert.IsType<ByteArraySetting>(bytesTarget.FixedSetting).Value);
464+
465+
BlockSettingMapper.Apply(new BlockSettingDto
466+
{
467+
Name = "bytes",
468+
Type = BlockSettingType.ByteArray,
469+
InputMode = SettingInputMode.Fixed,
470+
Value = JsonSerializer.SerializeToElement<string?>(null, Globals.JsonOptions)
471+
}, bytesTarget);
472+
473+
Assert.Empty(Assert.IsType<ByteArraySetting>(bytesTarget.FixedSetting).Value!);
447474
}
448475

449476
[Fact]

OpenBullet2.Web/Utils/BlockSettingMapper.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ internal static void Apply(BlockSettingDto? dto, BlockSetting setting)
2626

2727
setting.InputMode = dto.InputMode;
2828
setting.InputVariableName = dto.InputVariableName ?? string.Empty;
29-
var value = (JsonElement)dto.Value!;
29+
var value = dto.Value is JsonElement element
30+
? element
31+
: JsonSerializer.SerializeToElement(dto.Value, Globals.JsonOptions);
3032

3133
switch (dto.Type)
3234
{
@@ -48,7 +50,9 @@ internal static void Apply(BlockSettingDto? dto, BlockSetting setting)
4850
break;
4951

5052
case BlockSettingType.ByteArray:
51-
((ByteArraySetting)setting.FixedSetting!).Value = value.GetBytesFromBase64();
53+
((ByteArraySetting)setting.FixedSetting!).Value = value.ValueKind is JsonValueKind.Null
54+
? []
55+
: value.GetBytesFromBase64();
5256
break;
5357

5458
case BlockSettingType.ListOfStrings:

openbullet2-web-client/src/app/main/components/config/config-stacker/block-info/byte-array-setting/byte-array-setting.component.html

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
<div class="d-flex justify-content-between w-100">
2-
<app-input-text [key]="setting.name"
3-
*ngIf="setting.inputMode === SettingInputMode.Fixed"
4-
class="input-small w-100 monospace"
5-
ngDefaultControl
6-
(ngModelChange)="setValue($event)"
7-
[ngModel]="setting.value"
8-
[regex]="'^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$'"
9-
pTooltip="{{parameter?.description}}" />
2+
<div *ngIf="setting.inputMode === SettingInputMode.Fixed" class="d-flex w-100">
3+
<input type="text"
4+
spellcheck="false"
5+
[class]="computeClass()"
6+
[value]="displayValue"
7+
(input)="valueChanging($event)"
8+
(change)="inputChanged($event)"
9+
pTooltip="{{parameter?.description}}" />
10+
<button type="button"
11+
class="button-small encoding-button ml-1"
12+
[class.active]="viewMode === ByteArrayViewMode.Base64"
13+
pTooltip="Base64"
14+
(click)="changeViewMode(ByteArrayViewMode.Base64)">
15+
B64
16+
</button>
17+
<button type="button"
18+
class="button-small encoding-button ml-1"
19+
[class.active]="viewMode === ByteArrayViewMode.Hex"
20+
pTooltip="Hex"
21+
(click)="changeViewMode(ByteArrayViewMode.Hex)">
22+
HEX
23+
</button>
24+
</div>
1025
<app-setting-input-variable
1126
*ngIf="setting.inputMode === SettingInputMode.Variable"
1227
class="w-100"
1328
[setting]="setting"
1429
[stacker]="stacker"
15-
(onChange)="valueChanged()" />
30+
(onChange)="emitValueChanged()" />
1631
<app-setting-input-mode
1732
class="ml-1"
1833
[mode]="setting.inputMode"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.encoding-button {
2+
min-width: 42px;
3+
height: 27px;
4+
border: 1px solid var(--fg-inactive);
5+
border-radius: 4px;
6+
background-color: transparent;
7+
color: var(--fg-primary);
8+
font-size: 11px;
9+
line-height: 10px;
10+
}
11+
12+
.encoding-button.active {
13+
border-color: var(--fg-accent);
14+
background-color: var(--fg-accent);
15+
color: var(--fg-light);
16+
}
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,141 @@
1-
import { Component, EventEmitter, Input, Output } from '@angular/core';
1+
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
22
import { BlockParameterDto, SettingInputMode } from 'src/app/main/dtos/config/block-descriptor.dto';
33
import { BlockSettingDto } from 'src/app/main/dtos/config/block-instance.dto';
44
import { ConfigStackerComponent } from '../../config-stacker.component';
55

6+
enum ByteArrayViewMode {
7+
Base64 = 'base64',
8+
Hex = 'hex',
9+
}
10+
611
@Component({
712
selector: 'app-byte-array-setting',
813
templateUrl: './byte-array-setting.component.html',
914
styleUrls: ['./byte-array-setting.component.scss'],
1015
})
11-
export class ByteArraySettingComponent {
16+
export class ByteArraySettingComponent implements OnChanges {
1217
@Input() parameter: BlockParameterDto | null = null;
1318
@Input() setting!: BlockSettingDto;
1419
@Input() stacker!: ConfigStackerComponent;
1520
@Output() onChange: EventEmitter<void> = new EventEmitter<void>();
1621

1722
SettingInputMode = SettingInputMode;
23+
ByteArrayViewMode = ByteArrayViewMode;
24+
25+
viewMode = ByteArrayViewMode.Base64;
26+
displayValue = '';
27+
isValid = true;
28+
isTouched = false;
29+
30+
private lastBase64Value = '';
31+
32+
ngOnChanges(changes: SimpleChanges): void {
33+
if (!changes['setting'] && this.getBase64Value() === this.lastBase64Value) {
34+
return;
35+
}
36+
37+
this.syncDisplayValue();
38+
}
1839

1940
changeMode(mode: SettingInputMode) {
2041
this.setting.inputMode = mode;
2142
this.onChange.emit();
2243
}
2344

24-
// Given a base64 string, sets the value of the setting.
25-
// biome-ignore lint/suspicious/noExplicitAny: This function is only called with events from the input element.
26-
setValue(event: any) {
27-
// Make sure the input is a valid base64 string.
28-
if (
29-
event === null ||
30-
event === undefined ||
31-
!/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(event)
32-
) {
45+
changeViewMode(mode: ByteArrayViewMode) {
46+
if (this.viewMode === mode) {
3347
return;
3448
}
3549

36-
this.setting.value = event;
37-
this.valueChanged();
50+
this.viewMode = mode;
51+
this.syncDisplayValue();
3852
}
3953

40-
valueChanged() {
54+
valueChanging(event: Event) {
55+
this.displayValue = (event.target as HTMLInputElement).value;
56+
this.applyDisplayValue();
57+
this.isTouched = true;
58+
}
59+
60+
inputChanged(event: Event) {
61+
this.displayValue = (event.target as HTMLInputElement).value;
62+
this.applyDisplayValue();
63+
this.isTouched = true;
64+
}
65+
66+
emitValueChanged() {
4167
this.onChange.emit();
4268
}
69+
70+
computeClass(): string {
71+
let finalClass = 'input-small w-100 monospace';
72+
73+
if (this.isTouched) {
74+
finalClass += this.isValid ? ' input-valid' : ' input-invalid';
75+
}
76+
77+
return finalClass;
78+
}
79+
80+
private applyDisplayValue() {
81+
const base64Value = this.tryConvertToBase64(this.displayValue);
82+
this.isValid = base64Value !== null;
83+
84+
if (base64Value === null) {
85+
return;
86+
}
87+
88+
this.setting.value = base64Value;
89+
this.lastBase64Value = base64Value;
90+
this.emitValueChanged();
91+
}
92+
93+
private syncDisplayValue() {
94+
const base64Value = this.getBase64Value();
95+
this.lastBase64Value = base64Value;
96+
this.displayValue = this.viewMode === ByteArrayViewMode.Hex
97+
? this.base64ToHex(base64Value)
98+
: base64Value;
99+
this.isValid = true;
100+
this.isTouched = false;
101+
}
102+
103+
private getBase64Value(): string {
104+
return typeof this.setting?.value === 'string' ? this.setting.value : '';
105+
}
106+
107+
private tryConvertToBase64(value: string): string | null {
108+
if (this.viewMode === ByteArrayViewMode.Hex) {
109+
return this.tryHexToBase64(value);
110+
}
111+
112+
return this.isValidBase64(value) ? value : null;
113+
}
114+
115+
private isValidBase64(value: string): boolean {
116+
return /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(value);
117+
}
118+
119+
private tryHexToBase64(value: string): string | null {
120+
const normalized = value.replace(/\s/g, '').replace(/0x/g, '');
121+
122+
if (!/^([0-9a-fA-F]{2})*$/.test(normalized)) {
123+
return null;
124+
}
125+
126+
const bytes = [];
127+
for (let i = 0; i < normalized.length; i += 2) {
128+
bytes.push(String.fromCharCode(Number.parseInt(normalized.substring(i, i + 2), 16)));
129+
}
130+
131+
return btoa(bytes.join(''));
132+
}
133+
134+
private base64ToHex(value: string): string {
135+
if (!this.isValidBase64(value)) {
136+
return '';
137+
}
138+
139+
return Array.from(atob(value), (char) => char.charCodeAt(0).toString(16).padStart(2, '0')).join('');
140+
}
43141
}

0 commit comments

Comments
 (0)