Skip to content

Commit fdb9e5a

Browse files
nlfwraithgar
authored andcommittedJun 1, 2022
feat: allow reuse of external integrity stream
1 parent 63e1d51 commit fdb9e5a

File tree

3 files changed

+42
-24
lines changed

3 files changed

+42
-24
lines changed
 

‎lib/fetcher.js

+21-18
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const removeTrailingSlashes = require('./util/trailing-slashes.js')
1818
const getContents = require('@npmcli/installed-package-contents')
1919
const readPackageJsonFast = require('read-package-json-fast')
2020
const readPackageJson = promisify(require('read-package-json'))
21+
const Minipass = require('minipass')
2122

2223
// we only change ownership on unix platforms, and only if uid is 0
2324
const selfOwner = process.getuid && process.getuid() === 0 ? {
@@ -219,40 +220,42 @@ class FetcherBase {
219220
}
220221

221222
[_istream] (stream) {
222-
// everyone will need one of these, either for verifying or calculating
223-
// We always set it, because we have might only have a weak legacy hex
224-
// sha1 in the packument, and this MAY upgrade it to a stronger algo.
225-
// If we had an integrity, and it doesn't match, then this does not
226-
// override that error; the istream will raise the error before it
227-
// gets to the point of re-setting the integrity.
228-
const istream = ssri.integrityStream(this.opts)
229-
istream.on('integrity', i => this.integrity = i)
230-
stream.on('error', er => istream.emit('error', er))
231-
232-
// if not caching this, just pipe through to the istream and return it
223+
// if not caching this, just return it
233224
if (!this.opts.cache || !this[_cacheFetches]) {
225+
// instead of creating a new integrity stream, we only piggyback on the
226+
// provided stream's events
227+
if (stream.hasIntegrityEmitter) {
228+
stream.on('integrity', i => this.integrity = i)
229+
return stream
230+
}
231+
232+
const istream = ssri.integrityStream(this.opts)
233+
istream.on('integrity', i => this.integrity = i)
234+
stream.on('error', err => istream.emit('error', err))
234235
return stream.pipe(istream)
235236
}
236237

237238
// we have to return a stream that gets ALL the data, and proxies errors,
238239
// but then pipe from the original tarball stream into the cache as well.
239240
// To do this without losing any data, and since the cacache put stream
240241
// is not a passthrough, we have to pipe from the original stream into
241-
// the cache AFTER we pipe into the istream. Since the cache stream
242+
// the cache AFTER we pipe into the middleStream. Since the cache stream
242243
// has an asynchronous flush to write its contents to disk, we need to
243-
// defer the istream end until the cache stream ends.
244-
stream.pipe(istream, { end: false })
244+
// defer the middleStream end until the cache stream ends.
245+
const middleStream = new Minipass()
246+
stream.on('error', err => middleStream.emit('error', err))
247+
stream.pipe(middleStream, { end: false })
245248
const cstream = cacache.put.stream(
246249
this.opts.cache,
247250
`pacote:tarball:${this.from}`,
248251
this.opts
249252
)
253+
cstream.on('integrity', i => this.integrity = i)
254+
cstream.on('error', err => stream.emit('error', err))
250255
stream.pipe(cstream)
251-
// defer istream end until after cstream
252-
// cache write errors should not crash the fetch, this is best-effort.
253-
cstream.promise().catch(() => {}).then(() => istream.end())
254256

255-
return istream
257+
cstream.promise().catch(() => {}).then(() => middleStream.end())
258+
return middleStream
256259
}
257260

258261
pickIntegrityAlgorithm () {

‎lib/remote.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,28 @@ class RemoteFetcher extends Fetcher {
3131

3232
[_tarballFromResolved] () {
3333
const stream = new Minipass()
34+
stream.hasIntegrityEmitter = true
35+
3436
const fetchOpts = {
3537
...this.opts,
3638
headers: this[_headers](),
3739
spec: this.spec,
3840
integrity: this.integrity,
3941
algorithms: [this.pickIntegrityAlgorithm()],
4042
}
41-
fetch(this.resolved, fetchOpts).then(res => {
42-
const hash = res.headers.get('x-local-cache-hash')
43-
if (hash) {
44-
this.integrity = decodeURIComponent(hash)
45-
}
4643

44+
fetch(this.resolved, fetchOpts).then(res => {
4745
res.body.on('error',
4846
/* istanbul ignore next - exceedingly rare and hard to simulate */
4947
er => stream.emit('error', er)
50-
).pipe(stream)
48+
)
49+
50+
res.body.on('integrity', i => {
51+
this.integrity = i
52+
stream.emit('integrity', i)
53+
})
54+
55+
res.body.pipe(stream)
5156
}).catch(er => stream.emit('error', er))
5257

5358
return stream

‎test/remote.js

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const RemoteFetcher = require('../lib/remote.js')
22
const http = require('http')
3+
const ssri = require('ssri')
34
const t = require('tap')
45

56
const { resolve } = require('path')
@@ -12,9 +13,11 @@ const fs = require('fs')
1213
const abbrev = resolve(__dirname, 'fixtures/abbrev-1.1.1.tgz')
1314
const port = 12345 + (+process.env.TAP_CHILD_ID || 0)
1415
const server = `http://localhost:${port}`
16+
let abbrevIntegrity
1517
const requestLog = []
1618
t.test('start server', t => {
1719
const data = fs.readFileSync(abbrev)
20+
abbrevIntegrity = ssri.fromData(data)
1821
const httpServer = http.createServer((req, res) => {
1922
res.setHeader('cache-control', 'max-age=432000')
2023
res.setHeader('accept-ranges', 'bytes')
@@ -112,6 +115,13 @@ t.test('bad integrity', t => {
112115
})
113116
})
114117

118+
t.test('known integrity', async t => {
119+
const url = `${server}/abbrev.tgz`
120+
const f = new RemoteFetcher(url, { cache, integrity: abbrevIntegrity })
121+
await f.extract(me + '/good-integrity')
122+
t.same(f.integrity, abbrevIntegrity, 'got the right integrity back out')
123+
})
124+
115125
t.test('an missing tarball', t => {
116126
const url = `${server}/404`
117127
const f = new RemoteFetcher(url, { cache })

0 commit comments

Comments
 (0)
Please sign in to comment.