class CFPropertyList::Binary

def array_to_binary(val)

Convert array to binary format and add it to the object table
def array_to_binary(val)
  saved_object_count = @written_object_count
  @written_object_count += 1
  bdata = Binary.type_bytes("a", val.value.count) # a is 1010, type indicator for arrays
  val.value.each do |v|
    bdata += Binary.pack_it_with_size(@object_ref_size,  v.to_binary(self));
  end
  @object_table[saved_object_count] = bdata
  return saved_object_count
end

def bool_to_binary(val)

Convert a bool value to binary and add it to the object table
def bool_to_binary(val)
  saved_object_count = @written_object_count
  @written_object_count += 1
  @object_table[saved_object_count] = val ? "\x9" : "\x8" # 0x9 is 1001, type indicator for true; 0x8 is 1000, type indicator for false
  return saved_object_count
end

def data_to_binary(val)

Convert data value to binary format and add it to the object table
def data_to_binary(val)
  saved_object_count = @written_object_count
  @written_object_count += 1
  bdata = Binary.type_bytes("4", val.bytesize) # a is 1000, type indicator for data
  @object_table[saved_object_count] = bdata + val
  return saved_object_count
end

def date_to_binary(val)

Convert date value (apple format) to binary and adds it to the object table
def date_to_binary(val)
  saved_object_count = @written_object_count
  @written_object_count += 1
  val = val.getutc.to_f - CFDate::DATE_DIFF_APPLE_UNIX # CFDate is a real, number of seconds since 01/01/2001 00:00:00 GMT
  bdata = Binary.type_bytes("3", 3) # 3 is 0011, type indicator for date
  @object_table[saved_object_count] = bdata + [val].pack("d").reverse
  return saved_object_count
end

def dict_to_binary(val)

Convert dictionary to binary format and add it to the object table
def dict_to_binary(val)
  saved_object_count = @written_object_count
  @written_object_count += 1
  bdata = Binary.type_bytes("d",val.value.count) # d=1101, type indicator for dictionary
  val.value.each_key do |k|
    str = CFString.new(k)
    key = str.to_binary(self)
    bdata += Binary.pack_it_with_size(@object_ref_size,key)
  end
  val.value.each_value do |v|
    bdata += Binary.pack_it_with_size(@object_ref_size,v.to_binary(self))
  end
  @object_table[saved_object_count] = bdata
  return saved_object_count
end

def int_to_binary(value)

Codes an integer to binary format
def int_to_binary(value)
  nbytes = 0
  nbytes = 1 if value > 0xFF # 1 byte integer
  nbytes += 1 if value > 0xFFFF # 4 byte integer
  nbytes += 1 if value > 0xFFFFFFFF # 8 byte integer
  nbytes = 3 if value < 0 # 8 byte integer, since signed
  bdata = Binary.type_bytes("1", nbytes) # 1 is 0001, type indicator for integer
  buff = ""
  if(nbytes < 3) then
    fmt = "N"
    if(nbytes == 0) then
      fmt = "C"
    elsif(nbytes == 1)
      fmt = "n"
    end
    buff = [value].pack(fmt)
  else
    # 64 bit signed integer; we need the higher and the lower 32 bit of the value
    high_word = value >> 32
    low_word = value & 0xFFFFFFFF
    buff = [high_word,low_word].pack("NN")
  end
  return bdata + buff
end

def load(opts)

Read a binary plist file
def load(opts)
  @unique_table = {}
  @count_objects = 0
  @string_size = 0
  @int_size = 0
  @misc_size = 0
  @object_refs = 0
  @written_object_count = 0
  @object_table = []
  @object_ref_size = 0
  @offsets = []
  fd = nil
  if(opts.has_key?(:file)) then
    fd = File.open(opts[:file],"rb")
    file = opts[:file]
  else
    fd = StringIO.new(opts[:data],"rb")
    file = "<string>"
  end
  # first, we read the trailer: 32 byte from the end
  fd.seek(-32,IO::SEEK_END)
  buff = fd.read(32)
  offset_size, object_ref_size, number_of_objects, top_object, table_offset = buff.unpack "x6CCx4Nx4Nx4N"
  # after that, get the offset table
  fd.seek(table_offset, IO::SEEK_SET)
  coded_offset_table = fd.read(number_of_objects * offset_size)
  raise CFFormatError.new("#{file}: Format error!") unless coded_offset_table.bytesize == number_of_objects * offset_size
  @count_objects = number_of_objects
  # decode offset table
  formats = ["","C*","n*","(H6)*","N*"]
  @offsets = coded_offset_table.unpack(formats[offset_size])
  if(offset_size == 3) then
    0.upto(@offsets.count-1) { |i| @offsets[i] = @offsets[i].to_i(16) }
  end
  @object_ref_size = object_ref_size
  val = read_binary_object_at(file,fd,top_object)
  fd.close
  return val
end

def num_to_binary(value)

Converts a numeric value to binary and adds it to the object table
def num_to_binary(value)
  saved_object_count = @written_object_count
  @written_object_count += 1
  val = ""
  if(value.is_a?(CFInteger)) then
    val = int_to_binary(value.value)
  else
    val = real_to_binary(value.value)
  end
  @object_table[saved_object_count] = val
  return saved_object_count
end

def read_binary_array(fname,fd,length)

Read an binary array value, including contained objects
def read_binary_array(fname,fd,length)
  ary = []
  # first: read object refs
  if(length != 0) then
    buff = fd.read(length * @object_ref_size)
    objects = buff.unpack(@object_ref_size == 1 ? "C*" : "n*")
    # now: read objects
    0.upto(length-1) do |i|
      object = read_binary_object_at(fname,fd,objects[i])
      ary.push object
    end
  end
  return CFArray.new(ary)
end

def read_binary_data(fname,fd,length)

Read a binary data value
def read_binary_data(fname,fd,length)
  buff = "";
  buff = fd.read(length) if length > 0
  return CFData.new(buff,CFData::DATA_RAW)
end

def read_binary_date(fname,fd,length)

read a binary date value
def read_binary_date(fname,fd,length)
  raise CFFormatError.new("Date greater than 8 bytes: #{length}") if length > 3
  nbytes = 1 << length
  val = nil
  buff = fd.read(nbytes)
  case length
  when 0 then # 1 byte CFDate is an error
    raise CFFormatError.new("#{length+1} byte CFDate, error")
  when 1 then # 2 byte CFDate is an error
    raise CFFormatError.new("#{length+1} byte CFDate, error")
  when 2 then
    val = buff.reverse.unpack("f")
    val = val[0]
  when 3 then
    val = buff.reverse.unpack("d")
    val = val[0]
  end
  return CFDate.new(val,CFDate::TIMESTAMP_APPLE)
end

def read_binary_dict(fname,fd,length)

Read a dictionary value, including contained objects
def read_binary_dict(fname,fd,length)
  dict = {}
  # first: read keys
  if(length != 0) then
    buff = fd.read(length * @object_ref_size)
    keys = buff.unpack(@object_ref_size == 1 ? "C*" : "n*")
    # second: read object refs
    buff = fd.read(length * @object_ref_size)
    objects = buff.unpack(@object_ref_size == 1 ? "C*" : "n*")
    # read real keys and objects
    0.upto(length-1) do |i|
      key = read_binary_object_at(fname,fd,keys[i])
      object = read_binary_object_at(fname,fd,objects[i])
      dict[key.value] = object
    end
  end
  return CFDictionary.new(dict)
end

def read_binary_int(fname,fd,length)

read a binary int value
def read_binary_int(fname,fd,length)
  raise CFFormatError.new("Integer greater than 8 bytes: #{length}") if length > 3
  nbytes = 1 << length
  val = nil
  buff = fd.read(nbytes)
  case length
  when 0 then
    val = buff.unpack("C")
    val = val[0]
  when 1 then
    val = buff.unpack("n")
    val = val[0]
  when 2 then
    val = buff.unpack("N")
    val = val[0]
  when 3
    hiword,loword = buff.unpack("NN")
    val = hiword << 32 | loword
  end
  return CFInteger.new(val);
end

def read_binary_null_type(length)

read a „null” type (i.e. null byte, marker byte, bool value)
def read_binary_null_type(length)
  case length
  when 0 then return 0 # null byte
  when 8 then return CFBoolean.new(false)
  when 9 then return CFBoolean.new(true)
  when 15 then return 15 # fill type
  end
  raise CFFormatError.new("unknown null type: #{length}")
end

def read_binary_object(fname,fd)

Read an object type byte, decode it and delegate to the correct reader function
def read_binary_object(fname,fd)
  # first: read the marker byte
  buff = fd.read(1)
  object_length = buff.unpack("C*")
  object_length = object_length[0]  & 0xF
  buff = buff.unpack("H*")
  object_type = buff[0][0].chr
  if(object_type != "0" && object_length == 15) then
    object_length = read_binary_object(fname,fd)
    object_length = object_length.value
  end
  retval = nil
  case object_type
  when '0' then # null, false, true, fillbyte
    retval = read_binary_null_type(object_length)
  when '1' then # integer
    retval = read_binary_int(fname,fd,object_length)
  when '2' then # real
    retval = read_binary_real(fname,fd,object_length)
  when '3' then # date
    retval = read_binary_date(fname,fd,object_length)
  when '4' then # data
    retval = read_binary_data(fname,fd,object_length)
  when '5' then # byte string, usually utf8 encoded
    retval = read_binary_string(fname,fd,object_length)
  when '6' then # unicode string (utf16be)
    retval = read_binary_unicode_string(fname,fd,object_length)
  when 'a' then # array
    retval = read_binary_array(fname,fd,object_length)
  when 'd' then # dictionary
    retval = read_binary_dict(fname,fd,object_length)
  end
  return retval
end

def read_binary_object_at(fname,fd,pos)

Read an object type byte at position $pos, decode it and delegate to the correct reader function
def read_binary_object_at(fname,fd,pos)
  position = @offsets[pos]
  fd.seek(position,IO::SEEK_SET)
  return read_binary_object(fname,fd)
end

def read_binary_real(fname,fd,length)

read a binary real value
def read_binary_real(fname,fd,length)
  raise CFFormatError.new("Real greater than 8 bytes: #{length}") if length > 3
  nbytes = 1 << length
  val = nil
  buff = fd.read(nbytes)
  case length
  when 0 then # 1 byte float? must be an error
    raise CFFormatError.new("got #{length+1} byte float, must be an error!")
  when 1 then # 2 byte float? must be an error
    raise CFFormatError.new("got #{length+1} byte float, must be an error!")
  when 2 then
    val = buff.reverse.unpack("f")
    val = val[0]
  when 3 then
    val = buff.reverse.unpack("d")
    val = val[0]
  end
  return CFReal.new(val)
end

def read_binary_string(fname,fd,length)

Read a binary string value
def read_binary_string(fname,fd,length)
  buff = ""
  buff = fd.read(length) if length > 0
  @unique_table[buff] = true unless @unique_table.has_key?(buff)
  return CFString.new(buff)
end

def read_binary_unicode_string(fname,fd,length)

Read a unicode string value, coded as UTF-16BE
def read_binary_unicode_string(fname,fd,length)
  # The problem is: we get the length of the string IN CHARACTERS;
  # since a char in UTF-16 can be 16 or 32 bit long, we don't really know
  # how long the string is in bytes
  buff = fd.read(2*length)
  @unique_table[buff] = true unless @unique_table.has_key?(buff)
  return CFString.new(Binary.charset_convert(buff,"UTF-16BE","UTF-8"))
end

def real_to_binary(val)

Codes a real value to binary format
def real_to_binary(val)
  bdata = Binary.type_bytes("2",3) # 2 is 0010, type indicator for reals
  buff = [val].pack("d")
  return bdata + buff.reverse
end

def string_to_binary(val)

Uniques and transforms a string value to binary format and adds it to the object table
def string_to_binary(val)
  saved_object_count = -1
  unless(@unique_table.has_key?(val)) then
    saved_object_count = @written_object_count
    @written_object_count += 1
    @unique_table[val] = saved_object_count
    utf16 = false
    val.each_byte do |b|
      if(b > 127) then
        utf16 = true
        break
      end
    end
    if(utf16) then
      bdata = Binary.type_bytes("6",Binary.charset_strlen(val,"UTF-8")) # 6 is 0110, unicode string (utf16be)
      val = Binary.charset_convert(val,"UTF-8","UTF-16BE")
      val.force_encoding("ASCII-8BIT") if val.respond_to?("encode")
      @object_table[saved_object_count] = bdata + val
    else
      bdata = Binary.type_bytes("5",val.bytesize) # 5 is 0101 which is an ASCII string (seems to be ASCII encoded)
      @object_table[saved_object_count] = bdata + val
    end
  else
    saved_object_count = @unique_table[val]
  end
  return saved_object_count
end

def to_str(opts={})

Convert CFPropertyList to binary format; since we have to count our objects we simply unique CFDictionary and CFArray
def to_str(opts={})
  @unique_table = {}
  @count_objects = 0
  @string_size = 0
  @int_size = 0
  @misc_size = 0
  @object_refs = 0
  @written_object_count = 0
  @object_table = []
  @object_ref_size = 0
  @offsets = []
  binary_str = "bplist00"
  unique_and_count_values(opts[:root])
  @count_objects += @unique_table.count
  @object_ref_size = Binary.bytes_needed(@count_objects)
  file_size = @string_size + @int_size + @misc_size + @object_refs * @object_ref_size + 40
  offset_size = Binary.bytes_needed(file_size)
  table_offset = file_size - 32
  @object_table = []
  @written_object_count = 0
  @unique_table = {} # we needed it to calculate several values, but now we need an empty table
  opts[:root].to_binary(self)
  object_offset = 8
  offsets = []
  0.upto(@object_table.count-1) do |i|
    binary_str += @object_table[i]
    offsets[i] = object_offset
    object_offset += @object_table[i].bytesize
  end
  offsets.each do |offset|
    binary_str += Binary.pack_it_with_size(offset_size,offset)
  end
  binary_str += [offset_size, @object_ref_size].pack("x6CC")
  binary_str += [@count_objects].pack("x4N")
  binary_str += [0].pack("x4N")
  binary_str += [table_offset].pack("x4N")
  return binary_str
end

def unique_and_count_values(value)

will only be saved once and referenced later
„unique” and count values. „Unique” means, several objects (e.g. strings)
def unique_and_count_values(value)
  # no uniquing for other types than CFString and CFData
  if(value.is_a?(CFInteger) || value.is_a?(CFReal)) then
    val = value.value
    if(value.is_a?(CFInteger)) then
      @int_size += Binary.bytes_int(val)
    else
      @misc_size += 9 # 9 bytes (8 + marker byte) for real
    end
    @count_objects += 1
    return
  elsif(value.is_a?(CFDate)) then
    @misc_size += 9
    @count_objects += 1
    return
  elsif(value.is_a?(CFBoolean)) then
    @count_objects += 1
    @misc_size += 1
    return
  elsif(value.is_a?(CFArray)) then
    cnt = 0
    value.value.each do |v|
      cnt += 1
      unique_and_count_values(v)
      @object_refs += 1 # each array member is a ref
    end
    @count_objects += 1
    @int_size += Binary.bytes_size_int(cnt)
    @misc_size += 1 # marker byte for array
    return
  elsif(value.is_a?(CFDictionary)) then
    cnt = 0
    value.value.each_pair do |k,v|
      cnt += 1
      if(!@unique_table.has_key?(k))
        @unique_table[k] = 0
        @string_size += Binary.binary_strlen(k) + 1
        @int_size += Binary.bytes_size_int(Binary.charset_strlen(k,'UTF-8'))
      end
      @object_refs += 2 # both, key and value, are refs
      @unique_table[k] += 1
      unique_and_count_values(v)
    end
    @count_objects += 1
    @misc_size += 1 # marker byte for dict
    @int_size += Binary.bytes_size_int(cnt)
    return
  elsif(value.is_a?(CFData)) then
    val = value.decoded_value
    @int_size += Binary.bytes_size_int(val.length)
    @misc_size += val.length
    @count_objects += 1
    return
  end
  val = value.value
  if(!@unique_table.has_key?(val)) then
    @unique_table[val] = 0
    @string_size += Binary.binary_strlen(val) + 1
    @int_size += Binary.bytes_size_int(Binary.charset_strlen(val,'UTF-8'))
  end
  @unique_table[val] += 1
end