1 | 'use strict'
|
---|
2 |
|
---|
3 | const util = require('util')
|
---|
4 |
|
---|
5 | const fs = require('fs')
|
---|
6 | const fsm = require('fs-minipass')
|
---|
7 | const ssri = require('ssri')
|
---|
8 | const contentPath = require('./path')
|
---|
9 | const Pipeline = require('minipass-pipeline')
|
---|
10 |
|
---|
11 | const lstat = util.promisify(fs.lstat)
|
---|
12 | const readFile = util.promisify(fs.readFile)
|
---|
13 |
|
---|
14 | module.exports = read
|
---|
15 |
|
---|
16 | const MAX_SINGLE_READ_SIZE = 64 * 1024 * 1024
|
---|
17 | function read (cache, integrity, opts = {}) {
|
---|
18 | const { size } = opts
|
---|
19 | return withContentSri(cache, integrity, (cpath, sri) => {
|
---|
20 | // get size
|
---|
21 | return lstat(cpath).then(stat => ({ stat, cpath, sri }))
|
---|
22 | }).then(({ stat, cpath, sri }) => {
|
---|
23 | if (typeof size === 'number' && stat.size !== size)
|
---|
24 | throw sizeError(size, stat.size)
|
---|
25 |
|
---|
26 | if (stat.size > MAX_SINGLE_READ_SIZE)
|
---|
27 | return readPipeline(cpath, stat.size, sri, new Pipeline()).concat()
|
---|
28 |
|
---|
29 | return readFile(cpath, null).then((data) => {
|
---|
30 | if (!ssri.checkData(data, sri))
|
---|
31 | throw integrityError(sri, cpath)
|
---|
32 |
|
---|
33 | return data
|
---|
34 | })
|
---|
35 | })
|
---|
36 | }
|
---|
37 |
|
---|
38 | const readPipeline = (cpath, size, sri, stream) => {
|
---|
39 | stream.push(
|
---|
40 | new fsm.ReadStream(cpath, {
|
---|
41 | size,
|
---|
42 | readSize: MAX_SINGLE_READ_SIZE,
|
---|
43 | }),
|
---|
44 | ssri.integrityStream({
|
---|
45 | integrity: sri,
|
---|
46 | size,
|
---|
47 | })
|
---|
48 | )
|
---|
49 | return stream
|
---|
50 | }
|
---|
51 |
|
---|
52 | module.exports.sync = readSync
|
---|
53 |
|
---|
54 | function readSync (cache, integrity, opts = {}) {
|
---|
55 | const { size } = opts
|
---|
56 | return withContentSriSync(cache, integrity, (cpath, sri) => {
|
---|
57 | const data = fs.readFileSync(cpath)
|
---|
58 | if (typeof size === 'number' && size !== data.length)
|
---|
59 | throw sizeError(size, data.length)
|
---|
60 |
|
---|
61 | if (ssri.checkData(data, sri))
|
---|
62 | return data
|
---|
63 |
|
---|
64 | throw integrityError(sri, cpath)
|
---|
65 | })
|
---|
66 | }
|
---|
67 |
|
---|
68 | module.exports.stream = readStream
|
---|
69 | module.exports.readStream = readStream
|
---|
70 |
|
---|
71 | function readStream (cache, integrity, opts = {}) {
|
---|
72 | const { size } = opts
|
---|
73 | const stream = new Pipeline()
|
---|
74 | withContentSri(cache, integrity, (cpath, sri) => {
|
---|
75 | // just lstat to ensure it exists
|
---|
76 | return lstat(cpath).then((stat) => ({ stat, cpath, sri }))
|
---|
77 | }).then(({ stat, cpath, sri }) => {
|
---|
78 | if (typeof size === 'number' && size !== stat.size)
|
---|
79 | return stream.emit('error', sizeError(size, stat.size))
|
---|
80 |
|
---|
81 | readPipeline(cpath, stat.size, sri, stream)
|
---|
82 | }, er => stream.emit('error', er))
|
---|
83 |
|
---|
84 | return stream
|
---|
85 | }
|
---|
86 |
|
---|
87 | let copyFile
|
---|
88 | if (fs.copyFile) {
|
---|
89 | module.exports.copy = copy
|
---|
90 | module.exports.copy.sync = copySync
|
---|
91 | copyFile = util.promisify(fs.copyFile)
|
---|
92 | }
|
---|
93 |
|
---|
94 | function copy (cache, integrity, dest) {
|
---|
95 | return withContentSri(cache, integrity, (cpath, sri) => {
|
---|
96 | return copyFile(cpath, dest)
|
---|
97 | })
|
---|
98 | }
|
---|
99 |
|
---|
100 | function copySync (cache, integrity, dest) {
|
---|
101 | return withContentSriSync(cache, integrity, (cpath, sri) => {
|
---|
102 | return fs.copyFileSync(cpath, dest)
|
---|
103 | })
|
---|
104 | }
|
---|
105 |
|
---|
106 | module.exports.hasContent = hasContent
|
---|
107 |
|
---|
108 | function hasContent (cache, integrity) {
|
---|
109 | if (!integrity)
|
---|
110 | return Promise.resolve(false)
|
---|
111 |
|
---|
112 | return withContentSri(cache, integrity, (cpath, sri) => {
|
---|
113 | return lstat(cpath).then((stat) => ({ size: stat.size, sri, stat }))
|
---|
114 | }).catch((err) => {
|
---|
115 | if (err.code === 'ENOENT')
|
---|
116 | return false
|
---|
117 |
|
---|
118 | if (err.code === 'EPERM') {
|
---|
119 | /* istanbul ignore else */
|
---|
120 | if (process.platform !== 'win32')
|
---|
121 | throw err
|
---|
122 | else
|
---|
123 | return false
|
---|
124 | }
|
---|
125 | })
|
---|
126 | }
|
---|
127 |
|
---|
128 | module.exports.hasContent.sync = hasContentSync
|
---|
129 |
|
---|
130 | function hasContentSync (cache, integrity) {
|
---|
131 | if (!integrity)
|
---|
132 | return false
|
---|
133 |
|
---|
134 | return withContentSriSync(cache, integrity, (cpath, sri) => {
|
---|
135 | try {
|
---|
136 | const stat = fs.lstatSync(cpath)
|
---|
137 | return { size: stat.size, sri, stat }
|
---|
138 | } catch (err) {
|
---|
139 | if (err.code === 'ENOENT')
|
---|
140 | return false
|
---|
141 |
|
---|
142 | if (err.code === 'EPERM') {
|
---|
143 | /* istanbul ignore else */
|
---|
144 | if (process.platform !== 'win32')
|
---|
145 | throw err
|
---|
146 | else
|
---|
147 | return false
|
---|
148 | }
|
---|
149 | }
|
---|
150 | })
|
---|
151 | }
|
---|
152 |
|
---|
153 | function withContentSri (cache, integrity, fn) {
|
---|
154 | const tryFn = () => {
|
---|
155 | const sri = ssri.parse(integrity)
|
---|
156 | // If `integrity` has multiple entries, pick the first digest
|
---|
157 | // with available local data.
|
---|
158 | const algo = sri.pickAlgorithm()
|
---|
159 | const digests = sri[algo]
|
---|
160 |
|
---|
161 | if (digests.length <= 1) {
|
---|
162 | const cpath = contentPath(cache, digests[0])
|
---|
163 | return fn(cpath, digests[0])
|
---|
164 | } else {
|
---|
165 | // Can't use race here because a generic error can happen before
|
---|
166 | // a ENOENT error, and can happen before a valid result
|
---|
167 | return Promise
|
---|
168 | .all(digests.map((meta) => {
|
---|
169 | return withContentSri(cache, meta, fn)
|
---|
170 | .catch((err) => {
|
---|
171 | if (err.code === 'ENOENT') {
|
---|
172 | return Object.assign(
|
---|
173 | new Error('No matching content found for ' + sri.toString()),
|
---|
174 | { code: 'ENOENT' }
|
---|
175 | )
|
---|
176 | }
|
---|
177 | return err
|
---|
178 | })
|
---|
179 | }))
|
---|
180 | .then((results) => {
|
---|
181 | // Return the first non error if it is found
|
---|
182 | const result = results.find((r) => !(r instanceof Error))
|
---|
183 | if (result)
|
---|
184 | return result
|
---|
185 |
|
---|
186 | // Throw the No matching content found error
|
---|
187 | const enoentError = results.find((r) => r.code === 'ENOENT')
|
---|
188 | if (enoentError)
|
---|
189 | throw enoentError
|
---|
190 |
|
---|
191 | // Throw generic error
|
---|
192 | throw results.find((r) => r instanceof Error)
|
---|
193 | })
|
---|
194 | }
|
---|
195 | }
|
---|
196 |
|
---|
197 | return new Promise((resolve, reject) => {
|
---|
198 | try {
|
---|
199 | tryFn()
|
---|
200 | .then(resolve)
|
---|
201 | .catch(reject)
|
---|
202 | } catch (err) {
|
---|
203 | reject(err)
|
---|
204 | }
|
---|
205 | })
|
---|
206 | }
|
---|
207 |
|
---|
208 | function withContentSriSync (cache, integrity, fn) {
|
---|
209 | const sri = ssri.parse(integrity)
|
---|
210 | // If `integrity` has multiple entries, pick the first digest
|
---|
211 | // with available local data.
|
---|
212 | const algo = sri.pickAlgorithm()
|
---|
213 | const digests = sri[algo]
|
---|
214 | if (digests.length <= 1) {
|
---|
215 | const cpath = contentPath(cache, digests[0])
|
---|
216 | return fn(cpath, digests[0])
|
---|
217 | } else {
|
---|
218 | let lastErr = null
|
---|
219 | for (const meta of digests) {
|
---|
220 | try {
|
---|
221 | return withContentSriSync(cache, meta, fn)
|
---|
222 | } catch (err) {
|
---|
223 | lastErr = err
|
---|
224 | }
|
---|
225 | }
|
---|
226 | throw lastErr
|
---|
227 | }
|
---|
228 | }
|
---|
229 |
|
---|
230 | function sizeError (expected, found) {
|
---|
231 | const err = new Error(`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
|
---|
232 | err.expected = expected
|
---|
233 | err.found = found
|
---|
234 | err.code = 'EBADSIZE'
|
---|
235 | return err
|
---|
236 | }
|
---|
237 |
|
---|
238 | function integrityError (sri, path) {
|
---|
239 | const err = new Error(`Integrity verification failed for ${sri} (${path})`)
|
---|
240 | err.code = 'EINTEGRITY'
|
---|
241 | err.sri = sri
|
---|
242 | err.path = path
|
---|
243 | return err
|
---|
244 | }
|
---|