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