fission-suite/webnative

View on GitHub
src/fs/protocol/private/index.ts

Summary

Maintainability
A
45 mins
Test Coverage
import * as Uint8arrays from "uint8arrays"
import type { CID } from "multiformats/cid"

import * as Basic from "../basic.js"
import * as Check from "./types/check.js"
import * as Crypto from "../../../components/crypto/implementation.js"
import * as Depot from "../../../components/depot/implementation.js"
import * as Namefilter from "./namefilter.js"

import { BareNameFilter, PrivateName } from "./namefilter.js"
import { DecryptedNode, PrivateAddResult, Revision } from "./types.js"
import { Maybe, decodeCID } from "../../../common/index.js"
import MMPT from "./mmpt.js"


export const addNode = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  node: DecryptedNode,
  key: Uint8Array
): Promise<PrivateAddResult> => {
  const { cid, size } = await Basic.putEncryptedFile(depot, crypto, node, key)
  const filter = await Namefilter.addRevision(crypto, node.bareNameFilter, key, node.revision)
  const name = await Namefilter.toPrivateName(crypto, filter)

  await mmpt.add(name, cid)

  // if the node is a file, we also add the content to the MMPT
  if (Check.isPrivateFileInfo(node)) {
    const key = Uint8arrays.fromString(node.key, "base64pad")
    const contentBareFilter = await Namefilter.addToBare(crypto, node.bareNameFilter, Namefilter.legacyEncodingMistake(key, "base64pad"))
    const contentFilter = await Namefilter.addRevision(crypto, contentBareFilter, key, node.revision)
    const contentName = await Namefilter.toPrivateName(crypto, contentFilter)
    await mmpt.add(contentName, decodeCID(node.content))
  }

  const [ skeleton, isFile ] = Check.isPrivateFileInfo(node) ? [ {}, true ] : [ node.skeleton, false ]
  return { cid, name, key, size, isFile, skeleton }
}

export const readNode = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  cid: CID,
  key: Uint8Array
): Promise<DecryptedNode> => {
  const contentBytes = await Basic.getEncryptedFile(depot, crypto, cid, key)
  const content = JSON.parse(Uint8arrays.toString(contentBytes, "utf8"))
  if (!Check.isDecryptedNode(content)) {
    throw new Error(`Could not parse a valid filesystem object: ${content}`)
  }
  return content
}

export const getByName = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  name: PrivateName,
  key: Uint8Array
): Promise<Maybe<DecryptedNode>> => {
  const cid = await mmpt.get(name)
  if (cid === null) return null
  return getByCID(depot, crypto, cid, key)
}

export const getByCID = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  cid: CID,
  key: Uint8Array
): Promise<DecryptedNode> => {
  return await readNode(depot, crypto, cid, key)
}

export const getLatestByName = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  name: PrivateName,
  key: Uint8Array
): Promise<Maybe<DecryptedNode>> => {
  const cid = await mmpt.get(name)
  if (cid === null) return null
  return getLatestByCID(depot, crypto, mmpt, cid, key)
}

export const getLatestByCID = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  cid: CID,
  key: Uint8Array
): Promise<DecryptedNode> => {
  const node = await getByCID(depot, crypto, cid, key)
  const latest = await findLatestRevision(crypto, mmpt, node.bareNameFilter, key, node.revision)
  return latest?.cid
    ? await getByCID(depot, crypto, decodeCID(latest?.cid), key)
    : node
}

export const getLatestByBareNameFilter = async (
  depot: Depot.Implementation,
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  bareName: BareNameFilter,
  key: Uint8Array
): Promise<Maybe<DecryptedNode>> => {
  const revisionFilter = await Namefilter.addRevision(crypto, bareName, key, 1)
  const name = await Namefilter.toPrivateName(crypto, revisionFilter)
  return getLatestByName(depot, crypto, mmpt, name, key)
}

export const findLatestRevision = async (
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  bareName: BareNameFilter,
  key: Uint8Array,
  lastKnownRevision: number
): Promise<Maybe<Revision>> => {
  // Exponential search forward
  let lowerBound = lastKnownRevision, upperBound = null
  let i = 0
  let lastRevision: Maybe<Revision> = null

  while (upperBound === null) {
    const toCheck = lastKnownRevision + Math.pow(2, i)
    const thisRevision = await getRevision(crypto, mmpt, bareName, key, toCheck)

    if (thisRevision !== null) {
      lastRevision = thisRevision
      lowerBound = toCheck
    } else {
      upperBound = toCheck
    }

    i++
  }

  // Binary search back
  while (lowerBound < (upperBound - 1)) {
    const midpoint = Math.floor((upperBound + lowerBound) / 2)
    const thisRevision = await getRevision(crypto, mmpt, bareName, key, midpoint)

    if (thisRevision !== null) {
      lastRevision = thisRevision
      lowerBound = midpoint
    } else {
      upperBound = midpoint
    }
  }

  return lastRevision
}

export const getRevision = async (
  crypto: Crypto.Implementation,
  mmpt: MMPT,
  bareName: BareNameFilter,
  key: Uint8Array,
  revision: number
): Promise<Maybe<Revision>> => {
  const filter = await Namefilter.addRevision(crypto, bareName, key, revision)
  const name = await Namefilter.toPrivateName(crypto, filter)
  const cid = await mmpt.get(name)
  return cid ? { cid, name, number: revision } : null
}