shithub: purgatorio

ref: a411870ee4640241e3c494367d922847da84f972
dir: purgatorio/appl/lib/palm.b

View raw version
implement Palm;

#
# Copyright © 2001-2003 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 "palm.m";

# Exact value of "Jan 1, 1970 0:00:00 GMT" - "Jan 1, 1904 0:00:00 GMT"
Epochdelta: con 2082844800;
tzoff := 0;

init(): string
{
	sys = load Sys Sys->PATH;
	daytime = load Daytime Daytime->PATH;
	if(daytime == nil)
		return "can't load required module";
	tzoff = daytime->local(0).tzoff;
	return nil;
}

Record.new(id: int, attr: int, cat: int, size: int): ref Record
{
	return ref Record(id, attr, cat, array[size] of byte);
}

Resource.new(name: int, id: int, size: int): ref Resource
{
	return ref Resource(name, id, array[size] of byte);
}

Doc.open(m: Palmdb, file: ref Palmdb->PDB): (ref Doc, string)
{
	info := m->file.db.stat();
	if(info.dtype != "TEXt" || info.creator != "REAd")
		return (nil, "not a Doc file: wrong type or creator");
	r := m->file.read(0);
	if(r == nil)
		return (nil, sys->sprint("not a valid Doc file: %r"));
	a := r.data;
	if(len a < 16)
		return (nil, sys->sprint("not a valid Doc file: bad length: %d", len a));
	maxrec := m->file.db.nentries()-1;
	d := ref Doc;
	d.m = m;
	d.file = file;
	d.version = get2(a);
	err := "unknown";
	if(d.version != 1 && d.version != 2)
		err = "unknown Docfile version";
	# a[2:] is spare
	d.length = get4(a[4:]);
	d.nrec = get2(a[8:]);
	if(maxrec >= 0 && d.nrec > maxrec){
		d.nrec = maxrec;
		err = "invalid record count";
	}
	d.recsize = get2(a[10:]);
	d.position = get4(a[12:]);
	return (d, sys->sprint("unexpected Doc file format: %s", err));
}

Doc.iscompressed(d: self ref Doc): int
{
	return (d.version&7) == 2;		# high-order bits are sometimes used, ignore them
}

Doc.read(doc: self ref Doc, index: int): (string, string)
{
	m := doc.m;
	DB, PDB: import m;
	r := doc.file.read(index+1);
	if(r == nil)
		return (nil, sys->sprint("%r"));
	(s, serr) := doc.unpacktext(r.data);
	if(s == nil)
		return (nil, serr);
	return (s, nil);
}

Doc.unpacktext(doc: self ref Doc, a: array of byte): (string, string)
{
	nb := len a;
	s: string;
	if(!doc.iscompressed()){
		for(i := 0; i < nb; i++)
			s[len s] = int a[i];	# assumes Latin-1
		return (s, nil);
	}
	o := 0;
	for(i := 0; i < nb;){
		c := int a[i++];
		if(c >= 9 && c <= 16r7F || c == 0)
			s[o++] = c;
		else if(c >= 1 && c <= 8){
			if(i+c > nb)
				return (nil, "missing data in record");
			while(--c >= 0)
				s[o++] = int a[i++];
		}else if(c >= 16rC0 && c <= 16rFF){
			s[o] = ' ';
			s[o+1] = c & 16r7F;
			o += 2;
		}else{	# c >= 0x80 && c <= 16rBF
			v := int a[i++];
			m := ((c & 16r3F)<<5)|(v>>3);
			n := (v&7) + 3;
			if(m == 0 || m > o)
				return (nil, sys->sprint("data is corrupt: m=%d n=%d o=%d", m, n, o));
			for(; --n >= 0; o++)
				s[o] = s[o-m];
		}
	}
	return (s, nil);
}

Doc.textlength(doc: self ref Doc, a: array of byte): int
{
	nb := len a;
	if(!doc.iscompressed())
		return nb;
	o := 0;
	for(i := 0; i < nb;){
		c := int a[i++];
		if(c >= 9 && c <= 16r7F || c == 0)
			o++;
		else if(c >= 1 && c <= 8){
			if(i+c > nb)
				return -1;
			o += c;
			i += c;
		}else if(c >= 16rC0 && c <= 16rFF){
			o += 2;
		}else{	# c >= 0x80 && c <= 16rBF
			v := int a[i++];
			m := ((c & 16r3F)<<5)|(v>>3);
			n := (v&7) + 3;
			if(m == 0 || m > o)
				return -1;
			o += n;
		}
	}
	return o;
}

id2s(i: int): string
{
	if(i == 0)
		return "";
	return sys->sprint("%c%c%c%c", (i>>24)&16rFF, (i>>16)&16rFF, (i>>8)&16rFF, i&16rFF);
}

s2id(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;
}

DBInfo.new(name: string, attr: int, dtype: string, version: int, creator: string): ref DBInfo
{
	info := ref DBInfo;
	info.name = name;
	info.attr = attr;
	info.version = version;
	info.ctime = daytime->now();
	info.mtime = daytime->now();
	info.btime = 0;
	info.modno = 0;
	info.dtype = dtype;
	info.creator = creator;
	info.uidseed = 0;
	info.index = 0;
	return info;
}

Categories.new(labels: array of string): ref Categories
{
	c := ref Categories;
	c.renamed = 0;
	c.lastuid = 0;
	c.labels = array[16] of string;
	c.uids = array[] of {0 to 15 => 0};
	for(i := 0; i < len labels && i < 16; i++){
		c.labels[i] = labels[i];
		c.lastuid = 16r80 + i;
		c.uids[i] = c.lastuid;
	}
	return c;
}

Categories.unpack(a: array of byte): ref Categories
{
	if(len a < 16r114)
		return nil;		# doesn't match the structure
	c := ref Categories;
	c.renamed = get2(a);
	c.labels = array[16] of string;
	c.uids = array[16] of int;
	j := 2;
	for(i := 0; i < 16; i++){
		c.labels[i] = latin1(a[j:j+16], 0);
		j += 16;
		c.uids[i] = int a[16r102+i];
	}
	c.lastuid = int a[16r112];
	# one byte of padding is shown on p. 26, but
	# two more are invariably used in practice
	# before application specific data.
	if(len a > 16r116)
		c.appdata = a[16r116:];
	return c;
}

Categories.pack(c: self ref Categories): array of byte
{
	a := array[16r116 + len c.appdata] of byte;
	put2(a, c.renamed);
	j := 2;
	for(i := 0; i < 16; i++){
		puts(a[j:j+16], c.labels[i]);
		j += 16;
		a[16r102+i] = byte c.uids[i];
	}
	a[16r112] = byte c.lastuid;
	a[16r113] = byte 0;	# pad shown on p. 26
	a[16r114] = byte 0;	# extra two bytes of padding used in practice
	a[16r115] = byte 0;
	if(c.appdata != nil)
		a[16r116:] = c.appdata;
	return a;
}

Categories.mkidmap(c: self ref Categories): array of int
{
	a := array[256] of {* => 0};
	for(i := 0; i < len c.uids; i++)
		a[c.uids[i]] = i;
	return a;
}

#
# 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;
}

#
# map from Unicode to Palm name
#
filename(name: string): string
{
	s := "";
	for(i := 0; i < len name; i++){
		c := name[i];
		if(c == ' ')
			c = 16r00A0;	# unpaddable space
		else if(c == '/')
			c = 16r2215;	# division solidus
		s[len s] = c;
	}
	return s;
}

dbname(name: string): string
{
	s := "";
	for(i := 0; i < len name; i++){
		c := name[i];
		case c {
		0 =>			c = ' ';	# unlikely, but just in case
		16r2215 =>	c = '/';
		16r00A0 =>	c = ' ';
		}
		s[len s] = c;
	}
	return s;
}

#
# string conversion: can't use (string a) because
# the bytes are Latin1, not Unicode
#
gets(a: array of byte): string
{
	s := "";
	for(i := 0; i < len a; i++)
		s[len s] = int a[i];
	return s;
}

puts(a: array of byte, s: string)
{
	for(i := 0; i < len a-1 && i < len s; i++)
		a[i] = byte s[i];
	for(; i < len a; i++)
		a[i] = byte 0;
}

#
#  big-endian packing
#

get4(p: array of byte): int
{
	return (((((int p[0] << 8) | int p[1]) << 8) | int p[2]) << 8) | int p[3];
}

get3(p: array of byte): int
{
	return (((int p[0] << 8) | int p[1]) << 8) | int p[2];
}

get2(p: array of byte): int
{
	return (int p[0]<<8) | int p[1];
}

put4(p: array of byte, v: int)
{
	p[0] = byte (v>>24);
	p[1] = byte (v>>16);
	p[2] = byte (v>>8);
	p[3] = byte (v & 16rFF);
}

put3(p: array of byte, v: int)
{
	p[0] = byte (v>>16);
	p[1] = byte (v>>8);
	p[2] = byte (v & 16rFF);
}

put2(p: array of byte, v: int)
{
	p[0] = byte (v>>8);
	p[1] = byte (v & 16rFF);
}

#
# DL protocol argument wrapping, based on conventions
# extracted from include/Core/System/DLCommon.h in SDK 5
#
# tiny arguments
#	id: byte
#	size: byte	# excluding this header
#	data: byte[]
#
# small arguments
#	id: byte	# with 16r80 flag
#	pad: byte
#	size: byte[2]
#	data: byte[]
#
# long arguments
#	id: byte	# with 16r40 flag
#	pad: byte
#	size: byte[4]
#	data: byte[]

# wrapper format flag in request/response argument ID
ShortWrap: con 16r80;	# 2-byte count
LongWrap: con 16r40;	# 4-byte count

Eshort: con "response shorter than expected";

#
# set the system error string
#
e(s: string): string
{
	if(s != nil)
		sys->werrstr(s);
	return s;
}

argsize(args: array of (int, array of byte)): int
{
	totnb := 0;
	for(i := 0; i < len args; i++){
		(nil, a) := args[i];
		n := len a;
		if(n > 65535)
			totnb += 6;	# long wrap
		else if(n > 255)
			totnb += 4;	# short
		else
			totnb += 2;	# tiny
		totnb += n;
	}
	return totnb;
}

packargs(out: array of byte, args: array of (int, array of byte)): array of byte
{
	for(i := 0; i < len args; i++){
		(id, a) := args[i];
		n := len a;
		if(n > 65535){
			out[0] = byte (LongWrap|ShortWrap|id);
			out[1] = byte 0;
			put4(out[2:], n);
			out = out[6:];
		}else if(n > 255){
			out[0] = byte (ShortWrap|id);
			out[1] = byte 0;
			put2(out[2:], n);
			out = out[4:];
		}else{
			out[0] = byte id;
			out[1] = byte n;
			out = out[2:];
		}
		out[0:] = a;
		out = out[n:];
	}
	return out;
}

unpackargs(argc: int, reply: array of byte): (array of (int, array of byte), string)
{
	replies := array[argc] of (int, array of byte);
	o := 0;
	for(i := 0; i < len replies; i++){
		o = (o+1)&~1;	# each argument starts at even offset
		a := reply[o:];
		if(len a < 2)
			return (nil, e(Eshort));
		rid := int a[0];
		l: int;
		if(rid & LongWrap){
			if(len a < 6)
				return (nil, e(Eshort));
			l = get4(a[2:]);
			a = a[6:];
			o += 6;
		}else if(rid & ShortWrap){
			if(len a < 4)
				return (nil, e(Eshort));
			l = get2(a[2:]);
			a = a[4:];
			o += 4;
		}else{
			l = int a[1];
			a = a[2:];
			o += 2;
		}
		if(len a < l)
			return (nil, e(Eshort));
		replies[i] = (rid &~ 16rC0, a[0:l]);
		o += l;
	}
	return (replies, nil);
}