ds-store 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ds_store-1.0.0/PKG-INFO +25 -0
- ds_store-1.0.0/ds_store/__init__.py +3 -0
- ds_store-1.0.0/ds_store/buddy.py +455 -0
- ds_store-1.0.0/ds_store/store.py +1201 -0
- ds_store-1.0.0/ds_store.egg-info/PKG-INFO +25 -0
- ds_store-1.0.0/ds_store.egg-info/SOURCES.txt +9 -0
- ds_store-1.0.0/ds_store.egg-info/dependency_links.txt +1 -0
- ds_store-1.0.0/ds_store.egg-info/requires.txt +2 -0
- ds_store-1.0.0/ds_store.egg-info/top_level.txt +1 -0
- ds_store-1.0.0/setup.cfg +5 -0
- ds_store-1.0.0/setup.py +35 -0
ds_store-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 1.1
|
|
2
|
+
Name: ds_store
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Manipulate Finder .DS_Store files from Python
|
|
5
|
+
Home-page: http://alastairs-place.net/projects/ds_store
|
|
6
|
+
Author: Alastair Houghton
|
|
7
|
+
Author-email: alastair@alastairs-place.net
|
|
8
|
+
License: MIT License
|
|
9
|
+
Description:
|
|
10
|
+
``ds_store`` lets you examine and modify ``.DS_Store`` files from Python code;
|
|
11
|
+
since it is written in pure Python, it is portable and will run on any
|
|
12
|
+
platform, not just Mac OS X.
|
|
13
|
+
|
|
14
|
+
Credit is due to Wim Lewis <wiml@hhhh.org>, Mark Mentovai and
|
|
15
|
+
Yvan Barthélemy for reverse-engineering the .DS_Store file format.
|
|
16
|
+
See `Wim Lewis’ excellent documentation on CPAN`__ for more information.
|
|
17
|
+
|
|
18
|
+
__ http://search.cpan.org/~wiml/Mac-Finder-DSStore/DSStoreFormat.pod
|
|
19
|
+
|
|
20
|
+
Platform: Any
|
|
21
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
22
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
23
|
+
Classifier: Topic :: Desktop Environment
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Provides: ds_store
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import bisect
|
|
4
|
+
import struct
|
|
5
|
+
import binascii
|
|
6
|
+
import six
|
|
7
|
+
|
|
8
|
+
class BuddyError (Exception):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class Block(object):
|
|
12
|
+
def __init__(self, allocator, offset, size):
|
|
13
|
+
self._allocator = allocator
|
|
14
|
+
self._offset = offset
|
|
15
|
+
self._size = size
|
|
16
|
+
self._value = bytearray(allocator.read (offset, size))
|
|
17
|
+
self._pos = 0
|
|
18
|
+
self._dirty = False
|
|
19
|
+
|
|
20
|
+
def __len__(self):
|
|
21
|
+
return self._size
|
|
22
|
+
|
|
23
|
+
def __enter__(self):
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
27
|
+
self.close()
|
|
28
|
+
|
|
29
|
+
def close(self):
|
|
30
|
+
if self._dirty:
|
|
31
|
+
self.flush()
|
|
32
|
+
|
|
33
|
+
def flush(self):
|
|
34
|
+
if self._dirty:
|
|
35
|
+
self._dirty = False
|
|
36
|
+
self._allocator.write (self._offset, self._value)
|
|
37
|
+
|
|
38
|
+
def invalidate(self):
|
|
39
|
+
self._dirty = False
|
|
40
|
+
|
|
41
|
+
def zero_fill(self):
|
|
42
|
+
len = self._size - self._pos
|
|
43
|
+
zeroes = '\0' * len
|
|
44
|
+
self._value[self._pos:self._size] = zeroes
|
|
45
|
+
self._dirty = True
|
|
46
|
+
|
|
47
|
+
def tell(self):
|
|
48
|
+
return self._pos
|
|
49
|
+
|
|
50
|
+
def seek(self, pos, whence=os.SEEK_SET):
|
|
51
|
+
if whence == os.SEEK_CUR:
|
|
52
|
+
pos += self._pos
|
|
53
|
+
elif whence == os.SEEK_END:
|
|
54
|
+
pos = self._size - pos
|
|
55
|
+
|
|
56
|
+
if pos < 0 or pos > self._size:
|
|
57
|
+
raise ValueError('Seek out of range in Block instance')
|
|
58
|
+
|
|
59
|
+
self._pos = pos
|
|
60
|
+
|
|
61
|
+
def read(self, size_or_format):
|
|
62
|
+
if isinstance(size_or_format, basestring):
|
|
63
|
+
size = struct.calcsize (size_or_format)
|
|
64
|
+
fmt = size_or_format
|
|
65
|
+
else:
|
|
66
|
+
size = size_or_format
|
|
67
|
+
fmt = None
|
|
68
|
+
|
|
69
|
+
if self._size - self._pos < size:
|
|
70
|
+
raise BuddyError('Unable to read %lu bytes in block' % size)
|
|
71
|
+
|
|
72
|
+
data = self._value[self._pos:self._pos + size]
|
|
73
|
+
self._pos += size
|
|
74
|
+
|
|
75
|
+
if fmt is not None:
|
|
76
|
+
return struct.unpack (size_or_format, data)
|
|
77
|
+
else:
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
def write(self, data_or_format, *args):
|
|
81
|
+
if len(args):
|
|
82
|
+
data = struct.pack (data_or_format, *args)
|
|
83
|
+
else:
|
|
84
|
+
data = data_or_format
|
|
85
|
+
|
|
86
|
+
if self._pos + len(data) > self._size:
|
|
87
|
+
raise ValueError('Attempt to write past end of Block')
|
|
88
|
+
|
|
89
|
+
self._value[self._pos:self._pos + len(data)] = data
|
|
90
|
+
self._pos += len(data)
|
|
91
|
+
|
|
92
|
+
self._dirty = True
|
|
93
|
+
|
|
94
|
+
def insert(self, data_or_format, *args):
|
|
95
|
+
if len(args):
|
|
96
|
+
data = struct.pack (data_or_format, *args)
|
|
97
|
+
else:
|
|
98
|
+
data = data_or_format
|
|
99
|
+
|
|
100
|
+
del self._value[-len(data):]
|
|
101
|
+
self._value[self._pos:self._pos] = data
|
|
102
|
+
self._pos += len(data)
|
|
103
|
+
|
|
104
|
+
self._dirty = True
|
|
105
|
+
|
|
106
|
+
def delete(self, size):
|
|
107
|
+
if self._pos + size > self._size:
|
|
108
|
+
raise ValueError('Attempt to delete past end of Block')
|
|
109
|
+
del self._value[self._pos:self._pos + size]
|
|
110
|
+
self._value += b'\0' * size
|
|
111
|
+
self._dirty = True
|
|
112
|
+
|
|
113
|
+
def __str__(self):
|
|
114
|
+
return binascii.b2a_hex(self._value)
|
|
115
|
+
|
|
116
|
+
class Allocator (object):
|
|
117
|
+
def __init__(self, the_file):
|
|
118
|
+
self._file = the_file
|
|
119
|
+
self._dirty = False
|
|
120
|
+
|
|
121
|
+
self._file.seek(0)
|
|
122
|
+
|
|
123
|
+
# Read the header
|
|
124
|
+
magic1, magic2, offset, size, offset2, self._unknown1 \
|
|
125
|
+
= self.read (-4, '>I4sIII16s')
|
|
126
|
+
|
|
127
|
+
if magic2 != 'Bud1' or magic1 != 1:
|
|
128
|
+
raise BuddyError('Not a buddy file')
|
|
129
|
+
|
|
130
|
+
if offset != offset2:
|
|
131
|
+
raise BuddyError('Root addresses differ')
|
|
132
|
+
|
|
133
|
+
self._root = Block(self, offset, size)
|
|
134
|
+
|
|
135
|
+
# Read the block offsets
|
|
136
|
+
count, self._unknown2 = self._root.read('>II')
|
|
137
|
+
self._offsets = []
|
|
138
|
+
c = (count + 255) & ~255
|
|
139
|
+
while c:
|
|
140
|
+
self._offsets += self._root.read('>256I')
|
|
141
|
+
c -= 256
|
|
142
|
+
self._offsets = self._offsets[:count]
|
|
143
|
+
|
|
144
|
+
# Read the TOC
|
|
145
|
+
self._toc = {}
|
|
146
|
+
count = self._root.read('>I')[0]
|
|
147
|
+
for n in range(count):
|
|
148
|
+
nlen = self._root.read('B')[0]
|
|
149
|
+
name = str(self._root.read(nlen))
|
|
150
|
+
value = self._root.read('>I')[0]
|
|
151
|
+
self._toc[name] = value
|
|
152
|
+
|
|
153
|
+
# Read the free lists
|
|
154
|
+
self._free = []
|
|
155
|
+
for n in range(32):
|
|
156
|
+
count = self._root.read('>I')
|
|
157
|
+
self._free.append(list(self._root.read('>%uI' % count)))
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def open(cls, file_or_name, mode='r+'):
|
|
161
|
+
if isinstance(file_or_name, basestring):
|
|
162
|
+
f = open(file_or_name, mode)
|
|
163
|
+
else:
|
|
164
|
+
f = file_or_name
|
|
165
|
+
|
|
166
|
+
if mode == 'w' or mode == 'w+':
|
|
167
|
+
# Create an empty file in this case
|
|
168
|
+
f.truncate()
|
|
169
|
+
|
|
170
|
+
# An empty root block needs 1264 bytes:
|
|
171
|
+
#
|
|
172
|
+
# 0 4 offset count
|
|
173
|
+
# 4 4 unknown
|
|
174
|
+
# 8 4 root block offset (2048)
|
|
175
|
+
# 12 255 * 4 padding (offsets are in multiples of 256)
|
|
176
|
+
# 1032 4 toc count (0)
|
|
177
|
+
# 1036 228 free list
|
|
178
|
+
# total 1264
|
|
179
|
+
|
|
180
|
+
# The free list will contain the following:
|
|
181
|
+
#
|
|
182
|
+
# 0 5 * 4 no blocks of width less than 5
|
|
183
|
+
# 20 6 * 8 1 block each of widths 5 to 10
|
|
184
|
+
# 68 4 no blocks of width 11 (allocated for the root)
|
|
185
|
+
# 72 19 * 8 1 block each of widths 12 to 30
|
|
186
|
+
# 224 4 no blocks of width 31
|
|
187
|
+
# total 228
|
|
188
|
+
#
|
|
189
|
+
# (The reason for this layout is that we allocate 2**5 bytes for
|
|
190
|
+
# the header, which splits the initial 2GB region into every size
|
|
191
|
+
# below 2**31, including *two* blocks of size 2**5, one of which
|
|
192
|
+
# we take. The root block itself then needs a block of size
|
|
193
|
+
# 2**11. Conveniently, each of these initial blocks will be
|
|
194
|
+
# located at offset 2**n where n is its width.)
|
|
195
|
+
|
|
196
|
+
# Write the header
|
|
197
|
+
header = struct.pack(b'>I4sIII16s',
|
|
198
|
+
1, b'Bud1',
|
|
199
|
+
2048, 1264, 2048,
|
|
200
|
+
b'\x00\x00\x10\x0c'
|
|
201
|
+
b'\x00\x00\x00\x87'
|
|
202
|
+
b'\x00\x00\x20\x0b'
|
|
203
|
+
b'\x00\x00\x00\x00')
|
|
204
|
+
f.write (header)
|
|
205
|
+
f.write(b'\0' * 2016)
|
|
206
|
+
|
|
207
|
+
# Write the root block
|
|
208
|
+
free_list = [struct.pack(b'>5I', 0, 0, 0, 0, 0)]
|
|
209
|
+
for n in range(5, 11):
|
|
210
|
+
free_list.append (struct.pack (b'>II', 1, 2**n))
|
|
211
|
+
free_list.append (struct.pack (b'>I', 0))
|
|
212
|
+
for n in range(12, 31):
|
|
213
|
+
free_list.append (struct.pack (b'>II', 1, 2**n))
|
|
214
|
+
free_list.append (struct.pack (b'>I', 0))
|
|
215
|
+
|
|
216
|
+
root = ''.join([struct.pack(b'>III', 1, 0, 2048 | 5),
|
|
217
|
+
struct.pack(b'>I', 0) * 255,
|
|
218
|
+
struct.pack(b'>I', 0)] + free_list)
|
|
219
|
+
f.write (root)
|
|
220
|
+
|
|
221
|
+
return Allocator (f)
|
|
222
|
+
|
|
223
|
+
def __enter__(self):
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
227
|
+
self.close()
|
|
228
|
+
|
|
229
|
+
def close(self):
|
|
230
|
+
self.flush()
|
|
231
|
+
self._file.close()
|
|
232
|
+
|
|
233
|
+
def flush(self):
|
|
234
|
+
if self._dirty:
|
|
235
|
+
size = self._root_block_size()
|
|
236
|
+
self.allocate (size, 0)
|
|
237
|
+
with self.get_block(0) as rblk:
|
|
238
|
+
self._write_root_block_into (rblk)
|
|
239
|
+
|
|
240
|
+
addr = self._offsets[0]
|
|
241
|
+
offset = addr & ~0x1f
|
|
242
|
+
size = 1 << (addr & 0x1f)
|
|
243
|
+
|
|
244
|
+
self._file.seek(0, os.SEEK_SET)
|
|
245
|
+
self._file.write(struct.pack(b'>I4sIII16s',
|
|
246
|
+
1, b'Bud1',
|
|
247
|
+
offset, size, offset,
|
|
248
|
+
self._unknown1))
|
|
249
|
+
|
|
250
|
+
self._dirty = False
|
|
251
|
+
|
|
252
|
+
self._file.flush()
|
|
253
|
+
|
|
254
|
+
def read(self, offset, size_or_format):
|
|
255
|
+
"""Read data at `offset', or raise an exception. `size_or_format'
|
|
256
|
+
may either be a byte count, in which case we return raw data,
|
|
257
|
+
or a format string for `struct.unpack', in which case we
|
|
258
|
+
work out the size and unpack the data before returning it."""
|
|
259
|
+
# N.B. There is a fixed offset of four bytes(!)
|
|
260
|
+
self._file.seek (offset + 4, os.SEEK_SET)
|
|
261
|
+
|
|
262
|
+
if isinstance(size_or_format, basestring):
|
|
263
|
+
size = struct.calcsize (size_or_format)
|
|
264
|
+
fmt = size_or_format
|
|
265
|
+
else:
|
|
266
|
+
size = size_or_format
|
|
267
|
+
fmt = None
|
|
268
|
+
|
|
269
|
+
ret = self._file.read (size)
|
|
270
|
+
if len(ret) < size:
|
|
271
|
+
ret += b'\0' * (size - len(ret))
|
|
272
|
+
|
|
273
|
+
if fmt is not None:
|
|
274
|
+
ret = struct.unpack (fmt, ret)
|
|
275
|
+
|
|
276
|
+
return ret
|
|
277
|
+
|
|
278
|
+
def write(self, offset, data_or_format, *args):
|
|
279
|
+
"""Write data at `offset', or raise an exception. `data_or_format'
|
|
280
|
+
may either be the data to write, or a format string for `struct.pack',
|
|
281
|
+
in which case we pack the additional arguments and write the
|
|
282
|
+
resulting data."""
|
|
283
|
+
# N.B. There is a fixed offset of four bytes(!)
|
|
284
|
+
self._file.seek (offset + 4, os.SEEK_SET)
|
|
285
|
+
|
|
286
|
+
if len(args):
|
|
287
|
+
data = struct.pack (data_or_format, *args)
|
|
288
|
+
else:
|
|
289
|
+
data = data_or_format
|
|
290
|
+
|
|
291
|
+
self._file.write (data)
|
|
292
|
+
|
|
293
|
+
def get_block(self, block):
|
|
294
|
+
try:
|
|
295
|
+
addr = self._offsets[block]
|
|
296
|
+
except IndexError:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
offset = addr & ~0x1f
|
|
300
|
+
size = 1 << (addr & 0x1f)
|
|
301
|
+
|
|
302
|
+
return Block(self, offset, size)
|
|
303
|
+
|
|
304
|
+
def _root_block_size(self):
|
|
305
|
+
"""Return the number of bytes required by the root block."""
|
|
306
|
+
# Offsets
|
|
307
|
+
size = 8
|
|
308
|
+
size += 4 * ((len(self._offsets) + 255) & ~255)
|
|
309
|
+
|
|
310
|
+
# TOC
|
|
311
|
+
size += 4
|
|
312
|
+
size += sum([5 + len(s) for s in self._toc])
|
|
313
|
+
|
|
314
|
+
# Free list
|
|
315
|
+
size += sum([4 + 4 * len(fl) for fl in self._free])
|
|
316
|
+
|
|
317
|
+
return size
|
|
318
|
+
|
|
319
|
+
def _write_root_block_into(self, block):
|
|
320
|
+
# Offsets
|
|
321
|
+
block.write('>II', len(self._offsets), self._unknown2)
|
|
322
|
+
block.write('>%uI' % len(self._offsets), *self._offsets)
|
|
323
|
+
extra = len(self._offsets) & 255
|
|
324
|
+
if extra:
|
|
325
|
+
block.write(b'\0\0\0\0' * (256 - extra))
|
|
326
|
+
|
|
327
|
+
# TOC
|
|
328
|
+
keys = self._toc.keys()
|
|
329
|
+
keys.sort()
|
|
330
|
+
|
|
331
|
+
block.write('>I', len(keys))
|
|
332
|
+
for k in keys:
|
|
333
|
+
block.write('B', len(k))
|
|
334
|
+
block.write(k)
|
|
335
|
+
block.write('>I', self._toc[k])
|
|
336
|
+
|
|
337
|
+
# Free list
|
|
338
|
+
for w, f in enumerate(self._free):
|
|
339
|
+
block.write('>I', len(f))
|
|
340
|
+
if len(f):
|
|
341
|
+
block.write('>%uI' % len(f), *f)
|
|
342
|
+
|
|
343
|
+
def _buddy(self, offset, width):
|
|
344
|
+
f = self._free[width]
|
|
345
|
+
b = offset ^ (1 << width)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
ndx = f.index(b)
|
|
349
|
+
except ValueError:
|
|
350
|
+
ndx = None
|
|
351
|
+
|
|
352
|
+
return (f, b, ndx)
|
|
353
|
+
|
|
354
|
+
def _release(self, offset, width):
|
|
355
|
+
# Coalesce
|
|
356
|
+
while True:
|
|
357
|
+
f,b,ndx = self._buddy (offset, width)
|
|
358
|
+
|
|
359
|
+
if ndx is None:
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
offset &= b
|
|
363
|
+
width += 1
|
|
364
|
+
del f[ndx]
|
|
365
|
+
|
|
366
|
+
# Add to the list
|
|
367
|
+
bisect.insort(f, offset)
|
|
368
|
+
|
|
369
|
+
# Mark as dirty
|
|
370
|
+
self._dirty = True
|
|
371
|
+
|
|
372
|
+
def _alloc(self, width):
|
|
373
|
+
w = width
|
|
374
|
+
while not self._free[w]:
|
|
375
|
+
w += 1
|
|
376
|
+
while w > width:
|
|
377
|
+
offset = self._free[w].pop(0)
|
|
378
|
+
w -= 1
|
|
379
|
+
self._free[w] = [offset, offset ^ (1 << w)]
|
|
380
|
+
self._dirty = True
|
|
381
|
+
return self._free[width].pop(0)
|
|
382
|
+
|
|
383
|
+
def allocate(self, bytes, block=None):
|
|
384
|
+
"""Allocate or reallocate a block such that it has space for at least
|
|
385
|
+
`bytes' bytes."""
|
|
386
|
+
if block is None:
|
|
387
|
+
# Find the first unused block
|
|
388
|
+
try:
|
|
389
|
+
block = self._offsets.index(0)
|
|
390
|
+
except ValueError:
|
|
391
|
+
block = len(self._offsets)
|
|
392
|
+
self._offsets.append(0)
|
|
393
|
+
|
|
394
|
+
# Compute block width
|
|
395
|
+
width = max(bytes.bit_length(), 5)
|
|
396
|
+
|
|
397
|
+
addr = self._offsets[block]
|
|
398
|
+
offset = addr & ~0x1f
|
|
399
|
+
|
|
400
|
+
if addr:
|
|
401
|
+
blkwidth = addr & 0x1f
|
|
402
|
+
if blkwidth == width:
|
|
403
|
+
return block
|
|
404
|
+
self._release (offset, width)
|
|
405
|
+
self._offsets[block] = 0
|
|
406
|
+
|
|
407
|
+
offset = self._alloc(width)
|
|
408
|
+
self._offsets[block] = offset | width
|
|
409
|
+
return block
|
|
410
|
+
|
|
411
|
+
def release(self, block):
|
|
412
|
+
addr = self._offsets[block]
|
|
413
|
+
|
|
414
|
+
if addr:
|
|
415
|
+
width = addr & 0x1f
|
|
416
|
+
offset = addr & ~0x1f
|
|
417
|
+
self._release (offset, width)
|
|
418
|
+
|
|
419
|
+
if block == len(self._offsets):
|
|
420
|
+
del self._offsets[block]
|
|
421
|
+
else:
|
|
422
|
+
self._offsets[block] = 0
|
|
423
|
+
|
|
424
|
+
def __len__(self):
|
|
425
|
+
return len(self._toc)
|
|
426
|
+
|
|
427
|
+
def __getitem__(self, key):
|
|
428
|
+
if not isinstance(key, basestring):
|
|
429
|
+
raise TypeError('Keys must be of string type')
|
|
430
|
+
return self._toc[key]
|
|
431
|
+
|
|
432
|
+
def __setitem__(self, key, value):
|
|
433
|
+
if not isinstance(key, basestring):
|
|
434
|
+
raise TypeError('Keys must be of string type')
|
|
435
|
+
self._toc[key] = value
|
|
436
|
+
self._dirty = True
|
|
437
|
+
|
|
438
|
+
def __delitem__(self, key):
|
|
439
|
+
if not isinstance(key, basestring):
|
|
440
|
+
raise TypeError('Keys must be of string type')
|
|
441
|
+
del self._toc[key]
|
|
442
|
+
self._dirty = True
|
|
443
|
+
|
|
444
|
+
def iterkeys(self):
|
|
445
|
+
return six.iterkeys(self._toc)
|
|
446
|
+
|
|
447
|
+
def keys(self):
|
|
448
|
+
return six.iterkeys(self._toc)
|
|
449
|
+
|
|
450
|
+
def __iter__(self):
|
|
451
|
+
return six.iterkeys(self._toc)
|
|
452
|
+
|
|
453
|
+
def __contains__(self, key):
|
|
454
|
+
return key in self._toc
|
|
455
|
+
|