• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

halostatue / minitar / 17531210856

07 Sep 2025 04:42PM UTC coverage: 91.933% (+7.2%) from 84.689%
17531210856

Pull #151

github

web-flow
Merge 6d1337f92 into f9fcc1649
Pull Request #151: chore: Fix GNU long filename handling bug

335 of 467 branches covered (71.73%)

73 of 77 new or added lines in 8 files covered. (94.81%)

5 existing lines in 1 file now uncovered.

547 of 595 relevant lines covered (91.93%)

174.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.83
/lib/minitar/posix_header.rb
1
# frozen_string_literal: true
2

3
class Minitar
2✔
4
  # Implements the POSIX tar header as a Ruby class. The structure of
5
  # the POSIX tar header is:
6
  #
7
  #   struct tarfile_entry_posix
8
  #   {                      //                               pack   unpack
9
  #      char name[100];     // ASCII (+ Z unless filled)     a100   Z100
10
  #      char mode[8];       // 0 padded, octal, null         a8     A8
11
  #      char uid[8];        // 0 padded, octal, null         a8     A8
12
  #      char gid[8];        // 0 padded, octal, null         a8     A8
13
  #      char size[12];      // 0 padded, octal, null         a12    A12
14
  #      char mtime[12];     // 0 padded, octal, null         a12    A12
15
  #      char checksum[8];   // 0 padded, octal, null, space  a8     A8
16
  #      char typeflag[1];   // see below                     a      a
17
  #      char linkname[100]; // ASCII + (Z unless filled)     a100   Z100
18
  #      char magic[6];      // "ustar\0"                     a6     A6
19
  #      char version[2];    // "00"                          a2     A2
20
  #      char uname[32];     // ASCIIZ                        a32    Z32
21
  #      char gname[32];     // ASCIIZ                        a32    Z32
22
  #      char devmajor[8];   // 0 padded, octal, null         a8     A8
23
  #      char devminor[8];   // 0 padded, octal, null         a8     A8
24
  #      char prefix[155];   // ASCII (+ Z unless filled)     a155   Z155
25
  #   };
26
  #
27
  # The #typeflag is one of several known values. POSIX indicates that "A POSIX-compliant
28
  # implementation must treat any unrecognized typeflag value as a regular file."
29
  class PosixHeader
2✔
30
    BLOCK_SIZE = 512
2✔
31
    MAGIC_BYTES = "ustar"
2✔
32

33
    GNU_EXT_LONG_LINK = "././@LongLink"
2✔
34

35
    # Fields that must be set in a POSIX tar(1) header.
36
    REQUIRED_FIELDS = [:name, :size, :prefix, :mode].freeze
2✔
37
    # Fields that may be set in a POSIX tar(1) header.
38
    OPTIONAL_FIELDS = [
2✔
39
      :uid, :gid, :mtime, :checksum, :typeflag, :linkname, :magic, :version, :uname,
40
      :gname, :devmajor, :devminor
41
    ].freeze
42

43
    # All fields available in a POSIX tar(1) header.
44
    FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze
2✔
45

46
    FIELDS.each do |f|
2✔
47
      attr_reader f.to_sym
32✔
48
    end
49

50
    ##
51
    def name=(value)
2✔
52
      valid_name!(value)
2✔
NEW
53
      @name = value
×
54
    end
55

56
    attr_writer :size
2✔
57

58
    # The pack format passed to Array#pack for encoding a header.
59
    HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155"
2✔
60
    # The unpack format passed to String#unpack for decoding a header.
61
    HEADER_UNPACK_FORMAT = "Z100A8A8A8a12A12A8aZ100A6A2Z32Z32A8A8Z155"
2✔
62

63
    class << self
2✔
64
      # Creates a new PosixHeader from a data stream.
65
      def from_stream(stream) = from_data(stream.read(BLOCK_SIZE))
2✔
66

67
      # Creates a new PosixHeader from a BLOCK_SIZE-byte data buffer.
68
      def from_data(data)
2✔
69
        fields = data.unpack(HEADER_UNPACK_FORMAT)
586✔
70
        name = fields.shift
586✔
71
        mode = fields.shift.oct
586✔
72
        uid = fields.shift.oct
586✔
73
        gid = fields.shift.oct
586✔
74
        size = parse_numeric_field(fields.shift)
586✔
75
        mtime = fields.shift.oct
582✔
76
        checksum = fields.shift.oct
582✔
77
        typeflag = fields.shift
582✔
78
        linkname = fields.shift
582✔
79
        magic = fields.shift
582✔
80
        version = fields.shift.oct
582✔
81
        uname = fields.shift
582✔
82
        gname = fields.shift
582✔
83
        devmajor = fields.shift.oct
582✔
84
        devminor = fields.shift.oct
582✔
85
        prefix = fields.shift
582✔
86

87
        empty = !data.each_byte.any?(&:nonzero?)
582✔
88

89
        new(
582✔
90
          name: name,
91
          mode: mode,
92
          uid: uid,
93
          gid: gid,
94
          size: size,
95
          mtime: mtime,
96
          checksum: checksum,
97
          typeflag: typeflag,
98
          magic: magic,
99
          version: version,
100
          uname: uname,
101
          gname: gname,
102
          devmajor: devmajor,
103
          devminor: devminor,
104
          prefix: prefix,
105
          empty: empty,
106
          linkname: linkname
107
        )
108
      end
109

110
      private
2✔
111

112
      def parse_numeric_field(string)
2✔
113
        return string.oct if /\A[0-7 \0]*\z/.match?(string) # \0 appears as a padding
600✔
114
        return parse_base256(string) if string.bytes.first == 0x80 || string.bytes.first == 0xff
12✔
115
        raise ArgumentError, "#{string.inspect} is not a valid numeric field"
8✔
116
      end
117

118
      def parse_base256(string)
2✔
119
        # https://www.gnu.org/software/tar/manual/html_node/Extensions.html
120
        bytes = string.bytes
4✔
121
        case bytes.first
4!
122
        when 0x80 # Positive number: *non-leading* bytes, number in big-endian order
1✔
123
          bytes[1..].inject(0) { |r, byte| (r << 8) | byte }
24✔
124
        when 0xff # Negative number: *all* bytes, two's complement in big-endian order
1✔
125
          result = bytes.inject(0) { |r, byte| (r << 8) | byte }
26✔
126
          bit_length = bytes.size * 8
2✔
127
          result - (1 << bit_length)
2✔
128
        else
×
129
          raise ArgumentError, "Invalid binary field format"
×
130
        end
131
      end
132
    end
133

134
    # Creates a new PosixHeader. A PosixHeader cannot be created unless +name+, +size+,
135
    # +prefix+, and +mode+ are provided.
136
    def initialize(v)
2✔
137
      REQUIRED_FIELDS.each do |f|
1,034✔
138
        raise ArgumentError, "Field #{f} is required." unless v.key?(f)
4,124✔
139
      end
140

141
      v[:mtime] = v[:mtime].to_i
1,026✔
142
      v[:checksum] ||= ""
1,026✔
143
      v[:typeflag] ||= "0"
1,026✔
144
      v[:magic] ||= MAGIC_BYTES
1,026✔
145
      v[:version] ||= "00"
1,026✔
146

147
      FIELDS.each do |f|
1,026✔
148
        instance_variable_set(:"@#{f}", v[f])
16,416✔
149
      end
150

151
      @empty = v[:empty]
1,026✔
152

153
      valid_name!(v[:name]) unless v[:empty]
1,026✔
154
    end
155

156
    # Indicates if the header was an empty header.
157
    def empty? = @empty
2✔
158

159
    # Indicates if the header has a valid magic value.
160
    def valid? = empty? || @magic == MAGIC_BYTES
2✔
161

162
    # Returns +true+ if the header is a long name special header which indicates
163
    # that the next block of data is the filename.
164
    def long_name? = typeflag == "L" && name == GNU_EXT_LONG_LINK
2✔
165

166
    # Returns +true+ if the header is a PAX extended header which contains
167
    # metadata for the next file entry.
168
    def pax_header? = typeflag == "x"
2✔
169

170
    # Sets the +name+ to the +value+ provided and clears +prefix+.
171
    #
172
    # Used by Minitar::Reader#each_entry to set the long name when processing GNU long
173
    # filename extensions.
174
    #
175
    # The +value+ must be the complete name, including leading directory components.
176
    def long_name=(value)
2✔
177
      valid_name!(value)
144✔
178

179
      @prefix = ""
142✔
180
      @name = value
142✔
181
    end
182

183
    # A string representation of the header.
184
    def to_s
2✔
185
      update_checksum
422✔
186
      header(@checksum)
422✔
187
    end
188
    alias_method :to_str, :to_s
2✔
189

190
    # TODO: In Minitar 2, PosixHeader#to_str will be removed.
191

192
    # Update the checksum field.
193
    def update_checksum
2✔
194
      hh = header(" " * 8)
422✔
195
      @checksum = oct(calculate_checksum(hh), 6)
422✔
196
    end
197

198
    private
2✔
199

200
    def valid_name!(value)
2✔
201
      return if value.is_a?(String) && !value.empty?
1,086✔
202
      raise ArgumentError, "Field name must be a non-empty string"
6✔
203
    end
204

205
    def oct(num, len)
2✔
206
      if num.nil?
6,330✔
207
        "\0" * (len + 1)
3,016✔
208
      else
1,657✔
209
        "%0#{len}o" % num
3,314✔
210
      end
211
    end
212

213
    def calculate_checksum(hdr)
2✔
214
      hdr.unpack("C*").inject(:+)
422✔
215
    end
216

217
    def header(chksum)
2✔
218
      arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
844✔
219
        oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
220
        uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
221
      str = arr.pack(HEADER_PACK_FORMAT)
844✔
222
      str + "\0" * ((BLOCK_SIZE - str.bytesize) % BLOCK_SIZE)
844✔
223
    end
224

225
    ##
226
    # :attr_accessor: name
227
    # The name of the file. Required.
228
    #
229
    # By default, limited to 100 bytes, but may be up to BLOCK_SIZE bytes if using the
230
    # GNU long name tar extension.
231

232
    ##
233
    # :attr_accessor: size
234
    # The size of the file. Required.
235

236
    ##
237
    # :attr_reader: prefix
238
    # The prefix of the file; the path before #name. Limited to 155 bytes.
239
    # Required.
240

241
    ##
242
    # :attr_reader: mode
243
    # The Unix file mode of the file. Stored as an octal integer. Required.
244

245
    ##
246
    # :attr_reader: uid
247
    # The Unix owner user ID of the file. Stored as an octal integer.
248

249
    ##
250
    # :attr_reader: uname
251
    # The user name of the Unix owner of the file.
252

253
    ##
254
    # :attr_reader: gid
255
    # The Unix owner group ID of the file. Stored as an octal integer.
256

257
    ##
258
    # :attr_reader: gname
259
    # The group name of the Unix owner of the file.
260

261
    ##
262
    # :attr_reader: mtime
263
    # The modification time of the file in epoch seconds. Stored as an
264
    # octal integer.
265

266
    ##
267
    # :attr_reader: checksum
268
    # The checksum of the file. Stored as an octal integer. Calculated
269
    # before encoding the header as a string.
270

271
    ##
272
    # :attr_reader: typeflag
273
    # The type of record in the file.
274
    #
275
    # +0+::  Regular file. NULL should be treated as a synonym, for compatibility
276
    #        purposes.
277
    # +1+::  Hard link.
278
    # +2+::  Symbolic link.
279
    # +3+::  Character device node.
280
    # +4+::  Block device node.
281
    # +5+::  Directory.
282
    # +6+::  FIFO node.
283
    # +7+::  Reserved.
284
    # +L+::  GNU extension for long filenames when #name is <tt>././@LongLink</tt>.
285

286
    ##
287
    # :attr_reader: linkname
288
    # The target of the symbolic link.
289

290
    ##
291
    # :attr_reader: magic
292
    # Always "ustar\0".
293

294
    ##
295
    # :attr_reader: version
296
    # Always "00"
297

298
    ##
299
    # :attr_reader: devmajor
300
    # The major device ID. Not currently used.
301

302
    ##
303
    # :attr_reader: devminor
304
    # The minor device ID. Not currently used.
305
  end
306
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc