plist-3.1.0/0000755000175000017500000000000012131276043012223 5ustar boutilboutilplist-3.1.0/README.rdoc0000644000175000017500000001346012131276043014035 0ustar boutilboutil= All-purpose Property List manipulation library 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. == Usage === Parsing result = Plist::parse_xml('path/to/example.plist') result.class => Hash "#{result['FirstName']} #{result['LastName']}" => "John Public" result['ZipPostal'] => "12345" ==== Example Property List FirstName John LastName Public StreetAddr1 123 Anywhere St. StateProv CA City Some Town CountryName United States AreaCode 555 LocalPhoneNumber 5551212 ZipPostal 12345 === Generation plist also provides the ability to generate plists from Ruby objects. The following Ruby classes are converted into native plist types: Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the and containers (respectively). * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a element. * 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. ==== Creating a plist There are two ways to generate complete plists. Given an object: obj = [1, :two, {'c' => 0xd}] If you've mixed in Plist::Emit (which is already done for +Array+ and +Hash+), you can simply call +to_plist+: obj.to_plist This is equivalent to calling Plist::Emit.dump(obj). Either one will yield: 1 two c 13 You can also dump plist fragments by passing +false+ as the second parameter: Plist::Emit.dump('holy cow!', false) => "holy cow!" ==== Custom serialization 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. An example: class MyFancyString ... def to_plist_node return "#{self.defancify}" end end 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. If for whatever reason you can't add this method, your object will be serialized with Marshal.dump instead. == Links [Project Page] http://plist.rubyforge.org [GitHub] http://github.com/bleything/plist [RDoc] http://plist.rubyforge.org == Credits 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. Other folks who have helped along the way: [Martin Dittus] who pointed out that +Time+ wasn't enough for plist Dates, especially those in ~/Library/Cookies/Cookies.plist [Chuck Remes] who pushed Patrick towards implementing #to_plist [Mat Schaffer] who supplied code and test cases for elements [Michael Granger] for encouragement and help [Carsten Bormann, Chris Hoffman, Dana Contreras, Hongli Lai, Johan Sørensen] for contributing Ruby 1.9.x compatibility fixes == License and Copyright plist is released under the MIT License. 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. === MIT License Copyright (c) 2006-2010, Ben Bleything and Patrick May Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. plist-3.1.0/metadata.yml0000644000175000017500000000356612131276043014540 0ustar boutilboutil--- !ruby/object:Gem::Specification name: plist version: !ruby/object:Gem::Version prerelease: false segments: - 3 - 1 - 0 version: 3.1.0 platform: ruby authors: - Ben Bleything and Patrick May autorequire: plist bindir: bin cert_chain: [] date: 2010-02-23 00:00:00 -08:00 default_executable: dependencies: [] description: | 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. email: executables: [] extensions: [] extra_rdoc_files: [] files: - Rakefile - README.rdoc - CHANGELOG - LICENSE - lib/plist/generator.rb - lib/plist/parser.rb - lib/plist.rb - test/test_data_elements.rb - test/test_generator.rb - test/test_generator_basic_types.rb - test/test_generator_collections.rb - test/test_parser.rb - test/assets/AlbumData.xml - test/assets/commented.plist - test/assets/Cookies.plist - test/assets/example_data.bin - test/assets/example_data.jpg - test/assets/example_data.plist - test/assets/test_data_elements.plist - test/assets/test_empty_key.plist has_rdoc: true homepage: http://plist.rubyforge.org licenses: [] post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version segments: - 0 version: "0" required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version segments: - 0 version: "0" requirements: [] rubyforge_project: plist rubygems_version: 1.3.6 signing_key: specification_version: 3 summary: All-purpose Property List manipulation library. test_files: - test/test_data_elements.rb - test/test_generator.rb - test/test_generator_basic_types.rb - test/test_generator_collections.rb - test/test_parser.rb plist-3.1.0/test/0000755000175000017500000000000012131276043013202 5ustar boutilboutilplist-3.1.0/test/test_parser.rb0000644000175000017500000000520312131276043016062 0ustar boutilboutil#!/usr/bin/env ruby require 'test/unit' require 'plist' class TestParser < Test::Unit::TestCase def test_Plist_parse_xml result = Plist::parse_xml("test/assets/AlbumData.xml") # dict assert_kind_of( Hash, result ) expected = [ "List of Albums", "Minor Version", "Master Image List", "Major Version", "List of Keywords", "Archive Path", "List of Rolls", "Application Version" ] assert_equal( expected.sort, result.keys.sort ) # array assert_kind_of( Array, result["List of Rolls"] ) assert_equal( [ {"PhotoCount"=>1, "KeyList"=>["7"], "Parent"=>999000, "Album Type"=>"Regular", "AlbumName"=>"Roll 1", "AlbumId"=>6}], result["List of Rolls"] ) # string assert_kind_of( String, result["Application Version"] ) assert_equal( "5.0.4 (263)", result["Application Version"] ) # integer assert_kind_of( Integer, result["Major Version"] ) assert_equal( 2, result["Major Version"] ) # true assert_kind_of( TrueClass, result["List of Albums"][0]["Master"] ) assert( result["List of Albums"][0]["Master"] ) # false assert_kind_of( FalseClass, result["List of Albums"][1]["SlideShowUseTitles"] ) assert( ! result["List of Albums"][1]["SlideShowUseTitles"] ) end # uncomment this test to work on speed optimization #def test_load_something_big # plist = Plist::parse_xml( "~/Pictures/iPhoto Library/AlbumData.xml" ) #end # date fields are credited to def test_date_fields result = Plist::parse_xml("test/assets/Cookies.plist") assert_kind_of( DateTime, result.first['Expires'] ) assert_equal DateTime.parse( "2007-10-25T12:36:35Z" ), result.first['Expires'] end # bug fix for empty # reported by Matthias Peick # reported and fixed by Frederik Seiffert def test_empty_dict_key data = Plist::parse_xml("test/assets/test_empty_key.plist"); assert_equal("2", data['key']['subkey']) end # bug fix for decoding entities # reported by Matthias Peick def test_decode_entities data = Plist::parse_xml('Fish & Chips') assert_equal('Fish & Chips', data) end def test_comment_handling_and_empty_plist assert_nothing_raised do assert_nil( Plist::parse_xml( File.read('test/assets/commented.plist') ) ) end end def test_filename_or_xml_is_stringio require 'stringio' str = StringIO.new data = Plist::parse_xml(str) assert_nil data end end __END__ plist-3.1.0/test/test_generator_collections.rb0000644000175000017500000000257512131276043021163 0ustar boutilboutil#!/usr/bin/env ruby require 'test/unit' require 'plist' class TestGeneratorCollections < Test::Unit::TestCase def test_array expected = < 1 2 3 END assert_equal expected, [1,2,3].to_plist(false) end def test_empty_array expected = < END assert_equal expected, [].to_plist(false) end def test_hash expected = < abc 123 foo bar END # thanks to recent changes in the generator code, hash keys are sorted before emission, # so multi-element hash tests should be reliable. We're testing that here too. assert_equal expected, {:foo => :bar, :abc => 123}.to_plist(false) end def test_empty_hash expected = < END assert_equal expected, {}.to_plist(false) end def test_hash_with_array_element expected = < ary 1 b 3 END assert_equal expected, {:ary => [1,:b,'3']}.to_plist(false) end def test_array_with_hash_element expected = < foo bar b 3 END assert_equal expected, [{:foo => 'bar'}, :b, 3].to_plist(false) end end plist-3.1.0/test/test_data_elements.rb0000644000175000017500000000673512131276043017406 0ustar boutilboutil#!/usr/bin/env ruby require 'test/unit' require 'plist' require 'stringio' class MarshalableObject attr_accessor :foo def initialize(str) @foo = str end end class TestDataElements < Test::Unit::TestCase def setup @result = Plist.parse_xml( 'test/assets/test_data_elements.plist' ) end def test_data_object_header expected = < element below contains a Ruby object which has been serialized with Marshal.dump. --> BAhvOhZNYXJzaGFsYWJsZU9iamVjdAY6CUBmb28iHnRoaXMgb2JqZWN0IHdhcyBtYXJz aGFsZWQ= END expected_elements = expected.chomp.split( "\n" ) actual = Plist::Emit.dump( Object.new, false ) actual_elements = actual.chomp.split( "\n" ) # check for header assert_equal expected_elements.shift, actual_elements.shift # check for opening and closing data tags assert_equal expected_elements.shift, actual_elements.shift assert_equal expected_elements.pop, actual_elements.pop end def test_marshal_round_trip expected = MarshalableObject.new('this object was marshaled') actual = Plist.parse_xml( Plist::Emit.dump(expected, false) ) assert_kind_of expected.class, actual assert_equal expected.foo, actual.foo end def test_generator_io_and_file expected = < AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== END expected.chomp! fd = IO.sysopen('test/assets/example_data.bin') io = IO.open(fd, 'r') # File is a subclass of IO, so catching IO in the dispatcher should work for File as well... f = File.open('test/assets/example_data.bin') assert_equal expected, Plist::Emit.dump(io, false).chomp assert_equal expected, Plist::Emit.dump(f, false).chomp assert_instance_of StringIO, @result['io'] assert_instance_of StringIO, @result['file'] io.rewind f.rewind assert_equal io.read, @result['io'].read assert_equal f.read, @result['file'].read io.close f.close end def test_generator_string_io expected = < dGhpcyBpcyBhIHN0cmluZ2lvIG9iamVjdA== END sio = StringIO.new('this is a stringio object') assert_equal expected.chomp, Plist::Emit.dump(sio, false).chomp assert_instance_of StringIO, @result['stringio'] sio.rewind assert_equal sio.read, @result['stringio'].read end # this functionality is credited to Mat Schaffer, # who discovered the plist with the data tag # supplied the test data, and provided the parsing code. def test_data # test reading plist elements data = Plist::parse_xml("test/assets/example_data.plist"); assert_equal( File.open("test/assets/example_data.jpg"){|f| f.read }, data['image'].read ) # test writing data elements expected = File.read("test/assets/example_data.plist") result = data.to_plist #File.open('result.plist', 'w') {|f|f.write(result)} # debug assert_equal( expected, result ) # Test changing the object in the plist to a StringIO and writing. # This appears extraneous given that plist currently returns a StringIO, # so the above writing test also flexes StringIO#to_plist_node. # However, the interface promise is to return an IO, not a particular class. # plist used to return Tempfiles, which was changed solely for performance reasons. data['image'] = StringIO.new( File.read("test/assets/example_data.jpg")) assert_equal(expected, data.to_plist ) end end plist-3.1.0/test/test_generator.rb0000644000175000017500000000216212131276043016555 0ustar boutilboutil#!/usr/bin/env ruby require 'test/unit' require 'plist' class SerializableObject attr_accessor :foo def initialize(str) @foo = str end def to_plist_node return "#{CGI::escapeHTML @foo}" end end class TestGenerator < Test::Unit::TestCase def test_to_plist_vs_plist_emit_dump_no_envelope source = [1, :b, true] to_plist = source.to_plist(false) plist_emit_dump = Plist::Emit.dump(source, false) assert_equal to_plist, plist_emit_dump end def test_to_plist_vs_plist_emit_dump_with_envelope source = [1, :b, true] to_plist = source.to_plist plist_emit_dump = Plist::Emit.dump(source) assert_equal to_plist, plist_emit_dump end def test_dumping_serializable_object str = 'this object implements #to_plist_node' so = SerializableObject.new(str) assert_equal "#{str}", Plist::Emit.dump(so, false) end def test_write_plist data = [1, :two, {:c => 'dee'}] data.save_plist('test.plist') file = File.open('test.plist') {|f| f.read} assert_equal file, data.to_plist File.unlink('test.plist') end end plist-3.1.0/test/test_generator_basic_types.rb0000644000175000017500000000300212131276043021134 0ustar boutilboutil#!/usr/bin/env ruby require 'test/unit' require 'plist' class TestGeneratorBasicTypes < Test::Unit::TestCase def wrap(tag, content) return "<#{tag}>#{content}" end def test_strings expected = wrap('string', 'testdata') assert_equal expected, Plist::Emit.dump('testdata', false).chomp assert_equal expected, Plist::Emit.dump(:testdata, false).chomp end def test_strings_with_escaping expected = wrap('string', "<Fish & Chips>") assert_equal expected, Plist::Emit.dump('', false).chomp end def test_integers [42, 2376239847623987623, -8192].each do |i| assert_equal wrap('integer', i), Plist::Emit.dump(i, false).chomp end end def test_floats [3.14159, -38.3897, 2398476293847.9823749872349980].each do |i| assert_equal wrap('real', i), Plist::Emit.dump(i, false).chomp end end def test_booleans assert_equal "", Plist::Emit.dump(true, false).chomp assert_equal "", Plist::Emit.dump(false, false).chomp end def test_time test_time = Time.now assert_equal wrap('date', test_time.utc.strftime('%Y-%m-%dT%H:%M:%SZ')), Plist::Emit.dump(test_time, false).chomp end def test_dates test_date = Date.today test_datetime = DateTime.now assert_equal wrap('date', test_date.strftime('%Y-%m-%dT%H:%M:%SZ')), Plist::Emit.dump(test_date, false).chomp assert_equal wrap('date', test_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')), Plist::Emit.dump(test_datetime, false).chomp end end plist-3.1.0/test/assets/0000755000175000017500000000000012131276043014504 5ustar boutilboutilplist-3.1.0/test/assets/test_data_elements.plist0000644000175000017500000000157512131276043021435 0ustar boutilboutil stringio dGhpcyBpcyBhIHN0cmluZ2lvIG9iamVjdA== file AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAA== io AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAA== marshal BAhvOhZNYXJzaGFsYWJsZU9iamVjdAY6CUBmb28iHnRoaXMgb2JqZWN0IHdh cyBtYXJzaGFsZWQ= plist-3.1.0/test/assets/test_empty_key.plist0000644000175000017500000000051712131276043020631 0ustar boutilboutil key 1 subkey 2 plist-3.1.0/test/assets/Cookies.plist0000644000175000017500000000500412131276043017154 0ustar boutilboutil Created 151936595.697543 Domain .cleveland.com Expires 2007-10-25T12:36:35Z Name CTC Path / Value :broadband: Created 151778895.063041 Domain .gamefaqs.com Expires 2006-04-21T16:47:58Z Name ctk Path / Value NDM1YmJlYmU0NjZiOGYxZjc1NjgxODg0YmRkMA%3D%3D Created 183530456 Domain arstechnica.com Expires 2006-10-26T13:56:36Z Name fontFace Path / Value 1 Created 183004526 Domain .sourceforge.net Expires 2006-10-20T02:35:26Z Name FRQSTR Path / Value 18829595x86799:1:1440x87033:1:1440x86799:1:1440x87248:1:1440|18829595|18829595|18829595|18829595 Created 151053128.640531 Domain .tvguide.com Expires 2025-10-10T07:12:17Z Name DMSEG Path / Value 1BDF3D1CC07FC70F&D04451&434EC763&4351FD51&0& Created 151304125.760261 Domain .code.blogspot.com Expires 2038-01-18T00:00:00Z Name __utma Path / Value 11680422.1172819419.1129611326.1129611326.1129611326.1 Created 599529600 Domain .tvguide.com Expires 2020-01-01T00:00:00Z Name gfm Path / Value 0 plist-3.1.0/test/assets/example_data.plist0000644000175000017500000003663512131276043020222 0ustar boutilboutil IMs com.apple.syncservices:9583BE42-EC1A-4B3F-B248-7904BA60634B com.apple.syncservices:2A222E96-4CC6-4320-BE0C-61D5F08C2E64 com.apple.syncservices:64DAA772-4558-49F8-9B87-C4A32D5E05C1 com.apple.syncservices:AF7C6884-B760-4E55-A4D5-B1B114C8A7DC com.apple.syncservices:ECC7FAED-47D6-4DB9-8120-2F029C7D64C8 com.apple.syncservices:D704F079-F869-4613-9CFC-697C1976E8F4 URLs com.apple.syncservices:5BA97109-F8E3-46A9-AF0B-5F8C093F49EA com.apple.syncservices.RecordEntityName com.apple.contacts.Contact display as company person email addresses com.apple.syncservices:B97DCC9C-5B00-4D38-AB06-4B7A5D6BC369 com.apple.syncservices:E508D679-43E1-49E2-B5D7-F14A8E48C067 com.apple.syncservices:C6478063-34A5-4CCB-BD41-1F131D56F7BD com.apple.syncservices:2B3E352C-7831-4349-9A87-0FA4BD290515 first name Mat image /9j/4AAQSkZJRgABAQAAAQABAAD/7QAcUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAD/ 4g9ASUNDX1BST0ZJTEUAAQEAAA8wYXBwbAIAAABtbnRyUkdCIFhZWiAH1gAFABUAFAAg AAZhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGxx EHyAeZ4fvAacU+DW7cbVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5yWFla AAABLAAAABRnWFlaAAABQAAAABRiWFlaAAABVAAAABR3dHB0AAABaAAAABRjaGFkAAAB fAAAACxyVFJDAAABqAAAAA5nVFJDAAABuAAAAA5iVFJDAAAByAAAAA52Y2d0AAAB2AAA BhJuZGluAAAH7AAABj5kZXNjAAAOLAAAAGRkc2NtAAAOkAAAAEhtbW9kAAAO2AAAAChj cHJ0AAAPAAAAAC1YWVogAAAAAAAAZwIAADyoAAAJjlhZWiAAAAAAAABowAAAq7QAAB5o WFlaIAAAAAAAACcUAAAXvwAAqy9YWVogAAAAAAAA81IAAQAAAAEWz3NmMzIAAAAAAAEM QgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsY3VydgAAAAAAAAABAc0AAGN1 cnYAAAAAAAAAAQHNAABjdXJ2AAAAAAAAAAEBzQAAdmNndAAAAAAAAAAAAAMBAAACAAAA JwCJARYBsAJsA0kESAV0BsgIPAnXC5YNcA9oEX0ToxXSGAwaJRvMHW4fDSCbIi8jtSUy JqooFil9KuEsOS2MLtwwLTF1Mr80CDVINmQ3cjh8OYs6nTuuPL890j7lP/hBCUIUQxtE H0UdRhhHDkf9SOhJ30r0TBxNQE5iT4NQo1HEUudUDVUxVlZXhFizWeRbGlxSXY9e0mAU YVtin2PhZSBmWmeWaMtp/WsybGJtkm7Gb/hxKnJhc5502HYYd1d4mHmyerx7wHzFfc9+ 1n/egOmB94MHhBiFLIZCh1uIdomViraL2Yz+jiKPRZBokY+SrpPMlOWV/ZcVmCmZOJpG m1ScX51onnCfdqB8oYGiiqORpJmlo6asp7yozancqumr9q0ArgWvBK/8sO+x2bK/s5q0 bLU7thK2/7f8uPu5+Lr0u+68573bvtS/0MDKwcnCzMPUxN/F88cLyCPJQMpFyyHL5cyq zXHOOM79z8XQkdFe0izS/dPP1KbVf9Za1zjYF9j42dnamNtD2+TchN0i3cDeWt7z34zg JuC/4Vjh8OKL4yfjxeRi5QHlouZN5wDnwOh76TPp5eqP6zPr0exj7PLtce3w7l/uzu8z 75Pv9PBK8KDw+/GN8iny2fOD9Cn0yfVp9gj2qfdM9/H4ovla+hr66vvN/MD9yf7Q//8A AAAUAEYAkQD0AYQCNQMQBA0FPAaSCA8Jtgt1DUoPNBEtEy0VNhcUGJ0aIhujHRsekB/+ IWIixCQgJWwmuyf8KTcqcCumLNUuAy8vME0xXjJoM200cjV3Nnk3ejh6OXg6dDtwPGc9 Wz5NPzxAKEEUQf1C5EPRRM9F1kbcR+JI50nsSvBL9kz+TgNPB1ANURNSF1MdVCJVJ1Yv VzNYOVk/WkZbTlxTXV9ebF95YIxhomK9Y+BlBWYuZ15ol2nQaw5sTG2NbqdvsXC1cbpy xHPLdNN13nbsd/x5DXohezd8UH1rfop/q4DOgfODF4Q6hV2GhIejiMGJ2oryjAqNHo4t jzuQSZFUkl2TZZRrlXGWdpd/mIWZi5qQm5Gcl52dnp6fnKCdoZiilaOMpICldKZjp1Go PKkkqg2q9KvarL6tqK6Rr4Cwb7FgslOzSbRDtT+2PLc8uD25QLpFu0m8Tb1Qvk2/O8Ah wQrB9sLiw83EusWrxprHich5yWjKVctDzC/NGs4DzurPz9DE0cPSvNOx1J/VhtZm10HY F9jp2brahttT3CHc8N3F3prfdOBR4Svh+uLA44XkSeUO5dPml+db6B/o4+mm6mrrLuvz 7Lbteu4+7wPvx/CM8VDyFPLX8530YfUk9er2rfdx+DX4+vm9+oH7RfwK/M79kv5W/x7/ /wAAAAkAIABDAHAApwDoAVAB1gJ8Az4EHAUcBjUHXwiaCecLPAyVDekPExA2EVgSdBON FKEVrxa6F7sYtRmoGpMbdxxWHTAeAh7QH5kgZCE6Ih8i/yPgJL0llyZvJ0UoGijpKbgq hCtPLBcs3S2jLmgvLS/xMLQxdzI6MvYzrzRmNRo1yzZ8Ny432jiJOTk56jqdO1I8CjzF PYQ+RD8IP8xAkUFVQhZC2UOZRFhFFkXQRopHQ0f0SKRJVEoBSqxLVUv5TJVNJ02tTjBO tE85T75QRFDMUVZR41JyUwNTl1QtVMZVYVYAVqBXQlgDWO1Z81r7XANdCl4QXxpgKGE1 YkNjVGRqZYJmm2e4aNdp92sabEFtaW6Nb69wzXHrcwh0H3UwdkB3SXhQeVB6SntDfDV9 JX4Rfvp/44DPgciCx4PFhMCFuoauh5+IiYlyilqLPowfjQCN4I7Aj6GQg5FikkSTHpPt lLWVgZZTlyeX/JjWmbaal5t6nGKdSZ4ynxygBqDvodeivqOjpJill6aSp4eoeKlkqkur L6wTrPet3a7Gr7GwqLGgsqazr7S/tdu2+rgeuUS6aLuJvKu9yb7owATBIMI6w1TEb8WM xqrHx8jpygjLLMxSzYHOxtAm0YvS/9SF1hTXstlr2zXdFt8M4SjjZOXI6G7rae7F8ur4 1P//AABuZGluAAAAAAAABjYAAJQXAABYjQAAUbUAAI5iAAAoWwAAFqgAAFANAABUOQAC o9cAAkAAAAFKPQADAQAAAgAAABQALQBGAF8AdwCOAKUAvADTAOkA/wEWASwBQwFZAXAB hgGdAbQBywHjAfsCEwIsAkcCYgKGAqoC0AL3Ax8DSgN1A6EDzwP/BDEEZASZBNEFCgVF BYEFwQYCBkYGigbQBxkHYweuB/sISwimCQgJbwnVCjwKpQsQC3wL6gxZDMsNPw24DjQO tA85D8EQTxDjEXwSChKIEwUThxQKFI8VFxWfFikWsxc9F8oYWBjjGW8Z/BqJGxUbohwu HLkdRR3SHl4e7R99IBAgpSE+IdYiciMSI7MkVCT5JaAmRCbsJ5UoPyjnKY0qNyrfK4cs MizZLZkuaS8/MBgw7jHGMqIzfDRWNS82CDbjN7w4ljluOkc7Hjv1PMw9pD58P1dAM0ER QexCz0O0RJ9FjEZ5R2tIYkleSlxLW0xfTWdOcE99UIxRnVKsU75U0VXjVvhYCFkXWipb QVxYXXRelV++YPNiLmN6ZM1mNmewaTVqkmvTbQ9uUm+WcOByLHOBdNJ2IHdyeL96CXtO fJF9zH8CgDuBb4KjhBWF2oehiWOLLYz1jrSQdpIwk+yVnZdOmPiaopxKnfOf66JEpKen FqmZrCGurbFCs8+2V7jhu2C9qr/JwfzESsa7yV/MRs+Y02vX2tzd39riV+T157bqg+1L 8AjykvT69yr5LfsI/L3+g///AAAAJwBHAGQAgACaALMAzADkAPwBEwErAUMBWwF0AYwB pQG+AdgB8gIMAicCRQJkAooCsgLbAwUDMQNfA44DvgPxBCYEXASUBM4FDAVKBYsF0AYX BmAGqgb4B0gHmgfuCEcIpgkKCXIJ3ApICrcLKQudDBQMjw0LDYwOEQ6ZDyYPthBJEOER fBISEp4TLBO8FE8U4xV6FhMWrBdGF+UYhRklGcgabhsUG70caB0SHcEecB8hH9MghiE9 IfEipiNdJBEkxSV5Jikm2CeIKDYo4imKKjUq3iuHLDIs2S2ZLmkvPzAYMO4xxjKiM3w0 VjUvNgg24ze8OJY5bjpHOx479TzMPaQ+fD9XQDNBEUHsQs9DtESfRYxGeUdrSGJJXkpc S1tMX01nTnBPfVCMUZ1SrFO/VNRV7FcKWCNZP1phW4hcrl3cXwpgQWF+YrxkBWVOZqFn 92lPaqxsD21qbslwIHF5ctB0KHV5dsl4GXloerV8An1Pfpp/6oE8go6D9IV1hveIc4nw i3OM8o5tj+2Rb5LxlHmWApePmR+atpxRnfOffKD2onukCqWmp1GpEKrbrLSulLCBsmu0 WbY3uBe57bu7va2/vMHSw+nGBMghykHMZ86P0LfS4dUP1z/Zctum3dvgFuJV5I7m1ekU 617tqu/x8kT0lPbm+Tr7k/3w//8AAABVAIoAtwDhAQgBLgFUAXoBoAHGAe0CFQI+AmoC ngLUAwwDRwOFA8UECQRQBJoE6gU+BZcF9wZcBscHOQexCDEIrAkhCZsKFwqZCyALqww6 DNANag4KDq8PWRAHELkRbxIqEugTsBSAFVoWOhcfGA0Y/BnsGtwbyxy3HaMejR96IGoh YSJZI1skYCVvJoInpSjPKgArPSyHLf8vqzFXMwc0sTZUN/E5hjsVPJ4+JD9kQFtBUUJG Qz9EOUU2Ri9HJ0giSR5KGEsPTAVM/U3zTuhP3lDUUclSu1OvVKdVoVaiV6NYpVmsWrtb z1zlXgVfKGBVYYtixGQJZVBmomf3aU9qlWvTbRBuVW+ccOxyQ3OndQ52eXfveWl65nxo feh/aYDxgnWEE4XQh4eJMIrbjICOGY+wkUaS1ZRolfiXipkdmrWcUZ3zn3yg9qJ5pAal nac+qO2qoKxWrgmvubFlsv+0mbYct5+5GbqEu++9U761wBnBgcLvxFrFzMc+yLTKLMuo zSbOpNAh0Z/TG9Sa1hTXlNkQ2oncAd1q3rvf+eE24m/jneTA5eDm/egT6STqJ+sm7CHt E+4B7unv0PCp8X/yTfMZ8930m/VX9gT2svdW9+/4ifkZ+aL6K/qv+x37jPv6/Gj8tv0E /VL9oP3v/jz+gf7G/wz/Uf+X/9z//wAAZGVzYwAAAAAAAAAKQ29sb3IgTENEAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAAgAAAAxlblVTAAAAEgAAAChq YUpQAAAADgAAADoAQwBvAGwAbwByACAATABDAEQwqzDpMPwAIABMAEMARG1tb2QAAAAA AAAGEAAAnF4AAAAAwEuKAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAENvcHlyaWdodCBB cHBsZSBDb21wdXRlciwgSW5jLiwgMjAwNQAAAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEB AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBD AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB AQEBAQEBAQEBAQEBAQH/wAARCAAwADADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAA AAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1Fh ByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAEC AxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2 Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaX mJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP0 9fb3+Pn6/9oADAMBAAIRAxEAPwDd+Bfxs/4J+ftC/EFPhp4M+LXjLQtVl8P6pr9trvxE 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 zSptxk21dX5pWbuo3SS//9k= last name Schaffer phone numbers com.apple.syncservices:64ED42EB-109B-43A9-BEFC-3463D944251A com.apple.syncservices:FB7585EB-A3DE-46D5-920C-DF8028689BC5 com.apple.syncservices:020036FD-22B3-41C9-BC3D-CDA6083582C6 com.apple.syncservices:C6661518-90CA-45C7-A988-02F73B958951 primary URL com.apple.syncservices:5BA97109-F8E3-46A9-AF0B-5F8C093F49EA primary email address com.apple.syncservices:B97DCC9C-5B00-4D38-AB06-4B7A5D6BC369 primary phone number com.apple.syncservices:64ED42EB-109B-43A9-BEFC-3463D944251A primary related name com.apple.syncservices:CD0B7021-0228-4770-8FB0-3739479E9788 primary street address com.apple.syncservices:377B9105-9D15-4F69-BCD6-B01E587F7760 related names com.apple.syncservices:CD0B7021-0228-4770-8FB0-3739479E9788 street addresses com.apple.syncservices:377B9105-9D15-4F69-BCD6-B01E587F7760 plist-3.1.0/test/assets/example_data.jpg0000644000175000017500000002243712131276043017642 0ustar boutilboutilJFIFPhotoshop 3.08BIM@ICC_PROFILE0applmntrRGB XYZ  acspAPPL-applq|ySrXYZ,gXYZ@bXYZTwtpthchad|,rTRCgTRCbTRCvcgtndin>desc,ddscmHmmod(cprt-XYZ g< XYZ hhXYZ '/XYZ Rsf32 B&lcurvcurvcurvvcgt'lIHt< ph} %n "/#%2&()}*,9-.0-1u245H6d7r8|9:;<=>?A BCDEFGGHIJLM@NbOPQRT U1VVWXY[\R]^`a[bce fZghik2lbmnoq*rastvwWxyz{|}~ހ,B[vٌ"Eh̔)8FT_hpv|ͩܪٲl;۾Կ #@E!̪q8Б^,ԦZ8ژC܄"Zߌ&X'bM{33cq_3J))iLZF5 < u J4--6"!b"$ %l&')7*p+,.//0M1^2h3m4r5w6y7z8z9x:t;pM?<@(AABCDEFGHIJKLNOP QRST"U'V/W3X9Y?ZF[N\S]_^l_y`abcef.g^hiklLmnopqrstuvwy z!{7|P}k~΁:]ڊ -;IT]ekqvtcQ<$ ڬo`SIC?<<=@EIMPM;! ĺūƚljyhUC/ҼӱԟՆfAٺچS!ޚtQ+I[j.z>Pa$q5E V CpP|>5_ < 6XtwV0 d!:""#$%&o'E(()*+O,,-.h/-/01w2:234f556|7.78999:;R< <=>D??@AUBBCDXEEFGCGHITJJKUKLM'MN0NO9OPDPQVQRrSST-TUaVVWBXXYZ\] ^_`(a5bCcTdjefghiklAminopqstu0v@wIxPyPzJ{C|5}%~~ρȂǃńrZ>bD픵S'֙zbI2עxdK/ݮƯ۶Dhɾ :ToŌƪ,Ŕ&ыԅײk5 (dnindin6XQb([P T9@J=-F_w,CYp,GbJu1d EFcK  o <  | Y ? 49O|  )=Xo.E^}  !>!"r##$T$%&D&'(?()*7*+,2,-.i/?00123|4V5/66789n:G;;<=>|?W@3AABCDEFyGkHbI^J\K[L_MgNpO}PQRSTUVXYZ*[A\X]t^_`b.czdf6gi5jkmnRopr,stv wrxz {N|};oڇc-v0앝NJD!B϶W`Jƻ_FϘkWK*-'Gd+C[t 'Ed1_&\ J`HG r H )  &I|,OzF%nhp! !=!"#]$$%y&)&'(6()*5*+,2,-.i/?00123|4V5/66789n:G;;<=>|?W@3AABCDEFyGkHbI^J\K[L_MgNpO}PQRSTUW X#Y?Za[\]_ `Aa~bdeNfgiOjlmjnp qyrt(uyvxyhz|}O~j G P>\91 !  : j Yo*Z: z j!a"Y#[$`%o&'(*+=,-/1W346T79;<>$?d@[AQBFC?D9E6F/G'H"IJKLLMNOPQRSTUVWXYZ[\^_(`Uabd ePfgiOjkmnUoprCsuvywyiz|h}iuЇ0یFՔhQ|y>V eSZ>ȴ,˨&Τ!џԚהډj޻6o$'&!MWV+hR< Qdesc Color LCDmluc enUS(jaJP:Color LCD000 LCDmmod^KtextCopyright Apple Computer, Inc., 2005CC00 }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ? BAO - UmD𽿁%ucNgS\]b-**7wss #STZ7,%ZpgU':(]WRZ2W\ppq3N\:rJN)NR✗?M|:gmkkWkl/$z,crGJ#5WЧ`8ʴ yJR/1Uq45J)rƲEQ£m^Vd\c-|'ŸRK[:t-.\LzNe8xF%Z:FK٦gwƭ:,bRaha)Ӝh>j*O(~a dSj^YnZ6b[KkwFQ21eè01G8S]g x֜ISrn;[ʞom֩OʜEGN<ԔRiY-zma<;tuKf[ƃ}>M1;nXF EFJSpQWyy)hnuw}SwvvNo{i#$#fuyN1ҹ\p8nK|ݹ5h߳;)*kTVڧ} Ϣ?YZkz&c׊=V;+zI]6Iӧ8dY.$Q۪/31X>OX%)EB.SARnTFSmot 7*S#ԂqG% Rוrb׌ 4V&X\-IG/QJHNzunJ1zt8nV||3l18J4Q/Q6ZrVnMۣm a/=f`fc+7c@pP 2z1U1;9\W4,6߫l' aF5bfVֶ~wZhև}ož%F{K.YwKfV 447C$hD1XlB0I489c'SeFmr|niOf|*:r<ڋ9iAŦފ׌_WV~8.|UԼa|>5&4s=;SlEy-e_(8,ۇ1ZXo(N֩FcF$:u%*HMSa3?aԒN|u%%hϑ'K+_\ُï xږg~q=s|Ko?5_Iz4;KNLIKmxGf}f8> \N juѡKէ 2JUeiR+G,ҎJ,&*JDIӄRRBRU&ӍJ߅^pxƺ?-CHxY&'%X'h_=ׅ[moHMewR C 2mu8K3g^^2u%*}✮UI%NQ+:I9)&Ӄ2%:' \ZUtUΜ&4]칬;#Ӵ Op,Q,h\\EYR6VxU7N=eR8Qg9BN8Y_#9*0J:+Ɣ%)muKu{u9?xZ@PY}ge4pUQJ Ú5|e'dJT*Ae/RTWju)VJV J.;!W}u-u Γx@(K۴]WXwl43G>-& )g!e=;0ؼ#kǞI'yae%W~2,^#C3@|bE1)&91flHJ)ɨH*\JNV[4>ASM{Gw 8PoNtS);%t/.?S/"Xu?SGU]Ql=HdTKM/S77N5)L -o a⥚(SJ<>*kS3b%J8+/vN78Khxt;  o@5#Ru:~d|5bث,F9m&$67qcoqvfeե[{(Ӕm p#RHNX=0Xocxlv\D|&ǂ-dt;^Ŭ@5+<֐į=}|3>[`x5Yk*5qpK^*u}ZgBVJ5&ԔpiKsXùlM8QJLENA)J 1gέy h2xǺڥwNMuE.m./yzoaukrY۔H$.8?L~S9iTƦ>Yb?$,5,+{*k'Jg7ZnNKs|XZj58g u:R FT!JmV_ N1:t)¥JΟ*oWTm);uhڇ}h~kndbK۔Kk2ƎĪgVB4ʎ5+Ҧ3U5EJ?isF*(w|NP:үI\Tj {:_QJOG=|$oxr6]΋F  Application Version 5.0.4 (263) Archive Path /Users/username/Pictures/iPhoto Library List of Albums AlbumId 999000 AlbumName Library KeyList 7 Master PhotoCount 1 PlayMusic YES RepeatSlideShow YES SecondsPerSlide 3 SlideShowUseTitles SongPath TransitionDirection 0 TransitionName Dissolve TransitionSpeed 1 Album Type Special Roll AlbumId 999001 AlbumName Last Roll Filter Mode All Filters Count 1 Operation In Last Type Roll KeyList 7 PhotoCount 1 PlayMusic YES RepeatSlideShow YES SecondsPerSlide 3 SlideShowUseTitles SongPath TransitionDirection 0 TransitionName Dissolve TransitionSpeed 1 Album Type Special Month AlbumId 999002 AlbumName Last 12 Months Filter Mode All Filters KeyList 7 PhotoCount 1 PlayMusic YES RepeatSlideShow YES SecondsPerSlide 3 SlideShowUseTitles SongPath TransitionDirection 0 TransitionName Dissolve TransitionSpeed 1 Album Type Regular AlbumId 9 AlbumName An Album KeyList 7 PhotoCount 1 PlayMusic YES RepeatSlideShow YES SecondsPerSlide 3 SlideShowUseTitles SongPath TransitionDirection 0 TransitionName Dissolve TransitionSpeed 1 List of Keywords List of Rolls Album Type Regular AlbumId 6 AlbumName Roll 1 KeyList 7 Parent 999000 PhotoCount 1 Major Version 2 Master Image List 7 Aspect Ratio 1 Caption fallow_keep.png.450x450.2005-12-04 Comment a comment DateAsTimerInterval 158341389 ImagePath /Users/username/Pictures/iPhoto Library/2006/01/07/fallow_keep.png.450x450.2005-12-04.jpg MediaType Image MetaModDateAsTimerInterval 158341439.728129 ModDateAsTimerInterval 158341389 Rating 0 Roll 6 ThumbPath /Users/username/Pictures/iPhoto Library/2006/01/07/Thumbs/7.jpg Minor Version 0 plist-3.1.0/test/assets/commented.plist0000644000175000017500000000037712131276043017543 0ustar boutilboutil plist-3.1.0/LICENSE0000644000175000017500000000206712131276043013235 0ustar boutilboutilCopyright (c) 2006-2010, Ben Bleything and Patrick May Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. plist-3.1.0/CHANGELOG0000644000175000017500000000720312131276043013437 0ustar boutilboutil= plist - All-purpose Property List manipulation library === Release version 3.0.0! 2010-02-23: * Ruby 1.9.x compatibility! 2010-02-16: * excise a bunch of unnecessary @@ variables * fix up some tests for cross-version compatibility 2010-02-14: * generalized cleanup: * fix old file headers * modernize rakefile * clean up rdoc 2010-01-08: * move from RubyForge Subversion to GitHub 2007-02-22 (r81): * 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. 2006-09-20 (r80): * tweak a comment in generator.rb to make it clear that we're not using Base64.b64encode because it's broken. === Release version 3.0.0! 2006-09-20 (r77 - r79): * move IndentedString inside Plist::Emit and :nodoc: it * Tag 3.0.0! (from rev 78) 2006-09-19 (r73 - r75): * Really fix the rakefile this time (apparently I deleted some code that I needed...) * alter the fix_whitespace rake task to ignore the assets directory * cleanup whitespace 2006-09-18 (r70 - r72): * Update this file ;) * Fix Rakefile * gem install -t now works correctly * Remove super-sekr1t rdoc staging area from rdoc publishing task 2006-09-15 (r64 - r69): * Change behavior of empty collection elements to match What Apple Does * Fix some gem packaging infrastructure 2006-09-13 (r61 - r63): * Merge generator injection removal branch into trunk! 2006-09-13 (r52 - r60): * Fix indentation/newlines in generator (finally!) * Refix indentation to be more faithful to the way Apple emits their plists * Remove horrific regex and replace it with proper comment parsing * Empty plists return nil when parsed * Sort hash keys before emitting (now we can test multi-element hashes!) * Inject #<=> into Symbol so that sorting Symbol-keyed hashes won't freak out 2006-09-12 (r47 - r51): * More test rejiggering * New tests to expose some bugs 2006-09-10 (r33 - r46): * Update tests for new generator code * Rejigger some tests * 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 * Marshal.dump unrecognized objects into elements * Make the parser strip out comments and Marshal.load elements if possible * Update some rdoc === Release version 2.1.1! 2006-09-10 (r31 - r32): * Added encoding / decoding for entities (& etc) * Changed parsing of elements to return StringIO objects * Fixed bug with empty tags 2006-08-24 (r25 - r30): * Invert ownership of methods in the generator, allowing us to remove the self.extend(self) * New branch to remove method inject from parser 2006-08-23 (r22 - r24): * Add rcov task to Rakefile * Add some tests 2006-08-20 (r9 - r21): * Add a bunch of rdoc and rdoc infrastructure * Add rake task to clean up errant whitespace * Spin off a branch to remove a bunch of method injection in the generator code * Rename some tests for clarity's sake * Replace NARF generation code with Ben's generation code * Update tests * This broke indentation (will be fixed later) * Add Plist::Emit.dump, so you can dump objects which don't include Plist::Emit, update tests to match * Fix a bug with the method that wraps output in the plist header/footer 2006-08-19 (r1 - r8): * The beginnings of merging the plist project into the NARF plist library (under the plist project's name) * fancier project infrastructure (more tests, Rakefile, the like) * Add/update copyright notices in the source files * Move a bunch of documentation out to README * Split library into chunks * Properly delete files when cleaning up from tests plist-3.1.0/Rakefile0000644000175000017500000000752012131276043013674 0ustar boutilboutil# # Plist Rakefile # # Based heavily on Geoffrey Grosenbach's Rakefile for gruff. # Includes whitespace-fixing task based on code from Typo. # # Copyright 2006-2010 Ben Bleything and Patrick May # Distributed under the MIT License # require 'fileutils' require 'rubygems' require 'rake' require 'rake/testtask' require 'rake/packagetask' require 'rake/gempackagetask' require 'rake/contrib/rubyforgepublisher' require 'rdoc/task' $:.unshift(File.dirname(__FILE__) + "/lib") require 'plist' PKG_NAME = 'plist' PKG_VERSION = Plist::VERSION PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" RELEASE_NAME = "REL #{PKG_VERSION}" RUBYFORGE_PROJECT = "plist" RUBYFORGE_USER = ENV['RUBYFORGE_USER'] TEST_FILES = Dir.glob('test/test_*') TEST_ASSETS = Dir.glob('test/assets/*') LIB_FILES = Dir.glob('lib/**/*') RELEASE_FILES = [ "Rakefile", "README.rdoc", "CHANGELOG", "LICENSE" ] + LIB_FILES + TEST_FILES + TEST_ASSETS task :default => [ :test ] # Run the unit tests Rake::TestTask.new { |t| t.libs << "test" t.pattern = 'test/test_*.rb' t.verbose = true } desc "Clean pkg, coverage, and rdoc; remove .bak files" task :clean => [ :clobber_rdoc, :clobber_package, :clobber_coverage ] do puts cmd = "find . -type f -name *.bak -delete" `#{cmd}` end task :clobber_coverage do puts cmd = "rm -rf coverage" `#{cmd}` end desc "Generate coverage analysis with rcov (requires rcov to be installed)" task :rcov => [ :clobber_coverage ] do puts cmd = "rcov -Ilib --xrefs -T test/*.rb" puts `#{cmd}` end desc "Strip trailing whitespace and fix newlines for all release files" task :fix_whitespace => [ :clean ] do RELEASE_FILES.reject {|i| i =~ /assets/}.each do |filename| next if File.directory? filename File.open(filename) do |file| newfile = '' needs_love = false file.readlines.each_with_index do |line, lineno| if line =~ /[ \t]+$/ needs_love = true puts "#{filename}: trailing whitespace on line #{lineno}" line.gsub!(/[ \t]*$/, '') end if line.chomp == line needs_love = true puts "#{filename}: no newline on line #{lineno}" line << "\n" end newfile << line end if needs_love tempname = "#{filename}.new" File.open(tempname, 'w').write(newfile) File.chmod(File.stat(filename).mode, tempname) FileUtils.ln filename, "#{filename}.bak" FileUtils.ln tempname, filename, :force => true File.unlink(tempname) end end end end desc "Copy documentation to rubyforge" task :update_rdoc => [ :rdoc ] do Rake::SshDirPublisher.new("#{RUBYFORGE_USER}@rubyforge.org", "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}", "rdoc").upload end # Genereate the RDoc documentation RDoc::Task.new do |rdoc| rdoc.title = "All-purpose Property List manipulation library" rdoc.main = "README.rdoc" rdoc.rdoc_dir = 'rdoc' rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'CHANGELOG') rdoc.rdoc_files.include('lib/**') rdoc.options = [ '-H', # show hash marks on method names in comments '-N', # show line numbers ] end # Create compressed packages spec = Gem::Specification.new do |s| s.name = PKG_NAME s.version = PKG_VERSION s.summary = "All-purpose Property List manipulation library." s.description = <<-EOD 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. EOD s.authors = "Ben Bleything and Patrick May" s.homepage = "http://plist.rubyforge.org" s.rubyforge_project = RUBYFORGE_PROJECT s.has_rdoc = true s.files = RELEASE_FILES s.test_files = TEST_FILES s.autorequire = 'plist' end Rake::GemPackageTask.new(spec) do |p| p.gem_spec = spec p.need_tar = true p.need_zip = true end plist-3.1.0/lib/0000755000175000017500000000000012131276043012771 5ustar boutilboutilplist-3.1.0/lib/plist.rb0000644000175000017500000000054712131276043014457 0ustar boutilboutil#!/usr/bin/env ruby # # = plist # # This is the main file for plist. Everything interesting happens in # Plist and Plist::Emit. # # Copyright 2006-2010 Ben Bleything and Patrick May # Distributed under the MIT License # require 'base64' require 'cgi' require 'stringio' require 'plist/generator' require 'plist/parser' module Plist VERSION = '3.1.0' end plist-3.1.0/lib/plist/0000755000175000017500000000000012131276043014124 5ustar boutilboutilplist-3.1.0/lib/plist/parser.rb0000644000175000017500000001105512131276043015747 0ustar boutilboutil#!/usr/bin/env ruby # # = plist # # Copyright 2006-2010 Ben Bleything and Patrick May # Distributed under the MIT License # # Plist parses Mac OS X xml property list files into ruby data structures. # # === Load a plist file # This is the main point of the library: # # r = Plist::parse_xml( filename_or_xml ) module Plist # Note that I don't use these two elements much: # # + Date elements are returned as DateTime objects. # + Data elements are implemented as Tempfiles # # Plist::parse_xml will blow up if it encounters a data element. # If you encounter such an error, or if you have a Date element which # can't be parsed into a Time object, please send your plist file to # plist@hexane.org so that I can implement the proper support. def Plist::parse_xml( filename_or_xml ) listener = Listener.new #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener) parser = StreamParser.new(filename_or_xml, listener) parser.parse listener.result end class Listener #include REXML::StreamListener attr_accessor :result, :open def initialize @result = nil @open = Array.new end def tag_start(name, attributes) @open.push PTag::mappings[name].new end def text( contents ) @open.last.text = contents if @open.last end def tag_end(name) last = @open.pop if @open.empty? @result = last.to_ruby else @open.last.children.push last end end end class StreamParser def initialize( plist_data_or_file, listener ) if plist_data_or_file.respond_to? :read @xml = plist_data_or_file.read elsif File.exists? plist_data_or_file @xml = File.read( plist_data_or_file ) else @xml = plist_data_or_file end @listener = listener end TEXT = /([^<]+)/ XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um DOCTYPE_PATTERN = /\s*)/um COMMENT_START = /\A/um def parse plist_tags = PTag::mappings.keys.join('|') start_tag = /<(#{plist_tags})([^>]*)>/i end_tag = /<\/(#{plist_tags})[^>]*>/i require 'strscan' @scanner = StringScanner.new( @xml ) until @scanner.eos? if @scanner.scan(COMMENT_START) @scanner.scan(COMMENT_END) elsif @scanner.scan(XMLDECL_PATTERN) elsif @scanner.scan(DOCTYPE_PATTERN) elsif @scanner.scan(start_tag) @listener.tag_start(@scanner[1], nil) if (@scanner[2] =~ /\/$/) @listener.tag_end(@scanner[1]) end elsif @scanner.scan(TEXT) @listener.text(@scanner[1]) elsif @scanner.scan(end_tag) @listener.tag_end(@scanner[1]) else raise "Unimplemented element" end end end end class PTag @@mappings = { } def PTag::mappings @@mappings end def PTag::inherited( sub_class ) key = sub_class.to_s.downcase key.gsub!(/^plist::/, '' ) key.gsub!(/^p/, '') unless key == "plist" @@mappings[key] = sub_class end attr_accessor :text, :children def initialize @children = Array.new end def to_ruby raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}" end end class PList < PTag def to_ruby children.first.to_ruby if children.first end end class PDict < PTag def to_ruby dict = Hash.new key = nil children.each do |c| if key.nil? key = c.to_ruby else dict[key] = c.to_ruby key = nil end end dict end end class PKey < PTag def to_ruby CGI::unescapeHTML(text || '') end end class PString < PTag def to_ruby CGI::unescapeHTML(text || '') end end class PArray < PTag def to_ruby children.collect do |c| c.to_ruby end end end class PInteger < PTag def to_ruby text.to_i end end class PTrue < PTag def to_ruby true end end class PFalse < PTag def to_ruby false end end class PReal < PTag def to_ruby text.to_f end end require 'date' class PDate < PTag def to_ruby DateTime.parse(text) end end require 'base64' class PData < PTag def to_ruby data = Base64.decode64(text.gsub(/\s+/, '')) begin return Marshal.load(data) rescue Exception => e io = StringIO.new io.write data io.rewind return io end end end end plist-3.1.0/lib/plist/generator.rb0000644000175000017500000001461312131276043016444 0ustar boutilboutil#!/usr/bin/env ruby # # = plist # # Copyright 2006-2010 Ben Bleything and Patrick May # Distributed under the MIT License # module Plist ; end # === Create a plist # You can dump an object to a plist in one of two ways: # # * Plist::Emit.dump(obj) # * obj.to_plist # * This requires that you mixin the Plist::Emit module, which is already done for +Array+ and +Hash+. # # The following Ruby classes are converted into native plist types: # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time, true, false # * +Array+ and +Hash+ are both recursive; their elements will be converted into plist nodes inside the and containers (respectively). # * +IO+ (and its descendants) and +StringIO+ objects are read from and their contents placed in a element. # * 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. # # For detailed usage instructions, refer to USAGE[link:files/docs/USAGE.html] and the methods documented below. module Plist::Emit # Helper method for injecting into classes. Calls Plist::Emit.dump with +self+. def to_plist(envelope = true) return Plist::Emit.dump(self, envelope) end # Helper method for injecting into classes. Calls Plist::Emit.save_plist with +self+. def save_plist(filename) Plist::Emit.save_plist(self, filename) end # The following Ruby classes are converted into native plist types: # Array, Bignum, Date, DateTime, Fixnum, Float, Hash, Integer, String, Symbol, Time # # Write us (via RubyForge) if you think another class can be coerced safely into one of the expected plist classes. # # +IO+ and +StringIO+ objects are encoded and placed in elements; other objects are Marshal.dump'ed unless they implement +to_plist_node+. # # 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. def self.dump(obj, envelope = true) output = plist_node(obj) output = wrap(output) if envelope return output end # Writes the serialized object's plist to the specified filename. def self.save_plist(obj, filename) File.open(filename, 'wb') do |f| f.write(obj.to_plist) end end private def self.plist_node(element) output = '' if element.respond_to? :to_plist_node output << element.to_plist_node else case element when Array if element.empty? output << "\n" else output << tag('array') { element.collect {|e| plist_node(e)} } end when Hash if element.empty? output << "\n" else inner_tags = [] element.keys.sort.each do |k| v = element[k] inner_tags << tag('key', CGI::escapeHTML(k.to_s)) inner_tags << plist_node(v) end output << tag('dict') { inner_tags } end when true, false output << "<#{element}/>\n" when Time output << tag('date', element.utc.strftime('%Y-%m-%dT%H:%M:%SZ')) when Date # also catches DateTime output << tag('date', element.strftime('%Y-%m-%dT%H:%M:%SZ')) when String, Symbol, Fixnum, Bignum, Integer, Float output << tag(element_type(element), CGI::escapeHTML(element.to_s)) when IO, StringIO element.rewind contents = element.read # note that apple plists are wrapped at a different length then # what ruby's base64 wraps by default. # I used #encode64 instead of #b64encode (which allows a length arg) # because b64encode is b0rked and ignores the length arg. data = "\n" Base64::encode64(contents).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" } output << tag('data', data) else output << comment( 'The element below contains a Ruby object which has been serialized with Marshal.dump.' ) data = "\n" Base64::encode64(Marshal.dump(element)).gsub(/\s+/, '').scan(/.{1,68}/o) { data << $& << "\n" } output << tag('data', data ) end end return output end def self.comment(content) return "\n" end def self.tag(type, contents = '', &block) out = nil if block_given? out = IndentedString.new out << "<#{type}>" out.raise_indent out << block.call out.lower_indent out << "" else out = "<#{type}>#{contents.to_s}\n" end return out.to_s end def self.wrap(contents) output = '' output << '' + "\n" output << '' + "\n" output << '' + "\n" output << contents output << '' + "\n" return output end def self.element_type(item) case item when String, Symbol 'string' when Fixnum, Bignum, Integer 'integer' when Float 'real' else raise "Don't know about this data type... something must be wrong!" end end private class IndentedString #:nodoc: attr_accessor :indent_string def initialize(str = "\t") @indent_string = str @contents = '' @indent_level = 0 end def to_s return @contents end def raise_indent @indent_level += 1 end def lower_indent @indent_level -= 1 if @indent_level > 0 end def <<(val) if val.is_a? Array val.each do |f| self << f end else # if it's already indented, don't bother indenting further unless val =~ /\A#{@indent_string}/ indent = @indent_string * @indent_level @contents << val.gsub(/^/, indent) else @contents << val end # it already has a newline, don't add another @contents << "\n" unless val =~ /\n$/ end end end end # we need to add this so sorting hash keys works properly class Symbol #:nodoc: def <=> (other) self.to_s <=> other.to_s end end class Array #:nodoc: include Plist::Emit end class Hash #:nodoc: include Plist::Emit end