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