Skip to content

Commit 61785e1

Browse files
authoredMay 17, 2022
feat: allow external integrity/size source (#110)
1 parent 71d4389 commit 61785e1

File tree

3 files changed

+87
-13
lines changed

3 files changed

+87
-13
lines changed
 

‎README.md

+13
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,19 @@ with an `EINTEGRITY` error.
413413

414414
`algorithms` has no effect if this option is present.
415415

416+
##### `opts.integrityEmitter`
417+
418+
*Streaming only* If present, uses the provided event emitter as a source of
419+
truth for both integrity and size. This allows use cases where integrity is
420+
already being calculated outside of cacache to reuse that data instead of
421+
calculating it a second time.
422+
423+
The emitter must emit both the `'integrity'` and `'size'` events.
424+
425+
NOTE: If this option is provided, you must verify that you receive the correct
426+
integrity value yourself and emit an `'error'` event if there is a mismatch.
427+
[ssri Integrity Streams](https://github.com/npm/ssri#integrity-stream) do this for you when given an expected integrity.
428+
416429
##### `opts.algorithms`
417430

418431
Default: ['sha512']

‎lib/content/write.js

+16-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
const events = require('events')
34
const util = require('util')
45

56
const contentPath = require('./path')
@@ -114,6 +115,20 @@ async function handleContent (inputStream, cache, opts) {
114115
}
115116

116117
async function pipeToTmp (inputStream, cache, tmpTarget, opts) {
118+
const outStream = new fsm.WriteStream(tmpTarget, {
119+
flags: 'wx',
120+
})
121+
122+
if (opts.integrityEmitter) {
123+
// we need to create these all simultaneously since they can fire in any order
124+
const [integrity, size] = await Promise.all([
125+
events.once(opts.integrityEmitter, 'integrity').then(res => res[0]),
126+
events.once(opts.integrityEmitter, 'size').then(res => res[0]),
127+
new Pipeline(inputStream, outStream).promise(),
128+
])
129+
return { integrity, size }
130+
}
131+
117132
let integrity
118133
let size
119134
const hashStream = ssri.integrityStream({
@@ -128,19 +143,7 @@ async function pipeToTmp (inputStream, cache, tmpTarget, opts) {
128143
size = s
129144
})
130145

131-
const outStream = new fsm.WriteStream(tmpTarget, {
132-
flags: 'wx',
133-
})
134-
135-
// NB: this can throw if the hashStream has a problem with
136-
// it, and the data is fully written. but pipeToTmp is only
137-
// called in promisory contexts where that is handled.
138-
const pipeline = new Pipeline(
139-
inputStream,
140-
hashStream,
141-
outStream
142-
)
143-
146+
const pipeline = new Pipeline(inputStream, hashStream, outStream)
144147
await pipeline.promise()
145148
return { integrity, size }
146149
}

‎test/content/write.js

+58
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict'
22

3+
const events = require('events')
34
const fs = require('@npmcli/fs')
5+
const Minipass = require('minipass')
46
const path = require('path')
57
const rimraf = require('rimraf')
68
const ssri = require('ssri')
@@ -32,6 +34,62 @@ t.test('basic put', (t) => {
3234
})
3335
})
3436

37+
t.test('basic put, providing external integrity emitter', async (t) => {
38+
const CACHE = t.testdir()
39+
const CONTENT = 'foobarbaz'
40+
const INTEGRITY = ssri.fromData(CONTENT)
41+
42+
const write = t.mock('../../lib/content/write.js', {
43+
ssri: {
44+
...ssri,
45+
integrityStream: () => {
46+
throw new Error('Should not be called')
47+
},
48+
},
49+
})
50+
51+
const source = new Minipass().end(CONTENT)
52+
53+
const tee = new Minipass()
54+
55+
const integrityStream = ssri.integrityStream()
56+
// since the integrityStream is not going anywhere, we need to manually resume it
57+
// otherwise it'll get stuck in paused mode and will never process any data events
58+
integrityStream.resume()
59+
const integrityStreamP = Promise.all([
60+
events.once(integrityStream, 'integrity').then((res) => res[0]),
61+
events.once(integrityStream, 'size').then((res) => res[0]),
62+
])
63+
64+
const contentStream = write.stream(CACHE, { integrityEmitter: integrityStream })
65+
const contentStreamP = Promise.all([
66+
events.once(contentStream, 'integrity').then((res) => res[0]),
67+
events.once(contentStream, 'size').then((res) => res[0]),
68+
contentStream.promise(),
69+
])
70+
71+
tee.pipe(integrityStream)
72+
tee.pipe(contentStream)
73+
source.pipe(tee)
74+
75+
const [
76+
[ssriIntegrity, ssriSize],
77+
[contentIntegrity, contentSize],
78+
] = await Promise.all([
79+
integrityStreamP,
80+
contentStreamP,
81+
])
82+
83+
t.equal(ssriSize, CONTENT.length, 'ssri got the right size')
84+
t.equal(contentSize, CONTENT.length, 'content got the right size')
85+
t.same(ssriIntegrity, INTEGRITY, 'ssri got the right integrity')
86+
t.same(contentIntegrity, INTEGRITY, 'content got the right integrity')
87+
88+
const cpath = contentPath(CACHE, ssriIntegrity)
89+
t.ok(fs.lstatSync(cpath).isFile(), 'content inserted as a single file')
90+
t.equal(fs.readFileSync(cpath, 'utf8'), CONTENT, 'contents are identical to inserted content')
91+
})
92+
3593
t.test("checks input digest doesn't match data", (t) => {
3694
const CONTENT = 'foobarbaz'
3795
const integrity = ssri.fromData(CONTENT)

0 commit comments

Comments
 (0)
Please sign in to comment.