summaryrefslogtreecommitdiff
path: root/appl/lib/palmfile.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/lib/palmfile.b')
-rw-r--r--appl/lib/palmfile.b703
1 files changed, 703 insertions, 0 deletions
diff --git a/appl/lib/palmfile.b b/appl/lib/palmfile.b
new file mode 100644
index 00000000..b59bbd6f
--- /dev/null
+++ b/appl/lib/palmfile.b
@@ -0,0 +1,703 @@
+implement Palmfile;
+
+#
+# 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 "palmfile.m";
+
+
+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(): 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";
+ tzoff = daytime->local(0).tzoff;
+ return nil;
+}
+
+Eshort: con "file format error: too small";
+
+Pfile.open(name: string, mode: int): (ref Pfile, string)
+{
+ if(mode != Sys->OREAD)
+ return (nil, "invalid mode");
+ fd := sys->open(name, mode);
+ if(fd == nil)
+ return (nil, sys->sprint("%r"));
+ pf := mkpfile(name, mode);
+ (ok, d) := sys->fstat(fd);
+ if(ok < 0)
+ return (nil, sys->sprint("%r"));
+ length := int d.length;
+ if(length == 0)
+ return (nil, "empty file");
+
+ 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 := pf.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:]);
+ ip.appinfo = get4(p[52:]);
+ ip.sortinfo = get4(p[56:]);
+ if(ip.appinfo < 0 || ip.sortinfo < 0 || (ip.appinfo|ip.sortinfo)&1)
+ return (nil, "invalid header: bad offset");
+ ip.dtype = xs(get4(p[60:]));
+ ip.creator = xs(get4(p[64:]));
+ pf.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 & Fresource)
+ esize = Resourcehdrsize;
+
+ dataoffset := length;
+ pf.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 & 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;
+ pf.entries[i] = e;
+ }
+ if(laste != nil)
+ laste.size = length - laste.offset;
+ dataoffset = pf.entries[0].offset;
+ }else{
+ if(f.read(p, 2) != 2)
+ return (nil, Eshort); # discard placeholder bytes
+ }
+
+ n := 0;
+ if(ip.appinfo > 0){
+ n = ip.appinfo - int f.offset();
+ while(--n >= 0)
+ f.getb();
+ if(ip.sortinfo)
+ n = ip.sortinfo - ip.appinfo;
+ else
+ n = dataoffset - ip.appinfo;
+ pf.appinfo = array[n] of byte;
+ if(f.read(pf.appinfo, n) != n)
+ return (nil, Eshort);
+ }
+ if(ip.sortinfo > 0){
+ n = ip.sortinfo - int f.offset();
+ while(--n >= 0)
+ f.getb();
+ n = (dataoffset-ip.sortinfo)/2;
+ pf.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++)
+ pf.sortinfo[i] = get2(tmp[2*i:]);
+ }
+ pf.f = f; # safe to save open file reference
+ return (pf, nil);
+}
+
+Pfile.close(pf: self ref Pfile): int
+{
+ if(pf.f != nil){
+ pf.f.close();
+ pf.f = nil;
+ }
+ return 0;
+}
+
+Pfile.stat(pf: self ref Pfile): ref DBInfo
+{
+ return ref *pf.info;
+}
+
+Pfile.read(pf: self ref Pfile, i: int): (ref Record, string)
+{
+ if(i < 0 || i >= len pf.entries){
+ if(i == len pf.entries)
+ return (nil, nil); # treat as end-of-file
+ return (nil, "index out of range");
+ }
+ e := pf.entries[i];
+ r := ref Record;
+ r.index = i;
+ nb := e.size;
+ r.data = array[nb] of byte;
+ pf.f.seek(big e.offset, 0);
+ if(pf.f.read(r.data, nb) != nb)
+ return (nil, sys->sprint("%r"));
+ r.cat = e.attr & 16r0F;
+ r.attr = e.attr & 16rF0;
+ r.id = e.id;
+ r.name = e.name;
+ return (r, nil);
+}
+
+#Pfile.create(name: string, info: ref DBInfo): ref Pfile
+#{
+#}
+
+#Pfile.wstat(pf: self ref Pfile, ip: ref DBInfo): string
+#{
+# if(pf.mode != Sys->OWRITE)
+# return "not open for writing";
+# if((ip.attr & Fresource) != (pf.info.attr & Fresource))
+# return "cannot change file type";
+# # copy only a subset
+# pf.info.name = ip.name;
+# pf.info.attr = ip.attr;
+# pf.info.version = ip.version;
+# pf.info.ctime = ip.ctime;
+# pf.info.mtime = ip.mtime;
+# pf.info.btime = ip.btime;
+# pf.info.modno = ip.modno;
+# pf.info.dtype = ip.dtype;
+# pf.info.creator = ip.creator;
+# return nil;
+#}
+
+#Pfile.setappinfo(pf: self ref Pfile, data: array of byte): string
+#{
+# if(pf.mode != Sys->OWRITE)
+# return "not open for writing";
+# pf.appinfo = array[len data] of byte;
+# pf.appinfo[0:] = data;
+#}
+
+#Pfile.setsortinfo(pf: self ref Pfile, sort: array of int): string
+#{
+# if(pf.mode != Sys->OWRITE)
+# return "not open for writing";
+# pf.sortinfo = array[len sort] of int;
+# pf.sortinfo[0:] = sort;
+#}
+
+#
+# internal function to extend entry list if necessary, and return a
+# pointer to the next available slot
+#
+entryensure(pf: ref Pfile, i: int): ref Entry
+{
+ if(i < len pf.entries)
+ return pf.entries[i];
+ e := ref Entry(0, -1, 0, 0, 0);
+ n := len pf.entries;
+ if(n == 0)
+ n = 64;
+ else
+ n = (i+63) & ~63;
+ a := array[n] of ref Entry;
+ a[0:] = pf.entries;
+ a[i] = e;
+ pf.entries = a;
+ return e;
+}
+
+writefilehdr(pf: ref Pfile, mode: int, perm: int): string
+{
+ if(len pf.entries >= 64*1024)
+ return "too many records for Palm file"; # is there a way to extend it?
+
+ if((f := bufio->create(pf.fname, mode, perm)) == nil)
+ return sys->sprint("%r");
+
+ ip := pf.info;
+
+ esize := Datahdrsize;
+ if(ip.attr & Fresource)
+ esize = Resourcehdrsize;
+ offset := Dbhdrlen + esize*len pf.entries + 2;
+ offset += 2; # placeholder bytes or gap bytes
+ ip.appinfo = 0;
+ if(len pf.appinfo > 0){
+ ip.appinfo = offset;
+ offset += len pf.appinfo;
+ }
+ ip.sortinfo = 0;
+ if(len pf.sortinfo > 0){
+ ip.sortinfo = offset;
+ offset += 2*len pf.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:], ip.appinfo);
+ put4(p[56:], ip.sortinfo);
+ put4(p[60:], sx(ip.dtype));
+ put4(p[64:], sx(ip.creator));
+ put4(p[68:], pf.uidseed);
+ put4(p[72:], 0); # next record list ID
+ put2(p[76:], len pf.entries);
+
+ if(f.write(p, Dbhdrlen) != Dbhdrlen)
+ return ewrite(f);
+ if(len pf.entries > 0){
+ for(i := 0; i < len pf.entries; i++) {
+ e := pf.entries[i];
+ e.offset = offset;
+ if(ip.attr & 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(ip.appinfo != 0){
+ if(f.write(pf.appinfo, len pf.appinfo) != len pf.appinfo)
+ return ewrite(f);
+ }
+
+ if(ip.sortinfo != 0){
+ tmp := array[2*len pf.sortinfo] of byte;
+ for(i := 0; i < len pf.sortinfo; i++)
+ put2(tmp[2*i:], pf.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;
+}
+
+Doc.open(file: ref Pfile): (ref Doc, string)
+{
+ if(file.info.dtype != "TEXt" || file.info.creator != "REAd")
+ return (nil, "not a Doc file: wrong type or creator");
+ (r, err) := file.read(0);
+ if(r == nil){
+ if(err == nil)
+ err = "no directory record";
+ return (nil, sys->sprint("not a valid Doc file: %s", err));
+ }
+ a := r.data;
+ if(len a < 16)
+ return (nil, sys->sprint("not a valid Doc file: bad length: %d", len a));
+ maxrec := len file.entries-1;
+ d := ref Doc;
+ d.file = file;
+ d.version = get2(a);
+ 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)
+{
+ (r, err) := doc.file.read(index+1);
+ if(r == nil)
+ return (nil, err);
+ (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;
+}
+
+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 Pfile
+{
+ pf := ref Pfile;
+ pf.mode = mode;
+ pf.fname = name;
+ pf.appinfo = array[0] of byte; # making it non-nil saves having to check each access
+ pf.sortinfo = array[0] of int;
+ pf.uidseed = 0;
+ pf.info = DBInfo.new(name, 0, nil, 0, nil);
+ return pf;
+}
+
+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.appinfo = 0;
+ info.sortinfo = 0;
+ info.dtype = dtype;
+ info.creator = creator;
+ info.uidseed = 0;
+ info.index = 0;
+ info.more = 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);
+}