read.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. 'use strict'
  2. const util = require('util')
  3. const fs = require('fs')
  4. const fsm = require('fs-minipass')
  5. const ssri = require('ssri')
  6. const contentPath = require('./path')
  7. const Pipeline = require('minipass-pipeline')
  8. const lstat = util.promisify(fs.lstat)
  9. const readFile = util.promisify(fs.readFile)
  10. module.exports = read
  11. const MAX_SINGLE_READ_SIZE = 64 * 1024 * 1024
  12. function read (cache, integrity, opts = {}) {
  13. const { size } = opts
  14. return withContentSri(cache, integrity, (cpath, sri) => {
  15. // get size
  16. return lstat(cpath).then(stat => ({ stat, cpath, sri }))
  17. }).then(({ stat, cpath, sri }) => {
  18. if (typeof size === 'number' && stat.size !== size)
  19. throw sizeError(size, stat.size)
  20. if (stat.size > MAX_SINGLE_READ_SIZE)
  21. return readPipeline(cpath, stat.size, sri, new Pipeline()).concat()
  22. return readFile(cpath, null).then((data) => {
  23. if (!ssri.checkData(data, sri))
  24. throw integrityError(sri, cpath)
  25. return data
  26. })
  27. })
  28. }
  29. const readPipeline = (cpath, size, sri, stream) => {
  30. stream.push(
  31. new fsm.ReadStream(cpath, {
  32. size,
  33. readSize: MAX_SINGLE_READ_SIZE,
  34. }),
  35. ssri.integrityStream({
  36. integrity: sri,
  37. size,
  38. })
  39. )
  40. return stream
  41. }
  42. module.exports.sync = readSync
  43. function readSync (cache, integrity, opts = {}) {
  44. const { size } = opts
  45. return withContentSriSync(cache, integrity, (cpath, sri) => {
  46. const data = fs.readFileSync(cpath)
  47. if (typeof size === 'number' && size !== data.length)
  48. throw sizeError(size, data.length)
  49. if (ssri.checkData(data, sri))
  50. return data
  51. throw integrityError(sri, cpath)
  52. })
  53. }
  54. module.exports.stream = readStream
  55. module.exports.readStream = readStream
  56. function readStream (cache, integrity, opts = {}) {
  57. const { size } = opts
  58. const stream = new Pipeline()
  59. withContentSri(cache, integrity, (cpath, sri) => {
  60. // just lstat to ensure it exists
  61. return lstat(cpath).then((stat) => ({ stat, cpath, sri }))
  62. }).then(({ stat, cpath, sri }) => {
  63. if (typeof size === 'number' && size !== stat.size)
  64. return stream.emit('error', sizeError(size, stat.size))
  65. readPipeline(cpath, stat.size, sri, stream)
  66. }, er => stream.emit('error', er))
  67. return stream
  68. }
  69. let copyFile
  70. if (fs.copyFile) {
  71. module.exports.copy = copy
  72. module.exports.copy.sync = copySync
  73. copyFile = util.promisify(fs.copyFile)
  74. }
  75. function copy (cache, integrity, dest) {
  76. return withContentSri(cache, integrity, (cpath, sri) => {
  77. return copyFile(cpath, dest)
  78. })
  79. }
  80. function copySync (cache, integrity, dest) {
  81. return withContentSriSync(cache, integrity, (cpath, sri) => {
  82. return fs.copyFileSync(cpath, dest)
  83. })
  84. }
  85. module.exports.hasContent = hasContent
  86. function hasContent (cache, integrity) {
  87. if (!integrity)
  88. return Promise.resolve(false)
  89. return withContentSri(cache, integrity, (cpath, sri) => {
  90. return lstat(cpath).then((stat) => ({ size: stat.size, sri, stat }))
  91. }).catch((err) => {
  92. if (err.code === 'ENOENT')
  93. return false
  94. if (err.code === 'EPERM') {
  95. /* istanbul ignore else */
  96. if (process.platform !== 'win32')
  97. throw err
  98. else
  99. return false
  100. }
  101. })
  102. }
  103. module.exports.hasContent.sync = hasContentSync
  104. function hasContentSync (cache, integrity) {
  105. if (!integrity)
  106. return false
  107. return withContentSriSync(cache, integrity, (cpath, sri) => {
  108. try {
  109. const stat = fs.lstatSync(cpath)
  110. return { size: stat.size, sri, stat }
  111. } catch (err) {
  112. if (err.code === 'ENOENT')
  113. return false
  114. if (err.code === 'EPERM') {
  115. /* istanbul ignore else */
  116. if (process.platform !== 'win32')
  117. throw err
  118. else
  119. return false
  120. }
  121. }
  122. })
  123. }
  124. function withContentSri (cache, integrity, fn) {
  125. const tryFn = () => {
  126. const sri = ssri.parse(integrity)
  127. // If `integrity` has multiple entries, pick the first digest
  128. // with available local data.
  129. const algo = sri.pickAlgorithm()
  130. const digests = sri[algo]
  131. if (digests.length <= 1) {
  132. const cpath = contentPath(cache, digests[0])
  133. return fn(cpath, digests[0])
  134. } else {
  135. // Can't use race here because a generic error can happen before
  136. // a ENOENT error, and can happen before a valid result
  137. return Promise
  138. .all(digests.map((meta) => {
  139. return withContentSri(cache, meta, fn)
  140. .catch((err) => {
  141. if (err.code === 'ENOENT') {
  142. return Object.assign(
  143. new Error('No matching content found for ' + sri.toString()),
  144. { code: 'ENOENT' }
  145. )
  146. }
  147. return err
  148. })
  149. }))
  150. .then((results) => {
  151. // Return the first non error if it is found
  152. const result = results.find((r) => !(r instanceof Error))
  153. if (result)
  154. return result
  155. // Throw the No matching content found error
  156. const enoentError = results.find((r) => r.code === 'ENOENT')
  157. if (enoentError)
  158. throw enoentError
  159. // Throw generic error
  160. throw results.find((r) => r instanceof Error)
  161. })
  162. }
  163. }
  164. return new Promise((resolve, reject) => {
  165. try {
  166. tryFn()
  167. .then(resolve)
  168. .catch(reject)
  169. } catch (err) {
  170. reject(err)
  171. }
  172. })
  173. }
  174. function withContentSriSync (cache, integrity, fn) {
  175. const sri = ssri.parse(integrity)
  176. // If `integrity` has multiple entries, pick the first digest
  177. // with available local data.
  178. const algo = sri.pickAlgorithm()
  179. const digests = sri[algo]
  180. if (digests.length <= 1) {
  181. const cpath = contentPath(cache, digests[0])
  182. return fn(cpath, digests[0])
  183. } else {
  184. let lastErr = null
  185. for (const meta of digests) {
  186. try {
  187. return withContentSriSync(cache, meta, fn)
  188. } catch (err) {
  189. lastErr = err
  190. }
  191. }
  192. throw lastErr
  193. }
  194. }
  195. function sizeError (expected, found) {
  196. const err = new Error(`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
  197. err.expected = expected
  198. err.found = found
  199. err.code = 'EBADSIZE'
  200. return err
  201. }
  202. function integrityError (sri, path) {
  203. const err = new Error(`Integrity verification failed for ${sri} (${path})`)
  204. err.code = 'EINTEGRITY'
  205. err.sri = sri
  206. err.path = path
  207. return err
  208. }