implement Palmdb, Desklink; # # Palm Desk Link Protocol (DLP) # # Copyright © 2003 Vita Nuova Holdings Limited. All rights reserved. # # Request and response formats were extracted from # include/Core/System/DLCommon.h in the PalmOS SDK-5 # include "sys.m"; sys: Sys; include "daytime.m"; daytime: Daytime; Tm: import daytime; include "palm.m"; palm: Palm; DBInfo, Record, Resource, id2s, s2id, get2, put2, get4, put4, gets, argsize, packargs, unpackargs: import palm; include "timers.m"; include "desklink.m"; Maxrecbytes: con 16rFFFF; # operations defined by Palm T_ReadUserInfo, T_WriteUserInfo, T_ReadSysInfo, T_GetSysDateTime, T_SetSysDateTime, T_ReadStorageInfo, T_ReadDBList, T_OpenDB, T_CreateDB, T_CloseDB, T_DeleteDB, T_ReadAppBlock, T_WriteAppBlock, T_ReadSortBlock, T_WriteSortBlock, T_ReadNextModifiedRec, T_ReadRecord, T_WriteRecord, T_DeleteRecord, T_ReadResource, T_WriteResource, T_DeleteResource, T_CleanUpDatabase, T_ResetSyncFlags, T_CallApplication, T_ResetSystem, T_AddSyncLogEntry, T_ReadOpenDBInfo, T_MoveCategory, T_ProcessRPC, T_OpenConduit, T_EndOfSync, T_ResetDBIndex, T_ReadRecordIDList, # DLP 1.1 functions T_ReadNextRecInCategory, T_ReadNextModifiedRecInCategory, T_ReadAppPreference, T_WriteAppPreference, T_ReadNetSyncInfo, T_WriteNetSyncInfo, T_ReadFeature, # DLP 1.2 functions T_FindDB, T_SetDBInfo, # DLP 1.3 functions T_LoopBackTest, T_ExpSlotEnumerate, T_ExpCardPresent, T_ExpCardInfo: con 16r10+iota; # then there's a group of VFS requests that we don't currently use Response: con 16r80; Maxname: con 32; A1, A2: con Palm->ArgIDbase+iota; # argument IDs have request-specific interpretation (most have only one ID) Timeout: con 30; # seconds time out used by Palm's headers srvfd: ref Sys->FD; selfdb: Palmdb; errorlist := array [] of { "no error", "general Pilot system error", "unknown request", "out of dynamic memory on device", "invalid parameter", "not found", "no open databases", "database already open", "too many open databases", "database already exists", "cannot open database", "record previously deleted", "record busy", "operation not supported", "unexpected error (ErrUnused1)", "read only object", "not enough space", "size limit exceeded", "sync cancelled", "bad arg wrapper", "argument missing", "bad argument size", }; Eshort: con "desklink protocol: response too short"; debug := 0; connect(srvfile: string): (Palmdb, string) { sys = load Sys Sys->PATH; daytime = load Daytime Daytime->PATH; if(daytime == nil) return (nil, sys->sprint("can't load %s: %r", Daytime->PATH)); srvfd = sys->open(srvfile, Sys->ORDWR); if(srvfd == nil) return (nil, sys->sprint("can't open %s: %r", srvfile)); selfdb = load Palmdb "$self"; if(selfdb == nil) return (nil, sys->sprint("can't load self as Palmdb: %r")); return (selfdb, nil); } hangup(): int { srvfd = nil; return 0; } # # set the system error string # e(s: string): string { if(s != nil){ s = "palm: "+s; sys->werrstr(s); } return s; } # # sent before each conduit is opened by the desktop, # apparently to detect a pending cancel request (on the device) # OpenConduit(): int { return nexec(T_OpenConduit, A1, nil); } # # end of sync on desktop # EndOfSync(status: int): int { req := array[2] of byte; put2(req, status); return nexec(T_EndOfSync, A1, req); } ReadSysInfo(): ref SysInfo { if((reply := dexec(T_ReadSysInfo, A1, nil, 14)) == nil) return nil; s := ref SysInfo; s.romversion = get4(reply); s.locale = get4(reply[4:]); l := int reply[9]; # should be at most 4 apparently? s.product = gets(reply[10:10+l]); return s; } ReadSysInfoVer(): (int, int, int) { req := array[4] of byte; put2(req, 1); # major version put2(req, 2); # minor version if((reply := dexec(T_ReadSysInfo, A2, req, 12)) == nil) return (0, 0, 0); return (get4(reply), get4(reply[4:]), get4(reply[8:])); } ReadUserInfo(): ref User { if((reply := dexec(T_ReadUserInfo, 0, nil, 30)) == nil) return nil; u := ref User; u.userid = get4(reply); u.viewerid = get4(reply[4:]); u.lastsyncpc = get4(reply[8:]); u.succsynctime = getdate(reply[12:]); u.lastsynctime = getdate(reply[20:]); userlen := int reply[28]; pwlen := int reply[29]; u.username = gets(reply[30:30+userlen]); u.password = array[pwlen] of byte; u.password[0:] = reply[30+userlen:30+userlen+pwlen]; return u; } WriteUserInfo(u: ref User, flags: int): int { req := array[22+Maxname] of byte; put4(req, u.userid); put4(req[4:], u.viewerid); put4(req[8:], u.lastsyncpc); putdate(req[12:], u.lastsynctime); req[20] = byte flags; l := puts(req[22:], u.username); req[21] = byte l; return nexec(T_WriteUserInfo, A1, req[0:22+l]); } GetSysDateTime(): int { if((reply := dexec(T_GetSysDateTime, A1, nil, 8)) == nil) return -1; return getdate(reply); } SetSysDateTime(time: int): int { return nexec(T_SetSysDateTime, A1, putdate(array[8] of byte, time)); } ReadStorageInfo(cardno: int): (array of ref CardInfo, int, string) { req := array[2] of byte; req[0] = byte cardno; req[1] = byte 0; (reply, err) := rexec(T_ReadStorageInfo, A1, req, 30); if(reply == nil) return (nil, 0, err); nc := int reply[3]; if(nc <= 0) return (nil, 0, nil); more := int reply[1] != 0; a := array[nc] of ref CardInfo; p := 4; for(i:=0; i len a) size = len a; info.name = gets(a[44:size]); return (info, size); } ReadDBList(cardno: int, flags: int, start: int): (array of ref DBInfo, int, string) { req := array[4] of byte; req[0] = byte (flags | DBListMultiple); req[1] = byte cardno; put2(req[2:], start); (reply, err) := rexec(T_ReadDBList, A1, req, 48); if(reply == nil || int reply[3] == 0) return (nil, 0, err); # lastindex[2] flags[1] actcount[1] # flags is 16r80 => more to list more := (reply[2] & byte 16r80) != byte 0; dbs := array[int reply[3]] of ref DBInfo; #sys->print("ndb=%d more=%d lastindex=#%4.4ux\n", len dbs, more, get2(reply)); a := reply[4:]; for(i := 0; i < len dbs; i++){ (db, n) := unpackdbinfo(a); dbs[i] = db; a = a[n:]; } return (dbs, more, nil); } matchdb(cardno: int, flag: int, start: int, dbname: string, dtype: string, creator: string): (ref DBInfo, int) { for(;;){ (dbs, more, err) := ReadDBList(cardno, flag, start); if(dbs == nil) break; for(i := 0; i < len dbs; i++){ info := dbs[i]; if((dbname == nil || info.name == dbname) && (dtype == nil || info.dtype == dtype) && (creator == nil || info.creator == creator)) return (info, info.index); start = info.index+1; } } return (nil, 0); } FindDBInfo(cardno: int, start: int, dbname: string, dtype: string, creator: string): ref DBInfo { if(start < 16r1000) { (info, i) := matchdb(cardno, 16r80, start, dbname, dtype, creator); if(info != nil) return info; } (info, i) := matchdb(cardno, 16r40, start&~16r1000, dbname, dtype, creator); if(info != nil) info.index |= 16r1000; return info; } DeleteDB(name: string): int { (cardno, dbname) := parsedb(name); req := array[2+Maxname] of byte; req[0] = byte cardno; req[1] = byte 0; n := puts(req[2:], dbname); return nexec(T_DeleteDB, A1, req[0:2+n]); } ResetSystem(): int { return nexec(T_ResetSystem, 0, nil); } CloseDB_All(): int { return nexec(T_CloseDB, A2, nil); } AddSyncLogEntry(entry: string): int { req := array[256] of byte; n := puts(req, entry); return nexec(T_AddSyncLogEntry, A1, req[0:n]); } # # this implements a Palmdb->DB directly accessed using the desklink protocol # init(m: Palm): string { palm = m; return nil; } # # syntax is [cardno/]dbname # where cardno defaults to 0 # parsedb(name: string): (int, string) { (nf, flds) := sys->tokenize(name, "/"); if(nf > 1) return (int hd flds, hd tl flds); return (0, name); } DB.open(name: string, mode: int): (ref DB, string) { (cardno, dbname) := parsedb(name); req := array[2+Maxname] of byte; req[0] = byte cardno; req[1] = byte mode; n := puts(req[2:], dbname); (reply, err) := rexec(T_OpenDB, A1, req[0:2+n], 1); if(reply == nil) return (nil, err); db := ref DB; db.x = int reply[0]; inf := db.stat(); if(inf == nil) return (nil, sys->sprint("can't get DBInfo: %r")); db.attr = inf.attr; # mainly need to know whether it's Fresource or not return (db, nil); } DB.create(name: string, nil: int, nil: int, inf: ref DBInfo): (ref DB, string) { (cardno, dbname) := parsedb(name); req := array[14+Maxname] of byte; put4(req, s2id(inf.creator)); put4(req[4:], s2id(inf.dtype)); req[8] = byte cardno; req[9] = byte 0; put2(req[10:], inf.attr); put2(req[12:], inf.version); n := puts(req[14:], dbname); (reply, err) := rexec(T_CreateDB, A1, req[0:14+n], 1); if(reply == nil) return (nil, err); db := ref DB; db.x = int reply[0]; db.attr = inf.attr; return (db, nil); } DB.stat(db: self ref DB): ref DBInfo { (reply, err) := rexec(T_FindDB, A2, array[] of {byte 16r80, byte db.x}, 54); if(err != nil) return nil; return unpackdbinfo(reply[10:]).t0; } DB.wstat(db: self ref DB, inf: ref DBInfo, flags: int) { # TO DO } DB.close(db: self ref DB): string { return rexec(T_CloseDB, A1, array[] of {byte db.x}, 0).t1; } DB.records(db: self ref DB): ref PDB { if(db.attr & Palm->Fresource){ sys->werrstr("not a database file"); return nil; } return ref PDB(db); } DB.resources(db: self ref DB): ref PRC { if((db.attr & Palm->Fresource) == 0){ sys->werrstr("not a resource file"); return nil; } return ref PRC(db); } DB.readidlist(db: self ref DB, sort: int): array of int { req := array[6] of byte; req[0] = byte db.x; if(sort) req[1] = byte 16r80; else req[1] = byte 0; put2(req[2:], 0); put2(req[4:], -1); p := dexec(T_ReadRecordIDList, A1, req, 2); if(p == nil) return nil; ret := get2(p); ids := array[ret] of int; p = p[8:]; for (i := 0; i < ret; p = p[4:]) ids[i++] = get4(p); return ids; } DB.nentries(db: self ref DB): int { if((reply := dexec(T_ReadOpenDBInfo, A1, array[] of {byte db.x}, 2)) == nil) return -1; return get2(reply); } DB.rdappinfo(db: self ref DB): (array of byte, string) { req := array[6] of byte; req[0] = byte db.x; req[1] = byte 0; put2(req[2:], 0); # offset put2(req[4:], -1); # to end (reply, err) := rexec(T_ReadAppBlock, A1, req, 2); if(reply == nil) return (nil, err); if(get2(reply) < len reply-2) return (nil, "short reply"); return (reply[2:], nil); } DB.wrappinfo(db: self ref DB, data: array of byte): string { req := array[4 + len data] of byte; req[0] = byte db.x; req[1] = byte 0; put2(req[2:], len data); req[4:] = data; return rexec(T_WriteAppBlock, A1, req, 0).t1; } DB.rdsortinfo(db: self ref DB): (array of int, string) { req := array[6] of byte; req[0] = byte db.x; req[1] = byte 0; put2(req[2:], 0); put2(req[4:], -1); (reply, err) := rexec(T_ReadSortBlock, A1, req, 2); if(reply == nil) return (nil, err); n := len reply; a := reply[2:n]; n = (n-2)/2; s := array[n] of int; for(i := 0; i < n; i++) s[i] = get2(a[i*2:]); return (s, nil); } DB.wrsortinfo(db: self ref DB, s: array of int): string { n := len s; req := array[4+2*n] of byte; req[0] = byte db.x; req[1] = byte 0; put2(req[2:], 2*n); for(i := 0; i < n; i++) put2(req[2+i*2:], s[i]); return rexec(T_WriteSortBlock, A1, req, 0).t1; } PDB.purge(db: self ref PDB): string { return rexec(T_CleanUpDatabase, A1, array[] of {byte db.db.x}, 0).t1; } DB.resetsyncflags(db: self ref DB): string { return rexec(T_ResetSyncFlags, A1, array[] of {byte db.x}, 0).t1; } # # .pdb and other data base files # PDB.read(db: self ref PDB, index: int): ref Record { req := array[8] of byte; req[0] = byte db.db.x; req[1] = byte 0; put2(req[2:], index); put2(req[4:], 0); # offset put2(req[6:], Maxrecbytes); return unpackrec(dexec(T_ReadRecord, A2, req, 10)).t0; } PDB.readid(db: self ref PDB, id: int): (ref Record, int) { req := array[10] of byte; req[0] = byte db.db.x; req[1] = byte 0; put4(req[2:], id); put2(req[6:], 0); # offset put2(req[8:], Maxrecbytes); return unpackrec(dexec(T_ReadRecord, A1, req, 10)); } PDB.write(db: self ref PDB, r: ref Record): string { req := array[8+len r.data] of byte; req[0] = byte db.db.x; req[1] = byte 0; put4(req[2:], r.id); req[6] = byte (r.attr & Palm->Rsecret); req[7] = byte r.cat; req[8:] = r.data; (reply, err) := rexec(T_WriteRecord, A1, req, 4); if(reply == nil) return err; if(r.id == 0) r.id = get4(reply); return nil; } PDB.movecat(db: self ref PDB, from: int, tox: int): string { req := array[4] of byte; req[0] = byte db.db.x; req[1] = byte from; req[2] = byte tox; req[3] = byte 0; return rexec(T_MoveCategory, A1, req, 0).t1; } PDB.resetnext(db: self ref PDB): int { return nexec(T_ResetDBIndex, A1, array[] of {byte db.db.x}); } PDB.readnextmod(db: self ref PDB): (ref Record, int) { return unpackrec(dexec(T_ReadNextModifiedRec, A1, array[] of {byte db.db.x}, 10)); } PDB.delete(db: self ref PDB, id: int): string { req := array[6] of byte; req[0] = byte db.db.x; req[1] = byte 0; put4(req[2:], id); return rexec(T_DeleteRecord, A1, req, 0).t1; } PDB.deletecat(db: self ref PDB, cat: int): string { return rexec(T_DeleteRecord, A1, array[] of {byte db.db.x, byte 16r40, 2 to 6 => byte 0, 7=>byte cat}, 0).t1; } PDB.truncate(db: self ref PDB): string { return rexec(T_DeleteRecord, A1, array[] of {byte db.db.x, byte 16r80, 2 to 7 => byte 0}, 0).t1; } # # .prc resource files # PRC.write(db: self ref PRC, r: ref Resource): string { req := array[8+len r.data] of byte; req[0] = byte db.db.x; req[1] = byte 0; put4(req[2:], r.name); put2(req[6:], r.id); put2(req[8:], len r.data); return rexec(T_WriteResource, A1, req, 0).t1; } PRC.delete(db: self ref PRC, name: int, id: int): string { req := array[8] of byte; req[0] = byte db.db.x; req[1] = byte 0; put4(req[2:], name); put4(req[6:], id); return rexec(T_DeleteResource, A1, req, 0).t1; } PRC.readtype(db: self ref PRC, name: int, id: int): (ref Resource, int) { req := array[12] of byte; req[0] = byte db.db.x; req[1] = byte 0; put4(req[2:], name); put2(req[6:], id); put2(req[8:], 0); # Offset into record put2(req[10:], Maxrecbytes); return unpackresource(dexec(T_ReadResource, A2, req, 10)); } PRC.truncate(db: self ref PRC): string { return rexec(T_DeleteResource, A1, array[] of {byte db.db.x, byte 16r80, 2 to 7 => byte 0}, 0).t1; } PRC.read(db: self ref PRC, index: int): ref Resource { req := array[8] of byte; req[0] = byte db.db.x; req[1] = byte 0; put2(req[2:], index); put2(req[4:], 0); # offset put2(req[6:], Maxrecbytes); return unpackresource(dexec(T_ReadResource, A1, req, 12)).t0; } # # DL protocol # # request # id: byte # operation # argc: byte # arg count # args: byte[] # # response # id: byte # cmd|16r80 # argc: byte # argc response arguments follow header # error: byte[2] # error code # args: byte[] # # args wrapped by Palm->packargs etc. # # # RPC exchange with device # rpc(req: array of byte): (array of (int, array of byte), string) { if(sys->write(srvfd, req, len req) != len req) return (nil, sys->sprint("link: %r")); reply := array[65536] of byte; nb := sys->read(srvfd, reply, len reply); if(nb == 0) return (nil, "link: hangup"); if(nb < 0) return (nil, sys->sprint("link: %r")); r := int reply[0]; if((r & Response) == 0) return (nil, e(sys->sprint("received request #%2.2x not response", r))); if(r != (Response|int req[0])) return (nil, e(sys->sprint("wrong response #%x", r))); if(nb < 4) return (nil, e(Eshort)); rc := get2(reply[2:]); if(rc != 0){ if(rc < 0 || rc >= len errorlist) return (nil, e(sys->sprint("unknown error %d", rc))); return (nil, e(errorlist[rc])); } argc := int reply[1]; # count of following arguments if(argc == 0) return (nil, nil); return unpackargs(argc, reply[4:nb]); } rexec(cmd: int, argid: int, arg: array of byte, minlen: int): (array of byte, string) { args: array of (int, array of byte); if(arg != nil) args = array[] of {(argid, arg)}; req := array[2+argsize(args)] of byte; req[0] = byte cmd; req[1] = byte len args; packargs(req[2:], args); (replies, err) := rpc(req); if(replies == nil){ if(err != nil) return (nil, err); if(minlen > 0) return (nil, e(Eshort)); return (nil, nil); } (nil, reply) := replies[0]; if(len reply < minlen) return (nil, e(Eshort)); return (reply, nil); } dexec(cmd: int, argid: int, msg: array of byte, minlen: int): array of byte { (reply, nil) := rexec(cmd, argid, msg, minlen); return reply; } nexec(cmd: int, argid: int, msg: array of byte): int { (nil, err) := rexec(cmd, argid, msg, 0); if(err != nil) return -1; return 0; } unpackresource(a: array of byte): (ref Resource, int) { nb := len a; if(nb < 10) return (nil, -1); size := get2(a[8:]); if(nb-10 < size) return (nil, -1); r := Resource.new(get4(a), get2(a[4:]), size); r.data[0:] = a[10:10+size]; return (r, get2(a[6:])); } unpackrec(a: array of byte): (ref Record, int) { nb := len a; if(nb < 10) return (nil, -1); size := get2(a[6:]); if(nb-10 < size) return (nil, -1); r := Record.new(get4(a), int a[8], int a[9], size); r.data[0:] = a[10:10+size]; return (r, get2(a[4:])); } # # pack string (must be Latin1) as zero-terminated array of byte # puts(a: array of byte, s: string): int { for(i := 0; i < len s && i < len a-1; i++) a[i] = byte s[i]; a[i++] = byte 0; return i; } # # the conversion via local time might be wrong, # since the computers might be in different time zones, # but is hard to avoid # getdate(data: array of byte): int { yr := (int data[0] << 8) | int data[1]; if(yr == 0) return 0; # unspecified t := ref Tm; t.sec = int data[6]; t.min = int data[5]; t.hour = int data[4]; t.mday = int data[3]; t.mon = int data[2] - 1; t.year = yr - 1900; t.wday = 0; t.yday = 0; return daytime->tm2epoch(t); } putdate(data: array of byte, time: int): array of byte { t := daytime->local(time); y := t.year + 1900; if(time == 0) y = 0; # `unchanged' data[7] = byte 0; # pad data[6] = byte t.sec; data[5] = byte t.min; data[4] = byte t.hour; data[3] = byte t.mday; data[2] = byte (t.mon + 1); data[0] = byte ((y >> 8) & 16rff); data[1] = byte (y & 16rff); return data; }