Technical Reference

For plugin developers, integrators, and the morbidly curious.

Contents

  1. Architectural overview
  2. File map
  3. Security model
  4. REST API
  5. Shortcode internals
  6. Option schema
  7. Hooks and filters
  8. Theming (CSS variables)
  9. Z-machine interpreter
  10. Opcode coverage
  11. Known limits
  12. Contributing

1. Architectural overview

Krebf is two pieces joined at a narrow waist:

The PHP layer never touches Z-machine bytecode. Its only job is to (a) keep untrusted file references from escaping the configured games directory, (b) optionally proxy external HTTP(S) fetches through WordPress's safe HTTP API, and (c) hand the player a per-request configuration object via wp_add_inline_script. Once the browser has the binary and the configuration, the interpreter runs entirely client-side — saves to localStorage, transcripts to Blob downloads, nothing more touches the server.

2. File map

krebf/
├── krebf.php                    — main plugin file (bootstrap, constants)
├── readme.txt                   — WordPress.org format
├── uninstall.php                — drops options on plugin removal
├── includes/
│   ├── class-krebf-security.php — validation, path checks, SSRF defence
│   ├── class-krebf-plugin.php   — singleton, asset registration
│   ├── class-krebf-shortcode.php— [krebf] renderer
│   ├── class-krebf-rest.php     — /list, /game, /fetch endpoints
│   └── class-krebf-admin.php    — Settings → Krebf page
├── templates/
│   └── player.php               — HTML for a single player instance
├── assets/
│   ├── js/krebf.js              — interpreter (IIFE, 87 KB)
│   └── css/krebf.css            — styles scoped to .krebf-root (15 KB)
└── games/
    ├── index.php                — silence (shipped; real runtime folder
    │                              is wp-content/uploads/krebf-games/)
    ├── .htaccess                — deny-all so Apache/LiteSpeed refuse
    │                              direct HTTP access to binaries
    └── README.txt               — in-repo note on the actual runtime path

3. Security model

Five lines of defence, applied in this order to every request that could touch a file or open a network connection:

A. Capability check for admin writes

The settings page is gated behind current_user_can('manage_options') and all four registered options go through register_setting()'s sanitisation callbacks, which the Settings API invokes after verifying the request's nonce — nothing custom to misconfigure.

B. Slug validation

User-supplied game identifiers pass through Krebf_Security::classify_game_param(). A slug must satisfy:

C. Path-traversal boundary

Every resolved path is canonicalised with realpath() and checked to sit under the canonicalised games directory and WP_CONTENT_DIR. Symlinks are refused outright. The check is in Krebf_Security::resolve_slug():

$real = realpath( $candidate );
$rdir = realpath( $games_dir );
if ( strpos( $real, $rdir ) !== 0 ) continue;
if ( is_link( $real ) ) continue;

D. SSRF defence on external fetches

For /fetch the plugin resolves the hostname itself with gethostbynamel(), then filters every returned IP through FILTER_VALIDATE_IP with FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE. If any resolved address is non-public the URL is refused before WordPress's HTTP API sees it, closing the DNS-rebinding-to-private-network SSRF.

E. Content validation

Served bytes — whether from a local file or an external fetch — have their first byte inspected. If it isn't 1–8 (a valid Z-machine version number), the response is a 422 Unprocessable Entity, not a download. This catches phishing sites that pretend to host .z3 but actually serve HTML.

4. REST API

All three endpoints live under the namespace krebf/v1.

GET /wp-json/krebf/v1/list

Returns the public game list. No authentication.

200 OK
{
  "games": [
    { "slug": "cloak.z5",  "title": "Cloak",  "size": 33822, "version": 5 },
    { "slug": "zork1.z3",  "title": "Zork 1", "size": 84640, "version": 3 }
  ]
}

GET /wp-json/krebf/v1/game?slug=SLUG

Serves the story file as application/octet-stream. No authentication — but the slug must resolve to an actual file in the games folder.

200 OK
Content-Type: application/octet-stream
Content-Length: 84640
Content-Disposition: inline; filename="zork1.z3"
X-Content-Type-Options: nosniff
Cache-Control: public, max-age=3600

<raw Z-code bytes>

Error responses:

GET /wp-json/krebf/v1/fetch?url=URL

Proxies an external URL through WordPress's HTTP API. Requires a valid X-WP-Nonce header (or _wpnonce query parameter) even for logged-out visitors; the shortcode injects this nonce into window.krebfInstances[id].nonce.

Error responses:

5. Shortcode internals

The shortcode builds one configuration object per instance, serialises it into an inline script, and enqueues the stylesheet + interpreter lazily (only when a page actually contains [krebf]). The shape of the config object handed to JavaScript:

window.krebfInstances["krebf-1"] = {
  restNamespace: "krebf/v1",
  restBase:      "https://example.com/wp-json/krebf/v1/",
  nonce:         "…wp_rest nonce…",
  pluginUrl:     "https://example.com/wp-content/plugins/krebf/",
  games:         [ { slug: "...", title: "...", size: N } , ... ],
  instanceId:    "krebf-1",
  initial:       { kind: "slug", slug: "zork1" } | { kind: "url", url: "…" } | null
};

Notice what is not in that object: the absolute filesystem path of the games directory, any server paths at all, any credentials. Even the optional admin allow-list for external URLs stays server-side — the client just tries, and the server rejects with 403 if disallowed.

6. Option schema

Option keyTypeDefaultPurpose
krebf_games_dirstring(empty) Absolute path to the games folder. Empty means "use uploads/krebf-games/". Must be inside WP_CONTENT_DIR.
krebf_allow_externalbool0 Whether ?game=https://... URLs are honoured.
krebf_external_hostsstring(empty) Newline-separated allow-list of hostnames. Empty = any public host.
krebf_default_gamestring(empty) Slug or HTTPS URL loaded when a [krebf] page is visited without explicit ?game= or attribute.

7. Hooks and filters

In addition to the three register_* and four register_setting calls, Krebf exposes no custom filters in 1.0.1. Extensibility lives in two directions:

8. Theming (CSS variables)

Every colour and border is driven by a CSS custom property on .krebf-root. Override them from your theme:

.krebf-root {
  --krebf-amber:       #ffb547;  /* primary accent             */
  --krebf-amber-bright:#ffd89a;  /* hover / focus              */
  --krebf-amber-dim:   #8a5a1f;  /* secondary text             */
  --krebf-amber-glow:  rgba(255, 181, 71, 0.35); /* text-shadow */
  --krebf-brass:       #c08a3e;
  --krebf-brass-dark:  #6b4a20;
  --krebf-bg-deep:     #0a0806;  /* outer frame                */
  --krebf-bg-panel:    #141210;  /* interior panels            */
  --krebf-bg-panel-2:  #1c1915;  /* raised surfaces            */
  --krebf-cream:       #f4e4c1;  /* main text                  */
  --krebf-rule:        rgba(255, 181, 71, 0.15); /* borders    */
  --krebf-red-led:     #ff5a3c;  /* transcript LED             */
  --krebf-green-led:   #5aff8c;  /* I/O LED                    */
}

The --krebf- prefix means these variables will never collide with your theme's own custom properties.

9. Z-machine interpreter

Direct port of Fredrik Ramsberg's Ruby interpreter (github.com/fredrikr/krebf); the code organisation follows the original closely for auditability. When debugging an interpreter quirk, the Ruby source is the authoritative reference.

State model

At the top of the IIFE live the module-level variables that mirror the Ruby $globals:

let z, dynmemBackup, zcodeVersion, pc, storyfileName, quit,
    screenObj, streams, stack,
    objectTable, baseDictionary, routineOffset, stringOffset,
    abbrevTable, globalBase, alphabet, defaultUnicode, finalUnicode,
    undoData, font, transcriptBit,
    args, instruction,
    rndA, rndB, rndC, rndX;   // PRNG (Ozmoo-compatible)

Classes

Main loop

async function runMainLoop() {
  quit = false;
  krebfSetLed('led-run', true);
  while (!quit) {
    instruction = readInstruction();                // parse
    const fn = opcodeRoutines[type][opcodeNumber];
    const result = fn();                            // dispatch
    if (result && result.then) await result;        // async I/O?
    // …transcript/header housekeeping…
    if (++stepsSinceYield >= 5000) {
      await krebfYield(); screenObj.render();
    }
  }
}

The yield every 5,000 instructions stops long scripts from locking up the browser. krebfYield() is a setTimeout(resolve, 0) promise, which returns control to the event loop between iterations.

PRNG

The random number generator is a deterministic port of Ozmoo's algorithm for exact compatibility. Seeded state lives in rndA/B/C/X and advances via the same four-word update Ruby performs. Verified against the reference implementation: seed 1 produces 200, 109, 74, 59, 246.

10. Opcode coverage

All five opcode tables from the Z-machine spec are implemented in full. Table cardinalities match the source:

TableEntriesCoverage
0OP15100% (rtrue, rfalse, print, print_ret, nop, save/restore, restart, ret_popped, pop/catch, quit, new_line, show_status, verify, piracy)
1OP16100% (jz, get_sibling, get_child, get_parent, get_prop_len, inc, dec, print_addr, call_1s, remove_obj, print_obj, ret, jump, print_paddr, load, not/call_1n)
2OP29100% (comparisons, jumps, get_prop/next_prop, arithmetic, load/store, clear_attr/set_attr/test_attr, insert_obj, loadw/loadb, put_prop, call_2s/2n, set_colour, throw)
VAR32100% (call_vs/vn, storew/storeb, read, print_char/num, random, push/pull, split/set_window, erase_window/line, sound_effect, read_char, tokenise, encode_text, copy_table, print_table, check_arg_count, …)
EXT14save_z5, restore_z5, log_shift, art_shift, set_font, save_undo, restore_undo, print_unicode, check_unicode (v5+ extensions).

Dispatch

The interpreter decodes form bytes (long/short/variable/extended) into { opcode_type, opcode_number, operand_values } and indexes directly into opcodeRoutines[type][number]. Unimplemented entries are null; hitting one calls fatalErr() with the opcode coordinates.

11. Known limits

12. Contributing

Bug reports, v6 patches, Blorb parser contributions, i18n translations, and theme variants all welcome.

Bugs that reproduce against the Ruby reference belong upstream; bugs specific to browser rendering, the WordPress plugin, or the JavaScript port belong here.

fin