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