├── ktx_quick.zip
├── README.md
├── SnapshotImageFinder.py
├── SnapshotTriage.py
└── ccl_bplist.py
/ktx_quick.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abrignoni/iOS-Snapshot-Triage-Parser/HEAD/ktx_quick.zip
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iOS-Snapshot-Triage-Parser
2 |
3 | For details on how to use the scripts and macOS Automator quick action see the following blog post:
4 | https://abrignoni.blogspot.com/2019/09/ios-snapshots-triage-parser-working.html
5 |
6 | Script purpose and workflow:
7 | 1. Run SnapshotImageFinder.py to identify iOS snapshot images and extract them from a targeted iOS file system extraction directory. These files end with @3.ktx or @2.ktx.
8 |
9 | 2. Extract the macOS Automator quick action from the ktx_quick.zip file. With it convert all the .ktx files extracted in step #1 to .png format.
10 |
11 | 3. Run SnapshotTriage.py to extract, parse and match the images with the snapshot metadata contained in the extracted bplists from the applicationState.db datastore. This script accepts 2 parameters, the directory where the applicationState.db is located and the directory where the .png files are located.
12 |
13 | 4. After execution of the previews steps a triage report directory will be created containing the extracted bplists and iOS snapshot reports in HTML per app.
14 |
--------------------------------------------------------------------------------
/SnapshotImageFinder.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from argparse import RawTextHelpFormatter
3 | from six.moves.configparser import RawConfigParser
4 | from shutil import copy
5 | import sys
6 | import io
7 | import os
8 | import glob
9 |
10 | parser = argparse.ArgumentParser(description="\
11 | iOS Snapshot KTX File Finder\
12 | \n\n Locate iOS snapshot KTX files."
13 | , prog='SnapshotKtxFinder.py'
14 | , formatter_class=RawTextHelpFormatter)
15 | parser.add_argument('data_dir_to_analyze',help="Path to Data Directory.")
16 |
17 | args = parser.parse_args()
18 | data_dir = args.data_dir_to_analyze
19 |
20 | foldername = ('FoundSnapshotImages')
21 | pathfound = 0
22 | path = os.getcwd()
23 | outpath = path + "/" + foldername
24 | count = 0
25 | pathlog =[]
26 | os.makedirs(outpath)
27 | print("\n--------------------------------------------------------------------------------------")
28 | print("iOS Snapshot KTX file finder")
29 | print("By: Alexis Brignoni | @AlexisBrignoni | abrignoni.com")
30 | print("Source directory to be searched: " + data_dir)
31 | print("\n--------------------------------------------------------------------------------------")
32 | print("")
33 | print('Searching for iOS Snapshot images.')
34 | print('Please wait...')
35 |
36 | for root, dirs, filenames in os.walk(data_dir):
37 | for f in filenames:
38 | if f.endswith('@3x.ktx'):
39 | pathfound = os.path.join(root, f)
40 | copy(pathfound, outpath)
41 | pathlog.insert(count, pathfound)
42 | #print(pathlog[count])
43 | count = count + 1
44 |
45 | elif f.endswith('@2x.ktx'):
46 | pathfound = os.path.join(root, f)
47 | copy(pathfound, outpath)
48 | pathlog.insert(count, pathfound)
49 | #print(pathlog[count])
50 | count = count + 1
51 |
52 | if pathfound == 0:
53 | print('')
54 | print('No snapshot ktx or png files available.')
55 | else:
56 | print('')
57 | print('Snapshot ktx files moved to: '+ outpath)
58 | print('Snapshot ktx files moved total: '+ str(count))
59 | with open("SnapshotImageFinderPathsRreport.txt", "w") as output:
60 | output.write('iOS Snapshot KTX File Finder \n')
61 | output.write('By: Alexis Brignoni | @AlexisBrignoni | abrignoni.com \n')
62 | output.write('Snapshot KTX files moved to: '+ outpath+'\n')
63 | output.write('Snapshot ktx files moved total: '+ str(count)+'\n')
64 | output.write('\n')
65 | output.write('KTX file paths from source directory: \n')
66 | for item in pathlog:
67 | output.write('%s\n' % item)
68 | print('Copy log at: '+path+'\SnapshotImageFinderPathsRreport.txt')
69 |
--------------------------------------------------------------------------------
/SnapshotTriage.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import argparse
3 | from argparse import RawTextHelpFormatter
4 | from six.moves.configparser import RawConfigParser
5 | import sys
6 | import ccl_bplist
7 | import plistlib
8 | import io
9 | import os
10 | import glob
11 | import sqlite3
12 | from shutil import copy
13 | from time import process_time
14 |
15 | parser = argparse.ArgumentParser(description="\
16 | iOS Snapshot KTX Traige Parser\
17 | \n\n Parse iOS snapshot plists and matching ktx files."
18 | , prog='SnapTriage.py'
19 | , formatter_class=RawTextHelpFormatter)
20 | parser.add_argument('data_dir_snaps',help="Path to the Snapshot images Directory")
21 | parser.add_argument('data_dir_appState',help="Path to the applicationState.db Directory..")
22 |
23 | args = parser.parse_args()
24 | data_dir = args.data_dir_snaps
25 | appState_dir = args.data_dir_appState
26 | count = 0
27 | pathfound = 0
28 |
29 | #create directories
30 | #foldername = str(int(datetime.datetime.now().timestamp()))
31 | foldername = ("iOSSnapshotTriageReports_" + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
32 |
33 | #calculate timestamps
34 | unix = datetime.datetime(1970, 1, 1)
35 | cocoa = datetime.datetime(2001, 1, 1)
36 | delta = cocoa - unix
37 |
38 | for root, dirs, filenames in os.walk(appState_dir):
39 | for f in filenames:
40 | if f == "applicationState.db":
41 | pathfound = os.path.join(root, f)
42 |
43 |
44 | if pathfound == 0:
45 | print("No applicationState.db")
46 | else:
47 | path = os.getcwd()
48 | try:
49 | outpath = path + "/" + foldername
50 | os.mkdir(outpath)
51 | os.mkdir(outpath+"/ExtractedBplistsFirstLevel")
52 | os.mkdir(outpath+"/ExtractedBplistsSecondLevel")
53 | os.mkdir(outpath+"/Reports")
54 | os.mkdir(outpath+"/Reports/images")
55 | except OSError:
56 | print("Error making directories")
57 |
58 |
59 |
60 | print("\n--------------------------------------------------------------------------------------")
61 | print("iOS Snapshot Triage Parser.")
62 | print("Objective: Triage iOS Snapshot images.")
63 | print("By: Alexis Brignoni | @AlexisBrignoni | abrignoni.com")
64 | print("Processed images directory: " + data_dir)
65 | print("Snapshots database: " + appState_dir)
66 | print("\n--------------------------------------------------------------------------------------")
67 | print("")
68 |
69 | print("Database located at: "+pathfound)
70 | print("Please wait...")
71 |
72 | #connect sqlite databases
73 | #database = 'applicationState.db'
74 | db = sqlite3.connect(pathfound)
75 | cursor = db.cursor()
76 |
77 | cursor.execute('''SELECT
78 | application_identifier_tab.id,
79 | application_identifier_tab.application_identifier,
80 | kvs.value
81 | FROM kvs, application_identifier_tab, key_tab
82 | WHERE application_identifier_tab.id = kvs.application_identifier
83 | and key_tab.key = 'XBApplicationSnapshotManifest'
84 | and key_tab.id = kvs.key
85 | ''')
86 |
87 | all_rows = cursor.fetchall()
88 |
89 | for row in all_rows:
90 | bundleid = row[1]
91 | wbplist = row [2]
92 | print('Processing: '+bundleid)
93 | output_file = open('./'+foldername+'/ExtractedBplistsFirstLevel/'+bundleid+'.bplist', 'wb') #export from applicationState.db
94 | output_file.write(wbplist)
95 | output_file.close()
96 |
97 | g = open('./'+foldername+'/ExtractedBplistsFirstLevel/'+bundleid+'.bplist', 'rb')
98 | plistg = ccl_bplist.load(g)
99 |
100 | output_file = open('./'+foldername+'/ExtractedBplistsSecondLevel/'+bundleid+'.bplist', 'wb') #export from applicationState.db
101 | output_file.write(plistg)
102 | output_file.close()
103 |
104 | g = open('./'+foldername+'/ExtractedBplistsSecondLevel/'+bundleid+'.bplist', 'rb')
105 | plistg = ccl_bplist.load(g)
106 | long = len(plistg['$objects'])
107 |
108 | #start report
109 | h = open('./'+foldername+'/Reports/'+bundleid+'.html', 'w') #write report
110 | h.write('
')
111 | h.write('iOS Snapshots Triage Report
')
112 | h.write('Application: '+bundleid+'
')
113 | h.write('Data aggregated per following data source: '+pathfound)
114 | h.write('
')
115 | h.write('Press on the image to get full size')
116 | h.write('
')
117 | h.write ('')
118 | h.write('
')
119 |
120 | #opne table
121 | h.write('')
122 | for i in range (0, long):
123 | test = (plistg['$objects'][i])
124 | try:
125 | if test.endswith('@3x.ktx'):
126 | h.write('Filename: '+str(test)+' |
')
127 | h.write('')
128 | h.write('')
129 | image = test.split('.')
130 | imagenew = image[0]
131 | path2 = os.getcwd()
132 | imagepath = (path+'/'+data_dir+'/'+imagenew+'.png')
133 | imageoutpath = (outpath+'/Reports/images')
134 | #print ('path2: '+path2)
135 | #print('image: '+imagepath)
136 | #print('imagefinal: '+imageoutpath)
137 | #print('foldername: '+foldername)
138 | copy(imagepath, imageoutpath)
139 | h.write('')
140 | h.write(' ')
142 | h.write('')
143 | h.write(' | ')
144 | h.write('
')
145 | #new html block
146 | #convert the ktx to jpg and add to html
147 | #print(test)
148 |
149 | except:
150 | pass
151 |
152 | try:
153 | if test.endswith('@2x.ktx'):
154 | h.write('Filename: '+str(test)+' |
')
155 | h.write('')
156 | h.write('')
157 | image = test.split('.')
158 | imagenew = image[0]
159 | path2 = os.getcwd()
160 | imagepath = (path+'/'+data_dir+'/'+imagenew+'.png')
161 | imageoutpath = (outpath+'/Reports/images')
162 | #print ('path2: '+path2)
163 | #print('image: '+imagepath)
164 | #print('imagefinal: '+imageoutpath)
165 | #print('foldername: '+foldername)
166 | copy(imagepath, imageoutpath)
167 | h.write('')
168 | h.write(' ')
170 | h.write('')
171 | h.write(' | ')
172 | h.write('
')
173 | #new html block
174 | #convert the ktx to jpg and add to html
175 | #print(test)
176 | #new html block
177 | #convert the ktx to jpg and add to html
178 |
179 | except:
180 | pass
181 |
182 | try:
183 | if test.endswith('.png'):
184 | h.write('')
185 | h.write('')
186 | h.write('File not found on system. '+str(test)+'')
187 | h.write(' | ')
188 | h.write('
')
189 | #new html block
190 | #convert the ktx to jpg and add to html
191 | #print(test)
192 |
193 | except:
194 | pass
195 |
196 | try:
197 | if test['NS.time']:
198 | dates = test['NS.time']
199 | dia = str(dates)
200 | dias = (dia.rsplit('.', 1)[0])
201 | timestamp = datetime.datetime.fromtimestamp(int(dias)) + delta
202 |
203 | h.write('')
204 | h.write('| ')
205 | h.write(str(timestamp))
206 | h.write(' | ')
207 | h.write('
')
208 |
209 | #print(timestamp)
210 |
211 | except:
212 | pass
213 | h.write('')
214 | h.write('
')
215 | h.write('Script by: abrignoni.com')
216 | h.write('')
217 | h.close()
218 | count = count + 1
219 | print('Total of apps with processed snapshots: '+str(count))
220 |
--------------------------------------------------------------------------------
/ccl_bplist.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2012-2016, CCL Forensics
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of the CCL Forensics nor the
13 | names of its contributors may be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL CCL FORENSICS BE LIABLE FOR ANY
20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | """
27 |
28 | import sys
29 | import os
30 | import struct
31 | import datetime
32 |
33 | __version__ = "0.21"
34 | __description__ = "Converts Apple binary PList files into a native Python data structure"
35 | __contact__ = "Alex Caithness"
36 |
37 | _object_converter = None
38 | def set_object_converter(function):
39 | """Sets the object converter function to be used when retrieving objects from the bplist.
40 | default is None (which will return objects in their raw form).
41 | A built in converter (ccl_bplist.NSKeyedArchiver_common_objects_convertor) which is geared
42 | toward dealling with common types in NSKeyedArchiver is available which can simplify code greatly
43 | when dealling with these types of files."""
44 | if not hasattr(function, "__call__"):
45 | raise TypeError("function is not a function")
46 | global _object_converter
47 | _object_converter = function
48 |
49 | class BplistError(Exception):
50 | pass
51 |
52 | class BplistUID:
53 | def __init__(self, value):
54 | self.value = value
55 |
56 | def __repr__(self):
57 | return "UID: {0}".format(self.value)
58 |
59 | def __str__(self):
60 | return self.__repr__()
61 |
62 | def __decode_multibyte_int(b, signed=True):
63 | if len(b) == 1:
64 | fmt = ">B" # Always unsigned?
65 | elif len(b) == 2:
66 | fmt = ">h"
67 | elif len(b) == 3:
68 | if signed:
69 | return ((b[0] << 16) | struct.unpack(">H", b[1:])[0]) - ((b[0] >> 7) * 2 * 0x800000)
70 | else:
71 | return (b[0] << 16) | struct.unpack(">H", b[1:])[0]
72 | elif len(b) == 4:
73 | fmt = ">i"
74 | elif len(b) == 8:
75 | fmt = ">q"
76 | elif len(b) == 16:
77 | # special case for BigIntegers
78 | high, low = struct.unpack(">QQ", b)
79 | result = (high << 64) | low
80 | if high & 0x8000000000000000 and signed:
81 | result -= 0x100000000000000000000000000000000
82 | return result
83 | else:
84 | raise BplistError("Cannot decode multibyte int of length {0}".format(len(b)))
85 |
86 | if signed and len(b) > 1:
87 | return struct.unpack(fmt.lower(), b)[0]
88 | else:
89 | return struct.unpack(fmt.upper(), b)[0]
90 |
91 | def __decode_float(b, signed=True):
92 | if len(b) == 4:
93 | fmt = ">f"
94 | elif len(b) == 8:
95 | fmt = ">d"
96 | else:
97 | raise BplistError("Cannot decode float of length {0}".format(len(b)))
98 |
99 | if signed:
100 | return struct.unpack(fmt.lower(), b)[0]
101 | else:
102 | return struct.unpack(fmt.upper(), b)[0]
103 |
104 | def __decode_object(f, offset, collection_offset_size, offset_table):
105 | # Move to offset and read type
106 | #print("Decoding object at offset {0}".format(offset))
107 | f.seek(offset)
108 | # A little hack to keep the script portable between py2.x and py3k
109 | if sys.version_info[0] < 3:
110 | type_byte = ord(f.read(1)[0])
111 | else:
112 | type_byte = f.read(1)[0]
113 | #print("Type byte: {0}".format(hex(type_byte)))
114 | if type_byte == 0x00: # Null 0000 0000
115 | return None
116 | elif type_byte == 0x08: # False 0000 1000
117 | return False
118 | elif type_byte == 0x09: # True 0000 1001
119 | return True
120 | elif type_byte == 0x0F: # Fill 0000 1111
121 | raise BplistError("Fill type not currently supported at offset {0}".format(f.tell())) # Not sure what to return really...
122 | elif type_byte & 0xF0 == 0x10: # Int 0001 xxxx
123 | int_length = 2 ** (type_byte & 0x0F)
124 | int_bytes = f.read(int_length)
125 | return __decode_multibyte_int(int_bytes)
126 | elif type_byte & 0xF0 == 0x20: # Float 0010 nnnn
127 | float_length = 2 ** (type_byte & 0x0F)
128 | float_bytes = f.read(float_length)
129 | return __decode_float(float_bytes)
130 | elif type_byte & 0xFF == 0x33: # Date 0011 0011
131 | date_bytes = f.read(8)
132 | date_value = __decode_float(date_bytes)
133 | try:
134 | result = datetime.datetime(2001,1,1) + datetime.timedelta(seconds = date_value)
135 | except OverflowError:
136 | result = datetime.datetime.min
137 | return result
138 | elif type_byte & 0xF0 == 0x40: # Data 0100 nnnn
139 | if type_byte & 0x0F != 0x0F:
140 | # length in 4 lsb
141 | data_length = type_byte & 0x0F
142 | else:
143 | # A little hack to keep the script portable between py2.x and py3k
144 | if sys.version_info[0] < 3:
145 | int_type_byte = ord(f.read(1)[0])
146 | else:
147 | int_type_byte = f.read(1)[0]
148 | if int_type_byte & 0xF0 != 0x10:
149 | raise BplistError("Long Data field definition not followed by int type at offset {0}".format(f.tell()))
150 | int_length = 2 ** (int_type_byte & 0x0F)
151 | int_bytes = f.read(int_length)
152 | data_length = __decode_multibyte_int(int_bytes, False)
153 | return f.read(data_length)
154 | elif type_byte & 0xF0 == 0x50: # ASCII 0101 nnnn
155 | if type_byte & 0x0F != 0x0F:
156 | # length in 4 lsb
157 | ascii_length = type_byte & 0x0F
158 | else:
159 | # A little hack to keep the script portable between py2.x and py3k
160 | if sys.version_info[0] < 3:
161 | int_type_byte = ord(f.read(1)[0])
162 | else:
163 | int_type_byte = f.read(1)[0]
164 | if int_type_byte & 0xF0 != 0x10:
165 | raise BplistError("Long ASCII field definition not followed by int type at offset {0}".format(f.tell()))
166 | int_length = 2 ** (int_type_byte & 0x0F)
167 | int_bytes = f.read(int_length)
168 | ascii_length = __decode_multibyte_int(int_bytes, False)
169 | return f.read(ascii_length).decode("ascii")
170 | elif type_byte & 0xF0 == 0x60: # UTF-16 0110 nnnn
171 | if type_byte & 0x0F != 0x0F:
172 | # length in 4 lsb
173 | utf16_length = (type_byte & 0x0F) * 2 # Length is characters - 16bit width
174 | else:
175 | # A little hack to keep the script portable between py2.x and py3k
176 | if sys.version_info[0] < 3:
177 | int_type_byte = ord(f.read(1)[0])
178 | else:
179 | int_type_byte = f.read(1)[0]
180 | if int_type_byte & 0xF0 != 0x10:
181 | raise BplistError("Long UTF-16 field definition not followed by int type at offset {0}".format(f.tell()))
182 | int_length = 2 ** (int_type_byte & 0x0F)
183 | int_bytes = f.read(int_length)
184 | utf16_length = __decode_multibyte_int(int_bytes, False) * 2
185 | return f.read(utf16_length).decode("utf_16_be")
186 | elif type_byte & 0xF0 == 0x80: # UID 1000 nnnn
187 | uid_length = (type_byte & 0x0F) + 1
188 | uid_bytes = f.read(uid_length)
189 | return BplistUID(__decode_multibyte_int(uid_bytes, signed=False))
190 | elif type_byte & 0xF0 == 0xA0: # Array 1010 nnnn
191 | if type_byte & 0x0F != 0x0F:
192 | # length in 4 lsb
193 | array_count = type_byte & 0x0F
194 | else:
195 | # A little hack to keep the script portable between py2.x and py3k
196 | if sys.version_info[0] < 3:
197 | int_type_byte = ord(f.read(1)[0])
198 | else:
199 | int_type_byte = f.read(1)[0]
200 | if int_type_byte & 0xF0 != 0x10:
201 | raise BplistError("Long Array field definition not followed by int type at offset {0}".format(f.tell()))
202 | int_length = 2 ** (int_type_byte & 0x0F)
203 | int_bytes = f.read(int_length)
204 | array_count = __decode_multibyte_int(int_bytes, signed=False)
205 | array_refs = []
206 | for i in range(array_count):
207 | array_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
208 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in array_refs]
209 | elif type_byte & 0xF0 == 0xC0: # Set 1010 nnnn
210 | if type_byte & 0x0F != 0x0F:
211 | # length in 4 lsb
212 | set_count = type_byte & 0x0F
213 | else:
214 | # A little hack to keep the script portable between py2.x and py3k
215 | if sys.version_info[0] < 3:
216 | int_type_byte = ord(f.read(1)[0])
217 | else:
218 | int_type_byte = f.read(1)[0]
219 | if int_type_byte & 0xF0 != 0x10:
220 | raise BplistError("Long Set field definition not followed by int type at offset {0}".format(f.tell()))
221 | int_length = 2 ** (int_type_byte & 0x0F)
222 | int_bytes = f.read(int_length)
223 | set_count = __decode_multibyte_int(int_bytes, signed=False)
224 | set_refs = []
225 | for i in range(set_count):
226 | set_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
227 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in set_refs]
228 | elif type_byte & 0xF0 == 0xD0: # Dict 1011 nnnn
229 | if type_byte & 0x0F != 0x0F:
230 | # length in 4 lsb
231 | dict_count = type_byte & 0x0F
232 | else:
233 | # A little hack to keep the script portable between py2.x and py3k
234 | if sys.version_info[0] < 3:
235 | int_type_byte = ord(f.read(1)[0])
236 | else:
237 | int_type_byte = f.read(1)[0]
238 | #print("Dictionary length int byte: {0}".format(hex(int_type_byte)))
239 | if int_type_byte & 0xF0 != 0x10:
240 | raise BplistError("Long Dict field definition not followed by int type at offset {0}".format(f.tell()))
241 | int_length = 2 ** (int_type_byte & 0x0F)
242 | int_bytes = f.read(int_length)
243 | dict_count = __decode_multibyte_int(int_bytes, signed=False)
244 | key_refs = []
245 | #print("Dictionary count: {0}".format(dict_count))
246 | for i in range(dict_count):
247 | key_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
248 | value_refs = []
249 | for i in range(dict_count):
250 | value_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
251 |
252 | dict_result = {}
253 | for i in range(dict_count):
254 | #print("Key ref: {0}\tVal ref: {1}".format(key_refs[i], value_refs[i]))
255 | key = __decode_object(f, offset_table[key_refs[i]], collection_offset_size, offset_table)
256 | val = __decode_object(f, offset_table[value_refs[i]], collection_offset_size, offset_table)
257 | dict_result[key] = val
258 | return dict_result
259 |
260 |
261 | def load(f):
262 | """
263 | Reads and converts a file-like object containing a binary property list.
264 | Takes a file-like object (must support reading and seeking) as an argument
265 | Returns a data structure representing the data in the property list
266 | """
267 | # Check magic number
268 | if f.read(8) != b"bplist00":
269 | raise BplistError("Bad file header")
270 |
271 | # Read trailer
272 | f.seek(-32, os.SEEK_END)
273 | trailer = f.read(32)
274 | offset_int_size, collection_offset_size, object_count, top_level_object_index, offest_table_offset = struct.unpack(">6xbbQQQ", trailer)
275 |
276 | # Read offset table
277 | f.seek(offest_table_offset)
278 | offset_table = []
279 | for i in range(object_count):
280 | offset_table.append(__decode_multibyte_int(f.read(offset_int_size), False))
281 |
282 | return __decode_object(f, offset_table[top_level_object_index], collection_offset_size, offset_table)
283 |
284 |
285 | def NSKeyedArchiver_common_objects_convertor(o):
286 | """Built in converter function (suitable for submission to set_object_converter()) which automatically
287 | converts the following common data-types found in NSKeyedArchiver:
288 | NSDictionary/NSMutableDictionary;
289 | NSArray/NSMutableArray;
290 | NSSet/NSMutableSet
291 | NSString/NSMutableString
292 | NSDate
293 | $null strings"""
294 | # Conversion: NSDictionary
295 | if is_nsmutabledictionary(o):
296 | return convert_NSMutableDictionary(o)
297 | # Conversion: NSArray
298 | elif is_nsarray(o):
299 | return convert_NSArray(o)
300 | elif is_isnsset(o):
301 | return convert_NSSet(o)
302 | # Conversion: NSString
303 | elif is_nsstring(o):
304 | return convert_NSString(o)
305 | # Conversion: NSDate
306 | elif is_nsdate(o):
307 | return convert_NSDate(o)
308 | # Conversion: "$null" string
309 | elif isinstance(o, str) and o == "$null":
310 | return None
311 | # Fallback:
312 | else:
313 | return o
314 |
315 | def NSKeyedArchiver_convert(o, object_table):
316 | if isinstance(o, list):
317 | #return NsKeyedArchiverList(o, object_table)
318 | result = NsKeyedArchiverList(o, object_table)
319 | elif isinstance(o, dict):
320 | #return NsKeyedArchiverDictionary(o, object_table)
321 | result = NsKeyedArchiverDictionary(o, object_table)
322 | elif isinstance(o, BplistUID):
323 | #return NSKeyedArchiver_convert(object_table[o.value], object_table)
324 | result = NSKeyedArchiver_convert(object_table[o.value], object_table)
325 | else:
326 | #return o
327 | result = o
328 |
329 | if _object_converter:
330 | return _object_converter(result)
331 | else:
332 | return result
333 |
334 |
335 | class NsKeyedArchiverDictionary(dict):
336 | def __init__(self, original_dict, object_table):
337 | super(NsKeyedArchiverDictionary, self).__init__(original_dict)
338 | self.object_table = object_table
339 |
340 | def __getitem__(self, index):
341 | o = super(NsKeyedArchiverDictionary, self).__getitem__(index)
342 | return NSKeyedArchiver_convert(o, self.object_table)
343 |
344 | def get(self, key, default=None):
345 | return self[key] if key in self else default
346 |
347 | def values(self):
348 | for k in self:
349 | yield self[k]
350 |
351 | def items(self):
352 | for k in self:
353 | yield k, self[k]
354 |
355 | class NsKeyedArchiverList(list):
356 | def __init__(self, original_iterable, object_table):
357 | super(NsKeyedArchiverList, self).__init__(original_iterable)
358 | self.object_table = object_table
359 |
360 | def __getitem__(self, index):
361 | o = super(NsKeyedArchiverList, self).__getitem__(index)
362 | return NSKeyedArchiver_convert(o, self.object_table)
363 |
364 | def __iter__(self):
365 | for o in super(NsKeyedArchiverList, self).__iter__():
366 | yield NSKeyedArchiver_convert(o, self.object_table)
367 |
368 |
369 | def deserialise_NsKeyedArchiver(obj, parse_whole_structure=False):
370 | """Deserialises an NSKeyedArchiver bplist rebuilding the structure.
371 | obj should usually be the top-level object returned by the load()
372 | function."""
373 |
374 | # Check that this is an archiver and version we understand
375 | if not isinstance(obj, dict):
376 | raise TypeError("obj must be a dict")
377 | if "$archiver" not in obj or obj["$archiver"] not in ("NSKeyedArchiver", "NRKeyedArchiver"):
378 | raise ValueError("obj does not contain an '$archiver' key or the '$archiver' is unrecognised")
379 | if "$version" not in obj or obj["$version"] != 100000:
380 | raise ValueError("obj does not contain a '$version' key or the '$version' is unrecognised")
381 |
382 | object_table = obj["$objects"]
383 | if "root" in obj["$top"] and not parse_whole_structure:
384 | return NSKeyedArchiver_convert(obj["$top"]["root"], object_table)
385 | else:
386 | return NSKeyedArchiver_convert(obj["$top"], object_table)
387 |
388 | # NSMutableDictionary convenience functions
389 | def is_nsmutabledictionary(obj):
390 | if not isinstance(obj, dict):
391 | return False
392 | if "$class" not in obj.keys():
393 | return False
394 | if obj["$class"].get("$classname") not in ("NSMutableDictionary", "NSDictionary"):
395 | return False
396 | if "NS.keys" not in obj.keys():
397 | return False
398 | if "NS.objects" not in obj.keys():
399 | return False
400 |
401 | return True
402 |
403 | def convert_NSMutableDictionary(obj):
404 | """Converts a NSKeyedArchiver serialised NSMutableDictionary into
405 | a straight dictionary (rather than two lists as it is serialised
406 | as)"""
407 |
408 | # The dictionary is serialised as two lists (one for keys and one
409 | # for values) which obviously removes all convenience afforded by
410 | # dictionaries. This function converts this structure to an
411 | # actual dictionary so that values can be accessed by key.
412 |
413 | if not is_nsmutabledictionary(obj):
414 | raise ValueError("obj does not have the correct structure for a NSDictionary/NSMutableDictionary serialised to a NSKeyedArchiver")
415 | keys = obj["NS.keys"]
416 | vals = obj["NS.objects"]
417 |
418 | # sense check the keys and values:
419 | if not isinstance(keys, list):
420 | raise TypeError("The 'NS.keys' value is an unexpected type (expected list; actual: {0}".format(type(keys)))
421 | if not isinstance(vals, list):
422 | raise TypeError("The 'NS.objects' value is an unexpected type (expected list; actual: {0}".format(type(vals)))
423 | if len(keys) != len(vals):
424 | raise ValueError("The length of the 'NS.keys' list ({0}) is not equal to that of the 'NS.objects ({1})".format(len(keys), len(vals)))
425 |
426 | result = {}
427 | for i,k in enumerate(keys):
428 | if k in result:
429 | raise ValueError("The 'NS.keys' list contains duplicate entries")
430 | result[k] = vals[i]
431 |
432 | return result
433 |
434 | # NSArray convenience functions
435 | def is_nsarray(obj):
436 | if not isinstance(obj, dict):
437 | return False
438 | if "$class" not in obj.keys():
439 | return False
440 | if obj["$class"].get("$classname") not in ("NSArray", "NSMutableArray"):
441 | return False
442 | if "NS.objects" not in obj.keys():
443 | return False
444 |
445 | return True
446 |
447 | def convert_NSArray(obj):
448 | if not is_nsarray(obj):
449 | raise ValueError("obj does not have the correct structure for a NSArray/NSMutableArray serialised to a NSKeyedArchiver")
450 |
451 | return obj["NS.objects"]
452 |
453 | # NSSet convenience functions
454 | def is_isnsset(obj):
455 | if not isinstance(obj, dict):
456 | return False
457 | if "$class" not in obj.keys():
458 | return False
459 | if obj["$class"].get("$classname") not in ("NSSet", "NSMutableSet"):
460 | return False
461 | if "NS.objects" not in obj.keys():
462 | return False
463 |
464 | return True
465 |
466 | def convert_NSSet(obj):
467 | if not is_isnsset(obj):
468 | raise ValueError("obj does not have the correct structure for a NSSet/NSMutableSet serialised to a NSKeyedArchiver")
469 |
470 | return list(obj["NS.objects"])
471 |
472 | # NSString convenience functions
473 | def is_nsstring(obj):
474 | if not isinstance(obj, dict):
475 | return False
476 | if "$class" not in obj.keys():
477 | return False
478 | if obj["$class"].get("$classname") not in ("NSString", "NSMutableString"):
479 | return False
480 | if "NS.string" not in obj.keys():
481 | return False
482 | return True
483 |
484 | def convert_NSString(obj):
485 | if not is_nsstring(obj):
486 | raise ValueError("obj does not have the correct structure for a NSString/NSMutableString serialised to a NSKeyedArchiver")
487 |
488 | return obj["NS.string"]
489 |
490 | # NSDate convenience functions
491 | def is_nsdate(obj):
492 | if not isinstance(obj, dict):
493 | return False
494 | if "$class" not in obj.keys():
495 | return False
496 | if obj["$class"].get("$classname") not in ("NSDate"):
497 | return False
498 | if "NS.time" not in obj.keys():
499 | return False
500 |
501 | return True
502 |
503 | def convert_NSDate(obj):
504 | if not is_nsdate(obj):
505 | raise ValueError("obj does not have the correct structure for a NSDate serialised to a NSKeyedArchiver")
506 |
507 | return datetime.datetime(2001, 1, 1) + datetime.timedelta(seconds=obj["NS.time"])
508 |
--------------------------------------------------------------------------------