← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/incremental-export-validator.ts

15.2 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import type { SequenceWorkbenchPayloadDeclaration } from "./workbench-persistence-protocol";

const mediaTypeByFormat = {
  a3m: "text/x-a3m",
  "aligned-fasta": "text/x-fasta",
  bed: "text/x-bed",
  clustal: "text/x-clustal",
  csv: "text/csv",
  embl: "text/x-embl",
  fasta: "text/x-fasta",
  fastq: "text/x-fastq",
  genbank: "text/x-genbank",
  gff3: "text/x-gff3",
  gtf: "text/x-gtf",
  json: "application/json",
  newick: "text/x-newick",
  pdf: "application/pdf",
  stockholm: "text/x-stockholm",
  svg: "image/svg+xml",
  tsv: "text/tab-separated-values",
  vcf: "text/x-vcf",
} as const;

type ExportFormat = keyof typeof mediaTypeByFormat;
type JsonContext =
  | { state: "comma-or-end" | "value" | "value-or-end"; type: "array" }
  | {
      state: "colon" | "comma-or-end" | "key" | "key-or-end" | "value";
      type: "object";
    };

/**
 * Validates canonical UTF-8 and the format invariants used by workspace
 * publication without retaining the complete artifact. The retained prefix,
 * suffix and JSON nesting stack are deliberately bounded.
 */
export class IncrementalSequenceExportValidator {
  private readonly decoder = new TextDecoder("utf-8", { fatal: true });
  private readonly binaryDecoder = new TextDecoder("iso-8859-1");
  private firstNonWhitespace = "";
  private fastaAlignedLength?: number;
  private fastaCurrentSequenceLength = 0;
  private fastaRecordCount = 0;
  private fastqPhase: 0 | 1 | 3 = 0;
  private fastqQualityLength = 0;
  private fastqSequenceLength = 0;
  private jsonContexts: Array<JsonContext> = [];
  private jsonEscape = false;
  private jsonInString = false;
  private jsonRootState: "done" | "value" = "value";
  private jsonStringRole?: "key" | "value";
  private jsonToken?: { kind: "literal" | "number"; value: string };
  private jsonUnicodeDigits = 0;
  private retainedPeak = 0;
  private seenNonWhitespace = false;
  private suffix = "";
  private lineLength = 0;
  private linePrefix = "";
  private lineTouched = false;

  constructor(
    private readonly format: ExportFormat,
    mediaType: string,
  ) {
    if (mediaType !== mediaTypeByFormat[format]) {
      throw new Error(`The ${format} artifact media type is invalid.`);
    }
  }

  update(bytes: Uint8Array): void {
    this.consume(
      this.format === "pdf"
        ? this.binaryDecoder.decode(bytes, { stream: true })
        : this.decoder.decode(bytes, { stream: true }),
    );
  }

  finish(): void {
    this.consume(
      this.format === "pdf"
        ? this.binaryDecoder.decode()
        : this.decoder.decode(),
    );
    const trimmedPrefix = this.firstNonWhitespace;
    const trimmedSuffix = this.suffix.trimEnd();
    if (this.lineTouched) this.finishStructuredLine();
    if (this.format === "aligned-fasta") this.assertAlignedLength();
    if (!this.seenNonWhitespace && this.format !== "bed") {
      throw new Error("The exported artifact is empty.");
    }
    if (this.format === "json") {
      if (this.jsonToken != null) this.finishJsonToken();
      if (
        this.jsonInString ||
        this.jsonEscape ||
        this.jsonUnicodeDigits !== 0 ||
        this.jsonContexts.length !== 0 ||
        this.jsonRootState !== "done" ||
        this.jsonToken != null ||
        (!trimmedPrefix.startsWith("{") && !trimmedPrefix.startsWith("[")) ||
        !/[}\]]$/u.test(trimmedSuffix)
      ) {
        throw new Error("The exported JSON artifact is invalid.");
      }
    } else if (this.format === "fastq" && this.fastqPhase !== 0) {
      throw new Error(
        this.fastqPhase === 3
          ? "The exported FASTQ sequence and quality lengths do not match."
          : "The exported FASTQ artifact is incomplete.",
      );
    } else if (
      (this.format === "a3m" ||
        this.format === "fasta" ||
        this.format === "aligned-fasta") &&
      (this.fastaRecordCount === 0 || this.fastaCurrentSequenceLength === 0)
    ) {
      throw new Error("The exported FASTA artifact is invalid.");
    } else if (this.format === "fastq" && !trimmedPrefix.startsWith("@")) {
      throw new Error("The exported FASTQ artifact is invalid.");
    } else if (
      this.format === "clustal" &&
      !/^(?:CLUSTAL|MUSCLE)\b/u.test(trimmedPrefix)
    ) {
      throw new Error("The exported CLUSTAL alignment artifact is invalid.");
    } else if (
      this.format === "stockholm" &&
      (!trimmedPrefix.startsWith("# STOCKHOLM 1.0") ||
        !trimmedSuffix.endsWith("//"))
    ) {
      throw new Error("The exported Stockholm alignment artifact is invalid.");
    } else if (
      this.format === "gtf" &&
      (!trimmedPrefix.includes("\t") ||
        !/\bgene_id\s+"[^"]+"/u.test(trimmedPrefix))
    ) {
      throw new Error("The exported GTF annotation artifact is invalid.");
    } else if (
      this.format === "pdf" &&
      (!/^%PDF-\d\.\d/u.test(trimmedPrefix) ||
        !/startxref\s+\d+\s+%%EOF\s*$/u.test(trimmedSuffix))
    ) {
      throw new Error("The exported PDF artifact is invalid.");
    } else if (
      this.format === "genbank" &&
      (!trimmedPrefix.startsWith("LOCUS") || !trimmedSuffix.endsWith("//"))
    ) {
      throw new Error("The exported GenBank artifact is invalid.");
    } else if (
      this.format === "embl" &&
      (!trimmedPrefix.startsWith("ID") || !trimmedSuffix.endsWith("//"))
    ) {
      throw new Error("The exported EMBL artifact is invalid.");
    } else if (
      this.format === "gff3" &&
      !trimmedPrefix.startsWith("##gff-version 3")
    ) {
      throw new Error("The exported GFF3 artifact is invalid.");
    } else if (
      this.format === "svg" &&
      (!/^<svg[\s>]/u.test(trimmedPrefix) || !trimmedSuffix.endsWith("</svg>"))
    ) {
      throw new Error("The exported SVG artifact is invalid.");
    } else if (this.format === "newick" && !trimmedSuffix.endsWith(";")) {
      throw new Error("The exported Newick artifact is invalid.");
    } else if (
      this.format === "vcf" &&
      !trimmedPrefix.startsWith("##fileformat=VCF")
    ) {
      throw new Error("The exported VCF artifact is invalid.");
    }
  }

  get peakRetainedBytes(): number {
    return this.retainedPeak;
  }

  private consume(text: string): void {
    if (text.length === 0) return;
    if (this.firstNonWhitespace.length < 256) {
      const candidate = `${this.firstNonWhitespace}${text}`.trimStart();
      this.firstNonWhitespace = candidate.slice(0, 256);
    }
    if (/\S/u.test(text)) this.seenNonWhitespace = true;
    this.suffix = `${this.suffix}${text}`.slice(-256);
    if (this.format === "json") this.consumeJson(text);
    if (
      this.format === "fasta" ||
      this.format === "a3m" ||
      this.format === "aligned-fasta" ||
      this.format === "fastq"
    ) {
      this.consumeStructuredLines(text);
    }
    this.retainedPeak = Math.max(
      this.retainedPeak,
      this.firstNonWhitespace.length * 2 +
        this.suffix.length * 2 +
        this.jsonContexts.length * 8 +
        (this.jsonToken?.value.length ?? 0),
    );
  }

  private consumeStructuredLines(text: string): void {
    for (let offset = 0; offset < text.length;) {
      const newline = text.indexOf("\n", offset);
      const rawEnd = newline === -1 ? text.length : newline;
      const end =
        rawEnd > offset && text[rawEnd - 1] === "\r" ? rawEnd - 1 : rawEnd;
      const length = end - offset;
      if (length > 0) {
        this.lineTouched = true;
        this.lineLength += length;
        if (this.linePrefix.length < 16) {
          this.linePrefix += text.slice(
            offset,
            Math.min(end, offset + 16 - this.linePrefix.length),
          );
        }
      }
      if (newline === -1) break;
      this.finishStructuredLine();
      offset = newline + 1;
    }
  }

  private finishStructuredLine(): void {
    if (this.format === "fastq") this.finishFastqLine();
    else this.finishFastaLine();
    this.lineLength = 0;
    this.linePrefix = "";
    this.lineTouched = false;
  }

  private finishFastqLine(): void {
    if (this.fastqPhase === 0) {
      if (this.lineLength === 0) return;
      if (!this.linePrefix.startsWith("@") || this.lineLength < 2) {
        throw new Error("The exported FASTQ artifact has an invalid header.");
      }
      this.fastqQualityLength = 0;
      this.fastqSequenceLength = 0;
      this.fastqPhase = 1;
    } else if (this.fastqPhase === 1) {
      if (this.linePrefix.startsWith("+")) {
        if (this.fastqSequenceLength === 0) {
          throw new Error("The exported FASTQ artifact has an empty sequence.");
        }
        this.fastqPhase = 3;
      } else if (this.lineLength === 0) {
        throw new Error("The exported FASTQ artifact has an empty sequence.");
      } else {
        this.fastqSequenceLength += this.lineLength;
      }
    } else {
      this.fastqQualityLength += this.lineLength;
      if (this.fastqQualityLength > this.fastqSequenceLength) {
        throw new Error(
          "The exported FASTQ sequence and quality lengths do not match.",
        );
      }
      if (this.fastqQualityLength === this.fastqSequenceLength) {
        this.fastqPhase = 0;
      }
    }
  }

  private finishFastaLine(): void {
    if (this.lineLength === 0 || this.linePrefix.startsWith(";")) return;
    if (this.linePrefix.startsWith(">")) {
      if (this.fastaRecordCount > 0 && this.fastaCurrentSequenceLength === 0) {
        throw new Error("The exported FASTA artifact has an empty record.");
      }
      if (this.lineLength < 2) {
        throw new Error("The exported FASTA artifact has an empty header.");
      }
      if (this.format === "aligned-fasta" && this.fastaRecordCount > 0) {
        this.assertAlignedLength();
      }
      this.fastaRecordCount += 1;
      this.fastaCurrentSequenceLength = 0;
      return;
    }
    if (this.fastaRecordCount === 0) {
      throw new Error("The exported FASTA artifact is invalid.");
    }
    this.fastaCurrentSequenceLength += this.lineLength;
    if (this.format === "aligned-fasta" && this.fastaRecordCount === 1) {
      this.fastaAlignedLength = this.fastaCurrentSequenceLength;
    }
  }

  private assertAlignedLength(): void {
    if (
      this.fastaAlignedLength != null &&
      this.fastaCurrentSequenceLength !== this.fastaAlignedLength
    ) {
      throw new Error("The exported aligned FASTA rows have unequal lengths.");
    }
  }

  private consumeJson(text: string): void {
    for (let index = 0; index < text.length;) {
      const character = text[index]!;
      if (this.jsonUnicodeDigits > 0) {
        if (!/[0-9a-f]/iu.test(character)) {
          throw new Error("The exported JSON artifact is invalid.");
        }
        this.jsonUnicodeDigits -= 1;
        index += 1;
        continue;
      }
      if (this.jsonEscape) {
        if (character === "u") this.jsonUnicodeDigits = 4;
        else if (!/["\\/bfnrt]/u.test(character)) {
          throw new Error("The exported JSON artifact is invalid.");
        }
        this.jsonEscape = false;
        index += 1;
        continue;
      }
      if (this.jsonInString) {
        if (character === "\\") this.jsonEscape = true;
        else if (character === '"') {
          this.jsonInString = false;
          if (this.jsonStringRole === "key") {
            const context = this.jsonContexts.at(-1);
            if (context?.type !== "object") this.invalidJson();
            context.state = "colon";
          }
          this.jsonStringRole = undefined;
        } else if (character.charCodeAt(0) < 0x20) {
          this.invalidJson();
        }
        index += 1;
        continue;
      }
      if (this.jsonToken != null) {
        if (/[A-Za-z0-9+\-.eE]/u.test(character)) {
          if (this.jsonToken.value.length >= 128) this.invalidJson();
          this.jsonToken.value += character;
          index += 1;
          continue;
        }
        this.finishJsonToken();
        continue;
      }
      if (/\s/u.test(character)) {
        index += 1;
        continue;
      }
      if (character === '"') {
        const context = this.jsonContexts.at(-1);
        if (
          context?.type === "object" &&
          (context.state === "key" || context.state === "key-or-end")
        ) {
          this.jsonStringRole = "key";
        } else {
          this.startJsonValue();
          this.jsonStringRole = "value";
        }
        this.jsonInString = true;
      } else if (character === "{" || character === "[") {
        this.startJsonValue();
        if (this.jsonContexts.length >= 4_096) {
          throw new Error("The exported JSON artifact is nested too deeply.");
        }
        this.jsonContexts.push(
          character === "{"
            ? { state: "key-or-end", type: "object" }
            : { state: "value-or-end", type: "array" },
        );
      } else if (character === "}" || character === "]") {
        const context = this.jsonContexts.at(-1);
        if (
          (character === "}" &&
            (context?.type !== "object" ||
              (context.state !== "key-or-end" &&
                context.state !== "comma-or-end"))) ||
          (character === "]" &&
            (context?.type !== "array" ||
              (context.state !== "value-or-end" &&
                context.state !== "comma-or-end")))
        ) {
          this.invalidJson();
        }
        this.jsonContexts.pop();
      } else if (character === ":") {
        const context = this.jsonContexts.at(-1);
        if (context?.type !== "object" || context.state !== "colon") {
          this.invalidJson();
        }
        context.state = "value";
      } else if (character === ",") {
        const context = this.jsonContexts.at(-1);
        if (context?.state !== "comma-or-end") this.invalidJson();
        context.state = context.type === "object" ? "key" : "value";
      } else if (character === "-" || /[0-9]/u.test(character)) {
        this.startJsonValue();
        this.jsonToken = { kind: "number", value: character };
      } else if (character === "t" || character === "f" || character === "n") {
        this.startJsonValue();
        this.jsonToken = { kind: "literal", value: character };
      } else {
        this.invalidJson();
      }
      index += 1;
    }
  }

  private startJsonValue(): void {
    const context = this.jsonContexts.at(-1);
    if (context == null) {
      if (this.jsonRootState !== "value") this.invalidJson();
      this.jsonRootState = "done";
      return;
    }
    if (
      context.state !== "value" &&
      !(context.type === "array" && context.state === "value-or-end")
    ) {
      this.invalidJson();
    }
    context.state = "comma-or-end";
  }

  private finishJsonToken(): void {
    const token = this.jsonToken;
    this.jsonToken = undefined;
    if (
      token == null ||
      (token.kind === "literal" &&
        token.value !== "true" &&
        token.value !== "false" &&
        token.value !== "null") ||
      (token.kind === "number" &&
        !/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u.test(
          token.value,
        ))
    ) {
      this.invalidJson();
    }
  }

  private invalidJson(): never {
    throw new Error("The exported JSON artifact is invalid.");
  }
}

export function createIncrementalExportValidator(
  input: SequenceWorkbenchPayloadDeclaration,
): IncrementalSequenceExportValidator | undefined {
  if (
    input.kind !== "artifact" ||
    input.destination.kind !== "workspace" ||
    input.format == null ||
    input.mediaType == null
  ) {
    return undefined;
  }
  return new IncrementalSequenceExportValidator(input.format, input.mediaType);
}

SHA-256: 25162acb339010c802d3808276278b859f7cc8affa4f9e6fbd9a0fac8c5c0d12