File: writer.rb

package info (click to toggle)
ruby-minitar 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 364 kB
  • sloc: ruby: 2,602; makefile: 11
file content (302 lines) | stat: -rw-r--r-- 9,382 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# frozen_string_literal: true

class Minitar
  # The class that writes a tar format archive to a data stream.
  class Writer
    # The exception raised when the user attempts to write more data to
    # a BoundedWriteStream than has been allocated.
    WriteBoundaryOverflow = Class.new(StandardError)

    # A stream wrapper that can only be written to. Any attempt to read from this
    # restricted stream will result in a NameError being thrown.
    class WriteOnlyStream
      def initialize(io)
        @io = io
      end

      def write(data) = @io.write(data)
    end

    private_constant :WriteOnlyStream

    # A WriteOnlyStream that also has a size limit.
    class BoundedWriteStream < WriteOnlyStream
      # The maximum number of bytes that may be written to this data stream.
      attr_reader :limit
      # The current total number of bytes written to this data stream.
      attr_reader :written

      def initialize(io, limit)
        @io = io
        @limit = limit
        @written = 0
      end

      def write(data)
        size = data.bytesize
        raise WriteBoundaryOverflow if (size + @written) > @limit
        @io.write(data)
        @written += size
        size
      end
    end

    private_constant :BoundedWriteStream

    # With no associated block, +Writer::open+ is a synonym for +Writer::new+. If the
    # optional code block is given, it will be passed the new _writer_ as an argument and
    # the Writer object will automatically be closed when the block terminates. In this
    # instance, +Writer::open+ returns the value of the block.
    #
    # :call-seq:
    #    w = Minitar::Writer.open(STDOUT)
    #    w.add_file_simple('foo.txt', :size => 3)
    #    w.close
    #
    #    Minitar::Writer.open(STDOUT) do |w|
    #      w.add_file_simple('foo.txt', :size => 3)
    #    end
    def self.open(io) # :yields: Writer
      writer = new(io)
      return writer unless block_given?

      # This exception context must remain, otherwise the stream closes on open even if
      # a block is not given.
      begin
        yield writer
      ensure
        writer.close
      end
    end

    # Creates and returns a new Writer object.
    def initialize(io)
      @io = io
      @closed = false
    end

    # Adds a file to the archive as +name+. The data can be provided in the
    # <tt>opts[:data]</tt> or provided to a BoundedWriteStream that is yielded to the
    # provided block.
    #
    # If <tt>opts[:data]</tt> is provided, all other values to +opts+ are optional. If the
    # data is provided to the yielded BoundedWriteStream, <tt>opts[:size]</tt> must be
    # provided.
    #
    # Valid parameters to +opts+ are:
    #
    # <tt>:data</tt>::  Optional. The data to write to the archive.
    # <tt>:mode</tt>::  The Unix file permissions mode value. If not provided, defaults to
    #                   0o644.
    # <tt>:size</tt>::  The size, in bytes. If <tt>:data</tt> is provided, this parameter
    #                   may be ignored (if it is less than the size of the data provided)
    #                   or used to add padding (if it is greater than the size of the data
    #                   provided).
    # <tt>:uid</tt>::   The Unix file owner user ID number.
    # <tt>:gid</tt>::   The Unix file owner group ID number.
    # <tt>:mtime</tt>:: File modification time, interpreted as an integer.
    #
    # An exception will be raised if the Writer is already closed, or if more data is
    # written to the BoundedWriteStream than expected.
    #
    # :call-seq:
    #    writer.add_file_simple('foo.txt', :data => "bar")
    #    writer.add_file_simple('foo.txt', :size => 3) do |w|
    #      w.write("bar")
    #    end
    def add_file_simple(name, opts = {}) # :yields: BoundedWriteStream
      raise ClosedStream if @closed

      header = {
        mode: opts.fetch(:mode, 0o644),
        mtime: opts.fetch(:mtime, nil),
        gid: opts.fetch(:gid, nil),
        uid: opts.fetch(:uid, nil)
      }

      data = opts.fetch(:data, nil)
      size = opts.fetch(:size, nil)

      if block_given?
        raise ArgumentError, "Too much data (opts[:data] and block_given?)." if data
        raise ArgumentError, "No size provided" unless size
      else
        raise ArgumentError, "No data provided" unless data

        bytes = data.bytesize
        size = bytes if size.nil? || size < bytes
      end

      header[:size] = size

      short_name, prefix, needs_long_name = split_name(name)
      write_header(header, name, short_name, prefix, needs_long_name)

      os = BoundedWriteStream.new(@io, size)
      if block_given?
        yield os
      else
        os.write(data)
      end

      min_padding = size - os.written
      @io.write("\0" * min_padding)
      remainder = (512 - (size % 512)) % 512
      @io.write("\0" * remainder)
    end

    # Adds a file to the archive as +name+. The data can be provided in the
    # <tt>opts[:data]</tt> or provided to a yielded +WriteOnlyStream+. The size of the
    # file will be determined from the amount of data written to the stream.
    #
    # Valid parameters to +opts+ are:
    #
    # <tt>:mode</tt>::  The Unix file permissions mode value. If not provided, defaults to
    #                   0o644.
    # <tt>:uid</tt>::   The Unix file owner user ID number.
    # <tt>:gid</tt>::   The Unix file owner group ID number.
    # <tt>:mtime</tt>:: File modification time, interpreted as an integer.
    # <tt>:data</tt>::  Optional. The data to write to the archive.
    #
    # If <tt>opts[:data]</tt> is provided, this acts the same as #add_file_simple.
    # Otherwise, the file's size will be determined from the amount of data written to the
    # stream.
    #
    # For #add_file to be used without <tt>opts[:data]</tt>, the Writer must be wrapping
    # a stream object that is seekable. Otherwise, #add_file_simple must be used.
    #
    # +opts+ may be modified during the writing of the file to the stream.
    def add_file(name, opts = {}, &) # :yields: WriteOnlyStream, +opts+
      raise ClosedStream if @closed

      return add_file_simple(name, opts, &) if opts[:data]

      raise Minitar::NonSeekableStream unless Minitar.seekable?(@io)

      short_name, prefix, needs_long_name = split_name(name)

      data_offset = needs_long_name ? 3 * 512 : 512
      init_pos = @io.pos
      @io.write("\0" * data_offset) # placeholder for the header

      yield WriteOnlyStream.new(@io), opts

      size = @io.pos - (init_pos + data_offset)
      remainder = (512 - (size % 512)) % 512
      @io.write("\0" * remainder)

      final_pos, @io.pos = @io.pos, init_pos

      header = {
        mode: opts[:mode],
        mtime: opts[:mtime],
        size: size,
        gid: opts[:gid],
        uid: opts[:uid]
      }

      write_header(header, name, short_name, prefix, needs_long_name)

      @io.pos = final_pos
    end

    # Creates a directory entry in the tar.
    def mkdir(name, opts = {})
      raise ClosedStream if @closed

      header = {
        mode: opts[:mode],
        typeflag: "5",
        size: 0,
        gid: opts[:gid],
        uid: opts[:uid],
        mtime: opts[:mtime]
      }

      short_name, prefix, needs_long_name = split_name(name)
      write_header(header, name, short_name, prefix, needs_long_name)

      nil
    end

    # Creates a symbolic link entry in the tar.
    def symlink(name, link_target, opts = {})
      raise ClosedStream if @closed
      raise FileNameTooLong if link_target.size > 100

      name, prefix = split_name(name)
      header = {
        name: name,
        mode: opts[:mode],
        typeflag: "2",
        size: 0,
        linkname: link_target,
        gid: opts[:gid],
        uid: opts[:uid],
        mtime: opts[:mtime],
        prefix: prefix
      }
      @io.write(PosixHeader.new(header).to_s)
      nil
    end

    # Passes the #flush method to the wrapped stream, used for buffered streams.
    def flush
      raise ClosedStream if @closed
      @io.flush if @io.respond_to?(:flush)
    end

    # Returns false if the writer is open.
    def closed? = @closed

    # Closes the Writer. This does not close the underlying wrapped output stream.
    def close
      return if @closed
      @io.write("\0" * 1024)
      @closed = true
    end

    private

    def write_header(header, long_name, short_name, prefix, needs_long_name)
      if needs_long_name
        long_name_header = {
          prefix: "",
          name: PosixHeader::GNU_EXT_LONG_LINK,
          typeflag: "L",
          size: long_name.length + 1,
          mode: 0
        }
        @io.write(PosixHeader.new(long_name_header).to_s)
        @io.write(long_name)
        @io.write("\0" * (512 - (long_name.length % 512)))
      end

      new_header = header.merge({name: short_name, prefix: prefix})
      @io.write(PosixHeader.new(new_header).to_s)
    end

    def split_name(name)
      if name.bytesize <= 100
        prefix = ""
      else
        parts = name.split("/")
        newname = parts.pop

        nxt = ""

        loop do
          nxt = parts.pop || ""
          break if newname.bytesize + 1 + nxt.bytesize >= 100
          newname = "#{nxt}/#{newname}"
        end

        prefix = (parts + [nxt]).join("/")

        name = newname
      end

      [name, prefix, name.bytesize > 100 || prefix.bytesize > 155]
    end
  end
end