feat(new tool): Hex Converter
Convert hex buffer to (un)signed integer/float Fix #1447
This commit is contained in:
		
							parent
							
								
									08d977b8cd
								
							
						
					
					
						commit
						71faa2bb0c
					
				
							
								
								
									
										11
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -83,6 +83,7 @@ declare module '@vue/runtime-core' { | ||||
|     GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['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'] | ||||
|     HexConverter: typeof import('./src/tools/hex-converter/hex-converter.vue')['default'] | ||||
|     HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default'] | ||||
|     'Home.page': typeof import('./src/pages/Home.page.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'] | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||
|     NH1: typeof import('naive-ui')['NH1'] | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     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'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     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'] | ||||
|     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||
|  | ||||
| @ -53,6 +53,7 @@ | ||||
|     "@vueuse/head": "^1.0.0", | ||||
|     "@vueuse/router": "^10.0.0", | ||||
|     "bcryptjs": "^2.4.3", | ||||
|     "byte-data": "^19.0.1", | ||||
|     "change-case": "^4.1.2", | ||||
|     "colord": "^2.9.3", | ||||
|     "composerize-ts": "^0.6.2", | ||||
| @ -67,6 +68,7 @@ | ||||
|     "figlet": "^1.7.0", | ||||
|     "figue": "^1.2.0", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "hex-array": "^1.0.0", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "iarna-toml-esm": "^3.0.5", | ||||
|     "ibantools": "^4.3.3", | ||||
| @ -115,6 +117,7 @@ | ||||
|     "@types/bcryptjs": "^2.4.2", | ||||
|     "@types/crypto-js": "^4.1.1", | ||||
|     "@types/dompurify": "^3.0.5", | ||||
|     "@types/hex-array": "^1.0.2", | ||||
|     "@types/jsdom": "^21.0.0", | ||||
|     "@types/lodash": "^4.14.192", | ||||
|     "@types/mime-types": "^2.1.1", | ||||
|  | ||||
							
								
								
									
										14152
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14152
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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 /> | ||||
| 
 | ||||
|       <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 }} | ||||
|         </div> | ||||
|         <command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" /> | ||||
|  | ||||
							
								
								
									
										250
									
								
								src/tools/hex-converter/hex-converter.service.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								src/tools/hex-converter/hex-converter.service.test.ts
									
									
									
									
									
										Normal 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])); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										193
									
								
								src/tools/hex-converter/hex-converter.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/tools/hex-converter/hex-converter.service.ts
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
							
								
								
									
										303
									
								
								src/tools/hex-converter/hex-converter.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/tools/hex-converter/hex-converter.vue
									
									
									
									
									
										Normal 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)? | <type>[{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> | ||||
							
								
								
									
										12
									
								
								src/tools/hex-converter/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/hex-converter/index.ts
									
									
									
									
									
										Normal 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'), | ||||
| }); | ||||
| @ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; | ||||
| import { tool as base64StringConverter } from './base64-string-converter'; | ||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||
| import { tool as emailNormalizer } from './email-normalizer'; | ||||
| import { tool as hexConverter } from './hex-converter'; | ||||
| 
 | ||||
| import { tool as asciiTextDrawer } from './ascii-text-drawer'; | ||||
| 
 | ||||
| @ -98,6 +99,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|     components: [ | ||||
|       dateTimeConverter, | ||||
|       baseConverter, | ||||
|       hexConverter, | ||||
|       romanNumeralConverter, | ||||
|       base64StringConverter, | ||||
|       base64FileConverter, | ||||
|  | ||||
| @ -151,7 +151,7 @@ function onSearchInput() { | ||||
|       > | ||||
|         <div flex-1 truncate> | ||||
|           <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> | ||||
|               {{ selectedOption.label }} | ||||
|             </span> | ||||
|  | ||||
| @ -39,7 +39,7 @@ const headers = computed(() => { | ||||
| <template> | ||||
|   <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"> | ||||
|       <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> | ||||
|           <th v-for="header in headers" :key="header.key" scope="col" class="px-6 py-3 text-xs"> | ||||
|             {{ header.label }} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user