1 # frozen_string_literal: true
3 # rubocop:disable Style/AsciiComments
5 # Copyright (C) 2004 Mauricio Julio Fernández Pradier
6 # See LICENSE.txt for additional licensing information.
8 # rubocop:enable Style/AsciiComments
12 # struct tarfile_entry_posix {
13 # char name[100]; # ASCII + (Z unless filled)
14 # char mode[8]; # 0 padded, octal, null
15 # char uid[8]; # ditto
16 # char gid[8]; # ditto
17 # char size[12]; # 0 padded, octal, null
18 # char mtime[12]; # 0 padded, octal, null
19 # char checksum[8]; # 0 padded, octal, null, space
20 # char typeflag[1]; # file: "0" dir: "5"
21 # char linkname[100]; # ASCII + (Z unless filled)
22 # char magic[6]; # "ustar\0"
23 # char version[2]; # "00"
24 # char uname[32]; # ASCIIZ
25 # char gname[32]; # ASCIIZ
26 # char devmajor[8]; # 0 padded, octal, null
27 # char devminor[8]; # o padded, octal, null
28 # char prefix[155]; # ASCII + (Z unless filled)
31 # A header for a tar file
33 class Gem::Package::TarHeader
35 # Fields in the tar header
57 # Pack format for a tar header
59 PACK_FORMAT = "a100" + # name
77 # Unpack format for a tar header
79 UNPACK_FORMAT = "A100" + # name
98 EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc:
101 # Creates a tar header from IO +stream+
103 def self.from(stream)
104 header = stream.read 512
105 return EMPTY if header == EMPTY_HEADER
107 fields = header.unpack UNPACK_FORMAT
109 new name: fields.shift,
110 mode: strict_oct(fields.shift),
111 uid: oct_or_256based(fields.shift),
112 gid: oct_or_256based(fields.shift),
113 size: strict_oct(fields.shift),
114 mtime: strict_oct(fields.shift),
115 checksum: strict_oct(fields.shift),
116 typeflag: fields.shift,
117 linkname: fields.shift,
119 version: strict_oct(fields.shift),
122 devmajor: strict_oct(fields.shift),
123 devminor: strict_oct(fields.shift),
124 prefix: fields.shift,
129 def self.strict_oct(str)
131 return str.oct if /\A[0-7]*\z/.match?(str)
133 raise ArgumentError, "#{str.inspect} is not an octal string"
136 def self.oct_or_256based(str)
137 # \x80 flags a positive 256-based number
138 # \ff flags a negative 256-based number
139 # In case we have a match, parse it as a signed binary value
140 # in big-endian order, except that the high-order bit is ignored.
142 return str.unpack1("@4N") if /\A[\x80\xff]/n.match?(str)
147 # Creates a new TarHeader using +vals+
150 unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
151 raise ArgumentError, ":name, :size, :prefix and :mode required"
154 @checksum = vals[:checksum] || ""
155 @devmajor = vals[:devmajor] || 0
156 @devminor = vals[:devminor] || 0
157 @gid = vals[:gid] || 0
158 @gname = vals[:gname] || "wheel"
159 @linkname = vals[:linkname]
160 @magic = vals[:magic] || "ustar"
162 @mtime = vals[:mtime] || 0
164 @prefix = vals[:prefix]
166 @typeflag = vals[:typeflag]
167 @typeflag = "0" if @typeflag.nil? || @typeflag.empty?
168 @uid = vals[:uid] || 0
169 @uname = vals[:uname] || "wheel"
170 @version = vals[:version] || "00"
172 @empty = vals[:empty]
175 EMPTY = new({ # :nodoc:
189 private_constant :EMPTY
192 # Is the tar entry empty?
198 def ==(other) # :nodoc:
199 self.class === other &&
200 @checksum == other.checksum &&
201 @devmajor == other.devmajor &&
202 @devminor == other.devminor &&
204 @gname == other.gname &&
205 @linkname == other.linkname &&
206 @magic == other.magic &&
207 @mode == other.mode &&
208 @mtime == other.mtime &&
209 @name == other.name &&
210 @prefix == other.prefix &&
211 @size == other.size &&
212 @typeflag == other.typeflag &&
214 @uname == other.uname &&
215 @version == other.version
224 # Updates the TarHeader's checksum
227 header = header " " * 8
228 @checksum = oct calculate_checksum(header), 6
233 def calculate_checksum(header)
237 def header(checksum = @checksum)
258 header = header.pack PACK_FORMAT
260 header.ljust 512, "\0"
264 format("%0#{len}o", num)