📄 src/md5.ts
/**
 * Minimal, dependency-free MD5 (RFC 1321).
 *
 * Only used to compute the media CDN "hotlink" hash (see hash.ts) — not for
 * anything security sensitive.
 */

const SHIFTS = new Uint8Array([
  7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
  20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6,
  10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
]);

const SINE_TABLE = new Uint32Array(64);
for (let i = 0; i < 64; i++) {
  SINE_TABLE[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32);
}

export function md5(message: string): string {
  const messageBytes = new TextEncoder().encode(message);
  const messageLength = messageBytes.length;

  // Padded message: data + 0x80 + zeros + 64-bit little-endian bit length.
  const paddedLength = (((messageLength + 8) >>> 6) + 1) << 6;
  const padded = new Uint8Array(paddedLength);
  padded.set(messageBytes);
  padded[messageLength] = 0x80;
  const view = new DataView(padded.buffer);
  view.setUint32(paddedLength - 8, messageLength << 3, true);
  view.setUint32(paddedLength - 4, Math.floor(messageLength / 2 ** 29), true);

  let a0 = 0x67452301;
  let b0 = 0xefcdab89;
  let c0 = 0x98badcfe;
  let d0 = 0x10325476;

  const words = new Uint32Array(16);
  for (let offset = 0; offset < paddedLength; offset += 64) {
    for (let i = 0; i < 16; i++) {
      words[i] = view.getUint32(offset + i * 4, true);
    }

    let a = a0;
    let b = b0;
    let c = c0;
    let d = d0;

    for (let i = 0; i < 64; i++) {
      let f: number;
      let g: number;
      if (i < 16) {
        f = (b & c) | (~b & d);
        g = i;
      } else if (i < 32) {
        f = (d & b) | (~d & c);
        g = (5 * i + 1) % 16;
      } else if (i < 48) {
        f = b ^ c ^ d;
        g = (3 * i + 5) % 16;
      } else {
        f = c ^ (b | ~d);
        g = (7 * i) % 16;
      }
      const temp = d;
      d = c;
      c = b;
      const sum = (f + a + SINE_TABLE[i]! + words[g]!) >>> 0;
      const shift = SHIFTS[i]!;
      b = (b + ((sum << shift) | (sum >>> (32 - shift)))) >>> 0;
      a = temp;
    }

    a0 = (a0 + a) >>> 0;
    b0 = (b0 + b) >>> 0;
    c0 = (c0 + c) >>> 0;
    d0 = (d0 + d) >>> 0;
  }

  const digest = new DataView(new ArrayBuffer(16));
  digest.setUint32(0, a0, true);
  digest.setUint32(4, b0, true);
  digest.setUint32(8, c0, true);
  digest.setUint32(12, d0, true);

  let hex = '';
  for (let i = 0; i < 16; i++) {
    hex += digest.getUint8(i).toString(16).padStart(2, '0');
  }
  return hex;
}