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