mac-alias 1.0.0__tar.gz → 2.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.
@@ -1,19 +1,23 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: mac_alias
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: Generate/parse Mac OS Alias records from Python
5
5
  Home-page: http://alastairs-place.net/projects/mac_alias
6
6
  Author: Alastair Houghton
7
7
  Author-email: alastair@alastairs-place.net
8
8
  License: MIT License
9
- Description:
10
- ``mac_alias`` lets you generate or read binary Alias records from Python code.
9
+ Description: ``mac_alias`` lets you generate or read binary Alias and Bookmark records
10
+ from Python code.
11
+
11
12
  While it is written in pure Python, some OS X specific code is required to
12
- generate a proper Alias record for a given file, so this module currently
13
- is not portable to other platforms.
13
+ generate a proper Alias or Bookmark record for a given file, so this module
14
+ currently is not portable to other platforms.
14
15
 
15
16
  Credit is due to Wim Lewis <wiml@hhhh.org>, whose work on reverse engineering
16
- the alias format was extremely useful in writing this code.
17
+ the alias format was extremely useful in writing this code. Alastair Houghton
18
+ <alastair@alastairs-place.net> was responsible for reverse engineering the
19
+ bookmark format.
20
+
17
21
  Platform: darwin
18
22
  Classifier: Development Status :: 5 - Production/Stable
19
23
  Classifier: License :: OSI Approved :: MIT License
@@ -0,0 +1,11 @@
1
+ ``mac_alias`` lets you generate or read binary Alias and Bookmark records
2
+ from Python code.
3
+
4
+ While it is written in pure Python, some OS X specific code is required to
5
+ generate a proper Alias or Bookmark record for a given file, so this module
6
+ currently is not portable to other platforms.
7
+
8
+ Credit is due to Wim Lewis <wiml@hhhh.org>, whose work on reverse engineering
9
+ the alias format was extremely useful in writing this code. Alastair Houghton
10
+ <alastair@alastairs-place.net> was responsible for reverse engineering the
11
+ bookmark format.
@@ -0,0 +1,27 @@
1
+ from .alias import *
2
+ from .bookmark import *
3
+
4
+ __all__ = [ 'ALIAS_KIND_FILE', 'ALIAS_KIND_FOLDER',
5
+ 'ALIAS_HFS_VOLUME_SIGNATURE',
6
+ 'ALIAS_FIXED_DISK', 'ALIAS_NETWORK_DISK', 'ALIAS_400KB_FLOPPY_DISK',
7
+ 'ALIAS_800KB_FLOPPY_DISK', 'ALIAS_1_44MB_FLOPPY_DISK',
8
+ 'ALIAS_EJECTABLE_DISK',
9
+ 'ALIAS_NO_CNID',
10
+ 'kBookmarkPath', 'kBookmarkCNIDPath', 'kBookmarkFileProperties',
11
+ 'kBookmarkFileName', 'kBookmarkFileID', 'kBookmarkFileCreationDate',
12
+ 'kBookmarkTOCPath', 'kBookmarkVolumePath',
13
+ 'kBookmarkVolumeURL', 'kBookmarkVolumeName', 'kBookmarkVolumeUUID',
14
+ 'kBookmarkVolumeSize', 'kBookmarkVolumeCreationDate',
15
+ 'kBookmarkVolumeProperties', 'kBookmarkContainingFolder',
16
+ 'kBookmarkUserName', 'kBookmarkUID', 'kBookmarkWasFileReference',
17
+ 'kBookmarkCreationOptions', 'kBookmarkURLLengths',
18
+ 'kBookmarkSecurityExtension',
19
+ 'AppleShareInfo',
20
+ 'VolumeInfo',
21
+ 'TargetInfo',
22
+ 'Alias',
23
+ 'Bookmark',
24
+ 'Data',
25
+ 'URL' ]
26
+
27
+
@@ -1,5 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from __future__ import unicode_literals
3
+ from __future__ import division
3
4
 
4
5
  import struct
5
6
  import datetime
@@ -138,28 +139,28 @@ class TargetInfo (object):
138
139
  self.type_code = type_code
139
140
 
140
141
  #: The depth of the alias? Always seems to be -1 on OS X.
141
- self.levels_from = -1
142
+ self.levels_from = levels_from
142
143
 
143
144
  #: The depth of the target? Always seems to be -1 on OS X.
144
- self.levels_to = -1
145
+ self.levels_to = levels_to
145
146
 
146
147
  #: The (POSIX) name of the target's containing folder. *(optional)*
147
- self.folder_name = None
148
+ self.folder_name = folder_name
148
149
 
149
150
  #: The path from the volume root as a sequence of CNIDs. *(optional)*
150
- self.cnid_path = None
151
+ self.cnid_path = cnid_path
151
152
 
152
153
  #: The Carbon path of the target *(optional)*
153
- self.carbon_path = None
154
+ self.carbon_path = carbon_path
154
155
 
155
156
  #: The POSIX path of the target relative to the volume root. Note
156
157
  #: that this may or may not have a leading '/' character, but it is
157
158
  #: always relative to the containing volume. *(optional)*
158
- self.posix_path = None
159
+ self.posix_path = posix_path
159
160
 
160
161
  #: If the path points into a user's home folder, the number of folders
161
162
  #: deep that we go before we get to that home folder. *(optional)*
162
- self.user_home_prefix_len = None
163
+ self.user_home_prefix_len = user_home_prefix_len
163
164
 
164
165
  def __repr__(self):
165
166
  args = ['kind', 'filename', 'folder_cnid', 'cnid', 'creation_date',
@@ -262,7 +263,7 @@ class Alias (object):
262
263
  if tag == TAG_CARBON_FOLDER_NAME:
263
264
  alias.target.folder_name = value.replace('/',':')
264
265
  elif tag == TAG_CNID_PATH:
265
- alias.target.cnid_path = struct.unpack(b'>%uI' % (length / 4),
266
+ alias.target.cnid_path = struct.unpack(b'>%uI' % (length // 4),
266
267
  value)
267
268
  elif tag == TAG_CARBON_PATH:
268
269
  alias.target.carbon_path = value
@@ -482,7 +483,7 @@ class Alias (object):
482
483
  if len(ai.server) & 1:
483
484
  b.write(b'\0')
484
485
  if ai.username:
485
- b.write(struct.pack(b'>hh', TAG_APPLESHARE_SERVER_NAME,
486
+ b.write(struct.pack(b'>hh', TAG_APPLESHARE_USERNAME,
486
487
  len(ai.username)))
487
488
  b.write(ai.username)
488
489
  if len(ai.username) & 1:
@@ -513,13 +514,13 @@ class Alias (object):
513
514
  utf16 = self.target.filename.replace(':','/').encode('utf-16-be')
514
515
  b.write(struct.pack(b'>hhh', TAG_UNICODE_FILENAME,
515
516
  len(utf16) + 2,
516
- len(utf16) / 2))
517
+ len(utf16) // 2))
517
518
  b.write(utf16)
518
519
 
519
520
  utf16 = self.volume.name.replace(':','/').encode('utf-16-be')
520
521
  b.write(struct.pack(b'>hhh', TAG_UNICODE_VOLUME_NAME,
521
522
  len(utf16) + 2,
522
- len(utf16) / 2))
523
+ len(utf16) // 2))
523
524
  b.write(utf16)
524
525
 
525
526
  if self.target.posix_path:
@@ -0,0 +1,647 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file implements the Apple "bookmark" format, which is the replacement
4
+ # for the old-fashioned alias format. The details of this format were
5
+ # reverse engineered; some things are still not entirely clear.
6
+ #
7
+ from __future__ import unicode_literals
8
+
9
+ import struct
10
+ import uuid
11
+ import datetime
12
+ import os
13
+ import sys
14
+ import pprint
15
+
16
+ from urlparse import urljoin
17
+
18
+ if sys.platform == 'darwin':
19
+ from . import osx
20
+
21
+ from .utils import *
22
+
23
+ BMK_DATA_TYPE_MASK = 0xffffff00
24
+ BMK_DATA_SUBTYPE_MASK = 0x000000ff
25
+
26
+ BMK_STRING = 0x0100
27
+ BMK_DATA = 0x0200
28
+ BMK_NUMBER = 0x0300
29
+ BMK_DATE = 0x0400
30
+ BMK_BOOLEAN = 0x0500
31
+ BMK_ARRAY = 0x0600
32
+ BMK_DICT = 0x0700
33
+ BMK_UUID = 0x0800
34
+ BMK_URL = 0x0900
35
+ BMK_NULL = 0x0a00
36
+
37
+ BMK_ST_ZERO = 0x0000
38
+ BMK_ST_ONE = 0x0001
39
+
40
+ BMK_BOOLEAN_ST_FALSE = 0x0000
41
+ BMK_BOOLEAN_ST_TRUE = 0x0001
42
+
43
+ # Subtypes for BMK_NUMBER are really CFNumberType values
44
+ kCFNumberSInt8Type = 1
45
+ kCFNumberSInt16Type = 2
46
+ kCFNumberSInt32Type = 3
47
+ kCFNumberSInt64Type = 4
48
+ kCFNumberFloat32Type = 5
49
+ kCFNumberFloat64Type = 6
50
+ kCFNumberCharType = 7
51
+ kCFNumberShortType = 8
52
+ kCFNumberIntType = 9
53
+ kCFNumberLongType = 10
54
+ kCFNumberLongLongType = 11
55
+ kCFNumberFloatType = 12
56
+ kCFNumberDoubleType = 13
57
+ kCFNumberCFIndexType = 14
58
+ kCFNumberNSIntegerType = 15
59
+ kCFNumberCGFloatType = 16
60
+
61
+ # Resource property flags (from CFURLPriv.h)
62
+ kCFURLResourceIsRegularFile = 0x00000001
63
+ kCFURLResourceIsDirectory = 0x00000002
64
+ kCFURLResourceIsSymbolicLink = 0x00000004
65
+ kCFURLResourceIsVolume = 0x00000008
66
+ kCFURLResourceIsPackage = 0x00000010
67
+ kCFURLResourceIsSystemImmutable = 0x00000020
68
+ kCFURLResourceIsUserImmutable = 0x00000040
69
+ kCFURLResourceIsHidden = 0x00000080
70
+ kCFURLResourceHasHiddenExtension = 0x00000100
71
+ kCFURLResourceIsApplication = 0x00000200
72
+ kCFURLResourceIsCompressed = 0x00000400
73
+ kCFURLResourceIsSystemCompressed = 0x00000400
74
+ kCFURLCanSetHiddenExtension = 0x00000800
75
+ kCFURLResourceIsReadable = 0x00001000
76
+ kCFURLResourceIsWriteable = 0x00002000
77
+ kCFURLResourceIsExecutable = 0x00004000
78
+ kCFURLIsAliasFile = 0x00008000
79
+ kCFURLIsMountTrigger = 0x00010000
80
+
81
+ # Volume property flags (from CFURLPriv.h)
82
+ kCFURLVolumeIsLocal = 0x1 #
83
+ kCFURLVolumeIsAutomount = 0x2 #
84
+ kCFURLVolumeDontBrowse = 0x4 #
85
+ kCFURLVolumeIsReadOnly = 0x8 #
86
+ kCFURLVolumeIsQuarantined = 0x10
87
+ kCFURLVolumeIsEjectable = 0x20 #
88
+ kCFURLVolumeIsRemovable = 0x40 #
89
+ kCFURLVolumeIsInternal = 0x80 #
90
+ kCFURLVolumeIsExternal = 0x100 #
91
+ kCFURLVolumeIsDiskImage = 0x200 #
92
+ kCFURLVolumeIsFileVault = 0x400
93
+ kCFURLVolumeIsLocaliDiskMirror = 0x800
94
+ kCFURLVolumeIsiPod = 0x1000 #
95
+ kCFURLVolumeIsiDisk = 0x2000
96
+ kCFURLVolumeIsCD = 0x4000
97
+ kCFURLVolumeIsDVD = 0x8000
98
+ kCFURLVolumeIsDeviceFileSystem = 0x10000
99
+ kCFURLVolumeSupportsPersistentIDs = 0x100000000
100
+ kCFURLVolumeSupportsSearchFS = 0x200000000
101
+ kCFURLVolumeSupportsExchange = 0x400000000
102
+ # reserved 0x800000000
103
+ kCFURLVolumeSupportsSymbolicLinks = 0x1000000000
104
+ kCFURLVolumeSupportsDenyModes = 0x2000000000
105
+ kCFURLVolumeSupportsCopyFile = 0x4000000000
106
+ kCFURLVolumeSupportsReadDirAttr = 0x8000000000
107
+ kCFURLVolumeSupportsJournaling = 0x10000000000
108
+ kCFURLVolumeSupportsRename = 0x20000000000
109
+ kCFURLVolumeSupportsFastStatFS = 0x40000000000
110
+ kCFURLVolumeSupportsCaseSensitiveNames = 0x80000000000
111
+ kCFURLVolumeSupportsCasePreservedNames = 0x100000000000
112
+ kCFURLVolumeSupportsFLock = 0x200000000000
113
+ kCFURLVolumeHasNoRootDirectoryTimes = 0x400000000000
114
+ kCFURLVolumeSupportsExtendedSecurity = 0x800000000000
115
+ kCFURLVolumeSupports2TBFileSize = 0x1000000000000
116
+ kCFURLVolumeSupportsHardLinks = 0x2000000000000
117
+ kCFURLVolumeSupportsMandatoryByteRangeLocks = 0x4000000000000
118
+ kCFURLVolumeSupportsPathFromID = 0x8000000000000
119
+ # reserved 0x10000000000000
120
+ kCFURLVolumeIsJournaling = 0x20000000000000
121
+ kCFURLVolumeSupportsSparseFiles = 0x40000000000000
122
+ kCFURLVolumeSupportsZeroRuns = 0x80000000000000
123
+ kCFURLVolumeSupportsVolumeSizes = 0x100000000000000
124
+ kCFURLVolumeSupportsRemoteEvents = 0x200000000000000
125
+ kCFURLVolumeSupportsHiddenFiles = 0x400000000000000
126
+ kCFURLVolumeSupportsDecmpFSCompression = 0x800000000000000
127
+ kCFURLVolumeHas64BitObjectIDs = 0x1000000000000000
128
+ kCFURLVolumePropertyFlagsAll = 0xffffffffffffffff
129
+
130
+ BMK_URL_ST_ABSOLUTE = 0x0001
131
+ BMK_URL_ST_RELATIVE = 0x0002
132
+
133
+ # Bookmark keys
134
+ # = 0x1003
135
+ kBookmarkPath = 0x1004 # Array of path components
136
+ kBookmarkCNIDPath = 0x1005 # Array of CNIDs
137
+ kBookmarkFileProperties = 0x1010 # (CFURL rp flags,
138
+ # CFURL rp flags asked for,
139
+ # 8 bytes NULL)
140
+ kBookmarkFileName = 0x1020
141
+ kBookmarkFileID = 0x1030
142
+ kBookmarkFileCreationDate = 0x1040
143
+ # = 0x1054 # ?
144
+ # = 0x1055 # ?
145
+ # = 0x1056 # ?
146
+ # = 0x1101 # ?
147
+ # = 0x1102 # ?
148
+ kBookmarkTOCPath = 0x2000 # A list of (TOC id, ?) pairs
149
+ kBookmarkVolumePath = 0x2002
150
+ kBookmarkVolumeURL = 0x2005
151
+ kBookmarkVolumeName = 0x2010
152
+ kBookmarkVolumeUUID = 0x2011 # Stored (perversely) as a string
153
+ kBookmarkVolumeSize = 0x2012
154
+ kBookmarkVolumeCreationDate = 0x2013
155
+ kBookmarkVolumeProperties = 0x2020 # (CFURL vp flags,
156
+ # CFURL vp flags asked for,
157
+ # 8 bytes NULL)
158
+ kBookmarkVolumeIsRoot = 0x2030 # True if volume is FS root
159
+ kBookmarkVolumeBookmark = 0x2040 # Embedded bookmark for disk image (TOC id)
160
+ kBookmarkVolumeMountPoint = 0x2050 # A URL
161
+ # = 0x2070
162
+ kBookmarkContainingFolder = 0xc001 # Index of containing folder in path
163
+ kBookmarkUserName = 0xc011 # User that created bookmark
164
+ kBookmarkUID = 0xc012 # UID that created bookmark
165
+ kBookmarkWasFileReference = 0xd001 # True if the URL was a file reference
166
+ kBookmarkCreationOptions = 0xd010
167
+ kBookmarkURLLengths = 0xe003 # See below
168
+ # = 0xf017 # Localized name?
169
+ # = 0xf022
170
+ kBookmarkSecurityExtension = 0xf080
171
+ # = 0xf081
172
+
173
+ # kBookmarkURLLengths is an array that is set if the URL encoded by the
174
+ # bookmark had a base URL; in that case, each entry is the length of the
175
+ # base URL in question. Thus a URL
176
+ #
177
+ # file:///foo/bar/baz blam/blat.html
178
+ #
179
+ # will result in [3, 2], while the URL
180
+ #
181
+ # file:///foo bar/baz blam blat.html
182
+ #
183
+ # would result in [1, 2, 1, 1]
184
+
185
+
186
+ class Data (object):
187
+ def __init__(self, bytedata=None):
188
+ #: The bytes, stored as a byte string
189
+ self.bytes = bytes(bytedata)
190
+
191
+ def __repr__(self):
192
+ return 'Data(%r)' % self.bytes
193
+
194
+ class URL (object):
195
+ def __init__(self, base, rel=None):
196
+ if rel is not None:
197
+ #: The base URL, if any (a :class:`URL`)
198
+ self.base = base
199
+ #: The rest of the URL (a string)
200
+ self.relative = rel
201
+ else:
202
+ self.base = None
203
+ self.relative = base
204
+
205
+ @property
206
+ def absolute(self):
207
+ """Return an absolute URL."""
208
+ if self.base is None:
209
+ return self.relative
210
+ else:
211
+ base_abs = self.base.absolute
212
+ return urljoin(self.base.absolute, self.relative)
213
+
214
+ def __repr__(self):
215
+ return 'URL(%r)' % self.absolute
216
+
217
+ class Bookmark (object):
218
+ def __init__(self, tocs=None):
219
+ if tocs is None:
220
+ #: The TOCs for this Bookmark
221
+ self.tocs = []
222
+ else:
223
+ self.tocs = tocs
224
+
225
+ @classmethod
226
+ def _get_item(cls, data, hdrsize, offset):
227
+ offset += hdrsize
228
+ if offset > len(data) - 8:
229
+ raise ValueError('Offset out of range')
230
+
231
+ length,typecode = struct.unpack(b'<II', data[offset:offset+8])
232
+
233
+ if len(data) - offset < 8 + length:
234
+ raise ValueError('Data item truncated')
235
+
236
+ databytes = data[offset+8:offset+8+length]
237
+
238
+ dsubtype = typecode & BMK_DATA_SUBTYPE_MASK
239
+ dtype = typecode & BMK_DATA_TYPE_MASK
240
+
241
+ if dtype == BMK_STRING:
242
+ return databytes.decode('utf-8')
243
+ elif dtype == BMK_DATA:
244
+ return Data(databytes)
245
+ elif dtype == BMK_NUMBER:
246
+ if dsubtype == kCFNumberSInt8Type:
247
+ return ord(databytes[0])
248
+ elif dsubtype == kCFNumberSInt16Type:
249
+ return struct.unpack(b'<h', databytes)[0]
250
+ elif dsubtype == kCFNumberSInt32Type:
251
+ return struct.unpack(b'<i', databytes)[0]
252
+ elif dsubtype == kCFNumberSInt64Type:
253
+ return struct.unpack(b'<q', databytes)[0]
254
+ elif dsubtype == kCFNumberFloat32Type:
255
+ return struct.unpack(b'<f', databytes)[0]
256
+ elif dsubtype == kCFNumberFloat64Type:
257
+ return struct.unpack(b'<d', databytes)[0]
258
+ elif dtype == BMK_DATE:
259
+ # Yes, dates really are stored as *BIG-endian* doubles; everything
260
+ # else is little-endian
261
+ secs = datetime.timedelta(seconds=struct.unpack(b'>d', databytes)[0])
262
+ return osx_epoch + secs
263
+ elif dtype == BMK_BOOLEAN:
264
+ if dsubtype == BMK_BOOLEAN_ST_TRUE:
265
+ return True
266
+ elif dsubtype == BMK_BOOLEAN_ST_FALSE:
267
+ return False
268
+ elif dtype == BMK_UUID:
269
+ return uuid.UUID(bytes=databytes)
270
+ elif dtype == BMK_URL:
271
+ if dsubtype == BMK_URL_ST_ABSOLUTE:
272
+ return URL(databytes.decode('utf-8'))
273
+ elif dsubtype == BMK_URL_ST_RELATIVE:
274
+ baseoff,reloff = struct.unpack(b'<II', databytes)
275
+ base = cls._get_item(data, hdrsize, baseoff)
276
+ rel = cls._get_item(data, hdrsize, reloff)
277
+ return URL(base, rel)
278
+ elif dtype == BMK_ARRAY:
279
+ result = []
280
+ for aoff in xrange(offset+8,offset+8+length,4):
281
+ eltoff, = struct.unpack(b'<I', data[aoff:aoff+4])
282
+ result.append(cls._get_item(data, hdrsize, eltoff))
283
+ return result
284
+ elif dtype == BMK_DICT:
285
+ result = {}
286
+ for eoff in xrange(offset+8,offset+8+length,8):
287
+ keyoff,valoff = struct.unpack(b'<II', data[eoff:eoff+8])
288
+ key = cls._get_item(data, hdrsize, keyoff)
289
+ val = cls._get_item(data, hdrsize, valoff)
290
+ result[key] = val
291
+ return result
292
+ elif dtype == BMK_NULL:
293
+ return None
294
+
295
+ print 'Unknown data type %08x' % typecode
296
+ return (typecode, databytes)
297
+
298
+ @classmethod
299
+ def from_bytes(cls, data):
300
+ """Create a :class:`Bookmark` given byte data."""
301
+
302
+ if len(data) < 16:
303
+ raise ValueError('Not a bookmark file (too short)')
304
+
305
+ magic,size,dummy,hdrsize = struct.unpack(b'<4sIII', data[0:16])
306
+
307
+ if magic != 'book':
308
+ raise ValueError('Not a bookmark file (bad magic)')
309
+
310
+ if hdrsize < 16:
311
+ raise ValueError('Not a bookmark file (header size too short)')
312
+
313
+ if hdrsize > size:
314
+ raise ValueError('Not a bookmark file (header size too large)')
315
+
316
+ if size != len(data):
317
+ raise ValueError('Not a bookmark file (truncated)')
318
+
319
+ tocoffset, = struct.unpack(b'<I', data[hdrsize:hdrsize+4])
320
+
321
+ tocs = []
322
+
323
+ while tocoffset != 0:
324
+ tocbase = hdrsize + tocoffset
325
+ if tocoffset > size - hdrsize \
326
+ or size - tocbase < 20:
327
+ raise ValueError('TOC offset out of range')
328
+
329
+ tocsize,tocmagic,tocid,nexttoc,toccount \
330
+ = struct.unpack(b'<IIIII',
331
+ data[tocbase:tocbase+20])
332
+
333
+ if tocmagic != 0xfffffffe:
334
+ break
335
+
336
+ tocsize += 8
337
+
338
+ if size - tocbase < tocsize:
339
+ raise ValueError('TOC truncated')
340
+
341
+ if tocsize < 12 * toccount:
342
+ raise ValueError('TOC entries overrun TOC size')
343
+
344
+ toc = {}
345
+ for n in xrange(0,toccount):
346
+ ebase = tocbase + 20 + 12 * n
347
+ eid,eoffset,edummy = struct.unpack(b'<III',
348
+ data[ebase:ebase+12])
349
+
350
+ if eid & 0x80000000:
351
+ eid = cls._get_item(data, hdrsize, eid & 0x7fffffff)
352
+
353
+ toc[eid] = cls._get_item(data, hdrsize, eoffset)
354
+
355
+ tocs.append((tocid, toc))
356
+
357
+ tocoffset = nexttoc
358
+
359
+ return cls(tocs)
360
+
361
+ def __getitem__(self, key):
362
+ for tid,toc in self.tocs:
363
+ if key in toc:
364
+ return toc[key]
365
+ raise KeyError('Key not found')
366
+
367
+ def __setitem__(self, key, value):
368
+ if len(self.tocs) == 0:
369
+ self.tocs = [(1, {})]
370
+ self.tocs[0][1][key] = value
371
+
372
+ def get(self, key, default=None):
373
+ """Lookup the value for a given key, returning a default if not
374
+ present."""
375
+ for tid,toc in self.tocs:
376
+ if key in toc:
377
+ return toc[key]
378
+ return default
379
+
380
+ @classmethod
381
+ def _encode_item(cls, item, offset):
382
+ if item is True:
383
+ result = struct.pack(b'<II', 0, BMK_BOOLEAN | BMK_BOOLEAN_ST_TRUE)
384
+ elif item is False:
385
+ result = struct.pack(b'<II', 0, BMK_BOOLEAN | BMK_BOOLEAN_ST_FALSE)
386
+ elif isinstance(item, unicode):
387
+ encoded = item.encode('utf-8')
388
+ result = (struct.pack(b'<II', len(encoded), BMK_STRING | BMK_ST_ONE)
389
+ + encoded)
390
+ elif isinstance(item, bytes):
391
+ result = (struct.pack(b'<II', len(item), BMK_STRING | BMK_ST_ONE)
392
+ + item)
393
+ elif isinstance(item, Data):
394
+ result = (struct.pack(b'<II', len(item.bytes),
395
+ BMK_DATA | BMK_ST_ONE)
396
+ + bytes(item.bytes))
397
+ elif isinstance(item, bytearray):
398
+ result = (struct.pack(b'<II', len(item),
399
+ BMK_DATA | BMK_ST_ONE)
400
+ + bytes(item))
401
+ elif isinstance(item, int) or isinstance(item, long):
402
+ if item > -0x80000000 and item < 0x7fffffff:
403
+ result = struct.pack(b'<IIi', 4,
404
+ BMK_NUMBER | kCFNumberSInt32Type, item)
405
+ else:
406
+ result = struct.pack(b'<IIq', 8,
407
+ BMK_NUMBER | kCFNumberSInt64Type, item)
408
+ elif isinstance(item, float):
409
+ result = struct.pack(b'<IId', 8,
410
+ BMK_NUMBER | kCFNumberFloat64Type, item)
411
+ elif isinstance(item, datetime.datetime):
412
+ secs = item - osx_epoch
413
+ result = struct.pack(b'<II', 8, BMK_DATE | BMK_ST_ZERO) \
414
+ + struct.pack(b'>d', float(secs.total_seconds()))
415
+ elif isinstance(item, uuid.UUID):
416
+ result = struct.pack(b'<II', 16, BMK_UUID | BMK_ST_ONE) \
417
+ + item.bytes
418
+ elif isinstance(item, URL):
419
+ if item.base:
420
+ baseoff = offset + 16
421
+ reloff, baseenc = cls._encode_item(item.base, baseoff)
422
+ xoffset, relenc = cls._encode_item(item.relative, reloff)
423
+ result = b''.join([
424
+ struct.pack(b'<IIII', 8, BMK_URL | BMK_URL_ST_RELATIVE,
425
+ baseoff, reloff),
426
+ baseenc,
427
+ relenc])
428
+ else:
429
+ encoded = item.relative.encode('utf-8')
430
+ result = struct.pack(b'<II', len(encoded),
431
+ BMK_URL | BMK_URL_ST_ABSOLUTE) + encoded
432
+ elif isinstance(item, list):
433
+ ioffset = offset + 8 + len(item) * 4
434
+ result = [struct.pack(b'<II', len(item) * 4, BMK_ARRAY | BMK_ST_ONE)]
435
+ enc = []
436
+ for elt in item:
437
+ result.append(struct.pack(b'<I', ioffset))
438
+ ioffset, ienc = cls._encode_item(elt, ioffset)
439
+ enc.append(ienc)
440
+ result = b''.join(result + enc)
441
+ elif isinstance(item, dict):
442
+ ioffset = offset + 8 + len(item) * 8
443
+ result = [struct.pack(b'<II', len(item) * 8, BMK_DICT | BMK_ST_ONE)]
444
+ enc = []
445
+ for k,v in item.iteritems():
446
+ result.append(struct.pack(b'<I', ioffset))
447
+ ioffset, ienc = cls._encode_item(k, ioffset)
448
+ enc.append(ienc)
449
+ result.append(struct.pack(b'<I', ioffset))
450
+ ioffset, ienc = cls._encode_item(v, ioffset)
451
+ enc.append(ienc)
452
+ result = b''.join(result + enc)
453
+ elif item is None:
454
+ result = struct.pack(b'<II', 0, BMK_NULL | BMK_ST_ONE)
455
+ else:
456
+ raise ValueError('Unknown item type when encoding: %s' % item)
457
+
458
+ offset += len(result)
459
+
460
+ # Pad to a multiple of 4 bytes
461
+ if offset & 3:
462
+ extra = 4 - (offset & 3)
463
+ result += b'\0' * extra
464
+ offset += extra
465
+
466
+ return (offset, result)
467
+
468
+ def to_bytes(self):
469
+ """Convert this :class:`Bookmark` to a byte representation."""
470
+
471
+ result = []
472
+ tocs = []
473
+ offset = 4 # For the offset to the first TOC
474
+
475
+ # Generate the data and build the TOCs
476
+ for tid,toc in self.tocs:
477
+ entries = []
478
+
479
+ for k,v in toc.iteritems():
480
+ if isinstance(k, basestring):
481
+ noffset = offset
482
+ voffset, enc = self._encode_item(k, offset)
483
+ result.append(enc)
484
+ offset, enc = self._encode_item(v, voffset)
485
+ result.append(enc)
486
+ entries.append((noffset | 0x80000000, voffset))
487
+ else:
488
+ entries.append((k, offset))
489
+ offset, enc = self._encode_item(v, offset)
490
+ result.append(enc)
491
+
492
+ # TOC entries must be sorted - CoreServicesInternal does a
493
+ # binary search to find data
494
+ entries.sort()
495
+
496
+ tocs.append((tid, b''.join([struct.pack('<III',k,o,0)
497
+ for k,o in entries])))
498
+
499
+ first_toc_offset = offset
500
+
501
+ # Now generate the TOC headers
502
+ for ndx,toc in enumerate(tocs):
503
+ tid, data = toc
504
+ if ndx == len(tocs) - 1:
505
+ next_offset = 0
506
+ else:
507
+ next_offset = offset + 20 + len(data)
508
+
509
+ result.append(struct.pack(b'<IIIII', len(data) - 8,
510
+ 0xfffffffe,
511
+ tid,
512
+ next_offset,
513
+ len(data) / 12))
514
+ result.append(data)
515
+
516
+ offset += 20 + len(data)
517
+
518
+ # Finally, add the header (and the first TOC offset, which isn't part
519
+ # of the header, but goes just after it)
520
+ header = struct.pack(b'<4sIIIQQQQI', b'book',
521
+ offset + 48,
522
+ 0x10040000,
523
+ 48,
524
+ 0, 0, 0, 0, first_toc_offset)
525
+
526
+ result.insert(0, header)
527
+
528
+ return b''.join(result)
529
+
530
+ @classmethod
531
+ def for_file(cls, path):
532
+ """Construct a :class:`Bookmark` for a given file."""
533
+
534
+ # Find the filesystem
535
+ st = osx.statfs(path)
536
+ vol_path = st.f_mntonname
537
+
538
+ # Grab its attributes
539
+ attrs = [osx.ATTR_CMN_CRTIME,
540
+ osx.ATTR_VOL_SIZE
541
+ | osx.ATTR_VOL_NAME
542
+ | osx.ATTR_VOL_UUID,
543
+ 0, 0, 0]
544
+ volinfo = osx.getattrlist(vol_path, attrs, 0)
545
+
546
+ vol_crtime = volinfo[0]
547
+ vol_size = volinfo[1]
548
+ vol_name = volinfo[2]
549
+ vol_uuid = volinfo[3]
550
+
551
+ # Also grab various attributes of the file
552
+ attrs = [(osx.ATTR_CMN_OBJTYPE
553
+ | osx.ATTR_CMN_CRTIME
554
+ | osx.ATTR_CMN_FILEID), 0, 0, 0, 0]
555
+ info = osx.getattrlist(path, attrs, osx.FSOPT_NOFOLLOW)
556
+
557
+ cnid = info[2]
558
+ crtime = info[1]
559
+
560
+ if info[0] == osx.VREG:
561
+ flags = kCFURLResourceIsRegularFile
562
+ elif info[0] == osx.VDIR:
563
+ flags = kCFURLResourceIsDirectory
564
+ elif info[0] == osx.VLNK:
565
+ flags = kCFURLResourceIsSymbolicLink
566
+ else:
567
+ flags = kCFURLResourceIsRegularFile
568
+
569
+ dirname, filename = os.path.split(path)
570
+
571
+ relcount = 0
572
+ if not os.path.isabs(dirname):
573
+ curdir = os.getcwd()
574
+ head, tail = os.path.split(curdir)
575
+ relcount = 0
576
+ while head and tail:
577
+ relcount += 1
578
+ head, tail = os.path.split(head)
579
+ dirname = os.path.join(curdir, dirname)
580
+
581
+ foldername = os.path.basename(dirname)
582
+
583
+ rel_path = os.path.relpath(path, vol_path)
584
+
585
+ # Build the path arrays
586
+ name_path = []
587
+ cnid_path = []
588
+ head, tail = os.path.split(rel_path)
589
+ if not tail:
590
+ head, tail = os.path.split(head)
591
+ while head or tail:
592
+ if head:
593
+ attrs = [osx.ATTR_CMN_FILEID, 0, 0, 0, 0]
594
+ info = osx.getattrlist(os.path.join(vol_path, head), attrs, 0)
595
+ cnid_path.insert(0, info[0])
596
+ head, tail = os.path.split(head)
597
+ name_path.insert(0, tail)
598
+ else:
599
+ head, tail = os.path.split(head)
600
+ name_path.append(filename)
601
+ cnid_path.append(cnid)
602
+
603
+ url_lengths = [relcount, len(name_path) - relcount]
604
+
605
+ fileprops = Data(struct.pack(b'<QQQ', flags, 0x0f, 0))
606
+ volprops = Data(struct.pack(b'<QQQ', 0x81 | kCFURLVolumeSupportsPersistentIDs,
607
+ 0x13ef | kCFURLVolumeSupportsPersistentIDs, 0))
608
+
609
+ toc = {
610
+ kBookmarkPath: name_path,
611
+ kBookmarkCNIDPath: cnid_path,
612
+ kBookmarkFileCreationDate: crtime,
613
+ kBookmarkFileProperties: fileprops,
614
+ kBookmarkContainingFolder: len(name_path) - 2,
615
+ kBookmarkVolumePath: vol_path,
616
+ kBookmarkVolumeIsRoot: vol_path == '/',
617
+ kBookmarkVolumeURL: URL('file://' + vol_path),
618
+ kBookmarkVolumeName: vol_name,
619
+ kBookmarkVolumeSize: vol_size,
620
+ kBookmarkVolumeCreationDate: vol_crtime,
621
+ kBookmarkVolumeUUID: str(vol_uuid).upper(),
622
+ kBookmarkVolumeProperties: volprops,
623
+ kBookmarkCreationOptions: 512,
624
+ kBookmarkWasFileReference: True,
625
+ kBookmarkUserName: 'alastair',
626
+ kBookmarkUID: 501,
627
+ }
628
+
629
+ if relcount:
630
+ toc[kBookmarkURLLengths] = url_lengths
631
+
632
+ return Bookmark([(1, toc)])
633
+
634
+ def __repr__(self):
635
+ result = ['Bookmark([']
636
+ for tid,toc in self.tocs:
637
+ result.append('(0x%x, {\n' % tid)
638
+ for k,v in toc.iteritems():
639
+ if isinstance(k, basestring):
640
+ kf = repr(k)
641
+ else:
642
+ kf = '0x%04x' % k
643
+ result.append(' %s: %r\n' % (kf, v))
644
+ result.append('}),\n')
645
+ result.append('])')
646
+
647
+ return ''.join(result)
@@ -9,7 +9,7 @@ import uuid
9
9
 
10
10
  from .utils import *
11
11
 
12
- libc = cdll.LoadLibrary('libc.dylib')
12
+ libc = cdll.LoadLibrary('/usr/lib/libc.dylib')
13
13
 
14
14
  # Constants
15
15
  FSOPT_NOFOLLOW = 0x00000001
@@ -15,3 +15,4 @@ class UTC (datetime.tzinfo):
15
15
  utc = UTC()
16
16
  mac_epoch = datetime.datetime(1904,1,1,0,0,0,0,utc)
17
17
  unix_epoch = datetime.datetime(1970,1,1,0,0,0,0,utc)
18
+ osx_epoch = datetime.datetime(2001,1,1,0,0,0,0,utc)
@@ -1,19 +1,23 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: mac-alias
3
- Version: 1.0.0
3
+ Version: 2.0.0
4
4
  Summary: Generate/parse Mac OS Alias records from Python
5
5
  Home-page: http://alastairs-place.net/projects/mac_alias
6
6
  Author: Alastair Houghton
7
7
  Author-email: alastair@alastairs-place.net
8
8
  License: MIT License
9
- Description:
10
- ``mac_alias`` lets you generate or read binary Alias records from Python code.
9
+ Description: ``mac_alias`` lets you generate or read binary Alias and Bookmark records
10
+ from Python code.
11
+
11
12
  While it is written in pure Python, some OS X specific code is required to
12
- generate a proper Alias record for a given file, so this module currently
13
- is not portable to other platforms.
13
+ generate a proper Alias or Bookmark record for a given file, so this module
14
+ currently is not portable to other platforms.
14
15
 
15
16
  Credit is due to Wim Lewis <wiml@hhhh.org>, whose work on reverse engineering
16
- the alias format was extremely useful in writing this code.
17
+ the alias format was extremely useful in writing this code. Alastair Houghton
18
+ <alastair@alastairs-place.net> was responsible for reverse engineering the
19
+ bookmark format.
20
+
17
21
  Platform: darwin
18
22
  Classifier: Development Status :: 5 - Production/Stable
19
23
  Classifier: License :: OSI Approved :: MIT License
@@ -1,6 +1,8 @@
1
+ README.rst
1
2
  setup.py
2
3
  mac_alias/__init__.py
3
4
  mac_alias/alias.py
5
+ mac_alias/bookmark.py
4
6
  mac_alias/osx.py
5
7
  mac_alias/utils.py
6
8
  mac_alias.egg-info/PKG-INFO
@@ -1,17 +1,13 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from setuptools import setup
3
3
 
4
+ with open('README.rst', 'r') as f:
5
+ longdesc = f.read().decode('utf-8')
6
+
4
7
  setup(name='mac_alias',
5
- version='1.0.0',
8
+ version='2.0.0',
6
9
  description='Generate/parse Mac OS Alias records from Python',
7
- long_description=u"""
8
- ``mac_alias`` lets you generate or read binary Alias records from Python code.
9
- While it is written in pure Python, some OS X specific code is required to
10
- generate a proper Alias record for a given file, so this module currently
11
- is not portable to other platforms.
12
-
13
- Credit is due to Wim Lewis <wiml@hhhh.org>, whose work on reverse engineering
14
- the alias format was extremely useful in writing this code.""",
10
+ long_description=longdesc,
15
11
  author='Alastair Houghton',
16
12
  author_email='alastair@alastairs-place.net',
17
13
  url='http://alastairs-place.net/projects/mac_alias',
@@ -24,4 +20,3 @@ the alias format was extremely useful in writing this code.""",
24
20
  'Topic :: Desktop Environment',
25
21
  'Topic :: Software Development :: Libraries :: Python Modules'],
26
22
  provides=['mac_alias'])
27
-
@@ -1,14 +0,0 @@
1
- from .alias import *
2
-
3
- __all__ = [ 'ALIAS_KIND_FILE', 'ALIAS_KIND_FOLDER',
4
- 'ALIAS_HFS_VOLUME_SIGNATURE',
5
- 'ALIAS_FIXED_DISK', 'ALIAS_NETWORK_DISK', 'ALIAS_400KB_FLOPPY_DISK',
6
- 'ALIAS_800KB_FLOPPY_DISK', 'ALIAS_1_44MB_FLOPPY_DISK',
7
- 'ALIAS_EJECTABLE_DISK',
8
- 'ALIAS_NO_CNID',
9
- 'AppleShareInfo',
10
- 'VolumeInfo',
11
- 'TargetInfo',
12
- 'Alias' ]
13
-
14
-
File without changes