| 'use strict' |
| |
| const util = require('util') |
| |
| const fs = require('fs') |
| const fsm = require('fs-minipass') |
| const ssri = require('ssri') |
| const contentPath = require('./path') |
| const Pipeline = require('minipass-pipeline') |
| |
| const lstat = util.promisify(fs.lstat) |
| const readFile = util.promisify(fs.readFile) |
| |
| module.exports = read |
| |
| const MAX_SINGLE_READ_SIZE = 64 * 1024 * 1024 |
| function read (cache, integrity, opts = {}) { |
| const { size } = opts |
| return withContentSri(cache, integrity, (cpath, sri) => { |
| // get size |
| return lstat(cpath).then(stat => ({ stat, cpath, sri })) |
| }).then(({ stat, cpath, sri }) => { |
| if (typeof size === 'number' && stat.size !== size) |
| throw sizeError(size, stat.size) |
| |
| if (stat.size > MAX_SINGLE_READ_SIZE) |
| return readPipeline(cpath, stat.size, sri, new Pipeline()).concat() |
| |
| return readFile(cpath, null).then((data) => { |
| if (!ssri.checkData(data, sri)) |
| throw integrityError(sri, cpath) |
| |
| return data |
| }) |
| }) |
| } |
| |
| const readPipeline = (cpath, size, sri, stream) => { |
| stream.push( |
| new fsm.ReadStream(cpath, { |
| size, |
| readSize: MAX_SINGLE_READ_SIZE, |
| }), |
| ssri.integrityStream({ |
| integrity: sri, |
| size, |
| }) |
| ) |
| return stream |
| } |
| |
| module.exports.sync = readSync |
| |
| function readSync (cache, integrity, opts = {}) { |
| const { size } = opts |
| return withContentSriSync(cache, integrity, (cpath, sri) => { |
| const data = fs.readFileSync(cpath) |
| if (typeof size === 'number' && size !== data.length) |
| throw sizeError(size, data.length) |
| |
| if (ssri.checkData(data, sri)) |
| return data |
| |
| throw integrityError(sri, cpath) |
| }) |
| } |
| |
| module.exports.stream = readStream |
| module.exports.readStream = readStream |
| |
| function readStream (cache, integrity, opts = {}) { |
| const { size } = opts |
| const stream = new Pipeline() |
| withContentSri(cache, integrity, (cpath, sri) => { |
| // just lstat to ensure it exists |
| return lstat(cpath).then((stat) => ({ stat, cpath, sri })) |
| }).then(({ stat, cpath, sri }) => { |
| if (typeof size === 'number' && size !== stat.size) |
| return stream.emit('error', sizeError(size, stat.size)) |
| |
| readPipeline(cpath, stat.size, sri, stream) |
| }, er => stream.emit('error', er)) |
| |
| return stream |
| } |
| |
| let copyFile |
| if (fs.copyFile) { |
| module.exports.copy = copy |
| module.exports.copy.sync = copySync |
| copyFile = util.promisify(fs.copyFile) |
| } |
| |
| function copy (cache, integrity, dest) { |
| return withContentSri(cache, integrity, (cpath, sri) => { |
| return copyFile(cpath, dest) |
| }) |
| } |
| |
| function copySync (cache, integrity, dest) { |
| return withContentSriSync(cache, integrity, (cpath, sri) => { |
| return fs.copyFileSync(cpath, dest) |
| }) |
| } |
| |
| module.exports.hasContent = hasContent |
| |
| function hasContent (cache, integrity) { |
| if (!integrity) |
| return Promise.resolve(false) |
| |
| return withContentSri(cache, integrity, (cpath, sri) => { |
| return lstat(cpath).then((stat) => ({ size: stat.size, sri, stat })) |
| }).catch((err) => { |
| if (err.code === 'ENOENT') |
| return false |
| |
| if (err.code === 'EPERM') { |
| /* istanbul ignore else */ |
| if (process.platform !== 'win32') |
| throw err |
| else |
| return false |
| } |
| }) |
| } |
| |
| module.exports.hasContent.sync = hasContentSync |
| |
| function hasContentSync (cache, integrity) { |
| if (!integrity) |
| return false |
| |
| return withContentSriSync(cache, integrity, (cpath, sri) => { |
| try { |
| const stat = fs.lstatSync(cpath) |
| return { size: stat.size, sri, stat } |
| } catch (err) { |
| if (err.code === 'ENOENT') |
| return false |
| |
| if (err.code === 'EPERM') { |
| /* istanbul ignore else */ |
| if (process.platform !== 'win32') |
| throw err |
| else |
| return false |
| } |
| } |
| }) |
| } |
| |
| function withContentSri (cache, integrity, fn) { |
| const tryFn = () => { |
| const sri = ssri.parse(integrity) |
| // If `integrity` has multiple entries, pick the first digest |
| // with available local data. |
| const algo = sri.pickAlgorithm() |
| const digests = sri[algo] |
| |
| if (digests.length <= 1) { |
| const cpath = contentPath(cache, digests[0]) |
| return fn(cpath, digests[0]) |
| } else { |
| // Can't use race here because a generic error can happen before |
| // a ENOENT error, and can happen before a valid result |
| return Promise |
| .all(digests.map((meta) => { |
| return withContentSri(cache, meta, fn) |
| .catch((err) => { |
| if (err.code === 'ENOENT') { |
| return Object.assign( |
| new Error('No matching content found for ' + sri.toString()), |
| { code: 'ENOENT' } |
| ) |
| } |
| return err |
| }) |
| })) |
| .then((results) => { |
| // Return the first non error if it is found |
| const result = results.find((r) => !(r instanceof Error)) |
| if (result) |
| return result |
| |
| // Throw the No matching content found error |
| const enoentError = results.find((r) => r.code === 'ENOENT') |
| if (enoentError) |
| throw enoentError |
| |
| // Throw generic error |
| throw results.find((r) => r instanceof Error) |
| }) |
| } |
| } |
| |
| return new Promise((resolve, reject) => { |
| try { |
| tryFn() |
| .then(resolve) |
| .catch(reject) |
| } catch (err) { |
| reject(err) |
| } |
| }) |
| } |
| |
| function withContentSriSync (cache, integrity, fn) { |
| const sri = ssri.parse(integrity) |
| // If `integrity` has multiple entries, pick the first digest |
| // with available local data. |
| const algo = sri.pickAlgorithm() |
| const digests = sri[algo] |
| if (digests.length <= 1) { |
| const cpath = contentPath(cache, digests[0]) |
| return fn(cpath, digests[0]) |
| } else { |
| let lastErr = null |
| for (const meta of digests) { |
| try { |
| return withContentSriSync(cache, meta, fn) |
| } catch (err) { |
| lastErr = err |
| } |
| } |
| throw lastErr |
| } |
| } |
| |
| function sizeError (expected, found) { |
| const err = new Error(`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`) |
| err.expected = expected |
| err.found = found |
| err.code = 'EBADSIZE' |
| return err |
| } |
| |
| function integrityError (sri, path) { |
| const err = new Error(`Integrity verification failed for ${sri} (${path})`) |
| err.code = 'EINTEGRITY' |
| err.sri = sri |
| err.path = path |
| return err |
| } |