ManageIQ/manageiq-smartstate

View on GitHub
lib/fs/xfs/inode.rb

Summary

Maintainability
D
1 day
Test Coverage
F
35%
require 'binary_struct'
require 'memory_buffer'
require 'more_core_extensions/all'
require 'fs/xfs/superblock'
require 'fs/xfs/bmap_btree_record'
require 'fs/xfs/bmap_btree_block'
require 'fs/xfs/bmap_btree_root_node'

module XFS
  TIMESTAMP = BinaryStruct.new([
    'I',  'seconds',           # timestamp seconds
    'I',  'nanoseconds',       # timestamp nanoseconds
  ])

  INODE = BinaryStruct.new([
    'S>',  'magic',             # Inode Magic Number
    'S>',  'file_mode',         # Mode and Type of file
    'C',   'version',           # Inode Version
    'C',   'format',            # Format of Data Fork Data
    'S>',  'old_num_links',     # Old Number of Links to File
    'I>',  'uid',               # Owner's User Id
    'I>',  'gid',               # Owner's Group Id
    'I>',  'num_links',         # Number of Links to File
    'S>',  'projid_low',        # Lower Part of Owner's Project Id
    'S>',  'projid_high',       # Higher Part of Owner's Project Id
    'a6',  'pad',               # Unused, Zeroed Space
    'S>',  'flush_iterator',    # Incremented on Flush
    'I>',  'atime_secs',        # time last accessed seconds
    'I>',  'atime_nsecs',       # time last accessed nanoseconds
    'I>',  'mtime_secs',        # time last modified seconds
    'I>',  'mtime_nsecs',       # time last modified nanoseconds
    'I>',  'ctime_secs',        # time created / inode modified seconds
    'I>',  'ctime_nsecs',       # time created / inode modified nanoseconds
    'Q>',  'size',              # number of bytes in file
    'Q>',  'nblocks',           # Number of direct & btree blocks used
    'I>',  'extent_size',       # Basic/Minimum extent size for file
    'I>',  'num_extents',       # Number of extents in data fork
    'S>',  'attr_num_extents',  # Number of extents in attribute fork
    'C',   'attr_fork_offset',  # Attribute Fork Offset, <<3 for 64b align
    'c',   'attr_fork_format',  # Format of Attribute Fork's Data
    'I>',  'dmig_event_mask',   # DMIG event mask
    'S>',  'dmig_state_info',   # DMIG state info
    'S>',  'flags',             # random flags, XFS_DIFLAG_...
    'I>',  'gen_num',           # generation number
    'I>',  'next_unlinked',     # agi unlinked list ptr
  ])

  EXTENDED_INODE = BinaryStruct.new([
    'S>',  'magic',             # Inode Magic Number
    'S>',  'file_mode',         # Mode and Type of file
    'C',   'version',           # Inode Version
    'C',   'format',            # Format of Data Fork Data
    'S>',  'old_num_links',     # Old Number of Links to File
    'I>',  'uid',               # Owner's User Id
    'I>',  'gid',               # Owner's Group Id
    'I>',  'num_links',         # Number of Links to File
    'S>',  'projid_low',        # Lower Part of Owner's Project Id
    'S>',  'projid_high',       # Higher Part of Owner's Project Id
    'a6',  'pad',               # Unused, Zeroed Space
    'S>',  'flush_iterator',    # Incremented on Flush
    'I>',  'atime_secs',        # time last accessed seconds
    'I>',  'atime_nsecs',       # time last accessed nanoseconds
    'I>',  'mtime_secs',        # time last modified seconds
    'I>',  'mtime_nsecs',       # time last modified nanoseconds
    'I>',  'ctime_secs',        # time created / inode modified seconds
    'I>',  'ctime_nsecs',       # time created / inode modified nanoseconds
    'Q>',  'size',              # number of bytes in file
    'Q>',  'nblocks',           # Number of direct & btree blocks used
    'I>',  'extent_size',       # Basic/Minimum extent size for file
    'I>',  'num_extents',       # Number of extents in data fork
    'S>',  'attr_num_extents',  # Number of extents in attribute fork
    'C',   'attr_fork_offset',  # Attribute Fork Offset, <<3 for 64b align
    'c',   'attr_fork_format',  # Format of Attribute Fork's Data
    'I>',  'dmig_event_mask',   # DMIG event mask
    'S>',  'dmig_state_info',   # DMIG state info
    'S>',  'flags',             # random flags, XFS_DIFLAG_...
    'I>',  'gen_num',           # generation number
    'I>',  'next_unlinked',     # agi unlinked list ptr
    'I>',  'crc',               # CRC of the inode
    'Q>',  'change_count',      # number of attribute changes
    'Q>',  'lsn',               # flush sequence
    'Q>',  'flags2',            # more random flags
    'a16', 'pad2',              # more padding for future expansion
    'I>',  'crtime_secs',       # time created seconds
    'I>',  'crtime_nsecs',      # time created nanoseconds
    'Q>',  'inode_number',      # inode number
    'a16', 'uuid',              # UUID of the filesystem
  ])

  SIZEOF_INODE = INODE.size
  SIZEOF_EXTENDED_INODE = EXTENDED_INODE.size

  # ////////////////////////////////////////////////////////////////////////////
  # // Class.

  class Inode
    MAX_READ              = 4_294_967_296
    XFS_DINODE_MAGIC             = 0x494e

    # Bits 0 to 8 of file mode.
    PF_O_EXECUTE  = 0x0001  # owner execute
    PF_O_WRITE    = 0x0002  # owner write
    PF_O_READ     = 0x0004  # owner read
    PF_G_EXECUTE  = 0x0008  # group execute
    PF_G_WRITE    = 0x0010  # group write
    PF_G_READ     = 0x0020  # group read
    PF_U_EXECUTE  = 0x0040  # user execute
    PF_U_WRITE    = 0x0080  # user write
    PF_U_READ     = 0x0100  # user read

    # For accessor convenience.
    MSK_PERM_OWNER = (PF_O_EXECUTE | PF_O_WRITE | PF_O_READ)
    MSK_PERM_GROUP = (PF_G_EXECUTE | PF_G_WRITE | PF_G_READ)
    MSK_PERM_USER  = (PF_U_EXECUTE | PF_U_WRITE | PF_U_READ)

    # Bits 12 to 15 of file mode.
    FM_FIFO       = 0x1000  # fifo device (pipe)
    FM_CHAR       = 0x2000  # char device
    FM_DIRECTORY  = 0x4000  # directory
    FM_BLOCK_DEV  = 0x6000  # block device
    FM_FILE       = 0x8000  # regular file
    FM_SYM_LNK    = 0xa000  # symbolic link
    FM_SOCKET     = 0xc000  # socket device

    # Values our callers may know
    FT_UNKNOWN    = 0
    FT_FILE       = 1
    FT_DIRECTORY  = 2
    FT_CHAR       = 3
    FT_BLOCK      = 4
    FT_FIFO       = 5
    FT_SOCKET     = 6
    FT_SYM_LNK    = 7

    FILE_MODE_TO_FILE_TYPE_LOOKUP_TABLE = {
      FM_FIFO      => FT_FIFO,
      FM_CHAR      => FT_CHAR,
      FM_DIRECTORY => FT_DIRECTORY,
      FM_BLOCK_DEV => FT_BLOCK,
      FM_FILE      => FT_FILE,
      FM_SYM_LNK   => FT_SYM_LNK,
      FM_SOCKET    => FT_SOCKET
    }

    # For accessor convenience.
    MSK_FILE_MODE = 0xf000
    MSK_IS_DEV    = (FM_FIFO | FM_CHAR | FM_BLOCK_DEV | FM_SOCKET)

    # For Data Fork Data Format
    XFS_DINODE_FMT_DEV     = 0  # Device Type
    XFS_DINODE_FMT_LOCAL   = 1  # Bulk Data
    XFS_DINODE_FMT_EXTENTS = 2  # xfs_bmbt_rec
    XFS_DINODE_FMT_BTREE   = 3  # xfs_bmdr_block
    XFS_DINODE_FMT_UUID    = 4  # uuid

    XFS_DATA_FORK = 0
    XFS_ATTR_FORK = 1

    def dinode_good_version(version)
      version >= 1 && version <= 3
    end

    def dinode_size(version)
      if version == 3
        SIZEOF_EXTENDED_INODE
      else
        SIZEOF_INODE
      end
    end

    def dfork_q
      @in['attr_fork_offset'] != 0
    end

    def dfork_boff
      @in['attr_fork_offset'] << 3
    end

    def dfork_dsize
      dfork_q ? dfork_boff : litino
    end

    def dfork_asize
      dfork_q ? litino - dfork_boff : 0
    end

    def dfork_size(which_fork)
      which_fork == XFS_DATA_FORK ? dfork_dsize : dfork_asize
    end

    def dfork_dptr
      start = @offset + dinode_size(@version)
      @disk_buffer[start..start + @sb.inode_size]
    end

    def dfork_aptr
      dfork_dptr + dfork_boff
    end

    def litino
      @sb.inode_size - dinode_size(@version)
    end

    attr_reader :mode, :flags, :length, :disk_buffer, :version, :inode_number, :sb, :data_method, :in
    attr_accessor :data_fork, :attribute_fork

    def valid_inode?
      $log.error "XFS::Inode: Bad Magic # inode #{@inode_number}" unless @in['magic'] == XFS_DINODE_MAGIC
      raise "XFS::Inode: Invalid Magic Number for inode #{@inode_number}"  unless @in['magic'] == XFS_DINODE_MAGIC
      raise "XFS::Inode: Invalid Inode Version for inode #{@inode_number}" unless dinode_good_version(@in['version'])
      true
    end

    def inode_format
      if @format == XFS_DINODE_FMT_LOCAL
        data_method    = :local
      elsif @format == XFS_DINODE_FMT_EXTENTS
        data_method    = :extents
      else
        data_method    = :btree
      end
      data_method
    end

    def initialize(buffer, offset, superblock, inode_number)
      raise "XFS::Inode: Nil buffer for inode #{inode_number}" if buffer.nil?
      @sb               = superblock
      @inode_number     = inode_number
      @offset           = offset
      if @sb.inode_size < SIZEOF_EXTENDED_INODE
        @in             = INODE.decode(buffer[offset..(offset + SIZEOF_INODE)])
      else
        @in             = EXTENDED_INODE.decode(buffer[offset..(offset + SIZEOF_EXTENDED_INODE)])
      end
      valid_inode? || return
      rewind
      @disk_buffer      = buffer
      @mode             = @in['file_mode']
      @flags            = @in['flags']
      @version          = @in['version']
      @length           = @in['size']
      @format           = @in['format']
      @block_offset     = 1
      @data_method      = inode_format
    end

    # ////////////////////////////////////////////////////////////////////////////
    # // Method for data access
    def rewind
      @pos = 0
    end

    def seek(offset, method = IO::SEEK_SET)
      @pos = case method
             when IO::SEEK_SET then offset
             when IO::SEEK_CUR then @pos + offset
             when IO::SEEK_END then @length - offset
             end
      @pos = 0           if @pos < 0
      @pos = @length if @pos > @length
      @pos
    end

    def read(nbytes = @length)
      raise "XFS::Inode.read: Can't read 4G or more at a time (use a smaller read size)" if nbytes >= MAX_READ
      return nil if @pos >= @length

      nbytes = @length - @pos if @pos + nbytes > @length
      return read_short_form(nbytes) if @data_method == :local

      # get data.
      start_block, start_byte, nblocks = pos_to_block(@pos, nbytes)
      out = read_blocks(start_block, nblocks)
      @pos += nbytes
      out[start_byte, nbytes]
    end

    def write(buf, _len = buf.length)
      raise "XFS::Inode.write: Write functionality is not yet supported on XFS."
    end

    # ////////////////////////////////////////////////////////////////////////////
    # // Class helpers & accessors.

    def directory?
      mode_set?(FM_DIRECTORY)
    end

    def file?
      mode_set?(FM_FILE)
    end

    def device?
      (@mode & MSK_IS_DEV) > 0
    end

    def symlink?
      mode_set?(FM_SYM_LNK)
    end

    def access_time
      @access_time ||= Time.at(@in['atime_secs'])
    end

    # For compatibility with other filesystem methods
    def aTime
      access_time
    end

    def create_time
      @create_time ||= Time.at(@in['ctime_secs'])
    end

    # For compatibility with other filesystem methods
    def cTime
      create_time
    end

    def modification_time
      @modification_time ||= Time.at(@in['mtime_secs'])
    end

    # For compatibility with other filesystem methods
    def mTime
      modification_time
    end

    def permissions
      @permissions ||= @in['file_mode'] & (MSK_PERM_OWNER | MSK_PERM_GROUP | MSK_PERM_USER)
    end

    def owner_permissions
      @owner_permissions ||= @in['file_mode'] & MSK_PERM_OWNER
    end

    def group_permissions
      @group_permissions ||= @in['file_mode'] & MSK_PERM_GROUP
    end

    def user_permissions
      @user_permissions ||= @in['file_mode'] & MSK_PERM_USER
    end

    # ////////////////////////////////////////////////////////////////////////////
    # // Utility functions.

    def file_mode_to_file_type
      FILE_MODE_TO_FILE_TYPE_LOOKUP_TABLE[@mode & MSK_FILE_MODE]
    end

    def mode_set?(bit)
      (@mode & bit) == bit
    end

    def flag_set?(bit)
      (@flags & bit) == bit
    end

    def dump
      out = "\#<#{self.class}:0x#{format('%08x', object_id)}>\n"
      out += "Inode Number : #{@inode_number}\n"
      out += "File mode    : 0x#{format('%04x', @in['file_mode'])}\n"
      out += "UID          : #{@in['uid']}\n"
      out += "Size         : #{@in['size']}\n"
      out += "ATime Secs/NSecs: #{@in['atime_secs']}/#{@in['atime_nsecs']}\n"
      out += "CTime Secs/NSecs: #{@in['ctime_secs']}/#{@in['ctime_nsecs']}\n"
      out += "MTime Secs/NSecs: #{@in['mtime_secs']}/#{@in['mtime_nsecs']}\n"
      out += "GID          : #{@in['gid']}\n"
      out += "Link count   : #{@in['num_links']}\n"
      out += "Old Link cnt : #{@in['old_num_links']}\n"
      out += "Block count  : #{@in['nblocks']}\n"
      out += "Extent size  : #{@in['extent_size']}\n"
      out += "Num extents  : #{@in['num_extents']}\n"
      out += "Data Fork Fmt : #{@data_method}\n"
      out += "Attr Fork Exts: #{@in['attr_num_extents']}\n"
      out += "Attr Fork Off : #{@in['attr_fork_offset']}\n"
      out += "Attr Fork Fmt : #{@in['attr_fork_format']}\n"
      out += "Flags        : #{format('%04x', @in['flags'])}\n"
      out += "Version      : #{@in['version']}\n"
      out += "Flush Iter   : #{@in['flush_iterator']}\n"
      out += "Generation   : #{@in['gen_num']}\n"
      out
    end

    private

    def read_short_form(len)
      unless self.directory? || self.symlink?
        raise "XFS::Inode.read: Invalid ShortForm Directory for inode #{@inode_number}"
      end
      if @pos + len > @sb.inode_size
        raise "XFS::Inode.read_short_form: Invalid length #{len} for Shortform Inode #{@inode_number}"
      end
      fork = dfork_dptr
      data = fork[@pos..(@pos + len - 1)]
      @pos += len
      data
    end

    # NB: pos is 0-based, while len is 1-based
    def pos_to_block(pos, len)
      start_block, start_byte = pos.divmod(@sb.block_size)
      end_block, _end_byte = (pos + len - 1).divmod(@sb.block_size)
      nblocks = end_block - start_block + 1
      return start_block, start_byte, nblocks
    end

    def read_blocks(startBlock, nblocks = 1)
      out = MemoryBuffer.create(nblocks * @sb.block_size)
      dbp_len = data_block_pointers.length
      raise "XFS::Inode.read_blocks: startBlock=<#{startBlock}> is greater than #{dbp_len}" if startBlock > dbp_len - 1
      1.upto(nblocks) do |i|
        block_index = startBlock + i - 1
        dbp_len = data_block_pointers.length
        if block_index > dbp_len - 1
          raise "XFS::Inode.read_blocks: block_index=<#{block_index}> is greater than #{dbp_len}"
        end
        block = data_block_pointers[block_index]
        data  = @sb.get_block(block)
        out[(i - 1) * @sb.block_size, @sb.block_size] = data
      end
      out
    end

    #
    # This method is used for both extents and BTree leaf nodes
    #
    def bmap_btree_record_to_block_pointers(record, block_pointers_length)
      block_pointers = []
      # Fill in the missing blocks with 0-blocks
      block_pointers << 0 while (block_pointers_length + block_pointers.length) < record.start_offset
      1.upto(record.block_count) { |i| block_pointers << record.start_block + i - 1 }
      @block_offset += record.block_count
      block_pointers
    end

    def expected_blocks
      @expected_blocks ||= begin
        quotient, remainder = @length.divmod(@sb.block_size)
        quotient + ((remainder > 0) ? 1 : 0)
      end
    end

    def block_pointers_via_bmap_btree_block_node(btree_block, data, block_pointers_length)
      block_pointers = []
      return block_pointers unless (block_pointers_length + block_pointers.length) < expected_blocks
      maximum_records = (@sb.block_size - btree_block.header_size) / SIZEOF_BMAP_BTREE_ROOT_NODE_ENTRIES
      return if maximum_records == 0
      offset_size  = SIZEOF_BMAP_BTREE_ROOT_NODE_OFFSET
      block_size   = SIZEOF_BMAP_BTREE_ROOT_NODE_BLOCK
      1.upto(btree_block.number_records) do |i|
        start      = maximum_records * offset_size + (i - 1) * block_size
        block      = (data[start..start + block_size]).unpack('Q>').shift
        agbno      = @sb.fsb_to_agbno(block)
        agno       = @sb.fsb_to_agno(block)
        real_block = sb.agbno_to_real_block(agno, agbno)
        block_pointers.concat block_pointers_via_bmap_btree_block(real_block,
                                                                  block_pointers.length + block_pointers_length)
      end
      block_pointers
    end

    def block_pointers_via_bmap_btree_block_leaf(data, block_pointers_length, number_records)
      block_pointers = []
      return block_pointers unless (block_pointers_length + block_pointers.length) < expected_blocks
      1.upto(number_records) do |i|
        bmap_btree_record = BmapBTreeRecord.new(data[(SIZEOF_BMAP_BTREE_REC * (i - 1))..(SIZEOF_BMAP_BTREE_REC * i)],
                                                @sb)
        break if @sb.fsb_to_b(bmap_btree_record.start_offset) >= DirectoryEntry::XFS_DIR2_LEAF_OFFSET
        if (block_pointers_length + block_pointers.length) < expected_blocks
          block_pointers.concat bmap_btree_record_to_block_pointers(bmap_btree_record,
                                                                    block_pointers.length + block_pointers_length)
        end
      end
      block_pointers
    end

    def block_pointers_via_bmap_btree_block(block_number, block_pointers_length)
      block_pointers = []
      if block_pointers_length < expected_blocks
        data           = @sb.get_block(block_number)
        btree_block    = BmapBTreeBlock.new(data, @sb)
        if btree_block.level == 0
          block_pointers.concat block_pointers_via_bmap_btree_block_leaf(data[btree_block.header_size..-1],
                                                                         block_pointers.length + block_pointers_length,
                                                                         btree_block.number_records)
        else
          block_pointers.concat block_pointers_via_bmap_btree_block_node(btree_block, data[btree_block.header_size..-1],
                                                                         block_pointers.length + block_pointers_length)
        end
      end
      block_pointers
    end

    def block_pointers_via_btree
      block_pointers = []
      fork = dfork_dptr
      root_node = BmapBTreeRootNode.new(fork, self)
      root_node.blocks.each do |block_number|
        block_pointers.concat block_pointers_via_bmap_btree_block(block_number, block_pointers.length)
      end
      block_pointers
    end

    def extent_to_block_pointers(extent, bplen)
      block_pointers = []
      # Fill in the missing blocks with 0-blocks
      block_pointers << 0 while (bplen + block_pointers.length) < extent.start_offset
      1.upto(extent.block_count) { |i| block_pointers << extent.start_block + i - 1 }
      @block_offset += extent.block_count
      block_pointers
    end

    def block_pointers_via_extents
      block_pointers = []
      fork = dfork_dptr
      extent_count = @in['num_extents']
      return block_pointers if extent_count == 0
      1.upto(extent_count) do |i|
        bmap_btree_record = BmapBTreeRecord.new(fork[(SIZEOF_BMAP_BTREE_REC * (i - 1))..(SIZEOF_BMAP_BTREE_REC * i)],
                                                @sb)
        #
        # The following test is to weed out Leaf Metadata Blocks that have no directory content
        #
        break if @sb.fsb_to_b(bmap_btree_record.start_offset) >= DirectoryEntry::XFS_DIR2_LEAF_OFFSET
        block_pointers.concat extent_to_block_pointers(bmap_btree_record, block_pointers.length)
      end
      block_pointers
    end

    def read_block_pointers(block)
      @sb.get_block(block).unpack('L*')
    end

    def data_block_pointers
      if @data_block_pointers.nil?
        @data_block_pointers = block_pointers_via_extents          if @data_method == :extents
        @data_block_pointers = block_pointers_via_btree            if @data_method == :btree
        dbp_len = @data_block_pointers.length
        if expected_blocks != dbp_len
          raise "XFS::Inode.block_pointers: Block Pointers <#{dbp_len}> does not match Expected <#{expected_blocks}>"
        end
      end
      @data_block_pointers
    end
  end # Class Inode
end # Module XFS