├── .gitignore ├── test ├── assets │ ├── example_data.jpg │ ├── example_data.bin │ ├── commented.plist │ ├── test_empty_key.plist │ ├── test_data_elements.plist │ ├── Cookies.plist │ ├── AlbumData.xml │ └── example_data.plist ├── test_generator_collections.rb ├── test_generator_basic_types.rb ├── test_generator.rb ├── test_parser.rb └── test_data_elements.rb ├── .travis.yml ├── lib ├── plist.rb └── plist │ ├── parser.rb │ └── generator.rb ├── LICENSE ├── CHANGELOG ├── Rakefile └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | rdoc 2 | coverage 3 | pkg 4 | -------------------------------------------------------------------------------- /test/assets/example_data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/plist/master/test/assets/example_data.jpg -------------------------------------------------------------------------------- /test/assets/example_data.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 1.9.2 5 | - jruby-18mode 6 | - jruby-19mode 7 | - rbx-18mode 8 | - rbx-19mode 9 | - ruby-head 10 | - jruby-head 11 | - 1.8.7 12 | - ree -------------------------------------------------------------------------------- /test/assets/commented.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /test/assets/test_empty_key.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | key 6 | 7 | 8 | 1 9 | subkey 10 | 2 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/plist.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # = plist 4 | # 5 | # This is the main file for plist. Everything interesting happens in 6 | # Plist and Plist::Emit. 7 | # 8 | # Copyright 2006-2010 Ben Bleything and Patrick May 9 | # Distributed under the MIT License 10 | # 11 | 12 | require 'base64' 13 | require 'cgi' 14 | require 'stringio' 15 | 16 | require 'plist/generator' 17 | require 'plist/parser' 18 | 19 | module Plist 20 | VERSION = '3.1.0' 21 | end 22 | -------------------------------------------------------------------------------- /test/assets/test_data_elements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stringio 6 | dGhpcyBpcyBhIHN0cmluZ2lvIG9iamVjdA== 7 | 8 | file 9 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 10 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 11 | AAAAAAAAAAAAAA== 12 | 13 | io 14 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 15 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 16 | AAAAAAAAAAAAAA== 17 | 18 | marshal 19 | 20 | BAhvOhZNYXJzaGFsYWJsZU9iamVjdAY6CUBmb28iHnRoaXMgb2JqZWN0IHdh 21 | cyBtYXJzaGFsZWQ= 22 | 23 | nodata 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2010, Ben Bleything and Patrick May 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 15 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 16 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/test_generator_collections.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'plist' 5 | 6 | class TestGeneratorCollections < Test::Unit::TestCase 7 | def test_array 8 | expected = < 10 | 1 11 | 2 12 | 3 13 | 14 | END 15 | 16 | assert_equal expected, [1,2,3].to_plist(false) 17 | end 18 | 19 | def test_empty_array 20 | expected = < 22 | END 23 | 24 | assert_equal expected, [].to_plist(false) 25 | end 26 | 27 | def test_hash 28 | expected = < 30 | abc 31 | 123 32 | foo 33 | bar 34 | 35 | END 36 | # thanks to recent changes in the generator code, hash keys are sorted before emission, 37 | # so multi-element hash tests should be reliable. We're testing that here too. 38 | assert_equal expected, {:foo => :bar, :abc => 123}.to_plist(false) 39 | end 40 | 41 | def test_empty_hash 42 | expected = < 44 | END 45 | 46 | assert_equal expected, {}.to_plist(false) 47 | end 48 | 49 | def test_hash_with_array_element 50 | expected = < 52 | ary 53 | 54 | 1 55 | b 56 | 3 57 | 58 | 59 | END 60 | assert_equal expected, {:ary => [1,:b,'3']}.to_plist(false) 61 | end 62 | 63 | def test_array_with_hash_element 64 | expected = < 66 | 67 | foo 68 | bar 69 | 70 | b 71 | 3 72 | 73 | END 74 | 75 | assert_equal expected, [{:foo => 'bar'}, :b, 3].to_plist(false) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/test_generator_basic_types.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'plist' 5 | 6 | class TestGeneratorBasicTypes < Test::Unit::TestCase 7 | def wrap(tag, content) 8 | return "<#{tag}>#{content}" 9 | end 10 | 11 | def test_strings 12 | expected = wrap('string', 'testdata') 13 | 14 | assert_equal expected, Plist::Emit.dump('testdata', false).chomp 15 | assert_equal expected, Plist::Emit.dump(:testdata, false).chomp 16 | end 17 | 18 | def test_strings_with_escaping 19 | expected = wrap('string', "<Fish & Chips>") 20 | 21 | assert_equal expected, Plist::Emit.dump('', false).chomp 22 | end 23 | 24 | def test_integers 25 | [42, 2376239847623987623, -8192].each do |i| 26 | assert_equal wrap('integer', i), Plist::Emit.dump(i, false).chomp 27 | end 28 | end 29 | 30 | def test_floats 31 | [3.14159, -38.3897, 2398476293847.9823749872349980].each do |i| 32 | assert_equal wrap('real', i), Plist::Emit.dump(i, false).chomp 33 | end 34 | end 35 | 36 | def test_booleans 37 | assert_equal "", Plist::Emit.dump(true, false).chomp 38 | assert_equal "", Plist::Emit.dump(false, false).chomp 39 | end 40 | 41 | def test_time 42 | test_time = Time.now 43 | assert_equal wrap('date', test_time.utc.strftime('%Y-%m-%dT%H:%M:%SZ')), Plist::Emit.dump(test_time, false).chomp 44 | end 45 | 46 | def test_dates 47 | test_date = Date.today 48 | test_datetime = DateTime.now 49 | 50 | assert_equal wrap('date', test_date.strftime('%Y-%m-%dT%H:%M:%SZ')), Plist::Emit.dump(test_date, false).chomp 51 | assert_equal wrap('date', test_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')), Plist::Emit.dump(test_datetime, false).chomp 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_generator.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'plist' 5 | 6 | class SerializableObject 7 | attr_accessor :foo 8 | 9 | def initialize(str) 10 | @foo = str 11 | end 12 | 13 | def to_plist_node 14 | return "#{CGI::escapeHTML @foo}" 15 | end 16 | end 17 | 18 | class TestGenerator < Test::Unit::TestCase 19 | def test_to_plist_vs_plist_emit_dump_no_envelope 20 | source = [1, :b, true] 21 | 22 | to_plist = source.to_plist(false) 23 | plist_emit_dump = Plist::Emit.dump(source, false) 24 | 25 | assert_equal to_plist, plist_emit_dump 26 | end 27 | 28 | def test_to_plist_vs_plist_emit_dump_with_envelope 29 | source = [1, :b, true] 30 | 31 | to_plist = source.to_plist 32 | plist_emit_dump = Plist::Emit.dump(source) 33 | 34 | assert_equal to_plist, plist_emit_dump 35 | end 36 | 37 | def test_dumping_serializable_object 38 | str = 'this object implements #to_plist_node' 39 | so = SerializableObject.new(str) 40 | 41 | assert_equal "#{str}", Plist::Emit.dump(so, false) 42 | end 43 | 44 | def test_write_plist 45 | data = [1, :two, {:c => 'dee'}] 46 | 47 | data.save_plist('test.plist') 48 | file = File.open('test.plist') {|f| f.read} 49 | 50 | assert_equal file, data.to_plist 51 | 52 | File.unlink('test.plist') 53 | end 54 | 55 | # The hash in this test was failing with 'hsh.keys.sort', 56 | # we are making sure it works with 'hsh.keys.sort_by'. 57 | def test_sorting_keys 58 | hsh = {:key1 => 1, :key4 => 4, 'key2' => 2, :key3 => 3} 59 | output = Plist::Emit.plist_node(hsh) 60 | expected = <<-STR 61 | 62 | key1 63 | 1 64 | key2 65 | 2 66 | key3 67 | 3 68 | key4 69 | 4 70 | 71 | STR 72 | 73 | assert_equal expected, output.gsub(/[\t]/, "\s\s") 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/test_parser.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'plist' 5 | 6 | class TestParser < Test::Unit::TestCase 7 | def test_Plist_parse_xml 8 | result = Plist::parse_xml("test/assets/AlbumData.xml") 9 | 10 | # dict 11 | assert_kind_of( Hash, result ) 12 | 13 | expected = [ 14 | "List of Albums", 15 | "Minor Version", 16 | "Master Image List", 17 | "Major Version", 18 | "List of Keywords", 19 | "Archive Path", 20 | "List of Rolls", 21 | "Application Version" 22 | ] 23 | assert_equal( expected.sort, result.keys.sort ) 24 | 25 | # array 26 | assert_kind_of( Array, result["List of Rolls"] ) 27 | assert_equal( [ {"PhotoCount"=>1, 28 | "KeyList"=>["7"], 29 | "Parent"=>999000, 30 | "Album Type"=>"Regular", 31 | "AlbumName"=>"Roll 1", 32 | "AlbumId"=>6}], 33 | result["List of Rolls"] ) 34 | 35 | # string 36 | assert_kind_of( String, result["Application Version"] ) 37 | assert_equal( "5.0.4 (263)", result["Application Version"] ) 38 | 39 | # integer 40 | assert_kind_of( Integer, result["Major Version"] ) 41 | assert_equal( 2, result["Major Version"] ) 42 | 43 | # true 44 | assert_kind_of( TrueClass, result["List of Albums"][0]["Master"] ) 45 | assert( result["List of Albums"][0]["Master"] ) 46 | 47 | # false 48 | assert_kind_of( FalseClass, result["List of Albums"][1]["SlideShowUseTitles"] ) 49 | assert( ! result["List of Albums"][1]["SlideShowUseTitles"] ) 50 | 51 | end 52 | 53 | # uncomment this test to work on speed optimization 54 | #def test_load_something_big 55 | # plist = Plist::parse_xml( "~/Pictures/iPhoto Library/AlbumData.xml" ) 56 | #end 57 | 58 | # date fields are credited to 59 | def test_date_fields 60 | result = Plist::parse_xml("test/assets/Cookies.plist") 61 | assert_kind_of( DateTime, result.first['Expires'] ) 62 | assert_equal DateTime.parse( "2007-10-25T12:36:35Z" ), result.first['Expires'] 63 | end 64 | 65 | # bug fix for empty 66 | # reported by Matthias Peick 67 | # reported and fixed by Frederik Seiffert 68 | def test_empty_dict_key 69 | data = Plist::parse_xml("test/assets/test_empty_key.plist"); 70 | assert_equal("2", data['key']['subkey']) 71 | end 72 | 73 | # bug fix for decoding entities 74 | # reported by Matthias Peick 75 | def test_decode_entities 76 | data = Plist::parse_xml('Fish & Chips') 77 | assert_equal('Fish & Chips', data) 78 | end 79 | 80 | def test_comment_handling_and_empty_plist 81 | assert_nothing_raised do 82 | assert_nil( Plist::parse_xml( File.read('test/assets/commented.plist') ) ) 83 | end 84 | end 85 | 86 | def test_filename_or_xml_is_stringio 87 | require 'stringio' 88 | 89 | str = StringIO.new 90 | data = Plist::parse_xml(str) 91 | 92 | assert_nil data 93 | end 94 | 95 | end 96 | 97 | __END__ 98 | -------------------------------------------------------------------------------- /test/assets/Cookies.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Created 7 | 151936595.697543 8 | Domain 9 | .cleveland.com 10 | Expires 11 | 2007-10-25T12:36:35Z 12 | Name 13 | CTC 14 | Path 15 | / 16 | Value 17 | :broadband: 18 | 19 | 20 | Created 21 | 151778895.063041 22 | Domain 23 | .gamefaqs.com 24 | Expires 25 | 2006-04-21T16:47:58Z 26 | Name 27 | ctk 28 | Path 29 | / 30 | Value 31 | NDM1YmJlYmU0NjZiOGYxZjc1NjgxODg0YmRkMA%3D%3D 32 | 33 | 34 | Created 35 | 183530456 36 | Domain 37 | arstechnica.com 38 | Expires 39 | 2006-10-26T13:56:36Z 40 | Name 41 | fontFace 42 | Path 43 | / 44 | Value 45 | 1 46 | 47 | 48 | Created 49 | 183004526 50 | Domain 51 | .sourceforge.net 52 | Expires 53 | 2006-10-20T02:35:26Z 54 | Name 55 | FRQSTR 56 | Path 57 | / 58 | Value 59 | 18829595x86799:1:1440x87033:1:1440x86799:1:1440x87248:1:1440|18829595|18829595|18829595|18829595 60 | 61 | 62 | Created 63 | 151053128.640531 64 | Domain 65 | .tvguide.com 66 | Expires 67 | 2025-10-10T07:12:17Z 68 | Name 69 | DMSEG 70 | Path 71 | / 72 | Value 73 | 1BDF3D1CC07FC70F&D04451&434EC763&4351FD51&0& 74 | 75 | 76 | Created 77 | 151304125.760261 78 | Domain 79 | .code.blogspot.com 80 | Expires 81 | 2038-01-18T00:00:00Z 82 | Name 83 | __utma 84 | Path 85 | / 86 | Value 87 | 11680422.1172819419.1129611326.1129611326.1129611326.1 88 | 89 | 90 | Created 91 | 599529600 92 | Domain 93 | .tvguide.com 94 | Expires 95 | 2020-01-01T00:00:00Z 96 | Name 97 | gfm 98 | Path 99 | / 100 | Value 101 | 0 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /test/test_data_elements.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'test/unit' 4 | require 'plist' 5 | require 'stringio' 6 | 7 | class MarshalableObject 8 | attr_accessor :foo 9 | 10 | def initialize(str) 11 | @foo = str 12 | end 13 | end 14 | 15 | class TestDataElements < Test::Unit::TestCase 16 | 17 | def setup 18 | @result = Plist.parse_xml( 'test/assets/test_data_elements.plist' ) 19 | end 20 | 21 | def test_data_object_header 22 | expected = < element below contains a Ruby object which has been serialized with Marshal.dump. --> 24 | 25 | BAhvOhZNYXJzaGFsYWJsZU9iamVjdAY6CUBmb28iHnRoaXMgb2JqZWN0IHdhcyBtYXJz 26 | aGFsZWQ= 27 | 28 | END 29 | expected_elements = expected.chomp.split( "\n" ) 30 | 31 | actual = Plist::Emit.dump( Object.new, false ) 32 | actual_elements = actual.chomp.split( "\n" ) 33 | 34 | # check for header 35 | assert_equal expected_elements.shift, actual_elements.shift 36 | 37 | # check for opening and closing data tags 38 | assert_equal expected_elements.shift, actual_elements.shift 39 | assert_equal expected_elements.pop, actual_elements.pop 40 | end 41 | 42 | def test_marshal_round_trip 43 | expected = MarshalableObject.new('this object was marshaled') 44 | actual = Plist.parse_xml( Plist::Emit.dump(expected, false) ) 45 | 46 | assert_kind_of expected.class, actual 47 | assert_equal expected.foo, actual.foo 48 | end 49 | 50 | def test_generator_io_and_file 51 | expected = < 53 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 54 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 55 | 56 | END 57 | 58 | expected.chomp! 59 | 60 | fd = IO.sysopen('test/assets/example_data.bin') 61 | io = IO.open(fd, 'r') 62 | 63 | # File is a subclass of IO, so catching IO in the dispatcher should work for File as well... 64 | f = File.open('test/assets/example_data.bin') 65 | 66 | assert_equal expected, Plist::Emit.dump(io, false).chomp 67 | assert_equal expected, Plist::Emit.dump(f, false).chomp 68 | 69 | assert_instance_of StringIO, @result['io'] 70 | assert_instance_of StringIO, @result['file'] 71 | 72 | io.rewind 73 | f.rewind 74 | 75 | assert_equal io.read, @result['io'].read 76 | assert_equal f.read, @result['file'].read 77 | 78 | io.close 79 | f.close 80 | end 81 | 82 | def test_generator_string_io 83 | expected = < 85 | dGhpcyBpcyBhIHN0cmluZ2lvIG9iamVjdA== 86 | 87 | END 88 | 89 | sio = StringIO.new('this is a stringio object') 90 | 91 | assert_equal expected.chomp, Plist::Emit.dump(sio, false).chomp 92 | 93 | assert_instance_of StringIO, @result['stringio'] 94 | 95 | sio.rewind 96 | assert_equal sio.read, @result['stringio'].read 97 | end 98 | 99 | # this functionality is credited to Mat Schaffer, 100 | # who discovered the plist with the data tag 101 | # supplied the test data, and provided the parsing code. 102 | def test_data 103 | # test reading plist elements 104 | data = Plist::parse_xml("test/assets/example_data.plist"); 105 | assert_equal( File.open("test/assets/example_data.jpg"){|f| f.read }, data['image'].read ) 106 | 107 | # test writing data elements 108 | expected = File.read("test/assets/example_data.plist") 109 | result = data.to_plist 110 | #File.open('result.plist', 'w') {|f|f.write(result)} # debug 111 | assert_equal( expected, result ) 112 | 113 | # Test changing the object in the plist to a StringIO and writing. 114 | # This appears extraneous given that plist currently returns a StringIO, 115 | # so the above writing test also flexes StringIO#to_plist_node. 116 | # However, the interface promise is to return an IO, not a particular class. 117 | # plist used to return Tempfiles, which was changed solely for performance reasons. 118 | data['image'] = StringIO.new( File.read("test/assets/example_data.jpg")) 119 | 120 | assert_equal(expected, data.to_plist ) 121 | 122 | end 123 | 124 | end 125 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | = plist - All-purpose Property List manipulation library 2 | 3 | === Release version 3.0.0! 4 | 5 | 2010-02-23: 6 | * Ruby 1.9.x compatibility! 7 | 8 | 2010-02-16: 9 | * excise a bunch of unnecessary @@ variables 10 | * fix up some tests for cross-version compatibility 11 | 12 | 2010-02-14: 13 | * generalized cleanup: 14 | * fix old file headers 15 | * modernize rakefile 16 | * clean up rdoc 17 | 18 | 2010-01-08: 19 | * move from RubyForge Subversion to GitHub 20 | 21 | 2007-02-22 (r81): 22 | * make the plist parser accept strings contain XML or any object that responds to #read (File and StringIO being the intended targets here). Test and idea contributed by Chuck Remes. 23 | 24 | 2006-09-20 (r80): 25 | * tweak a comment in generator.rb to make it clear that we're not using Base64.b64encode because it's broken. 26 | 27 | === Release version 3.0.0! 28 | 29 | 2006-09-20 (r77 - r79): 30 | * move IndentedString inside Plist::Emit and :nodoc: it 31 | * Tag 3.0.0! (from rev 78) 32 | 33 | 2006-09-19 (r73 - r75): 34 | * Really fix the rakefile this time (apparently I deleted some code that I needed...) 35 | * alter the fix_whitespace rake task to ignore the assets directory 36 | * cleanup whitespace 37 | 38 | 2006-09-18 (r70 - r72): 39 | * Update this file ;) 40 | * Fix Rakefile 41 | * gem install -t now works correctly 42 | * Remove super-sekr1t rdoc staging area from rdoc publishing task 43 | 44 | 2006-09-15 (r64 - r69): 45 | * Change behavior of empty collection elements to match What Apple Does 46 | * Fix some gem packaging infrastructure 47 | 48 | 2006-09-13 (r61 - r63): 49 | * Merge generator injection removal branch into trunk! 50 | 51 | 2006-09-13 (r52 - r60): 52 | * Fix indentation/newlines in generator (finally!) 53 | * Refix indentation to be more faithful to the way Apple emits their plists 54 | * Remove horrific regex and replace it with proper comment parsing 55 | * Empty plists return nil when parsed 56 | * Sort hash keys before emitting (now we can test multi-element hashes!) 57 | * Inject #<=> into Symbol so that sorting Symbol-keyed hashes won't freak out 58 | 59 | 2006-09-12 (r47 - r51): 60 | * More test rejiggering 61 | * New tests to expose some bugs 62 | 63 | 2006-09-10 (r33 - r46): 64 | * Update tests for new generator code 65 | * Rejigger some tests 66 | * Make the generator try to call #to_plist_node on any object it tries to serialize, thus allowing class authors to define how their objects will be serialized 67 | * Marshal.dump unrecognized objects into elements 68 | * Make the parser strip out comments and Marshal.load elements if possible 69 | * Update some rdoc 70 | 71 | === Release version 2.1.1! 72 | 73 | 2006-09-10 (r31 - r32): 74 | * Added encoding / decoding for entities (& etc) 75 | * Changed parsing of elements to return StringIO objects 76 | * Fixed bug with empty tags 77 | 78 | 2006-08-24 (r25 - r30): 79 | * Invert ownership of methods in the generator, allowing us to remove the self.extend(self) 80 | * New branch to remove method inject from parser 81 | 82 | 2006-08-23 (r22 - r24): 83 | * Add rcov task to Rakefile 84 | * Add some tests 85 | 86 | 2006-08-20 (r9 - r21): 87 | * Add a bunch of rdoc and rdoc infrastructure 88 | * Add rake task to clean up errant whitespace 89 | * Spin off a branch to remove a bunch of method injection in the generator code 90 | * Rename some tests for clarity's sake 91 | * Replace NARF generation code with Ben's generation code 92 | * Update tests 93 | * This broke indentation (will be fixed later) 94 | * Add Plist::Emit.dump, so you can dump objects which don't include Plist::Emit, update tests to match 95 | * Fix a bug with the method that wraps output in the plist header/footer 96 | 97 | 2006-08-19 (r1 - r8): 98 | * The beginnings of merging the plist project into the NARF plist library (under the plist project's name) 99 | * fancier project infrastructure (more tests, Rakefile, the like) 100 | * Add/update copyright notices in the source files 101 | * Move a bunch of documentation out to README 102 | * Split library into chunks 103 | * Properly delete files when cleaning up from tests 104 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # 2 | # Plist Rakefile 3 | # 4 | # Based heavily on Geoffrey Grosenbach's Rakefile for gruff. 5 | # Includes whitespace-fixing task based on code from Typo. 6 | # 7 | # Copyright 2006-2010 Ben Bleything and Patrick May 8 | # Distributed under the MIT License 9 | # 10 | 11 | require 'fileutils' 12 | require 'rubygems' 13 | require 'rake' 14 | require 'rake/testtask' 15 | require 'rake/packagetask' 16 | require 'rake/contrib/rubyforgepublisher' 17 | require 'rubygems/package_task' 18 | 19 | $:.unshift(File.dirname(__FILE__) + "/lib") 20 | require 'plist' 21 | 22 | PKG_NAME = 'plist' 23 | PKG_VERSION = Plist::VERSION 24 | PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" 25 | 26 | RELEASE_NAME = "REL #{PKG_VERSION}" 27 | 28 | RUBYFORGE_PROJECT = "plist" 29 | RUBYFORGE_USER = ENV['RUBYFORGE_USER'] 30 | 31 | TEST_FILES = Dir.glob('test/test_*') 32 | TEST_ASSETS = Dir.glob('test/assets/*') 33 | LIB_FILES = Dir.glob('lib/**/*') 34 | RELEASE_FILES = [ "Rakefile", "README.rdoc", "CHANGELOG", "LICENSE" ] + LIB_FILES + TEST_FILES + TEST_ASSETS 35 | 36 | task :default => [ :test ] 37 | # Run the unit tests 38 | Rake::TestTask.new { |t| 39 | t.libs << "test" 40 | t.test_files = TEST_FILES 41 | t.verbose = true 42 | } 43 | 44 | desc "Clean pkg, coverage, and rdoc; remove .bak files" 45 | task :clean => [ :clobber_rdoc, :clobber_package, :clobber_coverage ] do 46 | puts cmd = "find . -type f -name *.bak -delete" 47 | `#{cmd}` 48 | end 49 | 50 | task :clobber_coverage do 51 | puts cmd = "rm -rf coverage" 52 | `#{cmd}` 53 | end 54 | 55 | desc "Generate coverage analysis with rcov (requires rcov to be installed)" 56 | task :rcov => [ :clobber_coverage ] do 57 | puts cmd = "rcov -Ilib --xrefs -T test/*.rb" 58 | puts `#{cmd}` 59 | end 60 | 61 | desc "Strip trailing whitespace and fix newlines for all release files" 62 | task :fix_whitespace => [ :clean ] do 63 | RELEASE_FILES.reject {|i| i =~ /assets/}.each do |filename| 64 | next if File.directory? filename 65 | 66 | File.open(filename) do |file| 67 | newfile = '' 68 | needs_love = false 69 | 70 | file.readlines.each_with_index do |line, lineno| 71 | if line =~ /[ \t]+$/ 72 | needs_love = true 73 | puts "#{filename}: trailing whitespace on line #{lineno}" 74 | line.gsub!(/[ \t]*$/, '') 75 | end 76 | 77 | if line.chomp == line 78 | needs_love = true 79 | puts "#{filename}: no newline on line #{lineno}" 80 | line << "\n" 81 | end 82 | 83 | newfile << line 84 | end 85 | 86 | if needs_love 87 | tempname = "#{filename}.new" 88 | 89 | File.open(tempname, 'w').write(newfile) 90 | File.chmod(File.stat(filename).mode, tempname) 91 | 92 | FileUtils.ln filename, "#{filename}.bak" 93 | FileUtils.ln tempname, filename, :force => true 94 | File.unlink(tempname) 95 | end 96 | end 97 | end 98 | end 99 | 100 | desc "Copy documentation to rubyforge" 101 | task :update_rdoc => [ :rdoc ] do 102 | Rake::SshDirPublisher.new("#{RUBYFORGE_USER}@rubyforge.org", "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}", "rdoc").upload 103 | end 104 | 105 | begin 106 | require 'rdoc/task' 107 | 108 | # Generate the RDoc documentation 109 | RDoc::Task.new do |rdoc| 110 | rdoc.title = "All-purpose Property List manipulation library" 111 | rdoc.main = "README.rdoc" 112 | 113 | rdoc.rdoc_dir = 'rdoc' 114 | rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'CHANGELOG') 115 | rdoc.rdoc_files.include('lib/**') 116 | 117 | rdoc.options = [ 118 | '-H', # show hash marks on method names in comments 119 | '-N', # show line numbers 120 | ] 121 | end 122 | rescue LoadError 123 | $stderr.puts "Could not load rdoc tasks" 124 | end 125 | 126 | # Create compressed packages 127 | spec = Gem::Specification.new do |s| 128 | s.name = PKG_NAME 129 | s.version = PKG_VERSION 130 | 131 | s.summary = "All-purpose Property List manipulation library." 132 | s.description = <<-EOD 133 | Plist is a library to manipulate Property List files, also known as plists. It can parse plist files into native Ruby data structures as well as generating new plist files from your Ruby objects. 134 | EOD 135 | 136 | s.authors = "Ben Bleything and Patrick May" 137 | s.homepage = "http://plist.rubyforge.org" 138 | 139 | s.rubyforge_project = RUBYFORGE_PROJECT 140 | 141 | s.has_rdoc = true 142 | 143 | s.files = RELEASE_FILES 144 | s.test_files = TEST_FILES 145 | 146 | s.autorequire = 'plist' 147 | end 148 | 149 | Gem::PackageTask.new(spec) do |p| 150 | p.gem_spec = spec 151 | p.need_tar = true 152 | p.need_zip = true 153 | end 154 | 155 | -------------------------------------------------------------------------------- /lib/plist/parser.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # = plist 4 | # 5 | # Copyright 2006-2010 Ben Bleything and Patrick May 6 | # Distributed under the MIT License 7 | # 8 | 9 | # Plist parses Mac OS X xml property list files into ruby data structures. 10 | # 11 | # === Load a plist file 12 | # This is the main point of the library: 13 | # 14 | # r = Plist::parse_xml( filename_or_xml ) 15 | module Plist 16 | # Note that I don't use these two elements much: 17 | # 18 | # + Date elements are returned as DateTime objects. 19 | # + Data elements are implemented as Tempfiles 20 | # 21 | # Plist::parse_xml will blow up if it encounters a Date element. 22 | # If you encounter such an error, or if you have a Date element which 23 | # can't be parsed into a Time object, please send your plist file to 24 | # plist@hexane.org so that I can implement the proper support. 25 | def Plist::parse_xml( filename_or_xml ) 26 | listener = Listener.new 27 | #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener) 28 | parser = StreamParser.new(filename_or_xml, listener) 29 | parser.parse 30 | listener.result 31 | end 32 | 33 | class Listener 34 | #include REXML::StreamListener 35 | 36 | attr_accessor :result, :open 37 | 38 | def initialize 39 | @result = nil 40 | @open = Array.new 41 | end 42 | 43 | 44 | def tag_start(name, attributes) 45 | @open.push PTag::mappings[name].new 46 | end 47 | 48 | def text( contents ) 49 | @open.last.text = contents if @open.last 50 | end 51 | 52 | def tag_end(name) 53 | last = @open.pop 54 | if @open.empty? 55 | @result = last.to_ruby 56 | else 57 | @open.last.children.push last 58 | end 59 | end 60 | end 61 | 62 | class StreamParser 63 | def initialize( plist_data_or_file, listener ) 64 | if plist_data_or_file.respond_to? :read 65 | @xml = plist_data_or_file.read 66 | elsif File.exist? plist_data_or_file 67 | @xml = File.read( plist_data_or_file ) 68 | else 69 | @xml = plist_data_or_file 70 | end 71 | 72 | @listener = listener 73 | end 74 | 75 | TEXT = /([^<]+)/ 76 | XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um 77 | DOCTYPE_PATTERN = /\s*)/um 78 | COMMENT_START = /\A/um 80 | 81 | 82 | def parse 83 | plist_tags = PTag::mappings.keys.join('|') 84 | start_tag = /<(#{plist_tags})([^>]*)>/i 85 | end_tag = /<\/(#{plist_tags})[^>]*>/i 86 | 87 | require 'strscan' 88 | 89 | @scanner = StringScanner.new( @xml ) 90 | until @scanner.eos? 91 | if @scanner.scan(COMMENT_START) 92 | @scanner.scan(COMMENT_END) 93 | elsif @scanner.scan(XMLDECL_PATTERN) 94 | elsif @scanner.scan(DOCTYPE_PATTERN) 95 | elsif @scanner.scan(start_tag) 96 | @listener.tag_start(@scanner[1], nil) 97 | if (@scanner[2] =~ /\/$/) 98 | @listener.tag_end(@scanner[1]) 99 | end 100 | elsif @scanner.scan(TEXT) 101 | @listener.text(@scanner[1]) 102 | elsif @scanner.scan(end_tag) 103 | @listener.tag_end(@scanner[1]) 104 | else 105 | raise "Unimplemented element" 106 | end 107 | end 108 | end 109 | end 110 | 111 | class PTag 112 | @@mappings = { } 113 | def PTag::mappings 114 | @@mappings 115 | end 116 | 117 | def PTag::inherited( sub_class ) 118 | key = sub_class.to_s.downcase 119 | key.gsub!(/^plist::/, '' ) 120 | key.gsub!(/^p/, '') unless key == "plist" 121 | 122 | @@mappings[key] = sub_class 123 | end 124 | 125 | attr_accessor :text, :children 126 | def initialize 127 | @children = Array.new 128 | end 129 | 130 | def to_ruby 131 | raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}" 132 | end 133 | end 134 | 135 | class PList < PTag 136 | def to_ruby 137 | children.first.to_ruby if children.first 138 | end 139 | end 140 | 141 | class PDict < PTag 142 | def to_ruby 143 | dict = Hash.new 144 | key = nil 145 | 146 | children.each do |c| 147 | if key.nil? 148 | key = c.to_ruby 149 | else 150 | dict[key] = c.to_ruby 151 | key = nil 152 | end 153 | end 154 | 155 | dict 156 | end 157 | end 158 | 159 | class PKey < PTag 160 | def to_ruby 161 | CGI::unescapeHTML(text || '') 162 | end 163 | end 164 | 165 | class PString < PTag 166 | def to_ruby 167 | CGI::unescapeHTML(text || '') 168 | end 169 | end 170 | 171 | class PArray < PTag 172 | def to_ruby 173 | children.collect do |c| 174 | c.to_ruby 175 | end 176 | end 177 | end 178 | 179 | class PInteger < PTag 180 | def to_ruby 181 | text.to_i 182 | end 183 | end 184 | 185 | class PTrue < PTag 186 | def to_ruby 187 | true 188 | end 189 | end 190 | 191 | class PFalse < PTag 192 | def to_ruby 193 | false 194 | end 195 | end 196 | 197 | class PReal < PTag 198 | def to_ruby 199 | text.to_f 200 | end 201 | end 202 | 203 | require 'date' 204 | class PDate < PTag 205 | def to_ruby 206 | DateTime.parse(text) 207 | end 208 | end 209 | 210 | require 'base64' 211 | class PData < PTag 212 | def to_ruby 213 | data = Base64.decode64(text.gsub(/\s+/, '')) unless text.nil? 214 | begin 215 | return Marshal.load(data) 216 | rescue Exception => e 217 | io = StringIO.new 218 | io.write data 219 | io.rewind 220 | return io 221 | end 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /test/assets/AlbumData.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Application Version 6 | 5.0.4 (263) 7 | Archive Path 8 | /Users/username/Pictures/iPhoto Library 9 | List of Albums 10 | 11 | 12 | AlbumId 13 | 999000 14 | AlbumName 15 | Library 16 | KeyList 17 | 18 | 7 19 | 20 | Master 21 | 22 | PhotoCount 23 | 1 24 | PlayMusic 25 | YES 26 | RepeatSlideShow 27 | YES 28 | SecondsPerSlide 29 | 3 30 | SlideShowUseTitles 31 | 32 | SongPath 33 | 34 | TransitionDirection 35 | 0 36 | TransitionName 37 | Dissolve 38 | TransitionSpeed 39 | 1 40 | 41 | 42 | Album Type 43 | Special Roll 44 | AlbumId 45 | 999001 46 | AlbumName 47 | Last Roll 48 | Filter Mode 49 | All 50 | Filters 51 | 52 | 53 | Count 54 | 1 55 | Operation 56 | In Last 57 | Type 58 | Roll 59 | 60 | 61 | KeyList 62 | 63 | 7 64 | 65 | PhotoCount 66 | 1 67 | PlayMusic 68 | YES 69 | RepeatSlideShow 70 | YES 71 | SecondsPerSlide 72 | 3 73 | SlideShowUseTitles 74 | 75 | SongPath 76 | 77 | TransitionDirection 78 | 0 79 | TransitionName 80 | Dissolve 81 | TransitionSpeed 82 | 1 83 | 84 | 85 | Album Type 86 | Special Month 87 | AlbumId 88 | 999002 89 | AlbumName 90 | Last 12 Months 91 | Filter Mode 92 | All 93 | Filters 94 | 95 | KeyList 96 | 97 | 7 98 | 99 | PhotoCount 100 | 1 101 | PlayMusic 102 | YES 103 | RepeatSlideShow 104 | YES 105 | SecondsPerSlide 106 | 3 107 | SlideShowUseTitles 108 | 109 | SongPath 110 | 111 | TransitionDirection 112 | 0 113 | TransitionName 114 | Dissolve 115 | TransitionSpeed 116 | 1 117 | 118 | 119 | Album Type 120 | Regular 121 | AlbumId 122 | 9 123 | AlbumName 124 | An Album 125 | KeyList 126 | 127 | 7 128 | 129 | PhotoCount 130 | 1 131 | PlayMusic 132 | YES 133 | RepeatSlideShow 134 | YES 135 | SecondsPerSlide 136 | 3 137 | SlideShowUseTitles 138 | 139 | SongPath 140 | 141 | TransitionDirection 142 | 0 143 | TransitionName 144 | Dissolve 145 | TransitionSpeed 146 | 1 147 | 148 | 149 | List of Keywords 150 | 151 | List of Rolls 152 | 153 | 154 | Album Type 155 | Regular 156 | AlbumId 157 | 6 158 | AlbumName 159 | Roll 1 160 | KeyList 161 | 162 | 7 163 | 164 | Parent 165 | 999000 166 | PhotoCount 167 | 1 168 | 169 | 170 | Major Version 171 | 2 172 | Master Image List 173 | 174 | 7 175 | 176 | Aspect Ratio 177 | 1 178 | Caption 179 | fallow_keep.png.450x450.2005-12-04 180 | Comment 181 | a comment 182 | DateAsTimerInterval 183 | 158341389 184 | ImagePath 185 | /Users/username/Pictures/iPhoto Library/2006/01/07/fallow_keep.png.450x450.2005-12-04.jpg 186 | MediaType 187 | Image 188 | MetaModDateAsTimerInterval 189 | 158341439.728129 190 | ModDateAsTimerInterval 191 | 158341389 192 | Rating 193 | 0 194 | Roll 195 | 6 196 | ThumbPath 197 | /Users/username/Pictures/iPhoto Library/2006/01/07/Thumbs/7.jpg 198 | 199 | 200 | Minor Version 201 | 0 202 | 203 | 204 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = All-purpose Property List manipulation library 2 | 3 | Plist is a library to manipulate Property List files, also known as plists. It can parse plist files into native Ruby data structures as well as generating new plist files from your Ruby objects. 4 | 5 | == Usage 6 | 7 | === Parsing 8 | 9 | result = Plist::parse_xml('path/to/example.plist') 10 | 11 | result.class 12 | => Hash 13 | 14 | "#{result['FirstName']} #{result['LastName']}" 15 | => "John Public" 16 | 17 | result['ZipPostal'] 18 | => "12345" 19 | 20 | ==== Example Property List 21 | 22 | 23 | 24 | 25 | 26 | FirstName 27 | John 28 | 29 | LastName 30 | Public 31 | 32 | StreetAddr1 33 | 123 Anywhere St. 34 | 35 | StateProv 36 | CA 37 | 38 | City 39 | Some Town 40 | 41 | CountryName 42 | United States 43 | 44 | AreaCode 45 | 555 46 | 47 | LocalPhoneNumber 48 | 5551212 49 | 50 | ZipPostal 51 | 12345 52 | 53 | 54 | 55 | === Generation 56 | 57 | plist also provides the ability to generate plists from Ruby objects. The following Ruby classes are converted into native plist types: 58 | Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false 59 | 60 | * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the and containers (respectively). 61 | * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a element. 62 | * User classes may implement +to_plist_node+ to dictate how they should be serialized; otherwise the object will be passed to Marshal.dump and the result placed in a element. See below for more details. 63 | 64 | ==== Creating a plist 65 | 66 | There are two ways to generate complete plists. Given an object: 67 | 68 | obj = [1, :two, {'c' => 0xd}] 69 | 70 | If you've mixed in Plist::Emit (which is already done for +Array+ and +Hash+), you can simply call +to_plist+: 71 | 72 | obj.to_plist 73 | 74 | This is equivalent to calling Plist::Emit.dump(obj). Either one will yield: 75 | 76 | 77 | 78 | 79 | 80 | 1 81 | two 82 | 83 | c 84 | 13 85 | 86 | 87 | 88 | 89 | You can also dump plist fragments by passing +false+ as the second parameter: 90 | 91 | Plist::Emit.dump('holy cow!', false) 92 | => "holy cow!" 93 | 94 | ==== Custom serialization 95 | 96 | If your class can be safely coerced into a native plist datatype, you can implement +to_plist_node+. Upon encountering an object of a class it doesn't recognize, the plist library will check to see if it responds to +to_plist_node+, and if so, insert the result of that call into the plist output. 97 | 98 | An example: 99 | 100 | class MyFancyString 101 | ... 102 | 103 | def to_plist_node 104 | return "#{self.defancify}" 105 | end 106 | end 107 | 108 | When you attempt to serialize a +MyFancyString+ object, the +to_plist_node+ method will be called and the object's contents will be defancified and placed in the plist. 109 | 110 | If for whatever reason you can't add this method, your object will be serialized with Marshal.dump instead. 111 | 112 | == Links 113 | 114 | [Project Page] http://plist.rubyforge.org 115 | [GitHub] http://github.com/bleything/plist 116 | [RDoc] http://plist.rubyforge.org 117 | 118 | == Credits 119 | 120 | plist is maintained by Ben Bleything and Patrick May . Patrick wrote most of the code; Ben is a recent addition to the project, having merged in his plist generation library. 121 | 122 | Other folks who have helped along the way: 123 | 124 | [Martin Dittus] who pointed out that +Time+ wasn't enough for plist Dates, especially those in ~/Library/Cookies/Cookies.plist 125 | [Chuck Remes] who pushed Patrick towards implementing #to_plist 126 | [Mat Schaffer] who supplied code and test cases for elements 127 | [Michael Granger] for encouragement and help 128 | [Carsten Bormann, Chris Hoffman, Dana Contreras, Hongli Lai, Johan Sørensen] for contributing Ruby 1.9.x compatibility fixes 129 | 130 | == License and Copyright 131 | 132 | plist is released under the MIT License. 133 | 134 | Portions of the code (notably the Rakefile) contain code pulled and/or adapted from other projects. These files contain a comment at the top describing what was used. 135 | 136 | === MIT License 137 | 138 | Copyright (c) 2006-2010, Ben Bleything and Patrick May 139 | 140 | Permission is hereby granted, free of charge, to any person obtaining 141 | a copy of this software and associated documentation files (the 142 | "Software"), to deal in the Software without restriction, including 143 | without limitation the rights to use, copy, modify, merge, publish, 144 | distribute, sublicense, and/or sell copies of the Software, and to 145 | permit persons to whom the Software is furnished to do so, subject to 146 | the following conditions: 147 | 148 | The above copyright notice and this permission notice shall be included 149 | in all copies or substantial portions of the Software. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 152 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 153 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 154 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 155 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 156 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 157 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 158 | 159 | -------------------------------------------------------------------------------- /lib/plist/generator.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # = plist 4 | # 5 | # Copyright 2006-2010 Ben Bleything and Patrick May 6 | # Distributed under the MIT License 7 | # 8 | 9 | module Plist ; end 10 | 11 | # === Create a plist 12 | # You can dump an object to a plist in one of two ways: 13 | # 14 | # * Plist::Emit.dump(obj) 15 | # * obj.to_plist 16 | # * This requires that you mixin the Plist::Emit module, which is already done for +Array+ and +Hash+. 17 | # 18 | # The following Ruby classes are converted into native plist types: 19 | # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false 20 | # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the and containers (respectively). 21 | # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a element. 22 | # * User classes may implement +to_plist_node+ to dictate how they should be serialized; otherwise the object will be passed to Marshal.dump and the result placed in a element. 23 | # 24 | # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below. 25 | module Plist::Emit 26 | # Helper method for injecting into classes. Calls Plist::Emit.dump with +self+. 27 | def to_plist(envelope = true) 28 | return Plist::Emit.dump(self, envelope) 29 | end 30 | 31 | # Helper method for injecting into classes. Calls Plist::Emit.save_plist with +self+. 32 | def save_plist(filename) 33 | Plist::Emit.save_plist(self, filename) 34 | end 35 | 36 | # The following Ruby classes are converted into native plist types: 37 | # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time 38 | # 39 | # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes. 40 | # 41 | # +IO+ and +StringIO+ objects are encoded and placed in elements; other objects are Marshal.dump'ed unless they implement +to_plist_node+. 42 | # 43 | # The +envelope+ parameters dictates whether or not the resultant plist fragment is wrapped in the normal XML/plist header and footer. Set it to false if you only want the fragment. 44 | def self.dump(obj, envelope = true) 45 | output = plist_node(obj) 46 | 47 | output = wrap(output) if envelope 48 | 49 | return output 50 | end 51 | 52 | # Writes the serialized object's plist to the specified filename. 53 | def self.save_plist(obj, filename) 54 | File.open(filename, 'wb') do |f| 55 | f.write(obj.to_plist) 56 | end 57 | end 58 | 59 | private 60 | def self.plist_node(element) 61 | output = '' 62 | 63 | if element.respond_to? :to_plist_node 64 | output << element.to_plist_node 65 | else 66 | case element 67 | when Array 68 | if element.empty? 69 | output << "\n" 70 | else 71 | output << tag('array') { 72 | element.collect {|e| plist_node(e)} 73 | } 74 | end 75 | when Hash 76 | if element.empty? 77 | output << "\n" 78 | else 79 | inner_tags = [] 80 | 81 | element.keys.sort_by{|k| k.to_s }.each do |k| 82 | v = element[k] 83 | inner_tags << tag('key', CGI::escapeHTML(k.to_s)) 84 | inner_tags << plist_node(v) 85 | end 86 | 87 | output << tag('dict') { 88 | inner_tags 89 | } 90 | end 91 | when true, false 92 | output << "<#{element}/>\n" 93 | when Time 94 | output << tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ')) 95 | when Date # also catches DateTime 96 | output << tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ')) 97 | when String, Symbol, Fixnum, Bignum, Integer, Float 98 | output << tag(element_type(element), CGI::escapeHTML(element.to_s)) 99 | when IO, StringIO 100 | element.rewind 101 | contents = element.read 102 | # note that apple plists are wrapped at a different length then 103 | # what ruby's base64 wraps by default. 104 | # I used #encode64 instead of #b64encode (which allows a length arg) 105 | # because b64encode is b0rked and ignores the length arg. 106 | data = "\n" 107 | Base64::encode64(contents).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" } 108 | output << tag('data', data) 109 | else 110 | output << comment( 'The element below contains a Ruby object which has been serialized with Marshal.dump.' ) 111 | data = "\n" 112 | Base64::encode64(Marshal.dump(element)).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" } 113 | output << tag('data', data ) 114 | end 115 | end 116 | 117 | return output 118 | end 119 | 120 | def self.comment(content) 121 | return "\n" 122 | end 123 | 124 | def self.tag(type, contents = '', &block) 125 | out = nil 126 | 127 | if block_given? 128 | out = IndentedString.new 129 | out << "<#{type}>" 130 | out.raise_indent 131 | 132 | out << block.call 133 | 134 | out.lower_indent 135 | out << "" 136 | else 137 | out = "<#{type}>#{contents.to_s}\n" 138 | end 139 | 140 | return out.to_s 141 | end 142 | 143 | def self.wrap(contents) 144 | output = '' 145 | 146 | output << '' + "\n" 147 | output << '' + "\n" 148 | output << '' + "\n" 149 | 150 | output << contents 151 | 152 | output << '' + "\n" 153 | 154 | return output 155 | end 156 | 157 | def self.element_type(item) 158 | case item 159 | when String, Symbol 160 | 'string' 161 | 162 | when Fixnum, Bignum, Integer 163 | 'integer' 164 | 165 | when Float 166 | 'real' 167 | 168 | else 169 | raise "Don't know about this data type... something must be wrong!" 170 | end 171 | end 172 | private 173 | class IndentedString #:nodoc: 174 | attr_accessor :indent_string 175 | 176 | def initialize(str = "\t") 177 | @indent_string = str 178 | @contents = '' 179 | @indent_level = 0 180 | end 181 | 182 | def to_s 183 | return @contents 184 | end 185 | 186 | def raise_indent 187 | @indent_level += 1 188 | end 189 | 190 | def lower_indent 191 | @indent_level -= 1 if @indent_level > 0 192 | end 193 | 194 | def <<(val) 195 | if val.is_a? Array 196 | val.each do |f| 197 | self << f 198 | end 199 | else 200 | # if it's already indented, don't bother indenting further 201 | unless val =~ /\A#{@indent_string}/ 202 | indent = @indent_string * @indent_level 203 | 204 | @contents << val.gsub(/^/, indent) 205 | else 206 | @contents << val 207 | end 208 | 209 | # it already has a newline, don't add another 210 | @contents << "\n" unless val =~ /\n$/ 211 | end 212 | end 213 | end 214 | end 215 | 216 | class Array #:nodoc: 217 | include Plist::Emit 218 | end 219 | 220 | class Hash #:nodoc: 221 | include Plist::Emit 222 | end 223 | -------------------------------------------------------------------------------- /test/assets/example_data.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IMs 6 | 7 | com.apple.syncservices:9583BE42-EC1A-4B3F-B248-7904BA60634B 8 | com.apple.syncservices:2A222E96-4CC6-4320-BE0C-61D5F08C2E64 9 | com.apple.syncservices:64DAA772-4558-49F8-9B87-C4A32D5E05C1 10 | com.apple.syncservices:AF7C6884-B760-4E55-A4D5-B1B114C8A7DC 11 | com.apple.syncservices:ECC7FAED-47D6-4DB9-8120-2F029C7D64C8 12 | com.apple.syncservices:D704F079-F869-4613-9CFC-697C1976E8F4 13 | 14 | URLs 15 | 16 | com.apple.syncservices:5BA97109-F8E3-46A9-AF0B-5F8C093F49EA 17 | 18 | com.apple.syncservices.RecordEntityName 19 | com.apple.contacts.Contact 20 | display as company 21 | person 22 | email addresses 23 | 24 | com.apple.syncservices:B97DCC9C-5B00-4D38-AB06-4B7A5D6BC369 25 | com.apple.syncservices:E508D679-43E1-49E2-B5D7-F14A8E48C067 26 | com.apple.syncservices:C6478063-34A5-4CCB-BD41-1F131D56F7BD 27 | com.apple.syncservices:2B3E352C-7831-4349-9A87-0FA4BD290515 28 | 29 | first name 30 | Mat 31 | image 32 | 33 | /9j/4AAQSkZJRgABAQAAAQABAAD/7QAcUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAD/ 34 | 4g9ASUNDX1BST0ZJTEUAAQEAAA8wYXBwbAIAAABtbnRyUkdCIFhZWiAH1gAFABUAFAAg 35 | AAZhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGxx 36 | EHyAeZ4fvAacU+DW7cbVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5yWFla 37 | AAABLAAAABRnWFlaAAABQAAAABRiWFlaAAABVAAAABR3dHB0AAABaAAAABRjaGFkAAAB 38 | fAAAACxyVFJDAAABqAAAAA5nVFJDAAABuAAAAA5iVFJDAAAByAAAAA52Y2d0AAAB2AAA 39 | BhJuZGluAAAH7AAABj5kZXNjAAAOLAAAAGRkc2NtAAAOkAAAAEhtbW9kAAAO2AAAAChj 40 | cHJ0AAAPAAAAAC1YWVogAAAAAAAAZwIAADyoAAAJjlhZWiAAAAAAAABowAAAq7QAAB5o 41 | WFlaIAAAAAAAACcUAAAXvwAAqy9YWVogAAAAAAAA81IAAQAAAAEWz3NmMzIAAAAAAAEM 42 | QgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsY3VydgAAAAAAAAABAc0AAGN1 43 | cnYAAAAAAAAAAQHNAABjdXJ2AAAAAAAAAAEBzQAAdmNndAAAAAAAAAAAAAMBAAACAAAA 44 | JwCJARYBsAJsA0kESAV0BsgIPAnXC5YNcA9oEX0ToxXSGAwaJRvMHW4fDSCbIi8jtSUy 45 | JqooFil9KuEsOS2MLtwwLTF1Mr80CDVINmQ3cjh8OYs6nTuuPL890j7lP/hBCUIUQxtE 46 | H0UdRhhHDkf9SOhJ30r0TBxNQE5iT4NQo1HEUudUDVUxVlZXhFizWeRbGlxSXY9e0mAU 47 | YVtin2PhZSBmWmeWaMtp/WsybGJtkm7Gb/hxKnJhc5502HYYd1d4mHmyerx7wHzFfc9+ 48 | 1n/egOmB94MHhBiFLIZCh1uIdomViraL2Yz+jiKPRZBokY+SrpPMlOWV/ZcVmCmZOJpG 49 | m1ScX51onnCfdqB8oYGiiqORpJmlo6asp7yozancqumr9q0ArgWvBK/8sO+x2bK/s5q0 50 | bLU7thK2/7f8uPu5+Lr0u+68573bvtS/0MDKwcnCzMPUxN/F88cLyCPJQMpFyyHL5cyq 51 | zXHOOM79z8XQkdFe0izS/dPP1KbVf9Za1zjYF9j42dnamNtD2+TchN0i3cDeWt7z34zg 52 | JuC/4Vjh8OKL4yfjxeRi5QHlouZN5wDnwOh76TPp5eqP6zPr0exj7PLtce3w7l/uzu8z 53 | 75Pv9PBK8KDw+/GN8iny2fOD9Cn0yfVp9gj2qfdM9/H4ovla+hr66vvN/MD9yf7Q//8A 54 | AAAUAEYAkQD0AYQCNQMQBA0FPAaSCA8Jtgt1DUoPNBEtEy0VNhcUGJ0aIhujHRsekB/+ 55 | IWIixCQgJWwmuyf8KTcqcCumLNUuAy8vME0xXjJoM200cjV3Nnk3ejh6OXg6dDtwPGc9 56 | Wz5NPzxAKEEUQf1C5EPRRM9F1kbcR+JI50nsSvBL9kz+TgNPB1ANURNSF1MdVCJVJ1Yv 57 | VzNYOVk/WkZbTlxTXV9ebF95YIxhomK9Y+BlBWYuZ15ol2nQaw5sTG2NbqdvsXC1cbpy 58 | xHPLdNN13nbsd/x5DXohezd8UH1rfop/q4DOgfODF4Q6hV2GhIejiMGJ2oryjAqNHo4t 59 | jzuQSZFUkl2TZZRrlXGWdpd/mIWZi5qQm5Gcl52dnp6fnKCdoZiilaOMpICldKZjp1Go 60 | PKkkqg2q9KvarL6tqK6Rr4Cwb7FgslOzSbRDtT+2PLc8uD25QLpFu0m8Tb1Qvk2/O8Ah 61 | wQrB9sLiw83EusWrxprHich5yWjKVctDzC/NGs4DzurPz9DE0cPSvNOx1J/VhtZm10HY 62 | F9jp2brahttT3CHc8N3F3prfdOBR4Svh+uLA44XkSeUO5dPml+db6B/o4+mm6mrrLuvz 63 | 7Lbteu4+7wPvx/CM8VDyFPLX8530YfUk9er2rfdx+DX4+vm9+oH7RfwK/M79kv5W/x7/ 64 | /wAAAAkAIABDAHAApwDoAVAB1gJ8Az4EHAUcBjUHXwiaCecLPAyVDekPExA2EVgSdBON 65 | FKEVrxa6F7sYtRmoGpMbdxxWHTAeAh7QH5kgZCE6Ih8i/yPgJL0llyZvJ0UoGijpKbgq 66 | hCtPLBcs3S2jLmgvLS/xMLQxdzI6MvYzrzRmNRo1yzZ8Ny432jiJOTk56jqdO1I8CjzF 67 | PYQ+RD8IP8xAkUFVQhZC2UOZRFhFFkXQRopHQ0f0SKRJVEoBSqxLVUv5TJVNJ02tTjBO 68 | tE85T75QRFDMUVZR41JyUwNTl1QtVMZVYVYAVqBXQlgDWO1Z81r7XANdCl4QXxpgKGE1 69 | YkNjVGRqZYJmm2e4aNdp92sabEFtaW6Nb69wzXHrcwh0H3UwdkB3SXhQeVB6SntDfDV9 70 | JX4Rfvp/44DPgciCx4PFhMCFuoauh5+IiYlyilqLPowfjQCN4I7Aj6GQg5FikkSTHpPt 71 | lLWVgZZTlyeX/JjWmbaal5t6nGKdSZ4ynxygBqDvodeivqOjpJill6aSp4eoeKlkqkur 72 | L6wTrPet3a7Gr7GwqLGgsqazr7S/tdu2+rgeuUS6aLuJvKu9yb7owATBIMI6w1TEb8WM 73 | xqrHx8jpygjLLMxSzYHOxtAm0YvS/9SF1hTXstlr2zXdFt8M4SjjZOXI6G7rae7F8ur4 74 | 1P//AABuZGluAAAAAAAABjYAAJQXAABYjQAAUbUAAI5iAAAoWwAAFqgAAFANAABUOQAC 75 | o9cAAkAAAAFKPQADAQAAAgAAABQALQBGAF8AdwCOAKUAvADTAOkA/wEWASwBQwFZAXAB 76 | hgGdAbQBywHjAfsCEwIsAkcCYgKGAqoC0AL3Ax8DSgN1A6EDzwP/BDEEZASZBNEFCgVF 77 | BYEFwQYCBkYGigbQBxkHYweuB/sISwimCQgJbwnVCjwKpQsQC3wL6gxZDMsNPw24DjQO 78 | tA85D8EQTxDjEXwSChKIEwUThxQKFI8VFxWfFikWsxc9F8oYWBjjGW8Z/BqJGxUbohwu 79 | HLkdRR3SHl4e7R99IBAgpSE+IdYiciMSI7MkVCT5JaAmRCbsJ5UoPyjnKY0qNyrfK4cs 80 | MizZLZkuaS8/MBgw7jHGMqIzfDRWNS82CDbjN7w4ljluOkc7Hjv1PMw9pD58P1dAM0ER 81 | QexCz0O0RJ9FjEZ5R2tIYkleSlxLW0xfTWdOcE99UIxRnVKsU75U0VXjVvhYCFkXWipb 82 | QVxYXXRelV++YPNiLmN6ZM1mNmewaTVqkmvTbQ9uUm+WcOByLHOBdNJ2IHdyeL96CXtO 83 | fJF9zH8CgDuBb4KjhBWF2oehiWOLLYz1jrSQdpIwk+yVnZdOmPiaopxKnfOf66JEpKen 84 | FqmZrCGurbFCs8+2V7jhu2C9qr/JwfzESsa7yV/MRs+Y02vX2tzd39riV+T157bqg+1L 85 | 8AjykvT69yr5LfsI/L3+g///AAAAJwBHAGQAgACaALMAzADkAPwBEwErAUMBWwF0AYwB 86 | pQG+AdgB8gIMAicCRQJkAooCsgLbAwUDMQNfA44DvgPxBCYEXASUBM4FDAVKBYsF0AYX 87 | BmAGqgb4B0gHmgfuCEcIpgkKCXIJ3ApICrcLKQudDBQMjw0LDYwOEQ6ZDyYPthBJEOER 88 | fBISEp4TLBO8FE8U4xV6FhMWrBdGF+UYhRklGcgabhsUG70caB0SHcEecB8hH9MghiE9 89 | IfEipiNdJBEkxSV5Jikm2CeIKDYo4imKKjUq3iuHLDIs2S2ZLmkvPzAYMO4xxjKiM3w0 90 | VjUvNgg24ze8OJY5bjpHOx479TzMPaQ+fD9XQDNBEUHsQs9DtESfRYxGeUdrSGJJXkpc 91 | S1tMX01nTnBPfVCMUZ1SrFO/VNRV7FcKWCNZP1phW4hcrl3cXwpgQWF+YrxkBWVOZqFn 92 | 92lPaqxsD21qbslwIHF5ctB0KHV5dsl4GXloerV8An1Pfpp/6oE8go6D9IV1hveIc4nw 93 | i3OM8o5tj+2Rb5LxlHmWApePmR+atpxRnfOffKD2onukCqWmp1GpEKrbrLSulLCBsmu0 94 | WbY3uBe57bu7va2/vMHSw+nGBMghykHMZ86P0LfS4dUP1z/Zctum3dvgFuJV5I7m1ekU 95 | 617tqu/x8kT0lPbm+Tr7k/3w//8AAABVAIoAtwDhAQgBLgFUAXoBoAHGAe0CFQI+AmoC 96 | ngLUAwwDRwOFA8UECQRQBJoE6gU+BZcF9wZcBscHOQexCDEIrAkhCZsKFwqZCyALqww6 97 | DNANag4KDq8PWRAHELkRbxIqEugTsBSAFVoWOhcfGA0Y/BnsGtwbyxy3HaMejR96IGoh 98 | YSJZI1skYCVvJoInpSjPKgArPSyHLf8vqzFXMwc0sTZUN/E5hjsVPJ4+JD9kQFtBUUJG 99 | Qz9EOUU2Ri9HJ0giSR5KGEsPTAVM/U3zTuhP3lDUUclSu1OvVKdVoVaiV6NYpVmsWrtb 100 | z1zlXgVfKGBVYYtixGQJZVBmomf3aU9qlWvTbRBuVW+ccOxyQ3OndQ52eXfveWl65nxo 101 | feh/aYDxgnWEE4XQh4eJMIrbjICOGY+wkUaS1ZRolfiXipkdmrWcUZ3zn3yg9qJ5pAal 102 | nac+qO2qoKxWrgmvubFlsv+0mbYct5+5GbqEu++9U761wBnBgcLvxFrFzMc+yLTKLMuo 103 | zSbOpNAh0Z/TG9Sa1hTXlNkQ2oncAd1q3rvf+eE24m/jneTA5eDm/egT6STqJ+sm7CHt 104 | E+4B7unv0PCp8X/yTfMZ8930m/VX9gT2svdW9+/4ifkZ+aL6K/qv+x37jPv6/Gj8tv0E 105 | /VL9oP3v/jz+gf7G/wz/Uf+X/9z//wAAZGVzYwAAAAAAAAAKQ29sb3IgTENEAAAAAAAA 106 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 107 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAAgAAAAxlblVTAAAAEgAAAChq 108 | YUpQAAAADgAAADoAQwBvAGwAbwByACAATABDAEQwqzDpMPwAIABMAEMARG1tb2QAAAAA 109 | AAAGEAAAnF4AAAAAwEuKAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAENvcHlyaWdodCBB 110 | cHBsZSBDb21wdXRlciwgSW5jLiwgMjAwNQAAAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEB 111 | AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBD 112 | AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB 113 | AQEBAQEBAQEBAQEBAQH/wAARCAAwADADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAA 114 | AAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1Fh 115 | ByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT 116 | VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz 117 | tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA 118 | AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC 119 | AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2 120 | Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaX 121 | mJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP0 122 | 9fb3+Pn6/9oADAMBAAIRAxEAPwDd+Bfxs/4J+ftC/EFPhp4M+LXjLQtVl8P6pr9trvxE 123 | 8L2/gfwldRaU9tHPYw6/rU6WZ1OcXJms7F3R7tLe5WItKio3+XePyHPcBh3icwwjwNNT 124 | VOVarjcsqSVacJTUZ4bB47FV4RajJzqTgqaulKXPKKf9XVcGklKhWpYyV1y0cHDGynGH 125 | M06inVwdOnJKTimoTlJ/yuKcl+c/7cf7TXw68GfEbWvg38B/HWtXuv8AgbxrbC88aeCN 126 | O8KeNtJ8W+HU0e2uJrRxr+lR6Vosa63Pc6Zd3Ed3dfakshcWN7FHPiT6nhzheq8PLM8f 127 | gMFjcur4R0qOIzXHV8DQp4uT9+pg/wCzpvE4yrQWigoToXlKUoylGy+SzDGqVaOFpYzE 128 | 4XE066nUw+Hw1PE1p0opcsayxEVRw8KjbV5WqWS5XGMtfCfCn/BSS1vtOvLddC0uw1zR 129 | TJDqGha0ltZ6zfmFTvNlOHjsGuCzRiWAWjqpwkZLudmmZ+GWd4KeGcatOvgsYlKlmWFo 130 | 1MVhKdOcrpVoPmqqPLGVqlRqLSTUVq3WD4jybEQrc/to4jDc6q4OtVp0a8pRi1aml7sr 131 | N/BHmaejfb6p/ZM/bi+E3x2+I+g/CT4qG0+HHijxfr9h4X8K6tpkU2peHZ9Z1G4e2tLb 132 | Wr28Nt9iW6uHtrRLq2t3t47uRvPEUTKEMdwRnmXU54jDqOIwMaEcR7edOFOpHl1nCph4 133 | 1pzpxkmpU5pybjuklFvKnm+V4m0I1qlPFKrKnKjJ80XHRwdOr7OPPKLUlOL5UmlZ6tH7 134 | LaX+wnruj+JtYQ6v4bg8O3TtdQX1tNfXGtz3S+X5Zvra4shbxoN9wD5NwTH8hwQ7AfNu 135 | WJlGC5G5RUZKU3BRV/j5eXmqKWiSbjzScfeS6dVOFOMpPn91t7J3fVN3dna6Ts1vpre/ 136 | e2n7I4sZgB4khcAjk2YDdQTzjnkZ7E4x0rnlGu5cynCNONpu655L4ZN83urduTVo37M7 137 | KSprVOTu2lb1vtqn5NN9rtn8DNrPotj4P1nxWuqaa+t6JqOmwWPg14rqPVb2O7ErHXpJ 138 | 5rZdNt9J06eOOGRZLiS8up3MUduqL5j/ANUzweIxWOw+AqOpTw+LjVjUxyXtKUVCLlPD 139 | QVJuo8RUhtpGEYtTbetvhcTmdLC4CtjcN7KtKlOEIwjUgnGdRyUKklLXlXLJ6xXM9r2l 140 | E89i8deMPFHiAW3h3SrS417WIUl1vVpsyM1pJCYUgUkGGzRYwpmntmSacARMTGSrfT4L 141 | g7A08LGjVq4mWFwtSUcvwFFKEYVIzU6lepK6dRJuSjGvenT+OK5uVnx8M2zLMcXKOEo0 142 | /rGIUZYvE1E23Brl5Fpy01aMbuHvTej926PQbb4J3MxhvNUvmj1mYK5mtWMaKwXaN7ke 143 | Y7BAEBUDcFAICjJ6MVUxuBi8O6EVhZKShTneXLHmV780LJOUvhs2pN+r+mwnCWHqRjXr 144 | YmbxVrylBta2fvJ3u5KNldtaaNaHkviCfW/CniVGlnurm4tLlvMuWZ7ad0tmVrW6EgkD 145 | NJGMNDcRtBNDJGhEnTFYbB4PGYOrQjCnSTSVOKinBTn/ABIRjKCcYyfxU5xlGUZtcu58 146 | bmlPFZZmEnz1Kjpyvzy5otqLtTm2pLmlFRVpQcWmrN6K39eMX/BXvxf8Ef8AglZ+yn+1 147 | FqHgOL4ufFXUvGGofAXxBa61rz6LpOrL4Rk1qB/E/ibUNOFzqj39/wCH9DtTp5hs7hW8 148 | RXkt9ewfZeJfxyjwDhc44yzbh54x5fhaWG/tKE6dGNap70aUY0acJKEHyTqxdSUqkZKl 149 | FxjJzkj6ivxNUweUYTM/Ye3q1JLDyg5OEOZ8znUlJc1oz5GoxSfvS68rt7T+zl/wXL/Z 150 | j+OHw68MeIfG2pad8Gfifq1x4ss9c+G9/qh8S2+jP4TsNV8QSXq66LDTGOj6l4Y0mbU7 151 | S/1Ow0yFrpLrSYZLm6tt0nicR+FmfZVmOIw+CoVczwVOhQrU8fGn7BVqdarRoez9k51L 152 | 1aeIrQoyp0pVZWlSnKKjKx7eR8UYLNKOHUqtLCYqtUqwlh6lRONJ04TqylKo1FKm6UJS 153 | VSajFtON20rfhV4X/wCCcHjT4tfs/wDww8a6P8UbLUNI8efAnxR43Fnp+iaPJ4Ul8Y+G 154 | PGep6PoOgprf2LTtU0DQtV0YWtzLqLw3NvBfWN3Fb6YI7oRQ+pU44y7KOMs4y+OQ1aOO 155 | yvNMPToVHjcwq5pWoY/AU8RWnPL62MrUMRjFiKOKouMVTkkqblVlOEpP162Twzjhz61V 156 | zLB08HUhhoPERwuBhl0HCrXjVqxxOEwkcZGhhqaw1aUY0qkVTlU9yT5YHydo3wjl+F89 157 | 14WvtOtbbW9Iu59N1mWAiRftlndS2d7HDdsA00MNzDLHG/yq8W11ABGP3ThLM8Zn7l7P 158 | D14ydSXPKvR9lOKcrqFVSSVOUacrOkk5qSkmvdODAZTgMuwlOpQnh60KkVy18PONWlW5 159 | dFWozsnOnNrmpyaXNF3suaw7xP4j07QJ1t7q70/T4LcQ+bqWuXDW8csspVEstOtoA1zc 160 | yAFczP5FvllSNpHJx97i+FZ4+lWviIU3To89ZVKMpQioON4QUZxnOULJTqOVOFlfmbbv 161 | liM5pYGrBSowp0rpOvUrxpQlKW0Yppt1HfZLdXt1OT8deFrTxx4Ov5LYQPdQWc19Zzxx 162 | NCztEjSeWzMiylZohgZ4BIODxX5/UwWJynFUW7VYTSlGVGnKPNFSST9nJRnzuMleMk9t 163 | G/dZ0ZlgqGc5biv3XLUp05SpzcXdyim7Qn/LJpptq7X2bXPDfi34j1/TP2evhl8MLfxV 164 | rE/gbQvFt74wg8JvdEaEnibxhoIfUPEUdqjYmupYLFNPtpbhi1ktvfRwwwi8mefsyPDQ 165 | qZzjM1qRoyxeY4OVPmUf9po0cFXpUYUZxcVKFOfNCcXDmjWs3fmp2Xx/F2WYHCcIZA8P 166 | SlTxFLGuFSpBv2WLji8PUryqVFdqdSlWpOFKVoyhCUoOLjytcj+yTqnw+t/irqs3xQaO 167 | f4f2fwx+NWu6hYXBiaC+13Tvg/45tPCsSRT7Unu/7b1WBLAEgTTTBGVlYgerxVh8WsqU 168 | sBHlzR4nLsHg6sedTprGZngvaXnC8o026NOdaNpNKkmveSv8rwl9SWbwWZu+XU8LjcTi 169 | qWjVVUcHiXFO9veXNKMVdPmkldXaf9FXhq6+Kv7OHwn+HOh6Z8UtLT4X/DvS/iH4V8Ca 170 | fd/DdbC31C113xbrFpoNtc6Trni5vEC3HijS9UvbtF1XWPEV/o93qqrZxGw0ibH8M0c+ 171 | p8U8UcYZ7Q/tGjj6OJorD5nUx2Wxq4rHZBks8dXjXw9PJvYRhg8XilTpRp0KHPDnVeX1 172 | lRhH+ycn4Fq4TPeHvDTF4bLcRkuaYvCYPPMTk/1vGYTB0cZWwHOqWLxGYS9q/er4Wnia 173 | UqsKlfCYudpYVKpU/I/4nfFr4uftMfFH4l/Hq50LwZ4bmvfG/h2y1X/hFdKudC07WdNk 174 | g1HS7O+07w3DqF5punXGoWPhG4uNYu7c28F5qtzcah9iDXU71/ZnDuNoZRlNX+1MU6+O 175 | x+WzxlWpGlRwtTDZxLC4apVo0XgcJh41HetD2FJw5oe97WtUk21+D5lh6NHi3NMi4aiq 176 | WVZHn08rwzqTrYiWIynDYidOjGftMROnSqKioSq1F7jc7wpQikiG30m31jxJE2vW01yb 177 | maOax+3QfadJuQkqy2sc0UmQZIZLVZoo5NsfmW4kiYyEof1ThPN8ZXqyoYyjar7D2uDW 178 | LhWdGrGpKM3erFRhNuUef2dWcpvk5lFOLS9LPchwlSU5xi3F1IN8saVRRqQTUZU41Oad 179 | KpZWVSHK9Ur7HrOv6bDY6XepLIsEk1tIomO1EeWZGjVVGVwhyQqDnaO5rzuLMtnVUq8a 180 | MY4q/tJ+znHlduVWhyxai3dqC5VLl1u7yRFCvTw2DnSc7QjTlBOV76pr3npdt35rtp6u 181 | z6/l58Tp9a/4QvwpaGeR9B0vxD4t0iYNGCn9tWc8Uds8tyAWkzpk3l2kbn92kdwq57fK 182 | ZCqNPNszozjBYqWGwOJopP3/AKrNP2ySvqo4mKcmrpc8HZc7b/M+IamOxGUZPTvN4DDY 183 | vMMPqvcjjI1rx55JJ3lhpR9l0SVXfp4bFDIsEs0eXtwjQzNA53xiRTGglCkOiybtg8wC 184 | OYgx7mZsD6uVSEqkKcmo1eaNSCqR0lytSk4XVpONrvlbnTT5rJI+QVOpTXtHdws4ylBv 185 | TnTSU+XVKTsldKMvhi73P6qv2xdTurPwB4OtLyJYdT8T+KdTj0fRVbVdUaxs9D0C/tNI 186 | 1mTVVEumpfQalr/hTS9TNzeTTjUpr+RMCS3lb/Obw+yDCxoVYYnF4qWa5fUorFOrSp08 187 | Pio8S47D5xmOKxGJapPE4uWHwWM5cHSoqNHB1oVKibSZ/ojwdm+Gy/PKWaZPl1SPD+Oy 188 | KtmWBqY7khm0MwyrJ8RleHwc8FG/1Wh/amb1I0sTOblXr4GNClzSpy5vN/2TfCPw7+It 189 | h8ZdI8S3UWgeA57C58PQapY2UEUPh25gvdR8UaBrKwWqWyyW+jxahZ397ksLq0gvImlN 190 | s0qJ+6Z3m2JyfGZbWoJ4qr/bEqsoyd1i6WDw9HLFSlJyly/WauBlZyvKM588OZxkfzRj 191 | atd18pjRpujia2Ejj6sIyak3m2NzTPLVLvlqv6vjsJTi01LljBS5L3l8RRyWL6te2unQ 192 | 3OvXmk6rfWv/AAkp8rTLS5tbC4MVrc2keueRr0djdWqx3NjE+lRzCGQqY1Ytt/rrhDMl 193 | mWTU6+YUcRUx1GSq1KtepShVpOVpRw3u83O8PTnTpSpRpqN+dxm4yczWTr1YupRdKhep 194 | ONWN/bOTTs5+9K/K1d82qejVrtG14gkm8ZSz+H3EYn0nwnqfiCOJUQrPdWV1Y+S5BL7J 195 | jbrKiBctGZpGVs7Su2fYnE1qH17DUIVauX0Y4qnQlyxhVp4erCWJTSfK60sO5Qi4yko+ 196 | 9BtrU8LEwjOvi8FiJRhKpQjJOOqjK7cZL3ZON5qO6TjfS2h4/wDD6HQ7DQbGDW9A0jXN 197 | I1K/vtZ1rw7rFtDqOn6gmo3P2mS3vIZ8jzVi2KssRjltJo0ktpI2jjcfjHGHDmPxbweL 198 | wuJxGXZmlOvTxmXVpYfE4JVbeyjTlBRtCnCUI1ISjUhO8o1YyhI9nhfF4DBYb6pj8Hhs 199 | dg685TxWExdNV6VR1JOUnUjJNqd7OE4OM4OzpSi46fKPxLk+GVz8RPiFqXwm8Jnwx4It 200 | ZLbSdP0Z9TvdXsLi7NnaxaxADqM1xcPpkusrPNaQtcSvBAI9k42RtH25fDP8PluTYHiP 201 | NVmOayqVsTVxcKNLD16dKnWqfVqsZ0IU4LEfVko1JqEY1JSacGnJS+RzWOQVs4z3EcO5 202 | bLCZTTjSwlHDSrVMRRq1506axEGq8pzdB4r3qacpSgoxlGcXFOP2zq154J8LaP8As6XU 203 | f7QyeJvHun+NPG8vj/Vl8SyeJdM0jw/p13d24/szStZWeLSptautO0C5tJdQuHm1ZodL 204 | 1fRoo10WO6X4Ovk2a458VU58NV6WArYbDf2RQpUZYavDHxnh3QxDrYXkq1HSofWXOWHt 205 | GNOc8PilJV6lKX0OJzPA5PHhyrlvFeFxGOxSxWFzSNHHRxGEqYHnqqVKrhanMqEHKcVC 206 | liV7Wc5RxOFs4Kov0E/YK+Nn7O3hj4W/G5fih4/+EXg+9u/F2qWVjpfinXdO0/xNdeGr 207 | 7wXaRS6h4bttLvb2LxB5eoXF9Z2mn29hdWtypFnblEgkt5/yLjjgzxA/tbJMflOV5zm8 208 | aeVU5YrDxqY+GFli3j8VzyTqLDUsFivZeyqVa+InSrxnN1puTkuf08tzfI5YWrRq4vL8 209 | NTjiZwp1qjpSxdDDwwlGVCFKbVatXw1OMZU6dCnCpQeHgqdKzp8q8RtvAfhX4lRt4in8 210 | O3WiaNqHm33haH7Q0GsabpBkH2JL25SeS4OorbqlxcxrMsaO0sSq6IrH+xvDDhTN8/yS 211 | hmedVq1CNOnKjhM1yyvSprEzwlWdNYrERcZKpT9pFxlzRiqqpuUo3nd8mOxOGw8aUKbl 212 | OpWp0q9J1VyP2VSCl7NqCXs6sV+8UUrxT5ejRz2kfCRvBXjK68Ry+Ir3xDaXul3OixwX 213 | RtUNja3DCQr/AKPAovDMyRI8kpV40QKI2ZpJG+yzPJs3y7E1I5hUji6FTD16Ea2Ei6c0 214 | p0pRnKrSTnSbk4x0oezjBqTlTbk3Hy8NTpYidfEKdWWIlFU5RruMoxUJOdqcoRTld2T5 215 | 7vlfuvV3/Nv4g/EkeD9N1Pw9Yy7deS51HTbK3QgyWECXEsAvblhnbshANtGx8yaYq+0R 216 | qzV5FKWFxeVYGtU5JYhUKUYxg1LkcLc9So+WylLlSVOSbTakvdSPkMyzOpgKlbC0m/a3 217 | lFKS1g5ac132jtZJN20sj5h0LWI7CIxXEl/bZMjLe2u2dY5JCQ0zwloyzEFhuLscnIBO 218 | APFzHAyxM/aQjhqr91OhWbpOcYq6hGpaSS0TtZbWfn5GXY2GGi4VZ4mi221WpWqRhKVl 219 | zSptxk21dX5pWbuo3SS//9k= 220 | 221 | last name 222 | Schaffer 223 | phone numbers 224 | 225 | com.apple.syncservices:64ED42EB-109B-43A9-BEFC-3463D944251A 226 | com.apple.syncservices:FB7585EB-A3DE-46D5-920C-DF8028689BC5 227 | com.apple.syncservices:020036FD-22B3-41C9-BC3D-CDA6083582C6 228 | com.apple.syncservices:C6661518-90CA-45C7-A988-02F73B958951 229 | 230 | primary URL 231 | 232 | com.apple.syncservices:5BA97109-F8E3-46A9-AF0B-5F8C093F49EA 233 | 234 | primary email address 235 | 236 | com.apple.syncservices:B97DCC9C-5B00-4D38-AB06-4B7A5D6BC369 237 | 238 | primary phone number 239 | 240 | com.apple.syncservices:64ED42EB-109B-43A9-BEFC-3463D944251A 241 | 242 | primary related name 243 | 244 | com.apple.syncservices:CD0B7021-0228-4770-8FB0-3739479E9788 245 | 246 | primary street address 247 | 248 | com.apple.syncservices:377B9105-9D15-4F69-BCD6-B01E587F7760 249 | 250 | related names 251 | 252 | com.apple.syncservices:CD0B7021-0228-4770-8FB0-3739479E9788 253 | 254 | street addresses 255 | 256 | com.apple.syncservices:377B9105-9D15-4F69-BCD6-B01E587F7760 257 | 258 | 259 | 260 | --------------------------------------------------------------------------------