class Archiverb::Tar

GNU tar implementation @see en.wikipedia.org/wiki/Tar_(file_format) @see www.gnu.org/software/tar/manual/html_node/Standard.html @see www.subspacefield.org/~vax/tar_format.html

Constants

AREGTYPE
BLKTYPE
CHRTYPE
CONTTYPE
DIRTYPE
FIFOTYPE
LNKTYPE
OLDGNU_MAGIC
REGTYPE
SYMTYPE
TMAGIC
TVERSION
XGLTYPE
XHDTYPE

Private Instance Methods

chksum(header) click to toggle source

The checksum is calculated by taking the sum of the unsigned byte values of the header block with the eight checksum bytes taken to be ascii spaces (decimal value 32). It is stored as a six digit octal number with leading zeroes followed by a NUL and then a space.

# File lib/archiverb/tar.rb, line 209
def chksum(header)
  sprintf("%.6o\0 ", (header[0..147] + header[156..500]).each_byte.inject(256) { |s,b| s+b })
end
next_header(io) click to toggle source
# File lib/archiverb/tar.rb, line 106
def next_header(io)
  return nil if io.eof?
  if (raw = io.read(512)).strip == ""
    return nil if(raw = io.read(512)).strip == ""
  end # raw.strip == ""

  header            = {}
  header[:name]     = raw[0..99].strip
  check             = chksum(raw)
  header[:chksum]   = raw[148..155]
  raise WrongChksum.new("#{header[:name]}: #{header[:chksum]} expected to be #{check}") unless header[:chksum] == check
  header[:mode]     = raw[100..107].to_i(8)
  header[:uid]      = Integer(raw[108..115].strip)
  header[:gid]      = Integer(raw[116..123].strip)
  header[:size]     = Integer(raw[124..135].strip)
  header[:mtime]    = raw[136..147].strip
  header[:mtime]    = "0#{header[:mtime]}" if header[:mtime].length == 11
  header[:mtime]    = Time.at(Integer(header[:mtime]))
  # @todo check for XHDTYPE, or XGLTYPE
  header[:ftype]     = stat_type(raw[156])
  header[:readlink]  = raw[157..256].strip
  header[:magic]     = raw[257..262].strip
  header[:version]   = raw[263..264].strip
  header[:uname]     = raw[265..296].strip
  header[:gname]     = raw[297..328].strip
  header[:dev_major] = raw[329..336].strip
  header[:dev_minor] = raw[337..344].strip
  header[:prefix]    = raw[345..500].strip
  header.merge!(pull_ustar(raw)) if header[:magic] == TMAGIC
  header
end
pull_ustar(raw) click to toggle source
# File lib/archiverb/tar.rb, line 188
def pull_ustar(raw)
  header              = {}
  header[:prefix]     = raw[345].strip
  header[:fill2]      = raw[346].strip
  header[:fill3]      = raw[347..354].strip
  header[:isextended] = raw[355]
  header[:sparse]     = raw[356..451].strip
  header[:realsize]   = raw[452..463].strip
  header[:offset]     = raw[464..475].strip
  header[:atime]      = raw[476..487].strip
  header[:ctime]      = raw[488..499].strip
  header[:mfill]      = raw[500..507].strip
  header[:xmagic]     = raw[508..511].strip
  header
end
read_file(header, io) click to toggle source
# File lib/archiverb/tar.rb, line 180
def read_file(header, io)
  io.read(header[:size]).tap do |raw|
    if (diff = header[:size] % 512) != 0
      io.read(512 - diff)
    end
  end # raw
end
stat_type(bit) click to toggle source
# File lib/archiverb/tar.rb, line 159
def stat_type(bit)
  case bit
  when REGTYPE, AREGTYPE
    'file'
  when LNKTYPE, SYMTYPE
    'link'
  when CHRTYPE
    'characterSpecial'
  when BLKTYPE
    'blockSpecial'
  when DIRTYPE
    'directory'
  when FIFOTYPE
    'fifo'
  when CONTTYPE, XHDTYPE, XGLTYPE
    'unknown'
  else
    warn "file type #{bit} is not supported; treating it as a regular file"
    'regular'
  end
end
tar_type(stat) click to toggle source
# File lib/archiverb/tar.rb, line 138
def tar_type(stat)
  case stat.ftype
  when 'file'
    REGTYPE
  when 'directory'
    DIRTYPE
  when 'characterSpecial'
    CHRTYPE
  when 'blockSpecial'
    BLKTYPE
  when 'fifo'
    FIFOTYPE
  when 'link'
    #LNKTYPE
    SYMTYPE
  else
    warn "file type: #{stat.ftype} is not supported; treating it as a regular file"
    REGTYPE
  end
end
write_to(io) click to toggle source
# File lib/archiverb/tar.rb, line 28
def write_to(io)
  @files.each do |name, file|
    if name.length > 100
      name, prefix = name[0..99], name[100..-1]
      if prefix > 155
        raise ArgumentError.new("file name cannot exceed 255 characters: #{name}#{prefix}")
      end
    else
      prefix = ""
    end
    header = "#{name}" + ("\0" * (100 - name.length))
    # @todo double check the modes on links
    # input header: data/henryIV.txt0000755000076500000240000000000011706250470015332 2heneryIV.txtustar  calebstaff
    # output header: data/henryIV.txt0050423000076500000240000000001411706305252015333 2heneryIV.txtustar  calebstaff

    # offset 100
    header += sprintf("%.7o\0", file.mode)
    # offset 108
    header += sprintf("%.7o\0", file.uid)
    # offset 116
    header += sprintf("%.7o\0", file.gid)
    # offset 124
    header += sprintf("%.11o\0", file.size)
    # offset 136
    header += sprintf("%.11o\0", file.mtime.to_i)
    # offset 148
    header += " " * 8 # write 8 blanks for the checksum, we'll replace it later

    # offset 156
    if [LNKTYPE, SYMTYPE].include?(type = tar_type(file.stat))
      header += sprintf("%.1o", type)
      raise ArgumentError.new("#{name} link type files' stat objects must contain a readlink") if file.stat.readlink.nil?
      # offset 157
      header += "#{file.stat.readlink}\0" + ("\0" * (99 - file.stat.readlink.length))
    else
      type = DIRTYPE if name[-1] == "/"
      header += sprintf("%.1o", type)
      # offset 157
      header += "\0"*100
    end

    # offset 257
    header += OLDGNU_MAGIC

    uname = file.stat.uname || Etc.getpwuid(file.uid).name
    gname = file.stat.gname || Etc.getgrgid(file.gid).name
    # offset 265
    header += "#{uname}\0" + ("\0" * (31 - uname.length))
    # offset 297
    header += "#{gname}\0" + ("\0" * (31 - gname.length))

    # offset 329
    if type == CHRTYPE || type ==BLKTYPE
      header += sprintf("%.7o\0", file.stat.dev_major)
      header += sprintf("%.7o\0", file.stat.dev_minor)
    else
      header += "\0" * 16
    end

    # offset 345
    header += "#{prefix}" + ("\0" * (155 - (prefix || "").length))

    # offset 500
    header += "\0" * 12

    header = header[0..147] + chksum(header) + header[156..-1]
    io.write(header)
    io.write(file.read).tap do |len|
      unless (overflow = len % 512) == 0
        io.write("\0" * (512 - (overflow)))
      end
    end
  end # name, file

  io.write("\0" * 1024)
  self
end