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