[rubygems/rubygems] Use a constant empty tar header to avoid extra allocations
[ruby.git] / lib / rubygems / package / tar_header.rb
blobdd5e835a1e59b127e391cd97d05644b0fadb1ce6
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
11 #--
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)
29 # };
30 #++
31 # A header for a tar file
33 class Gem::Package::TarHeader
34   ##
35   # Fields in the tar header
37   FIELDS = [
38     :checksum,
39     :devmajor,
40     :devminor,
41     :gid,
42     :gname,
43     :linkname,
44     :magic,
45     :mode,
46     :mtime,
47     :name,
48     :prefix,
49     :size,
50     :typeflag,
51     :uid,
52     :uname,
53     :version,
54   ].freeze
56   ##
57   # Pack format for a tar header
59   PACK_FORMAT = "a100" + # name
60                 "a8"   + # mode
61                 "a8"   + # uid
62                 "a8"   + # gid
63                 "a12"  + # size
64                 "a12"  + # mtime
65                 "a7a"  + # chksum
66                 "a"    + # typeflag
67                 "a100" + # linkname
68                 "a6"   + # magic
69                 "a2"   + # version
70                 "a32"  + # uname
71                 "a32"  + # gname
72                 "a8"   + # devmajor
73                 "a8"   + # devminor
74                 "a155"   # prefix
76   ##
77   # Unpack format for a tar header
79   UNPACK_FORMAT = "A100" + # name
80                   "A8"   + # mode
81                   "A8"   + # uid
82                   "A8"   + # gid
83                   "A12"  + # size
84                   "A12"  + # mtime
85                   "A8"   + # checksum
86                   "A"    + # typeflag
87                   "A100" + # linkname
88                   "A6"   + # magic
89                   "A2"   + # version
90                   "A32"  + # uname
91                   "A32"  + # gname
92                   "A8"   + # devmajor
93                   "A8"   + # devminor
94                   "A155"   # prefix
96   attr_reader(*FIELDS)
98   EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc:
100   ##
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,
118         magic: fields.shift,
119         version: strict_oct(fields.shift),
120         uname: fields.shift,
121         gname: fields.shift,
122         devmajor: strict_oct(fields.shift),
123         devminor: strict_oct(fields.shift),
124         prefix: fields.shift,
126         empty: false
127   end
129   def self.strict_oct(str)
130     str.strip!
131     return str.oct if /\A[0-7]*\z/.match?(str)
133     raise ArgumentError, "#{str.inspect} is not an octal string"
134   end
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)
143     strict_oct(str)
144   end
146   ##
147   # Creates a new TarHeader using +vals+
149   def initialize(vals)
150     unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
151       raise ArgumentError, ":name, :size, :prefix and :mode required"
152     end
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"
161     @mode = vals[:mode]
162     @mtime = vals[:mtime] || 0
163     @name = vals[:name]
164     @prefix = vals[:prefix]
165     @size = vals[:size]
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]
173   end
175   EMPTY = new({ # :nodoc:
176     checksum: 0,
177     gname: "",
178     linkname: "",
179     magic: "",
180     mode: 0,
181     name: "",
182     prefix: "",
183     size: 0,
184     uname: "",
185     version: 0,
187     empty: true,
188   }).freeze
189   private_constant :EMPTY
191   ##
192   # Is the tar entry empty?
194   def empty?
195     @empty
196   end
198   def ==(other) # :nodoc:
199     self.class === other &&
200       @checksum == other.checksum &&
201       @devmajor == other.devmajor &&
202       @devminor == other.devminor &&
203       @gid      == other.gid      &&
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 &&
213       @uid      == other.uid      &&
214       @uname    == other.uname    &&
215       @version  == other.version
216   end
218   def to_s # :nodoc:
219     update_checksum
220     header
221   end
223   ##
224   # Updates the TarHeader's checksum
226   def update_checksum
227     header = header " " * 8
228     @checksum = oct calculate_checksum(header), 6
229   end
231   private
233   def calculate_checksum(header)
234     header.sum(0)
235   end
237   def header(checksum = @checksum)
238     header = [
239       name,
240       oct(mode, 7),
241       oct(uid, 7),
242       oct(gid, 7),
243       oct(size, 11),
244       oct(mtime, 11),
245       checksum,
246       " ",
247       typeflag,
248       linkname,
249       magic,
250       oct(version, 2),
251       uname,
252       gname,
253       oct(devmajor, 7),
254       oct(devminor, 7),
255       prefix,
256     ]
258     header = header.pack PACK_FORMAT
260     header.ljust 512, "\0"
261   end
263   def oct(num, len)
264     format("%0#{len}o", num)
265   end