You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

195 lines
5.2 KiB

4 years ago
  1. 'use strict'
  2. const BB = require('bluebird')
  3. const contentPath = require('./path')
  4. const figgyPudding = require('figgy-pudding')
  5. const fs = require('graceful-fs')
  6. const PassThrough = require('stream').PassThrough
  7. const pipe = BB.promisify(require('mississippi').pipe)
  8. const ssri = require('ssri')
  9. const Y = require('../util/y.js')
  10. const lstatAsync = BB.promisify(fs.lstat)
  11. const readFileAsync = BB.promisify(fs.readFile)
  12. const ReadOpts = figgyPudding({
  13. size: {}
  14. })
  15. module.exports = read
  16. function read (cache, integrity, opts) {
  17. opts = ReadOpts(opts)
  18. return withContentSri(cache, integrity, (cpath, sri) => {
  19. return readFileAsync(cpath, null).then(data => {
  20. if (typeof opts.size === 'number' && opts.size !== data.length) {
  21. throw sizeError(opts.size, data.length)
  22. } else if (ssri.checkData(data, sri)) {
  23. return data
  24. } else {
  25. throw integrityError(sri, cpath)
  26. }
  27. })
  28. })
  29. }
  30. module.exports.sync = readSync
  31. function readSync (cache, integrity, opts) {
  32. opts = ReadOpts(opts)
  33. return withContentSriSync(cache, integrity, (cpath, sri) => {
  34. const data = fs.readFileSync(cpath)
  35. if (typeof opts.size === 'number' && opts.size !== data.length) {
  36. throw sizeError(opts.size, data.length)
  37. } else if (ssri.checkData(data, sri)) {
  38. return data
  39. } else {
  40. throw integrityError(sri, cpath)
  41. }
  42. })
  43. }
  44. module.exports.stream = readStream
  45. module.exports.readStream = readStream
  46. function readStream (cache, integrity, opts) {
  47. opts = ReadOpts(opts)
  48. const stream = new PassThrough()
  49. withContentSri(cache, integrity, (cpath, sri) => {
  50. return lstatAsync(cpath).then(stat => ({ cpath, sri, stat }))
  51. }).then(({ cpath, sri, stat }) => {
  52. return pipe(
  53. fs.createReadStream(cpath),
  54. ssri.integrityStream({
  55. integrity: sri,
  56. size: opts.size
  57. }),
  58. stream
  59. )
  60. }).catch(err => {
  61. stream.emit('error', err)
  62. })
  63. return stream
  64. }
  65. let copyFileAsync
  66. if (fs.copyFile) {
  67. module.exports.copy = copy
  68. module.exports.copy.sync = copySync
  69. copyFileAsync = BB.promisify(fs.copyFile)
  70. }
  71. function copy (cache, integrity, dest, opts) {
  72. opts = ReadOpts(opts)
  73. return withContentSri(cache, integrity, (cpath, sri) => {
  74. return copyFileAsync(cpath, dest)
  75. })
  76. }
  77. function copySync (cache, integrity, dest, opts) {
  78. opts = ReadOpts(opts)
  79. return withContentSriSync(cache, integrity, (cpath, sri) => {
  80. return fs.copyFileSync(cpath, dest)
  81. })
  82. }
  83. module.exports.hasContent = hasContent
  84. function hasContent (cache, integrity) {
  85. if (!integrity) { return BB.resolve(false) }
  86. return withContentSri(cache, integrity, (cpath, sri) => {
  87. return lstatAsync(cpath).then(stat => ({ size: stat.size, sri, stat }))
  88. }).catch(err => {
  89. if (err.code === 'ENOENT') { return false }
  90. if (err.code === 'EPERM') {
  91. if (process.platform !== 'win32') {
  92. throw err
  93. } else {
  94. return false
  95. }
  96. }
  97. })
  98. }
  99. module.exports.hasContent.sync = hasContentSync
  100. function hasContentSync (cache, integrity) {
  101. if (!integrity) { return false }
  102. return withContentSriSync(cache, integrity, (cpath, sri) => {
  103. try {
  104. const stat = fs.lstatSync(cpath)
  105. return { size: stat.size, sri, stat }
  106. } catch (err) {
  107. if (err.code === 'ENOENT') { return false }
  108. if (err.code === 'EPERM') {
  109. if (process.platform !== 'win32') {
  110. throw err
  111. } else {
  112. return false
  113. }
  114. }
  115. }
  116. })
  117. }
  118. function withContentSri (cache, integrity, fn) {
  119. return BB.try(() => {
  120. const sri = ssri.parse(integrity)
  121. // If `integrity` has multiple entries, pick the first digest
  122. // with available local data.
  123. const algo = sri.pickAlgorithm()
  124. const digests = sri[algo]
  125. if (digests.length <= 1) {
  126. const cpath = contentPath(cache, digests[0])
  127. return fn(cpath, digests[0])
  128. } else {
  129. return BB.any(sri[sri.pickAlgorithm()].map(meta => {
  130. return withContentSri(cache, meta, fn)
  131. }, { concurrency: 1 }))
  132. .catch(err => {
  133. if ([].some.call(err, e => e.code === 'ENOENT')) {
  134. throw Object.assign(
  135. new Error('No matching content found for ' + sri.toString()),
  136. { code: 'ENOENT' }
  137. )
  138. } else {
  139. throw err[0]
  140. }
  141. })
  142. }
  143. })
  144. }
  145. function withContentSriSync (cache, integrity, fn) {
  146. const sri = ssri.parse(integrity)
  147. // If `integrity` has multiple entries, pick the first digest
  148. // with available local data.
  149. const algo = sri.pickAlgorithm()
  150. const digests = sri[algo]
  151. if (digests.length <= 1) {
  152. const cpath = contentPath(cache, digests[0])
  153. return fn(cpath, digests[0])
  154. } else {
  155. let lastErr = null
  156. for (const meta of sri[sri.pickAlgorithm()]) {
  157. try {
  158. return withContentSriSync(cache, meta, fn)
  159. } catch (err) {
  160. lastErr = err
  161. }
  162. }
  163. if (lastErr) { throw lastErr }
  164. }
  165. }
  166. function sizeError (expected, found) {
  167. var err = new Error(Y`Bad data size: expected inserted data to be ${expected} bytes, but got ${found} instead`)
  168. err.expected = expected
  169. err.found = found
  170. err.code = 'EBADSIZE'
  171. return err
  172. }
  173. function integrityError (sri, path) {
  174. var err = new Error(Y`Integrity verification failed for ${sri} (${path})`)
  175. err.code = 'EINTEGRITY'
  176. err.sri = sri
  177. err.path = path
  178. return err
  179. }