xref: /inferno-os/appl/lib/palmfile.b (revision 37da2899f40661e3e9631e497da8dc59b971cbd0)
1implement Palmfile;
2
3#
4# Copyright © 2001-2002 Vita Nuova Holdings Limited.  All rights reserved.
5#
6# Based on ``Palm® File Format Specification'', Document Number 3008-004, 1 May 2001, by Palm Inc.
7# Doc compression based on description by Paul Lucas, 18 August 1998
8#
9
10include "sys.m";
11	sys: Sys;
12
13include "daytime.m";
14	daytime: Daytime;
15
16include "bufio.m";
17	bufio: Bufio;
18	Iobuf: import bufio;
19
20include "palmfile.m";
21
22
23Dbhdrlen: con 72+6;
24Datahdrsize: con 4+1+3;
25Resourcehdrsize: con 4+2+4;
26
27# Exact value of "Jan 1, 1970 0:00:00 GMT" - "Jan 1, 1904 0:00:00 GMT"
28Epochdelta: con 2082844800;
29tzoff := 0;
30
31init(): string
32{
33	sys = load Sys Sys->PATH;
34	bufio = load Bufio Bufio->PATH;
35	daytime = load Daytime Daytime->PATH;
36	if(bufio == nil || daytime == nil)
37		return "can't load required module";
38	tzoff = daytime->local(0).tzoff;
39	return nil;
40}
41
42Eshort: con "file format error: too small";
43
44Pfile.open(name: string, mode: int): (ref Pfile, string)
45{
46	if(mode != Sys->OREAD)
47		return (nil, "invalid mode");
48	fd := sys->open(name, mode);
49	if(fd == nil)
50		return (nil, sys->sprint("%r"));
51	pf := mkpfile(name, mode);
52	(ok, d) := sys->fstat(fd);
53	if(ok < 0)
54		return (nil, sys->sprint("%r"));
55	length := int d.length;
56	if(length == 0)
57		return (nil, "empty file");
58
59	f := bufio->fopen(fd, mode);	# automatically closed if open fails
60
61	p := array[Dbhdrlen] of byte;
62	if(f.read(p, Dbhdrlen) != Dbhdrlen)
63		return (nil, "invalid file header: too short");
64
65	ip := pf.info;
66	ip.name = gets(p[0:32]);
67	ip.attr = get2(p[32:]);
68	ip.version = get2(p[34:]);
69	ip.ctime = pilot2epoch(get4(p[36:]));
70	ip.mtime = pilot2epoch(get4(p[40:]));
71	ip.btime = pilot2epoch(get4(p[44:]));
72	ip.modno = get4(p[48:]);
73	ip.appinfo = get4(p[52:]);
74	ip.sortinfo = get4(p[56:]);
75	if(ip.appinfo < 0 || ip.sortinfo < 0 || (ip.appinfo|ip.sortinfo)&1)
76		return (nil, "invalid header: bad offset");
77	ip.dtype = xs(get4(p[60:]));
78	ip.creator = xs(get4(p[64:]));
79	pf.uidseed = ip.uidseed = get4(p[68:]);
80
81	if(get4(p[72:]) != 0)
82		return (nil, "chained headers not supported");	# Palm says to reject such files
83	nrec := get2(p[76:]);
84	if(nrec < 0)
85		return (nil, sys->sprint("invalid header: bad record count: %d", nrec));
86
87	esize := Datahdrsize;
88	if(ip.attr & Fresource)
89		esize = Resourcehdrsize;
90
91	dataoffset := length;
92	pf.entries = array[nrec] of ref Entry;
93	if(nrec > 0){
94		laste: ref Entry;
95		buf := array[esize] of byte;
96		for(i := 0; i < nrec; i++){
97			if(f.read(buf, len buf) != len buf)
98				return (nil, Eshort);
99			e := ref Entry;
100			if(ip.attr & Fresource){
101				# resource entry: type[4], id[2], offset[4]
102				e.name = get4(buf);
103				e.id = get2(buf[4:]);
104				e.offset = get4(buf[6:]);
105				e.attr = 0;
106			}else{
107				# record entry: offset[4], attr[1], id[3]
108				e.offset = get4(buf);
109				e.attr = int buf[4];
110				e.id = get3(buf[5:]);
111				e.name = 0;
112			}
113			if(laste != nil)
114				laste.size = e.offset - laste.offset;
115			laste = e;
116			pf.entries[i] = e;
117		}
118		if(laste != nil)
119			laste.size = length - laste.offset;
120		dataoffset = pf.entries[0].offset;
121	}else{
122		if(f.read(p, 2) != 2)
123			return (nil, Eshort);	# discard placeholder bytes
124	}
125
126	n := 0;
127	if(ip.appinfo > 0){
128		n = ip.appinfo - int f.offset();
129		while(--n >= 0)
130			f.getb();
131		if(ip.sortinfo)
132			n = ip.sortinfo - ip.appinfo;
133		else
134			n = dataoffset - ip.appinfo;
135		pf.appinfo = array[n] of byte;
136		if(f.read(pf.appinfo, n) != n)
137			return (nil, Eshort);
138	}
139	if(ip.sortinfo > 0){
140		n = ip.sortinfo - int f.offset();
141		while(--n >= 0)
142			f.getb();
143		n = (dataoffset-ip.sortinfo)/2;
144		pf.sortinfo = array[n] of int;
145		tmp := array[2*n] of byte;
146		if(f.read(tmp, len tmp) != len tmp)
147			return (nil, Eshort);
148		for(i := 0; i < n; i++)
149			pf.sortinfo[i] = get2(tmp[2*i:]);
150	}
151	pf.f = f;	# safe to save open file reference
152	return (pf, nil);
153}
154
155Pfile.close(pf: self ref Pfile): int
156{
157	if(pf.f != nil){
158		pf.f.close();
159		pf.f = nil;
160	}
161	return 0;
162}
163
164Pfile.stat(pf: self ref Pfile): ref DBInfo
165{
166	return ref *pf.info;
167}
168
169Pfile.read(pf: self ref Pfile, i: int): (ref Record, string)
170{
171	if(i < 0 || i >= len pf.entries){
172		if(i == len pf.entries)
173			return (nil, nil);	# treat as end-of-file
174		return (nil, "index out of range");
175	}
176	e := pf.entries[i];
177	r := ref Record;
178	r.index = i;
179	nb := e.size;
180	r.data = array[nb] of byte;
181	pf.f.seek(big e.offset, 0);
182	if(pf.f.read(r.data, nb) != nb)
183		return (nil, sys->sprint("%r"));
184	r.cat = e.attr & 16r0F;
185	r.attr = e.attr & 16rF0;
186	r.id = e.id;
187	r.name = e.name;
188	return (r, nil);
189}
190
191#Pfile.create(name: string, info: ref DBInfo): ref Pfile
192#{
193#}
194
195#Pfile.wstat(pf: self ref Pfile, ip: ref DBInfo): string
196#{
197#	if(pf.mode != Sys->OWRITE)
198#		return "not open for writing";
199#	if((ip.attr & Fresource) != (pf.info.attr & Fresource))
200#		return "cannot change file type";
201#	# copy only a subset
202#	pf.info.name = ip.name;
203#	pf.info.attr = ip.attr;
204#	pf.info.version = ip.version;
205#	pf.info.ctime = ip.ctime;
206#	pf.info.mtime = ip.mtime;
207#	pf.info.btime = ip.btime;
208#	pf.info.modno = ip.modno;
209#	pf.info.dtype = ip.dtype;
210#	pf.info.creator = ip.creator;
211#	return nil;
212#}
213
214#Pfile.setappinfo(pf: self ref Pfile, data: array of byte): string
215#{
216#	if(pf.mode != Sys->OWRITE)
217#		return "not open for writing";
218#	pf.appinfo = array[len data] of byte;
219#	pf.appinfo[0:] = data;
220#}
221
222#Pfile.setsortinfo(pf: self ref Pfile, sort: array of int): string
223#{
224#	if(pf.mode != Sys->OWRITE)
225#		return "not open for writing";
226#	pf.sortinfo = array[len sort] of int;
227#	pf.sortinfo[0:] = sort;
228#}
229
230#
231# internal function to extend entry list if necessary, and return a
232# pointer to the next available slot
233#
234entryensure(pf: ref Pfile, i: int): ref Entry
235{
236	if(i < len pf.entries)
237		return pf.entries[i];
238	e := ref Entry(0, -1, 0, 0, 0);
239	n := len pf.entries;
240	if(n == 0)
241		n = 64;
242	else
243		n = (i+63) & ~63;
244	a := array[n] of ref Entry;
245	a[0:] = pf.entries;
246	a[i] = e;
247	pf.entries = a;
248	return e;
249}
250
251writefilehdr(pf: ref Pfile, mode: int, perm: int): string
252{
253	if(len pf.entries >= 64*1024)
254		return "too many records for Palm file";	# is there a way to extend it?
255
256	if((f := bufio->create(pf.fname, mode, perm)) == nil)
257		return sys->sprint("%r");
258
259	ip := pf.info;
260
261	esize := Datahdrsize;
262	if(ip.attr & Fresource)
263		esize = Resourcehdrsize;
264	offset := Dbhdrlen + esize*len pf.entries + 2;
265	offset += 2;	# placeholder bytes or gap bytes
266	ip.appinfo = 0;
267	if(len pf.appinfo > 0){
268		ip.appinfo = offset;
269		offset += len pf.appinfo;
270	}
271	ip.sortinfo = 0;
272	if(len pf.sortinfo > 0){
273		ip.sortinfo = offset;
274		offset += 2*len pf.sortinfo;	# 2-byte entries
275	}
276	p := array[Dbhdrlen] of byte;	# bigger than any entry as well
277	puts(p[0:32], ip.name);
278	put2(p[32:], ip.attr);
279	put2(p[34:], ip.version);
280	put4(p[36:], epoch2pilot(ip.ctime));
281	put4(p[40:], epoch2pilot(ip.mtime));
282	put4(p[44:], epoch2pilot(ip.btime));
283	put4(p[48:], ip.modno);
284	put4(p[52:], ip.appinfo);
285	put4(p[56:], ip.sortinfo);
286	put4(p[60:], sx(ip.dtype));
287	put4(p[64:], sx(ip.creator));
288	put4(p[68:], pf.uidseed);
289	put4(p[72:], 0);		# next record list ID
290	put2(p[76:], len pf.entries);
291
292	if(f.write(p, Dbhdrlen) != Dbhdrlen)
293		return ewrite(f);
294	if(len pf.entries > 0){
295		for(i := 0; i < len pf.entries; i++) {
296			e := pf.entries[i];
297			e.offset = offset;
298			if(ip.attr & Fresource) {
299				put4(p, e.name);
300				put2(p[4:], e.id);
301				put4(p[6:], e.offset);
302			} else {
303				put4(p, e.offset);
304				p[4] = byte e.attr;
305				put3(p[5:], e.id);
306			}
307			if(f.write(p, esize) != esize)
308				return ewrite(f);
309			offset += e.size;
310		}
311	}
312
313	f.putb(byte 0);	# placeholder bytes (figure 1.4) or gap bytes (p. 15)
314	f.putb(byte 0);
315
316	if(ip.appinfo != 0){
317		if(f.write(pf.appinfo, len pf.appinfo) != len pf.appinfo)
318			return ewrite(f);
319	}
320
321	if(ip.sortinfo != 0){
322		tmp := array[2*len pf.sortinfo] of byte;
323		for(i := 0; i < len pf.sortinfo; i++)
324			put2(tmp[2*i:], pf.sortinfo[i]);
325		if(f.write(tmp, len tmp) != len tmp)
326			return ewrite(f);
327	}
328
329	if(f.flush() != 0)
330		return ewrite(f);
331
332	return nil;
333}
334
335ewrite(f: ref Iobuf): string
336{
337	e := sys->sprint("write error: %r");
338	f.close();
339	return e;
340}
341
342Doc.open(file: ref Pfile): (ref Doc, string)
343{
344	if(file.info.dtype != "TEXt" || file.info.creator != "REAd")
345		return (nil, "not a Doc file: wrong type or creator");
346	(r, err) := file.read(0);
347	if(r == nil){
348		if(err == nil)
349			err = "no directory record";
350		return (nil, sys->sprint("not a valid Doc file: %s", err));
351	}
352	a := r.data;
353	if(len a < 16)
354		return (nil, sys->sprint("not a valid Doc file: bad length: %d", len a));
355	maxrec := len file.entries-1;
356	d := ref Doc;
357	d.file = file;
358	d.version = get2(a);
359	if(d.version != 1 && d.version != 2)
360		err = "unknown Docfile version";
361	# a[2:] is spare
362	d.length = get4(a[4:]);
363	d.nrec = get2(a[8:]);
364	if(maxrec >= 0 && d.nrec > maxrec){
365		d.nrec = maxrec;
366		err = "invalid record count";
367	}
368	d.recsize = get2(a[10:]);
369	d.position = get4(a[12:]);
370	return (d, sys->sprint("unexpected Doc file format: %s", err));
371}
372
373Doc.iscompressed(d: self ref Doc): int
374{
375	return (d.version&7) == 2;		# high-order bits are sometimes used, ignore them
376}
377
378Doc.read(doc: self ref Doc, index: int): (string, string)
379{
380	(r, err) := doc.file.read(index+1);
381	if(r == nil)
382		return (nil, err);
383	(s, serr) := doc.unpacktext(r.data);
384	if(s == nil)
385		return (nil, serr);
386	return (s, nil);
387}
388
389Doc.unpacktext(doc: self ref Doc, a: array of byte): (string, string)
390{
391	nb := len a;
392	s: string;
393	if(!doc.iscompressed()){
394		for(i := 0; i < nb; i++)
395			s[len s] = int a[i];	# assumes Latin-1
396		return (s, nil);
397	}
398	o := 0;
399	for(i := 0; i < nb;){
400		c := int a[i++];
401		if(c >= 9 && c <= 16r7F || c == 0)
402			s[o++] = c;
403		else if(c >= 1 && c <= 8){
404			if(i+c > nb)
405				return (nil, "missing data in record");
406			while(--c >= 0)
407				s[o++] = int a[i++];
408		}else if(c >= 16rC0 && c <= 16rFF){
409			s[o] = ' ';
410			s[o+1] = c & 16r7F;
411			o += 2;
412		}else{	# c >= 0x80 && c <= 16rBF
413			v := int a[i++];
414			m := ((c & 16r3F)<<5)|(v>>3);
415			n := (v&7) + 3;
416			if(m == 0 || m > o)
417				return (nil, sys->sprint("data is corrupt: m=%d n=%d o=%d", m, n, o));
418			for(; --n >= 0; o++)
419				s[o] = s[o-m];
420		}
421	}
422	return (s, nil);
423}
424
425Doc.textlength(doc: self ref Doc, a: array of byte): int
426{
427	nb := len a;
428	if(!doc.iscompressed())
429		return nb;
430	o := 0;
431	for(i := 0; i < nb;){
432		c := int a[i++];
433		if(c >= 9 && c <= 16r7F || c == 0)
434			o++;
435		else if(c >= 1 && c <= 8){
436			if(i+c > nb)
437				return -1;
438			o += c;
439			i += c;
440		}else if(c >= 16rC0 && c <= 16rFF){
441			o += 2;
442		}else{	# c >= 0x80 && c <= 16rBF
443			v := int a[i++];
444			m := ((c & 16r3F)<<5)|(v>>3);
445			n := (v&7) + 3;
446			if(m == 0 || m > o)
447				return -1;
448			o += n;
449		}
450	}
451	return o;
452}
453
454xs(i: int): string
455{
456	if(i == 0)
457		return "";
458	if(i & int 16r80808080)
459		return sys->sprint("%8.8ux", i);
460	return sys->sprint("%c%c%c%c", (i>>24)&16rFF, (i>>16)&16rFF, (i>>8)&16rFF, i&16rFF);
461}
462
463sx(s: string): int
464{
465	n := 0;
466	for(i := 0; i < 4; i++){
467		c := 0;
468		if(i < len s)
469			c = s[i] & 16rFF;
470		n = (n<<8) | c;
471	}
472	return n;
473}
474
475mkpfile(name: string, mode: int): ref Pfile
476{
477	pf := ref Pfile;
478	pf.mode = mode;
479	pf.fname = name;
480	pf.appinfo = array[0] of byte;		# making it non-nil saves having to check each access
481	pf.sortinfo = array[0] of int;
482	pf.uidseed = 0;
483	pf.info = DBInfo.new(name, 0, nil, 0, nil);
484	return pf;
485}
486
487DBInfo.new(name: string, attr: int, dtype: string, version: int, creator: string): ref DBInfo
488{
489	info := ref DBInfo;
490	info.name = name;
491	info.attr = attr;
492	info.version = version;
493	info.ctime = daytime->now();
494	info.mtime = daytime->now();
495	info.btime = 0;
496	info.modno = 0;
497	info.appinfo = 0;
498	info.sortinfo = 0;
499	info.dtype = dtype;
500	info.creator = creator;
501	info.uidseed = 0;
502	info.index = 0;
503	info.more = 0;
504	return info;
505}
506
507Categories.new(labels: array of string): ref Categories
508{
509	c := ref Categories;
510	c.renamed = 0;
511	c.lastuid = 0;
512	c.labels = array[16] of string;
513	c.uids = array[] of {0 to 15 => 0};
514	for(i := 0; i < len labels && i < 16; i++){
515		c.labels[i] = labels[i];
516		c.lastuid = 16r80 + i;
517		c.uids[i] = c.lastuid;
518	}
519	return c;
520}
521
522Categories.unpack(a: array of byte): ref Categories
523{
524	if(len a < 16r114)
525		return nil;		# doesn't match the structure
526	c := ref Categories;
527	c.renamed = get2(a);
528	c.labels = array[16] of string;
529	c.uids = array[16] of int;
530	j := 2;
531	for(i := 0; i < 16; i++){
532		c.labels[i] = latin1(a[j:j+16], 0);
533		j += 16;
534		c.uids[i] = int a[16r102+i];
535	}
536	c.lastuid = int a[16r112];
537	# one byte of padding is shown on p. 26, but
538	# two more are invariably used in practice
539	# before application specific data.
540	if(len a > 16r116)
541		c.appdata = a[16r116:];
542	return c;
543}
544
545Categories.pack(c: self ref Categories): array of byte
546{
547	a := array[16r116 + len c.appdata] of byte;
548	put2(a, c.renamed);
549	j := 2;
550	for(i := 0; i < 16; i++){
551		puts(a[j:j+16], c.labels[i]);
552		j += 16;
553		a[16r102+i] = byte c.uids[i];
554	}
555	a[16r112] = byte c.lastuid;
556	a[16r113] = byte 0;	# pad shown on p. 26
557	a[16r114] = byte 0;	# extra two bytes of padding used in practice
558	a[16r115] = byte 0;
559	if(c.appdata != nil)
560		a[16r116:] = c.appdata;
561	return a;
562}
563
564Categories.mkidmap(c: self ref Categories): array of int
565{
566	a := array[256] of {* => 0};
567	for(i := 0; i < len c.uids; i++)
568		a[c.uids[i]] = i;
569	return a;
570}
571
572#
573# because PalmOS treats all times as local times, and doesn't associate
574# them with time zones, we'll convert using local time on Plan 9 and Inferno
575#
576
577pilot2epoch(t: int): int
578{
579	if(t == 0)
580		return 0;	# we'll assume it's not set
581	return t - Epochdelta + tzoff;
582}
583
584epoch2pilot(t: int): int
585{
586	if(t == 0)
587		return t;
588	return t - tzoff + Epochdelta;
589}
590
591#
592# map Palm name to string, assuming iso-8859-1,
593# but remap space and /
594#
595latin1(a: array of byte, remap: int): string
596{
597	s := "";
598	for(i := 0; i < len a; i++){
599		c := int a[i];
600		if(c == 0)
601			break;
602		if(remap){
603			if(c == ' ')
604				c = 16r00A0;	# unpaddable space
605			else if(c == '/')
606				c = 16r2215;	# division /
607		}
608		s[len s] = c;
609	}
610	return s;
611}
612
613#
614# map from Unicode to Palm name
615#
616filename(name: string): string
617{
618	s := "";
619	for(i := 0; i < len name; i++){
620		c := name[i];
621		if(c == ' ')
622			c = 16r00A0;	# unpaddable space
623		else if(c == '/')
624			c = 16r2215;	# division solidus
625		s[len s] = c;
626	}
627	return s;
628}
629
630dbname(name: string): string
631{
632	s := "";
633	for(i := 0; i < len name; i++){
634		c := name[i];
635		case c {
636		0 =>			c = ' ';	# unlikely, but just in case
637		16r2215 =>	c = '/';
638		16r00A0 =>	c = ' ';
639		}
640		s[len s] = c;
641	}
642	return s;
643}
644
645#
646# string conversion: can't use (string a) because
647# the bytes are Latin1, not Unicode
648#
649gets(a: array of byte): string
650{
651	s := "";
652	for(i := 0; i < len a; i++)
653		s[len s] = int a[i];
654	return s;
655}
656
657puts(a: array of byte, s: string)
658{
659	for(i := 0; i < len a-1 && i < len s; i++)
660		a[i] = byte s[i];
661	for(; i < len a; i++)
662		a[i] = byte 0;
663}
664
665#
666#  big-endian packing
667#
668
669get4(p: array of byte): int
670{
671	return (((((int p[0] << 8) | int p[1]) << 8) | int p[2]) << 8) | int p[3];
672}
673
674get3(p: array of byte): int
675{
676	return (((int p[0] << 8) | int p[1]) << 8) | int p[2];
677}
678
679get2(p: array of byte): int
680{
681	return (int p[0]<<8) | int p[1];
682}
683
684put4(p: array of byte, v: int)
685{
686	p[0] = byte (v>>24);
687	p[1] = byte (v>>16);
688	p[2] = byte (v>>8);
689	p[3] = byte (v & 16rFF);
690}
691
692put3(p: array of byte, v: int)
693{
694	p[0] = byte (v>>16);
695	p[1] = byte (v>>8);
696	p[2] = byte (v & 16rFF);
697}
698
699put2(p: array of byte, v: int)
700{
701	p[0] = byte (v>>8);
702	p[1] = byte (v & 16rFF);
703}
704