lib/fs/xfs/inode.rb
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