read.js 6.3 KB

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