macOS '.DS_Store' format: Ruby parsing library

Apple macOS '.DS_Store' file format.

File extension

DS_Store

KS implementation details

License: MIT
Minimal Kaitai Struct required: 0.9

References

This page hosts a formal specification of macOS '.DS_Store' format using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Usage

Runtime library

All parsing code for Ruby generated by Kaitai Struct depends on the Ruby runtime library. You have to install it before you can parse data.

The Ruby runtime library can be installed from RubyGems:

gem install kaitai-struct

Code

Parse a local file and get structure in memory:

data = DsStore.from_file("path/to/local/file.DS_Store")

Or parse structure from a string of bytes:

bytes = "\x00\x01\x02..."
data = DsStore.new(Kaitai::Struct::Stream.new(bytes))

After that, one can get various attributes from the structure by invoking getter methods like:

data.block_address_mask # => Bitmask used to calculate the position and the size of each block
of the B-tree from the block addresses.

Ruby source code to parse macOS '.DS_Store' format

ds_store.rb

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

require 'kaitai/struct/struct'

unless Gem::Version.new(Kaitai::Struct::VERSION) >= Gem::Version.new('0.9')
  raise "Incompatible Kaitai Struct Ruby API: 0.9 or later is required, but you have #{Kaitai::Struct::VERSION}"
end


##
# Apple macOS '.DS_Store' file format.
# @see https://en.wikipedia.org/wiki/.DS_Store Source
# @see https://metacpan.org/dist/Mac-Finder-DSStore/view/DSStoreFormat.pod Source
# @see https://0day.work/parsing-the-ds_store-file-format/ Source
class DsStore < Kaitai::Struct::Struct
  def initialize(_io, _parent = nil, _root = self)
    super(_io, _parent, _root)
    _read
  end

  def _read
    @alignment_header = @_io.read_bytes(4)
    raise Kaitai::Struct::ValidationNotEqualError.new([0, 0, 0, 1].pack('C*'), alignment_header, _io, "/seq/0") if not alignment_header == [0, 0, 0, 1].pack('C*')
    @buddy_allocator_header = BuddyAllocatorHeader.new(@_io, self, @_root)
    self
  end
  class BuddyAllocatorHeader < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @magic = @_io.read_bytes(4)
      raise Kaitai::Struct::ValidationNotEqualError.new([66, 117, 100, 49].pack('C*'), magic, _io, "/types/buddy_allocator_header/seq/0") if not magic == [66, 117, 100, 49].pack('C*')
      @ofs_bookkeeping_info_block = @_io.read_u4be
      @len_bookkeeping_info_block = @_io.read_u4be
      @copy_ofs_bookkeeping_info_block = @_io.read_u4be
      @_unnamed4 = @_io.read_bytes(16)
      self
    end

    ##
    # Magic number 'Bud1'.
    attr_reader :magic
    attr_reader :ofs_bookkeeping_info_block
    attr_reader :len_bookkeeping_info_block

    ##
    # Needs to match 'offset_bookkeeping_info_block'.
    attr_reader :copy_ofs_bookkeeping_info_block

    ##
    # Unused field which might simply be the unused space at the end of the block,
    # since the minimum allocation size is 32 bytes.
    attr_reader :_unnamed4
  end
  class BuddyAllocatorBody < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @num_blocks = @_io.read_u4be
      @_unnamed1 = @_io.read_bytes(4)
      @block_addresses = []
      (num_block_addresses).times { |i|
        @block_addresses << BlockDescriptor.new(@_io, self, @_root)
      }
      @num_directories = @_io.read_u4be
      @directory_entries = []
      (num_directories).times { |i|
        @directory_entries << DirectoryEntry.new(@_io, self, @_root)
      }
      @free_lists = []
      (num_free_lists).times { |i|
        @free_lists << FreeList.new(@_io, self, @_root)
      }
      self
    end
    class BlockDescriptor < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @address_raw = @_io.read_u4be
        self
      end
      def offset
        return @offset unless @offset.nil?
        @offset = ((address_raw & ~(_root.block_address_mask)) + 4)
        @offset
      end
      def size
        return @size unless @size.nil?
        @size = (1 << (address_raw & _root.block_address_mask))
        @size
      end
      attr_reader :address_raw
    end
    class DirectoryEntry < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @len_name = @_io.read_u1
        @name = (@_io.read_bytes(len_name)).force_encoding("UTF-8")
        @block_id = @_io.read_u4be
        self
      end
      attr_reader :len_name
      attr_reader :name
      attr_reader :block_id
    end
    class FreeList < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @counter = @_io.read_u4be
        @offsets = []
        (counter).times { |i|
          @offsets << @_io.read_u4be
        }
        self
      end
      attr_reader :counter
      attr_reader :offsets
    end
    def num_block_addresses
      return @num_block_addresses unless @num_block_addresses.nil?
      @num_block_addresses = 256
      @num_block_addresses
    end
    def num_free_lists
      return @num_free_lists unless @num_free_lists.nil?
      @num_free_lists = 32
      @num_free_lists
    end

    ##
    # Master blocks of the different B-trees.
    def directories
      return @directories unless @directories.nil?
      io = _root._io
      @directories = []
      (num_directories).times { |i|
        @directories << MasterBlockRef.new(io, self, @_root, i)
      }
      @directories
    end

    ##
    # Number of blocks in the allocated-blocks list.
    attr_reader :num_blocks

    ##
    # Unknown field which appears to always be 0.
    attr_reader :_unnamed1

    ##
    # Addresses of the different blocks.
    attr_reader :block_addresses

    ##
    # Indicates the number of directory entries.
    attr_reader :num_directories

    ##
    # Each directory is an independent B-tree.
    attr_reader :directory_entries
    attr_reader :free_lists
  end
  class MasterBlockRef < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self, idx)
      super(_io, _parent, _root)
      @idx = idx
      _read
    end

    def _read
      self
    end
    class MasterBlock < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self)
        super(_io, _parent, _root)
        _read
      end

      def _read
        @block_id = @_io.read_u4be
        @num_internal_nodes = @_io.read_u4be
        @num_records = @_io.read_u4be
        @num_nodes = @_io.read_u4be
        @_unnamed4 = @_io.read_u4be
        self
      end
      def root_block
        return @root_block unless @root_block.nil?
        io = _root._io
        _pos = io.pos
        io.seek(_root.buddy_allocator_body.block_addresses[block_id].offset)
        @root_block = Block.new(io, self, @_root)
        io.seek(_pos)
        @root_block
      end

      ##
      # Block number of the B-tree's root node.
      attr_reader :block_id

      ##
      # Number of internal node levels.
      attr_reader :num_internal_nodes

      ##
      # Number of records in the tree.
      attr_reader :num_records

      ##
      # Number of nodes in the tree.
      attr_reader :num_nodes

      ##
      # Always 0x1000, probably the B-tree node page size.
      attr_reader :_unnamed4
    end
    def master_block
      return @master_block unless @master_block.nil?
      _pos = @_io.pos
      @_io.seek(_parent.block_addresses[_parent.directory_entries[idx].block_id].offset)
      @_raw_master_block = @_io.read_bytes(_parent.block_addresses[_parent.directory_entries[idx].block_id].size)
      _io__raw_master_block = Kaitai::Struct::Stream.new(@_raw_master_block)
      @master_block = MasterBlock.new(_io__raw_master_block, self, @_root)
      @_io.seek(_pos)
      @master_block
    end
    attr_reader :idx
    attr_reader :_raw_master_block
  end
  class Block < Kaitai::Struct::Struct
    def initialize(_io, _parent = nil, _root = self)
      super(_io, _parent, _root)
      _read
    end

    def _read
      @mode = @_io.read_u4be
      @counter = @_io.read_u4be
      @data = []
      (counter).times { |i|
        @data << BlockData.new(@_io, self, @_root, mode)
      }
      self
    end
    class BlockData < Kaitai::Struct::Struct
      def initialize(_io, _parent = nil, _root = self, mode)
        super(_io, _parent, _root)
        @mode = mode
        _read
      end

      def _read
        if mode > 0
          @block_id = @_io.read_u4be
        end
        @record = Record.new(@_io, self, @_root)
        self
      end
      class Record < Kaitai::Struct::Struct
        def initialize(_io, _parent = nil, _root = self)
          super(_io, _parent, _root)
          _read
        end

        def _read
          @filename = Ustr.new(@_io, self, @_root)
          @structure_type = FourCharCode.new(@_io, self, @_root)
          @data_type = (@_io.read_bytes(4)).force_encoding("UTF-8")
          case data_type
          when "long"
            @value = @_io.read_u4be
          when "shor"
            @value = @_io.read_u4be
          when "comp"
            @value = @_io.read_u8be
          when "bool"
            @value = @_io.read_u1
          when "ustr"
            @value = Ustr.new(@_io, self, @_root)
          when "dutc"
            @value = @_io.read_u8be
          when "type"
            @value = FourCharCode.new(@_io, self, @_root)
          when "blob"
            @value = RecordBlob.new(@_io, self, @_root)
          end
          self
        end
        class RecordBlob < Kaitai::Struct::Struct
          def initialize(_io, _parent = nil, _root = self)
            super(_io, _parent, _root)
            _read
          end

          def _read
            @length = @_io.read_u4be
            @value = @_io.read_bytes(length)
            self
          end
          attr_reader :length
          attr_reader :value
        end
        class Ustr < Kaitai::Struct::Struct
          def initialize(_io, _parent = nil, _root = self)
            super(_io, _parent, _root)
            _read
          end

          def _read
            @length = @_io.read_u4be
            @value = (@_io.read_bytes((2 * length))).force_encoding("UTF-16BE")
            self
          end
          attr_reader :length
          attr_reader :value
        end
        class FourCharCode < Kaitai::Struct::Struct
          def initialize(_io, _parent = nil, _root = self)
            super(_io, _parent, _root)
            _read
          end

          def _read
            @value = (@_io.read_bytes(4)).force_encoding("UTF-8")
            self
          end
          attr_reader :value
        end
        attr_reader :filename

        ##
        # Description of the entry's property.
        attr_reader :structure_type

        ##
        # Data type of the value.
        attr_reader :data_type
        attr_reader :value
      end
      def block
        return @block unless @block.nil?
        if mode > 0
          io = _root._io
          _pos = io.pos
          io.seek(_root.buddy_allocator_body.block_addresses[block_id].offset)
          @block = Block.new(io, self, @_root)
          io.seek(_pos)
        end
        @block
      end
      attr_reader :block_id
      attr_reader :record
      attr_reader :mode
    end

    ##
    # Rightmost child block pointer.
    def rightmost_block
      return @rightmost_block unless @rightmost_block.nil?
      if mode > 0
        io = _root._io
        _pos = io.pos
        io.seek(_root.buddy_allocator_body.block_addresses[mode].offset)
        @rightmost_block = Block.new(io, self, @_root)
        io.seek(_pos)
      end
      @rightmost_block
    end

    ##
    # If mode is 0, this is a leaf node, otherwise it is an internal node.
    attr_reader :mode

    ##
    # Number of records or number of block id + record pairs.
    attr_reader :counter
    attr_reader :data
  end
  def buddy_allocator_body
    return @buddy_allocator_body unless @buddy_allocator_body.nil?
    _pos = @_io.pos
    @_io.seek((buddy_allocator_header.ofs_bookkeeping_info_block + 4))
    @_raw_buddy_allocator_body = @_io.read_bytes(buddy_allocator_header.len_bookkeeping_info_block)
    _io__raw_buddy_allocator_body = Kaitai::Struct::Stream.new(@_raw_buddy_allocator_body)
    @buddy_allocator_body = BuddyAllocatorBody.new(_io__raw_buddy_allocator_body, self, @_root)
    @_io.seek(_pos)
    @buddy_allocator_body
  end

  ##
  # Bitmask used to calculate the position and the size of each block
  # of the B-tree from the block addresses.
  def block_address_mask
    return @block_address_mask unless @block_address_mask.nil?
    @block_address_mask = 31
    @block_address_mask
  end
  attr_reader :alignment_header
  attr_reader :buddy_allocator_header
  attr_reader :_raw_buddy_allocator_body
end