11cb0ef41Sopenharmony_ci'use strict'
21cb0ef41Sopenharmony_ci
31cb0ef41Sopenharmony_ciconst crypto = require('crypto')
41cb0ef41Sopenharmony_ciconst {
51cb0ef41Sopenharmony_ci  appendFile,
61cb0ef41Sopenharmony_ci  mkdir,
71cb0ef41Sopenharmony_ci  readFile,
81cb0ef41Sopenharmony_ci  readdir,
91cb0ef41Sopenharmony_ci  rm,
101cb0ef41Sopenharmony_ci  writeFile,
111cb0ef41Sopenharmony_ci} = require('fs/promises')
121cb0ef41Sopenharmony_ciconst { Minipass } = require('minipass')
131cb0ef41Sopenharmony_ciconst path = require('path')
141cb0ef41Sopenharmony_ciconst ssri = require('ssri')
151cb0ef41Sopenharmony_ciconst uniqueFilename = require('unique-filename')
161cb0ef41Sopenharmony_ci
171cb0ef41Sopenharmony_ciconst contentPath = require('./content/path')
181cb0ef41Sopenharmony_ciconst hashToSegments = require('./util/hash-to-segments')
191cb0ef41Sopenharmony_ciconst indexV = require('../package.json')['cache-version'].index
201cb0ef41Sopenharmony_ciconst { moveFile } = require('@npmcli/fs')
211cb0ef41Sopenharmony_ci
221cb0ef41Sopenharmony_cimodule.exports.NotFoundError = class NotFoundError extends Error {
231cb0ef41Sopenharmony_ci  constructor (cache, key) {
241cb0ef41Sopenharmony_ci    super(`No cache entry for ${key} found in ${cache}`)
251cb0ef41Sopenharmony_ci    this.code = 'ENOENT'
261cb0ef41Sopenharmony_ci    this.cache = cache
271cb0ef41Sopenharmony_ci    this.key = key
281cb0ef41Sopenharmony_ci  }
291cb0ef41Sopenharmony_ci}
301cb0ef41Sopenharmony_ci
311cb0ef41Sopenharmony_cimodule.exports.compact = compact
321cb0ef41Sopenharmony_ci
331cb0ef41Sopenharmony_ciasync function compact (cache, key, matchFn, opts = {}) {
341cb0ef41Sopenharmony_ci  const bucket = bucketPath(cache, key)
351cb0ef41Sopenharmony_ci  const entries = await bucketEntries(bucket)
361cb0ef41Sopenharmony_ci  const newEntries = []
371cb0ef41Sopenharmony_ci  // we loop backwards because the bottom-most result is the newest
381cb0ef41Sopenharmony_ci  // since we add new entries with appendFile
391cb0ef41Sopenharmony_ci  for (let i = entries.length - 1; i >= 0; --i) {
401cb0ef41Sopenharmony_ci    const entry = entries[i]
411cb0ef41Sopenharmony_ci    // a null integrity could mean either a delete was appended
421cb0ef41Sopenharmony_ci    // or the user has simply stored an index that does not map
431cb0ef41Sopenharmony_ci    // to any content. we determine if the user wants to keep the
441cb0ef41Sopenharmony_ci    // null integrity based on the validateEntry function passed in options.
451cb0ef41Sopenharmony_ci    // if the integrity is null and no validateEntry is provided, we break
461cb0ef41Sopenharmony_ci    // as we consider the null integrity to be a deletion of everything
471cb0ef41Sopenharmony_ci    // that came before it.
481cb0ef41Sopenharmony_ci    if (entry.integrity === null && !opts.validateEntry) {
491cb0ef41Sopenharmony_ci      break
501cb0ef41Sopenharmony_ci    }
511cb0ef41Sopenharmony_ci
521cb0ef41Sopenharmony_ci    // if this entry is valid, and it is either the first entry or
531cb0ef41Sopenharmony_ci    // the newEntries array doesn't already include an entry that
541cb0ef41Sopenharmony_ci    // matches this one based on the provided matchFn, then we add
551cb0ef41Sopenharmony_ci    // it to the beginning of our list
561cb0ef41Sopenharmony_ci    if ((!opts.validateEntry || opts.validateEntry(entry) === true) &&
571cb0ef41Sopenharmony_ci      (newEntries.length === 0 ||
581cb0ef41Sopenharmony_ci        !newEntries.find((oldEntry) => matchFn(oldEntry, entry)))) {
591cb0ef41Sopenharmony_ci      newEntries.unshift(entry)
601cb0ef41Sopenharmony_ci    }
611cb0ef41Sopenharmony_ci  }
621cb0ef41Sopenharmony_ci
631cb0ef41Sopenharmony_ci  const newIndex = '\n' + newEntries.map((entry) => {
641cb0ef41Sopenharmony_ci    const stringified = JSON.stringify(entry)
651cb0ef41Sopenharmony_ci    const hash = hashEntry(stringified)
661cb0ef41Sopenharmony_ci    return `${hash}\t${stringified}`
671cb0ef41Sopenharmony_ci  }).join('\n')
681cb0ef41Sopenharmony_ci
691cb0ef41Sopenharmony_ci  const setup = async () => {
701cb0ef41Sopenharmony_ci    const target = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix)
711cb0ef41Sopenharmony_ci    await mkdir(path.dirname(target), { recursive: true })
721cb0ef41Sopenharmony_ci    return {
731cb0ef41Sopenharmony_ci      target,
741cb0ef41Sopenharmony_ci      moved: false,
751cb0ef41Sopenharmony_ci    }
761cb0ef41Sopenharmony_ci  }
771cb0ef41Sopenharmony_ci
781cb0ef41Sopenharmony_ci  const teardown = async (tmp) => {
791cb0ef41Sopenharmony_ci    if (!tmp.moved) {
801cb0ef41Sopenharmony_ci      return rm(tmp.target, { recursive: true, force: true })
811cb0ef41Sopenharmony_ci    }
821cb0ef41Sopenharmony_ci  }
831cb0ef41Sopenharmony_ci
841cb0ef41Sopenharmony_ci  const write = async (tmp) => {
851cb0ef41Sopenharmony_ci    await writeFile(tmp.target, newIndex, { flag: 'wx' })
861cb0ef41Sopenharmony_ci    await mkdir(path.dirname(bucket), { recursive: true })
871cb0ef41Sopenharmony_ci    // we use @npmcli/move-file directly here because we
881cb0ef41Sopenharmony_ci    // want to overwrite the existing file
891cb0ef41Sopenharmony_ci    await moveFile(tmp.target, bucket)
901cb0ef41Sopenharmony_ci    tmp.moved = true
911cb0ef41Sopenharmony_ci  }
921cb0ef41Sopenharmony_ci
931cb0ef41Sopenharmony_ci  // write the file atomically
941cb0ef41Sopenharmony_ci  const tmp = await setup()
951cb0ef41Sopenharmony_ci  try {
961cb0ef41Sopenharmony_ci    await write(tmp)
971cb0ef41Sopenharmony_ci  } finally {
981cb0ef41Sopenharmony_ci    await teardown(tmp)
991cb0ef41Sopenharmony_ci  }
1001cb0ef41Sopenharmony_ci
1011cb0ef41Sopenharmony_ci  // we reverse the list we generated such that the newest
1021cb0ef41Sopenharmony_ci  // entries come first in order to make looping through them easier
1031cb0ef41Sopenharmony_ci  // the true passed to formatEntry tells it to keep null
1041cb0ef41Sopenharmony_ci  // integrity values, if they made it this far it's because
1051cb0ef41Sopenharmony_ci  // validateEntry returned true, and as such we should return it
1061cb0ef41Sopenharmony_ci  return newEntries.reverse().map((entry) => formatEntry(cache, entry, true))
1071cb0ef41Sopenharmony_ci}
1081cb0ef41Sopenharmony_ci
1091cb0ef41Sopenharmony_cimodule.exports.insert = insert
1101cb0ef41Sopenharmony_ci
1111cb0ef41Sopenharmony_ciasync function insert (cache, key, integrity, opts = {}) {
1121cb0ef41Sopenharmony_ci  const { metadata, size, time } = opts
1131cb0ef41Sopenharmony_ci  const bucket = bucketPath(cache, key)
1141cb0ef41Sopenharmony_ci  const entry = {
1151cb0ef41Sopenharmony_ci    key,
1161cb0ef41Sopenharmony_ci    integrity: integrity && ssri.stringify(integrity),
1171cb0ef41Sopenharmony_ci    time: time || Date.now(),
1181cb0ef41Sopenharmony_ci    size,
1191cb0ef41Sopenharmony_ci    metadata,
1201cb0ef41Sopenharmony_ci  }
1211cb0ef41Sopenharmony_ci  try {
1221cb0ef41Sopenharmony_ci    await mkdir(path.dirname(bucket), { recursive: true })
1231cb0ef41Sopenharmony_ci    const stringified = JSON.stringify(entry)
1241cb0ef41Sopenharmony_ci    // NOTE - Cleverness ahoy!
1251cb0ef41Sopenharmony_ci    //
1261cb0ef41Sopenharmony_ci    // This works because it's tremendously unlikely for an entry to corrupt
1271cb0ef41Sopenharmony_ci    // another while still preserving the string length of the JSON in
1281cb0ef41Sopenharmony_ci    // question. So, we just slap the length in there and verify it on read.
1291cb0ef41Sopenharmony_ci    //
1301cb0ef41Sopenharmony_ci    // Thanks to @isaacs for the whiteboarding session that ended up with
1311cb0ef41Sopenharmony_ci    // this.
1321cb0ef41Sopenharmony_ci    await appendFile(bucket, `\n${hashEntry(stringified)}\t${stringified}`)
1331cb0ef41Sopenharmony_ci  } catch (err) {
1341cb0ef41Sopenharmony_ci    if (err.code === 'ENOENT') {
1351cb0ef41Sopenharmony_ci      return undefined
1361cb0ef41Sopenharmony_ci    }
1371cb0ef41Sopenharmony_ci
1381cb0ef41Sopenharmony_ci    throw err
1391cb0ef41Sopenharmony_ci  }
1401cb0ef41Sopenharmony_ci  return formatEntry(cache, entry)
1411cb0ef41Sopenharmony_ci}
1421cb0ef41Sopenharmony_ci
1431cb0ef41Sopenharmony_cimodule.exports.find = find
1441cb0ef41Sopenharmony_ci
1451cb0ef41Sopenharmony_ciasync function find (cache, key) {
1461cb0ef41Sopenharmony_ci  const bucket = bucketPath(cache, key)
1471cb0ef41Sopenharmony_ci  try {
1481cb0ef41Sopenharmony_ci    const entries = await bucketEntries(bucket)
1491cb0ef41Sopenharmony_ci    return entries.reduce((latest, next) => {
1501cb0ef41Sopenharmony_ci      if (next && next.key === key) {
1511cb0ef41Sopenharmony_ci        return formatEntry(cache, next)
1521cb0ef41Sopenharmony_ci      } else {
1531cb0ef41Sopenharmony_ci        return latest
1541cb0ef41Sopenharmony_ci      }
1551cb0ef41Sopenharmony_ci    }, null)
1561cb0ef41Sopenharmony_ci  } catch (err) {
1571cb0ef41Sopenharmony_ci    if (err.code === 'ENOENT') {
1581cb0ef41Sopenharmony_ci      return null
1591cb0ef41Sopenharmony_ci    } else {
1601cb0ef41Sopenharmony_ci      throw err
1611cb0ef41Sopenharmony_ci    }
1621cb0ef41Sopenharmony_ci  }
1631cb0ef41Sopenharmony_ci}
1641cb0ef41Sopenharmony_ci
1651cb0ef41Sopenharmony_cimodule.exports.delete = del
1661cb0ef41Sopenharmony_ci
1671cb0ef41Sopenharmony_cifunction del (cache, key, opts = {}) {
1681cb0ef41Sopenharmony_ci  if (!opts.removeFully) {
1691cb0ef41Sopenharmony_ci    return insert(cache, key, null, opts)
1701cb0ef41Sopenharmony_ci  }
1711cb0ef41Sopenharmony_ci
1721cb0ef41Sopenharmony_ci  const bucket = bucketPath(cache, key)
1731cb0ef41Sopenharmony_ci  return rm(bucket, { recursive: true, force: true })
1741cb0ef41Sopenharmony_ci}
1751cb0ef41Sopenharmony_ci
1761cb0ef41Sopenharmony_cimodule.exports.lsStream = lsStream
1771cb0ef41Sopenharmony_ci
1781cb0ef41Sopenharmony_cifunction lsStream (cache) {
1791cb0ef41Sopenharmony_ci  const indexDir = bucketDir(cache)
1801cb0ef41Sopenharmony_ci  const stream = new Minipass({ objectMode: true })
1811cb0ef41Sopenharmony_ci
1821cb0ef41Sopenharmony_ci  // Set all this up to run on the stream and then just return the stream
1831cb0ef41Sopenharmony_ci  Promise.resolve().then(async () => {
1841cb0ef41Sopenharmony_ci    const buckets = await readdirOrEmpty(indexDir)
1851cb0ef41Sopenharmony_ci    await Promise.all(buckets.map(async (bucket) => {
1861cb0ef41Sopenharmony_ci      const bucketPath = path.join(indexDir, bucket)
1871cb0ef41Sopenharmony_ci      const subbuckets = await readdirOrEmpty(bucketPath)
1881cb0ef41Sopenharmony_ci      await Promise.all(subbuckets.map(async (subbucket) => {
1891cb0ef41Sopenharmony_ci        const subbucketPath = path.join(bucketPath, subbucket)
1901cb0ef41Sopenharmony_ci
1911cb0ef41Sopenharmony_ci        // "/cachename/<bucket 0xFF>/<bucket 0xFF>./*"
1921cb0ef41Sopenharmony_ci        const subbucketEntries = await readdirOrEmpty(subbucketPath)
1931cb0ef41Sopenharmony_ci        await Promise.all(subbucketEntries.map(async (entry) => {
1941cb0ef41Sopenharmony_ci          const entryPath = path.join(subbucketPath, entry)
1951cb0ef41Sopenharmony_ci          try {
1961cb0ef41Sopenharmony_ci            const entries = await bucketEntries(entryPath)
1971cb0ef41Sopenharmony_ci            // using a Map here prevents duplicate keys from showing up
1981cb0ef41Sopenharmony_ci            // twice, I guess?
1991cb0ef41Sopenharmony_ci            const reduced = entries.reduce((acc, entry) => {
2001cb0ef41Sopenharmony_ci              acc.set(entry.key, entry)
2011cb0ef41Sopenharmony_ci              return acc
2021cb0ef41Sopenharmony_ci            }, new Map())
2031cb0ef41Sopenharmony_ci            // reduced is a map of key => entry
2041cb0ef41Sopenharmony_ci            for (const entry of reduced.values()) {
2051cb0ef41Sopenharmony_ci              const formatted = formatEntry(cache, entry)
2061cb0ef41Sopenharmony_ci              if (formatted) {
2071cb0ef41Sopenharmony_ci                stream.write(formatted)
2081cb0ef41Sopenharmony_ci              }
2091cb0ef41Sopenharmony_ci            }
2101cb0ef41Sopenharmony_ci          } catch (err) {
2111cb0ef41Sopenharmony_ci            if (err.code === 'ENOENT') {
2121cb0ef41Sopenharmony_ci              return undefined
2131cb0ef41Sopenharmony_ci            }
2141cb0ef41Sopenharmony_ci            throw err
2151cb0ef41Sopenharmony_ci          }
2161cb0ef41Sopenharmony_ci        }))
2171cb0ef41Sopenharmony_ci      }))
2181cb0ef41Sopenharmony_ci    }))
2191cb0ef41Sopenharmony_ci    stream.end()
2201cb0ef41Sopenharmony_ci    return stream
2211cb0ef41Sopenharmony_ci  }).catch(err => stream.emit('error', err))
2221cb0ef41Sopenharmony_ci
2231cb0ef41Sopenharmony_ci  return stream
2241cb0ef41Sopenharmony_ci}
2251cb0ef41Sopenharmony_ci
2261cb0ef41Sopenharmony_cimodule.exports.ls = ls
2271cb0ef41Sopenharmony_ci
2281cb0ef41Sopenharmony_ciasync function ls (cache) {
2291cb0ef41Sopenharmony_ci  const entries = await lsStream(cache).collect()
2301cb0ef41Sopenharmony_ci  return entries.reduce((acc, xs) => {
2311cb0ef41Sopenharmony_ci    acc[xs.key] = xs
2321cb0ef41Sopenharmony_ci    return acc
2331cb0ef41Sopenharmony_ci  }, {})
2341cb0ef41Sopenharmony_ci}
2351cb0ef41Sopenharmony_ci
2361cb0ef41Sopenharmony_cimodule.exports.bucketEntries = bucketEntries
2371cb0ef41Sopenharmony_ci
2381cb0ef41Sopenharmony_ciasync function bucketEntries (bucket, filter) {
2391cb0ef41Sopenharmony_ci  const data = await readFile(bucket, 'utf8')
2401cb0ef41Sopenharmony_ci  return _bucketEntries(data, filter)
2411cb0ef41Sopenharmony_ci}
2421cb0ef41Sopenharmony_ci
2431cb0ef41Sopenharmony_cifunction _bucketEntries (data, filter) {
2441cb0ef41Sopenharmony_ci  const entries = []
2451cb0ef41Sopenharmony_ci  data.split('\n').forEach((entry) => {
2461cb0ef41Sopenharmony_ci    if (!entry) {
2471cb0ef41Sopenharmony_ci      return
2481cb0ef41Sopenharmony_ci    }
2491cb0ef41Sopenharmony_ci
2501cb0ef41Sopenharmony_ci    const pieces = entry.split('\t')
2511cb0ef41Sopenharmony_ci    if (!pieces[1] || hashEntry(pieces[1]) !== pieces[0]) {
2521cb0ef41Sopenharmony_ci      // Hash is no good! Corruption or malice? Doesn't matter!
2531cb0ef41Sopenharmony_ci      // EJECT EJECT
2541cb0ef41Sopenharmony_ci      return
2551cb0ef41Sopenharmony_ci    }
2561cb0ef41Sopenharmony_ci    let obj
2571cb0ef41Sopenharmony_ci    try {
2581cb0ef41Sopenharmony_ci      obj = JSON.parse(pieces[1])
2591cb0ef41Sopenharmony_ci    } catch (_) {
2601cb0ef41Sopenharmony_ci      // eslint-ignore-next-line no-empty-block
2611cb0ef41Sopenharmony_ci    }
2621cb0ef41Sopenharmony_ci    // coverage disabled here, no need to test with an entry that parses to something falsey
2631cb0ef41Sopenharmony_ci    // istanbul ignore else
2641cb0ef41Sopenharmony_ci    if (obj) {
2651cb0ef41Sopenharmony_ci      entries.push(obj)
2661cb0ef41Sopenharmony_ci    }
2671cb0ef41Sopenharmony_ci  })
2681cb0ef41Sopenharmony_ci  return entries
2691cb0ef41Sopenharmony_ci}
2701cb0ef41Sopenharmony_ci
2711cb0ef41Sopenharmony_cimodule.exports.bucketDir = bucketDir
2721cb0ef41Sopenharmony_ci
2731cb0ef41Sopenharmony_cifunction bucketDir (cache) {
2741cb0ef41Sopenharmony_ci  return path.join(cache, `index-v${indexV}`)
2751cb0ef41Sopenharmony_ci}
2761cb0ef41Sopenharmony_ci
2771cb0ef41Sopenharmony_cimodule.exports.bucketPath = bucketPath
2781cb0ef41Sopenharmony_ci
2791cb0ef41Sopenharmony_cifunction bucketPath (cache, key) {
2801cb0ef41Sopenharmony_ci  const hashed = hashKey(key)
2811cb0ef41Sopenharmony_ci  return path.join.apply(
2821cb0ef41Sopenharmony_ci    path,
2831cb0ef41Sopenharmony_ci    [bucketDir(cache)].concat(hashToSegments(hashed))
2841cb0ef41Sopenharmony_ci  )
2851cb0ef41Sopenharmony_ci}
2861cb0ef41Sopenharmony_ci
2871cb0ef41Sopenharmony_cimodule.exports.hashKey = hashKey
2881cb0ef41Sopenharmony_ci
2891cb0ef41Sopenharmony_cifunction hashKey (key) {
2901cb0ef41Sopenharmony_ci  return hash(key, 'sha256')
2911cb0ef41Sopenharmony_ci}
2921cb0ef41Sopenharmony_ci
2931cb0ef41Sopenharmony_cimodule.exports.hashEntry = hashEntry
2941cb0ef41Sopenharmony_ci
2951cb0ef41Sopenharmony_cifunction hashEntry (str) {
2961cb0ef41Sopenharmony_ci  return hash(str, 'sha1')
2971cb0ef41Sopenharmony_ci}
2981cb0ef41Sopenharmony_ci
2991cb0ef41Sopenharmony_cifunction hash (str, digest) {
3001cb0ef41Sopenharmony_ci  return crypto
3011cb0ef41Sopenharmony_ci    .createHash(digest)
3021cb0ef41Sopenharmony_ci    .update(str)
3031cb0ef41Sopenharmony_ci    .digest('hex')
3041cb0ef41Sopenharmony_ci}
3051cb0ef41Sopenharmony_ci
3061cb0ef41Sopenharmony_cifunction formatEntry (cache, entry, keepAll) {
3071cb0ef41Sopenharmony_ci  // Treat null digests as deletions. They'll shadow any previous entries.
3081cb0ef41Sopenharmony_ci  if (!entry.integrity && !keepAll) {
3091cb0ef41Sopenharmony_ci    return null
3101cb0ef41Sopenharmony_ci  }
3111cb0ef41Sopenharmony_ci
3121cb0ef41Sopenharmony_ci  return {
3131cb0ef41Sopenharmony_ci    key: entry.key,
3141cb0ef41Sopenharmony_ci    integrity: entry.integrity,
3151cb0ef41Sopenharmony_ci    path: entry.integrity ? contentPath(cache, entry.integrity) : undefined,
3161cb0ef41Sopenharmony_ci    size: entry.size,
3171cb0ef41Sopenharmony_ci    time: entry.time,
3181cb0ef41Sopenharmony_ci    metadata: entry.metadata,
3191cb0ef41Sopenharmony_ci  }
3201cb0ef41Sopenharmony_ci}
3211cb0ef41Sopenharmony_ci
3221cb0ef41Sopenharmony_cifunction readdirOrEmpty (dir) {
3231cb0ef41Sopenharmony_ci  return readdir(dir).catch((err) => {
3241cb0ef41Sopenharmony_ci    if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
3251cb0ef41Sopenharmony_ci      return []
3261cb0ef41Sopenharmony_ci    }
3271cb0ef41Sopenharmony_ci
3281cb0ef41Sopenharmony_ci    throw err
3291cb0ef41Sopenharmony_ci  })
3301cb0ef41Sopenharmony_ci}
331