OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / blind-boundary-scratch.js

125 lines · javascript ref:
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

// Boundary tests create Unix sockets and persistence fixtures. Keep every one
// below the checkout even when a caller provides a custom scratch root.
const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
const scratchEnvironment = 'HIVERELAY_BLIND_BOUNDARY_TEST_ROOT'
const defaultScratchRoot = path.join(repositoryRoot, '.t')

function isContained (parent, candidate) {
  const relative = path.relative(parent, candidate)
  return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)
}

function configuredScratchRoot () {
  const configured = process.env[scratchEnvironment]
  const root = configured
    ? path.resolve(repositoryRoot, configured)
    : defaultScratchRoot
  if (!isContained(repositoryRoot, root)) {
    throw new Error(`${scratchEnvironment} must remain inside the repository checkout`)
  }
  return root
}

function scratchPathParts (root) {
  const relative = path.relative(repositoryRoot, root)
  if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
    throw new Error(`${scratchEnvironment} must remain inside the repository checkout`)
  }
  return relative.split(path.sep)
}

function missing (error) {
  return error && error.code === 'ENOENT'
}

// Do not use one recursive mkdir for a caller-supplied nested root. A
// pre-existing symlink ancestor would make mkdir create children outside the
// checkout before a later realpath check could reject it. Walk each component
// first and create only the next child of an already verified real directory.
async function ensureScratchRootInsideCheckout (root) {
  const realRepositoryRoot = await fs.realpath(repositoryRoot)
  const parts = scratchPathParts(root)
  let lexical = repositoryRoot
  let realRoot = realRepositoryRoot

  for (const part of parts) {
    lexical = path.join(lexical, part)
    let stat
    try {
      stat = await fs.lstat(lexical)
    } catch (error) {
      if (!missing(error)) throw error
      try {
        await fs.mkdir(lexical, { mode: 0o700 })
      } catch (createError) {
        if (createError?.code !== 'EEXIST') throw createError
      }
      stat = await fs.lstat(lexical)
    }
    if (!stat.isDirectory() || stat.isSymbolicLink()) {
      throw new Error('blind boundary scratch root must not traverse a symbolic-link or non-directory ancestor')
    }
    realRoot = await fs.realpath(lexical)
    if (!isContained(realRepositoryRoot, realRoot)) {
      throw new Error(`${scratchEnvironment} resolves outside the repository checkout`)
    }
  }

  await fs.chmod(realRoot, 0o700)
  return realRoot
}

function assertPrefix (prefix) {
  if (typeof prefix !== 'string' || prefix.length === 0 || prefix === '.' || prefix === '..' || path.basename(prefix) !== prefix) {
    throw new TypeError('blind boundary scratch prefix must be one basename')
  }
}

function assertParts (parts) {
  if (parts.length === 0 || parts.some(part => typeof part !== 'string' || part.length === 0 || part === '.' || part === '..' || path.basename(part) !== part)) {
    throw new TypeError('blind boundary scratch path must contain only basename segments')
  }
}

export async function blindBoundaryScratchRoot () {
  const root = configuredScratchRoot()
  return ensureScratchRootInsideCheckout(root)
}

export async function createBlindBoundaryScratch (prefix) {
  assertPrefix(prefix)
  const root = await blindBoundaryScratchRoot()
  const created = await fs.mkdtemp(path.join(root, prefix))
  const realCreated = await fs.realpath(created)
  if (!isContained(root, realCreated)) {
    throw new Error('blind boundary scratch allocation escaped its configured root')
  }
  await fs.chmod(realCreated, 0o700)
  return realCreated
}

export async function blindBoundaryScratchPath (...parts) {
  assertParts(parts)
  const root = await blindBoundaryScratchRoot()
  const candidate = path.resolve(root, ...parts)
  if (!isContained(root, candidate)) throw new Error('blind boundary scratch path escaped its configured root')
  return candidate
}

export async function removeBlindBoundaryScratch (candidate) {
  const root = await blindBoundaryScratchRoot()
  let realCandidate
  try {
    realCandidate = await fs.realpath(candidate)
  } catch (error) {
    if (error && error.code === 'ENOENT') return
    throw error
  }
  if (!isContained(root, realCandidate)) throw new Error('refusing to remove a path outside blind boundary scratch')
  await fs.rm(realCandidate, { recursive: true, force: true })
}