11cb0ef41Sopenharmony_ci'use strict'
21cb0ef41Sopenharmony_ci
31cb0ef41Sopenharmony_ciconst {
41cb0ef41Sopenharmony_ci  mkdir,
51cb0ef41Sopenharmony_ci  readFile,
61cb0ef41Sopenharmony_ci  rm,
71cb0ef41Sopenharmony_ci  stat,
81cb0ef41Sopenharmony_ci  truncate,
91cb0ef41Sopenharmony_ci  writeFile,
101cb0ef41Sopenharmony_ci} = require('fs/promises')
111cb0ef41Sopenharmony_ciconst pMap = require('p-map')
121cb0ef41Sopenharmony_ciconst contentPath = require('./content/path')
131cb0ef41Sopenharmony_ciconst fsm = require('fs-minipass')
141cb0ef41Sopenharmony_ciconst glob = require('./util/glob.js')
151cb0ef41Sopenharmony_ciconst index = require('./entry-index')
161cb0ef41Sopenharmony_ciconst path = require('path')
171cb0ef41Sopenharmony_ciconst ssri = require('ssri')
181cb0ef41Sopenharmony_ci
191cb0ef41Sopenharmony_ciconst hasOwnProperty = (obj, key) =>
201cb0ef41Sopenharmony_ci  Object.prototype.hasOwnProperty.call(obj, key)
211cb0ef41Sopenharmony_ci
221cb0ef41Sopenharmony_ciconst verifyOpts = (opts) => ({
231cb0ef41Sopenharmony_ci  concurrency: 20,
241cb0ef41Sopenharmony_ci  log: { silly () {} },
251cb0ef41Sopenharmony_ci  ...opts,
261cb0ef41Sopenharmony_ci})
271cb0ef41Sopenharmony_ci
281cb0ef41Sopenharmony_cimodule.exports = verify
291cb0ef41Sopenharmony_ci
301cb0ef41Sopenharmony_ciasync function verify (cache, opts) {
311cb0ef41Sopenharmony_ci  opts = verifyOpts(opts)
321cb0ef41Sopenharmony_ci  opts.log.silly('verify', 'verifying cache at', cache)
331cb0ef41Sopenharmony_ci
341cb0ef41Sopenharmony_ci  const steps = [
351cb0ef41Sopenharmony_ci    markStartTime,
361cb0ef41Sopenharmony_ci    fixPerms,
371cb0ef41Sopenharmony_ci    garbageCollect,
381cb0ef41Sopenharmony_ci    rebuildIndex,
391cb0ef41Sopenharmony_ci    cleanTmp,
401cb0ef41Sopenharmony_ci    writeVerifile,
411cb0ef41Sopenharmony_ci    markEndTime,
421cb0ef41Sopenharmony_ci  ]
431cb0ef41Sopenharmony_ci
441cb0ef41Sopenharmony_ci  const stats = {}
451cb0ef41Sopenharmony_ci  for (const step of steps) {
461cb0ef41Sopenharmony_ci    const label = step.name
471cb0ef41Sopenharmony_ci    const start = new Date()
481cb0ef41Sopenharmony_ci    const s = await step(cache, opts)
491cb0ef41Sopenharmony_ci    if (s) {
501cb0ef41Sopenharmony_ci      Object.keys(s).forEach((k) => {
511cb0ef41Sopenharmony_ci        stats[k] = s[k]
521cb0ef41Sopenharmony_ci      })
531cb0ef41Sopenharmony_ci    }
541cb0ef41Sopenharmony_ci    const end = new Date()
551cb0ef41Sopenharmony_ci    if (!stats.runTime) {
561cb0ef41Sopenharmony_ci      stats.runTime = {}
571cb0ef41Sopenharmony_ci    }
581cb0ef41Sopenharmony_ci    stats.runTime[label] = end - start
591cb0ef41Sopenharmony_ci  }
601cb0ef41Sopenharmony_ci  stats.runTime.total = stats.endTime - stats.startTime
611cb0ef41Sopenharmony_ci  opts.log.silly(
621cb0ef41Sopenharmony_ci    'verify',
631cb0ef41Sopenharmony_ci    'verification finished for',
641cb0ef41Sopenharmony_ci    cache,
651cb0ef41Sopenharmony_ci    'in',
661cb0ef41Sopenharmony_ci    `${stats.runTime.total}ms`
671cb0ef41Sopenharmony_ci  )
681cb0ef41Sopenharmony_ci  return stats
691cb0ef41Sopenharmony_ci}
701cb0ef41Sopenharmony_ci
711cb0ef41Sopenharmony_ciasync function markStartTime (cache, opts) {
721cb0ef41Sopenharmony_ci  return { startTime: new Date() }
731cb0ef41Sopenharmony_ci}
741cb0ef41Sopenharmony_ci
751cb0ef41Sopenharmony_ciasync function markEndTime (cache, opts) {
761cb0ef41Sopenharmony_ci  return { endTime: new Date() }
771cb0ef41Sopenharmony_ci}
781cb0ef41Sopenharmony_ci
791cb0ef41Sopenharmony_ciasync function fixPerms (cache, opts) {
801cb0ef41Sopenharmony_ci  opts.log.silly('verify', 'fixing cache permissions')
811cb0ef41Sopenharmony_ci  await mkdir(cache, { recursive: true })
821cb0ef41Sopenharmony_ci  return null
831cb0ef41Sopenharmony_ci}
841cb0ef41Sopenharmony_ci
851cb0ef41Sopenharmony_ci// Implements a naive mark-and-sweep tracing garbage collector.
861cb0ef41Sopenharmony_ci//
871cb0ef41Sopenharmony_ci// The algorithm is basically as follows:
881cb0ef41Sopenharmony_ci// 1. Read (and filter) all index entries ("pointers")
891cb0ef41Sopenharmony_ci// 2. Mark each integrity value as "live"
901cb0ef41Sopenharmony_ci// 3. Read entire filesystem tree in `content-vX/` dir
911cb0ef41Sopenharmony_ci// 4. If content is live, verify its checksum and delete it if it fails
921cb0ef41Sopenharmony_ci// 5. If content is not marked as live, rm it.
931cb0ef41Sopenharmony_ci//
941cb0ef41Sopenharmony_ciasync function garbageCollect (cache, opts) {
951cb0ef41Sopenharmony_ci  opts.log.silly('verify', 'garbage collecting content')
961cb0ef41Sopenharmony_ci  const indexStream = index.lsStream(cache)
971cb0ef41Sopenharmony_ci  const liveContent = new Set()
981cb0ef41Sopenharmony_ci  indexStream.on('data', (entry) => {
991cb0ef41Sopenharmony_ci    if (opts.filter && !opts.filter(entry)) {
1001cb0ef41Sopenharmony_ci      return
1011cb0ef41Sopenharmony_ci    }
1021cb0ef41Sopenharmony_ci
1031cb0ef41Sopenharmony_ci    // integrity is stringified, re-parse it so we can get each hash
1041cb0ef41Sopenharmony_ci    const integrity = ssri.parse(entry.integrity)
1051cb0ef41Sopenharmony_ci    for (const algo in integrity) {
1061cb0ef41Sopenharmony_ci      liveContent.add(integrity[algo].toString())
1071cb0ef41Sopenharmony_ci    }
1081cb0ef41Sopenharmony_ci  })
1091cb0ef41Sopenharmony_ci  await new Promise((resolve, reject) => {
1101cb0ef41Sopenharmony_ci    indexStream.on('end', resolve).on('error', reject)
1111cb0ef41Sopenharmony_ci  })
1121cb0ef41Sopenharmony_ci  const contentDir = contentPath.contentDir(cache)
1131cb0ef41Sopenharmony_ci  const files = await glob(path.join(contentDir, '**'), {
1141cb0ef41Sopenharmony_ci    follow: false,
1151cb0ef41Sopenharmony_ci    nodir: true,
1161cb0ef41Sopenharmony_ci    nosort: true,
1171cb0ef41Sopenharmony_ci  })
1181cb0ef41Sopenharmony_ci  const stats = {
1191cb0ef41Sopenharmony_ci    verifiedContent: 0,
1201cb0ef41Sopenharmony_ci    reclaimedCount: 0,
1211cb0ef41Sopenharmony_ci    reclaimedSize: 0,
1221cb0ef41Sopenharmony_ci    badContentCount: 0,
1231cb0ef41Sopenharmony_ci    keptSize: 0,
1241cb0ef41Sopenharmony_ci  }
1251cb0ef41Sopenharmony_ci  await pMap(
1261cb0ef41Sopenharmony_ci    files,
1271cb0ef41Sopenharmony_ci    async (f) => {
1281cb0ef41Sopenharmony_ci      const split = f.split(/[/\\]/)
1291cb0ef41Sopenharmony_ci      const digest = split.slice(split.length - 3).join('')
1301cb0ef41Sopenharmony_ci      const algo = split[split.length - 4]
1311cb0ef41Sopenharmony_ci      const integrity = ssri.fromHex(digest, algo)
1321cb0ef41Sopenharmony_ci      if (liveContent.has(integrity.toString())) {
1331cb0ef41Sopenharmony_ci        const info = await verifyContent(f, integrity)
1341cb0ef41Sopenharmony_ci        if (!info.valid) {
1351cb0ef41Sopenharmony_ci          stats.reclaimedCount++
1361cb0ef41Sopenharmony_ci          stats.badContentCount++
1371cb0ef41Sopenharmony_ci          stats.reclaimedSize += info.size
1381cb0ef41Sopenharmony_ci        } else {
1391cb0ef41Sopenharmony_ci          stats.verifiedContent++
1401cb0ef41Sopenharmony_ci          stats.keptSize += info.size
1411cb0ef41Sopenharmony_ci        }
1421cb0ef41Sopenharmony_ci      } else {
1431cb0ef41Sopenharmony_ci        // No entries refer to this content. We can delete.
1441cb0ef41Sopenharmony_ci        stats.reclaimedCount++
1451cb0ef41Sopenharmony_ci        const s = await stat(f)
1461cb0ef41Sopenharmony_ci        await rm(f, { recursive: true, force: true })
1471cb0ef41Sopenharmony_ci        stats.reclaimedSize += s.size
1481cb0ef41Sopenharmony_ci      }
1491cb0ef41Sopenharmony_ci      return stats
1501cb0ef41Sopenharmony_ci    },
1511cb0ef41Sopenharmony_ci    { concurrency: opts.concurrency }
1521cb0ef41Sopenharmony_ci  )
1531cb0ef41Sopenharmony_ci  return stats
1541cb0ef41Sopenharmony_ci}
1551cb0ef41Sopenharmony_ci
1561cb0ef41Sopenharmony_ciasync function verifyContent (filepath, sri) {
1571cb0ef41Sopenharmony_ci  const contentInfo = {}
1581cb0ef41Sopenharmony_ci  try {
1591cb0ef41Sopenharmony_ci    const { size } = await stat(filepath)
1601cb0ef41Sopenharmony_ci    contentInfo.size = size
1611cb0ef41Sopenharmony_ci    contentInfo.valid = true
1621cb0ef41Sopenharmony_ci    await ssri.checkStream(new fsm.ReadStream(filepath), sri)
1631cb0ef41Sopenharmony_ci  } catch (err) {
1641cb0ef41Sopenharmony_ci    if (err.code === 'ENOENT') {
1651cb0ef41Sopenharmony_ci      return { size: 0, valid: false }
1661cb0ef41Sopenharmony_ci    }
1671cb0ef41Sopenharmony_ci    if (err.code !== 'EINTEGRITY') {
1681cb0ef41Sopenharmony_ci      throw err
1691cb0ef41Sopenharmony_ci    }
1701cb0ef41Sopenharmony_ci
1711cb0ef41Sopenharmony_ci    await rm(filepath, { recursive: true, force: true })
1721cb0ef41Sopenharmony_ci    contentInfo.valid = false
1731cb0ef41Sopenharmony_ci  }
1741cb0ef41Sopenharmony_ci  return contentInfo
1751cb0ef41Sopenharmony_ci}
1761cb0ef41Sopenharmony_ci
1771cb0ef41Sopenharmony_ciasync function rebuildIndex (cache, opts) {
1781cb0ef41Sopenharmony_ci  opts.log.silly('verify', 'rebuilding index')
1791cb0ef41Sopenharmony_ci  const entries = await index.ls(cache)
1801cb0ef41Sopenharmony_ci  const stats = {
1811cb0ef41Sopenharmony_ci    missingContent: 0,
1821cb0ef41Sopenharmony_ci    rejectedEntries: 0,
1831cb0ef41Sopenharmony_ci    totalEntries: 0,
1841cb0ef41Sopenharmony_ci  }
1851cb0ef41Sopenharmony_ci  const buckets = {}
1861cb0ef41Sopenharmony_ci  for (const k in entries) {
1871cb0ef41Sopenharmony_ci    /* istanbul ignore else */
1881cb0ef41Sopenharmony_ci    if (hasOwnProperty(entries, k)) {
1891cb0ef41Sopenharmony_ci      const hashed = index.hashKey(k)
1901cb0ef41Sopenharmony_ci      const entry = entries[k]
1911cb0ef41Sopenharmony_ci      const excluded = opts.filter && !opts.filter(entry)
1921cb0ef41Sopenharmony_ci      excluded && stats.rejectedEntries++
1931cb0ef41Sopenharmony_ci      if (buckets[hashed] && !excluded) {
1941cb0ef41Sopenharmony_ci        buckets[hashed].push(entry)
1951cb0ef41Sopenharmony_ci      } else if (buckets[hashed] && excluded) {
1961cb0ef41Sopenharmony_ci        // skip
1971cb0ef41Sopenharmony_ci      } else if (excluded) {
1981cb0ef41Sopenharmony_ci        buckets[hashed] = []
1991cb0ef41Sopenharmony_ci        buckets[hashed]._path = index.bucketPath(cache, k)
2001cb0ef41Sopenharmony_ci      } else {
2011cb0ef41Sopenharmony_ci        buckets[hashed] = [entry]
2021cb0ef41Sopenharmony_ci        buckets[hashed]._path = index.bucketPath(cache, k)
2031cb0ef41Sopenharmony_ci      }
2041cb0ef41Sopenharmony_ci    }
2051cb0ef41Sopenharmony_ci  }
2061cb0ef41Sopenharmony_ci  await pMap(
2071cb0ef41Sopenharmony_ci    Object.keys(buckets),
2081cb0ef41Sopenharmony_ci    (key) => {
2091cb0ef41Sopenharmony_ci      return rebuildBucket(cache, buckets[key], stats, opts)
2101cb0ef41Sopenharmony_ci    },
2111cb0ef41Sopenharmony_ci    { concurrency: opts.concurrency }
2121cb0ef41Sopenharmony_ci  )
2131cb0ef41Sopenharmony_ci  return stats
2141cb0ef41Sopenharmony_ci}
2151cb0ef41Sopenharmony_ci
2161cb0ef41Sopenharmony_ciasync function rebuildBucket (cache, bucket, stats, opts) {
2171cb0ef41Sopenharmony_ci  await truncate(bucket._path)
2181cb0ef41Sopenharmony_ci  // This needs to be serialized because cacache explicitly
2191cb0ef41Sopenharmony_ci  // lets very racy bucket conflicts clobber each other.
2201cb0ef41Sopenharmony_ci  for (const entry of bucket) {
2211cb0ef41Sopenharmony_ci    const content = contentPath(cache, entry.integrity)
2221cb0ef41Sopenharmony_ci    try {
2231cb0ef41Sopenharmony_ci      await stat(content)
2241cb0ef41Sopenharmony_ci      await index.insert(cache, entry.key, entry.integrity, {
2251cb0ef41Sopenharmony_ci        metadata: entry.metadata,
2261cb0ef41Sopenharmony_ci        size: entry.size,
2271cb0ef41Sopenharmony_ci        time: entry.time,
2281cb0ef41Sopenharmony_ci      })
2291cb0ef41Sopenharmony_ci      stats.totalEntries++
2301cb0ef41Sopenharmony_ci    } catch (err) {
2311cb0ef41Sopenharmony_ci      if (err.code === 'ENOENT') {
2321cb0ef41Sopenharmony_ci        stats.rejectedEntries++
2331cb0ef41Sopenharmony_ci        stats.missingContent++
2341cb0ef41Sopenharmony_ci      } else {
2351cb0ef41Sopenharmony_ci        throw err
2361cb0ef41Sopenharmony_ci      }
2371cb0ef41Sopenharmony_ci    }
2381cb0ef41Sopenharmony_ci  }
2391cb0ef41Sopenharmony_ci}
2401cb0ef41Sopenharmony_ci
2411cb0ef41Sopenharmony_cifunction cleanTmp (cache, opts) {
2421cb0ef41Sopenharmony_ci  opts.log.silly('verify', 'cleaning tmp directory')
2431cb0ef41Sopenharmony_ci  return rm(path.join(cache, 'tmp'), { recursive: true, force: true })
2441cb0ef41Sopenharmony_ci}
2451cb0ef41Sopenharmony_ci
2461cb0ef41Sopenharmony_ciasync function writeVerifile (cache, opts) {
2471cb0ef41Sopenharmony_ci  const verifile = path.join(cache, '_lastverified')
2481cb0ef41Sopenharmony_ci  opts.log.silly('verify', 'writing verifile to ' + verifile)
2491cb0ef41Sopenharmony_ci  return writeFile(verifile, `${Date.now()}`)
2501cb0ef41Sopenharmony_ci}
2511cb0ef41Sopenharmony_ci
2521cb0ef41Sopenharmony_cimodule.exports.lastRun = lastRun
2531cb0ef41Sopenharmony_ci
2541cb0ef41Sopenharmony_ciasync function lastRun (cache) {
2551cb0ef41Sopenharmony_ci  const data = await readFile(path.join(cache, '_lastverified'), { encoding: 'utf8' })
2561cb0ef41Sopenharmony_ci  return new Date(+data)
2571cb0ef41Sopenharmony_ci}
258