chromium/third_party/blink/web_tests/wpt_internal/aggregation-service/support/aggregation-service.js

const NULL_CONTRIBUTION = Object.freeze({
  bucket: encodeBigInt(0n, 16),
  value: encodeBigInt(0n, 4),
  id: encodeBigInt(0n, 1),
});

/**
 * Returns a frozen payload object suitable for comparison with an object
 * returned by `CborParser.parse()`. The returned payload's `data` field is a
 * shallow copy of the `contributions` array, padded with `nullContribution`
 * until there are `padToNumContributions` elements.
 */
function buildExpectedPayload(
    contributions, padToNumContributions,
    nullContribution = NULL_CONTRIBUTION) {
  assert_less_than_equal(
      contributions.length, padToNumContributions,
      `Must have fewer than ${padToNumContributions} contributions`);

  const numNulls = padToNumContributions - contributions.length;
  const contributionsPadded =
      [...contributions].concat(Array(numNulls).fill(nullContribution));

  return Object.freeze({
    operation: 'histogram',
    data: contributionsPadded,
  });
}

/**
 * Asserts that the given payloads are equal by value. Provides more legible
 * error messages than `assert_object_equals()`.
 */
function assert_payload_equals(actualPayload, expectedPayload) {
  assert_equals(
      actualPayload.operation, expectedPayload.operation,
      'Payloads should have the same operation.');
  assert_equals(
      actualPayload.data.length, expectedPayload.data.length,
      'Payloads should have the same number of contributions.');

  assert_equals(
      Object.keys(actualPayload).length, 2,
      `actualPayload has the wrong number of keys: ${
          Object.keys(actualPayload)}`);
  assert_equals(
      Object.keys(expectedPayload).length, 2,
      `expectedPayload has the wrong number of keys: ${
          Object.keys(expectedPayload)}`);

  for (let i = 0; i < actualPayload.data.length; ++i) {
    const actualContribution = actualPayload.data[i];
    const expectedContribution = expectedPayload.data[i];

    assert_equals(
        Object.keys(actualContribution).length, 3,
        `Contribution at index ${i} has the wrong number of keys: ${
            Object.keys(actualContribution)}`);

    assert_equals(
        Object.keys(expectedContribution).length, 3,
        `Expected contribution at index ${i} has the wrong number of keys: ${
            Object.keys(expectedContribution)}`);

    for (let key of ['bucket', 'value', 'id']) {
      const actual = actualContribution[key];
      const expected = expectedContribution[key];
      assert_array_equals(
          actual, expected,
          `Contribution at index ${i} should have expected ${key}.`);
    }
  }
}

/**
 * Returns a `Uint8Array` of `size` bytes containing the big-endian
 * representation of the `BigInt` parameter `num`. Asserts that `num` can be
 * represented in `size` bytes.
 */
function encodeBigInt(num, size) {
  assert_equals(num.constructor.name, 'BigInt');
  const numOriginal = num;

  const array = Array(size).fill(0);
  for (let i = 0; i < size; ++i) {
    array[size - i - 1] = Number(num % 256n);
    num >>= 8n;
  }
  assert_equals(
      num, 0n,
      `encodeBigInt must encode ${numOriginal} in ${
          size} bytes with a remainder of zero`);
  return Uint8Array.from(array);
}

/**
 * Returns a `Uint8Array` containing the base64-decoded bytes of `data_base64`.
 * Throws an exception when the input is not valid base64.
 */
function decodeBase64ToUint8Array(data_base64) {
  // In JavaScript, strings are sequences of UTF-16 code units. The `atob()`
  // function returns returns a string where each code unit lies between 0
  // and 255 inclusive. Thus, filling a `Uint8Array` by mapping over the
  // code units with `charCodeAt(0)` will not invoke any text encoding
  // malarkey or produce any out-of-range values. While `TextEncoder`
  // sometimes does what we want, it *will* fail on values > 0x7f, which are
  // non-ASCII characters.
  return Uint8Array.from(atob(data_base64), c => c.charCodeAt(0));
}

class CborParser {
  /**
   * Returns a JavaScript object parsed from `data_base64`, which should be a
   * base64-encoded string containing CBOR-encoded data. Asserts that the input
   * is well-formed and is completely understood by the parser. This is an
   * ad-hoc parser; it knows just enough about CBOR to parse payloads destined
   * for the Aggregation Service.
   */
  static parse(data_base64) {
    const bytes = decodeBase64ToUint8Array(data_base64);
    const parser = new CborParser(bytes);
    const object = parser.#parseCbor();
    assert_equals(
        parser.#bytes.length, 0,
        'CborParser did not consume trailing bytes:\n' +
            CborParser.#hexdump(parser.#bytes));
    return object;
  }

  #bytes

  constructor(bytes) {
    assert_equals(bytes.constructor.name, 'Uint8Array');
    this.#bytes = bytes;
  }

  // Reads the "initial byte" from `this.data` and recursively parses the data
  // item it indicates. This is based on the RFC8949 definition of CBOR, which
  // is fully backwards compatible with RFC7049.
  //
  // For reference, see RFC8949's Appendix B: Jump Table For Initial Byte:
  // https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.b
  #parseCbor() {
    const [b] = this.#consume(1);

    if (0x40 <= b && b <= 0x57) {  // Parse a byte string.
      return this.#consume(b - 0x40);
    }

    if (0x60 <= b && b <= 0x77) {  // Parse a UTF-8 string.
      const str = this.#consume(b - 0x60);
      return (new TextDecoder()).decode(str);
    }

    if (0x80 <= b && b <= 0x97) {
      return this.#parseCborArray(b - 0x80);
    }
    if (b === 0x98) {
      return this.#parseCborArray(this.#parseUnsignedNum(1));
    }
    if (b === 0x99) {
      return this.#parseCborArray(this.#parseUnsignedNum(2));
    }
    if (b === 0x9a) {
      return this.#parseCborArray(this.#parseUnsignedNum(4));
    }

    if (0xa0 <= b && b <= 0xb7) {
      return this.#parseCborMap(b - 0xa0);
    }

    assert_unreached(`Unsupported initial byte 0x${b.toString(16)}`);
  }

  #parseUnsignedNum(len) {
    const bytes = this.#consume(len);
    return bytes.reduce((acc, byte) => acc << 8 | byte, 0);
  }

  #parseCborArray(len) {
    const out = [];
    for (let i = 0; i < len; ++i) {
      out.push(this.#parseCbor());
    }
    return out;
  }

  #parseCborMap(len) {
    const out = {};
    for (let i = 0; i < len; ++i) {
      const key = this.#parseCbor();
      const value = this.#parseCbor();
      out[key] = value;
    }
    return out;
  }

  #consume(len) {
    assert_less_than_equal(len, this.#bytes.length);
    const prefix = this.#bytes.slice(0, len);
    this.#bytes = this.#bytes.slice(len);
    return prefix;
  }

  static #hexdump(bytes) {
    assert_equals(bytes.constructor.name, 'Uint8Array');
    const WIDTH = 40;
    let dump = '';
    for (let i = 0; i < bytes.length; i += WIDTH) {
      const chunk = bytes.slice(i, i + WIDTH);
      const line = chunk.reduce(
          (acc, byte) => acc + byte.toString(16).padStart(2, '0'), '');
      dump += line + '\n';
    }
    return dump;
  }
}