shithub: purgatorio

ref: 3efb5bbb4061056e523858b134c555949591efe2
dir: /appl/lib/palmdb.b/

View raw version
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;
}