rwdlanguage 0.01

Sign up to get free protection for your applications and to get access to all the features.
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.