530 lines
13 KiB
Python
530 lines
13 KiB
Python
#
|
|
# $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $
|
|
#
|
|
# Copyright 1998-2001 Rob Tillotson <rob@pyrite.org>
|
|
# 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 <robt@debian.org>'
|
|
|
|
|
|
# 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)
|