mac-alias 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.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 1.1
2
+ Name: mac_alias
3
+ Version: 1.0.0
4
+ Summary: Generate/parse Mac OS Alias records from Python
5
+ Home-page: http://alastairs-place.net/projects/mac_alias
6
+ Author: Alastair Houghton
7
+ Author-email: alastair@alastairs-place.net
8
+ License: MIT License
9
+ Description:
10
+ ``mac_alias`` lets you generate or read binary Alias records from Python code.
11
+ 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.
14
+
15
+ 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
+ Platform: darwin
18
+ Classifier: Development Status :: 5 - Production/Stable
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Topic :: Desktop Environment
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Provides: mac_alias
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,586 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import unicode_literals
3
+
4
+ import struct
5
+ import datetime
6
+ import io
7
+ import re
8
+ import os
9
+ import os.path
10
+ import stat
11
+ import sys
12
+
13
+ if sys.platform == 'darwin':
14
+ from . import osx
15
+
16
+ from .utils import *
17
+
18
+ ALIAS_KIND_FILE = 0
19
+ ALIAS_KIND_FOLDER = 1
20
+
21
+ ALIAS_HFS_VOLUME_SIGNATURE = b'H+'
22
+
23
+ ALIAS_FIXED_DISK = 0
24
+ ALIAS_NETWORK_DISK = 1
25
+ ALIAS_400KB_FLOPPY_DISK = 2
26
+ ALIAS_800KB_FLOPPY_DISK = 3
27
+ ALIAS_1_44MB_FLOPPY_DISK = 4
28
+ ALIAS_EJECTABLE_DISK = 5
29
+
30
+ ALIAS_NO_CNID = 0xffffffff
31
+
32
+ class AppleShareInfo (object):
33
+ def __init__(self, zone=None, server=None, user=None):
34
+ #: The AppleShare zone
35
+ self.zone = zone
36
+ #: The AFP server
37
+ self.server = server
38
+ #: The username
39
+ self.user = user
40
+
41
+ def __repr__(self):
42
+ return 'AppleShareInfo(%r,%r,%r)' % (self.zone, self.server, self.user)
43
+
44
+ class VolumeInfo (object):
45
+ def __init__(self, name, creation_date, fs_type, disk_type,
46
+ attribute_flags, fs_id, appleshare_info=None,
47
+ driver_name=None, posix_path=None, disk_image_alias=None,
48
+ dialup_info=None, network_mount_info=None):
49
+ #: The name of the volume on which the target resides
50
+ self.name = name
51
+
52
+ #: The creation date of the target's volume
53
+ self.creation_date = creation_date
54
+
55
+ #: The filesystem type (a two character code, e.g. ``b'H+'`` for HFS+)
56
+ self.fs_type = fs_type
57
+
58
+ #: The type of disk; should be one of
59
+ #:
60
+ #: * ALIAS_FIXED_DISK
61
+ #: * ALIAS_NETWORK_DISK
62
+ #: * ALIAS_400KB_FLOPPY_DISK
63
+ #: * ALIAS_800KB_FLOPPY_DISK
64
+ #: * ALIAS_1_44MB_FLOPPY_DISK
65
+ #: * ALIAS_EJECTABLE_DISK
66
+ self.disk_type = disk_type
67
+
68
+ #: Filesystem attribute flags (from HFS volume header)
69
+ self.attribute_flags = attribute_flags
70
+
71
+ #: Filesystem identifier
72
+ self.fs_id = fs_id
73
+
74
+ #: AppleShare information (for automatic remounting of network shares)
75
+ #: *(optional)*
76
+ self.appleshare_info = appleshare_info
77
+
78
+ #: Driver name (*probably* contains a disk driver name on older Macs)
79
+ #: *(optional)*
80
+ self.driver_name = driver_name
81
+
82
+ #: POSIX path of the mount point of the target's volume
83
+ #: *(optional)*
84
+ self.posix_path = posix_path
85
+
86
+ #: :class:`Alias` object pointing at the disk image on which the
87
+ #: target's volume resides *(optional)*
88
+ self.disk_image_alias = disk_image_alias
89
+
90
+ #: Dialup information (for automatic establishment of dialup connections)
91
+ self.dialup_info = dialup_info
92
+
93
+ #: Network mount information (for automatic remounting)
94
+ self.network_mount_info = network_mount_info
95
+
96
+ def __repr__(self):
97
+ args = ['name', 'creation_date', 'fs_type', 'disk_type',
98
+ 'attribute_flags', 'fs_id']
99
+ values = []
100
+ for a in args:
101
+ v = getattr(self, a)
102
+ values.append(repr(v))
103
+
104
+ kwargs = ['appleshare_info', 'driver_name', 'posix_path',
105
+ 'disk_image_alias', 'dialup_info', 'network_mount_info']
106
+ for a in kwargs:
107
+ v = getattr(self, a)
108
+ if v is not None:
109
+ values.append('%s=%r' % (a, v))
110
+ return 'VolumeInfo(%s)' % ','.join(values)
111
+
112
+ class TargetInfo (object):
113
+ def __init__(self, kind, filename, folder_cnid, cnid, creation_date,
114
+ creator_code, type_code, levels_from=-1, levels_to=-1,
115
+ folder_name=None, cnid_path=None, carbon_path=None,
116
+ posix_path=None, user_home_prefix_len=None):
117
+ #: Either ALIAS_KIND_FILE or ALIAS_KIND_FOLDER
118
+ self.kind = kind
119
+
120
+ #: The filename of the target
121
+ self.filename = filename
122
+
123
+ #: The CNID (Catalog Node ID) of the target's containing folder;
124
+ #: CNIDs are similar to but different than traditional UNIX inode
125
+ #: numbers
126
+ self.folder_cnid = folder_cnid
127
+
128
+ #: The CNID (Catalog Node ID) of the target
129
+ self.cnid = cnid
130
+
131
+ #: The target's *creation* date.
132
+ self.creation_date = creation_date
133
+
134
+ #: The target's Mac creator code (a four-character binary string)
135
+ self.creator_code = creator_code
136
+
137
+ #: The target's Mac type code (a four-character binary string)
138
+ self.type_code = type_code
139
+
140
+ #: The depth of the alias? Always seems to be -1 on OS X.
141
+ self.levels_from = -1
142
+
143
+ #: The depth of the target? Always seems to be -1 on OS X.
144
+ self.levels_to = -1
145
+
146
+ #: The (POSIX) name of the target's containing folder. *(optional)*
147
+ self.folder_name = None
148
+
149
+ #: The path from the volume root as a sequence of CNIDs. *(optional)*
150
+ self.cnid_path = None
151
+
152
+ #: The Carbon path of the target *(optional)*
153
+ self.carbon_path = None
154
+
155
+ #: The POSIX path of the target relative to the volume root. Note
156
+ #: that this may or may not have a leading '/' character, but it is
157
+ #: always relative to the containing volume. *(optional)*
158
+ self.posix_path = None
159
+
160
+ #: If the path points into a user's home folder, the number of folders
161
+ #: deep that we go before we get to that home folder. *(optional)*
162
+ self.user_home_prefix_len = None
163
+
164
+ def __repr__(self):
165
+ args = ['kind', 'filename', 'folder_cnid', 'cnid', 'creation_date',
166
+ 'creator_code', 'type_code']
167
+ values = []
168
+ for a in args:
169
+ v = getattr(self, a)
170
+ values.append(repr(v))
171
+
172
+ if self.levels_from != -1:
173
+ values.append('levels_from=%r' % self.levels_from)
174
+ if self.levels_to != -1:
175
+ values.append('levels_to=%r' % self.levels_to)
176
+
177
+ kwargs = ['folder_name', 'cnid_path', 'carbon_path',
178
+ 'posix_path', 'user_home_prefix_len']
179
+ for a in kwargs:
180
+ v = getattr(self, a)
181
+ values.append('%s=%r' % (a, v))
182
+
183
+ return 'TargetInfo(%s)' % ','.join(values)
184
+
185
+ TAG_CARBON_FOLDER_NAME = 0
186
+ TAG_CNID_PATH = 1
187
+ TAG_CARBON_PATH = 2
188
+ TAG_APPLESHARE_ZONE = 3
189
+ TAG_APPLESHARE_SERVER_NAME = 4
190
+ TAG_APPLESHARE_USERNAME = 5
191
+ TAG_DRIVER_NAME = 6
192
+ TAG_NETWORK_MOUNT_INFO = 9
193
+ TAG_DIALUP_INFO = 10
194
+ TAG_UNICODE_FILENAME = 14
195
+ TAG_UNICODE_VOLUME_NAME = 15
196
+ TAG_HIGH_RES_VOLUME_CREATION_DATE = 16
197
+ TAG_HIGH_RES_CREATION_DATE = 17
198
+ TAG_POSIX_PATH = 18
199
+ TAG_POSIX_PATH_TO_MOUNTPOINT = 19
200
+ TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE = 20
201
+ TAG_USER_HOME_LENGTH_PREFIX = 21
202
+
203
+ class Alias (object):
204
+ def __init__(self, appinfo=b'\0\0\0\0', version=2, volume=None,
205
+ target=None, extra=[]):
206
+ """Construct a new :class:`Alias` object with the specified
207
+ contents."""
208
+
209
+ #: Application specific information (four byte byte-string)
210
+ self.appinfo = appinfo
211
+
212
+ #: Version (we support only version 2)
213
+ self.version = version
214
+
215
+ #: A :class:`VolumeInfo` object describing the target's volume
216
+ self.volume = volume
217
+
218
+ #: A :class:`TargetInfo` object describing the target
219
+ self.target = target
220
+
221
+ #: A list of extra `(tag, value)` pairs
222
+ self.extra = list(extra)
223
+
224
+ @classmethod
225
+ def _from_fd(cls, b):
226
+ appinfo, recsize, version = struct.unpack(b'>4shh', b.read(8))
227
+
228
+ if recsize < 150:
229
+ raise ValueError('Incorrect alias length')
230
+
231
+ if version != 2:
232
+ raise ValueError('Unsupported alias version %u' % version)
233
+
234
+ kind, volname, voldate, fstype, disktype, \
235
+ folder_cnid, filename, cnid, crdate, creator_code, type_code, \
236
+ levels_from, levels_to, volattrs, volfsid, reserved = \
237
+ struct.unpack(b'>h28pI2shI64pII4s4shhI2s10s', b.read(142))
238
+
239
+ voldate = mac_epoch + datetime.timedelta(seconds=voldate)
240
+ crdate = mac_epoch + datetime.timedelta(seconds=crdate)
241
+
242
+ alias = Alias()
243
+ alias.appinfo = appinfo
244
+
245
+ alias.volume = VolumeInfo (volname.replace('/',':'),
246
+ voldate, fstype, disktype,
247
+ volattrs, volfsid)
248
+ alias.target = TargetInfo (kind, filename.replace('/',':'),
249
+ folder_cnid, cnid,
250
+ crdate, creator_code, type_code)
251
+ alias.target.levels_from = levels_from
252
+ alias.target.levels_to = levels_to
253
+
254
+ tag = struct.unpack(b'>h', b.read(2))[0]
255
+
256
+ while tag != -1:
257
+ length = struct.unpack(b'>h', b.read(2))[0]
258
+ value = b.read(length)
259
+ if length & 1:
260
+ b.read(1)
261
+
262
+ if tag == TAG_CARBON_FOLDER_NAME:
263
+ alias.target.folder_name = value.replace('/',':')
264
+ elif tag == TAG_CNID_PATH:
265
+ alias.target.cnid_path = struct.unpack(b'>%uI' % (length / 4),
266
+ value)
267
+ elif tag == TAG_CARBON_PATH:
268
+ alias.target.carbon_path = value
269
+ elif tag == TAG_APPLESHARE_ZONE:
270
+ if alias.volume.appleshare_info is None:
271
+ alias.volume.appleshare_info = AppleShareInfo()
272
+ alias.volume.appleshare_info.zone = value
273
+ elif tag == TAG_APPLESHARE_SERVER_NAME:
274
+ if alias.volume.appleshare_info is None:
275
+ alias.volume.appleshare_info = AppleShareInfo()
276
+ alias.volume.appleshare_info.server = value
277
+ elif tag == TAG_APPLESHARE_USERNAME:
278
+ if alias.volume.appleshare_info is None:
279
+ alias.volume.appleshare_info = AppleShareInfo()
280
+ alias.volume.appleshare_info.user = value
281
+ elif tag == TAG_DRIVER_NAME:
282
+ alias.volume.driver_name = value
283
+ elif tag == TAG_NETWORK_MOUNT_INFO:
284
+ alias.volume.network_mount_info = value
285
+ elif tag == TAG_DIALUP_INFO:
286
+ alias.volume.dialup_info = value
287
+ elif tag == TAG_UNICODE_FILENAME:
288
+ alias.target.filename = value[2:].decode('utf-16be')
289
+ elif tag == TAG_UNICODE_VOLUME_NAME:
290
+ alias.volume.name = value[2:].decode('utf-16be')
291
+ elif tag == TAG_HIGH_RES_VOLUME_CREATION_DATE:
292
+ seconds = struct.unpack(b'>Q', value)[0] / 65536.0
293
+ alias.volume.creation_date \
294
+ = mac_epoch + datetime.timedelta(seconds=seconds)
295
+ elif tag == TAG_HIGH_RES_CREATION_DATE:
296
+ seconds = struct.unpack(b'>Q', value)[0] / 65536.0
297
+ alias.target.creation_date \
298
+ = mac_epoch + datetime.timedelta(seconds=seconds)
299
+ elif tag == TAG_POSIX_PATH:
300
+ alias.target.posix_path = value
301
+ elif tag == TAG_POSIX_PATH_TO_MOUNTPOINT:
302
+ alias.volume.posix_path = value
303
+ elif tag == TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE:
304
+ alias.volume.disk_image_alias = Alias.from_bytes(value)
305
+ elif tag == TAG_USER_HOME_LENGTH_PREFIX:
306
+ alias.target.user_home_prefix_len = struct.unpack(b'>h', value)[0]
307
+ else:
308
+ alias.extra.append((tag, value))
309
+
310
+ tag = struct.unpack(b'>h', b.read(2))[0]
311
+
312
+ return alias
313
+
314
+ @classmethod
315
+ def from_bytes(cls, bytes):
316
+ """Construct an :class:`Alias` object given binary Alias data."""
317
+ with io.BytesIO(bytes) as b:
318
+ return cls._from_fd(b)
319
+
320
+ @classmethod
321
+ def for_file(cls, path):
322
+ """Create an :class:`Alias` that points at the specified file."""
323
+ if sys.platform != 'darwin':
324
+ raise Exception('Not implemented (requires special support)')
325
+
326
+ a = Alias()
327
+
328
+ # Find the filesystem
329
+ st = osx.statfs(path)
330
+ vol_path = st.f_mntonname
331
+
332
+ # Grab its attributes
333
+ attrs = [osx.ATTR_CMN_CRTIME,
334
+ osx.ATTR_VOL_NAME,
335
+ 0, 0, 0]
336
+ volinfo = osx.getattrlist(vol_path, attrs, 0)
337
+
338
+ vol_crtime = volinfo[0]
339
+ vol_name = volinfo[1]
340
+
341
+ # Also grab various attributes of the file
342
+ attrs = [(osx.ATTR_CMN_OBJTYPE
343
+ | osx.ATTR_CMN_CRTIME
344
+ | osx.ATTR_CMN_FNDRINFO
345
+ | osx.ATTR_CMN_FILEID
346
+ | osx.ATTR_CMN_PARENTID), 0, 0, 0, 0]
347
+ info = osx.getattrlist(path, attrs, osx.FSOPT_NOFOLLOW)
348
+
349
+ if info[0] == osx.VDIR:
350
+ kind = ALIAS_KIND_FOLDER
351
+ else:
352
+ kind = ALIAS_KIND_FILE
353
+
354
+ cnid = info[3]
355
+ folder_cnid = info[4]
356
+
357
+ dirname, filename = os.path.split(path)
358
+
359
+ if dirname == '' or dirname == '.':
360
+ dirname = os.getcwd()
361
+
362
+ foldername = os.path.basename(dirname)
363
+
364
+ creation_date = info[1]
365
+
366
+ if kind == ALIAS_KIND_FILE:
367
+ creator_code = struct.pack(b'I', info[2].fileInfo.fileCreator)
368
+ type_code = struct.pack(b'I', info[2].fileInfo.fileType)
369
+ else:
370
+ creator_code = b'\0\0\0\0'
371
+ type_code = b'\0\0\0\0'
372
+
373
+ a.target = TargetInfo(kind, filename, folder_cnid, cnid, creation_date,
374
+ creator_code, type_code)
375
+ a.volume = VolumeInfo(vol_name, vol_crtime, b'H+',
376
+ ALIAS_FIXED_DISK, 0, b'\0\0')
377
+
378
+ a.target.folder_name = foldername
379
+ a.volume.posix_path = vol_path
380
+
381
+ rel_path = os.path.relpath(path, vol_path)
382
+
383
+ # Leave off the initial '/' if vol_path is '/' (no idea why)
384
+ if vol_path == '/':
385
+ a.target.posix_path = rel_path
386
+ else:
387
+ a.target.posix_path = '/' + rel_path
388
+
389
+ # Construct the Carbon and CNID paths
390
+ carbon_path = []
391
+ cnid_path = []
392
+ head, tail = os.path.split(rel_path)
393
+ if not tail:
394
+ head, tail = os.path.split(head)
395
+ while head or tail:
396
+ if head:
397
+ attrs = [osx.ATTR_CMN_FILEID, 0, 0, 0, 0]
398
+ info = osx.getattrlist(os.path.join(vol_path, head), attrs, 0)
399
+ cnid_path.append(info[0])
400
+ carbon_tail = tail.replace(':','/')
401
+ carbon_path.insert(0, carbon_tail)
402
+ head, tail = os.path.split(head)
403
+ carbon_path = vol_name + ':' + ':\0'.join(carbon_path)
404
+
405
+ a.target.carbon_path = carbon_path
406
+ a.target.cnid_path = cnid_path
407
+
408
+ return a
409
+
410
+ def _to_fd(self, b):
411
+ # We'll come back and fix the length when we're done
412
+ pos = b.tell()
413
+ b.write(struct.pack(b'>4shh', self.appinfo, 0, self.version))
414
+
415
+ carbon_volname = self.volume.name.replace(':','/').encode('utf-8')
416
+ carbon_filename = self.target.filename.replace(':','/').encode('utf-8')
417
+ voldate = (self.volume.creation_date - mac_epoch).total_seconds()
418
+ crdate = (self.target.creation_date - mac_epoch).total_seconds()
419
+
420
+ # NOTE: crdate should be in local time, but that's system dependent
421
+ # (so doing so is ridiculous, and nothing could rely on it).
422
+ b.write(struct.pack(b'>h28pI2shI64pII4s4shhI2s10s',
423
+ self.target.kind,
424
+ carbon_volname, voldate,
425
+ self.volume.fs_type,
426
+ self.volume.disk_type,
427
+ self.target.folder_cnid,
428
+ carbon_filename,
429
+ self.target.cnid,
430
+ crdate,
431
+ self.target.creator_code,
432
+ self.target.type_code,
433
+ self.target.levels_from,
434
+ self.target.levels_to,
435
+ self.volume.attribute_flags,
436
+ self.volume.fs_id,
437
+ b'\0'*10))
438
+
439
+ # Excuse the odd order; we're copying Finder
440
+ if self.target.folder_name:
441
+ carbon_foldername = self.target.folder_name.replace(':','/')\
442
+ .encode('utf-8')
443
+ b.write(struct.pack(b'>hh', TAG_CARBON_FOLDER_NAME,
444
+ len(carbon_foldername)))
445
+ b.write(carbon_foldername)
446
+ if len(carbon_foldername) & 1:
447
+ b.write(b'\0')
448
+
449
+ b.write(struct.pack(b'>hhQhhQ',
450
+ TAG_HIGH_RES_VOLUME_CREATION_DATE,
451
+ 8, long(voldate * 65536),
452
+ TAG_HIGH_RES_CREATION_DATE,
453
+ 8, long(crdate * 65536)))
454
+
455
+ if self.target.cnid_path:
456
+ cnid_path = struct.pack(b'>%uI' % len(self.target.cnid_path),
457
+ *self.target.cnid_path)
458
+ b.write(struct.pack(b'>hh', TAG_CNID_PATH,
459
+ len(cnid_path)))
460
+ b.write(cnid_path)
461
+
462
+ if self.target.carbon_path:
463
+ carbon_path=self.target.carbon_path.encode('utf-8')
464
+ b.write(struct.pack(b'>hh', TAG_CARBON_PATH,
465
+ len(carbon_path)))
466
+ b.write(carbon_path)
467
+ if len(carbon_path) & 1:
468
+ b.write(b'\0')
469
+
470
+ if self.volume.appleshare_info:
471
+ ai = self.volume.appleshare_info
472
+ if ai.zone:
473
+ b.write(struct.pack(b'>hh', TAG_APPLESHARE_ZONE,
474
+ len(ai.zone)))
475
+ b.write(ai.zone)
476
+ if len(ai.zone) & 1:
477
+ b.write(b'\0')
478
+ if ai.server:
479
+ b.write(struct.pack(b'>hh', TAG_APPLESHARE_SERVER_NAME,
480
+ len(ai.server)))
481
+ b.write(ai.server)
482
+ if len(ai.server) & 1:
483
+ b.write(b'\0')
484
+ if ai.username:
485
+ b.write(struct.pack(b'>hh', TAG_APPLESHARE_SERVER_NAME,
486
+ len(ai.username)))
487
+ b.write(ai.username)
488
+ if len(ai.username) & 1:
489
+ b.write(b'\0')
490
+
491
+ if self.volume.driver_name:
492
+ driver_name = self.volume.driver_name.encode('utf-8')
493
+ b.write(struct.pack(b'>hh', TAG_DRIVER_NAME,
494
+ len(driver_name)))
495
+ b.write(driver_name)
496
+ if len(driver_name) & 1:
497
+ b.write(b'\0')
498
+
499
+ if self.volume.network_mount_info:
500
+ b.write(struct.pack(b'>hh', TAG_NETWORK_MOUNT_INFO,
501
+ len(self.volume.network_mount_info)))
502
+ b.write(self.volume.network_mount_info)
503
+ if len(self.volume.network_mount_info) & 1:
504
+ b.write(b'\0')
505
+
506
+ if self.volume.dialup_info:
507
+ b.write(struct.pack(b'>hh', TAG_DIALUP_INFO,
508
+ len(self.volume.network_mount_info)))
509
+ b.write(self.volume.network_mount_info)
510
+ if len(self.volume.network_mount_info) & 1:
511
+ b.write(b'\0')
512
+
513
+ utf16 = self.target.filename.replace(':','/').encode('utf-16-be')
514
+ b.write(struct.pack(b'>hhh', TAG_UNICODE_FILENAME,
515
+ len(utf16) + 2,
516
+ len(utf16) / 2))
517
+ b.write(utf16)
518
+
519
+ utf16 = self.volume.name.replace(':','/').encode('utf-16-be')
520
+ b.write(struct.pack(b'>hhh', TAG_UNICODE_VOLUME_NAME,
521
+ len(utf16) + 2,
522
+ len(utf16) / 2))
523
+ b.write(utf16)
524
+
525
+ if self.target.posix_path:
526
+ posix_path = self.target.posix_path.encode('utf-8')
527
+ b.write(struct.pack(b'>hh', TAG_POSIX_PATH,
528
+ len(posix_path)))
529
+ b.write(posix_path)
530
+ if len(posix_path) & 1:
531
+ b.write(b'\0')
532
+
533
+ if self.volume.posix_path:
534
+ posix_path = self.volume.posix_path.encode('utf-8')
535
+ b.write(struct.pack(b'>hh', TAG_POSIX_PATH_TO_MOUNTPOINT,
536
+ len(posix_path)))
537
+ b.write(posix_path)
538
+ if len(posix_path) & 1:
539
+ b.write(b'\0')
540
+
541
+ if self.volume.disk_image_alias:
542
+ d = self.volume.disk_image_alias.to_bytes()
543
+ b.write(struct.pack(b'>hh', TAG_RECURSIVE_ALIAS_OF_DISK_IMAGE,
544
+ len(d)))
545
+ b.write(d)
546
+ if len(d) & 1:
547
+ b.write(b'\0')
548
+
549
+ if self.target.user_home_prefix_len is not None:
550
+ b.write(struct.pack(b'>hhh', TAG_USER_HOME_LENGTH_PREFIX,
551
+ 2, self.target.user_home_prefix_len))
552
+
553
+ for t,v in self.extra:
554
+ b.write(struct.pack(b'>hh', t, len(v)))
555
+ b.write(v)
556
+ if len(v) & 1:
557
+ b.write(b'\0')
558
+
559
+ b.write(struct.pack(b'>hh', -1, 0))
560
+
561
+ blen = b.tell() - pos
562
+ b.seek(pos + 4, os.SEEK_SET)
563
+ b.write(struct.pack(b'>h', blen))
564
+
565
+ def to_bytes(self):
566
+ """Returns the binary representation for this :class:`Alias`."""
567
+ with io.BytesIO() as b:
568
+ self._to_fd(b)
569
+ return b.getvalue()
570
+
571
+ def __str__(self):
572
+ return '<Alias target=%s>' % self.target.filename
573
+
574
+ def __repr__(self):
575
+ values = []
576
+ if self.appinfo != b'\0\0\0\0':
577
+ values.append('appinfo=%r' % self.appinfo)
578
+ if self.version != 2:
579
+ values.append('version=%r' % self.version)
580
+ if self.volume is not None:
581
+ values.append('volume=%r' % self.volume)
582
+ if self.target is not None:
583
+ values.append('target=%r' % self.target)
584
+ if self.extra:
585
+ values.append('extra=%r' % self.extra)
586
+ return 'Alias(%s)' % ','.join(values)