• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const fs = require('fs/promises')
4const fsm = require('fs-minipass')
5const ssri = require('ssri')
6const contentPath = require('./path')
7const Pipeline = require('minipass-pipeline')
8
9module.exports = read
10
11const MAX_SINGLE_READ_SIZE = 64 * 1024 * 1024
12async function read (cache, integrity, opts = {}) {
13  const { size } = opts
14  const { stat, cpath, sri } = await withContentSri(cache, integrity, async (cpath, sri) => {
15    // get size
16    const stat = await fs.stat(cpath)
17    return { stat, cpath, sri }
18  })
19  if (typeof size === 'number' && stat.size !== size) {
20    throw sizeError(size, stat.size)
21  }
22
23  if (stat.size > MAX_SINGLE_READ_SIZE) {
24    return readPipeline(cpath, stat.size, sri, new Pipeline()).concat()
25  }
26
27  const data = await fs.readFile(cpath, { encoding: null })
28  if (!ssri.checkData(data, sri)) {
29    throw integrityError(sri, cpath)
30  }
31
32  return data
33}
34
35const readPipeline = (cpath, size, sri, stream) => {
36  stream.push(
37    new fsm.ReadStream(cpath, {
38      size,
39      readSize: MAX_SINGLE_READ_SIZE,
40    }),
41    ssri.integrityStream({
42      integrity: sri,
43      size,
44    })
45  )
46  return stream
47}
48
49module.exports.stream = readStream
50module.exports.readStream = readStream
51
52function readStream (cache, integrity, opts = {}) {
53  const { size } = opts
54  const stream = new Pipeline()
55  // Set all this up to run on the stream and then just return the stream
56  Promise.resolve().then(async () => {
57    const { stat, cpath, sri } = await withContentSri(cache, integrity, async (cpath, sri) => {
58      // just stat to ensure it exists
59      const stat = await fs.stat(cpath)
60      return { stat, cpath, sri }
61    })
62    if (typeof size === 'number' && size !== stat.size) {
63      return stream.emit('error', sizeError(size, stat.size))
64    }
65
66    return readPipeline(cpath, stat.size, sri, stream)
67  }).catch(err => stream.emit('error', err))
68
69  return stream
70}
71
72module.exports.copy = copy
73
74function copy (cache, integrity, dest) {
75  return withContentSri(cache, integrity, (cpath, sri) => {
76    return fs.copyFile(cpath, dest)
77  })
78}
79
80module.exports.hasContent = hasContent
81
82async function hasContent (cache, integrity) {
83  if (!integrity) {
84    return false
85  }
86
87  try {
88    return await withContentSri(cache, integrity, async (cpath, sri) => {
89      const stat = await fs.stat(cpath)
90      return { size: stat.size, sri, stat }
91    })
92  } catch (err) {
93    if (err.code === 'ENOENT') {
94      return false
95    }
96
97    if (err.code === 'EPERM') {
98      /* istanbul ignore else */
99      if (process.platform !== 'win32') {
100        throw err
101      } else {
102        return false
103      }
104    }
105  }
106}
107
108async function withContentSri (cache, integrity, fn) {
109  const sri = ssri.parse(integrity)
110  // If `integrity` has multiple entries, pick the first digest
111  // with available local data.
112  const algo = sri.pickAlgorithm()
113  const digests = sri[algo]
114
115  if (digests.length <= 1) {
116    const cpath = contentPath(cache, digests[0])
117    return fn(cpath, digests[0])
118  } else {
119    // Can't use race here because a generic error can happen before
120    // a ENOENT error, and can happen before a valid result
121    const results = await Promise.all(digests.map(async (meta) => {
122      try {
123        return await withContentSri(cache, meta, fn)
124      } catch (err) {
125        if (err.code === 'ENOENT') {
126          return Object.assign(
127            new Error('No matching content found for ' + sri.toString()),
128            { code: 'ENOENT' }
129          )
130        }
131        return err
132      }
133    }))
134    // Return the first non error if it is found
135    const result = results.find((r) => !(r instanceof Error))
136    if (result) {
137      return result
138    }
139
140    // Throw the No matching content found error
141    const enoentError = results.find((r) => r.code === 'ENOENT')
142    if (enoentError) {
143      throw enoentError
144    }
145
146    // Throw generic error
147    throw results.find((r) => r instanceof Error)
148  }
149}
150
151function sizeError (expected, found) {
152  /* eslint-disable-next-line max-len */
153  const err = new Error(`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
154  err.expected = expected
155  err.found = found
156  err.code = 'EBADSIZE'
157  return err
158}
159
160function integrityError (sri, path) {
161  const err = new Error(`Integrity verification failed for ${sri} (${path})`)
162  err.code = 'EINTEGRITY'
163  err.sri = sri
164  err.path = path
165  return err
166}
167