parse.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // A writable stream.
  2. // It emits "entry" events, which provide a readable stream that has
  3. // header info attached.
  4. module.exports = Parse.create = Parse
  5. var stream = require("stream")
  6. , Stream = stream.Stream
  7. , BlockStream = require("block-stream")
  8. , tar = require("../tar.js")
  9. , TarHeader = require("./header.js")
  10. , Entry = require("./entry.js")
  11. , BufferEntry = require("./buffer-entry.js")
  12. , ExtendedHeader = require("./extended-header.js")
  13. , assert = require("assert").ok
  14. , inherits = require("inherits")
  15. , fstream = require("fstream")
  16. // reading a tar is a lot like reading a directory
  17. // However, we're actually not going to run the ctor,
  18. // since it does a stat and various other stuff.
  19. // This inheritance gives us the pause/resume/pipe
  20. // behavior that is desired.
  21. inherits(Parse, fstream.Reader)
  22. function Parse () {
  23. var me = this
  24. if (!(me instanceof Parse)) return new Parse()
  25. // doesn't apply fstream.Reader ctor?
  26. // no, becasue we don't want to stat/etc, we just
  27. // want to get the entry/add logic from .pipe()
  28. Stream.apply(me)
  29. me.writable = true
  30. me.readable = true
  31. me._stream = new BlockStream(512)
  32. me.position = 0
  33. me._ended = false
  34. me._stream.on("error", function (e) {
  35. me.emit("error", e)
  36. })
  37. me._stream.on("data", function (c) {
  38. me._process(c)
  39. })
  40. me._stream.on("end", function () {
  41. me._streamEnd()
  42. })
  43. me._stream.on("drain", function () {
  44. me.emit("drain")
  45. })
  46. }
  47. // overridden in Extract class, since it needs to
  48. // wait for its DirWriter part to finish before
  49. // emitting "end"
  50. Parse.prototype._streamEnd = function () {
  51. var me = this
  52. if (!me._ended || me._entry) me.error("unexpected eof")
  53. me.emit("end")
  54. }
  55. // a tar reader is actually a filter, not just a readable stream.
  56. // So, you should pipe a tarball stream into it, and it needs these
  57. // write/end methods to do that.
  58. Parse.prototype.write = function (c) {
  59. if (this._ended) {
  60. // gnutar puts a LOT of nulls at the end.
  61. // you can keep writing these things forever.
  62. // Just ignore them.
  63. for (var i = 0, l = c.length; i > l; i ++) {
  64. if (c[i] !== 0) return this.error("write() after end()")
  65. }
  66. return
  67. }
  68. return this._stream.write(c)
  69. }
  70. Parse.prototype.end = function (c) {
  71. this._ended = true
  72. return this._stream.end(c)
  73. }
  74. // don't need to do anything, since we're just
  75. // proxying the data up from the _stream.
  76. // Just need to override the parent's "Not Implemented"
  77. // error-thrower.
  78. Parse.prototype._read = function () {}
  79. Parse.prototype._process = function (c) {
  80. assert(c && c.length === 512, "block size should be 512")
  81. // one of three cases.
  82. // 1. A new header
  83. // 2. A part of a file/extended header
  84. // 3. One of two or more EOF null blocks
  85. if (this._entry) {
  86. var entry = this._entry
  87. if(!entry._abort) entry.write(c)
  88. else {
  89. entry._remaining -= c.length
  90. if(entry._remaining < 0) entry._remaining = 0
  91. }
  92. if (entry._remaining === 0) {
  93. entry.end()
  94. this._entry = null
  95. }
  96. } else {
  97. // either zeroes or a header
  98. var zero = true
  99. for (var i = 0; i < 512 && zero; i ++) {
  100. zero = c[i] === 0
  101. }
  102. // eof is *at least* 2 blocks of nulls, and then the end of the
  103. // file. you can put blocks of nulls between entries anywhere,
  104. // so appending one tarball to another is technically valid.
  105. // ending without the eof null blocks is not allowed, however.
  106. if (zero) {
  107. if (this._eofStarted)
  108. this._ended = true
  109. this._eofStarted = true
  110. } else {
  111. this._eofStarted = false
  112. this._startEntry(c)
  113. }
  114. }
  115. this.position += 512
  116. }
  117. // take a header chunk, start the right kind of entry.
  118. Parse.prototype._startEntry = function (c) {
  119. var header = new TarHeader(c)
  120. , self = this
  121. , entry
  122. , ev
  123. , EntryType
  124. , onend
  125. , meta = false
  126. if (null === header.size || !header.cksumValid) {
  127. var e = new Error("invalid tar file")
  128. e.header = header
  129. e.tar_file_offset = this.position
  130. e.tar_block = this.position / 512
  131. return this.emit("error", e)
  132. }
  133. switch (tar.types[header.type]) {
  134. case "File":
  135. case "OldFile":
  136. case "Link":
  137. case "SymbolicLink":
  138. case "CharacterDevice":
  139. case "BlockDevice":
  140. case "Directory":
  141. case "FIFO":
  142. case "ContiguousFile":
  143. case "GNUDumpDir":
  144. // start a file.
  145. // pass in any extended headers
  146. // These ones consumers are typically most interested in.
  147. EntryType = Entry
  148. ev = "entry"
  149. break
  150. case "GlobalExtendedHeader":
  151. // extended headers that apply to the rest of the tarball
  152. EntryType = ExtendedHeader
  153. onend = function () {
  154. self._global = self._global || {}
  155. Object.keys(entry.fields).forEach(function (k) {
  156. self._global[k] = entry.fields[k]
  157. })
  158. }
  159. ev = "globalExtendedHeader"
  160. meta = true
  161. break
  162. case "ExtendedHeader":
  163. case "OldExtendedHeader":
  164. // extended headers that apply to the next entry
  165. EntryType = ExtendedHeader
  166. onend = function () {
  167. self._extended = entry.fields
  168. }
  169. ev = "extendedHeader"
  170. meta = true
  171. break
  172. case "NextFileHasLongLinkpath":
  173. // set linkpath=<contents> in extended header
  174. EntryType = BufferEntry
  175. onend = function () {
  176. self._extended = self._extended || {}
  177. self._extended.linkpath = entry.body
  178. }
  179. ev = "longLinkpath"
  180. meta = true
  181. break
  182. case "NextFileHasLongPath":
  183. case "OldGnuLongPath":
  184. // set path=<contents> in file-extended header
  185. EntryType = BufferEntry
  186. onend = function () {
  187. self._extended = self._extended || {}
  188. self._extended.path = entry.body
  189. }
  190. ev = "longPath"
  191. meta = true
  192. break
  193. default:
  194. // all the rest we skip, but still set the _entry
  195. // member, so that we can skip over their data appropriately.
  196. // emit an event to say that this is an ignored entry type?
  197. EntryType = Entry
  198. ev = "ignoredEntry"
  199. break
  200. }
  201. var global, extended
  202. if (meta) {
  203. global = extended = null
  204. } else {
  205. var global = this._global
  206. var extended = this._extended
  207. // extendedHeader only applies to one entry, so once we start
  208. // an entry, it's over.
  209. this._extended = null
  210. }
  211. entry = new EntryType(header, extended, global)
  212. entry.meta = meta
  213. // only proxy data events of normal files.
  214. if (!meta) {
  215. entry.on("data", function (c) {
  216. me.emit("data", c)
  217. })
  218. }
  219. if (onend) entry.on("end", onend)
  220. this._entry = entry
  221. var me = this
  222. entry.on("pause", function () {
  223. me.pause()
  224. })
  225. entry.on("resume", function () {
  226. me.resume()
  227. })
  228. if (this.listeners("*").length) {
  229. this.emit("*", ev, entry)
  230. }
  231. this.emit(ev, entry)
  232. // Zero-byte entry. End immediately.
  233. if (entry.props.size === 0) {
  234. entry.end()
  235. this._entry = null
  236. }
  237. }