# # $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $ # # Copyright 1998-2001 Rob Tillotson # All Rights Reserved # # Permission to use, copy, modify, and distribute this software and # its documentation for any purpose and without fee or royalty is # hereby granted, provided that the above copyright notice appear in # all copies and that both the copyright notice and this permission # notice appear in supporting documentation or portions thereof, # including modifications, that you you make. # # THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE! # """PRC/PDB file I/O in pure Python. This module serves two purposes: one, it allows access to Palm OS(tm) database files on the desktop in pure Python without requiring pilot-link (hence, it may be useful for import/export utilities), and two, it caches the contents of the file in memory so it can be freely modified using an identical API to databases over a DLP connection. """ __version__ = '$Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $' __copyright__ = 'Copyright 1998-2001 Rob Tillotson ' # temporary hack until we get gettext support again def _(s): return s # # DBInfo structure: # # int more # unsigned int flags # unsigned int miscflags # unsigned long type # unsigned long creator # unsigned int version # unsigned long modnum # time_t createDate, modifydate, backupdate # unsigned int index # char name[34] # # # DB Header: # 32 name # 2 flags # 2 version # 4 creation time # 4 modification time # 4 backup time # 4 modification number # 4 appinfo offset # 4 sortinfo offset # 4 type # 4 creator # 4 unique id seed (garbage?) # 4 next record list id (normally 0) # 2 num of records for this header # (maybe 2 more bytes) # # Resource entry header: (if low bit of attr = 1) # 4 type # 2 id # 4 offset # # record entry header: (if low bit of attr = 0) # 4 offset # 1 attributes # 3 unique id # # then 2 bytes of 0 # # then appinfo then sortinfo # import sys, os, stat, struct PI_HDR_SIZE = 78 PI_RESOURCE_ENT_SIZE = 10 PI_RECORD_ENT_SIZE = 8 PILOT_TIME_DELTA = 2082844800L flagResource = 0x0001 flagReadOnly = 0x0002 flagAppInfoDirty = 0x0004 flagBackup = 0x0008 flagOpen = 0x8000 # 2.x flagNewer = 0x0010 flagReset = 0x0020 # flagExcludeFromSync = 0x0080 attrDeleted = 0x80 attrDirty = 0x40 attrBusy = 0x20 attrSecret = 0x10 attrArchived = 0x08 default_info = { 'name': '', 'type': 'DATA', 'creator': ' ', 'createDate': 0, 'modifyDate': 0, 'backupDate': 0, 'modnum': 0, 'version': 0, 'flagReset': 0, 'flagResource': 0, 'flagNewer': 0, 'flagExcludeFromSync': 0, 'flagAppInfoDirty': 0, 'flagReadOnly': 0, 'flagBackup': 0, 'flagOpen': 0, 'more': 0, 'index': 0 } def null_terminated(s): for x in range(0, len(s)): if s[x] == '\000': return s[:x] return s def trim_null(s): return string.split(s, '\0')[0] def pad_null(s, l): if len(s) > l - 1: s = s[:l-1] s = s + '\0' if len(s) < l: s = s + '\0' * (l - len(s)) return s # # new stuff # Record object to be put in tree... class PRecord: def __init__(self, attr=0, id=0, category=0, raw=''): self.raw = raw self.id = id self.attr = attr self.category = category # comparison and hashing are done by ID; # thus, the id value *may not be changed* once # the object is created. def __cmp__(self, obj): if type(obj) == type(0): return cmp(self.id, obj) else: return cmp(self.id, obj.id) def __hash__(self): return self.id class PResource: def __init__(self, typ=' ', id=0, raw=''): self.raw = raw self.id = id self.type = typ def __cmp__(self, obj): if type(obj) == type(()): return cmp( (self.type, self.id), obj) else: return cmp( (self.type, self.id), (obj.type, obj.id) ) def __hash__(self): return hash((self.type, self.id)) class PCache: def __init__(self): self.data = [] self.appblock = '' self.sortblock = '' self.dirty = 0 self.next = 0 self.info = {} self.info.update(default_info) # if allow_zero_ids is 1, then this prc behaves appropriately # for a desktop database. That is, it never attempts to assign # an ID, and lets new records be inserted with an ID of zero. self.allow_zero_ids = 0 # pi-file API def getRecords(self): return len(self.data) def getAppBlock(self): return self.appblock and self.appblock or None def setAppBlock(self, raw): self.dirty = 1 self.appblock = raw def getSortBlock(self): return self.sortblock and self.sortblock or None def setSortBlock(self, raw): self.dirty = 1 self.appblock = raw def checkID(self, id): return id in self.data def getRecord(self, i): try: r = self.data[i] except: return None return r.raw, i, r.id, r.attr, r.category def getRecordByID(self, id): try: i = self.data.index(id) r = self.data[i] except: return None return r.raw, i, r.id, r.attr, r.category def getResource(self, i): try: r = self.data[i] except: return None return r.raw, r.type, r.id def getDBInfo(self): return self.info def setDBInfo(self, info): self.dirty = 1 self.info = {} self.info.update(info) def updateDBInfo(self, info): self.dirty = 1 self.info.update(info) def setRecord(self, attr, id, cat, data): if not self.allow_zero_ids and not id: if not len(self.data): id = 1 else: xid = self.data[0].id + 1 while xid in self.data: xid = xid + 1 id = xid r = PRecord(attr, id, cat, data) if id and id in self.data: self.data.remove(id) self.data.append(r) self.dirty = 1 return id def setRecordIdx(self, i, data): self.data[i].raw = data self.dirty = 1 def setResource(self, typ, id, data): if (typ, id) in self.data: self.data.remove((typ,id)) r = PResource(typ, id, data) self.data.append(r) self.dirty = 1 return id def getNextRecord(self, cat): while self.next < len(self.data): r = self.data[self.next] i = self.next self.next = self.next + 1 if r.category == cat: return r.raw, i, r.id, r.attr, r.category return '' def getNextModRecord(self, cat=-1): while self.next < len(self.data): r = self.data[self.next] i = self.next self.next = self.next + 1 if (r.attr & attrModified) and (cat < 0 or r.category == cat): return r.raw, i, r.id, r.attr, r.category def getResourceByID(self, type, id): try: r = self.data[self.data.index((type,id))] except: return None return r.raw, r.type, r.id def deleteRecord(self, id): if not id in self.data: return None self.data.remove(id) self.dirty = 1 def deleteRecords(self): self.data = [] self.dirty = 1 def deleteResource(self, type, id): if not (type,id) in self.data: return None self.data.remove((type,id)) self.dirty = 1 def deleteResources(self): self.data = [] self.dirty = 1 def getRecordIDs(self, sort=0): m = map(lambda x: x.id, self.data) if sort: m.sort() return m def moveCategory(self, frm, to): for r in self.data: if r.category == frm: r.category = to self.dirty = 1 def deleteCategory(self, cat): raise RuntimeError, _("unimplemented") def purge(self): ndata = [] # change to filter later for r in self.data: if (r.attr & attrDeleted): continue ndata.append(r) self.data = ndata self.dirty = 1 def resetNext(self): self.next = 0 def resetFlags(self): # special behavior for resources if not self.info.get('flagResource',0): # use map() for r in self.data: r.attr = r.attr & ~attrDirty self.dirty = 1 import pprint class File(PCache): def __init__(self, name=None, read=1, write=0, info={}): PCache.__init__(self) self.filename = name self.info.update(info) self.writeback = write self.isopen = 0 if read: self.load(name) self.isopen = 1 def close(self): if self.writeback and self.dirty: self.save(self.filename) self.isopen = 0 def __del__(self): if self.isopen: self.close() def load(self, f): if type(f) == type(''): f = open(f, 'rb') data = f.read() self.unpack(data) def unpack(self, data): if len(data) < PI_HDR_SIZE: raise IOError, _("file too short") (name, flags, ver, ctime, mtime, btime, mnum, appinfo, sortinfo, typ, creator, uid, nextrec, numrec) \ = struct.unpack('>32shhLLLlll4s4sllh', data[:PI_HDR_SIZE]) if nextrec or appinfo < 0 or sortinfo < 0 or numrec < 0: raise IOError, _("invalid database header") self.info = { 'name': null_terminated(name), 'type': typ, 'creator': creator, 'createDate': ctime - PILOT_TIME_DELTA, 'modifyDate': mtime - PILOT_TIME_DELTA, 'backupDate': btime - PILOT_TIME_DELTA, 'modnum': mnum, 'version': ver, 'flagReset': flags & flagReset, 'flagResource': flags & flagResource, 'flagNewer': flags & flagNewer, 'flagExcludeFromSync': flags & flagExcludeFromSync, 'flagAppInfoDirty': flags & flagAppInfoDirty, 'flagReadOnly': flags & flagReadOnly, 'flagBackup': flags & flagBackup, 'flagOpen': flags & flagOpen, 'more': 0, 'index': 0 } rsrc = flags & flagResource if rsrc: s = PI_RESOURCE_ENT_SIZE else: s = PI_RECORD_ENT_SIZE entries = [] pos = PI_HDR_SIZE for x in range(0,numrec): hstr = data[pos:pos+s] pos = pos + s if not hstr or len(hstr) < s: raise IOError, _("bad database header") if rsrc: (typ, id, offset) = struct.unpack('>4shl', hstr) entries.append((offset, typ, id)) else: (offset, auid) = struct.unpack('>ll', hstr) attr = (auid & 0xff000000) >> 24 uid = auid & 0x00ffffff entries.append((offset, attr, uid)) offset = len(data) entries.reverse() for of, q, id in entries: size = offset - of if size < 0: raise IOError, _("bad pdb/prc record entry (size < 0)") d = data[of:offset] offset = of if len(d) != size: raise IOError, _("failed to read record") if rsrc: r = PResource(q, id, d) self.data.append(r) else: r = PRecord(q & 0xf0, id, q & 0x0f, d) self.data.append(r) self.data.reverse() if sortinfo: sortinfo_size = offset - sortinfo offset = sortinfo else: sortinfo_size = 0 if appinfo: appinfo_size = offset - appinfo offset = appinfo else: appinfo_size = 0 if appinfo_size < 0 or sortinfo_size < 0: raise IOError, _("bad database header (appinfo or sortinfo size < 0)") if appinfo_size: self.appblock = data[appinfo:appinfo+appinfo_size] if len(self.appblock) != appinfo_size: raise IOError, _("failed to read appinfo block") if sortinfo_size: self.sortblock = data[sortinfo:sortinfo+sortinfo_size] if len(self.sortblock) != sortinfo_size: raise IOError, _("failed to read sortinfo block") def save(self, f): """Dump the cache to a file. """ if type(f) == type(''): f = open(f, 'wb') # first, we need to precalculate the offsets. if self.info.get('flagResource'): entries_len = 10 * len(self.data) else: entries_len = 8 * len(self.data) off = PI_HDR_SIZE + entries_len + 2 if self.appblock: appinfo_offset = off off = off + len(self.appblock) else: appinfo_offset = 0 if self.sortblock: sortinfo_offset = off off = off + len(self.sortblock) else: sortinfo_offset = 0 rec_offsets = [] for x in self.data: rec_offsets.append(off) off = off + len(x.raw) info = self.info flg = 0 if info.get('flagResource',0): flg = flg | flagResource if info.get('flagReadOnly',0): flg = flg | flagReadOnly if info.get('flagAppInfoDirty',0): flg = flg | flagAppInfoDirty if info.get('flagBackup',0): flg = flg | flagBackup if info.get('flagOpen',0): flg = flg | flagOpen if info.get('flagNewer',0): flg = flg | flagNewer if info.get('flagReset',0): flg = flg | flagReset # excludefromsync doesn't actually get stored? hdr = struct.pack('>32shhLLLlll4s4sllh', pad_null(info.get('name',''), 32), flg, info.get('version',0), info.get('createDate',0L)+PILOT_TIME_DELTA, info.get('modifyDate',0L)+PILOT_TIME_DELTA, info.get('backupDate',0L)+PILOT_TIME_DELTA, info.get('modnum',0), appinfo_offset, # appinfo sortinfo_offset, # sortinfo info.get('type',' '), info.get('creator',' '), 0, # uid??? 0, # nextrec??? len(self.data)) f.write(hdr) entries = [] record_data = [] rsrc = self.info.get('flagResource') for x, off in map(None, self.data, rec_offsets): if rsrc: record_data.append(x.raw) entries.append(struct.pack('>4shl', x.type, x.id, off)) else: record_data.append(x.raw) a = ((x.attr | x.category) << 24) | x.id entries.append(struct.pack('>ll', off, a)) for x in entries: f.write(x) f.write('\0\0') # padding? dunno, it's always there. if self.appblock: f.write(self.appblock) if self.sortblock: f.write(self.sortblock) for x in record_data: f.write(x)