316 lines
9.2 KiB
Ruby
316 lines
9.2 KiB
Ruby
# -*- coding: utf-8 -*-
|
|
#
|
|
# CFPropertyList implementation
|
|
# class to read, manipulate and write both XML and binary property list
|
|
# files (plist(5)) as defined by Apple
|
|
#
|
|
# == Example
|
|
#
|
|
# # create a arbitrary data structure of basic data types
|
|
# data = {
|
|
# 'name' => 'John Doe',
|
|
# 'missing' => true,
|
|
# 'last_seen' => Time.now,
|
|
# 'friends' => ['Jane Doe','Julian Doe'],
|
|
# 'likes' => {
|
|
# 'me' => false
|
|
# }
|
|
# }
|
|
#
|
|
# # create CFPropertyList::List object
|
|
# plist = CFPropertyList::List.new
|
|
#
|
|
# # call CFPropertyList.guess() to create corresponding CFType values
|
|
# # pass in optional :convert_unknown_to_string => true to convert things like symbols into strings.
|
|
# plist.value = CFPropertyList.guess(data)
|
|
#
|
|
# # write plist to file
|
|
# plist.save("example.plist", CFPropertyList::List::FORMAT_BINARY)
|
|
#
|
|
# # … later, read it again
|
|
# plist = CFPropertyList::List.new({:file => "example.plist"})
|
|
# data = CFPropertyList.native_types(plist.value)
|
|
#
|
|
# Author:: Christian Kruse (mailto:cjk@wwwtech.de)
|
|
# Copyright:: Copyright (c) 2010
|
|
# License:: Distributes under the same terms as Ruby
|
|
|
|
require 'libxml'
|
|
require 'kconv'
|
|
require 'date'
|
|
|
|
module CFPropertyList
|
|
# interface class for PList parsers
|
|
class ParserInterface
|
|
# load a plist
|
|
def load(opts={})
|
|
return ""
|
|
end
|
|
|
|
# convert a plist to string
|
|
def to_str(opts={})
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
dirname = File.dirname(__FILE__)
|
|
require dirname + '/rbCFPlistError.rb'
|
|
require dirname + '/rbCFTypes.rb'
|
|
require dirname + '/rbXMLCFPropertyList.rb'
|
|
require dirname + '/rbBinaryCFPropertyList.rb'
|
|
|
|
require 'iconv' unless "".respond_to?("encode")
|
|
|
|
module CFPropertyList
|
|
# Create CFType hierarchy by guessing the correct CFType, e.g.
|
|
#
|
|
# x = {
|
|
# 'a' => ['b','c','d']
|
|
# }
|
|
# cftypes = CFPropertyList.guess(x)
|
|
#
|
|
# pass optional options hash. Only possible value actually:
|
|
# :convert_unknown_to_string => true
|
|
# :converter_method => :method_name
|
|
#
|
|
# cftypes = CFPropertyList.guess(x,:convert_unknown_to_string => true)
|
|
def guess(object, options = {})
|
|
if(object.is_a?(Fixnum) || object.is_a?(Integer)) then
|
|
return CFInteger.new(object)
|
|
elsif(object.is_a?(Float) || (Object.const_defined?('BigDecimal') and object.is_a?(BigDecimal))) then
|
|
return CFReal.new(object)
|
|
elsif(object.is_a?(TrueClass) || object.is_a?(FalseClass)) then
|
|
return CFBoolean.new(object)
|
|
elsif(object.is_a?(String)) then
|
|
return CFString.new(object)
|
|
elsif(object.is_a?(Time) || object.is_a?(DateTime)) then
|
|
return CFDate.new(object)
|
|
elsif(object.is_a?(IO)) then
|
|
return CFData.new(object.read, CFData::DATA_RAW)
|
|
elsif(object.is_a?(Array)) then
|
|
ary = Array.new
|
|
object.each do
|
|
|o|
|
|
ary.push CFPropertyList.guess(o, options)
|
|
end
|
|
|
|
return CFArray.new(ary)
|
|
elsif(object.is_a?(Hash)) then
|
|
hsh = Hash.new
|
|
object.each_pair do
|
|
|k,v|
|
|
k = k.to_s if k.is_a?(Symbol)
|
|
hsh[k] = CFPropertyList.guess(v, options)
|
|
end
|
|
|
|
return CFDictionary.new(hsh)
|
|
elsif options[:converter_method] and object.respond_to?(options[:converter_method]) then
|
|
return CFPropertyList.guess(object.send(options[:converter_method]))
|
|
elsif options[:convert_unknown_to_string] then
|
|
return CFString.new(object.to_s)
|
|
else
|
|
raise CFTypeError.new("Unknown class #{object.class.to_s}! Try using :convert_unknown_to_string if you want to use unknown object types!")
|
|
end
|
|
end
|
|
|
|
# Converts a CFType hiercharchy to native Ruby types
|
|
def native_types(object,keys_as_symbols=false)
|
|
return if object.nil?
|
|
|
|
if(object.is_a?(CFDate) || object.is_a?(CFString) || object.is_a?(CFInteger) || object.is_a?(CFReal) || object.is_a?(CFBoolean)) then
|
|
return object.value
|
|
elsif(object.is_a?(CFData)) then
|
|
return object.decoded_value
|
|
elsif(object.is_a?(CFArray)) then
|
|
ary = []
|
|
object.value.each do
|
|
|v|
|
|
ary.push CFPropertyList.native_types(v)
|
|
end
|
|
|
|
return ary
|
|
elsif(object.is_a?(CFDictionary)) then
|
|
hsh = {}
|
|
object.value.each_pair do
|
|
|k,v|
|
|
k = k.to_sym if keys_as_symbols
|
|
hsh[k] = CFPropertyList.native_types(v)
|
|
end
|
|
|
|
return hsh
|
|
end
|
|
end
|
|
|
|
module_function :guess, :native_types
|
|
|
|
class List
|
|
# Format constant for binary format
|
|
FORMAT_BINARY = 1
|
|
|
|
# Format constant for XML format
|
|
FORMAT_XML = 2
|
|
|
|
# Format constant for automatic format recognizing
|
|
FORMAT_AUTO = 0
|
|
|
|
@@parsers = [Binary,XML]
|
|
|
|
# Path of PropertyList
|
|
attr_accessor :filename
|
|
# Path of PropertyList
|
|
attr_accessor :format
|
|
# the root value in the plist file
|
|
attr_accessor :value
|
|
|
|
def initialize(opts={})
|
|
@filename = opts[:file]
|
|
@format = opts[:format] || FORMAT_AUTO
|
|
@data = opts[:data]
|
|
|
|
load(@filename) unless @filename.nil?
|
|
load_str(@data) unless @data.nil?
|
|
end
|
|
|
|
# Load an XML PropertyList
|
|
# filename = nil:: The filename to read from; if nil, read from the file defined by instance variable +filename+
|
|
def load_xml(filename=nil)
|
|
load(filename,List::FORMAT_XML)
|
|
end
|
|
|
|
# read a binary plist file
|
|
# filename = nil:: The filename to read from; if nil, read from the file defined by instance variable +filename+
|
|
def load_binary(filename=nil)
|
|
load(filename,List::FORMAT_BINARY)
|
|
end
|
|
|
|
# load a plist from a XML string
|
|
# str:: The string containing the plist
|
|
def load_xml_str(str=nil)
|
|
load_str(str,List::FORMAT_XML)
|
|
end
|
|
|
|
# load a plist from a binary string
|
|
# str:: The string containing the plist
|
|
def load_binary_str(str=nil)
|
|
load_str(str,List::FORMAT_BINARY)
|
|
end
|
|
|
|
# load a plist from a string
|
|
# str = nil:: The string containing the plist
|
|
# format = nil:: The format of the plist
|
|
def load_str(str=nil,format=nil)
|
|
str = @data if str.nil?
|
|
format = @format if format.nil?
|
|
|
|
@value = {}
|
|
case format
|
|
when List::FORMAT_BINARY, List::FORMAT_XML then
|
|
prsr = @@parsers[format-1].new
|
|
@value = prsr.load({:data => str})
|
|
|
|
when List::FORMAT_AUTO then # what we now do is ugly, but neccessary to recognize the file format
|
|
filetype = str[0..5]
|
|
version = str[6..7]
|
|
|
|
prsr = nil
|
|
if filetype == "bplist" then
|
|
raise CFFormatError.new("Wong file version #{version}") unless version == "00"
|
|
prsr = Binary.new
|
|
else
|
|
prsr = XML.new
|
|
end
|
|
|
|
@value = prsr.load({:data => str})
|
|
end
|
|
end
|
|
|
|
# Read a plist file
|
|
# file = nil:: The filename of the file to read. If nil, use +filename+ instance variable
|
|
# format = nil:: The format of the plist file. Auto-detect if nil
|
|
def load(file=nil,format=nil)
|
|
file = @filename if file.nil?
|
|
format = @format if format.nil?
|
|
@value = {}
|
|
|
|
raise IOError.new("File #{file} not readable!") unless File.readable? file
|
|
|
|
case format
|
|
when List::FORMAT_BINARY, List::FORMAT_XML then
|
|
prsr = @@parsers[format-1].new
|
|
@value = prsr.load({:file => file})
|
|
|
|
when List::FORMAT_AUTO then # what we now do is ugly, but neccessary to recognize the file format
|
|
magic_number = IO.read(file,8)
|
|
filetype = magic_number[0..5]
|
|
version = magic_number[6..7]
|
|
|
|
prsr = nil
|
|
if filetype == "bplist" then
|
|
raise CFFormatError.new("Wong file version #{version}") unless version == "00"
|
|
prsr = Binary.new
|
|
else
|
|
prsr = XML.new
|
|
end
|
|
|
|
@value = prsr.load({:file => file})
|
|
end
|
|
end
|
|
|
|
# Serialize CFPropertyList object to specified format and write it to file
|
|
# file = nil:: The filename of the file to write to. Uses +filename+ instance variable if nil
|
|
# format = nil:: The format to save in. Uses +format+ instance variable if nil
|
|
def save(file=nil,format=nil,opts={})
|
|
format = @format if format.nil?
|
|
file = @filename if file.nil?
|
|
|
|
raise CFFormatError.new("Format #{format} not supported, use List::FORMAT_BINARY or List::FORMAT_XML") if format != FORMAT_BINARY && format != FORMAT_XML
|
|
|
|
if(!File.exists?(file)) then
|
|
raise IOError.new("File #{file} not writable!") unless File.writable?(File.dirname(file))
|
|
elsif(!File.writable?(file)) then
|
|
raise IOError.new("File #{file} not writable!")
|
|
end
|
|
|
|
opts[:root] = @value
|
|
prsr = @@parsers[format-1].new
|
|
content = prsr.to_str(opts)
|
|
|
|
File.open(file, 'wb') {
|
|
|fd|
|
|
fd.write content
|
|
}
|
|
end
|
|
|
|
# convert plist to string
|
|
# format = List::FORMAT_BINARY:: The format to save the plist
|
|
# opts={}:: Pass parser options
|
|
def to_str(format=List::FORMAT_BINARY,opts={})
|
|
prsr = @@parsers[format-1].new
|
|
opts[:root] = @value
|
|
return prsr.to_str(opts)
|
|
end
|
|
end
|
|
end
|
|
|
|
class Array
|
|
def to_plist(options={})
|
|
options[:plist_format] ||= CFPropertyList::List::FORMAT_BINARY
|
|
|
|
plist = CFPropertyList::List.new
|
|
plist.value = CFPropertyList.guess(self, options)
|
|
plist.to_str(options[:plist_format])
|
|
end
|
|
end
|
|
|
|
class Hash
|
|
def to_plist(options={})
|
|
options[:plist_format] ||= CFPropertyList::List::FORMAT_BINARY
|
|
|
|
plist = CFPropertyList::List.new
|
|
plist.value = CFPropertyList.guess(self, options)
|
|
plist.to_str(options[:plist_format])
|
|
end
|
|
end
|
|
|
|
# eof
|