UNPKG

24.8 kBPlain TextView Raw
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)