amp 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (295) hide show
  1. data/.gitignore +1 -0
  2. data/.hgignore +26 -0
  3. data/AUTHORS +2 -0
  4. data/History.txt +6 -0
  5. data/LICENSE +37 -0
  6. data/MANIFESTO +7 -0
  7. data/Manifest.txt +294 -0
  8. data/README.md +129 -0
  9. data/Rakefile +102 -0
  10. data/SCHEDULE.markdown +12 -0
  11. data/STYLE +27 -0
  12. data/TODO.markdown +149 -0
  13. data/ampfile.rb +47 -0
  14. data/bin/amp +30 -0
  15. data/bin/amp1.9 +30 -0
  16. data/ext/amp/bz2/README.txt +39 -0
  17. data/ext/amp/bz2/bz2.c +1582 -0
  18. data/ext/amp/bz2/extconf.rb +77 -0
  19. data/ext/amp/bz2/mkmf.log +29 -0
  20. data/ext/amp/mercurial_patch/extconf.rb +5 -0
  21. data/ext/amp/mercurial_patch/mpatch.c +405 -0
  22. data/ext/amp/priority_queue/extconf.rb +5 -0
  23. data/ext/amp/priority_queue/priority_queue.c +947 -0
  24. data/ext/amp/support/extconf.rb +5 -0
  25. data/ext/amp/support/support.c +250 -0
  26. data/lib/amp.rb +200 -0
  27. data/lib/amp/commands/command.rb +507 -0
  28. data/lib/amp/commands/command_support.rb +137 -0
  29. data/lib/amp/commands/commands/config.rb +143 -0
  30. data/lib/amp/commands/commands/help.rb +29 -0
  31. data/lib/amp/commands/commands/init.rb +10 -0
  32. data/lib/amp/commands/commands/templates.rb +137 -0
  33. data/lib/amp/commands/commands/version.rb +7 -0
  34. data/lib/amp/commands/commands/workflow.rb +28 -0
  35. data/lib/amp/commands/commands/workflows/git/add.rb +65 -0
  36. data/lib/amp/commands/commands/workflows/git/copy.rb +27 -0
  37. data/lib/amp/commands/commands/workflows/git/mv.rb +23 -0
  38. data/lib/amp/commands/commands/workflows/git/rm.rb +60 -0
  39. data/lib/amp/commands/commands/workflows/hg/add.rb +53 -0
  40. data/lib/amp/commands/commands/workflows/hg/addremove.rb +86 -0
  41. data/lib/amp/commands/commands/workflows/hg/annotate.rb +46 -0
  42. data/lib/amp/commands/commands/workflows/hg/archive.rb +126 -0
  43. data/lib/amp/commands/commands/workflows/hg/branch.rb +28 -0
  44. data/lib/amp/commands/commands/workflows/hg/branches.rb +30 -0
  45. data/lib/amp/commands/commands/workflows/hg/bundle.rb +115 -0
  46. data/lib/amp/commands/commands/workflows/hg/clone.rb +95 -0
  47. data/lib/amp/commands/commands/workflows/hg/commit.rb +42 -0
  48. data/lib/amp/commands/commands/workflows/hg/copy.rb +31 -0
  49. data/lib/amp/commands/commands/workflows/hg/debug/dirstate.rb +32 -0
  50. data/lib/amp/commands/commands/workflows/hg/debug/index.rb +36 -0
  51. data/lib/amp/commands/commands/workflows/hg/default.rb +9 -0
  52. data/lib/amp/commands/commands/workflows/hg/diff.rb +30 -0
  53. data/lib/amp/commands/commands/workflows/hg/forget.rb +11 -0
  54. data/lib/amp/commands/commands/workflows/hg/heads.rb +25 -0
  55. data/lib/amp/commands/commands/workflows/hg/identify.rb +23 -0
  56. data/lib/amp/commands/commands/workflows/hg/import.rb +135 -0
  57. data/lib/amp/commands/commands/workflows/hg/incoming.rb +85 -0
  58. data/lib/amp/commands/commands/workflows/hg/info.rb +18 -0
  59. data/lib/amp/commands/commands/workflows/hg/log.rb +21 -0
  60. data/lib/amp/commands/commands/workflows/hg/manifest.rb +13 -0
  61. data/lib/amp/commands/commands/workflows/hg/merge.rb +53 -0
  62. data/lib/amp/commands/commands/workflows/hg/move.rb +28 -0
  63. data/lib/amp/commands/commands/workflows/hg/outgoing.rb +61 -0
  64. data/lib/amp/commands/commands/workflows/hg/pull.rb +74 -0
  65. data/lib/amp/commands/commands/workflows/hg/push.rb +20 -0
  66. data/lib/amp/commands/commands/workflows/hg/remove.rb +45 -0
  67. data/lib/amp/commands/commands/workflows/hg/resolve.rb +83 -0
  68. data/lib/amp/commands/commands/workflows/hg/revert.rb +53 -0
  69. data/lib/amp/commands/commands/workflows/hg/root.rb +13 -0
  70. data/lib/amp/commands/commands/workflows/hg/serve.rb +38 -0
  71. data/lib/amp/commands/commands/workflows/hg/status.rb +116 -0
  72. data/lib/amp/commands/commands/workflows/hg/tag.rb +69 -0
  73. data/lib/amp/commands/commands/workflows/hg/tags.rb +27 -0
  74. data/lib/amp/commands/commands/workflows/hg/tip.rb +13 -0
  75. data/lib/amp/commands/commands/workflows/hg/update.rb +27 -0
  76. data/lib/amp/commands/commands/workflows/hg/verify.rb +9 -0
  77. data/lib/amp/commands/commands/workflows/hg/view.rb +36 -0
  78. data/lib/amp/commands/dispatch.rb +181 -0
  79. data/lib/amp/commands/hooks.rb +81 -0
  80. data/lib/amp/dependencies/amp_support.rb +1 -0
  81. data/lib/amp/dependencies/amp_support/ruby_amp_support.rb +103 -0
  82. data/lib/amp/dependencies/minitar.rb +979 -0
  83. data/lib/amp/dependencies/priority_queue.rb +18 -0
  84. data/lib/amp/dependencies/priority_queue/c_priority_queue.rb +1 -0
  85. data/lib/amp/dependencies/priority_queue/poor_priority_queue.rb +46 -0
  86. data/lib/amp/dependencies/priority_queue/ruby_priority_queue.rb +525 -0
  87. data/lib/amp/dependencies/python_config.rb +211 -0
  88. data/lib/amp/dependencies/trollop.rb +713 -0
  89. data/lib/amp/dependencies/zip/ioextras.rb +155 -0
  90. data/lib/amp/dependencies/zip/stdrubyext.rb +111 -0
  91. data/lib/amp/dependencies/zip/tempfile_bugfixed.rb +186 -0
  92. data/lib/amp/dependencies/zip/zip.rb +1850 -0
  93. data/lib/amp/dependencies/zip/zipfilesystem.rb +609 -0
  94. data/lib/amp/dependencies/zip/ziprequire.rb +90 -0
  95. data/lib/amp/encoding/base85.rb +97 -0
  96. data/lib/amp/encoding/binary_diff.rb +82 -0
  97. data/lib/amp/encoding/difflib.rb +166 -0
  98. data/lib/amp/encoding/mercurial_diff.rb +378 -0
  99. data/lib/amp/encoding/mercurial_patch.rb +1 -0
  100. data/lib/amp/encoding/patch.rb +292 -0
  101. data/lib/amp/encoding/pure_ruby/ruby_mercurial_patch.rb +123 -0
  102. data/lib/amp/extensions/ditz.rb +41 -0
  103. data/lib/amp/extensions/lighthouse.rb +167 -0
  104. data/lib/amp/graphs/ancestor.rb +147 -0
  105. data/lib/amp/graphs/copies.rb +261 -0
  106. data/lib/amp/merges/merge_state.rb +164 -0
  107. data/lib/amp/merges/merge_ui.rb +322 -0
  108. data/lib/amp/merges/simple_merge.rb +450 -0
  109. data/lib/amp/profiling_hacks.rb +36 -0
  110. data/lib/amp/repository/branch_manager.rb +234 -0
  111. data/lib/amp/repository/dir_state.rb +950 -0
  112. data/lib/amp/repository/journal.rb +203 -0
  113. data/lib/amp/repository/lock.rb +207 -0
  114. data/lib/amp/repository/repositories/bundle_repository.rb +214 -0
  115. data/lib/amp/repository/repositories/http_repository.rb +377 -0
  116. data/lib/amp/repository/repositories/local_repository.rb +2661 -0
  117. data/lib/amp/repository/repository.rb +94 -0
  118. data/lib/amp/repository/store.rb +485 -0
  119. data/lib/amp/repository/tag_manager.rb +319 -0
  120. data/lib/amp/repository/updatable.rb +532 -0
  121. data/lib/amp/repository/verification.rb +431 -0
  122. data/lib/amp/repository/versioned_file.rb +475 -0
  123. data/lib/amp/revlogs/bundle_revlogs.rb +246 -0
  124. data/lib/amp/revlogs/changegroup.rb +217 -0
  125. data/lib/amp/revlogs/changelog.rb +338 -0
  126. data/lib/amp/revlogs/changeset.rb +521 -0
  127. data/lib/amp/revlogs/file_log.rb +165 -0
  128. data/lib/amp/revlogs/index.rb +493 -0
  129. data/lib/amp/revlogs/manifest.rb +195 -0
  130. data/lib/amp/revlogs/node.rb +18 -0
  131. data/lib/amp/revlogs/revlog.rb +1032 -0
  132. data/lib/amp/revlogs/revlog_support.rb +126 -0
  133. data/lib/amp/server/amp_user.rb +44 -0
  134. data/lib/amp/server/extension/amp_extension.rb +396 -0
  135. data/lib/amp/server/extension/authorization.rb +201 -0
  136. data/lib/amp/server/fancy_http_server.rb +252 -0
  137. data/lib/amp/server/fancy_views/_browser.haml +28 -0
  138. data/lib/amp/server/fancy_views/_diff_file.haml +13 -0
  139. data/lib/amp/server/fancy_views/_navbar.haml +17 -0
  140. data/lib/amp/server/fancy_views/changeset.haml +31 -0
  141. data/lib/amp/server/fancy_views/commits.haml +32 -0
  142. data/lib/amp/server/fancy_views/file.haml +35 -0
  143. data/lib/amp/server/fancy_views/file_diff.haml +23 -0
  144. data/lib/amp/server/fancy_views/harshcss/all_hallows_eve.css +72 -0
  145. data/lib/amp/server/fancy_views/harshcss/amy.css +147 -0
  146. data/lib/amp/server/fancy_views/harshcss/twilight.css +138 -0
  147. data/lib/amp/server/fancy_views/stylesheet.sass +175 -0
  148. data/lib/amp/server/http_server.rb +140 -0
  149. data/lib/amp/server/repo_user_management.rb +287 -0
  150. data/lib/amp/support/amp_config.rb +164 -0
  151. data/lib/amp/support/amp_ui.rb +287 -0
  152. data/lib/amp/support/docs.rb +54 -0
  153. data/lib/amp/support/generator.rb +78 -0
  154. data/lib/amp/support/ignore.rb +144 -0
  155. data/lib/amp/support/loaders.rb +93 -0
  156. data/lib/amp/support/logger.rb +103 -0
  157. data/lib/amp/support/match.rb +151 -0
  158. data/lib/amp/support/multi_io.rb +87 -0
  159. data/lib/amp/support/openers.rb +121 -0
  160. data/lib/amp/support/ruby_19_compatibility.rb +66 -0
  161. data/lib/amp/support/support.rb +1095 -0
  162. data/lib/amp/templates/blank.commit.erb +23 -0
  163. data/lib/amp/templates/blank.log.erb +18 -0
  164. data/lib/amp/templates/default.commit.erb +23 -0
  165. data/lib/amp/templates/default.log.erb +26 -0
  166. data/lib/amp/templates/template.rb +165 -0
  167. data/site/Rakefile +24 -0
  168. data/site/src/about/ampfile.haml +57 -0
  169. data/site/src/about/commands.haml +106 -0
  170. data/site/src/about/index.haml +33 -0
  171. data/site/src/about/performance.haml +31 -0
  172. data/site/src/about/workflows.haml +34 -0
  173. data/site/src/contribute/index.haml +65 -0
  174. data/site/src/contribute/style.haml +297 -0
  175. data/site/src/css/active4d.css +114 -0
  176. data/site/src/css/all_hallows_eve.css +72 -0
  177. data/site/src/css/all_themes.css +3299 -0
  178. data/site/src/css/amp.css +260 -0
  179. data/site/src/css/amy.css +147 -0
  180. data/site/src/css/blackboard.css +88 -0
  181. data/site/src/css/brilliance_black.css +605 -0
  182. data/site/src/css/brilliance_dull.css +599 -0
  183. data/site/src/css/cobalt.css +149 -0
  184. data/site/src/css/cur_amp.css +185 -0
  185. data/site/src/css/dawn.css +121 -0
  186. data/site/src/css/eiffel.css +121 -0
  187. data/site/src/css/espresso_libre.css +109 -0
  188. data/site/src/css/idle.css +62 -0
  189. data/site/src/css/iplastic.css +80 -0
  190. data/site/src/css/lazy.css +73 -0
  191. data/site/src/css/mac_classic.css +123 -0
  192. data/site/src/css/magicwb_amiga.css +104 -0
  193. data/site/src/css/pastels_on_dark.css +188 -0
  194. data/site/src/css/reset.css +55 -0
  195. data/site/src/css/slush_poppies.css +85 -0
  196. data/site/src/css/spacecadet.css +51 -0
  197. data/site/src/css/sunburst.css +180 -0
  198. data/site/src/css/twilight.css +137 -0
  199. data/site/src/css/zenburnesque.css +91 -0
  200. data/site/src/get/index.haml +32 -0
  201. data/site/src/helpers.rb +121 -0
  202. data/site/src/images/amp_logo.png +0 -0
  203. data/site/src/images/carbonica.png +0 -0
  204. data/site/src/images/revolution.png +0 -0
  205. data/site/src/images/tab-bg.png +0 -0
  206. data/site/src/images/tab-sliding-left.png +0 -0
  207. data/site/src/images/tab-sliding-right.png +0 -0
  208. data/site/src/include/_footer.haml +22 -0
  209. data/site/src/include/_header.haml +17 -0
  210. data/site/src/index.haml +104 -0
  211. data/site/src/learn/index.haml +46 -0
  212. data/site/src/scripts/jquery-1.3.2.min.js +19 -0
  213. data/site/src/scripts/jquery.cookie.js +96 -0
  214. data/tasks/stats.rake +155 -0
  215. data/tasks/yard.rake +171 -0
  216. data/test/dirstate_tests/dirstate +0 -0
  217. data/test/dirstate_tests/hgrc +5 -0
  218. data/test/dirstate_tests/test_dir_state.rb +192 -0
  219. data/test/functional_tests/resources/.hgignore +2 -0
  220. data/test/functional_tests/resources/STYLE.txt +25 -0
  221. data/test/functional_tests/resources/command.rb +372 -0
  222. data/test/functional_tests/resources/commands/annotate.rb +57 -0
  223. data/test/functional_tests/resources/commands/experimental/lolcats.rb +17 -0
  224. data/test/functional_tests/resources/commands/heads.rb +22 -0
  225. data/test/functional_tests/resources/commands/manifest.rb +12 -0
  226. data/test/functional_tests/resources/commands/status.rb +90 -0
  227. data/test/functional_tests/resources/version2/.hgignore +5 -0
  228. data/test/functional_tests/resources/version2/STYLE.txt +25 -0
  229. data/test/functional_tests/resources/version2/command.rb +372 -0
  230. data/test/functional_tests/resources/version2/commands/annotate.rb +45 -0
  231. data/test/functional_tests/resources/version2/commands/experimental/lolcats.rb +17 -0
  232. data/test/functional_tests/resources/version2/commands/heads.rb +22 -0
  233. data/test/functional_tests/resources/version2/commands/manifest.rb +12 -0
  234. data/test/functional_tests/resources/version2/commands/status.rb +90 -0
  235. data/test/functional_tests/resources/version3/.hgignore +5 -0
  236. data/test/functional_tests/resources/version3/STYLE.txt +31 -0
  237. data/test/functional_tests/resources/version3/command.rb +376 -0
  238. data/test/functional_tests/resources/version3/commands/annotate.rb +45 -0
  239. data/test/functional_tests/resources/version3/commands/experimental/lolcats.rb +17 -0
  240. data/test/functional_tests/resources/version3/commands/heads.rb +22 -0
  241. data/test/functional_tests/resources/version3/commands/manifest.rb +12 -0
  242. data/test/functional_tests/resources/version3/commands/status.rb +90 -0
  243. data/test/functional_tests/resources/version4/.hgignore +5 -0
  244. data/test/functional_tests/resources/version4/STYLE.txt +31 -0
  245. data/test/functional_tests/resources/version4/command.rb +376 -0
  246. data/test/functional_tests/resources/version4/commands/experimental/lolcats.rb +17 -0
  247. data/test/functional_tests/resources/version4/commands/heads.rb +22 -0
  248. data/test/functional_tests/resources/version4/commands/manifest.rb +12 -0
  249. data/test/functional_tests/resources/version4/commands/stats.rb +25 -0
  250. data/test/functional_tests/resources/version4/commands/status.rb +90 -0
  251. data/test/functional_tests/resources/version5_1/.hgignore +5 -0
  252. data/test/functional_tests/resources/version5_1/STYLE.txt +2 -0
  253. data/test/functional_tests/resources/version5_1/command.rb +374 -0
  254. data/test/functional_tests/resources/version5_1/commands/experimental/lolcats.rb +17 -0
  255. data/test/functional_tests/resources/version5_1/commands/heads.rb +22 -0
  256. data/test/functional_tests/resources/version5_1/commands/manifest.rb +12 -0
  257. data/test/functional_tests/resources/version5_1/commands/stats.rb +25 -0
  258. data/test/functional_tests/resources/version5_1/commands/status.rb +90 -0
  259. data/test/functional_tests/resources/version5_2/.hgignore +5 -0
  260. data/test/functional_tests/resources/version5_2/STYLE.txt +14 -0
  261. data/test/functional_tests/resources/version5_2/command.rb +376 -0
  262. data/test/functional_tests/resources/version5_2/commands/experimental/lolcats.rb +17 -0
  263. data/test/functional_tests/resources/version5_2/commands/manifest.rb +12 -0
  264. data/test/functional_tests/resources/version5_2/commands/newz.rb +12 -0
  265. data/test/functional_tests/resources/version5_2/commands/stats.rb +25 -0
  266. data/test/functional_tests/resources/version5_2/commands/status.rb +90 -0
  267. data/test/functional_tests/test_functional.rb +604 -0
  268. data/test/localrepo_tests/test_local_repo.rb +121 -0
  269. data/test/localrepo_tests/testrepo.tar.gz +0 -0
  270. data/test/manifest_tests/00manifest.i +0 -0
  271. data/test/manifest_tests/test_manifest.rb +72 -0
  272. data/test/merge_tests/base.txt +10 -0
  273. data/test/merge_tests/expected.local.txt +16 -0
  274. data/test/merge_tests/local.txt +11 -0
  275. data/test/merge_tests/remote.txt +11 -0
  276. data/test/merge_tests/test_merge.rb +26 -0
  277. data/test/revlog_tests/00changelog.i +0 -0
  278. data/test/revlog_tests/revision_added_changelog.i +0 -0
  279. data/test/revlog_tests/test_adding_index.i +0 -0
  280. data/test/revlog_tests/test_revlog.rb +333 -0
  281. data/test/revlog_tests/testindex.i +0 -0
  282. data/test/store_tests/store.tar.gz +0 -0
  283. data/test/store_tests/test_fncache_store.rb +122 -0
  284. data/test/test_amp.rb +9 -0
  285. data/test/test_base85.rb +14 -0
  286. data/test/test_bdiff.rb +42 -0
  287. data/test/test_commands.rb +122 -0
  288. data/test/test_difflib.rb +50 -0
  289. data/test/test_helper.rb +15 -0
  290. data/test/test_journal.rb +29 -0
  291. data/test/test_match.rb +134 -0
  292. data/test/test_mdiff.rb +74 -0
  293. data/test/test_mpatch.rb +14 -0
  294. data/test/test_support.rb +24 -0
  295. metadata +385 -0
@@ -0,0 +1,1850 @@
1
+ # graciously stolen from Thomas Sondergaard
2
+
3
+ require 'delegate'
4
+ require 'singleton'
5
+ require 'tempfile'
6
+ require 'ftools'
7
+ require 'stringio'
8
+ require 'zlib'
9
+
10
+ need{ 'stdrubyext' }
11
+ need{ 'ioextras' }
12
+
13
+ if Tempfile.superclass == SimpleDelegator
14
+ need{ 'tempfile_bugfixed' }
15
+ Tempfile = BugFix::Tempfile
16
+ end
17
+
18
+ module Zlib #:nodoc:all
19
+ if ! const_defined? :MAX_WBITS
20
+ MAX_WBITS = Zlib::Deflate.MAX_WBITS
21
+ end
22
+ end
23
+
24
+ module Zip
25
+
26
+ VERSION = '0.9.1'
27
+
28
+ RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i
29
+
30
+ RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
31
+
32
+ # Ruby 1.7.x compatibility
33
+ # In ruby 1.6.x and 1.8.0 reading from an empty stream returns
34
+ # an empty string the first time and then nil.
35
+ # not so in 1.7.x
36
+ EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7
37
+
38
+ # ZipInputStream is the basic class for reading zip entries in a
39
+ # zip file. It is possible to create a ZipInputStream object directly,
40
+ # passing the zip file name to the constructor, but more often than not
41
+ # the ZipInputStream will be obtained from a ZipFile (perhaps using the
42
+ # ZipFileSystem interface) object for a particular entry in the zip
43
+ # archive.
44
+ #
45
+ # A ZipInputStream inherits IOExtras::AbstractInputStream in order
46
+ # to provide an IO-like interface for reading from a single zip
47
+ # entry. Beyond methods for mimicking an IO-object it contains
48
+ # the method get_next_entry for iterating through the entries of
49
+ # an archive. get_next_entry returns a ZipEntry object that describes
50
+ # the zip entry the ZipInputStream is currently reading from.
51
+ #
52
+ # Example that creates a zip archive with ZipOutputStream and reads it
53
+ # back again with a ZipInputStream.
54
+ #
55
+ # require 'zip/zip'
56
+ #
57
+ # Zip::ZipOutputStream::open("my.zip") {
58
+ # |io|
59
+ #
60
+ # io.put_next_entry("first_entry.txt")
61
+ # io.write "Hello world!"
62
+ #
63
+ # io.put_next_entry("adir/first_entry.txt")
64
+ # io.write "Hello again!"
65
+ # }
66
+ #
67
+ #
68
+ # Zip::ZipInputStream::open("my.zip") {
69
+ # |io|
70
+ #
71
+ # while (entry = io.get_next_entry)
72
+ # puts "Contents of #{entry.name}: '#{io.read}'"
73
+ # end
74
+ # }
75
+ #
76
+ # java.util.zip.ZipInputStream is the original inspiration for this
77
+ # class.
78
+
79
+ class ZipInputStream
80
+ include IOExtras::AbstractInputStream
81
+
82
+ # Opens the indicated zip file. An exception is thrown
83
+ # if the specified offset in the specified filename is
84
+ # not a local zip entry header.
85
+ def initialize(filename, offset = 0)
86
+ super()
87
+ @archiveIO = File.open(filename, "rb")
88
+ @archiveIO.seek(offset, IO::SEEK_SET)
89
+ @decompressor = NullDecompressor.instance
90
+ @currentEntry = nil
91
+ end
92
+
93
+ def close
94
+ @archiveIO.close
95
+ end
96
+
97
+ # Same as #initialize but if a block is passed the opened
98
+ # stream is passed to the block and closed when the block
99
+ # returns.
100
+ def ZipInputStream.open(filename)
101
+ return new(filename) unless block_given?
102
+
103
+ zio = new(filename)
104
+ yield zio
105
+ ensure
106
+ zio.close if zio
107
+ end
108
+
109
+ # Returns a ZipEntry object. It is necessary to call this
110
+ # method on a newly created ZipInputStream before reading from
111
+ # the first entry in the archive. Returns nil when there are
112
+ # no more entries.
113
+
114
+ def get_next_entry
115
+ @archiveIO.seek(@currentEntry.next_header_offset,
116
+ IO::SEEK_SET) if @currentEntry
117
+ open_entry
118
+ end
119
+
120
+ # Rewinds the stream to the beginning of the current entry
121
+ def rewind
122
+ return if @currentEntry.nil?
123
+ @lineno = 0
124
+ @archiveIO.seek(@currentEntry.localHeaderOffset,
125
+ IO::SEEK_SET)
126
+ open_entry
127
+ end
128
+
129
+ # Modeled after IO.sysread
130
+ def sysread(numberOfBytes = nil, buf = nil)
131
+ @decompressor.sysread(numberOfBytes, buf)
132
+ end
133
+
134
+ def eof
135
+ @outputBuffer.empty? && @decompressor.eof
136
+ end
137
+ alias :eof? :eof
138
+
139
+ protected
140
+
141
+ def open_entry
142
+ @currentEntry = ZipEntry.read_local_entry(@archiveIO)
143
+ if (@currentEntry == nil)
144
+ @decompressor = NullDecompressor.instance
145
+ elsif @currentEntry.compression_method == ZipEntry::STORED
146
+ @decompressor = PassThruDecompressor.new(@archiveIO,
147
+ @currentEntry.size)
148
+ elsif @currentEntry.compression_method == ZipEntry::DEFLATED
149
+ @decompressor = Inflater.new(@archiveIO)
150
+ else
151
+ raise ZipCompressionMethodError,
152
+ "Unsupported compression method #{@currentEntry.compression_method}"
153
+ end
154
+ flush
155
+ return @currentEntry
156
+ end
157
+
158
+ def produce_input
159
+ @decompressor.produce_input
160
+ end
161
+
162
+ def input_finished?
163
+ @decompressor.input_finished?
164
+ end
165
+ end
166
+
167
+
168
+
169
+ class Decompressor #:nodoc:all
170
+ CHUNK_SIZE=32768
171
+ def initialize(inputStream)
172
+ super()
173
+ @inputStream=inputStream
174
+ end
175
+ end
176
+
177
+ class Inflater < Decompressor #:nodoc:all
178
+ def initialize(inputStream)
179
+ super
180
+ @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
181
+ @outputBuffer=""
182
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
183
+ end
184
+
185
+ def sysread(numberOfBytes = nil, buf = nil)
186
+ readEverything = (numberOfBytes == nil)
187
+ while (readEverything || @outputBuffer.length < numberOfBytes)
188
+ break if internal_input_finished?
189
+ @outputBuffer << internal_produce_input(buf)
190
+ end
191
+ return value_when_finished if @outputBuffer.length==0 && input_finished?
192
+ endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
193
+ return @outputBuffer.slice!(0 ... endIndex)
194
+ end
195
+
196
+ def produce_input
197
+ if (@outputBuffer.empty?)
198
+ return internal_produce_input
199
+ else
200
+ return @outputBuffer.slice!(0...(@outputBuffer.length))
201
+ end
202
+ end
203
+
204
+ # to be used with produce_input, not read (as read may still have more data cached)
205
+ # is data cached anywhere other than @outputBuffer? the comment above may be wrong
206
+ def input_finished?
207
+ @outputBuffer.empty? && internal_input_finished?
208
+ end
209
+ alias :eof :input_finished?
210
+ alias :eof? :input_finished?
211
+
212
+ private
213
+
214
+ def internal_produce_input(buf = nil)
215
+ retried = 0
216
+ begin
217
+ @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE, buf))
218
+ rescue Zlib::BufError
219
+ raise if (retried >= 5) # how many times should we retry?
220
+ retried += 1
221
+ retry
222
+ end
223
+ end
224
+
225
+ def internal_input_finished?
226
+ @zlibInflater.finished?
227
+ end
228
+
229
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
230
+ def value_when_finished # mimic behaviour of ruby File object.
231
+ return nil if @hasReturnedEmptyString
232
+ @hasReturnedEmptyString=true
233
+ return ""
234
+ end
235
+ end
236
+
237
+ class PassThruDecompressor < Decompressor #:nodoc:all
238
+ def initialize(inputStream, charsToRead)
239
+ super inputStream
240
+ @charsToRead = charsToRead
241
+ @readSoFar = 0
242
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
243
+ end
244
+
245
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
246
+ def sysread(numberOfBytes = nil, buf = nil)
247
+ if input_finished?
248
+ hasReturnedEmptyStringVal=@hasReturnedEmptyString
249
+ @hasReturnedEmptyString=true
250
+ return "" unless hasReturnedEmptyStringVal
251
+ return nil
252
+ end
253
+
254
+ if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
255
+ numberOfBytes = @charsToRead-@readSoFar
256
+ end
257
+ @readSoFar += numberOfBytes
258
+ @inputStream.read(numberOfBytes, buf)
259
+ end
260
+
261
+ def produce_input
262
+ sysread(Decompressor::CHUNK_SIZE)
263
+ end
264
+
265
+ def input_finished?
266
+ (@readSoFar >= @charsToRead)
267
+ end
268
+ alias :eof :input_finished?
269
+ alias :eof? :input_finished?
270
+ end
271
+
272
+ class NullDecompressor #:nodoc:all
273
+ include Singleton
274
+ def sysread(numberOfBytes = nil, buf = nil)
275
+ nil
276
+ end
277
+
278
+ def produce_input
279
+ nil
280
+ end
281
+
282
+ def input_finished?
283
+ true
284
+ end
285
+
286
+ def eof
287
+ true
288
+ end
289
+ alias :eof? :eof
290
+ end
291
+
292
+ class NullInputStream < NullDecompressor #:nodoc:all
293
+ include IOExtras::AbstractInputStream
294
+ end
295
+
296
+ class ZipEntry
297
+ STORED = 0
298
+ DEFLATED = 8
299
+
300
+ FSTYPE_FAT = 0
301
+ FSTYPE_AMIGA = 1
302
+ FSTYPE_VMS = 2
303
+ FSTYPE_UNIX = 3
304
+ FSTYPE_VM_CMS = 4
305
+ FSTYPE_ATARI = 5
306
+ FSTYPE_HPFS = 6
307
+ FSTYPE_MAC = 7
308
+ FSTYPE_Z_SYSTEM = 8
309
+ FSTYPE_CPM = 9
310
+ FSTYPE_TOPS20 = 10
311
+ FSTYPE_NTFS = 11
312
+ FSTYPE_QDOS = 12
313
+ FSTYPE_ACORN = 13
314
+ FSTYPE_VFAT = 14
315
+ FSTYPE_MVS = 15
316
+ FSTYPE_BEOS = 16
317
+ FSTYPE_TANDEM = 17
318
+ FSTYPE_THEOS = 18
319
+ FSTYPE_MAC_OSX = 19
320
+ FSTYPE_ATHEOS = 30
321
+
322
+ FSTYPES = {
323
+ FSTYPE_FAT => 'FAT'.freeze,
324
+ FSTYPE_AMIGA => 'Amiga'.freeze,
325
+ FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
326
+ FSTYPE_UNIX => 'Unix'.freeze,
327
+ FSTYPE_VM_CMS => 'VM/CMS'.freeze,
328
+ FSTYPE_ATARI => 'Atari ST'.freeze,
329
+ FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
330
+ FSTYPE_MAC => 'Macintosh'.freeze,
331
+ FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
332
+ FSTYPE_CPM => 'CP/M'.freeze,
333
+ FSTYPE_TOPS20 => 'TOPS-20'.freeze,
334
+ FSTYPE_NTFS => 'NTFS'.freeze,
335
+ FSTYPE_QDOS => 'SMS/QDOS'.freeze,
336
+ FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
337
+ FSTYPE_VFAT => 'Win32 VFAT'.freeze,
338
+ FSTYPE_MVS => 'MVS'.freeze,
339
+ FSTYPE_BEOS => 'BeOS'.freeze,
340
+ FSTYPE_TANDEM => 'Tandem NSK'.freeze,
341
+ FSTYPE_THEOS => 'Theos'.freeze,
342
+ FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
343
+ FSTYPE_ATHEOS => 'AtheOS'.freeze,
344
+ }.freeze
345
+
346
+ attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
347
+ :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature
348
+
349
+ attr_accessor :follow_symlinks
350
+ attr_accessor :restore_times, :restore_permissions, :restore_ownership
351
+ attr_accessor :unix_uid, :unix_gid, :unix_perms
352
+
353
+ attr_reader :ftype, :filepath # :nodoc:
354
+
355
+ def initialize(zipfile = "", name = "", comment = "", extra = "",
356
+ compressed_size = 0, crc = 0,
357
+ compression_method = ZipEntry::DEFLATED, size = 0,
358
+ time = Time.now)
359
+ super()
360
+ if name.starts_with("/")
361
+ raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
362
+ end
363
+ @localHeaderOffset = 0
364
+ @internalFileAttributes = 1
365
+ @externalFileAttributes = 0
366
+ @version = 52 # this library's version
367
+ @ftype = nil # unspecified or unknown
368
+ @filepath = nil
369
+ if Zip::RUNNING_ON_WINDOWS
370
+ @fstype = FSTYPE_FAT
371
+ else
372
+ @fstype = FSTYPE_UNIX
373
+ end
374
+ @zipfile, @comment, @compressed_size, @crc, @extra, @compression_method,
375
+ @name, @size = zipfile, comment, compressed_size, crc,
376
+ extra, compression_method, name, size
377
+ @time = time
378
+
379
+ @follow_symlinks = false
380
+
381
+ @restore_times = true
382
+ @restore_permissions = false
383
+ @restore_ownership = false
384
+
385
+ # BUG: need an extra field to support uid/gid's
386
+ @unix_uid = nil
387
+ @unix_gid = nil
388
+ @unix_perms = nil
389
+ # @posix_acl = nil
390
+ # @ntfs_acl = nil
391
+
392
+ if name_is_directory?
393
+ @ftype = :directory
394
+ else
395
+ @ftype = :file
396
+ end
397
+
398
+ unless ZipExtraField === @extra
399
+ @extra = ZipExtraField.new(@extra.to_s)
400
+ end
401
+ end
402
+
403
+ def time
404
+ if @extra["UniversalTime"]
405
+ @extra["UniversalTime"].mtime
406
+ else
407
+ # Atandard time field in central directory has local time
408
+ # under archive creator. Then, we can't get timezone.
409
+ @time
410
+ end
411
+ end
412
+ alias :mtime :time
413
+
414
+ def time=(aTime)
415
+ unless @extra.member?("UniversalTime")
416
+ @extra.create("UniversalTime")
417
+ end
418
+ @extra["UniversalTime"].mtime = aTime
419
+ @time = aTime
420
+ end
421
+
422
+ # Returns +true+ if the entry is a directory.
423
+ def directory?
424
+ raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
425
+ @ftype == :directory
426
+ end
427
+ alias :is_directory :directory?
428
+
429
+ # Returns +true+ if the entry is a file.
430
+ def file?
431
+ raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
432
+ @ftype == :file
433
+ end
434
+
435
+ # Returns +true+ if the entry is a symlink.
436
+ def symlink?
437
+ raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
438
+ @ftype == :link
439
+ end
440
+
441
+ def name_is_directory? #:nodoc:all
442
+ (%r{\/$} =~ @name) != nil
443
+ end
444
+
445
+ def local_entry_offset #:nodoc:all
446
+ localHeaderOffset + local_header_size
447
+ end
448
+
449
+ def local_header_size #:nodoc:all
450
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0)
451
+ end
452
+
453
+ def cdir_header_size #:nodoc:all
454
+ CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
455
+ (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0)
456
+ end
457
+
458
+ def next_header_offset #:nodoc:all
459
+ local_entry_offset + self.compressed_size
460
+ end
461
+
462
+ # Extracts entry to file destPath (defaults to @name).
463
+ def extract(destPath = @name, &onExistsProc)
464
+ onExistsProc ||= proc { false }
465
+
466
+ if directory?
467
+ create_directory(destPath, &onExistsProc)
468
+ elsif file?
469
+ write_file(destPath, &onExistsProc)
470
+ elsif symlink?
471
+ create_symlink(destPath, &onExistsProc)
472
+ else
473
+ raise RuntimeError, "unknown file type #{self.inspect}"
474
+ end
475
+
476
+ self
477
+ end
478
+
479
+ def to_s
480
+ @name
481
+ end
482
+
483
+ protected
484
+
485
+ def ZipEntry.read_zip_short(io) # :nodoc:
486
+ io.read(2).unpack('v')[0]
487
+ end
488
+
489
+ def ZipEntry.read_zip_long(io) # :nodoc:
490
+ io.read(4).unpack('V')[0]
491
+ end
492
+ public
493
+
494
+ LOCAL_ENTRY_SIGNATURE = 0x04034b50
495
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
496
+ LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4
497
+
498
+ def read_local_entry(io) #:nodoc:all
499
+ @localHeaderOffset = io.tell
500
+ staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH)
501
+ unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH)
502
+ raise ZipError, "Premature end of file. Not enough data for zip entry local header"
503
+ end
504
+
505
+ @header_signature ,
506
+ @version ,
507
+ @fstype ,
508
+ @gp_flags ,
509
+ @compression_method,
510
+ lastModTime ,
511
+ lastModDate ,
512
+ @crc ,
513
+ @compressed_size ,
514
+ @size ,
515
+ nameLength ,
516
+ extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv')
517
+
518
+ unless (@header_signature == LOCAL_ENTRY_SIGNATURE)
519
+ raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
520
+ end
521
+ set_time(lastModDate, lastModTime)
522
+
523
+ @name = io.read(nameLength)
524
+ extra = io.read(extraLength)
525
+
526
+ if (extra && extra.length != extraLength)
527
+ raise ZipError, "Truncated local zip entry header"
528
+ else
529
+ if ZipExtraField === @extra
530
+ @extra.merge(extra)
531
+ else
532
+ @extra = ZipExtraField.new(extra)
533
+ end
534
+ end
535
+ end
536
+
537
+ def ZipEntry.read_local_entry(io)
538
+ entry = new(io.path)
539
+ entry.read_local_entry(io)
540
+ return entry
541
+ rescue ZipError
542
+ return nil
543
+ end
544
+
545
+ def write_local_entry(io) #:nodoc:all
546
+ @localHeaderOffset = io.tell
547
+
548
+ io <<
549
+ [LOCAL_ENTRY_SIGNATURE ,
550
+ 0 ,
551
+ 0 , # @gp_flags ,
552
+ @compression_method ,
553
+ @time.to_binary_dos_time , # @lastModTime ,
554
+ @time.to_binary_dos_date , # @lastModDate ,
555
+ @crc ,
556
+ @compressed_size ,
557
+ @size ,
558
+ @name ? @name.length : 0,
559
+ @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv')
560
+ io << @name
561
+ io << (@extra ? @extra.to_local_bin : "")
562
+ end
563
+
564
+ CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
565
+ CDIR_ENTRY_STATIC_HEADER_LENGTH = 46
566
+
567
+ def read_c_dir_entry(io) #:nodoc:all
568
+ staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH)
569
+ unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH)
570
+ raise ZipError, "Premature end of file. Not enough data for zip cdir entry header"
571
+ end
572
+
573
+ @header_signature ,
574
+ @version , # version of encoding software
575
+ @fstype , # filesystem type
576
+ @versionNeededToExtract,
577
+ @gp_flags ,
578
+ @compression_method ,
579
+ lastModTime ,
580
+ lastModDate ,
581
+ @crc ,
582
+ @compressed_size ,
583
+ @size ,
584
+ nameLength ,
585
+ extraLength ,
586
+ commentLength ,
587
+ diskNumberStart ,
588
+ @internalFileAttributes,
589
+ @externalFileAttributes,
590
+ @localHeaderOffset ,
591
+ @name ,
592
+ @extra ,
593
+ @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV')
594
+
595
+ unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
596
+ raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
597
+ end
598
+ set_time(lastModDate, lastModTime)
599
+
600
+ @name = io.read(nameLength)
601
+ if ZipExtraField === @extra
602
+ @extra.merge(io.read(extraLength))
603
+ else
604
+ @extra = ZipExtraField.new(io.read(extraLength))
605
+ end
606
+ @comment = io.read(commentLength)
607
+ unless (@comment && @comment.length == commentLength)
608
+ raise ZipError, "Truncated cdir zip entry header"
609
+ end
610
+
611
+ case @fstype
612
+ when FSTYPE_UNIX
613
+ @unix_perms = (@externalFileAttributes >> 16) & 07777
614
+
615
+ case (@externalFileAttributes >> 28)
616
+ when 04
617
+ @ftype = :directory
618
+ when 010
619
+ @ftype = :file
620
+ when 012
621
+ @ftype = :link
622
+ else
623
+ raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}"
624
+ end
625
+ else
626
+ if name_is_directory?
627
+ @ftype = :directory
628
+ else
629
+ @ftype = :file
630
+ end
631
+ end
632
+ end
633
+
634
+ def ZipEntry.read_c_dir_entry(io) #:nodoc:all
635
+ entry = new(io.path)
636
+ entry.read_c_dir_entry(io)
637
+ return entry
638
+ rescue ZipError
639
+ return nil
640
+ end
641
+
642
+ def file_stat(path) # :nodoc:
643
+ if @follow_symlinks
644
+ return File::stat(path)
645
+ else
646
+ return File::lstat(path)
647
+ end
648
+ end
649
+
650
+ def get_extra_attributes_from_path(path) # :nodoc:
651
+ unless Zip::RUNNING_ON_WINDOWS
652
+ stat = file_stat(path)
653
+ @unix_uid = stat.uid
654
+ @unix_gid = stat.gid
655
+ @unix_perms = stat.mode & 07777
656
+ end
657
+ end
658
+
659
+ def set_extra_attributes_on_path(destPath) # :nodoc:
660
+ return unless (file? or directory?)
661
+
662
+ case @fstype
663
+ when FSTYPE_UNIX
664
+ # BUG: does not update timestamps into account
665
+ # ignore setuid/setgid bits by default. honor if @restore_ownership
666
+ unix_perms_mask = 01777
667
+ unix_perms_mask = 07777 if (@restore_ownership)
668
+ File::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms)
669
+ File::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0)
670
+ # File::utimes()
671
+ end
672
+ end
673
+
674
+ def write_c_dir_entry(io) #:nodoc:all
675
+ case @fstype
676
+ when FSTYPE_UNIX
677
+ ft = nil
678
+ case @ftype
679
+ when :file
680
+ ft = 010
681
+ @unix_perms ||= 0644
682
+ when :directory
683
+ ft = 004
684
+ @unix_perms ||= 0755
685
+ when :symlink
686
+ ft = 012
687
+ @unix_perms ||= 0755
688
+ else
689
+ raise ZipInternalError, "unknown file type #{self.inspect}"
690
+ end
691
+
692
+ @externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16
693
+ end
694
+
695
+ io <<
696
+ [CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
697
+ @version , # version of encoding software
698
+ @fstype , # filesystem type
699
+ 0 , # @versionNeededToExtract ,
700
+ 0 , # @gp_flags ,
701
+ @compression_method ,
702
+ @time.to_binary_dos_time , # @lastModTime ,
703
+ @time.to_binary_dos_date , # @lastModDate ,
704
+ @crc ,
705
+ @compressed_size ,
706
+ @size ,
707
+ @name ? @name.length : 0 ,
708
+ @extra ? @extra.c_dir_length : 0 ,
709
+ @comment ? comment.length : 0 ,
710
+ 0 , # disk number start
711
+ @internalFileAttributes , # file type (binary=0, text=1)
712
+ @externalFileAttributes , # native filesystem attributes
713
+ @localHeaderOffset ,
714
+ @name ,
715
+ @extra ,
716
+ @comment ].pack('VCCvvvvvVVVvvvvvVV')
717
+
718
+ io << @name
719
+ io << (@extra ? @extra.to_c_dir_bin : "")
720
+ io << @comment
721
+ end
722
+
723
+ def == (other)
724
+ return false unless other.class == self.class
725
+ # Compares contents of local entry and exposed fields
726
+ (@compression_method == other.compression_method &&
727
+ @crc == other.crc &&
728
+ @compressed_size == other.compressed_size &&
729
+ @size == other.size &&
730
+ @name == other.name &&
731
+ @extra == other.extra &&
732
+ @filepath == other.filepath &&
733
+ self.time.dos_equals(other.time))
734
+ end
735
+
736
+ def <=> (other)
737
+ return to_s <=> other.to_s
738
+ end
739
+
740
+ # Returns an IO like object for the given ZipEntry.
741
+ # Warning: may behave weird with symlinks.
742
+ def get_input_stream(&aProc)
743
+ if @ftype == :directory
744
+ return yield(NullInputStream.instance) if block_given?
745
+ return NullInputStream.instance
746
+ elsif @filepath
747
+ case @ftype
748
+ when :file
749
+ return File.open(@filepath, "rb", &aProc)
750
+
751
+ when :symlink
752
+ linkpath = File::readlink(@filepath)
753
+ stringio = StringIO.new(linkpath)
754
+ return yield(stringio) if block_given?
755
+ return stringio
756
+ else
757
+ raise "unknown @ftype #{@ftype}"
758
+ end
759
+ else
760
+ zis = ZipInputStream.new(@zipfile, localHeaderOffset)
761
+ zis.get_next_entry
762
+ if block_given?
763
+ begin
764
+ return yield(zis)
765
+ ensure
766
+ zis.close
767
+ end
768
+ else
769
+ return zis
770
+ end
771
+ end
772
+ end
773
+
774
+ def gather_fileinfo_from_srcpath(srcPath) # :nodoc:
775
+ stat = file_stat(srcPath)
776
+ case stat.ftype
777
+ when 'file'
778
+ if name_is_directory?
779
+ raise ArgumentError,
780
+ "entry name '#{newEntry}' indicates directory entry, but "+
781
+ "'#{srcPath}' is not a directory"
782
+ end
783
+ @ftype = :file
784
+ when 'directory'
785
+ if ! name_is_directory?
786
+ @name += "/"
787
+ end
788
+ @ftype = :directory
789
+ when 'link'
790
+ if name_is_directory?
791
+ raise ArgumentError,
792
+ "entry name '#{newEntry}' indicates directory entry, but "+
793
+ "'#{srcPath}' is not a directory"
794
+ end
795
+ @ftype = :symlink
796
+ else
797
+ raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}"
798
+ end
799
+
800
+ @filepath = srcPath
801
+ get_extra_attributes_from_path(@filepath)
802
+ end
803
+
804
+ def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all
805
+ if @ftype == :directory
806
+ aZipOutputStream.put_next_entry(self)
807
+ elsif @filepath
808
+ aZipOutputStream.put_next_entry(self)
809
+ get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
810
+ else
811
+ aZipOutputStream.copy_raw_entry(self)
812
+ end
813
+ end
814
+
815
+ def parent_as_string
816
+ entry_name = name.chomp("/")
817
+ slash_index = entry_name.rindex("/")
818
+ slash_index ? entry_name.slice(0, slash_index+1) : nil
819
+ end
820
+
821
+ def get_raw_input_stream(&aProc)
822
+ File.open(@zipfile, "rb", &aProc)
823
+ end
824
+
825
+ private
826
+
827
+ def set_time(binaryDosDate, binaryDosTime)
828
+ @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime)
829
+ rescue ArgumentError
830
+ puts "Invalid date/time in zip entry"
831
+ end
832
+
833
+ def write_file(destPath, continueOnExistsProc = proc { false })
834
+ if File.exists?(destPath) && ! yield(self, destPath)
835
+ raise ZipDestinationFileExistsError,
836
+ "Destination '#{destPath}' already exists"
837
+ end
838
+ File.open(destPath, "wb") do |os|
839
+ get_input_stream do |is|
840
+ set_extra_attributes_on_path(destPath)
841
+
842
+ buf = ''
843
+ while buf = is.sysread(Decompressor::CHUNK_SIZE, buf)
844
+ os << buf
845
+ end
846
+ end
847
+ end
848
+ end
849
+
850
+ def create_directory(destPath)
851
+ if File.directory? destPath
852
+ return
853
+ elsif File.exists? destPath
854
+ if block_given? && yield(self, destPath)
855
+ File.rm_f destPath
856
+ else
857
+ raise ZipDestinationFileExistsError,
858
+ "Cannot create directory '#{destPath}'. "+
859
+ "A file already exists with that name"
860
+ end
861
+ end
862
+ Dir.mkdir destPath
863
+ set_extra_attributes_on_path(destPath)
864
+ end
865
+
866
+ # BUG: create_symlink() does not use &onExistsProc
867
+ def create_symlink(destPath)
868
+ stat = nil
869
+ begin
870
+ stat = File::lstat(destPath)
871
+ rescue Errno::ENOENT
872
+ end
873
+
874
+ io = get_input_stream
875
+ linkto = io.read
876
+
877
+ if stat
878
+ if stat.symlink?
879
+ if File::readlink(destPath) == linkto
880
+ return
881
+ else
882
+ raise ZipDestinationFileExistsError,
883
+ "Cannot create symlink '#{destPath}'. "+
884
+ "A symlink already exists with that name"
885
+ end
886
+ else
887
+ raise ZipDestinationFileExistsError,
888
+ "Cannot create symlink '#{destPath}'. "+
889
+ "A file already exists with that name"
890
+ end
891
+ end
892
+
893
+ File::symlink(linkto, destPath)
894
+ end
895
+ end
896
+
897
+
898
+ # ZipOutputStream is the basic class for writing zip files. It is
899
+ # possible to create a ZipOutputStream object directly, passing
900
+ # the zip file name to the constructor, but more often than not
901
+ # the ZipOutputStream will be obtained from a ZipFile (perhaps using the
902
+ # ZipFileSystem interface) object for a particular entry in the zip
903
+ # archive.
904
+ #
905
+ # A ZipOutputStream inherits IOExtras::AbstractOutputStream in order
906
+ # to provide an IO-like interface for writing to a single zip
907
+ # entry. Beyond methods for mimicking an IO-object it contains
908
+ # the method put_next_entry that closes the current entry
909
+ # and creates a new.
910
+ #
911
+ # Please refer to ZipInputStream for example code.
912
+ #
913
+ # java.util.zip.ZipOutputStream is the original inspiration for this
914
+ # class.
915
+
916
+ class ZipOutputStream
917
+ include IOExtras::AbstractOutputStream
918
+
919
+ attr_accessor :comment
920
+
921
+ # Opens the indicated zip file. If a file with that name already
922
+ # exists it will be overwritten.
923
+ def initialize(fileName)
924
+ super()
925
+ @fileName = fileName
926
+ @outputStream = File.new(@fileName, "wb")
927
+ @entrySet = ZipEntrySet.new
928
+ @compressor = NullCompressor.instance
929
+ @closed = false
930
+ @currentEntry = nil
931
+ @comment = nil
932
+ end
933
+
934
+ # Same as #initialize but if a block is passed the opened
935
+ # stream is passed to the block and closed when the block
936
+ # returns.
937
+ def ZipOutputStream.open(fileName)
938
+ return new(fileName) unless block_given?
939
+ zos = new(fileName)
940
+ yield zos
941
+ ensure
942
+ zos.close if zos
943
+ end
944
+
945
+ # Closes the stream and writes the central directory to the zip file
946
+ def close
947
+ return if @closed
948
+ finalize_current_entry
949
+ update_local_headers
950
+ write_central_directory
951
+ @outputStream.close
952
+ @closed = true
953
+ end
954
+
955
+ # Closes the current entry and opens a new for writing.
956
+ # +entry+ can be a ZipEntry object or a string.
957
+ def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
958
+ raise ZipError, "zip stream is closed" if @closed
959
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s)
960
+ init_next_entry(newEntry, level)
961
+ @currentEntry=newEntry
962
+ end
963
+
964
+ def copy_raw_entry(entry)
965
+ entry = entry.dup
966
+ raise ZipError, "zip stream is closed" if @closed
967
+ raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry)
968
+ finalize_current_entry
969
+ @entrySet << entry
970
+ src_pos = entry.local_entry_offset
971
+ entry.write_local_entry(@outputStream)
972
+ @compressor = NullCompressor.instance
973
+ @outputStream << entry.get_raw_input_stream {
974
+ |is|
975
+ is.seek(src_pos, IO::SEEK_SET)
976
+ is.read(entry.compressed_size)
977
+ }
978
+ @compressor = NullCompressor.instance
979
+ @currentEntry = nil
980
+ end
981
+
982
+ private
983
+ def finalize_current_entry
984
+ return unless @currentEntry
985
+ finish
986
+ @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset -
987
+ @currentEntry.local_header_size
988
+ @currentEntry.size = @compressor.size
989
+ @currentEntry.crc = @compressor.crc
990
+ @currentEntry = nil
991
+ @compressor = NullCompressor.instance
992
+ end
993
+
994
+ def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
995
+ finalize_current_entry
996
+ @entrySet << entry
997
+ entry.write_local_entry(@outputStream)
998
+ @compressor = get_compressor(entry, level)
999
+ end
1000
+
1001
+ def get_compressor(entry, level)
1002
+ case entry.compression_method
1003
+ when ZipEntry::DEFLATED then Deflater.new(@outputStream, level)
1004
+ when ZipEntry::STORED then PassThruCompressor.new(@outputStream)
1005
+ else raise ZipCompressionMethodError,
1006
+ "Invalid compression method: '#{entry.compression_method}'"
1007
+ end
1008
+ end
1009
+
1010
+ def update_local_headers
1011
+ pos = @outputStream.tell
1012
+ @entrySet.each {
1013
+ |entry|
1014
+ @outputStream.pos = entry.localHeaderOffset
1015
+ entry.write_local_entry(@outputStream)
1016
+ }
1017
+ @outputStream.pos = pos
1018
+ end
1019
+
1020
+ def write_central_directory
1021
+ cdir = ZipCentralDirectory.new(@entrySet, @comment)
1022
+ cdir.write_to_stream(@outputStream)
1023
+ end
1024
+
1025
+ protected
1026
+
1027
+ def finish
1028
+ @compressor.finish
1029
+ end
1030
+
1031
+ public
1032
+ # Modeled after IO.<<
1033
+ def << (data)
1034
+ @compressor << data
1035
+ end
1036
+ end
1037
+
1038
+
1039
+ class Compressor #:nodoc:all
1040
+ def finish
1041
+ end
1042
+ end
1043
+
1044
+ class PassThruCompressor < Compressor #:nodoc:all
1045
+ def initialize(outputStream)
1046
+ super()
1047
+ @outputStream = outputStream
1048
+ @crc = Zlib::crc32
1049
+ @size = 0
1050
+ end
1051
+
1052
+ def << (data)
1053
+ val = data.to_s
1054
+ @crc = Zlib::crc32(val, @crc)
1055
+ @size += val.size
1056
+ @outputStream << val
1057
+ end
1058
+
1059
+ attr_reader :size, :crc
1060
+ end
1061
+
1062
+ class NullCompressor < Compressor #:nodoc:all
1063
+ include Singleton
1064
+
1065
+ def << (data)
1066
+ raise IOError, "closed stream"
1067
+ end
1068
+
1069
+ attr_reader :size, :compressed_size
1070
+ end
1071
+
1072
+ class Deflater < Compressor #:nodoc:all
1073
+ def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION)
1074
+ super()
1075
+ @outputStream = outputStream
1076
+ @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
1077
+ @size = 0
1078
+ @crc = Zlib::crc32
1079
+ end
1080
+
1081
+ def << (data)
1082
+ val = data.to_s
1083
+ @crc = Zlib::crc32(val, @crc)
1084
+ @size += val.size
1085
+ @outputStream << @zlibDeflater.deflate(data)
1086
+ end
1087
+
1088
+ def finish
1089
+ until @zlibDeflater.finished?
1090
+ @outputStream << @zlibDeflater.finish
1091
+ end
1092
+ end
1093
+
1094
+ attr_reader :size, :crc
1095
+ end
1096
+
1097
+
1098
+ class ZipEntrySet #:nodoc:all
1099
+ include Enumerable
1100
+
1101
+ def initialize(anEnumerable = [])
1102
+ super()
1103
+ @entrySet = {}
1104
+ anEnumerable.each { |o| push(o) }
1105
+ end
1106
+
1107
+ def include?(entry)
1108
+ @entrySet.include?(entry.to_s)
1109
+ end
1110
+
1111
+ def <<(entry)
1112
+ @entrySet[entry.to_s] = entry
1113
+ end
1114
+ alias :push :<<
1115
+
1116
+ def size
1117
+ @entrySet.size
1118
+ end
1119
+ alias :length :size
1120
+
1121
+ def delete(entry)
1122
+ @entrySet.delete(entry.to_s) ? entry : nil
1123
+ end
1124
+
1125
+ def each(&aProc)
1126
+ @entrySet.values.each(&aProc)
1127
+ end
1128
+
1129
+ def entries
1130
+ @entrySet.values
1131
+ end
1132
+
1133
+ # deep clone
1134
+ def dup
1135
+ newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup })
1136
+ end
1137
+
1138
+ def == (other)
1139
+ return false unless other.kind_of?(ZipEntrySet)
1140
+ return @entrySet == other.entrySet
1141
+ end
1142
+
1143
+ def parent(entry)
1144
+ @entrySet[entry.parent_as_string]
1145
+ end
1146
+
1147
+ def glob(pattern, flags = File::FNM_PATHNAME|File::FNM_DOTMATCH)
1148
+ entries.select {
1149
+ |entry|
1150
+ File.fnmatch(pattern, entry.name.chomp('/'), flags)
1151
+ }
1152
+ end
1153
+
1154
+ #TODO attr_accessor :auto_create_directories
1155
+ protected
1156
+ attr_accessor :entrySet
1157
+ end
1158
+
1159
+
1160
+ class ZipCentralDirectory
1161
+ include Enumerable
1162
+
1163
+ END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
1164
+ MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18
1165
+ STATIC_EOCD_SIZE = 22
1166
+
1167
+ attr_reader :comment
1168
+
1169
+ # Returns an Enumerable containing the entries.
1170
+ def entries
1171
+ @entrySet.entries
1172
+ end
1173
+
1174
+ def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc:
1175
+ super()
1176
+ @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries)
1177
+ @comment = comment
1178
+ end
1179
+
1180
+ def write_to_stream(io) #:nodoc:
1181
+ offset = io.tell
1182
+ @entrySet.each { |entry| entry.write_c_dir_entry(io) }
1183
+ write_e_o_c_d(io, offset)
1184
+ end
1185
+
1186
+ def write_e_o_c_d(io, offset) #:nodoc:
1187
+ io <<
1188
+ [END_OF_CENTRAL_DIRECTORY_SIGNATURE,
1189
+ 0 , # @numberOfThisDisk
1190
+ 0 , # @numberOfDiskWithStartOfCDir
1191
+ @entrySet? @entrySet.size : 0 ,
1192
+ @entrySet? @entrySet.size : 0 ,
1193
+ cdir_size ,
1194
+ offset ,
1195
+ @comment ? @comment.length : 0 ].pack('VvvvvVVv')
1196
+ io << @comment
1197
+ end
1198
+ private :write_e_o_c_d
1199
+
1200
+ def cdir_size #:nodoc:
1201
+ # does not include eocd
1202
+ @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value }
1203
+ end
1204
+ private :cdir_size
1205
+
1206
+ def read_e_o_c_d(io) #:nodoc:
1207
+ buf = get_e_o_c_d(io)
1208
+ @numberOfThisDisk = ZipEntry::read_zip_short(buf)
1209
+ @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf)
1210
+ @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf)
1211
+ @size = ZipEntry::read_zip_short(buf)
1212
+ @sizeInBytes = ZipEntry::read_zip_long(buf)
1213
+ @cdirOffset = ZipEntry::read_zip_long(buf)
1214
+ commentLength = ZipEntry::read_zip_short(buf)
1215
+ @comment = buf.read(commentLength)
1216
+ raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0
1217
+ end
1218
+
1219
+ def read_central_directory_entries(io) #:nodoc:
1220
+ begin
1221
+ io.seek(@cdirOffset, IO::SEEK_SET)
1222
+ rescue Errno::EINVAL
1223
+ raise ZipError, "Zip consistency problem while reading central directory entry"
1224
+ end
1225
+ @entrySet = ZipEntrySet.new
1226
+ @size.times {
1227
+ @entrySet << ZipEntry.read_c_dir_entry(io)
1228
+ }
1229
+ end
1230
+
1231
+ def read_from_stream(io) #:nodoc:
1232
+ read_e_o_c_d(io)
1233
+ read_central_directory_entries(io)
1234
+ end
1235
+
1236
+ def get_e_o_c_d(io) #:nodoc:
1237
+ begin
1238
+ io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END)
1239
+ rescue Errno::EINVAL
1240
+ io.seek(0, IO::SEEK_SET)
1241
+ rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL
1242
+ io.seek(0, IO::SEEK_SET)
1243
+ end
1244
+
1245
+ # 'buf = io.read' substituted with lump of code to work around FreeBSD 4.5 issue
1246
+ retried = false
1247
+ buf = nil
1248
+ begin
1249
+ buf = io.read
1250
+ rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG
1251
+ raise if (retried)
1252
+ retried = true
1253
+
1254
+ io.seek(0, IO::SEEK_SET)
1255
+ retry
1256
+ end
1257
+
1258
+ sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V'))
1259
+ raise ZipError, "Zip end of central directory signature not found" unless sigIndex
1260
+ buf=buf.slice!((sigIndex+4)...(buf.size))
1261
+ def buf.read(count)
1262
+ slice!(0, count)
1263
+ end
1264
+ return buf
1265
+ end
1266
+
1267
+ # For iterating over the entries.
1268
+ def each(&proc)
1269
+ @entrySet.each(&proc)
1270
+ end
1271
+
1272
+ # Returns the number of entries in the central directory (and
1273
+ # consequently in the zip archive).
1274
+ def size
1275
+ @entrySet.size
1276
+ end
1277
+
1278
+ def ZipCentralDirectory.read_from_stream(io) #:nodoc:
1279
+ cdir = new
1280
+ cdir.read_from_stream(io)
1281
+ return cdir
1282
+ rescue ZipError
1283
+ return nil
1284
+ end
1285
+
1286
+ def == (other) #:nodoc:
1287
+ return false unless other.kind_of?(ZipCentralDirectory)
1288
+ @entrySet.entries.sort == other.entries.sort && comment == other.comment
1289
+ end
1290
+ end
1291
+
1292
+
1293
+ class ZipError < StandardError ; end
1294
+
1295
+ class ZipEntryExistsError < ZipError; end
1296
+ class ZipDestinationFileExistsError < ZipError; end
1297
+ class ZipCompressionMethodError < ZipError; end
1298
+ class ZipEntryNameError < ZipError; end
1299
+ class ZipInternalError < ZipError; end
1300
+
1301
+ # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
1302
+ # The most important methods are those inherited from
1303
+ # ZipCentralDirectory for accessing information about the entries in
1304
+ # the archive and methods such as get_input_stream and
1305
+ # get_output_stream for reading from and writing entries to the
1306
+ # archive. The class includes a few convenience methods such as
1307
+ # #extract for extracting entries to the filesystem, and #remove,
1308
+ # #replace, #rename and #mkdir for making simple modifications to
1309
+ # the archive.
1310
+ #
1311
+ # Modifications to a zip archive are not committed until #commit or
1312
+ # #close is called. The method #open accepts a block following
1313
+ # the pattern from File.open offering a simple way to
1314
+ # automatically close the archive when the block returns.
1315
+ #
1316
+ # The following example opens zip archive <code>my.zip</code>
1317
+ # (creating it if it doesn't exist) and adds an entry
1318
+ # <code>first.txt</code> and a directory entry <code>a_dir</code>
1319
+ # to it.
1320
+ #
1321
+ # require 'zip/zip'
1322
+ #
1323
+ # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
1324
+ # |zipfile|
1325
+ # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
1326
+ # zipfile.mkdir("a_dir")
1327
+ # }
1328
+ #
1329
+ # The next example reopens <code>my.zip</code> writes the contents of
1330
+ # <code>first.txt</code> to standard out and deletes the entry from
1331
+ # the archive.
1332
+ #
1333
+ # require 'zip/zip'
1334
+ #
1335
+ # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
1336
+ # |zipfile|
1337
+ # puts zipfile.read("first.txt")
1338
+ # zipfile.remove("first.txt")
1339
+ # }
1340
+ #
1341
+ # ZipFileSystem offers an alternative API that emulates ruby's
1342
+ # interface for accessing the filesystem, ie. the File and Dir classes.
1343
+
1344
+ class ZipFile < ZipCentralDirectory
1345
+
1346
+ CREATE = 1
1347
+
1348
+ attr_reader :name
1349
+
1350
+ # default -> false
1351
+ attr_accessor :restore_ownership
1352
+ # default -> false
1353
+ attr_accessor :restore_permissions
1354
+ # default -> true
1355
+ attr_accessor :restore_times
1356
+
1357
+ # Opens a zip archive. Pass true as the second parameter to create
1358
+ # a new archive if it doesn't exist already.
1359
+ def initialize(fileName, create = nil)
1360
+ super()
1361
+ @name = fileName
1362
+ @comment = ""
1363
+ if (File.exists?(fileName))
1364
+ File.open(name, "rb") { |f| read_from_stream(f) }
1365
+ elsif (create)
1366
+ @entrySet = ZipEntrySet.new
1367
+ else
1368
+ raise ZipError, "File #{fileName} not found"
1369
+ end
1370
+ @create = create
1371
+ @storedEntries = @entrySet.dup
1372
+
1373
+ @restore_ownership = false
1374
+ @restore_permissions = false
1375
+ @restore_times = true
1376
+ end
1377
+
1378
+ # Same as #new. If a block is passed the ZipFile object is passed
1379
+ # to the block and is automatically closed afterwards just as with
1380
+ # ruby's builtin File.open method.
1381
+ def ZipFile.open(fileName, create = nil)
1382
+ zf = ZipFile.new(fileName, create)
1383
+ if block_given?
1384
+ begin
1385
+ yield zf
1386
+ ensure
1387
+ zf.close
1388
+ end
1389
+ else
1390
+ zf
1391
+ end
1392
+ end
1393
+
1394
+ # Returns the zip files comment, if it has one
1395
+ attr_accessor :comment
1396
+
1397
+ # Iterates over the contents of the ZipFile. This is more efficient
1398
+ # than using a ZipInputStream since this methods simply iterates
1399
+ # through the entries in the central directory structure in the archive
1400
+ # whereas ZipInputStream jumps through the entire archive accessing the
1401
+ # local entry headers (which contain the same information as the
1402
+ # central directory).
1403
+ def ZipFile.foreach(aZipFileName, &block)
1404
+ ZipFile.open(aZipFileName) {
1405
+ |zipFile|
1406
+ zipFile.each(&block)
1407
+ }
1408
+ end
1409
+
1410
+ # Returns an input stream to the specified entry. If a block is passed
1411
+ # the stream object is passed to the block and the stream is automatically
1412
+ # closed afterwards just as with ruby's builtin File.open method.
1413
+ def get_input_stream(entry, &aProc)
1414
+ get_entry(entry).get_input_stream(&aProc)
1415
+ end
1416
+
1417
+ # Returns an output stream to the specified entry. If a block is passed
1418
+ # the stream object is passed to the block and the stream is automatically
1419
+ # closed afterwards just as with ruby's builtin File.open method.
1420
+ def get_output_stream(entry, &aProc)
1421
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
1422
+ if newEntry.directory?
1423
+ raise ArgumentError,
1424
+ "cannot open stream to directory entry - '#{newEntry}'"
1425
+ end
1426
+ zipStreamableEntry = ZipStreamableStream.new(newEntry)
1427
+ @entrySet << zipStreamableEntry
1428
+ zipStreamableEntry.get_output_stream(&aProc)
1429
+ end
1430
+
1431
+ # Returns the name of the zip archive
1432
+ def to_s
1433
+ @name
1434
+ end
1435
+
1436
+ # Returns a string containing the contents of the specified entry
1437
+ def read(entry)
1438
+ get_input_stream(entry) { |is| is.read }
1439
+ end
1440
+
1441
+ # Convenience method for adding the contents of a file to the archive
1442
+ def add(entry, srcPath, &continueOnExistsProc)
1443
+ continueOnExistsProc ||= proc { false }
1444
+ check_entry_exists(entry, continueOnExistsProc, "add")
1445
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
1446
+ newEntry.gather_fileinfo_from_srcpath(srcPath)
1447
+ @entrySet << newEntry
1448
+ end
1449
+
1450
+ # Removes the specified entry.
1451
+ def remove(entry)
1452
+ @entrySet.delete(get_entry(entry))
1453
+ end
1454
+
1455
+ # Renames the specified entry.
1456
+ def rename(entry, newName, &continueOnExistsProc)
1457
+ foundEntry = get_entry(entry)
1458
+ check_entry_exists(newName, continueOnExistsProc, "rename")
1459
+ foundEntry.name=newName
1460
+ end
1461
+
1462
+ # Replaces the specified entry with the contents of srcPath (from
1463
+ # the file system).
1464
+ def replace(entry, srcPath)
1465
+ check_file(srcPath)
1466
+ add(remove(entry), srcPath)
1467
+ end
1468
+
1469
+ # Extracts entry to file destPath.
1470
+ def extract(entry, destPath, &onExistsProc)
1471
+ onExistsProc ||= proc { false }
1472
+ foundEntry = get_entry(entry)
1473
+ foundEntry.extract(destPath, &onExistsProc)
1474
+ end
1475
+
1476
+ # Commits changes that has been made since the previous commit to
1477
+ # the zip archive.
1478
+ def commit
1479
+ return if ! commit_required?
1480
+ on_success_replace(name) {
1481
+ |tmpFile|
1482
+ ZipOutputStream.open(tmpFile) {
1483
+ |zos|
1484
+
1485
+ @entrySet.each { |e| e.write_to_zip_output_stream(zos) }
1486
+ zos.comment = comment
1487
+ }
1488
+ true
1489
+ }
1490
+ initialize(name)
1491
+ end
1492
+
1493
+ # Closes the zip file committing any changes that has been made.
1494
+ def close
1495
+ commit
1496
+ end
1497
+
1498
+ # Returns true if any changes has been made to this archive since
1499
+ # the previous commit
1500
+ def commit_required?
1501
+ return @entrySet != @storedEntries || @create == ZipFile::CREATE
1502
+ end
1503
+
1504
+ # Searches for entry with the specified name. Returns nil if
1505
+ # no entry is found. See also get_entry
1506
+ def find_entry(entry)
1507
+ @entrySet.detect {
1508
+ |e|
1509
+ e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
1510
+ }
1511
+ end
1512
+
1513
+ # Searches for an entry just as find_entry, but throws Errno::ENOENT
1514
+ # if no entry is found.
1515
+ def get_entry(entry)
1516
+ selectedEntry = find_entry(entry)
1517
+ unless selectedEntry
1518
+ raise Errno::ENOENT, entry
1519
+ end
1520
+ selectedEntry.restore_ownership = @restore_ownership
1521
+ selectedEntry.restore_permissions = @restore_permissions
1522
+ selectedEntry.restore_times = @restore_times
1523
+
1524
+ return selectedEntry
1525
+ end
1526
+
1527
+ # Creates a directory
1528
+ def mkdir(entryName, permissionInt = 0755)
1529
+ if find_entry(entryName)
1530
+ raise Errno::EEXIST, "File exists - #{entryName}"
1531
+ end
1532
+ @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt)
1533
+ end
1534
+
1535
+ private
1536
+
1537
+ def is_directory(newEntry, srcPath)
1538
+ srcPathIsDirectory = File.directory?(srcPath)
1539
+ if newEntry.is_directory && ! srcPathIsDirectory
1540
+ raise ArgumentError,
1541
+ "entry name '#{newEntry}' indicates directory entry, but "+
1542
+ "'#{srcPath}' is not a directory"
1543
+ elsif ! newEntry.is_directory && srcPathIsDirectory
1544
+ newEntry.name += "/"
1545
+ end
1546
+ return newEntry.is_directory && srcPathIsDirectory
1547
+ end
1548
+
1549
+ def check_entry_exists(entryName, continueOnExistsProc, procedureName)
1550
+ continueOnExistsProc ||= proc { false }
1551
+ if @entrySet.detect { |e| e.name == entryName }
1552
+ if continueOnExistsProc.call
1553
+ remove get_entry(entryName)
1554
+ else
1555
+ raise ZipEntryExistsError,
1556
+ procedureName+" failed. Entry #{entryName} already exists"
1557
+ end
1558
+ end
1559
+ end
1560
+
1561
+ def check_file(path)
1562
+ unless File.readable? path
1563
+ raise Errno::ENOENT, path
1564
+ end
1565
+ end
1566
+
1567
+ def on_success_replace(aFilename)
1568
+ tmpfile = get_tempfile
1569
+ tmpFilename = tmpfile.path
1570
+ tmpfile.close
1571
+ if yield tmpFilename
1572
+ File.move(tmpFilename, name)
1573
+ end
1574
+ end
1575
+
1576
+ def get_tempfile
1577
+ tempFile = Tempfile.new(File.basename(name), File.dirname(name))
1578
+ tempFile.binmode
1579
+ tempFile
1580
+ end
1581
+
1582
+ end
1583
+
1584
+ class ZipStreamableDirectory < ZipEntry
1585
+ def initialize(zipfile, entry, srcPath = nil, permissionInt = nil)
1586
+ super(zipfile, entry)
1587
+
1588
+ @ftype = :directory
1589
+ entry.get_extra_attributes_from_path(srcPath) if (srcPath)
1590
+ @unix_perms = permissionInt if (permissionInt)
1591
+ end
1592
+ end
1593
+
1594
+ class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all
1595
+ def initialize(entry)
1596
+ super(entry)
1597
+ @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile))
1598
+ @tempFile.binmode
1599
+ end
1600
+
1601
+ def get_output_stream
1602
+ if block_given?
1603
+ begin
1604
+ yield(@tempFile)
1605
+ ensure
1606
+ @tempFile.close
1607
+ end
1608
+ else
1609
+ @tempFile
1610
+ end
1611
+ end
1612
+
1613
+ def get_input_stream
1614
+ if ! @tempFile.closed?
1615
+ raise StandardError, "cannot open entry for reading while its open for writing - #{name}"
1616
+ end
1617
+ @tempFile.open # reopens tempfile from top
1618
+ @tempFile.binmode
1619
+ if block_given?
1620
+ begin
1621
+ yield(@tempFile)
1622
+ ensure
1623
+ @tempFile.close
1624
+ end
1625
+ else
1626
+ @tempFile
1627
+ end
1628
+ end
1629
+
1630
+ def write_to_zip_output_stream(aZipOutputStream)
1631
+ aZipOutputStream.put_next_entry(self)
1632
+ get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
1633
+ end
1634
+ end
1635
+
1636
+ class ZipExtraField < Hash
1637
+ ID_MAP = {}
1638
+
1639
+ # Meta class for extra fields
1640
+ class Generic
1641
+ def self.register_map
1642
+ if self.const_defined?(:HEADER_ID)
1643
+ ID_MAP[self.const_get(:HEADER_ID)] = self
1644
+ end
1645
+ end
1646
+
1647
+ def self.name
1648
+ self.to_s.split("::")[-1]
1649
+ end
1650
+
1651
+ # return field [size, content] or false
1652
+ def initial_parse(binstr)
1653
+ if ! binstr
1654
+ # If nil, start with empty.
1655
+ return false
1656
+ elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
1657
+ $stderr.puts "Warning: weired extra feild header ID. skip parsing"
1658
+ return false
1659
+ end
1660
+ [binstr[2,2].unpack("v")[0], binstr[4..-1]]
1661
+ end
1662
+
1663
+ def ==(other)
1664
+ self.class != other.class and return false
1665
+ each { |k, v|
1666
+ v != other[k] and return false
1667
+ }
1668
+ true
1669
+ end
1670
+
1671
+ def to_local_bin
1672
+ s = pack_for_local
1673
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1674
+ end
1675
+
1676
+ def to_c_dir_bin
1677
+ s = pack_for_c_dir
1678
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1679
+ end
1680
+ end
1681
+
1682
+ # Info-ZIP Additional timestamp field
1683
+ class UniversalTime < Generic
1684
+ HEADER_ID = "UT"
1685
+ register_map
1686
+
1687
+ def initialize(binstr = nil)
1688
+ @ctime = nil
1689
+ @mtime = nil
1690
+ @atime = nil
1691
+ @flag = nil
1692
+ binstr and merge(binstr)
1693
+ end
1694
+ attr_accessor :atime, :ctime, :mtime, :flag
1695
+
1696
+ def merge(binstr)
1697
+ binstr == "" and return
1698
+ size, content = initial_parse(binstr)
1699
+ size or return
1700
+ @flag, mtime, atime, ctime = content.unpack("CVVV")
1701
+ mtime and @mtime ||= Time.at(mtime)
1702
+ atime and @atime ||= Time.at(atime)
1703
+ ctime and @ctime ||= Time.at(ctime)
1704
+ end
1705
+
1706
+ def ==(other)
1707
+ @mtime == other.mtime &&
1708
+ @atime == other.atime &&
1709
+ @ctime == other.ctime
1710
+ end
1711
+
1712
+ def pack_for_local
1713
+ s = [@flag].pack("C")
1714
+ @flag & 1 != 0 and s << [@mtime.to_i].pack("V")
1715
+ @flag & 2 != 0 and s << [@atime.to_i].pack("V")
1716
+ @flag & 4 != 0 and s << [@ctime.to_i].pack("V")
1717
+ s
1718
+ end
1719
+
1720
+ def pack_for_c_dir
1721
+ s = [@flag].pack("C")
1722
+ @flag & 1 == 1 and s << [@mtime.to_i].pack("V")
1723
+ s
1724
+ end
1725
+ end
1726
+
1727
+ # Info-ZIP Extra for UNIX uid/gid
1728
+ class IUnix < Generic
1729
+ HEADER_ID = "Ux"
1730
+ register_map
1731
+
1732
+ def initialize(binstr = nil)
1733
+ @uid = 0
1734
+ @gid = 0
1735
+ binstr and merge(binstr)
1736
+ end
1737
+ attr_accessor :uid, :gid
1738
+
1739
+ def merge(binstr)
1740
+ binstr == "" and return
1741
+ size, content = initial_parse(binstr)
1742
+ # size: 0 for central direcotry. 4 for local header
1743
+ return if(! size || size == 0)
1744
+ uid, gid = content.unpack("vv")
1745
+ @uid ||= uid
1746
+ @gid ||= gid
1747
+ end
1748
+
1749
+ def ==(other)
1750
+ @uid == other.uid &&
1751
+ @gid == other.gid
1752
+ end
1753
+
1754
+ def pack_for_local
1755
+ [@uid, @gid].pack("vv")
1756
+ end
1757
+
1758
+ def pack_for_c_dir
1759
+ ""
1760
+ end
1761
+ end
1762
+
1763
+ ## start main of ZipExtraField < Hash
1764
+ def initialize(binstr = nil)
1765
+ binstr and merge(binstr)
1766
+ end
1767
+
1768
+ def merge(binstr)
1769
+ binstr == "" and return
1770
+ i = 0
1771
+ while i < binstr.length
1772
+ id = binstr[i,2]
1773
+ len = binstr[i+2,2].to_s.unpack("v")[0]
1774
+ if id && ID_MAP.member?(id)
1775
+ field_name = ID_MAP[id].name
1776
+ if self.member?(field_name)
1777
+ self[field_name].mergea(binstr[i, len+4])
1778
+ else
1779
+ field_obj = ID_MAP[id].new(binstr[i, len+4])
1780
+ self[field_name] = field_obj
1781
+ end
1782
+ elsif id
1783
+ unless self["Unknown"]
1784
+ s = ""
1785
+ class << s
1786
+ alias_method :to_c_dir_bin, :to_s
1787
+ alias_method :to_local_bin, :to_s
1788
+ end
1789
+ self["Unknown"] = s
1790
+ end
1791
+ if ! len || len+4 > binstr[i..-1].length
1792
+ self["Unknown"] << binstr[i..-1]
1793
+ break;
1794
+ end
1795
+ self["Unknown"] << binstr[i, len+4]
1796
+ end
1797
+ i += len+4
1798
+ end
1799
+ end
1800
+
1801
+ def create(name)
1802
+ field_class = nil
1803
+ ID_MAP.each { |id, klass|
1804
+ if klass.name == name
1805
+ field_class = klass
1806
+ break
1807
+ end
1808
+ }
1809
+ if ! field_class
1810
+ raise ZipError, "Unknown extra field '#{name}'"
1811
+ end
1812
+ self[name] = field_class.new()
1813
+ end
1814
+
1815
+ def to_local_bin
1816
+ s = ""
1817
+ each { |k, v|
1818
+ s << v.to_local_bin
1819
+ }
1820
+ s
1821
+ end
1822
+ alias :to_s :to_local_bin
1823
+
1824
+ def to_c_dir_bin
1825
+ s = ""
1826
+ each { |k, v|
1827
+ s << v.to_c_dir_bin
1828
+ }
1829
+ s
1830
+ end
1831
+
1832
+ def c_dir_length
1833
+ to_c_dir_bin.length
1834
+ end
1835
+ def local_length
1836
+ to_local_bin.length
1837
+ end
1838
+ alias :c_dir_size :c_dir_length
1839
+ alias :local_size :local_length
1840
+ alias :length :local_length
1841
+ alias :size :local_length
1842
+ end # end ZipExtraField
1843
+
1844
+ end # Zip namespace module
1845
+
1846
+
1847
+
1848
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
1849
+ # rubyzip is free software; you can redistribute it and/or
1850
+ # modify it under the terms of the ruby license.