OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / test / strip-fuzz.test.js

160 lines · javascript ref:
// UTF-8 fuzzer for strip() — library.md §7 item 3 asks for this by name:
//
//   "Fuzz UTF-8 specifically. strip §8 names it: byte-vs-character offsets is 'the single most
//    likely defect here and produces facets that land on the wrong characters only for non-ASCII
//    authors.' Thirty-six hand-written vectors may never catch it; a fuzzer will."
//
// The oracle is not "does it look right" — it is EXACT: for a generated `[LABEL](url)`, slicing the
// output text by the facet's BYTE range must decode to exactly LABEL. A character-offset
// implementation passes every ASCII case and fails the moment one multi-byte character precedes a
// facet, which is why the hand-written vectors cannot be trusted alone here.
//
// Deterministic by seed. A fuzzer whose failures cannot be reproduced is a rumour, not a test, so
// the seed is printed and any failure reports the exact source that produced it.

import test from 'brittle'
import { strip } from '../lib/strip.js'

// xorshift32 — tiny, deterministic, dependency-free. Reproducibility is the point.
function rng (seed) {
  let s = seed >>> 0 || 1
  return () => {
    s ^= s << 13; s >>>= 0
    s ^= s >> 17
    s ^= s << 5; s >>>= 0
    return s / 0x100000000
  }
}

// Alphabet chosen to cover every UTF-8 width, plus the cases that break naive implementations.
const ALPHABET = [
  'a', 'Z', '7', ' ', '-',                    // 1 byte
  'é', 'ü', 'ß', 'ñ',                          // 2 bytes
  '日', '本', 'א', 'ه',                        // 3 bytes
  '🎉', '👩', '🌍', '🧀',                       // 4 bytes (surrogate pair in JS)
  'é',                                   // combining acute — 1 grapheme, 3 bytes, 2 code units
  '👩‍💻',                                // ZWJ sequence — 1 grapheme, 11 bytes
  ' '                                     // NBSP — whitespace that is NOT ASCII space
]

const pick = (r, xs) => xs[Math.floor(r() * xs.length) % xs.length]

function randomText (r, n) {
  let s = ''
  for (let i = 0; i < n; i++) s += pick(r, ALPHABET)
  return s
}

// A label that cannot contain markdown-significant characters, so the expected facet span is exact.
function randomLabel (r) {
  let s = randomText(r, 1 + Math.floor(r() * 6))
  s = s.replace(/[[\]()*_~`#|\\<>\n]/g, 'x').trim()
  return s.length ? s : 'x'
}

const byteLen = (s) => Buffer.byteLength(s, 'utf8')

// Does this byte index fall on a UTF-8 character boundary? A continuation byte is 0b10xxxxxx.
function onBoundary (buf, i) {
  if (i <= 0 || i >= buf.length) return true
  return (buf[i] & 0xc0) !== 0x80
}

test('fuzz: facet byte ranges decode to exactly the label they cover', (t) => {
  const SEED = 0x5eed1234
  const r = rng(SEED)
  t.comment('seed ' + SEED.toString(16) + ' — change it to explore, keep it to reproduce')

  for (let i = 0; i < 600; i++) {
    // A run of arbitrary UTF-8 BEFORE the facet is what shifts a char-offset implementation.
    const prefix = randomText(r, Math.floor(r() * 8))
    const label = randomLabel(r)
    const suffix = randomText(r, Math.floor(r() * 5))
    const url = 'https://example.com/' + (i % 97)
    const src = prefix + '[' + label + '](' + url + ')' + suffix

    const { text, facets } = strip(src)
    const buf = Buffer.from(text, 'utf8')

    if (facets.length !== 1) {
      t.fail('expected exactly one facet for ' + JSON.stringify(src) + ' — got ' + facets.length)
      continue
    }
    const { byteStart, byteEnd } = facets[0]

    // ★ THE ORACLE. Slice by BYTES and decode: it must be the label, exactly.
    const covered = buf.subarray(byteStart, byteEnd).toString('utf8')
    if (covered !== label) {
      t.fail('facet covers ' + JSON.stringify(covered) + ', expected ' + JSON.stringify(label) +
        '\n      source: ' + JSON.stringify(src) + '\n      text:   ' + JSON.stringify(text))
      continue
    }

    t.ok(onBoundary(buf, byteStart) && onBoundary(buf, byteEnd),
      'offsets land on UTF-8 character boundaries')
  }
})

test('fuzz: hashtag facets cover "#tag" including the hash (§3.7)', (t) => {
  const r = rng(0xbeef01)
  for (let i = 0; i < 300; i++) {
    const prefix = randomText(r, Math.floor(r() * 8))
    const tag = 'tag' + i
    const src = prefix + ' #' + tag + ' rest'

    const { text, facets } = strip(src)
    const buf = Buffer.from(text, 'utf8')
    const tagFacets = facets.filter((f) => f.type === 'tag')
    if (tagFacets.length !== 1) { t.fail('expected one tag facet for ' + JSON.stringify(src)); continue }

    const { byteStart, byteEnd } = tagFacets[0]
    t.is(buf.subarray(byteStart, byteEnd).toString('utf8'), '#' + tag,
      'the facet spans #tag; the VALUE drops the hash')
  }
})

test('fuzz: invariants hold on arbitrary markdown-ish input (§6, §2)', (t) => {
  const r = rng(0xc0ffee)
  const NOISE = ['**', '*', '~~', '`', '# ', '> ', '- ', '1. ', '|', '[[', ']]', ':::', '![', '](',
    '\n', '\n\n', '\n\n\n', '](https://x.com)', '[^1]', '\\\n', '   ', '---\n', '<b>', '$$']

  for (let i = 0; i < 800; i++) {
    let src = ''
    const parts = 1 + Math.floor(r() * 10)
    for (let p = 0; p < parts; p++) src += (r() < 0.5 ? randomText(r, 1 + Math.floor(r() * 5)) : pick(r, NOISE))

    let out
    try {
      out = strip(src)
    } catch (err) {
      t.fail('threw on ' + JSON.stringify(src) + ': ' + err.message)
      continue
    }

    const buf = Buffer.from(out.text, 'utf8')
    let prevEnd = -1
    let ok = true
    for (const f of out.facets) {
      const { byteStart, byteEnd } = f
      if (!(byteStart >= 0 && byteEnd <= buf.length)) { ok = false; t.fail('out of bounds: ' + JSON.stringify(src)); break }
      if (!(byteEnd > byteStart)) { ok = false; t.fail('empty facet not dropped (§6.3): ' + JSON.stringify(src)); break }
      if (byteStart < prevEnd) { ok = false; t.fail('overlapping facets (§6.2): ' + JSON.stringify(src)); break }
      if (!onBoundary(buf, byteStart) || !onBoundary(buf, byteEnd)) { ok = false; t.fail('offset mid-character: ' + JSON.stringify(src)); break }
      if (typeof f.type !== 'string' || !('value' in f)) { ok = false; t.fail('not a native facet (§6.5): ' + JSON.stringify(src)); break }
      prevEnd = byteEnd
    }
    t.ok(ok, 'invariants hold')
  }
})

test('fuzz: §5 normalisation is idempotent on its own output (§7 property law)', (t) => {
  const r = rng(0xd0d0)
  for (let i = 0; i < 300; i++) {
    const src = randomText(r, 1 + Math.floor(r() * 20)) + pick(r, ['\n\n\n', '   \n', '\n   ', '  '])
    const once = strip(src).text
    // Plain text is valid markdown, so stripping the output again must be a fixed point:
    // trailing whitespace is gone, newline runs are collapsed, the ends are trimmed.
    t.is(strip(once).text, once, 'strip(strip(x).text).text === strip(x).text')
  }
})