Electron Adventures: Episode 63: Hex Editor Data Decoding
Tomasz Wegrzanowski
Posted on September 26, 2021
Our hex editor displays data now, so now it's time for additional functionality:
- loading files (for now just static
sample.bin
one) - tracking what user wants to see by mouseover
- displaying data decoding in the table
Disable Svelte accessibility warnings
But before we do that, there's one very overdue thing. Svelte comes with builtin linter, and most of its rules like unused CSS rules, or unused properties, make perfect sense.
But it also comes with accessibility warnings, all tuned to ridiculously high level, and giving completely wrong advice 90% of the time. Wrong as in "it crashes the browser if you do that" (this one got fixed after I reported it, but they tend to ignore any issues below browser crashing).
I ran out of patience for this nonsense. There's no switch to disable that, but we can edit rollup.config.js
:
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
compilerOptions: {
// enable run-time checks when not in production
dev: !production
},
onwarn: (warning, handler) => {
if (warning.code.slice(0,4) === 'a11y') return
handler(warning)
}
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
Adding this four line onwarn
handler will disable all accessibility warnings, and greatly improve your quality of life. Unfortunately VSCode Svelte plugin doesn't read rollup.config.js
so it will still underline them, and the only way to ignore them there is one by one. But hey - every little thing helps.
preload.js
I generated sample.bin
and put in it the repo. Now we can load it with preload.js
and expose it in the browser:
let fs = require("fs")
let { contextBridge } = require("electron")
let data = fs.readFileSync(`${__dirname}/sample.bin`)
contextBridge.exposeInMainWorld(
"api", { data }
)
Buffer
vs Uint8Array
Unfortunately we run into an issue with how Electron works. fs.readFileSync
(as well as await fs.readFile
etc.) returns a Buffer
object. Buffer
is a subclass of Uint8Array
with some extra functionality that we definitely need.
So easy enough, hand it over to the browser with contextBridge.exposeInMainWorld
... And that doesn't work. All that data gets serialized and deserialized, and for some inexplicable reason every other type just works, but Buffer
gets magically converted to Uint8Array
.
To get Buffer
on the browser side, we need to npm install buffer
, and convert that file we read, from Uint8Array
back to Buffer
.
As a minor aside, the API is really inconsistent between capitalizing things Uint
vs UInt
.
src/App.svelte
Right, let's get started. First we need to convert that Buffer
, and add custom event changeoffset
handler so we can be told which byte is being mouseovered.
Notice the extra slash in import {Buffer} from "buffer/"
. This is necessary due to some conflict between node-side Buffer
and browser-side Buffer
.
<script>
import {Buffer} from "buffer/"
import MainView from "./MainView.svelte"
import Decodings from "./Decodings.svelte"
import StatusBar from "./StatusBar.svelte"
let data = Buffer.from(window.api.data)
let offset = 0
</script>
<div class="editor">
<MainView {data} on:changeoffset={e => offset = e.detail}/>
<Decodings {data} {offset} />
<StatusBar {offset} />
</div>
<svelte:head>
<title>fancy-data.bin</title>
</svelte:head>
src/HexGroup.svelte
We need to modify HexGroup
component to tell us which element is being mouseovered.
This was the component which was generating completely incorrect accessibility warnings, which got me to finally shut them all up.
Svelte custom events are a bit verbose, so alternatively we could use a store, or store+context for this. Whichever solution we'd end up with, all of them require some amount of boilerplate.
<script>
import { printf } from "fast-printf"
import { createEventDispatcher } from "svelte"
let dispatch = createEventDispatcher()
export let data
export let offset
</script>
<td class="hex">
<span on:mouseover={() => dispatch("changeoffset", offset)}>
{data[0] !== undefined ? printf("%02x", data[0]) : ""}
</span>
<span on:mouseover={() => dispatch("changeoffset", offset+1)}>
{data[1] !== undefined ? printf("%02x", data[1]) : ""}
</span>
<span on:mouseover={() => dispatch("changeoffset", offset+2)}>
{data[2] !== undefined ? printf("%02x", data[2]) : ""}
</span>
<span on:mouseover={() => dispatch("changeoffset", offset+3)}>
{data[3] !== undefined ? printf("%02x", data[3]) : ""}
</span>
</td>
src/Slice.svelte
This component needs two changes. First we need to tell the HexGroup
what is its offset, and that makes sense.
Second, and that's needless boilerplate, Svelte requires us to explicitly list every custom event we want to bubble up, so some pointless on:changeoffset
boilerplate.
<script>
import { printf } from "fast-printf"
import HexGroup from "./HexGroup.svelte"
import AsciiSlice from "./AsciiSlice.svelte"
export let offset
export let data
</script>
<tr>
<td class="offset">{printf("%06d", offset)}</td>
<HexGroup data={data.slice(0, 4)} on:changeoffset offset={offset} />
<HexGroup data={data.slice(4, 8)} on:changeoffset offset={offset+4} />
<HexGroup data={data.slice(8, 12)} on:changeoffset offset={offset+8} />
<HexGroup data={data.slice(12, 16)} on:changeoffset offset={offset+12} />
<AsciiSlice {data} />
</tr>
<style>
tr:nth-child(even) {
background-color: #555;
}
</style>
src/MainView.svelte
Again, we need to declare every event we bubble, so pointless on:changeoffset
boilerplate goes here too.
<script>
import Slice from "./Slice.svelte"
export let data
let slices
$: {
slices = []
for (let i=0; i<data.length; i+=16) {
slices.push({
offset: i,
data: data.slice(i, i+16),
})
}
}
</script>
<div class="main">
<table>
{#each slices as slice}
<Slice {...slice} on:changeoffset />
{/each}
</table>
</div>
<style>
.main {
flex: 1 1 auto;
overflow-y: auto;
}
table {
width: 100%;
}
</style>
src/Decodings.svelte
Now that we got all the data, we need to display its decodings. As this episode was already getting fairly long, I removed string and RGB decodings, and only kept various fix width numbers.
The template and styling are very straightforward:
<table>
<tr><th>Type</th><th>Value</th></tr>
<tr><td>Int8</td><td>{int8}</td></tr>
<tr><td>UInt8</td><td>{uint8}</td></tr>
<tr><td>Int16</td><td>{int16}</td></tr>
<tr><td>UInt16</td><td>{uint16}</td></tr>
<tr><td>Int32</td><td>{int32}</td></tr>
<tr><td>UInt32</td><td>{uint32}</td></tr>
<tr><td>Int64</td><td>{int64}</td></tr>
<tr><td>UInt64</td><td>{uint64}</td></tr>
<tr><td>Float32</td><td>{float32}</td></tr>
<tr><td>Float64</td><td>{float64}</td></tr>
</table>
<style>
table {
margin-top: 8px;
}
th {
text-align: left;
}
tr:nth-child(even) {
background-color: #555;
}
</style>
For decoding themselves, Buffer
class provides us with everything we'll need. If we didn't have that, for signed and unsigned 8/16/32 bit integers it would be easy enough to do them on our own. 64 bit numbers need JavaScript BigInt
, as 64-bit numbers are too big to fit in normal JavaScript numbers. Doing float decoding on our own would be a bit more tricky, but not too crazy.
Buffer
methods take offset you want to convert at. If you try to convert near the end where there's not enough data left over, you'll get an ERR_OUT_OF_RANGE
exception. That would break the view, so we track bytesAvailable
and only call those methods if you know there's enough data.
<script>
export let data
export let offset
let int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64
$: bytesAvailable = data.length - offset
$: {
int8 = data.readInt8(offset)
uint8 = data.readUInt8(offset)
if (bytesAvailable >= 2) {
int16 = data.readInt16LE(offset)
uint16 = data.readUInt16LE(offset)
} else {
int16 = ""
uint16 = ""
}
if (bytesAvailable >= 4) {
int32 = data.readInt32LE(offset)
uint32 = data.readUInt32LE(offset)
float32 = data.readFloatLE(offset)
} else {
int32 = ""
uint32 = ""
float32 = ""
}
if (bytesAvailable >= 8) {
int64 = data.readBigInt64LE(offset)
uint64 = data.readBigUInt64LE(offset)
float64 = data.readDoubleLE(offset)
} else {
int64 = ""
uint64 = ""
float64 = ""
}
}
</script>
Another thing we could do here is add some thousands separator as 32-bit and 64-bit numbers can be very difficult to read if they're just long strings of numbers.
Result
Here's the results:
In the next episode, we'll make the hex editor load files.
As usual, all the code for the episode is here.
Posted on September 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.