amp 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.