~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
106 lines · 3.4 KB Raw History
/**
 * Manifest parsing: what a pushed manifest references, so the record can
 * keep those blobs and the server can refuse a manifest whose parts are
 * missing. The bytes themselves are stored untouched; a digest covers the
 * exact serialization.
 */

export const MEDIA_TYPES = {
  dockerManifest: 'application/vnd.docker.distribution.manifest.v2+json',
  dockerManifestList:
    'application/vnd.docker.distribution.manifest.list.v2+json',
  ociManifest: 'application/vnd.oci.image.manifest.v1+json',
  ociIndex: 'application/vnd.oci.image.index.v1+json',
};

const INDEX_TYPES = new Set([
  MEDIA_TYPES.dockerManifestList,
  MEDIA_TYPES.ociIndex,
]);

/** Largest manifest accepted, in bytes. Real manifests are a few KB. */
export const MAX_MANIFEST_SIZE = 4 * 1024 * 1024;

/**
 * @typedef {Object} ManifestDescriptor
 * @property {string} digest
 * @property {string} [mediaType]
 * @property {number} [size]
 * @property {string[]} [urls] - locations outside this registry; such a
 *   layer is not stored here and existence checks skip it
 */

/**
 * @typedef {Object} ParsedManifest
 * @property {string} mediaType - from the body, else the Content-Type header
 * @property {boolean} isIndex
 * @property {ManifestDescriptor[]} blobs - config and layers an image
 *   manifest references
 * @property {ManifestDescriptor[]} children - manifests an index references
 */

/**
 * @param {unknown} value
 * @returns {ManifestDescriptor|null}
 */
function asDescriptor(value) {
  if (!value || typeof value !== 'object') return null;
  const entry = /** @type {Record<string, unknown>} */ (value);
  if (typeof entry.digest !== 'string') return null;
  return {
    digest: entry.digest,
    mediaType:
      typeof entry.mediaType === 'string' ? entry.mediaType : undefined,
    size: typeof entry.size === 'number' ? entry.size : undefined,
    urls: Array.isArray(entry.urls)
      ? entry.urls.filter((u) => typeof u === 'string')
      : undefined,
  };
}

/**
 * @param {Uint8Array} bytes - manifest body, exactly as pushed
 * @param {string} contentType - Content-Type header from the push
 * @returns {ParsedManifest}
 */
export function parseManifest(bytes, contentType) {
  /** @type {Record<string, unknown>} */
  let body;
  try {
    body = JSON.parse(new TextDecoder().decode(bytes));
  } catch {
    throw new Error('manifest is not valid JSON');
  }
  if (!body || typeof body !== 'object' || Array.isArray(body)) {
    throw new Error('manifest is not a JSON object');
  }

  const declaredType =
    typeof body.mediaType === 'string' ? body.mediaType : null;
  const headerType = contentType.split(';')[0].trim();
  const mediaType = declaredType ?? headerType;
  if (declaredType && headerType && declaredType !== headerType) {
    throw new Error(
      `manifest mediaType ${declaredType} does not match Content-Type ${headerType}`,
    );
  }

  const isIndex = INDEX_TYPES.has(mediaType) || Array.isArray(body.manifests);

  if (isIndex) {
    const children = (Array.isArray(body.manifests) ? body.manifests : [])
      .map(asDescriptor)
      .filter((d) => d !== null);
    return { mediaType, isIndex, blobs: [], children };
  }

  /** @type {ManifestDescriptor[]} */
  const blobs = [];
  const config = asDescriptor(body.config);
  if (config) blobs.push(config);
  for (const layer of Array.isArray(body.layers) ? body.layers : []) {
    const descriptor = asDescriptor(layer);
    if (descriptor) blobs.push(descriptor);
  }
  return { mediaType, isIndex, blobs, children: [] };
}