76dcd89b43986cfe992baec5722f909a82eca36358cf58808ed8aec93df4be8f0ae5022cf534b19afd9dfeba0887665cc0aff0cb3613eaa99deaf96d145421 6.6 KB

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