Electron Adventures: Episode 63: Hex Editor Data Decoding

taw

Tomasz Wegrzanowski

Posted on September 26, 2021

Electron Adventures: Episode 63: Hex Editor Data Decoding

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
    }
};
Enter fullscreen mode Exit fullscreen mode

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 }
)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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

As usual, all the code for the episode is here.

💖 💪 🙅 🚩
taw
Tomasz Wegrzanowski

Posted on September 26, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related