Electron Adventures: Episode 63: Hex Editor Data Decoding
Tags: javascript, electron, svelte
Published: 2021-09-26 13:35:07

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`:

```javascript
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:

```javascript
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`.

```html
<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.

```html
<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.

```html
<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.

```html
<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:

```html
<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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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.

```html
<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:

![Episode 63 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jas3j4hnim8tt286r1sh.png)

In the next episode, we'll make the hex editor load files.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-63-hex-editor-data-analysis).