shoes 3.0.1

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 (275) hide show
  1. data/.gitignore +12 -0
  2. data/CHANGELOG +29 -0
  3. data/COPYING +30 -0
  4. data/README.md +76 -0
  5. data/README.old +117 -0
  6. data/Rakefile +130 -0
  7. data/Rakefile.bk +651 -0
  8. data/app.yaml +7 -0
  9. data/bin/main.skel +104 -0
  10. data/bugs/issue-012e9468ddc7b0cb7e1503413a8a11c4f8707b67.yaml +21 -0
  11. data/bugs/issue-0711ff8b67baa63586f11d0096fee9dd4436bf58.yaml +23 -0
  12. data/bugs/issue-07f87998d93eb401e22395c11085676389b935c4.yaml +22 -0
  13. data/bugs/issue-0bff2d80008803cbc7efbcdacdc60ef2163664d2.yaml +22 -0
  14. data/bugs/issue-0c66f223d19efbca7b692f3b91961f407ad7abec.yaml +24 -0
  15. data/bugs/issue-183bc3c7a8f575aff2b24e0bf22aa0dfcc8e4fc2.yaml +29 -0
  16. data/bugs/issue-1bad1e60a04cb3adb6a8f3ded128af2e16b56ffe.yaml +18 -0
  17. data/bugs/issue-1f1d43f76bf4de79d7f94adbae6dd506a9d81633.yaml +41 -0
  18. data/bugs/issue-320924117cadb045cc91f2a6fca922b4d81d4bb5.yaml +20 -0
  19. data/bugs/issue-33e5c8355fbf65252ea9e939714651df8bd8cb3b.yaml +25 -0
  20. data/bugs/issue-36f7c8262a72bcd5e28cfa4ed29328b8237ea79b.yaml +20 -0
  21. data/bugs/issue-39bf04ee50c3bef67b89b8e9beb28657805587ce.yaml +18 -0
  22. data/bugs/issue-3e28ba7754f73d02ef416eef989af707a8c00f01.yaml +23 -0
  23. data/bugs/issue-3f638cc03f41f9c3fdf217355321f1563f8fee45.yaml +22 -0
  24. data/bugs/issue-41e48e14f1ef4f6deb69be68165d7dd3ffd99156.yaml +22 -0
  25. data/bugs/issue-4c9408166a2aa5dd333bbb05520deebf4ded08e0.yaml +22 -0
  26. data/bugs/issue-55c687e108ecd5aeb8e0d0aeb4294984f84acd3e.yaml +18 -0
  27. data/bugs/issue-5811d34692cc292717a58ec1df1f8948cf30d826.yaml +28 -0
  28. data/bugs/issue-5a7c1f59a0526f9cea7fb366867cf1e6ed8ef69d.yaml +22 -0
  29. data/bugs/issue-688a2c2566bef6a03efa57a4ae1f7cfa8fc74444.yaml +21 -0
  30. data/bugs/issue-78b16c7988ade2ef96e716fa8cb9a004bd4ced65.yaml +33 -0
  31. data/bugs/issue-804ee49f9800154eddf0650690c7a9bb5626253f.yaml +27 -0
  32. data/bugs/issue-807dd1c610b2e941a5d454e26b2eac27e89a0ab4.yaml +22 -0
  33. data/bugs/issue-822ee33ec11ebd25f90ea86e8438de7891e63aee.yaml +18 -0
  34. data/bugs/issue-82ff7152cebb8a1cb065b864cb4fe22de2328146.yaml +29 -0
  35. data/bugs/issue-856afbdcdd4970ad54b8ce4a6c017fbaab45f49d.yaml +31 -0
  36. data/bugs/issue-8d49e32fb21522651c67490adb4d80076dbb14e1.yaml +24 -0
  37. data/bugs/issue-91583359653a9d530c1e32be72d874d847a306e7.yaml +21 -0
  38. data/bugs/issue-af41a5229613172764e7d3c98431172db337053d.yaml +20 -0
  39. data/bugs/issue-bd9856b3788dd429c998a5af1b2aae8221cb80b7.yaml +28 -0
  40. data/bugs/issue-be22aef5c564fb7b5e9938343136a9cd6a2edb7b.yaml +22 -0
  41. data/bugs/issue-bf8697523c77c326e7e35fc82cf3dd739ae92310.yaml +27 -0
  42. data/bugs/issue-cdd4b4795f34d6ff46e56ece10f8216a4a5456b2.yaml +25 -0
  43. data/bugs/issue-d9bfb5581d745ef9deb3b9b2e08ef74d1cd43082.yaml +20 -0
  44. data/bugs/issue-e0ce2f687cc096f35715d28b0af88589f4ab6cc6.yaml +18 -0
  45. data/bugs/issue-e4b0aca4a10a574b333aeaecaaf221dbc5ed22c6.yaml +38 -0
  46. data/bugs/issue-f263746594b95ba778455730478ee8df60ee639d.yaml +22 -0
  47. data/bugs/project.yaml +72 -0
  48. data/builddeps.sh +102 -0
  49. data/fonts/Coolvetica.ttf +0 -0
  50. data/fonts/Lacuna.ttf +0 -0
  51. data/gemlib/shoes.rb +1 -0
  52. data/lib/shoes.rb +548 -0
  53. data/lib/shoes/cache.rb +54 -0
  54. data/lib/shoes/chipmunk.rb +35 -0
  55. data/lib/shoes/data.rb +39 -0
  56. data/lib/shoes/help.rb +468 -0
  57. data/lib/shoes/image.rb +25 -0
  58. data/lib/shoes/inspect.rb +128 -0
  59. data/lib/shoes/log.rb +48 -0
  60. data/lib/shoes/minitar.rb +986 -0
  61. data/lib/shoes/override.rb +38 -0
  62. data/lib/shoes/pack.rb +543 -0
  63. data/lib/shoes/search.rb +46 -0
  64. data/lib/shoes/setup.rb +329 -0
  65. data/lib/shoes/shy.rb +131 -0
  66. data/lib/shoes/shybuilder.rb +44 -0
  67. data/lib/shoes/version.rb +3 -0
  68. data/make/darwin/deps.vlc +12 -0
  69. data/make/darwin/dylibs.shoes +22 -0
  70. data/make/darwin/dylibs.video +11 -0
  71. data/make/darwin/env.rb +81 -0
  72. data/make/darwin/tasks.rb +105 -0
  73. data/make/linux/env.rb +65 -0
  74. data/make/linux/tasks.rb +61 -0
  75. data/make/make.rb +85 -0
  76. data/make/mingw/dlls +19 -0
  77. data/make/mingw/env.rb +69 -0
  78. data/make/mingw/tasks.rb +70 -0
  79. data/make/rakefile_common.rb +8 -0
  80. data/manual-snapshots/class-book.png +0 -0
  81. data/manual-snapshots/expert-definr.png +0 -0
  82. data/manual-snapshots/expert-funnies.png +0 -0
  83. data/manual-snapshots/expert-irb.png +0 -0
  84. data/manual-snapshots/expert-minesweeper.png +0 -0
  85. data/manual-snapshots/expert-othello.png +0 -0
  86. data/manual-snapshots/expert-pong.png +0 -0
  87. data/manual-snapshots/expert-tankspank.png +0 -0
  88. data/manual-snapshots/good-arc.png +0 -0
  89. data/manual-snapshots/good-clock.png +0 -0
  90. data/manual-snapshots/good-follow.png +0 -0
  91. data/manual-snapshots/good-reminder.png +0 -0
  92. data/manual-snapshots/good-vjot.png +0 -0
  93. data/manual-snapshots/simple-accordion.png +0 -0
  94. data/manual-snapshots/simple-anim-shapes.png +0 -0
  95. data/manual-snapshots/simple-anim-text.png +0 -0
  96. data/manual-snapshots/simple-arc.png +0 -0
  97. data/manual-snapshots/simple-bounce.png +0 -0
  98. data/manual-snapshots/simple-calc.png +0 -0
  99. data/manual-snapshots/simple-chipmunk.png +0 -0
  100. data/manual-snapshots/simple-control-sizes.png +0 -0
  101. data/manual-snapshots/simple-curve.png +0 -0
  102. data/manual-snapshots/simple-dialogs.png +0 -0
  103. data/manual-snapshots/simple-downloader.png +0 -0
  104. data/manual-snapshots/simple-draw.png +0 -0
  105. data/manual-snapshots/simple-editor.png +0 -0
  106. data/manual-snapshots/simple-form.png +0 -0
  107. data/manual-snapshots/simple-mask.png +0 -0
  108. data/manual-snapshots/simple-menu.png +0 -0
  109. data/manual-snapshots/simple-menu1.png +0 -0
  110. data/manual-snapshots/simple-rubygems.png +0 -0
  111. data/manual-snapshots/simple-slide.png +0 -0
  112. data/manual-snapshots/simple-sphere.png +0 -0
  113. data/manual-snapshots/simple-sqlite3.png +0 -0
  114. data/manual-snapshots/simple-timer.png +0 -0
  115. data/manual-snapshots/simple-video.png +0 -0
  116. data/platform/mac/Info.plist +55 -0
  117. data/platform/mac/build-deps.sh +658 -0
  118. data/platform/mac/command-manual.rb +1 -0
  119. data/platform/mac/deps-osx.patch +159 -0
  120. data/platform/mac/dmg_ds_store +0 -0
  121. data/platform/mac/pangorc +2 -0
  122. data/platform/mac/pkg-dmg +1447 -0
  123. data/platform/mac/shoes +31 -0
  124. data/platform/mac/shoes-launch +7 -0
  125. data/platform/mac/stub.m +178 -0
  126. data/platform/mac/version.plist +14 -0
  127. data/platform/msw/base.nsi +644 -0
  128. data/platform/msw/installer-1.bmp +0 -0
  129. data/platform/msw/installer-2.bmp +0 -0
  130. data/platform/msw/shoes.exe.manifest +17 -0
  131. data/platform/msw/shoes.ico +0 -0
  132. data/platform/msw/stub-inject.c +59 -0
  133. data/platform/msw/stub.c +271 -0
  134. data/platform/msw/stub32.h +14 -0
  135. data/platform/msw/stub32.rc +16 -0
  136. data/platform/nix/INSTALL +56 -0
  137. data/platform/nix/Makefile +144 -0
  138. data/platform/nix/shoes.launch +20 -0
  139. data/platform/skel.rb +27 -0
  140. data/rakefile_darwin.rb +7 -0
  141. data/rakefile_linux.rb +3 -0
  142. data/rakefile_mingw.rb +7 -0
  143. data/samples/class-book.rb +43 -0
  144. data/samples/class-book.yaml +387 -0
  145. data/samples/expert-definr.rb +23 -0
  146. data/samples/expert-funnies.rb +51 -0
  147. data/samples/expert-irb.rb +112 -0
  148. data/samples/expert-minesweeper.rb +267 -0
  149. data/samples/expert-othello.rb +319 -0
  150. data/samples/expert-pong.rb +62 -0
  151. data/samples/expert-tankspank.rb +385 -0
  152. data/samples/good-arc.rb +37 -0
  153. data/samples/good-clock.rb +51 -0
  154. data/samples/good-follow.rb +26 -0
  155. data/samples/good-reminder.rb +174 -0
  156. data/samples/good-vjot.rb +56 -0
  157. data/samples/simple-accordion.rb +75 -0
  158. data/samples/simple-anim-shapes.rb +17 -0
  159. data/samples/simple-anim-text.rb +13 -0
  160. data/samples/simple-arc.rb +23 -0
  161. data/samples/simple-bounce.rb +24 -0
  162. data/samples/simple-calc.rb +70 -0
  163. data/samples/simple-chipmunk.rb +26 -0
  164. data/samples/simple-control-sizes.rb +24 -0
  165. data/samples/simple-curve.rb +26 -0
  166. data/samples/simple-dialogs.rb +29 -0
  167. data/samples/simple-downloader.rb +27 -0
  168. data/samples/simple-draw.rb +13 -0
  169. data/samples/simple-editor.rb +28 -0
  170. data/samples/simple-form.rb +28 -0
  171. data/samples/simple-form.shy +0 -0
  172. data/samples/simple-mask.rb +21 -0
  173. data/samples/simple-menu.rb +31 -0
  174. data/samples/simple-menu1.rb +35 -0
  175. data/samples/simple-rubygems.rb +29 -0
  176. data/samples/simple-slide.rb +45 -0
  177. data/samples/simple-sphere.rb +28 -0
  178. data/samples/simple-sqlite3.rb +13 -0
  179. data/samples/simple-timer.rb +13 -0
  180. data/samples/simple-video.rb +13 -0
  181. data/shoes.gemspec +21 -0
  182. data/shoes/app.c +591 -0
  183. data/shoes/app.h +110 -0
  184. data/shoes/appwin32.h +13 -0
  185. data/shoes/appwin32.rc +28 -0
  186. data/shoes/canvas.c +2202 -0
  187. data/shoes/canvas.h +682 -0
  188. data/shoes/code.h +14 -0
  189. data/shoes/config.h +232 -0
  190. data/shoes/effects.c +243 -0
  191. data/shoes/effects.h +7 -0
  192. data/shoes/http.h +44 -0
  193. data/shoes/http/common.h +86 -0
  194. data/shoes/http/curl.c +259 -0
  195. data/shoes/http/nsurl.m +274 -0
  196. data/shoes/http/windownload.c +114 -0
  197. data/shoes/http/winhttp.c +216 -0
  198. data/shoes/http/winhttp.h +19 -0
  199. data/shoes/image.c +1020 -0
  200. data/shoes/internal.c +46 -0
  201. data/shoes/internal.h +63 -0
  202. data/shoes/native.h +110 -0
  203. data/shoes/native/cocoa.h +105 -0
  204. data/shoes/native/cocoa.m +1557 -0
  205. data/shoes/native/gtk.c +1257 -0
  206. data/shoes/native/windows.c +2392 -0
  207. data/shoes/ruby.c +5221 -0
  208. data/shoes/ruby.h +299 -0
  209. data/shoes/world.c +243 -0
  210. data/shoes/world.h +63 -0
  211. data/static/Shoes.icns +0 -0
  212. data/static/avatar.png +0 -0
  213. data/static/code_highlighter.js +188 -0
  214. data/static/code_highlighter_ruby.js +26 -0
  215. data/static/icon-debug.png +0 -0
  216. data/static/icon-error.png +0 -0
  217. data/static/icon-info.png +0 -0
  218. data/static/icon-warn.png +0 -0
  219. data/static/listbox_button1.png +0 -0
  220. data/static/listbox_button2.png +0 -0
  221. data/static/man-app.png +0 -0
  222. data/static/man-builds.png +0 -0
  223. data/static/man-builds1.png +0 -0
  224. data/static/man-editor-notepad.png +0 -0
  225. data/static/man-editor-osx.png +0 -0
  226. data/static/man-ele-background.png +0 -0
  227. data/static/man-ele-border.png +0 -0
  228. data/static/man-ele-button.png +0 -0
  229. data/static/man-ele-check.png +0 -0
  230. data/static/man-ele-editbox.png +0 -0
  231. data/static/man-ele-editline.png +0 -0
  232. data/static/man-ele-image.png +0 -0
  233. data/static/man-ele-listbox.png +0 -0
  234. data/static/man-ele-progress.png +0 -0
  235. data/static/man-ele-radio.png +0 -0
  236. data/static/man-ele-shape.png +0 -0
  237. data/static/man-ele-textblock.png +0 -0
  238. data/static/man-ele-video.png +0 -0
  239. data/static/man-intro-dmg.png +0 -0
  240. data/static/man-intro-exe.png +0 -0
  241. data/static/man-look-tiger.png +0 -0
  242. data/static/man-look-ubuntu.png +0 -0
  243. data/static/man-look-vista.png +0 -0
  244. data/static/man-run-osx.png +0 -0
  245. data/static/man-run-vista.png +0 -0
  246. data/static/man-run-xp.png +0 -0
  247. data/static/man-shot1.png +0 -0
  248. data/static/manual-en.txt +3531 -0
  249. data/static/manual-ja.txt +2825 -0
  250. data/static/manual.css +167 -0
  251. data/static/menu-corner1.png +0 -0
  252. data/static/menu-corner2.png +0 -0
  253. data/static/menu-gray.png +0 -0
  254. data/static/menu-left.png +0 -0
  255. data/static/menu-right.png +0 -0
  256. data/static/menu-top.png +0 -0
  257. data/static/shoes-dmg.jpg +0 -0
  258. data/static/shoes-icon-blue.png +0 -0
  259. data/static/shoes-icon.png +0 -0
  260. data/static/shoes-manual-apps.gif +0 -0
  261. data/static/shoes_main_window.png +0 -0
  262. data/static/stripe.png +0 -0
  263. data/static/stubs/blank.exe +0 -0
  264. data/static/stubs/blank.hfz +0 -0
  265. data/static/stubs/blank.run +375 -0
  266. data/static/stubs/cocoa-install +0 -0
  267. data/static/stubs/sh-install +49 -0
  268. data/static/stubs/shoes-stub-inject.exe +0 -0
  269. data/static/stubs/shoes-stub.exe +0 -0
  270. data/static/tutor-back.png +0 -0
  271. data/test/shoes_test.rb +8 -0
  272. data/test/test_helper.rb +25 -0
  273. data/use-deps +12 -0
  274. data/use-tmp-dep +8 -0
  275. metadata +509 -0
@@ -0,0 +1,25 @@
1
+ require 'digest/sha1'
2
+
3
+ class Shoes
4
+ def self.image_temp_path uri, uext
5
+ File.join(Dir::tmpdir, "#{uri.host}-#{Time.now.usec}" + uext)
6
+ end
7
+ def self.image_cache_path hash, ext
8
+ dir = File.join(CACHE_DIR, hash[0,2])
9
+ Dir.mkdir(dir) unless File.exists?(dir)
10
+ File.join(dir, hash[2..-1]) + ext.downcase
11
+ end
12
+ def snapshot(options = {}, &block)
13
+ options[:format] ||= :svg
14
+
15
+ options[:filename] ||= ( tf_path = ( require 'tempfile'
16
+ tf = Tempfile.new(File.basename(__FILE__)).path ))
17
+
18
+ _snapshot options do
19
+ block.call
20
+ end
21
+ return File.read(options[:filename])
22
+ ensure
23
+ File.unlink(tf_path) if tf_path
24
+ end
25
+ end
@@ -0,0 +1,128 @@
1
+ module Kernel
2
+ def inspect(hits = {})
3
+ return "(#{self.class} ...)" if hits[self]
4
+ hits[self] = true
5
+ if instance_variables.empty?
6
+ "(#{self.class})"
7
+ else
8
+ "(#{self.class} " +
9
+ instance_variables.map do |x|
10
+ v = instance_variable_get(x)
11
+ "#{x}=" + (v.method(:inspect).arity == 0 ? v.inspect : v.inspect(hits))
12
+ end.join(' ') +
13
+ ")"
14
+ end
15
+ end
16
+ #def to_html
17
+ # obj = self
18
+ # Web.Bit {
19
+ # h1 "A #{obj.class}"
20
+ # }
21
+ #end
22
+ def to_s; inspect end
23
+ end
24
+
25
+ class Array
26
+ def inspect(hits = {})
27
+ return "[...]" if hits[self]
28
+ hits[self] = true
29
+ "[" + map { |x| x.method(:inspect).arity == 0 ? x.inspect : x.inspect(hits) }.join(', ') + "]"
30
+ end
31
+ def to_html
32
+ ary = self
33
+ Web.Bit {
34
+ h5 "A List of Things"
35
+ h1 "An Array"
36
+ unless ary.empty?
37
+ ol {
38
+ ary.map { |x| li { self << HTML(x) } }
39
+ }
40
+ end
41
+ }
42
+ end
43
+ def / len
44
+ a = []
45
+ each_with_index do |x, i|
46
+ a << [] if i % len == 0
47
+ a.last << x
48
+ end
49
+ a
50
+ end
51
+ end
52
+
53
+ class Hash
54
+ def inspect(hits = {})
55
+ return "{...}" if hits[self]
56
+ hits[self] = true
57
+ mappings = map do |k,v|
58
+ key = k.method(:inspect).arity == 0 ? k.inspect : k.inspect(hits)
59
+ val = v.method(:inspect).arity == 0 ? v.inspect : v.inspect(hits)
60
+ "#{key} => #{val}"
61
+ end
62
+ "{ #{mappings.join(', ')} }"
63
+ end
64
+ def to_html
65
+ h = self
66
+ Web.Bit {
67
+ h5 "Pairs of Things"
68
+ h1 "A Hash"
69
+ unless h.empty?
70
+ ul {
71
+ h.each { |k, v| li { self << "<div class='hashkey'>#{HTML(k)}</div><div class='hashvalue'>#{HTML(v)}</div>" } }
72
+ }
73
+ end
74
+ }
75
+ end
76
+ end
77
+
78
+ class File
79
+ def inspect(hits = nil)
80
+ "(#{self.class} #{path})"
81
+ end
82
+ end
83
+
84
+ class Proc
85
+ def inspect(hits = nil)
86
+ v = "a"
87
+ pvars = []
88
+ (arity < 0 ? -(arity+1) : arity).times do |i|
89
+ pvars << v
90
+ v = v.succ
91
+ end
92
+ pvars << "*#{v}" if arity < 0
93
+ "(Proc |#{pvars.join(',')}|)"
94
+ end
95
+ end
96
+
97
+ class Class
98
+ def make_inspect m = :inspect
99
+ alias_method :the_original_inspect, m
100
+ class_eval %{
101
+ def inspect(hits = nil)
102
+ the_original_inspect
103
+ end
104
+ }
105
+ #def to_html(hits = nil)
106
+ # #{m} + " <div class='classname'>" + self.class.name + "</div>"
107
+ #end
108
+ end
109
+ end
110
+
111
+ class Module; make_inspect :name end
112
+ class Regexp; make_inspect end
113
+ class String; make_inspect end
114
+ class Symbol; make_inspect end
115
+ class Time; make_inspect end
116
+ class Numeric; make_inspect :to_s end
117
+ class Bignum; make_inspect :to_s end
118
+ class Fixnum; make_inspect :to_s end
119
+ class Float; make_inspect :to_s end
120
+ class TrueClass; make_inspect :to_s end
121
+ class FalseClass; make_inspect :to_s end
122
+ class NilClass; make_inspect end
123
+
124
+ class Shoes::Types::App
125
+ def inspect(hits = nil)
126
+ "(#{self.class} #{name.dump})"
127
+ end
128
+ end
@@ -0,0 +1,48 @@
1
+ module Shoes::LogWindow
2
+ def setup
3
+ stack do
4
+ flow do
5
+ background black
6
+ stack :width => -100 do
7
+ tagline "Shoes Console", :stroke => white
8
+ end
9
+ button "Clear", :margin => 6, :width => 80, :height => 40 do
10
+ Shoes.log.clear
11
+ end
12
+ end
13
+ @log, @hash = stack, nil
14
+ update
15
+ every(0.2) do
16
+ update
17
+ end
18
+ end
19
+ end
20
+ def update
21
+ if @hash != Shoes.log.hash
22
+ @hash = Shoes.log.hash
23
+ @log.clear do
24
+ i = 0
25
+ Shoes.log.each do |typ, msg, at, mid, rbf, rbl|
26
+ stack do
27
+ background "#f1f5e1" if i % 2 == 0
28
+ inscription strong(typ.to_s.capitalize, :stroke => "#05C"), " in ",
29
+ span(rbf, " line ", rbl, :stroke => "#335"), " | ",
30
+ span(at, :stroke => "#777"),
31
+ :stroke => "#059", :margin => 4, :margin_bottom => 0
32
+ flow do
33
+ stack :margin => 6, :width => 20 do
34
+ image "#{DIR}/static/icon-#{typ}.png"
35
+ end
36
+ stack :margin => 4, :width => -20 do
37
+ s = msg.to_s.force_encoding "UTF-8"
38
+ s << "\n#{msg.backtrace.join("\n")}" if msg.kind_of?(Exception)
39
+ para s, :margin => 4, :margin_top => 0
40
+ end
41
+ end
42
+ end
43
+ i += 1
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,986 @@
1
+ #!/usr/bin/env ruby
2
+ #--
3
+ # Archive::Tar::Minitar 0.5.1
4
+ # Copyright � 2004 Mauricio Julio Fern�ndez Pradier and Austin Ziegler
5
+ #
6
+ # This program is based on and incorporates parts of RPA::Package from
7
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been
8
+ # adapted to be more generic by Austin.
9
+ #
10
+ # It is licensed under the GNU General Public Licence or Ruby's licence.
11
+ #
12
+ # $Id: minitar.rb,v 1.3 2004/09/22 17:47:43 austin Exp $
13
+ #++
14
+
15
+ module Archive; end
16
+ module Archive::Tar; end
17
+
18
+ # = Archive::Tar::PosixHeader
19
+ # Implements the POSIX tar header as a Ruby class. The structure of
20
+ # the POSIX tar header is:
21
+ #
22
+ # struct tarfile_entry_posix
23
+ # { // pack/unpack
24
+ # char name[100]; // ASCII (+ Z unless filled) a100/Z100
25
+ # char mode[8]; // 0 padded, octal, null a8 /A8
26
+ # char uid[8]; // ditto a8 /A8
27
+ # char gid[8]; // ditto a8 /A8
28
+ # char size[12]; // 0 padded, octal, null a12 /A12
29
+ # char mtime[12]; // 0 padded, octal, null a12 /A12
30
+ # char checksum[8]; // 0 padded, octal, null, space a8 /A8
31
+ # char typeflag[1]; // see below a /a
32
+ # char linkname[100]; // ASCII + (Z unless filled) a100/Z100
33
+ # char magic[6]; // "ustar\0" a6 /A6
34
+ # char version[2]; // "00" a2 /A2
35
+ # char uname[32]; // ASCIIZ a32 /Z32
36
+ # char gname[32]; // ASCIIZ a32 /Z32
37
+ # char devmajor[8]; // 0 padded, octal, null a8 /A8
38
+ # char devminor[8]; // 0 padded, octal, null a8 /A8
39
+ # char prefix[155]; // ASCII (+ Z unless filled) a155/Z155
40
+ # };
41
+ #
42
+ # The +typeflag+ may be one of the following known values:
43
+ #
44
+ # <tt>"0"</tt>:: Regular file. NULL should be treated as a synonym, for
45
+ # compatibility purposes.
46
+ # <tt>"1"</tt>:: Hard link.
47
+ # <tt>"2"</tt>:: Symbolic link.
48
+ # <tt>"3"</tt>:: Character device node.
49
+ # <tt>"4"</tt>:: Block device node.
50
+ # <tt>"5"</tt>:: Directory.
51
+ # <tt>"6"</tt>:: FIFO node.
52
+ # <tt>"7"</tt>:: Reserved.
53
+ #
54
+ # POSIX indicates that "A POSIX-compliant implementation must treat any
55
+ # unrecognized typeflag value as a regular file."
56
+ class Archive::Tar::PosixHeader
57
+ FIELDS = %w(name mode uid gid size mtime checksum typeflag linkname) +
58
+ %w(magic version uname gname devmajor devminor prefix)
59
+
60
+ FIELDS.each { |field| attr_reader field.intern }
61
+
62
+ HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155"
63
+ HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155"
64
+
65
+ # Creates a new PosixHeader from a data stream.
66
+ def self.new_from_stream(stream)
67
+ data = stream.read(512)
68
+ fields = data.unpack(HEADER_UNPACK_FORMAT)
69
+ name = fields.shift
70
+ mode = fields.shift.oct
71
+ uid = fields.shift.oct
72
+ gid = fields.shift.oct
73
+ size = fields.shift.oct
74
+ mtime = fields.shift.oct
75
+ checksum = fields.shift.oct
76
+ typeflag = fields.shift
77
+ linkname = fields.shift
78
+ magic = fields.shift
79
+ version = fields.shift.oct
80
+ uname = fields.shift
81
+ gname = fields.shift
82
+ devmajor = fields.shift.oct
83
+ devminor = fields.shift.oct
84
+ prefix = fields.shift
85
+
86
+ empty = (data == "\0" * 512)
87
+
88
+ new(:name => name, :mode => mode, :uid => uid, :gid => gid,
89
+ :size => size, :mtime => mtime, :checksum => checksum,
90
+ :typeflag => typeflag, :magic => magic, :version => version,
91
+ :uname => uname, :gname => gname, :devmajor => devmajor,
92
+ :devminor => devminor, :prefix => prefix, :empty => empty)
93
+ end
94
+
95
+ # Creates a new PosixHeader. A PosixHeader cannot be created unless the
96
+ # #name, #size, #prefix, and #mode are provided.
97
+ def initialize(vals)
98
+ unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
99
+ raise ArgumentError
100
+ end
101
+
102
+ vals[:mtime] ||= 0
103
+ vals[:checksum] ||= ""
104
+ vals[:typeflag] ||= "0"
105
+ vals[:magic] ||= "ustar"
106
+ vals[:version] ||= "00"
107
+
108
+ FIELDS.each do |field|
109
+ instance_variable_set("@#{field}", vals[field.intern])
110
+ end
111
+ @empty = vals[:empty]
112
+ end
113
+
114
+ def empty?
115
+ @empty
116
+ end
117
+
118
+ def to_s
119
+ update_checksum
120
+ header(@checksum)
121
+ end
122
+
123
+ # Update the checksum field.
124
+ def update_checksum
125
+ hh = header(" " * 8)
126
+ @checksum = oct(calculate_checksum(hh), 6)
127
+ end
128
+
129
+ private
130
+ def oct(num, len)
131
+ if num.nil?
132
+ "\0" * (len + 1)
133
+ else
134
+ "%0#{len}o" % num
135
+ end
136
+ end
137
+
138
+ def calculate_checksum(hdr)
139
+ hdr.unpack("C*").inject { |aa, bb| aa + bb }
140
+ end
141
+
142
+ def header(chksum)
143
+ arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
144
+ oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
145
+ uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
146
+ str = arr.pack(HEADER_PACK_FORMAT)
147
+ str + "\0" * ((512 - str.size) % 512)
148
+ end
149
+ end
150
+
151
+ require 'fileutils'
152
+ require 'find'
153
+
154
+ # = Archive::Tar::Minitar 0.5.1
155
+ # Archive::Tar::Minitar is a pure-Ruby library and command-line
156
+ # utility that provides the ability to deal with POSIX tar(1) archive
157
+ # files. The implementation is based heavily on Mauricio Fern�ndez's
158
+ # implementation in rpa-base, but has been reorganised to promote
159
+ # reuse in other projects.
160
+ #
161
+ # This tar class performs a subset of all tar (POSIX tape archive)
162
+ # operations. We can only deal with typeflags 0, 1, 2, and 5 (see
163
+ # Archive::Tar::PosixHeader). All other typeflags will be treated as
164
+ # normal files.
165
+ #
166
+ # NOTE::: support for typeflags 1 and 2 is not yet implemented in this
167
+ # version.
168
+ #
169
+ # This release is version 0.5.1. The library can only handle files and
170
+ # directories at this point. A future version will be expanded to
171
+ # handle symbolic links and hard links in a portable manner. The
172
+ # command line utility, minitar, can only create archives, extract
173
+ # from archives, and list archive contents.
174
+ #
175
+ # == Synopsis
176
+ # Using this library is easy. The simplest case is:
177
+ #
178
+ # require 'zlib'
179
+ # require 'archive/tar/minitar'
180
+ # include Archive::Tar
181
+ #
182
+ # # Packs everything that matches Find.find('tests')
183
+ # File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) }
184
+ # # Unpacks 'test.tar' to 'x', creating 'x' if necessary.
185
+ # Minitar.unpack('test.tar', 'x')
186
+ #
187
+ # A gzipped tar can be written with:
188
+ #
189
+ # tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb'))
190
+ # # Warning: tgz will be closed!
191
+ # Minitar.pack('tests', tgz)
192
+ #
193
+ # tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb'))
194
+ # # Warning: tgz will be closed!
195
+ # Minitar.unpack(tgz, 'x')
196
+ #
197
+ # As the case above shows, one need not write to a file. However, it
198
+ # will sometimes require that one dive a little deeper into the API,
199
+ # as in the case of StringIO objects. Note that I'm not providing a
200
+ # block with Minitar::Output, as Minitar::Output#close automatically
201
+ # closes both the Output object and the wrapped data stream object.
202
+ #
203
+ # begin
204
+ # sgz = Zlib::GzipWriter.new(StringIO.new(""))
205
+ # tar = Output.new(sgz)
206
+ # Find.find('tests') do |entry|
207
+ # Minitar.pack_file(entry, tar)
208
+ # end
209
+ # ensure
210
+ # # Closes both tar and sgz.
211
+ # tar.close
212
+ # end
213
+ #
214
+ # == Copyright
215
+ # Copyright 2004 Mauricio Julio Fern�ndez Pradier and Austin Ziegler
216
+ #
217
+ # This program is based on and incorporates parts of RPA::Package from
218
+ # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and
219
+ # has been adapted to be more generic by Austin.
220
+ #
221
+ # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru
222
+ # Takabayashi <satoru@namazu.org>, copyright � 2001 - 2004.
223
+ #
224
+ # This program is free software. It may be redistributed and/or
225
+ # modified under the terms of the GPL version 2 (or later) or Ruby's
226
+ # licence.
227
+ module Archive::Tar::Minitar
228
+ VERSION = "0.5.1"
229
+
230
+ # The exception raised when a wrapped data stream class is expected to
231
+ # respond to #rewind or #pos but does not.
232
+ class NonSeekableStream < StandardError; end
233
+ # The exception raised when a block is required for proper operation of
234
+ # the method.
235
+ class BlockRequired < ArgumentError; end
236
+ # The exception raised when operations are performed on a stream that has
237
+ # previously been closed.
238
+ class ClosedStream < StandardError; end
239
+ # The exception raised when a filename exceeds 256 bytes in length,
240
+ # the maximum supported by the standard Tar format.
241
+ class FileNameTooLong < StandardError; end
242
+ # The exception raised when a data stream ends before the amount of data
243
+ # expected in the archive's PosixHeader.
244
+ class UnexpectedEOF < StandardError; end
245
+
246
+ # The class that writes a tar format archive to a data stream.
247
+ class Writer
248
+ # A stream wrapper that can only be written to. Any attempt to read
249
+ # from this restricted stream will result in a NameError being thrown.
250
+ class RestrictedStream
251
+ def initialize(anIO)
252
+ @io = anIO
253
+ end
254
+
255
+ def write(data)
256
+ @io.write(data)
257
+ end
258
+ end
259
+
260
+ # A RestrictedStream that also has a size limit.
261
+ class BoundedStream < Archive::Tar::Minitar::Writer::RestrictedStream
262
+ # The exception raised when the user attempts to write more data to
263
+ # a BoundedStream than has been allocated.
264
+ class FileOverflow < RuntimeError; end
265
+
266
+ # The maximum number of bytes that may be written to this data
267
+ # stream.
268
+ attr_reader :limit
269
+ # The current total number of bytes written to this data stream.
270
+ attr_reader :written
271
+
272
+ def initialize(io, limit)
273
+ @io = io
274
+ @limit = limit
275
+ @written = 0
276
+ end
277
+
278
+ def write(data)
279
+ raise FileOverflow if (data.size + @written) > @limit
280
+ @io.write(data)
281
+ @written += data.size
282
+ data.size
283
+ end
284
+ end
285
+
286
+ # With no associated block, +Writer::open+ is a synonym for
287
+ # +Writer::new+. If the optional code block is given, it will be
288
+ # passed the new _writer_ as an argument and the Writer object will
289
+ # automatically be closed when the block terminates. In this instance,
290
+ # +Writer::open+ returns the value of the block.
291
+ def self.open(anIO)
292
+ writer = Writer.new(anIO)
293
+
294
+ return writer unless block_given?
295
+
296
+ begin
297
+ res = yield writer
298
+ ensure
299
+ writer.close
300
+ end
301
+
302
+ res
303
+ end
304
+
305
+ # Creates and returns a new Writer object.
306
+ def initialize(anIO)
307
+ @io = anIO
308
+ @closed = false
309
+ end
310
+
311
+ # Adds a file to the archive as +name+. +opts+ must contain the
312
+ # following values:
313
+ #
314
+ # <tt>:mode</tt>:: The Unix file permissions mode value.
315
+ # <tt>:size</tt>:: The size, in bytes.
316
+ #
317
+ # +opts+ may contain the following values:
318
+ #
319
+ # <tt>:uid</tt>: The Unix file owner user ID number.
320
+ # <tt>:gid</tt>: The Unix file owner group ID number.
321
+ # <tt>:mtime</tt>:: The *integer* modification time value.
322
+ #
323
+ # It will not be possible to add more than <tt>opts[:size]</tt> bytes
324
+ # to the file.
325
+ def add_file_simple(name, opts = {}) # :yields BoundedStream:
326
+ raise Archive::Tar::Minitar::BlockRequired unless block_given?
327
+ raise Archive::Tar::ClosedStream if @closed
328
+
329
+ name, prefix = split_name(name)
330
+
331
+ header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
332
+ :size => opts[:size], :gid => opts[:gid], :uid => opts[:uid],
333
+ :prefix => prefix }
334
+ header = Archive::Tar::PosixHeader.new(header).to_s
335
+ @io.write(header)
336
+
337
+ os = BoundedStream.new(@io, opts[:size])
338
+ yield os
339
+ # FIXME: what if an exception is raised in the block?
340
+
341
+ min_padding = opts[:size] - os.written
342
+ @io.write("\0" * min_padding)
343
+ remainder = (512 - (opts[:size] % 512)) % 512
344
+ @io.write("\0" * remainder)
345
+ end
346
+
347
+ # Adds a file to the archive as +name+. +opts+ must contain the
348
+ # following value:
349
+ #
350
+ # <tt>:mode</tt>:: The Unix file permissions mode value.
351
+ #
352
+ # +opts+ may contain the following values:
353
+ #
354
+ # <tt>:uid</tt>: The Unix file owner user ID number.
355
+ # <tt>:gid</tt>: The Unix file owner group ID number.
356
+ # <tt>:mtime</tt>:: The *integer* modification time value.
357
+ #
358
+ # The file's size will be determined from the amount of data written
359
+ # to the stream.
360
+ #
361
+ # For #add_file to be used, the Archive::Tar::Minitar::Writer must be
362
+ # wrapping a stream object that is seekable (e.g., it responds to
363
+ # #pos=). Otherwise, #add_file_simple must be used.
364
+ #
365
+ # +opts+ may be modified during the writing to the stream.
366
+ def add_file(name, opts = {}) # :yields RestrictedStream, +opts+:
367
+ raise Archive::Tar::Minitar::BlockRequired unless block_given?
368
+ raise Archive::Tar::Minitar::ClosedStream if @closed
369
+ raise Archive::Tar::Minitar::NonSeekableStream unless @io.respond_to?(:pos=)
370
+
371
+ name, prefix = split_name(name)
372
+ init_pos = @io.pos
373
+ @io.write("\0" * 512) # placeholder for the header
374
+
375
+ yield RestrictedStream.new(@io), opts
376
+ # FIXME: what if an exception is raised in the block?
377
+
378
+ size = @io.pos - (init_pos + 512)
379
+ remainder = (512 - (size % 512)) % 512
380
+ @io.write("\0" * remainder)
381
+
382
+ final_pos = @io.pos
383
+ @io.pos = init_pos
384
+
385
+ header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime],
386
+ :size => size, :gid => opts[:gid], :uid => opts[:uid],
387
+ :prefix => prefix }
388
+ header = Archive::Tar::PosixHeader.new(header).to_s
389
+ @io.write(header)
390
+ @io.pos = final_pos
391
+ end
392
+
393
+ # Creates a directory in the tar.
394
+ def mkdir(name, opts = {})
395
+ raise ClosedStream if @closed
396
+ name, prefix = split_name(name)
397
+ header = { :name => name, :mode => opts[:mode], :typeflag => "5",
398
+ :size => 0, :gid => opts[:gid], :uid => opts[:uid],
399
+ :mtime => opts[:mtime], :prefix => prefix }
400
+ header = Archive::Tar::PosixHeader.new(header).to_s
401
+ @io.write(header)
402
+ nil
403
+ end
404
+
405
+ # Passes the #flush method to the wrapped stream, used for buffered
406
+ # streams.
407
+ def flush
408
+ raise ClosedStream if @closed
409
+ @io.flush if @io.respond_to?(:flush)
410
+ end
411
+
412
+ # Closes the Writer.
413
+ def close
414
+ return if @closed
415
+ @io.write("\0" * 1024)
416
+ @closed = true
417
+ end
418
+
419
+ private
420
+ def split_name(name)
421
+ raise FileNameTooLong if name.size > 256
422
+ if name.size <= 100
423
+ prefix = ""
424
+ else
425
+ parts = name.split(/\//)
426
+ newname = parts.pop
427
+
428
+ nxt = ""
429
+
430
+ loop do
431
+ nxt = parts.pop
432
+ break if newname.size + 1 + nxt.size > 100
433
+ newname = "#{nxt}/#{newname}"
434
+ end
435
+
436
+ prefix = (parts + [nxt]).join("/")
437
+
438
+ name = newname
439
+
440
+ raise FileNameTooLong if name.size > 100 || prefix.size > 155
441
+ end
442
+ return name, prefix
443
+ end
444
+ end
445
+
446
+ # The class that reads a tar format archive from a data stream. The data
447
+ # stream may be sequential or random access, but certain features only work
448
+ # with random access data streams.
449
+ class Reader
450
+ # This marks the EntryStream closed for reading without closing the
451
+ # actual data stream.
452
+ module InvalidEntryStream
453
+ def read(len = nil); raise ClosedStream; end
454
+ def getc; raise ClosedStream; end
455
+ def rewind; raise ClosedStream; end
456
+ end
457
+
458
+ # EntryStreams are pseudo-streams on top of the main data stream.
459
+ class EntryStream
460
+ Archive::Tar::PosixHeader::FIELDS.each do |field|
461
+ attr_reader field.intern
462
+ end
463
+
464
+ def initialize(header, anIO)
465
+ @io = anIO
466
+ @name = header.name
467
+ @mode = header.mode
468
+ @uid = header.uid
469
+ @gid = header.gid
470
+ @size = header.size
471
+ @mtime = header.mtime
472
+ @checksum = header.checksum
473
+ @typeflag = header.typeflag
474
+ @linkname = header.linkname
475
+ @magic = header.magic
476
+ @version = header.version
477
+ @uname = header.uname
478
+ @gname = header.gname
479
+ @devmajor = header.devmajor
480
+ @devminor = header.devminor
481
+ @prefix = header.prefix
482
+ @read = 0
483
+ @orig_pos = @io.pos
484
+ end
485
+
486
+ # Reads +len+ bytes (or all remaining data) from the entry. Returns
487
+ # +nil+ if there is no more data to read.
488
+ def read(len = nil)
489
+ return nil if @read >= @size
490
+ len ||= @size - @read
491
+ max_read = [len, @size - @read].min
492
+ ret = @io.read(max_read)
493
+ @read += ret.size
494
+ ret
495
+ end
496
+
497
+ # Reads one byte from the entry. Returns +nil+ if there is no more data
498
+ # to read.
499
+ def getc
500
+ return nil if @read >= @size
501
+ ret = @io.getc
502
+ @read += 1 if ret
503
+ ret
504
+ end
505
+
506
+ # Returns +true+ if the entry represents a directory.
507
+ def directory?
508
+ @typeflag == "5"
509
+ end
510
+ alias_method :directory, :directory?
511
+
512
+ # Returns +true+ if the entry represents a plain file.
513
+ def file?
514
+ @typeflag == "0"
515
+ end
516
+ alias_method :file, :file?
517
+
518
+ # Returns +true+ if the current read pointer is at the end of the
519
+ # EntryStream data.
520
+ def eof?
521
+ @read >= @size
522
+ end
523
+
524
+ # Returns the current read pointer in the EntryStream.
525
+ def pos
526
+ @read
527
+ end
528
+
529
+ # Sets the current read pointer to the beginning of the EntryStream.
530
+ def rewind
531
+ raise NonSeekableStream unless @io.respond_to?(:pos=)
532
+ @io.pos = @orig_pos
533
+ @read = 0
534
+ end
535
+
536
+ def orig_pos
537
+ @orig_pos
538
+ end
539
+
540
+ def bytes_read
541
+ @read
542
+ end
543
+
544
+ # Returns the full and proper name of the entry.
545
+ def full_name
546
+ if @prefix != ""
547
+ File.join(@prefix, @name)
548
+ else
549
+ @name
550
+ end
551
+ end
552
+
553
+ # Closes the entry.
554
+ def close
555
+ invalidate
556
+ end
557
+
558
+ private
559
+ def invalidate
560
+ extend InvalidEntryStream
561
+ end
562
+ end
563
+
564
+ # With no associated block, +Reader::open+ is a synonym for
565
+ # +Reader::new+. If the optional code block is given, it will be passed
566
+ # the new _writer_ as an argument and the Reader object will
567
+ # automatically be closed when the block terminates. In this instance,
568
+ # +Reader::open+ returns the value of the block.
569
+ def self.open(anIO)
570
+ reader = Reader.new(anIO)
571
+
572
+ return reader unless block_given?
573
+
574
+ begin
575
+ res = yield reader
576
+ ensure
577
+ reader.close
578
+ end
579
+
580
+ res
581
+ end
582
+
583
+ # Creates and returns a new Reader object.
584
+ def initialize(anIO)
585
+ @io = anIO
586
+ @init_pos = anIO.pos
587
+ end
588
+
589
+ # Iterates through each entry in the data stream.
590
+ def each(&block)
591
+ each_entry(&block)
592
+ end
593
+
594
+ # Resets the read pointer to the beginning of data stream. Do not call
595
+ # this during a #each or #each_entry iteration. This only works with
596
+ # random access data streams that respond to #rewind and #pos.
597
+ def rewind
598
+ if @init_pos == 0
599
+ raise NonSeekableStream unless @io.respond_to?(:rewind)
600
+ @io.rewind
601
+ else
602
+ raise NonSeekableStream unless @io.respond_to?(:pos=)
603
+ @io.pos = @init_pos
604
+ end
605
+ end
606
+
607
+ # Iterates through each entry in the data stream.
608
+ def each_entry
609
+ loop do
610
+ return if @io.eof?
611
+
612
+ header = Archive::Tar::PosixHeader.new_from_stream(@io)
613
+ return if header.empty?
614
+
615
+ entry = EntryStream.new(header, @io)
616
+ size = entry.size
617
+
618
+ yield entry
619
+
620
+ skip = (512 - (size % 512)) % 512
621
+
622
+ if @io.respond_to?(:seek)
623
+ # avoid reading...
624
+ @io.seek(size - entry.bytes_read, IO::SEEK_CUR)
625
+ else
626
+ pending = size - entry.bytes_read
627
+ while pending > 0
628
+ bread = @io.read([pending, 4096].min).size
629
+ raise UnexpectedEOF if @io.eof?
630
+ pending -= bread
631
+ end
632
+ end
633
+ @io.read(skip) # discard trailing zeros
634
+ # make sure nobody can use #read, #getc or #rewind anymore
635
+ entry.close
636
+ end
637
+ end
638
+
639
+ def close
640
+ end
641
+ end
642
+
643
+ # Wraps a Archive::Tar::Minitar::Reader with convenience methods and
644
+ # wrapped stream management; Input only works with random access data
645
+ # streams. See Input::new for details.
646
+ class Input
647
+ include Enumerable
648
+
649
+ # With no associated block, +Input::open+ is a synonym for
650
+ # +Input::new+. If the optional code block is given, it will be passed
651
+ # the new _writer_ as an argument and the Input object will
652
+ # automatically be closed when the block terminates. In this instance,
653
+ # +Input::open+ returns the value of the block.
654
+ def self.open(input)
655
+ stream = Input.new(input)
656
+ return stream unless block_given?
657
+
658
+ begin
659
+ res = yield stream
660
+ ensure
661
+ stream.close
662
+ end
663
+
664
+ res
665
+ end
666
+
667
+ # Creates a new Input object. If +input+ is a stream object that responds
668
+ # to #read), then it will simply be wrapped. Otherwise, one will be
669
+ # created and opened using Kernel#open. When Input#close is called, the
670
+ # stream object wrapped will be closed.
671
+ def initialize(input)
672
+ if input.respond_to?(:read)
673
+ @io = input
674
+ else
675
+ @io = open(input, "rb")
676
+ end
677
+ @tarreader = Archive::Tar::Minitar::Reader.new(@io)
678
+ end
679
+
680
+ # Iterates through each entry and rewinds to the beginning of the stream
681
+ # when finished.
682
+ def each(&block)
683
+ @tarreader.each { |entry| yield entry }
684
+ ensure
685
+ @tarreader.rewind
686
+ end
687
+
688
+ # Extracts the current +entry+ to +destdir+. If a block is provided, it
689
+ # yields an +action+ Symbol, the full name of the file being extracted
690
+ # (+name+), and a Hash of statistical information (+stats+).
691
+ #
692
+ # The +action+ will be one of:
693
+ # <tt>:dir</tt>:: The +entry+ is a directory.
694
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
695
+ # file is just beginning.
696
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
697
+ # of the +entry+.
698
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
699
+ #
700
+ # The +stats+ hash contains the following keys:
701
+ # <tt>:current</tt>:: The current total number of bytes read in the
702
+ # +entry+.
703
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
704
+ # cycle.
705
+ # <tt>:entry</tt>:: The entry being extracted; this is a
706
+ # Reader::EntryStream, with all methods thereof.
707
+ def extract_entry(destdir, entry) # :yields action, name, stats:
708
+ stats = {
709
+ :current => 0,
710
+ :currinc => 0,
711
+ :entry => entry
712
+ }
713
+
714
+ if entry.directory?
715
+ dest = File.join(destdir, entry.full_name)
716
+
717
+ yield :dir, entry.full_name, stats if block_given?
718
+
719
+ if Archive::Tar::Minitar.dir?(dest)
720
+ begin
721
+ FileUtils.chmod(entry.mode, dest)
722
+ rescue Exception
723
+ nil
724
+ end
725
+ else
726
+ FileUtils.mkdir_p(dest, :mode => entry.mode)
727
+ FileUtils.chmod(entry.mode, dest)
728
+ end
729
+
730
+ fsync_dir(dest)
731
+ fsync_dir(File.join(dest, ".."))
732
+ return
733
+ else # it's a file
734
+ destdir = File.join(destdir, File.dirname(entry.full_name))
735
+ FileUtils.mkdir_p(destdir, :mode => 0755)
736
+
737
+ destfile = File.join(destdir, File.basename(entry.full_name))
738
+ FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT
739
+
740
+ yield :file_start, entry.full_name, stats if block_given?
741
+
742
+ File.open(destfile, "wb", entry.mode) do |os|
743
+ loop do
744
+ data = entry.read(4096)
745
+ break unless data
746
+
747
+ stats[:currinc] = os.write(data)
748
+ stats[:current] += stats[:currinc]
749
+
750
+ yield :file_progress, entry.full_name, stats if block_given?
751
+ end
752
+ os.fsync
753
+ end
754
+
755
+ FileUtils.chmod(entry.mode, destfile)
756
+ fsync_dir(File.dirname(destfile))
757
+ fsync_dir(File.join(File.dirname(destfile), ".."))
758
+
759
+ yield :file_done, entry.full_name, stats if block_given?
760
+ end
761
+ end
762
+
763
+ # Returns the Reader object for direct access.
764
+ def tar
765
+ @tarreader
766
+ end
767
+
768
+ # Closes the Reader object and the wrapped data stream.
769
+ def close
770
+ @io.close
771
+ @tarreader.close
772
+ end
773
+
774
+ private
775
+ def fsync_dir(dirname)
776
+ # make sure this hits the disc
777
+ dir = open(dirname, 'rb')
778
+ dir.fsync
779
+ rescue # ignore IOError if it's an unpatched (old) Ruby
780
+ nil
781
+ ensure
782
+ dir.close if dir rescue nil
783
+ end
784
+ end
785
+
786
+ # Wraps a Archive::Tar::Minitar::Writer with convenience methods and
787
+ # wrapped stream management; Output only works with random access data
788
+ # streams. See Output::new for details.
789
+ class Output
790
+ # With no associated block, +Output::open+ is a synonym for
791
+ # +Output::new+. If the optional code block is given, it will be passed
792
+ # the new _writer_ as an argument and the Output object will
793
+ # automatically be closed when the block terminates. In this instance,
794
+ # +Output::open+ returns the value of the block.
795
+ def self.open(output)
796
+ stream = Output.new(output)
797
+ return stream unless block_given?
798
+
799
+ begin
800
+ res = yield stream
801
+ ensure
802
+ stream.close
803
+ end
804
+
805
+ res
806
+ end
807
+
808
+ # Creates a new Output object. If +output+ is a stream object that
809
+ # responds to #read), then it will simply be wrapped. Otherwise, one will
810
+ # be created and opened using Kernel#open. When Output#close is called,
811
+ # the stream object wrapped will be closed.
812
+ def initialize(output)
813
+ if output.respond_to?(:write)
814
+ @io = output
815
+ else
816
+ @io = ::File.open(output, "wb")
817
+ end
818
+ @tarwriter = Archive::Tar::Minitar::Writer.new(@io)
819
+ end
820
+
821
+ # Returns the Writer object for direct access.
822
+ def tar
823
+ @tarwriter
824
+ end
825
+
826
+ # Closes the Writer object and the wrapped data stream.
827
+ def close
828
+ @tarwriter.close
829
+ @io.close
830
+ end
831
+ end
832
+
833
+ class << self
834
+ # Tests if +path+ refers to a directory. Fixes an apparently
835
+ # corrupted <tt>stat()</tt> call on Windows.
836
+ def dir?(path)
837
+ File.directory?((path[-1] == ?/) ? path : "#{path}/")
838
+ end
839
+
840
+ # A convenience method for wrapping Archive::Tar::Minitar::Input.open
841
+ # (mode +r+) and Archive::Tar::Minitar::Output.open (mode +w+). No other
842
+ # modes are currently supported.
843
+ def open(dest, mode = "r", &block)
844
+ case mode
845
+ when "r"
846
+ Input.open(dest, &block)
847
+ when "w"
848
+ Output.open(dest, &block)
849
+ else
850
+ raise "Unknown open mode for Archive::Tar::Minitar.open."
851
+ end
852
+ end
853
+
854
+ # A convenience method to packs the file provided. +entry+ may either be
855
+ # a filename (in which case various values for the file (see below) will
856
+ # be obtained from <tt>File#stat(entry)</tt> or a Hash with the fields:
857
+ #
858
+ # <tt>:name</tt>:: The filename to be packed into the tarchive.
859
+ # *REQUIRED*.
860
+ # <tt>:mode</tt>:: The mode to be applied.
861
+ # <tt>:uid</tt>:: The user owner of the file. (Ignored on Windows.)
862
+ # <tt>:gid</tt>:: The group owner of the file. (Ignored on Windows.)
863
+ # <tt>:mtime</tt>:: The modification Time of the file.
864
+ #
865
+ # During packing, if a block is provided, #pack_file yields an +action+
866
+ # Symol, the full name of the file being packed, and a Hash of
867
+ # statistical information, just as with
868
+ # Archive::Tar::Minitar::Input#extract_entry.
869
+ #
870
+ # The +action+ will be one of:
871
+ # <tt>:dir</tt>:: The +entry+ is a directory.
872
+ # <tt>:file_start</tt>:: The +entry+ is a file; the extract of the
873
+ # file is just beginning.
874
+ # <tt>:file_progress</tt>:: Yielded every 4096 bytes during the extract
875
+ # of the +entry+.
876
+ # <tt>:file_done</tt>:: Yielded when the +entry+ is completed.
877
+ #
878
+ # The +stats+ hash contains the following keys:
879
+ # <tt>:current</tt>:: The current total number of bytes read in the
880
+ # +entry+.
881
+ # <tt>:currinc</tt>:: The current number of bytes read in this read
882
+ # cycle.
883
+ # <tt>:name</tt>:: The filename to be packed into the tarchive.
884
+ # *REQUIRED*.
885
+ # <tt>:mode</tt>:: The mode to be applied.
886
+ # <tt>:uid</tt>:: The user owner of the file. (+nil+ on Windows.)
887
+ # <tt>:gid</tt>:: The group owner of the file. (+nil+ on Windows.)
888
+ # <tt>:mtime</tt>:: The modification Time of the file.
889
+ def pack_file(entry, outputter) #:yields action, name, stats:
890
+ outputter = outputter.tar if outputter.kind_of?(Archive::Tar::Minitar::Output)
891
+
892
+ stats = {}
893
+
894
+ if entry.kind_of?(Hash)
895
+ name = entry[:name]
896
+
897
+ entry.each { |kk, vv| stats[kk] = vv unless vv.nil? }
898
+ else
899
+ name = entry
900
+ end
901
+
902
+ name = name.sub(%r{\./}, '')
903
+ stat = File.stat(name)
904
+ stats[:mode] ||= stat.mode
905
+ stats[:mtime] ||= stat.mtime
906
+ stats[:size] = stat.size
907
+ if name == "sh-install" # an ugly shoes-specific hack, to
908
+ stats[:mode] = 0755 # get the file to be 0755 on windows
909
+ end
910
+
911
+ if RUBY_PLATFORM =~ /win32/
912
+ stats[:uid] = nil
913
+ stats[:gid] = nil
914
+ else
915
+ stats[:uid] ||= stat.uid
916
+ stats[:gid] ||= stat.gid
917
+ end
918
+
919
+ case
920
+ when File.file?(name)
921
+ outputter.add_file_simple(name, stats) do |os|
922
+ stats[:current] = 0
923
+ yield :file_start, name, stats if block_given?
924
+ File.open(name, "rb") do |ff|
925
+ until ff.eof?
926
+ stats[:currinc] = os.write(ff.read(4096))
927
+ stats[:current] += stats[:currinc]
928
+ yield :file_progress, name, stats if block_given?
929
+ end
930
+ end
931
+ yield :file_done, name, stats if block_given?
932
+ end
933
+ when dir?(name)
934
+ yield :dir, name, stats if block_given?
935
+ outputter.mkdir(name, stats)
936
+ else
937
+ raise "Don't yet know how to pack this type of file."
938
+ end
939
+ end
940
+
941
+ # A convenience method to pack files specified by +src+ into +dest+. If
942
+ # +src+ is an Array, then each file detailed therein will be packed into
943
+ # the resulting Archive::Tar::Minitar::Output stream; if +recurse_dirs+
944
+ # is true, then directories will be recursed.
945
+ #
946
+ # If +src+ is an Array, it will be treated as the argument to Find.find;
947
+ # all files matching will be packed.
948
+ def pack(src, dest, recurse_dirs = true, &block)
949
+ Output.open(dest) do |outp|
950
+ if src.kind_of?(Array)
951
+ src.each do |entry|
952
+ pack_file(entry, outp, &block)
953
+ if dir?(entry) and recurse_dirs
954
+ Dir["#{entry}/**/**"].each do |ee|
955
+ pack_file(ee, outp, &block)
956
+ end
957
+ end
958
+ end
959
+ else
960
+ Find.find(src) do |entry|
961
+ pack_file(entry, outp, &block)
962
+ end
963
+ end
964
+ end
965
+ end
966
+
967
+ # A convenience method to unpack files from +src+ into the directory
968
+ # specified by +dest+. Only those files named explicitly in +files+
969
+ # will be extracted.
970
+ def unpack(src, dest, files = [], &block)
971
+ Input.open(src) do |inp|
972
+ if File.exist?(dest) and (not dir?(dest))
973
+ raise "Can't unpack to a non-directory."
974
+ elsif not File.exist?(dest)
975
+ FileUtils.mkdir_p(dest)
976
+ end
977
+
978
+ inp.each do |entry|
979
+ if files.empty? or files.include?(entry.full_name)
980
+ inp.extract_entry(dest, entry, &block)
981
+ end
982
+ end
983
+ end
984
+ end
985
+ end
986
+ end