ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
dir: /appl/lib/palmdb.b/
implement Palmdb;
#
# Copyright © 2001-2002 Vita Nuova Holdings Limited. All rights reserved.
#
# Based on ``Palm® File Format Specification'', Document Number 3008-004, 1 May 2001, by Palm Inc.
# Doc compression based on description by Paul Lucas, 18 August 1998
#
include "sys.m";
sys: Sys;
include "daytime.m";
daytime: Daytime;
include "bufio.m";
bufio: Bufio;
Iobuf: import bufio;
include "palm.m";
palm: Palm;
DBInfo, Record, Resource, get2, get3, get4, put2, put3, put4, gets, puts: import palm;
filename, dbname: import palm;
Entry: adt {
id: int; # resource: id; record: unique ID
offset: int;
size: int;
name: int; # resource entry only
attr: int; # record entry only
};
Ofile: adt {
fname: string;
f: ref Iobuf;
mode: int;
info: ref DBInfo;
appinfo: array of byte;
sortinfo: array of int;
uidseed: int;
entries: array of ref Entry;
};
files: array of ref Ofile;
Dbhdrlen: con 72+6;
Datahdrsize: con 4+1+3;
Resourcehdrsize: con 4+2+4;
# Exact value of "Jan 1, 1970 0:00:00 GMT" - "Jan 1, 1904 0:00:00 GMT"
Epochdelta: con 2082844800;
tzoff := 0;
init(m: Palm): string
{
sys = load Sys Sys->PATH;
bufio = load Bufio Bufio->PATH;
daytime = load Daytime Daytime->PATH;
if(bufio == nil || daytime == nil)
return "can't load required module";
palm = m;
tzoff = daytime->local(0).tzoff;
return nil;
}
Eshort: con "file format error: too small";
DB.open(name: string, mode: int): (ref DB, string)
{
if(mode != Sys->OREAD)
return (nil, "invalid mode");
fd := sys->open(name, mode);
if(fd == nil)
return (nil, sys->sprint("%r"));
(ok, d) := sys->fstat(fd);
if(ok < 0)
return (nil, sys->sprint("%r"));
length := int d.length;
if(length == 0)
return (nil, "empty file");
(pf, ofile, fx) := mkpfile(name, mode);
f := bufio->fopen(fd, mode); # automatically closed if open fails
p := array[Dbhdrlen] of byte;
if(f.read(p, Dbhdrlen) != Dbhdrlen)
return (nil, "invalid file header: too short");
ip := ofile.info;
ip.name = gets(p[0:32]);
ip.attr = get2(p[32:]);
ip.version = get2(p[34:]);
ip.ctime = pilot2epoch(get4(p[36:]));
ip.mtime = pilot2epoch(get4(p[40:]));
ip.btime = pilot2epoch(get4(p[44:]));
ip.modno = get4(p[48:]);
appinfo := get4(p[52:]);
sortinfo := get4(p[56:]);
if(appinfo < 0 || sortinfo < 0 || (appinfo|sortinfo)&1)
return (nil, "invalid header: bad offset");
ip.dtype = xs(get4(p[60:]));
ip.creator = xs(get4(p[64:]));
ofile.uidseed = ip.uidseed = get4(p[68:]);
if(get4(p[72:]) != 0)
return (nil, "chained headers not supported"); # Palm says to reject such files
nrec := get2(p[76:]);
if(nrec < 0)
return (nil, sys->sprint("invalid header: bad record count: %d", nrec));
esize := Datahdrsize;
if(ip.attr & Palm->Fresource)
esize = Resourcehdrsize;
dataoffset := length;
ofile.entries = array[nrec] of ref Entry;
if(nrec > 0){
laste: ref Entry;
buf := array[esize] of byte;
for(i := 0; i < nrec; i++){
if(f.read(buf, len buf) != len buf)
return (nil, Eshort);
e := ref Entry;
if(ip.attr & Palm->Fresource){
# resource entry: type[4], id[2], offset[4]
e.name = get4(buf);
e.id = get2(buf[4:]);
e.offset = get4(buf[6:]);
e.attr = 0;
}else{
# record entry: offset[4], attr[1], id[3]
e.offset = get4(buf);
e.attr = int buf[4];
e.id = get3(buf[5:]);
e.name = 0;
}
if(laste != nil)
laste.size = e.offset - laste.offset;
laste = e;
ofile.entries[i] = e;
}
if(laste != nil)
laste.size = length - laste.offset;
dataoffset = ofile.entries[0].offset;
}else{
if(f.read(p, 2) != 2)
return (nil, Eshort); # discard placeholder bytes
}
n := 0;
if(appinfo > 0){
n = appinfo - int f.offset();
while(--n >= 0)
f.getb();
if(sortinfo)
n = sortinfo - appinfo;
else
n = dataoffset - appinfo;
ofile.appinfo = array[n] of byte;
if(f.read(ofile.appinfo, n) != n)
return (nil, Eshort);
}
if(sortinfo > 0){
n = sortinfo - int f.offset();
while(--n >= 0)
f.getb();
n = (dataoffset-sortinfo)/2;
ofile.sortinfo = array[n] of int;
tmp := array[2*n] of byte;
if(f.read(tmp, len tmp) != len tmp)
return (nil, Eshort);
for(i := 0; i < n; i++)
ofile.sortinfo[i] = get2(tmp[2*i:]);
}
ofile.f = f; # safe to save open file reference
files[fx] = ofile;
return (pf, nil);
}
DB.close(db: self ref DB): string
{
ofile := files[db.x];
if(ofile.f != nil){
ofile.f.close();
ofile.f = nil;
}
files[db.x] = nil;
return nil;
}
DB.stat(db: self ref DB): ref DBInfo
{
return ref *files[db.x].info;
}
DB.create(name: string, mode: int, perm: int, info: ref DBInfo): (ref DB, string)
{
return (nil, "DB.create not implemented");
}
DB.wstat(db: self ref DB, ip: ref DBInfo, flags: int)
{
raise "DB.wstat not implemented";
}
#DB.wstat(db: self ref DB, ip: ref DBInfo): string
#{
# ofile := files[db.x];
# if(ofile.mode != Sys->OWRITE)
# return "not open for writing";
# if((ip.attr & Palm->Fresource) != (ofile.info.attr & Palm->Fresource))
# return "cannot change file type";
# # copy only a subset
# ofile.info.name = ip.name;
# ofile.info.attr = ip.attr;
# ofile.info.version = ip.version;
# ofile.info.ctime = ip.ctime;
# ofile.info.mtime = ip.mtime;
# ofile.info.btime = ip.btime;
# ofile.info.modno = ip.modno;
# ofile.info.dtype = ip.dtype;
# ofile.info.creator = ip.creator;
# return nil;
#}
DB.rdappinfo(db: self ref DB): (array of byte, string)
{
return (files[db.x].appinfo, nil);
}
DB.wrappinfo(db: self ref DB, data: array of byte): string
{
ofile := files[db.x];
if(ofile.mode != Sys->OWRITE)
return "not open for writing";
ofile.appinfo = array[len data] of byte;
ofile.appinfo[0:] = data;
return nil;
}
DB.rdsortinfo(db: self ref DB): (array of int, string)
{
return (files[db.x].sortinfo, nil);
}
DB.wrsortinfo(db: self ref DB, sort: array of int): string
{
ofile := files[db.x];
if(ofile.mode != Sys->OWRITE)
return "not open for writing";
ofile.sortinfo = array[len sort] of int;
ofile.sortinfo[0:] = sort;
return nil;
}
DB.readidlist(db: self ref DB, nil: int): array of int
{
ent := files[db.x].entries;
a := array[len ent] of int;
for(i := 0; i < len a; i++)
a[i] = ent[i].id;
return a;
}
DB.nentries(db: self ref DB): int
{
return len files[db.x].entries;
}
DB.resetsyncflags(db: self ref DB): string
{
raise "DB.resetsyncflags not implemented";
}
DB.records(db: self ref DB): ref PDB
{
if(db == nil || db.attr & Palm->Fresource)
return nil;
return ref PDB(db);
}
DB.resources(db: self ref DB): ref PRC
{
if(db == nil || (db.attr & Palm->Fresource) == 0)
return nil;
return ref PRC(db);
}
PDB.read(pdb: self ref PDB, i: int): ref Record
{
ofile := files[pdb.db.x];
if(i < 0 || i >= len ofile.entries){
if(i == len ofile.entries)
return nil; # treat as end-of-file
#return "index out of range";
return nil;
}
e := ofile.entries[i];
nb := e.size;
r := ref Record(e.id, e.attr & 16rF0, e.attr & 16r0F, array[nb] of byte);
ofile.f.seek(big e.offset, 0);
if(ofile.f.read(r.data, nb) != nb)
return nil;
return r;
}
PDB.readid(pdb: self ref PDB, id: int): (ref Record, int)
{
ofile := files[pdb.db.x];
ent := ofile.entries;
for(i := 0; i < len ent; i++)
if((e := ent[i]).id == id){
nb := e.size;
r := ref Record(e.id, e.attr & 16rF0, e.attr & 16r0F, array[e.size] of byte);
ofile.f.seek(big e.offset, 0);
if(ofile.f.read(r.data, nb) != nb)
return (nil, -1);
return (r, id);
}
sys->werrstr("ID not found");
return (nil, -1);
}
PDB.resetnext(db: self ref PDB): int
{
raise "PDB.resetnext not implemented";
}
PDB.readnextmod(db: self ref PDB): (ref Record, int)
{
raise "PDB.readnextmod not implemented";
}
PDB.write(db: self ref PDB, r: ref Record): string
{
return "PDB.write not implemented";
}
PDB.truncate(db: self ref PDB): string
{
return "PDB.truncate not implemented";
}
PDB.delete(db: self ref PDB, id: int): string
{
return "PDB.delete not implemented";
}
PDB.deletecat(db: self ref PDB, cat: int): string
{
return "PDB.deletecat not implemented";
}
PDB.purge(db: self ref PDB): string
{
return "PDB.purge not implemented";
}
PDB.movecat(db: self ref PDB, old: int, new: int): string
{
return "PDB.movecat not implemented";
}
PRC.read(db: self ref PRC, index: int): ref Resource
{
return nil;
}
PRC.readtype(db: self ref PRC, name: int, id: int): (ref Resource, int)
{
return (nil, -1);
}
PRC.write(db: self ref PRC, r: ref Resource): string
{
return "PRC.write not implemented";
}
PRC.truncate(db: self ref PRC): string
{
return "PRC.truncate not implemented";
}
PRC.delete(db: self ref PRC, name: int, id: int): string
{
return "PRC.delete not implemented";
}
#
# internal function to extend entry list if necessary, and return a
# pointer to the next available slot
#
entryensure(db: ref DB, i: int): ref Entry
{
ofile := files[db.x];
if(i < len ofile.entries)
return ofile.entries[i];
e := ref Entry(0, -1, 0, 0, 0);
n := len ofile.entries;
if(n == 0)
n = 64;
else
n = (i+63) & ~63;
a := array[n] of ref Entry;
a[0:] = ofile.entries;
a[i] = e;
ofile.entries = a;
return e;
}
writefilehdr(db: ref DB, mode: int, perm: int): string
{
ofile := files[db.x];
if(len ofile.entries >= 64*1024)
return "too many records for Palm file"; # is there a way to extend it?
if((f := bufio->create(ofile.fname, mode, perm)) == nil)
return sys->sprint("%r");
ip := ofile.info;
esize := Datahdrsize;
if(ip.attr & Palm->Fresource)
esize = Resourcehdrsize;
offset := Dbhdrlen + esize*len ofile.entries + 2;
offset += 2; # placeholder bytes or gap bytes
appinfo := 0;
if(len ofile.appinfo > 0){
appinfo = offset;
offset += len ofile.appinfo;
}
sortinfo := 0;
if(len ofile.sortinfo > 0){
sortinfo = offset;
offset += 2*len ofile.sortinfo; # 2-byte entries
}
p := array[Dbhdrlen] of byte; # bigger than any entry as well
puts(p[0:32], ip.name);
put2(p[32:], ip.attr);
put2(p[34:], ip.version);
put4(p[36:], epoch2pilot(ip.ctime));
put4(p[40:], epoch2pilot(ip.mtime));
put4(p[44:], epoch2pilot(ip.btime));
put4(p[48:], ip.modno);
put4(p[52:], appinfo);
put4(p[56:], sortinfo);
put4(p[60:], sx(ip.dtype));
put4(p[64:], sx(ip.creator));
put4(p[68:], ofile.uidseed);
put4(p[72:], 0); # next record list ID
put2(p[76:], len ofile.entries);
if(f.write(p, Dbhdrlen) != Dbhdrlen)
return ewrite(f);
if(len ofile.entries > 0){
for(i := 0; i < len ofile.entries; i++) {
e := ofile.entries[i];
e.offset = offset;
if(ip.attr & Palm->Fresource) {
put4(p, e.name);
put2(p[4:], e.id);
put4(p[6:], e.offset);
} else {
put4(p, e.offset);
p[4] = byte e.attr;
put3(p[5:], e.id);
}
if(f.write(p, esize) != esize)
return ewrite(f);
offset += e.size;
}
}
f.putb(byte 0); # placeholder bytes (figure 1.4) or gap bytes (p. 15)
f.putb(byte 0);
if(appinfo != 0){
if(f.write(ofile.appinfo, len ofile.appinfo) != len ofile.appinfo)
return ewrite(f);
}
if(sortinfo != 0){
tmp := array[2*len ofile.sortinfo] of byte;
for(i := 0; i < len ofile.sortinfo; i++)
put2(tmp[2*i:], ofile.sortinfo[i]);
if(f.write(tmp, len tmp) != len tmp)
return ewrite(f);
}
if(f.flush() != 0)
return ewrite(f);
return nil;
}
ewrite(f: ref Iobuf): string
{
e := sys->sprint("write error: %r");
f.close();
return e;
}
xs(i: int): string
{
if(i == 0)
return "";
if(i & int 16r80808080)
return sys->sprint("%8.8ux", i);
return sys->sprint("%c%c%c%c", (i>>24)&16rFF, (i>>16)&16rFF, (i>>8)&16rFF, i&16rFF);
}
sx(s: string): int
{
n := 0;
for(i := 0; i < 4; i++){
c := 0;
if(i < len s)
c = s[i] & 16rFF;
n = (n<<8) | c;
}
return n;
}
mkpfile(name: string, mode: int): (ref DB, ref Ofile, int)
{
ofile := ref Ofile(name, nil, mode, DBInfo.new(name, 0, nil, 0, nil),
array[0] of byte, array[0] of int, 0, nil);
for(x := 0; x < len files; x++)
if(files[x] == nil)
return (ref DB(x, mode, 0), ofile, x);
a := array[x] of ref Ofile;
a[0:] = files;
files = a;
return (ref DB(x, mode, 0), ofile, x);
}
#
# because PalmOS treats all times as local times, and doesn't associate
# them with time zones, we'll convert using local time on Plan 9 and Inferno
#
pilot2epoch(t: int): int
{
if(t == 0)
return 0; # we'll assume it's not set
return t - Epochdelta + tzoff;
}
epoch2pilot(t: int): int
{
if(t == 0)
return t;
return t - tzoff + Epochdelta;
}
#
# map Palm name to string, assuming iso-8859-1,
# but remap space and /
#
latin1(a: array of byte, remap: int): string
{
s := "";
for(i := 0; i < len a; i++){
c := int a[i];
if(c == 0)
break;
if(remap){
if(c == ' ')
c = 16r00A0; # unpaddable space
else if(c == '/')
c = 16r2215; # division /
}
s[len s] = c;
}
return s;
}