class Minitar
  # Implements the POSIX tar header as a Ruby class. The structure of
  # the POSIX tar header is:
  #
  #   struct tarfile_entry_posix
  #   {                      //                               pack/unpack
  #      char name[100];     // ASCII (+ Z unless filled)     a100/Z100
  #      char mode[8];       // 0 padded, octal, null         a8  /A8
  #      char uid[8];        // 0 padded, octal, null         a8  /A8
  #      char gid[8];        // 0 padded, octal, null         a8  /A8
  #      char size[12];      // 0 padded, octal, null         a12 /A12
  #      char mtime[12];     // 0 padded, octal, null         a12 /A12
  #      char checksum[8];   // 0 padded, octal, null, space  a8  /A8
  #      char typeflag[1];   // see below                     a   /a
  #      char linkname[100]; // ASCII + (Z unless filled)     a100/Z100
  #      char magic[6];      // "ustar\0"                     a6  /A6
  #      char version[2];    // "00"                          a2  /A2
  #      char uname[32];     // ASCIIZ                        a32 /Z32
  #      char gname[32];     // ASCIIZ                        a32 /Z32
  #      char devmajor[8];   // 0 padded, octal, null         a8  /A8
  #      char devminor[8];   // 0 padded, octal, null         a8  /A8
  #      char prefix[155];   // ASCII (+ Z unless filled)     a155/Z155
  #   };
  #
  # The #typeflag is one of several known values.
  #
  # POSIX indicates that "A POSIX-compliant implementation must treat any
  # unrecognized typeflag value as a regular file."
  class PosixHeader
    BLOCK_SIZE = 512
    MAGIC_BYTES = "ustar".freeze

    GNU_EXT_LONG_LINK = "././@LongLink"

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

    # All fields available in a POSIX tar(1) header.
    FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze

    FIELDS.each do |f|
      attr_reader f.to_sym unless f.to_sym == :name
    end

    # The name of the file. By default, limited to 100 bytes. Required. May be
    # longer (up to BLOCK_SIZE bytes) if using the GNU long name tar extension.
    attr_accessor :name

    # The pack format passed to Array#pack for encoding a header.
    HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155".freeze
    # The unpack format passed to String#unpack for decoding a header.
    HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155".freeze

    class << self
      # Creates a new PosixHeader from a data stream.
      def from_stream(stream)
        from_data(stream.read(BLOCK_SIZE))
      end

      # Creates a new PosixHeader from a data stream. Deprecated; use
      # PosixHeader.from_stream instead.
      def new_from_stream(stream)
        warn "#{__method__} has been deprecated; use from_stream instead."
        from_stream(stream)
      end

      # Creates a new PosixHeader from a BLOCK_SIZE-byte data buffer.
      def from_data(data)
        fields = data.unpack(HEADER_UNPACK_FORMAT)
        name = fields.shift
        mode = fields.shift.oct
        uid = fields.shift.oct
        gid = fields.shift.oct
        size = strict_oct(fields.shift)
        mtime = fields.shift.oct
        checksum = fields.shift.oct
        typeflag = fields.shift
        linkname = fields.shift
        magic = fields.shift
        version = fields.shift.oct
        uname = fields.shift
        gname = fields.shift
        devmajor = fields.shift.oct
        devminor = fields.shift.oct
        prefix = fields.shift

        empty = !data.each_byte.any?(&:nonzero?)

        new(
          name: name,
          mode: mode,
          uid: uid,
          gid: gid,
          size: size,
          mtime: mtime,
          checksum: checksum,
          typeflag: typeflag,
          magic: magic,
          version: version,
          uname: uname,
          gname: gname,
          devmajor: devmajor,
          devminor: devminor,
          prefix: prefix,
          empty: empty,
          linkname: linkname
        )
      end

      private

      def strict_oct(string)
        return string.oct if /\A[0-7 ]*\z/.match?(string)
        raise ArgumentError, "#{string.inspect} is not a valid octal string"
      end
    end

    # Creates a new PosixHeader. A PosixHeader cannot be created unless
    # +name+, +size+, +prefix+, and +mode+ are provided.
    def initialize(v)
      REQUIRED_FIELDS.each do |f|
        raise ArgumentError, "Field #{f} is required." unless v.key?(f)
      end

      v[:mtime] = v[:mtime].to_i
      v[:checksum] ||= ""
      v[:typeflag] ||= "0"
      v[:magic] ||= MAGIC_BYTES
      v[:version] ||= "00"

      FIELDS.each do |f|
        instance_variable_set(:"@#{f}", v[f])
      end

      @empty = v[:empty]
    end

    # Indicates if the header was an empty header.
    def empty?
      @empty
    end

    # Indicates if the header has a valid magic value.
    def valid?
      empty? || @magic == MAGIC_BYTES
    end

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

    # A string representation of the header.
    def to_s
      update_checksum
      header(@checksum)
    end
    alias_method :to_str, :to_s

    # Update the checksum field.
    def update_checksum
      hh = header(" " * 8)
      @checksum = oct(calculate_checksum(hh), 6)
    end

    private

    def oct(num, len)
      if num.nil?
        "\0" * (len + 1)
      else
        "%0#{len}o" % num
      end
    end

    def calculate_checksum(hdr)
      hdr.unpack("C*").inject { |a, e| a + e }
    end

    def header(chksum)
      arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
        oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
        uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
      str = arr.pack(HEADER_PACK_FORMAT)
      str + "\0" * ((BLOCK_SIZE - str.bytesize) % BLOCK_SIZE)
    end

    ##
    # :attr_reader: size
    # The size of the file. Required.

    ##
    # :attr_reader: prefix
    # The prefix of the file; the path before #name. Limited to 155 bytes.
    # Required.

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

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

    ##
    # :attr_reader: uname
    # The user name of the Unix owner of the file.

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

    ##
    # :attr_reader: gname
    # The group name of the Unix owner of the file.

    ##
    # :attr_reader: mtime
    # The modification time of the file in epoch seconds. Stored as an
    # octal integer.

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

    ##
    # :attr_reader: typeflag
    # The type of record in the file.
    #
    # +0+::  Regular file. NULL should be treated as a synonym, for compatibility
    #        purposes.
    # +1+::  Hard link.
    # +2+::  Symbolic link.
    # +3+::  Character device node.
    # +4+::  Block device node.
    # +5+::  Directory.
    # +6+::  FIFO node.
    # +7+::  Reserved.

    ##
    # :attr_reader: linkname
    # The name of the link stored. Not currently used.

    ##
    # :attr_reader: magic
    # Always "ustar\0".

    ##
    # :attr_reader: version
    # Always "00"

    ##
    # :attr_reader: devmajor
    # The major device ID. Not currently used.

    ##
    # :attr_reader: devminor
    # The minor device ID. Not currently used.
  end
end
