1# -*- coding: utf-8 -*-
2#--
3# Copyright (C) 2004 Mauricio Julio Fernández Pradier
4# See LICENSE.txt for additional licensing information.
5#++
6
7##
8# Allows writing of tar files
9
10class Gem::Package::TarWriter
11
12  class FileOverflow < StandardError; end
13
14  ##
15  # IO wrapper that allows writing a limited amount of data
16
17  class BoundedStream
18
19    ##
20    # Maximum number of bytes that can be written
21
22    attr_reader :limit
23
24    ##
25    # Number of bytes written
26
27    attr_reader :written
28
29    ##
30    # Wraps +io+ and allows up to +limit+ bytes to be written
31
32    def initialize(io, limit)
33      @io = io
34      @limit = limit
35      @written = 0
36    end
37
38    ##
39    # Writes +data+ onto the IO, raising a FileOverflow exception if the
40    # number of bytes will be more than #limit
41
42    def write(data)
43      if data.bytesize + @written > @limit
44        raise FileOverflow, "You tried to feed more data than fits in the file."
45      end
46      @io.write data
47      @written += data.bytesize
48      data.bytesize
49    end
50
51  end
52
53  ##
54  # IO wrapper that provides only #write
55
56  class RestrictedStream
57
58    ##
59    # Creates a new RestrictedStream wrapping +io+
60
61    def initialize(io)
62      @io = io
63    end
64
65    ##
66    # Writes +data+ onto the IO
67
68    def write(data)
69      @io.write data
70    end
71
72  end
73
74  ##
75  # Creates a new TarWriter, yielding it if a block is given
76
77  def self.new(io)
78    writer = super
79
80    return writer unless block_given?
81
82    begin
83      yield writer
84    ensure
85      writer.close
86    end
87
88    nil
89  end
90
91  ##
92  # Creates a new TarWriter that will write to +io+
93
94  def initialize(io)
95    @io = io
96    @closed = false
97  end
98
99  ##
100  # Adds file +name+ with permissions +mode+, and yields an IO for writing the
101  # file to
102
103  def add_file(name, mode) # :yields: io
104    check_closed
105
106    raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
107
108    name, prefix = split_name name
109
110    init_pos = @io.pos
111    @io.write "\0" * 512 # placeholder for the header
112
113    yield RestrictedStream.new(@io) if block_given?
114
115    size = @io.pos - init_pos - 512
116
117    remainder = (512 - (size % 512)) % 512
118    @io.write "\0" * remainder
119
120    final_pos = @io.pos
121    @io.pos = init_pos
122
123    header = Gem::Package::TarHeader.new :name => name, :mode => mode,
124                                         :size => size, :prefix => prefix
125
126    @io.write header
127    @io.pos = final_pos
128
129    self
130  end
131
132  ##
133  # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
134  # the file.  The +digest_algorithm+ is written to a read-only +name+.sum
135  # file following the given file contents containing the digest name and
136  # hexdigest separated by a tab.
137  #
138  # The created digest object is returned.
139
140  def add_file_digest name, mode, digest_algorithms # :yields: io
141    digests = digest_algorithms.map do |digest_algorithm|
142      digest = digest_algorithm.new
143      [digest.name, digest]
144    end
145
146    digests = Hash[*digests.flatten]
147
148    add_file name, mode do |io|
149      Gem::Package::DigestIO.wrap io, digests do |digest_io|
150        yield digest_io
151      end
152    end
153
154    digests
155  end
156
157  ##
158  # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
159  # the file.  The +signer+ is used to add a digest file using its
160  # digest_algorithm per add_file_digest and a cryptographic signature in
161  # +name+.sig.  If the signer has no key only the checksum file is added.
162  #
163  # Returns the digest.
164
165  def add_file_signed name, mode, signer
166    digest_algorithms = [
167      signer.digest_algorithm,
168      OpenSSL::Digest::SHA512,
169    ].uniq
170
171    digests = add_file_digest name, mode, digest_algorithms do |io|
172      yield io
173    end
174
175    signature_digest = digests.values.find do |digest|
176      digest.name == signer.digest_name
177    end
178
179    signature = signer.sign signature_digest.digest
180
181    add_file_simple "#{name}.sig", 0444, signature.length do |io|
182      io.write signature
183    end if signature
184
185    digests
186  end
187
188  ##
189  # Add file +name+ with permissions +mode+ +size+ bytes long.  Yields an IO
190  # to write the file to.
191
192  def add_file_simple(name, mode, size) # :yields: io
193    check_closed
194
195    name, prefix = split_name name
196
197    header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
198                                         :size => size, :prefix => prefix).to_s
199
200    @io.write header
201    os = BoundedStream.new @io, size
202
203    yield os if block_given?
204
205    min_padding = size - os.written
206    @io.write("\0" * min_padding)
207
208    remainder = (512 - (size % 512)) % 512
209    @io.write("\0" * remainder)
210
211    self
212  end
213
214  ##
215  # Raises IOError if the TarWriter is closed
216
217  def check_closed
218    raise IOError, "closed #{self.class}" if closed?
219  end
220
221  ##
222  # Closes the TarWriter
223
224  def close
225    check_closed
226
227    @io.write "\0" * 1024
228    flush
229
230    @closed = true
231  end
232
233  ##
234  # Is the TarWriter closed?
235
236  def closed?
237    @closed
238  end
239
240  ##
241  # Flushes the TarWriter's IO
242
243  def flush
244    check_closed
245
246    @io.flush if @io.respond_to? :flush
247  end
248
249  ##
250  # Creates a new directory in the tar file +name+ with +mode+
251
252  def mkdir(name, mode)
253    check_closed
254
255    name, prefix = split_name(name)
256
257    header = Gem::Package::TarHeader.new :name => name, :mode => mode,
258                                         :typeflag => "5", :size => 0,
259                                         :prefix => prefix
260
261    @io.write header
262
263    self
264  end
265
266  ##
267  # Splits +name+ into a name and prefix that can fit in the TarHeader
268
269  def split_name(name) # :nodoc:
270    raise Gem::Package::TooLongFileName if name.bytesize > 256
271
272    if name.bytesize <= 100 then
273      prefix = ""
274    else
275      parts = name.split(/\//)
276      newname = parts.pop
277      nxt = ""
278
279      loop do
280        nxt = parts.pop
281        break if newname.bytesize + 1 + nxt.bytesize > 100
282        newname = nxt + "/" + newname
283      end
284
285      prefix = (parts + [nxt]).join "/"
286      name = newname
287
288      if name.bytesize > 100 or prefix.bytesize > 155 then
289        raise Gem::Package::TooLongFileName
290      end
291    end
292
293    return name, prefix
294  end
295
296end
297
298