parse.js 6.9 KB

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