waysact/webpack-subresource-integrity

View on GitHub
webpack-subresource-integrity/src/scc.ts

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * Copyright (c) 2015-present, Waysact Pty Ltd
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import type { Graph, StronglyConnectedComponent } from "./types";
import { assert } from "./util";

interface TarjanVertexMetadata {
  index: number;
  lowlink: number;
  onstack?: boolean;
}

class SccVerticesBuilder<T> {
  private vertices: Set<T>;
  private edges: Map<T, Set<T>>;

  private vertexMetadata = new Map<T, TarjanVertexMetadata>();
  private stack: T[] = [];
  private index = 0;
  private stronglyConnectedComponents = new Set<
    StronglyConnectedComponent<T>
  >();

  constructor({ vertices, edges }: Graph<T>) {
    this.vertices = vertices;
    this.edges = edges;

    for (const vertex of this.vertices) {
      if (!this.vertexMetadata.has(vertex)) {
        this.strongConnect(vertex);
      }
    }
  }

  public build(): Set<StronglyConnectedComponent<T>> {
    return this.stronglyConnectedComponents;
  }

  private strongConnectChildren(vertex: T, vertexData: TarjanVertexMetadata) {
    for (const child of this.edges.get(vertex) ?? []) {
      this.strongConnectChild(child, vertexData);
    }
  }

  private strongConnectChild(child: T, vertexData: TarjanVertexMetadata) {
    const childData = this.vertexMetadata.get(child);
    if (childData?.onstack === false) {
      return;
    }
    vertexData.lowlink = Math.min(
      vertexData.lowlink,
      childData
        ? // Child is in stack and hence in the current SCC
          // If child is not on stack, then (vertex, child) is an edge pointing to an SCC already found and must be ignored
          // Note: The next line may look odd - but is correct.
          // It says childData.index not childData.lowlink; that is deliberate and from the original paper
          childData.index
        : // Child has not yet been visited; recurse on it
          this.strongConnect(child).lowlink
    );
  }

  private strongConnect(vertex: T) {
    // Set the depth index for v to the smallest unused index
    const vertexData: TarjanVertexMetadata = {
      index: this.index,
      lowlink: this.index,
      onstack: true,
    };
    this.vertexMetadata.set(vertex, vertexData);
    this.index++;
    this.stack.push(vertex);

    this.strongConnectChildren(vertex, vertexData);

    // If vertex is a root node, pop the stack and generate an SCC
    if (vertexData.index === vertexData.lowlink) {
      const newStronglyConnectedComponent = { nodes: new Set<T>() };
      let currentNode: T | undefined;
      do {
        currentNode = this.stack.pop();
        assert(currentNode, "Working stack was empty");
        const metadata = this.vertexMetadata.get(currentNode);
        assert(metadata, "All nodes on stack should have metadata");
        metadata.onstack = false;
        newStronglyConnectedComponent.nodes.add(currentNode);
      } while (currentNode !== vertex);

      this.stronglyConnectedComponents.add(newStronglyConnectedComponent);
    }

    return vertexData;
  }
}

class SccEdgesBuilder<T> {
  private vertices: Set<StronglyConnectedComponent<T>>;
  private edges: Map<T, Set<T>>;
  private vertexToSCCMap = new Map<T, StronglyConnectedComponent<T>>();

  constructor(
    vertices: Set<StronglyConnectedComponent<T>>,
    edges: Map<T, Set<T>>
  ) {
    this.vertices = vertices;
    this.edges = edges;

    for (const scc of this.vertices) {
      for (const vertex of scc.nodes) {
        this.vertexToSCCMap.set(vertex, scc);
      }
    }
  }

  public build() {
    // Now that all SCCs have been identified, rebuild the graph
    const sccEdges = new Map<
      StronglyConnectedComponent<T>,
      Set<StronglyConnectedComponent<T>>
    >();

    for (const scc of this.vertices) {
      sccEdges.set(scc, this.getChildSCCNodes(scc));
    }

    return sccEdges;
  }

  private getChildSCCNodes(scc: StronglyConnectedComponent<T>) {
    const childSCCNodes = new Set<StronglyConnectedComponent<T>>();
    scc.nodes.forEach((vertex) => {
      const edge = this.edges.get(vertex);
      if (!edge) {
        return;
      }
      edge.forEach((childVertex) => {
        const childScc = this.vertexToSCCMap.get(childVertex);
        if (childScc && childScc !== scc) {
          childSCCNodes.add(childScc);
        }
      });
    });
    return childSCCNodes;
  }
}

/**
 * Tarjan's strongly connected components algorithm
 * https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
 */
export function createDAGfromGraph<T>(
  graph: Graph<T>
): Graph<StronglyConnectedComponent<T>> {
  const vertices = new SccVerticesBuilder(graph).build();
  const edges = new SccEdgesBuilder(vertices, graph.edges).build();
  return { vertices, edges };
}