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