Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow external integrity/size source #110

Merged
merged 1 commit into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,19 @@ with an `EINTEGRITY` error.

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

##### `opts.integrityEmitter`

*Streaming only* If present, uses the provided event emitter as a source of
truth for both integrity and size. This allows use cases where integrity is
already being calculated outside of cacache to reuse that data instead of
calculating it a second time.

The emitter must emit both the `'integrity'` and `'size'` events.

NOTE: If this option is provided, you must verify that you receive the correct
integrity value yourself and emit an `'error'` event if there is a mismatch.
[ssri Integrity Streams](https://github.com/npm/ssri#integrity-stream) do this for you when given an expected integrity.

##### `opts.algorithms`

Default: ['sha512']
Expand Down
29 changes: 16 additions & 13 deletions lib/content/write.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const events = require('events')
const util = require('util')

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

async function pipeToTmp (inputStream, cache, tmpTarget, opts) {
const outStream = new fsm.WriteStream(tmpTarget, {
flags: 'wx',
})

if (opts.integrityEmitter) {
// we need to create these all simultaneously since they can fire in any order
const [integrity, size] = await Promise.all([
events.once(opts.integrityEmitter, 'integrity').then(res => res[0]),
events.once(opts.integrityEmitter, 'size').then(res => res[0]),
new Pipeline(inputStream, outStream).promise(),
])
return { integrity, size }
}

let integrity
let size
const hashStream = ssri.integrityStream({
Expand All @@ -128,19 +143,7 @@ async function pipeToTmp (inputStream, cache, tmpTarget, opts) {
size = s
})

const outStream = new fsm.WriteStream(tmpTarget, {
flags: 'wx',
})

// NB: this can throw if the hashStream has a problem with
// it, and the data is fully written. but pipeToTmp is only
// called in promisory contexts where that is handled.
const pipeline = new Pipeline(
inputStream,
hashStream,
outStream
)

const pipeline = new Pipeline(inputStream, hashStream, outStream)
await pipeline.promise()
return { integrity, size }
}
Expand Down
58 changes: 58 additions & 0 deletions test/content/write.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict'

const events = require('events')
const fs = require('@npmcli/fs')
const Minipass = require('minipass')
const path = require('path')
const rimraf = require('rimraf')
const ssri = require('ssri')
Expand Down Expand Up @@ -32,6 +34,62 @@ t.test('basic put', (t) => {
})
})

t.test('basic put, providing external integrity emitter', async (t) => {
const CACHE = t.testdir()
const CONTENT = 'foobarbaz'
const INTEGRITY = ssri.fromData(CONTENT)

const write = t.mock('../../lib/content/write.js', {
ssri: {
...ssri,
integrityStream: () => {
throw new Error('Should not be called')
},
},
})

const source = new Minipass().end(CONTENT)

const tee = new Minipass()

const integrityStream = ssri.integrityStream()
// since the integrityStream is not going anywhere, we need to manually resume it
// otherwise it'll get stuck in paused mode and will never process any data events
integrityStream.resume()
const integrityStreamP = Promise.all([
events.once(integrityStream, 'integrity').then((res) => res[0]),
events.once(integrityStream, 'size').then((res) => res[0]),
])

const contentStream = write.stream(CACHE, { integrityEmitter: integrityStream })
const contentStreamP = Promise.all([
events.once(contentStream, 'integrity').then((res) => res[0]),
events.once(contentStream, 'size').then((res) => res[0]),
contentStream.promise(),
])

tee.pipe(integrityStream)
tee.pipe(contentStream)
source.pipe(tee)

const [
[ssriIntegrity, ssriSize],
[contentIntegrity, contentSize],
] = await Promise.all([
integrityStreamP,
contentStreamP,
])

t.equal(ssriSize, CONTENT.length, 'ssri got the right size')
t.equal(contentSize, CONTENT.length, 'content got the right size')
t.same(ssriIntegrity, INTEGRITY, 'ssri got the right integrity')
t.same(contentIntegrity, INTEGRITY, 'content got the right integrity')

const cpath = contentPath(CACHE, ssriIntegrity)
t.ok(fs.lstatSync(cpath).isFile(), 'content inserted as a single file')
t.equal(fs.readFileSync(cpath, 'utf8'), CONTENT, 'contents are identical to inserted content')
})

t.test("checks input digest doesn't match data", (t) => {
const CONTENT = 'foobarbaz'
const integrity = ssri.fromData(CONTENT)
Expand Down