feat(new tool): Hex Converter

Convert hex buffer to (un)signed integer/float
Fix #1447
This commit is contained in:
sharevb 2025-03-09 21:18:13 +01:00 committed by ShareVB
parent 08d977b8cd
commit 71faa2bb0c
11 changed files with 7085 additions and 8471 deletions

11
components.d.ts vendored
View file

@ -83,6 +83,7 @@ declare module '@vue/runtime-core' {
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default'] GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default'] 'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default'] HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
HexConverter: typeof import('./src/tools/hex-converter/hex-converter.vue')['default']
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default'] HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
'Home.page': typeof import('./src/pages/Home.page.vue')['default'] 'Home.page': typeof import('./src/pages/Home.page.vue')['default']
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
@ -131,18 +132,24 @@ declare module '@vue/runtime-core' {
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCheckbox: typeof import('naive-ui')['NCheckbox'] NCheckbox: typeof import('naive-ui')['NCheckbox']
NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NFormItem: typeof import('naive-ui')['NFormItem']
NH1: typeof import('naive-ui')['NH1'] NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout'] NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu'] NMenu: typeof import('naive-ui')['NMenu']
NP: typeof import('naive-ui')['NP']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NTable: typeof import('naive-ui')['NTable'] NSwitch: typeof import('naive-ui')['NSwitch']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']

View file

@ -53,6 +53,7 @@
"@vueuse/head": "^1.0.0", "@vueuse/head": "^1.0.0",
"@vueuse/router": "^10.0.0", "@vueuse/router": "^10.0.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"byte-data": "^19.0.1",
"change-case": "^4.1.2", "change-case": "^4.1.2",
"colord": "^2.9.3", "colord": "^2.9.3",
"composerize-ts": "^0.6.2", "composerize-ts": "^0.6.2",
@ -67,6 +68,7 @@
"figlet": "^1.7.0", "figlet": "^1.7.0",
"figue": "^1.2.0", "figue": "^1.2.0",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"hex-array": "^1.0.0",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"iarna-toml-esm": "^3.0.5", "iarna-toml-esm": "^3.0.5",
"ibantools": "^4.3.3", "ibantools": "^4.3.3",
@ -115,6 +117,7 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/hex-array": "^1.0.2",
"@types/jsdom": "^21.0.0", "@types/jsdom": "^21.0.0",
"@types/lodash": "^4.14.192", "@types/lodash": "^4.14.192",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",

14776
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -128,7 +128,7 @@ function activateOption(option: PaletteOption) {
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable /> <c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
<div v-for="(options, category) in filteredSearchResult" :key="category"> <div v-for="(options, category) in filteredSearchResult" :key="category">
<div ml-3 mt-3 text-sm font-bold text-primary op-60> <div ml-3 mt-3 text-sm text-primary font-bold op-60>
{{ category }} {{ category }}
</div> </div>
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" /> <command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />

View file

@ -0,0 +1,250 @@
import { describe, expect, it } from 'vitest';
import { cleanHex, decodeNumber, decodeStruct, encodeStruct, getCoderFromTypeName, parseNumber } from './hex-converter.service';
describe('cleanHex', () => {
it('should remove 0x and \\x prefixes from a hex string', () => {
expect(cleanHex('0x1234')).toBe('1234');
expect(cleanHex('\\x1234')).toBe('1234');
expect(cleanHex('1234')).toBe('1234');
});
});
describe('decodeNumber', () => {
it('should convert a number to binary', () => {
expect(decodeNumber(10, 8, 'bin')).toBe('00001010');
});
it('should convert a number to hex', () => {
expect(decodeNumber(10, 8, 'hex')).toBe('0a');
});
it('should convert a number to character', () => {
expect(decodeNumber(65, 8, 'char')).toBe('A');
});
it('should return the number itself for decimal conversion', () => {
expect(decodeNumber(10, 8, 'dec')).toBe(10);
});
});
describe('parseNumber', () => {
it('should parse hexadecimal input correctly', () => {
expect(parseNumber('0x1F')).toBe(31);
});
it('should parse binary input correctly', () => {
expect(parseNumber('0b1101')).toBe(13);
});
it('should parse string input correctly into code points', () => {
expect(parseNumber('abc')).toEqual([97, 98, 99]);
});
it('should return number when input is already a number', () => {
expect(parseNumber(42)).toBe(42);
});
it('should return 0 for falsy input', () => {
expect(parseNumber('')).toBe(0);
});
});
describe('getCoderFromTypeName', () => {
it('should return correct coder options for a basic integer type', () => {
const result = getCoderFromTypeName('uint16');
expect(result.type.bits).toBe(16);
expect(result.type.signed).toBe(false);
expect(result.size).toBe(1);
});
it('should handle big-endian types correctly', () => {
const result = getCoderFromTypeName('int32be');
expect(result.type.bits).toBe(32);
expect(result.type.be).toBe(true);
});
it('should return correct coder options for a float type', () => {
const result = getCoderFromTypeName('float');
expect(result.type.bits).toBe(32);
expect(result.type.fp).toBe(true);
});
it('should return correct coder options for a character type', () => {
const result = getCoderFromTypeName('char[4]');
expect(result.type.bits).toBe(8);
expect(result.size).toBe(4);
expect(result.formatter(65, 8)).toBe('A');
});
});
describe('decodeStruct', () => {
it('should decode a flat struct correctly from a hex array', () => {
const struct = {
field1: 'uint16',
field2: 'char[3]',
};
const hexArray = new Uint8Array([0x01, 0x0, 0x61, 0x62, 0x63]);
const result = decodeStruct({ struct, hexArray });
expect(result.field1).toBe(1);
expect(result.field2).toBe('abc');
});
it('should decode a nested struct correctly', () => {
const struct = {
type: 'int8',
header: {
version: 'uint8',
type: 'uint16be',
},
payload: {
value: 'floatbe',
},
};
const hexArray = new Uint8Array([0xFF, 0x01, 0x00, 0x02, 0x3F, 0x80, 0x0, 0x0]); // float 1.0
const result = decodeStruct({ struct, hexArray });
expect(result).toEqual({
type: -1,
header: { version: 1, type: 2 },
payload: { value: 1.0 },
});
});
it('should throw on bad buffer length', () => {
const struct = {
type: 'int8',
header: {
version: 'uint8',
type: 'uint16be',
},
payload: {
value: 'floatbe',
},
};
const hexArray = new Uint8Array([0xFF, 0x01, 0x00, 0x02, 0x3F, 0x80]); // missing last two bytes
expect(() => decodeStruct({ struct, hexArray })).toThrowError(
'Bad buffer length reading value(floatbe) at offset 4',
);
});
it('should throw an error when decoding an unsupported array type', () => {
const struct = {
list: ['uint8'],
};
const hexArray = new Uint8Array([0x01, 0x02, 0x03]);
expect(() => decodeStruct({ struct, hexArray })).toThrowError(
'Cannot decode a struct with array',
);
});
it('should throw an error when decoding an unsized array type', () => {
const struct = {
list: 'uint8[]',
};
const hexArray = new Uint8Array([0x01, 0x02, 0x03]);
expect(() => decodeStruct({ struct, hexArray })).toThrowError(
'Unsupported unsized array: uint8[]',
);
});
});
describe('encodeStruct', () => {
it('should encode a flat struct correctly', () => {
const struct = {
field1: 'uint8',
field2: 'char[3]',
};
const jsonObject = {
field1: 1,
field2: 'abc',
};
const result = encodeStruct({ struct, jsonObject });
expect(result).toEqual(new Uint8Array([0x01, 0x61, 0x62, 0x63]));
});
it('should encode a nested struct correctly', () => {
const struct = {
type: 'int8',
header: {
version: 'uint8',
type: 'uint16be',
},
payload: {
value: 'floatbe',
},
};
const jsonObject = {
type: -1,
header: { version: 1, type: 2 },
payload: { value: 1.0 },
};
const result = encodeStruct({ struct, jsonObject });
expect(result).toEqual(new Uint8Array([0xFF, 0x01, 0x00, 0x02, 0x3F, 0x80, 0x0, 0x0]));
});
it('should throw an error if array size is incorrect', () => {
const struct = {
field1: 'uint8',
field2: 'char[3]',
};
const jsonObject = {
field1: 1,
field2: 'abcd', // Too long
};
expect(() => encodeStruct({ struct, jsonObject })).toThrowError(
'Unexpected array size \'field2\'=\'97,98,99,100\' expected 3 elements',
);
});
it('should throw an error if value type is incorrect', () => {
const struct = {
field1: 'uint8',
field2: 'char[3]',
};
const jsonObject = {
field1: 1,
field2: 123, // Invalid type
};
expect(() => encodeStruct({ struct, jsonObject })).toThrowError(
'Unexpected non array \'field2\'=\'123\'',
);
});
it('should throw an error if value type is incorrect', () => {
const struct = {
field1: 'uint8',
field2: 'int16',
};
const jsonObject = {
field1: 1,
field2: '123', // Invalid type
};
expect(() => encodeStruct({ struct, jsonObject })).toThrowError(
'Unexpected array size \'field2\'=\'49,50,51\' expected 1 elements',
);
});
it('should encode multiple integer types correctly', () => {
const struct = {
a: 'uint8',
b: 'uint16',
c: 'uint32',
};
const jsonObject = {
a: 1,
b: 513,
c: 67305985,
};
const result = encodeStruct({ struct, jsonObject });
expect(result).toEqual(new Uint8Array([0x01, 0x01, 0x02, 0x01, 0x02, 0x03, 0x04]));
});
});

View file

@ -0,0 +1,193 @@
import { pack, unpack } from 'byte-data';
export type Conversion = 'dec' | 'bin' | 'hex' | 'char';
export function cleanHex(hex: string): string {
return hex.replace(/\\x|0x/g, '');
}
export function decodeNumber(n: number, bits: number, conv: Conversion) {
if (conv === 'bin') {
return n.toString(2).padStart(bits, '0');
}
if (conv === 'hex') {
return n.toString(16).padStart(bits / 4, '0');
}
if (conv === 'char') {
return String.fromCodePoint(n);
}
return n;
}
export function parseNumber(input: string | number): number | number[] {
if (!input) {
return 0;
}
if (typeof input === 'number') {
return input;
}
if (/^0x[0-9a-fA-F]+$/.test(input)) {
return Number.parseInt(input.substring(2), 16); // Parse as hexadecimal
}
else if (/^0b[01]+$/.test(input)) {
return Number.parseInt(input.substring(2), 2); // Parse as binary
}
return [...input].map(c => c.codePointAt(0) || 0);
}
function mergeModelAndObject(model: Record<string, any>, object: Record<string, any>): Record<string, any> {
const merged: Record<string, any> = {};
for (const key in model) {
if (Object.prototype.hasOwnProperty.call(model, key)) {
if (Array.isArray(object[key])) {
merged[key] = [model[key], object[key].map(parseNumber)];
}
else if (typeof object[key] === 'object' && !Array.isArray(object[key])) {
merged[key] = mergeModelAndObject(model[key], object[key]);
}
else {
merged[key] = [model[key], parseNumber(object[key])];
}
}
}
return merged;
}
interface CoderOption {
type: {
bits: number
fp?: boolean
be?: boolean
signed?: boolean
}
size: number
formatter: (n: number, bits: number) => string | number
join?: boolean
};
export function getCoderFromTypeName(typeName: string): CoderOption {
if (typeName.includes('[]')) {
throw new Error(`Unsupported unsized array: ${typeName}`);
}
const [, prefix, baseTypeName, bigEndian, arraySize] = /^((?:0x|0b)?)(u?int\d+|w?char|half|float|double)(be)?(?:\[(\d+)\])?$/.exec(typeName) || [];
let conv = 'dec';
if (prefix === '0x') {
conv = 'hex';
}
if (prefix === '0b') {
conv = 'bin';
}
const arraySizeNumber = Number.isNaN(Number(arraySize)) ? 1 : Number(arraySize);
if (baseTypeName === 'char' || baseTypeName === 'wchar') {
return {
type: {
bits: baseTypeName === 'char' ? 8 : 16,
be: !!bigEndian,
},
size: arraySizeNumber,
formatter: n => String.fromCodePoint(n),
join: true,
};
}
if (baseTypeName === 'float' || baseTypeName === 'double' || baseTypeName === 'half') {
return {
type: {
bits: baseTypeName === 'float' ? 32 : (baseTypeName === 'double' ? 64 : 16),
be: !!bigEndian,
fp: true,
},
size: arraySizeNumber,
formatter: n => n,
};
}
const [, unsigned, bits] = /^(u?)int(\d+)$/.exec(baseTypeName || '') || [];
return {
type: {
bits: Number(bits),
be: !!bigEndian,
signed: !unsigned,
},
size: arraySizeNumber,
formatter: (n, bits) => decodeNumber(n, bits, conv as Conversion),
};
}
export function decodeStruct({ struct, hexArray }: { struct: object; hexArray: Uint8Array }) {
let offset = 0;
const readMember = (obj: any) => {
const result: Record<string, any> = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (Array.isArray(obj[key])) {
throw new TypeError(`Cannot decode a struct with array (key=${key}). Must be expressed as string with fixed length`);
}
else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
result[key] = readMember(obj[key]);
}
else {
const coderOption = getCoderFromTypeName(obj[key]);
const arr = [];
for (let i = 0; i < coderOption.size; i++) {
const dataSize = Math.ceil(coderOption.type.bits / 8);
if (offset + dataSize > hexArray.length) {
throw new Error(`Bad buffer length reading ${key}(${obj[key]}) at offset ${offset}`);
}
arr.push(coderOption.formatter(unpack(hexArray, coderOption.type, offset), coderOption.type.bits));
offset += dataSize;
}
if (coderOption.join) {
result[key] = arr.join('');
}
else {
result[key] = coderOption.size > 1 ? arr : arr[0];
}
}
}
}
return result;
};
return readMember(struct);
}
export function encodeStruct({ struct, jsonObject }: { struct: object; jsonObject: object }): Uint8Array {
const mergedObject = mergeModelAndObject(struct, jsonObject);
let buffer: Array<number> = [];
const writeMember = (obj: any) => {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
writeMember(obj[key]);
}
else if (Array.isArray(obj[key])) {
const [typeName, value] = obj[key];
const coderOption = getCoderFromTypeName(typeName);
if (coderOption.size > 1 && !Array.isArray(value)) {
throw new TypeError(`Unexpected non array '${key}'='${value}'`);
}
if (Array.isArray(value) && value.length !== coderOption.size) {
throw new TypeError(`Unexpected array size '${key}'='${value}' expected ${coderOption.size} elements`);
}
const valueArr = !Array.isArray(value) ? [value] : value;
for (let i = 0; i < coderOption.size; i++) {
buffer = [...buffer, ...pack(valueArr[i], coderOption.type)];
}
}
else {
throw new TypeError(`Unexpected '${key}'='${obj[key]}'`);
}
}
}
};
writeMember(mergedObject);
return new Uint8Array(buffer);
}

View file

@ -0,0 +1,303 @@
<script setup lang="ts">
import hexArray from 'hex-array';
import { packArray, packString, unpackArray, unpackString } from 'byte-data';
import JSON5 from 'json5';
import { type Conversion, cleanHex, decodeNumber, decodeStruct, encodeStruct } from './hex-converter.service';
import { useValidation } from '@/composable/validation';
const mode = ref<'simple' | 'struct'>('simple');
const bits = ref(32);
const floatingPoint = ref(false);
const signed = ref(false);
const bigEndian = ref(false);
const decodeAs = ref<'dec' | 'bin' | 'hexa' | 'char' | 'utf8'>('dec');
const uppercase = ref(false);
const grouping = ref(1);
const rowlength = ref(0);
const hexInput = ref('');
const decodedOutput = computed(() => {
try {
const buffer = hexArray.fromString(cleanHex(hexInput.value));
if (decodeAs.value === 'utf8') {
return unpackString(buffer);
}
else {
return unpackArray(buffer, {
bits: bits.value,
fp: floatingPoint.value,
signed: signed.value,
be: bigEndian.value,
}, 0, buffer.length, true).map(n => decodeNumber(n, bits.value, decodeAs.value as Conversion)).join(' ');
}
}
catch (e: any) {
return e.toString();
}
});
const numberInput = ref('');
const encodedOutput = computed(() => {
try {
const values = numberInput.value.split(/\s+/).map(Number);
return hexArray.toString(
new Uint8Array(
packArray(values, {
bits: bits.value,
fp: floatingPoint.value,
signed: signed.value,
be: bigEndian.value,
})),
{
uppercase: uppercase.value,
grouping: grouping.value,
rowlength: rowlength.value,
},
);
}
catch (e: any) {
return e.toString();
}
});
const stringInput = ref('');
const utf8Output = computed(() => {
try {
return hexArray.toString(
new Uint8Array(packString(stringInput.value)),
{
uppercase: uppercase.value,
grouping: grouping.value,
rowlength: rowlength.value,
},
);
}
catch (e: any) {
return e.toString();
}
});
const structDefinition = ref(`{
x: "int32",
y: "int32",
}`);
const structDefinitionValidation = useValidation({
source: structDefinition,
rules: [
{
message: 'Struct definition is not a valid JSON',
validator: value => JSON5.parse(value.trim()),
},
],
});
const hexStructInput = ref('');
const decodedStructOutput = computed(() => {
try {
return JSON.stringify(
decodeStruct({
struct: JSON5.parse(structDefinition.value),
hexArray: hexArray.fromString(cleanHex(hexStructInput.value)),
}),
null, 2);
}
catch (e: any) {
return e.toString();
}
});
const jsonStructInput = ref('');
const encodedStructOutput = computed(() => {
try {
return hexArray.toString(
encodeStruct({
struct: JSON5.parse(structDefinition.value),
jsonObject: JSON5.parse(jsonStructInput.value),
}), {
uppercase: uppercase.value,
grouping: grouping.value,
rowlength: rowlength.value,
});
}
catch (e: any) {
return e.toString();
}
});
</script>
<template>
<div>
<n-radio-group v-model:value="mode" name="radiogroup" mb-2 flex justify-center>
<n-space>
<n-radio
value="simple"
label="Simple Encoder/Decoder"
/>
<n-radio
value="struct"
label="C/C++ typed struct"
/>
</n-space>
</n-radio-group>
<div v-if="mode === 'simple'">
<c-card title="Hex Options" mb-1>
<c-select
v-model:value="decodeAs"
label="Decode/Encode As:"
label-position="left" mb-1
:options="[{ value: 'dec', label: 'Decimal' }, { value: 'bin', label: 'Binary' }, { value: 'hex', label: 'Hexadecimal' }, { value: 'char', label: 'Char/ASCII' }, { value: 'utf8', label: 'UTF8 string' }]"
/>
<n-space v-if="decodeAs !== 'utf8'" align="baseline" justify="center">
<n-form-item label="Bits:" label-placement="left">
<n-input-number v-model:value="bits" :min="1" style="width: 6em" />
</n-form-item>
<n-form-item>
<n-checkbox v-model:checked="floatingPoint">
Floating Point
</n-checkbox>
</n-form-item>
<n-form-item>
<n-checkbox v-model:checked="signed">
Signed
</n-checkbox>
</n-form-item>
<n-form-item>
<n-checkbox v-model:checked="bigEndian">
Big Endian
</n-checkbox>
</n-form-item>
</n-space>
</c-card>
<c-card title="Hex Data Decoder" mb-3>
<c-input-text
v-model:value="hexInput"
multiline
placeholder="Put your Hex data here..."
rows="2"
label="Hex Data to decode"
raw-text
mb-5
/>
<n-form-item label="Your decoded values:">
<textarea-copyable :value="decodedOutput" />
</n-form-item>
</c-card>
<c-card v-if="decodeAs !== 'utf8'" title="Hex Data Encoder" mt-3>
<c-input-text
v-model:value="numberInput"
multiline
placeholder="Put your Numbers array here..."
rows="2"
label="Numbers array to encode"
raw-text
mb-5
/>
<n-form-item label="Your encoded numbers array as Hex:">
<textarea-copyable :value="encodedOutput" />
</n-form-item>
</c-card>
<c-card v-if="decodeAs === 'utf8'" title="Hex UTF8 String Encoder" mt-3>
<c-input-text
v-model:value="stringInput"
multiline
placeholder="Put your text here..."
rows="5"
label="String to encode"
raw-text
mb-5
/>
<n-form-item label="Your encoded string as UTF8 Hex:">
<textarea-copyable :value="utf8Output" />
</n-form-item>
</c-card>
<c-card title="Hex Encoding Output" mt-1>
<n-space align="baseline" justify="center">
<n-form-item label="Uppercase" label-placement="left">
<n-switch v-model:value="uppercase" />
</n-form-item>
<n-form-item label="Group by" label-placement="left">
<n-input-number v-model:value="grouping" :min="0" style="width: 6em" mr-1 /> digits (0 = no grouping)
</n-form-item>
<n-form-item label="Split as rows by" label-placement="left">
<n-input-number v-model:value="rowlength" :min="0" style="width: 6em" mr-1 /> group of digits (0 = no rows)
</n-form-item>
</n-space>
</c-card>
</div>
<div v-if="mode === 'struct'">
<c-card title="Struct Definition">
<c-input-text
v-model:value="structDefinition"
multiline
placeholder="Put your Struct defintion here..."
rows="5"
label="C/C+ like struct definition"
raw-text
mb-5
:validation="structDefinitionValidation"
/>
<details>
<summary>Instructions</summary>
<n-p>
Define you struct definition in JSON format: keys = struct member names ; value = type
<br>
Types syntax: u?int{size}(be)? | float(be)? | double(be)? | char | wchar(be)? | &lt;type&gt;[{array size}]
<br>
where "u" means "unsigned" ; "be" means "Big Endian" ; {size} is number of bits ; {array size} fixed size for arrays
<br>
can prefix integer with 0x or 0b to display as hex and binary
</n-p>
</details>
</c-card>
<c-card title="Hex Struct Decoder" m-t-1>
<c-input-text
v-model:value="hexStructInput"
multiline
placeholder="Put your Hex data here..."
rows="5"
label="Hex Data to decode"
raw-text
mb-5
/>
<n-form-item label="Your decoded values:">
<textarea-copyable :value="decodedStructOutput" />
</n-form-item>
</c-card>
<c-card title="Hex Struct Encoder" m-t-1>
<c-input-text
v-model:value="jsonStructInput"
multiline
placeholder="Put your Struct to encode here..."
rows="5"
label="Struct json to encode"
raw-text
mb-5
/>
<n-form-item label="Your encoded struct as Hex:">
<textarea-copyable :value="encodedStructOutput" />
</n-form-item>
</c-card>
<c-card title="Hex Encoding Output" mt-1>
<n-space align="baseline" justify="center">
<n-form-item label="Uppercase" label-placement="left">
<n-switch v-model:value="uppercase" />
</n-form-item>
<n-form-item label="Group by" label-placement="left">
<n-input-number v-model:value="grouping" :min="0" style="width: 6em" mr-1 /> digits (0 = no grouping)
</n-form-item>
<n-form-item label="Split as rows by" label-placement="left">
<n-input-number v-model:value="rowlength" :min="0" style="width: 6em" mr-1 /> group of digits (0 = no rows)
</n-form-item>
</n-space>
</c-card>
</div>
</div>
</template>

View file

@ -0,0 +1,12 @@
import { Binary } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Hex Encoder/Decoder',
path: '/hex-converter',
description: 'Encode and decode Hex buffers to number (bits, endianess, sign or floating point or chars) and structures',
keywords: ['hex', 'encode', 'decode', 'endianess', 'float', 'bits', 'hex', 'struct'],
component: () => import('./hex-converter.vue'),
icon: Binary,
createdAt: new Date('2025-02-09'),
});

View file

@ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter'; import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator'; import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as emailNormalizer } from './email-normalizer'; import { tool as emailNormalizer } from './email-normalizer';
import { tool as hexConverter } from './hex-converter';
import { tool as asciiTextDrawer } from './ascii-text-drawer'; import { tool as asciiTextDrawer } from './ascii-text-drawer';
@ -98,6 +99,7 @@ export const toolsByCategory: ToolCategory[] = [
components: [ components: [
dateTimeConverter, dateTimeConverter,
baseConverter, baseConverter,
hexConverter,
romanNumeralConverter, romanNumeralConverter,
base64StringConverter, base64StringConverter,
base64FileConverter, base64FileConverter,

View file

@ -151,7 +151,7 @@ function onSearchInput() {
> >
<div flex-1 truncate> <div flex-1 truncate>
<slot name="displayed-value"> <slot name="displayed-value">
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput"> <input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full color-current lh-normal @input="onSearchInput">
<span v-else-if="selectedOption" lh-normal> <span v-else-if="selectedOption" lh-normal>
{{ selectedOption.label }} {{ selectedOption.label }}
</span> </span>

View file

@ -39,7 +39,7 @@ const headers = computed(() => {
<template> <template>
<div class="relative overflow-x-auto rounded"> <div class="relative overflow-x-auto rounded">
<table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description"> <table class="w-full border-collapse text-left text-sm text-gray-500 dark:text-gray-400" role="table" :aria-label="description">
<thead v-if="!hideHeaders" class="bg-#ffffff uppercase text-gray-700 dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5"> <thead v-if="!hideHeaders" class="bg-#ffffff text-gray-700 uppercase dark:bg-#333333 dark:text-gray-400" border-b="1px solid dark:transparent #efeff5">
<tr> <tr>
<th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs"> <th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs">
{{ header.label }} {{ header.label }}