<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Volker Schwaberow - Blog</title><description>Software engineering, C++, systems architecture, DevOps, retrocomputing, and Amiga emulation.</description><link>https://schwaberow.de/</link><language>en-us</language><item><title>Tightening the Bolts on npwg</title><link>https://schwaberow.de/blog/security-hardening-and-cleanup-in-npwg/</link><guid isPermaLink="true">https://schwaberow.de/blog/security-hardening-and-cleanup-in-npwg/</guid><description>A look inside the 0.5.2 release of npwg, walking through clipboard isolation, memory hygiene, hash pinning, and pruning unnecessary dependencies.</description><pubDate>Tue, 25 Aug 2026 00:00:00 GMT</pubDate><content:encoded>On the surface, writing a password generator feels straightforward. You take an operating system entropy source, map indices across a character set, and print the result. 

Over time, as a tool grows to support diceware passphrases, custom character templates, interactive console prompts, and clipboard helpers, small edge cases accumulate. Code that worked in early prototypes can leave subtle gaps in memory handling or process boundaries.

For version 0.5.2 of **[npwg](https://github.com/vschwaberow/npwg)**, I took a step back from adding new features and focused entirely on auditing the internals.

### What needed fixing

Going through the codebase with fresh eyes surfaced a few areas where practical security and reliability could be improved:

1. **Passing secrets through process environments**  
   On Linux, the background daemon responsible for keeping the clipboard alive after the CLI exits received the generated password via an environment variable. On multi-user systems, environment variables can be inspected through `/proc/$PID/environ`. In 0.5.2, secrets are passed exclusively through standard input (`stdin`). The daemon now holds the secret in memory for 45 seconds, wipes the clipboard, and exits cleanly.

2. **Blind trust on network downloads**  
   The Diceware module downloads the EFF large wordlist on demand. Previously, the file was accepted upon download without verification. The new loader checks the downloaded content against a pinned SHA-256 hash before writing anything to disk.

3. **Lingering buffers in memory**  
   Temporary strings used during password mutation, clipboard handling, and interactive menus remained in memory until standard garbage collection or OS reclaim. All sensitive buffers are now wrapped with the `zeroize` crate, zeroing out memory as soon as values go out of scope.

4. **Multi-byte character slicing**  
   Certain mutation and strength-scoring routines indexed raw UTF-8 bytes instead of Unicode character boundaries. When users mutated passwords containing non-ASCII characters, this could trigger panics or produce skewed entropy scores. The generator now indexes characters consistently.

### Cutting the dependency fat

A security utility should stay lean. Over several releases, the dependency tree had accumulated crates that were either experimental or superseded by standard library improvements.

In this release, I removed five crates entirely: `chacha20`, `dashmap`, `regex`, `futures`, and `rand_distr`. I also trimmed unused feature flags from `tokio`. This reduced compile times, cut down the binary footprint, and narrowed the supply-chain surface.

Continuous integration now runs `cargo fmt`, `cargo clippy`, and unit test suites on every pull request across Linux, Windows, and macOS.

### Trying the update

You can update or install `npwg` directly through Cargo:

```bash
cargo install npwg --locked
```

Prebuilt binaries for Linux, Windows, and macOS are also available on the [GitHub Releases page](https://github.com/vschwaberow/npwg/releases).</content:encoded></item><item><title>Cycle Counting Habits That Still Apply in C and C++</title><link>https://schwaberow.de/blog/cycle-counting-habits-from-6502-to-cpp/</link><guid isPermaLink="true">https://schwaberow.de/blog/cycle-counting-habits-from-6502-to-cpp/</guid><description>Zero-page thinking on the 6502 maps to recognizing invariants and memory cost in C and C++.</description><pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate><content:encoded>On the 6502 you sit there and count cycles. Absolute load costs more than zero-page. I still look at C and C++ the same way. Where does the value live, and how often do I touch it?

## Zero-page on the 6502

Zero-page is just the first 256 bytes (`$0000` to `$00FF`). Modes that address it are usually shorter and cheaper than the absolute ones. `LDA` loads the accumulator, `STA` stores it. `LDA zp` is 2 bytes and 3 cycles. `LDA abs` is 3 bytes and 4 cycles. Same story for `STA`.

```asm
LDA $1234    ; absolute, 4 cycles
STA $2000    ; absolute, 4 cycles

LDA $34      ; zero-page, 3 cycles
STA $20      ; zero-page, 3 cycles
```

If something sits in a hot loop, you put it in zero-page and leave it there. Reloading it from a far address every trip around the loop is waste. The 6502 barely has registers, so people treated zero-page like an overflow register file.

## The same idea in C++

Today a register is usually the cheap place. Writing the value as a local is how I say that in source. Whether it actually stays in a register is up to the compiler. Spill it, and you are back to a memory load.

What I run into more often is chasing pointers through a loop, especially when one load waits on the last one or the data thrashes the cache.

```cpp
for (const auto&amp; entry : entries) {
  process(context.config-&gt;scale * entry.value);
}
```

Here `context.config-&gt;scale` does not depend on `entry`. If nothing in the loop changes it, pull it out.

```cpp
const auto scale = context.config-&gt;scale;
for (const auto&amp; entry : entries) {
  process(scale * entry.value);
}
```

Under `-O2`, GCC may already move the load out of the loop. Sometimes both versions compile to exactly the same thing.

The interesting case is when it cannot prove the invariant. Maybe `process()` writes through some alias that touches `context.config-&gt;scale`. Then the compiler has to keep the load inside the loop. Pull `scale` out yourself and the cost can drop, but only if that invariant is real. Guess wrong, and you changed what the program means.

## How to check

I look at the assembly. [Compiler Explorer](https://godbolt.org/) or `objdump -drC` under `-O2` or `-O3` is enough to see how many loads sit in the hot loop. Demangled names help when C++ gets noisy. With `perf stat` I compare cycles, instructions, and the cache events that matter on that CPU. Branch misses rarely tell me much here, because the control flow did not change.

I only bother with tiny rewrites on code I have actually timed. Spot the invariant, spot the access cost, then check whether the compiler already cleaned it up.

The 6502 made you think about memory because you had no choice. C++ makes it easier to forget. I still find the old habit useful: know what gets loaded, know how often, and check what the compiler actually emitted.</content:encoded></item><item><title>Contributing to Copperline, a Cycle-Driven Amiga Emulator in Rust</title><link>https://schwaberow.de/blog/contributions-to-copperline-amiga-emulator-in-rust/</link><guid isPermaLink="true">https://schwaberow.de/blog/contributions-to-copperline-amiga-emulator-in-rust/</guid><description>Notes from six merged Copperline patches covering WasmBoard faults, filesystem caching, video timing, debugger behavior, disk labels, and M68000 disassembly.</description><pubDate>Sun, 23 Aug 2026 00:00:00 GMT</pubDate><content:encoded>I spent the past few weeks contributing to [Copperline](https://github.com/CopperlineHQ/Copperline), a cycle-driven Commodore Amiga emulator written in Rust.

Copperline interests me as another implementation alongside FS-UAE and vAmiga. I do not follow &quot;rewrite it in Rust&quot; as a rule.

FS-UAE and vAmiga solve the same problem in C and C++, and their code has gone through years of development and use.

Working on Copperline lets me explore a different design without dismissing the work that already exists.

The work ranged from a looping WebAssembly board to the missing G in the *Chambers of Shaolin* intro. Along the way, I touched six parts of the emulator.

### WasmBoard fault isolation and transactional DMA

Copperline runs Zorro expansion boards as WebAssembly modules. A looping guest exhausted its fuel and trapped, but Copperline called it again every 50 Hz vertical blank.

In one test, this cycle consumed 2.5 billion host CPU instructions per second. Emulation fell from 50 FPS to about 1 FPS.

[Pull request 451](https://github.com/CopperlineHQ/Copperline/pull/451) turns a trap into a permanent fault state. Copperline then removes the board&apos;s mappings, timers, and interrupt request lines.

I also staged guest DMA writes in a temporary buffer. Copperline commits them after a successful execution slice and discards them after a trap.

Savestates preserve the fault state. The host stays at 50 FPS, and an interrupted DMA transfer leaves Amiga memory unchanged.

### Two-level case-insensitive LRU path cache

AmigaDOS ignores letter case in paths. `resolve_path()` handled different host spelling by scanning directories with `read_dir()`.

Deep WHDLoad collections made it scan the same directories repeatedly. In [pull request 448](https://github.com/CopperlineHQ/Copperline/pull/448), I added two LRU caches.

One cache resolves individual components. The other caches complete paths. Both reuse allocated slots, so a hit does not allocate on the heap.

For 6,000 lookups, directory scans needed 15.31 ms. The caches needed 4.73 ms, which gave a 3.23× speedup on a warm host disk cache.

### Video raster centering for non-standard display windows

[Issue 465](https://github.com/CopperlineHQ/Copperline/issues/465) showed the *Chambers of Shaolin* intro shifted 89 pixels left. I traced that offset to `present_h_shift`.

In [pull request 467](https://github.com/CopperlineHQ/Copperline/pull/467), I replaced the dynamic calculation with the calibrated 24-pixel shift for standard frames.

The frame moved back into place, but part of the logo remained missing. Maintainer [Andrew Hutchings](https://github.com/LinuxJedi) pointed this out during review.

Andrew reopened the issue and wrote [pull request 468](https://github.com/CopperlineHQ/Copperline/pull/468). The remaining fault came from Denise&apos;s horizontal display-window flip-flop.

Its beam counter wraps after `$1C7`, but the intro sets a stop value of `$1D8`. The comparator never fires, so the window stays open.

Copperline modeled that state in `scan_h_window_line`, but its painter still began at `DIWSTRT`.

PR 468 extends the paint start on carried-open rows and subtracts that extension from `native_x_offset`. The fetched data keeps its beam position.

Andrew compared the result with A500 photos, vAmiga, and FS-UAE. The missing G in GRANDSLAM returned, and issue 465 closed.

Sprite clipping still uses the register-derived window. Copperline records that separate gap in `docs/internals/video.md`.

I had started with a centering bug. The second half turned out to be a timing bug.

### Debugger address masking and copper list alignment

The debugger only helps when it follows the machine that it observes. [Pull request 437](https://github.com/CopperlineHQ/Copperline/pull/437) fixed three places where it did not.

`last_writer` now applies the 24-bit or 32-bit bus mask for the selected Amiga. This removes phantom writes caused by address mirroring.

Copper dumps now start on a 16-bit word boundary. The command parser also accepts the `$` hexadecimal prefix and `fp` register alias.

### Latin1 volume label encoding

The volume-label fault came down to bytes. AmigaDOS stores a label as a BCPL string and expects Latin1, while host tools supplied UTF-8.

[Pull request 438](https://github.com/CopperlineHQ/Copperline/pull/438) converts the label before writing the root block. Workbench now displays German umlauts and accented letters correctly.

### M68000 disassembler refactoring

The M68000 disassembler repeated operand formatting across its integer ALU paths. [Pull request 454](https://github.com/CopperlineHQ/Copperline/pull/454) moved that logic into shared code.

The same code now handles `&lt;ea&gt;, Dn` and `Dn, &lt;ea&gt;`. Regression tests cover every addressing mode, so the refactor cannot silently change debugger output.

### Pull requests

The table lists my six contributions and Andrew Hutchings&apos; follow-up to the video fix.

| PR | Area | Change |
| :--- | :--- | :--- |
| [#437](https://github.com/CopperlineHQ/Copperline/pull/437) | Debugger | Bus masking, Copper alignment, and parser forms |
| [#438](https://github.com/CopperlineHQ/Copperline/pull/438) | Disk images | Latin1 encoding for BCPL volume labels |
| [#448](https://github.com/CopperlineHQ/Copperline/pull/448) | Filesystem | Two-level LRU path cache |
| [#451](https://github.com/CopperlineHQ/Copperline/pull/451) | WasmBoard | Fault isolation and transactional DMA |
| [#454](https://github.com/CopperlineHQ/Copperline/pull/454) | Disassembler | Shared ALU formatting and regression tests |
| [#467](https://github.com/CopperlineHQ/Copperline/pull/467) | Video | Fixed centering for standard frames |
| [#468](https://github.com/CopperlineHQ/Copperline/pull/468) | Video | Andrew Hutchings&apos; carried-open DIW fix |</content:encoded></item><item><title>Order from Chaos with wordlist_sort in C++26</title><link>https://schwaberow.de/blog/fast-wordlist-processing-with-cpp26/</link><guid isPermaLink="true">https://schwaberow.de/blog/fast-wordlist-processing-with-cpp26/</guid><description>Why I built wordlist_sort, how C++26 is used, and the practical challenges of maintaining a zero-dependency codebase.</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>Working with wordlists over the years usually means dealing with raw, unformatted text files. When aggregating lists from password audits, domain recon, or data dumps, the input files often contain duplicates, HTML tags, irregular whitespace, and hashes.

The traditional approach relies on standard shell pipelines:

```bash
cat list1.txt list2.txt | tr &apos;[:upper:]&apos; &apos;[:lower:]&apos; | grep -v &apos;^[0-9]&apos; | sort -u &gt; clean.txt
```

This works for smaller inputs. With multi-gigabyte files and tens of millions of entries, shell pipelines spend significant time on process boundaries, intermediate copies, and disk I/O.

On July 18, 2024, I started **[wordlist_sort](https://github.com/vschwaberow/wordlist_sort)** to handle transformations, filtering, and deduplication in a single tool directly in memory.

### What it does

The program takes an output destination and one or more input files, applies the selected transformations, and writes the sorted result.

For example, to convert entries to lowercase, remove leading whitespace, filter words longer than 16 characters, and deduplicate the result:

```bash
./build/word_sorter --maxlen 16 --lower --detab --deduplicate output.txt list1.txt list2.txt list3.txt
```

Available filter options include:

- **Hex hash stripping with `--hash-remove`**  
  Removes hexadecimal strings with 32 or more characters.

- **HTML stripping with `--dewebify`**  
  Removes HTML tags from scraped web text.

- **Email splitting with `--email-sort`**  
  Splits email addresses into user and domain tokens.

- **Character frequency filtering with `--dup-sense`**  
  Filters words where a single character exceeds a defined percentage threshold.

### C++26 implementation details

The project serves as an implementation test for modern C++26 features without external dependencies:

- **Standard I/O with `std::print`**  
  Formatted terminal output directly via the standard library without stream overhead.

- **Error handling with `std::expected`**  
  Explicit return types for error propagation instead of exceptions.

- **Memory views with `std::span` and `std::ranges`**  
  Direct in-place transformations over non-owning contiguous memory buffers.

- **Parallel file processing**  
  Input files are read into memory in bulk and processed concurrently via `std::async` tasks before merging into the main container using move operations.

### Challenges during development

Maintaining a clean build with the C++26 standard involved several specific adjustments:

1. **Compiler warnings on new standard features**  
   The build enforces `-Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror`. Combining strict conversion checks with new standard library algorithms required explicit type casts and careful index handling for buffer offsets.

2. **Error handling across threads**  
   Replacing exceptions with `std::expected` and `std::optional` required refactoring worker task return paths so that errors propagate as structured values.

3. **Replacing CLI11 with a zero-dependency C++26 parser**  
   Early versions used CLI11 for command-line parsing. While functional, it introduced external package management overhead, relied on exceptions for control flow, and performed heap allocations for storing string options. Modern C++26 provides the tools to write a compact parser without third-party code: `std::span` and `std::string_view` enable zero-copy argument inspection, `std::from_chars` parses integers without throwing exceptions, and `std::expected` handles input validation explicitly. Replacing the library reduced compile times, cut binary size, and left CMake with zero external dependencies.

On completion, the binary outputs word counts and elapsed runtime in milliseconds.

### Project repository

The source code is licensed under the MIT license and available on GitHub as [wordlist_sort](https://github.com/vschwaberow/wordlist_sort).</content:encoded></item><item><title>Reducing Format Time in vAmiga Hard Drive Images</title><link>https://schwaberow.de/blog/reducing-format-time-in-vamiga-hard-drive-images/</link><guid isPermaLink="true">https://schwaberow.de/blog/reducing-format-time-in-vamiga-hard-drive-images/</guid><description>Replacing per-block bitmap updates with bulk fill operations in FileSystem::format. On 524288 blocks, format time drops from 205 ms to 0.25 ms.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>I have been contributing to the [vAmiga project](https://dirkwhoffmann.github.io/vAmiga/) for a while. Previous work includes an MCP server for the Amiga debugger and an ImGui port of the UI, including the MCP server integration. The ImGui branch lives in [vAmigaImgui](https://github.com/vschwaberow/vAmigaImgui).

While reading the vAmiga source, I found a `TODO: SPEED THIS UP` comment in `FileSystem::format`. The existing code checked every potential block on a partition to mark free blocks individually. Each check ran `unordered_map` lookups and bit shifts on the allocation bitmap. On multi-gigabyte Amiga hard drive images, that means millions of calls and a slow format path. Dirk Hoffmann had noted the issue in source and deferred it to a later iteration.

The fix inverts the approach. Instead of probing block by block, the bitmap is initialized as entirely free with one bulk operation. Metadata blocks are marked used afterward.

The change has three steps:

1. **Bulk fill**
   Use `std::ranges::fill` to set free blocks to `0xFF` in the bitmap block payload.
2. **Boundary correction**
   Mark phantom bits outside the volume limits as used.
3. **Metadata blocks**
   Flag the root and bitmap blocks as allocated.

The per-block hash map loop is replaced by a single fill that the compiler lowers to tight machine code. Runtime no longer scales with partition size for this step; cost stays tied to metadata size.

On 524288 blocks, format time was about 205 milliseconds before the change and 0.25 milliseconds after. The patch is merged to the vAmiga dev branch.

Source and full benchmark numbers are in [vAmiga issue 979](https://github.com/dirkwhoffmann/vAmiga/issues/979).</content:encoded></item></channel></rss>