Index: node_modules/cacache/lib/verify.js
===================================================================
--- node_modules/cacache/lib/verify.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
+++ node_modules/cacache/lib/verify.js	(revision 81bc7da776d8cda42939ef4ab350c88d13b4941f)
@@ -0,0 +1,287 @@
+'use strict'
+
+const util = require('util')
+
+const pMap = require('p-map')
+const contentPath = require('./content/path')
+const fixOwner = require('./util/fix-owner')
+const fs = require('fs')
+const fsm = require('fs-minipass')
+const glob = util.promisify(require('glob'))
+const index = require('./entry-index')
+const path = require('path')
+const rimraf = util.promisify(require('rimraf'))
+const ssri = require('ssri')
+
+const hasOwnProperty = (obj, key) =>
+  Object.prototype.hasOwnProperty.call(obj, key)
+
+const stat = util.promisify(fs.stat)
+const truncate = util.promisify(fs.truncate)
+const writeFile = util.promisify(fs.writeFile)
+const readFile = util.promisify(fs.readFile)
+
+const verifyOpts = (opts) => ({
+  concurrency: 20,
+  log: { silly () {} },
+  ...opts,
+})
+
+module.exports = verify
+
+function verify (cache, opts) {
+  opts = verifyOpts(opts)
+  opts.log.silly('verify', 'verifying cache at', cache)
+
+  const steps = [
+    markStartTime,
+    fixPerms,
+    garbageCollect,
+    rebuildIndex,
+    cleanTmp,
+    writeVerifile,
+    markEndTime,
+  ]
+
+  return steps
+    .reduce((promise, step, i) => {
+      const label = step.name
+      const start = new Date()
+      return promise.then((stats) => {
+        return step(cache, opts).then((s) => {
+          s &&
+            Object.keys(s).forEach((k) => {
+              stats[k] = s[k]
+            })
+          const end = new Date()
+          if (!stats.runTime)
+            stats.runTime = {}
+
+          stats.runTime[label] = end - start
+          return Promise.resolve(stats)
+        })
+      })
+    }, Promise.resolve({}))
+    .then((stats) => {
+      stats.runTime.total = stats.endTime - stats.startTime
+      opts.log.silly(
+        'verify',
+        'verification finished for',
+        cache,
+        'in',
+        `${stats.runTime.total}ms`
+      )
+      return stats
+    })
+}
+
+function markStartTime (cache, opts) {
+  return Promise.resolve({ startTime: new Date() })
+}
+
+function markEndTime (cache, opts) {
+  return Promise.resolve({ endTime: new Date() })
+}
+
+function fixPerms (cache, opts) {
+  opts.log.silly('verify', 'fixing cache permissions')
+  return fixOwner
+    .mkdirfix(cache, cache)
+    .then(() => {
+      // TODO - fix file permissions too
+      return fixOwner.chownr(cache, cache)
+    })
+    .then(() => null)
+}
+
+// Implements a naive mark-and-sweep tracing garbage collector.
+//
+// The algorithm is basically as follows:
+// 1. Read (and filter) all index entries ("pointers")
+// 2. Mark each integrity value as "live"
+// 3. Read entire filesystem tree in `content-vX/` dir
+// 4. If content is live, verify its checksum and delete it if it fails
+// 5. If content is not marked as live, rimraf it.
+//
+function garbageCollect (cache, opts) {
+  opts.log.silly('verify', 'garbage collecting content')
+  const indexStream = index.lsStream(cache)
+  const liveContent = new Set()
+  indexStream.on('data', (entry) => {
+    if (opts.filter && !opts.filter(entry))
+      return
+
+    liveContent.add(entry.integrity.toString())
+  })
+  return new Promise((resolve, reject) => {
+    indexStream.on('end', resolve).on('error', reject)
+  }).then(() => {
+    const contentDir = contentPath.contentDir(cache)
+    return glob(path.join(contentDir, '**'), {
+      follow: false,
+      nodir: true,
+      nosort: true,
+    }).then((files) => {
+      return Promise.resolve({
+        verifiedContent: 0,
+        reclaimedCount: 0,
+        reclaimedSize: 0,
+        badContentCount: 0,
+        keptSize: 0,
+      }).then((stats) =>
+        pMap(
+          files,
+          (f) => {
+            const split = f.split(/[/\\]/)
+            const digest = split.slice(split.length - 3).join('')
+            const algo = split[split.length - 4]
+            const integrity = ssri.fromHex(digest, algo)
+            if (liveContent.has(integrity.toString())) {
+              return verifyContent(f, integrity).then((info) => {
+                if (!info.valid) {
+                  stats.reclaimedCount++
+                  stats.badContentCount++
+                  stats.reclaimedSize += info.size
+                } else {
+                  stats.verifiedContent++
+                  stats.keptSize += info.size
+                }
+                return stats
+              })
+            } else {
+              // No entries refer to this content. We can delete.
+              stats.reclaimedCount++
+              return stat(f).then((s) => {
+                return rimraf(f).then(() => {
+                  stats.reclaimedSize += s.size
+                  return stats
+                })
+              })
+            }
+          },
+          { concurrency: opts.concurrency }
+        ).then(() => stats)
+      )
+    })
+  })
+}
+
+function verifyContent (filepath, sri) {
+  return stat(filepath)
+    .then((s) => {
+      const contentInfo = {
+        size: s.size,
+        valid: true,
+      }
+      return ssri
+        .checkStream(new fsm.ReadStream(filepath), sri)
+        .catch((err) => {
+          if (err.code !== 'EINTEGRITY')
+            throw err
+
+          return rimraf(filepath).then(() => {
+            contentInfo.valid = false
+          })
+        })
+        .then(() => contentInfo)
+    })
+    .catch((err) => {
+      if (err.code === 'ENOENT')
+        return { size: 0, valid: false }
+
+      throw err
+    })
+}
+
+function rebuildIndex (cache, opts) {
+  opts.log.silly('verify', 'rebuilding index')
+  return index.ls(cache).then((entries) => {
+    const stats = {
+      missingContent: 0,
+      rejectedEntries: 0,
+      totalEntries: 0,
+    }
+    const buckets = {}
+    for (const k in entries) {
+      /* istanbul ignore else */
+      if (hasOwnProperty(entries, k)) {
+        const hashed = index.hashKey(k)
+        const entry = entries[k]
+        const excluded = opts.filter && !opts.filter(entry)
+        excluded && stats.rejectedEntries++
+        if (buckets[hashed] && !excluded)
+          buckets[hashed].push(entry)
+        else if (buckets[hashed] && excluded) {
+          // skip
+        } else if (excluded) {
+          buckets[hashed] = []
+          buckets[hashed]._path = index.bucketPath(cache, k)
+        } else {
+          buckets[hashed] = [entry]
+          buckets[hashed]._path = index.bucketPath(cache, k)
+        }
+      }
+    }
+    return pMap(
+      Object.keys(buckets),
+      (key) => {
+        return rebuildBucket(cache, buckets[key], stats, opts)
+      },
+      { concurrency: opts.concurrency }
+    ).then(() => stats)
+  })
+}
+
+function rebuildBucket (cache, bucket, stats, opts) {
+  return truncate(bucket._path).then(() => {
+    // This needs to be serialized because cacache explicitly
+    // lets very racy bucket conflicts clobber each other.
+    return bucket.reduce((promise, entry) => {
+      return promise.then(() => {
+        const content = contentPath(cache, entry.integrity)
+        return stat(content)
+          .then(() => {
+            return index
+              .insert(cache, entry.key, entry.integrity, {
+                metadata: entry.metadata,
+                size: entry.size,
+              })
+              .then(() => {
+                stats.totalEntries++
+              })
+          })
+          .catch((err) => {
+            if (err.code === 'ENOENT') {
+              stats.rejectedEntries++
+              stats.missingContent++
+              return
+            }
+            throw err
+          })
+      })
+    }, Promise.resolve())
+  })
+}
+
+function cleanTmp (cache, opts) {
+  opts.log.silly('verify', 'cleaning tmp directory')
+  return rimraf(path.join(cache, 'tmp'))
+}
+
+function writeVerifile (cache, opts) {
+  const verifile = path.join(cache, '_lastverified')
+  opts.log.silly('verify', 'writing verifile to ' + verifile)
+  try {
+    return writeFile(verifile, '' + +new Date())
+  } finally {
+    fixOwner.chownr.sync(cache, verifile)
+  }
+}
+
+module.exports.lastRun = lastRun
+
+function lastRun (cache) {
+  return readFile(path.join(cache, '_lastverified'), 'utf8').then(
+    (data) => new Date(+data)
+  )
+}
