├── .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}#{tag}>"
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 << "#{type}>"
136 | else
137 | out = "<#{type}>#{contents.to_s}#{type}>\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 | 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
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 |
--------------------------------------------------------------------------------