#!/bin/sh
# Check that the paywall's base64url encoder agrees with the daemon's.
#
# The paivana ID is "<expiration>-<base64url(sha256(...))>".  The daemon
# builds it with GNUNET_STRINGS_base64url_encode(); the browser rebuilds
# it in paywall.js to recognise the payment it just made.  If the two
# spell base64url differently the ID never matches, the payment appears
# not to go through, and neither side reports anything wrong -- so this
# compares them directly, on vectors the daemon itself generates.
#
# It is worth having because the two have already diverged twice: once
# on the decode side (the daemon emits the RFC 4648 section 5 alphabet,
# atob() only understands section 4) and once on the encode side (the
# browser used Uint8Array.prototype.toBase64, which older engines do not
# have).  Both broke the payment path.
#
# Exits 77 ("skipped") when there is no node to run the JavaScript in.
#
# Environment:
#   SRCDIR    directory holding paywall.js (default: this script's ../frontend)
#   BUILDDIR  directory holding base64url_vectors (default: $PWD)

set -eu

srcdir=${SRCDIR:-$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)}
builddir=${BUILDDIR:-$PWD}
frontend="$srcdir/../frontend"

if ! command -v node >/dev/null 2>&1; then
  echo "no node found, skipping the base64url cross-check" >&2
  exit 77
fi
if [ ! -x "$builddir/base64url_vectors" ]; then
  echo "base64url_vectors not built at $builddir" >&2
  exit 1
fi
if [ ! -f "$frontend/paywall.js" ]; then
  echo "paywall.js not found at $frontend" >&2
  exit 1
fi

"$builddir/base64url_vectors" > "$builddir/base64url_vectors.txt"

PAYWALL_JS="$frontend/paywall.js" \
VECTORS="$builddir/base64url_vectors.txt" \
node -e '
const fs = require("fs");

// Lift base64url() out of paywall.js rather than reimplementing it:
// a copy here would be a second implementation to keep in step, and
// this test exists precisely because keeping implementations in step
// by hand is what failed.  The module cannot simply be imported --
// it reads window.location at load -- so the one function is cut out
// by matching braces from its declaration.
const src = fs.readFileSync(process.env.PAYWALL_JS, "utf8");
const start = src.indexOf("function base64url(");
if (start < 0) {
  console.error("could not find base64url() in paywall.js;" +
                " if it was renamed, update this test rather than delete it");
  process.exit(1);
}
let depth = 0, end = -1, seen = false;
for (let i = src.indexOf("{", start); i < src.length; i++) {
  if (src[i] === "{") { depth++; seen = true; }
  else if (src[i] === "}") { depth--; }
  if (seen && depth === 0) { end = i + 1; break; }
}
if (end < 0) {
  console.error("could not delimit base64url() in paywall.js");
  process.exit(1);
}
const base64url = new Function(
  `${src.slice(start, end)}; return base64url;`)();

const lines = fs.readFileSync(process.env.VECTORS, "utf8")
      .split("\n").filter((l) => l.length > 0);
if (lines.length < 300) {
  console.error(`only ${lines.length} vectors; expected the generator` +
                " to produce hundreds -- refusing to pass vacuously");
  process.exit(1);
}
let bad = 0;
for (const line of lines) {
  const sp = line.indexOf(" ");
  const hex = line.slice(0, sp);
  const want = line.slice(sp + 1);
  const bytes = hex === "-"
        ? new Uint8Array(0)
        : Uint8Array.from(hex.match(/../g).map((h) => parseInt(h, 16)));
  const got = base64url(bytes);
  if (got !== want) {
    if (bad < 5) {
      console.error(`MISMATCH for ${hex}: paywall.js gave "${got}",` +
                    ` the daemon gives "${want}"`);
    }
    bad++;
  }
}
if (bad > 0) {
  console.error(`${bad} of ${lines.length} vectors disagree`);
  process.exit(1);
}
console.log(`base64url agrees with the daemon on ${lines.length} vectors`);
'
