rwdlanguage 0.01

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