// Detect It Easy: detection rule file
// Author: Kaens (TG @kaens)
// TODO parse PQA too, why not
/* beautify ignore:start */

meta("format", "Palm OS file ");

function detect() {
    //ref https://jichu4n.github.io/palm-pdb/assets/Palm%20File%20Format%20Specification.pdf
    // & https://github.com/jichu4n/palm-os-sdk/blob/master/sdk-3.1/include/Core/System/DataPrv.h
    if (/S98[0-3]/.test(X.SA(0, 4)) && X.U32(4) <= 0x20 && !X.U32(0xC)
        && (!X.U32(0x10) || isWithin(X.U32(0x10), 0x20, 0x800000))
        && X.U32(0x14) < 0x20000
        && (!X.U32(0x18) || isWithin(X.U32(0x18), X.U32(0x14), 0x800000))
        && X.U32(0x1C) <= 0x40) return //too many FPs with the S98 chiptunes
    var bad = '', sus = 0;

    function palmDateToStr(dt, desc) {
        // Palm OS date starts from Jan 1, 1904......
        if (typeof desc != 'string') desc = '';
        var msecs = new Date(1904, 0, 1).getTime() + dt * 1000;
        var date = new Date(msecs);
        if (!isWithin(date.getFullYear(), 1996, 2040) //...but not many file makers seem to have actually known that lol
            && date.getFullYear() != 1907) { // 1907 is courtesy Graffiti Sniper - Golgo 13 xmas cards. The game's from 2001
            msecs = new Date(0).getTime() + (dt * 1000); date = new Date(msecs)
        }
        if (!isWithin(date.getFullYear(), 1996, 2040) && date.getFullYear() != 1907)
            if (isWithin(dt, 1996, 2040))
                return 'y' + dt // some don't even realise it has a format and think it's just the year. (ZioGolf's zgrsc.pdb :D)
            else return ''; //thankfully even with the special snowflakes it's tight enough for detection
        var r = date.toISOString().slice(0, 19);
        return r.slice(0, 4) == '1907' ? '2001' + r.slice(4, 19) : r
    }

    const dmHdrAttrBackup = 1, dmHdrAttrLaunchableData = 2;
    var name = X.SA(0, 0x20), nc = charStat(name, true);
    if (nc.indexOf('asc') < 0) return;
    var attr = X.U16(0x20, _BE), v = X.U16(0x22, _BE), dtCre = X.U32(0x24, _BE), dtMod = X.U32(0x28, _BE),
        dtBak = X.U32(0x2C, _BE), modnum = X.U32(0x30, _BE), appinfo = X.U32(0x34, _BE),
        sortinfo = X.U32(0x38, _BE), tp = 'PDB', tp_ = X.SA(0x3C, 4), by = X.SA(0x40, 4);
    var idseed = X.U32(0x44, _BE), nextrec = X.U16(0x48, _BE), numrec = X.U16(0x4C, _BE),
        p = numrec ? 0x4E : 0x50;
    if (!numrec) sus += 3; //useless pdb, or, more realistically, a FP
    if (numrec > 0x7FFF) sus += 2; //haven't seen this many yet...
    if (modnum > 0xFFF) sus += 2; //can't be that many versions, right?
    if (nextrec) { sus++; bad = bad.addIfNone('!baddbdir') } //Palm specs say reject this db (artefacts of pre-v4 OSes)
    if (idseed > 0xFFFFFF) sus++;
    if (!/\w{3,}/.test(by)) return;
    switch (tp_) {
        case 'appl': tp = 'PRC'; break;
        case 'pqa ': if (by == 'clpr') tp = 'PQA'; else return;
        default:
            if (charStat(X.readBytes(p, 4), true).indexOf('allasc') >= 0) {
                tp = 'PRC'; bad = bad.addIfNone('!badtype' + X.SA(p, 4))
            }
    }
    var dt = palmDateToStr(dtMod, 'mod'), endp, reclen = 8, m = i = 0, mhk = '', uniq = [];
    if (dtMod && dt === '') { sus += 2; bad = bad.addIfNone('!nodate') }
    if (dtCre && palmDateToStr(dtCre, 'cre') === '') sus++;
    if (dtBak && palmDateToStr(dtBak, 'bak') === '') sus++;

    if (tp === 'PRC') for (reclen = 10, endp = p + reclen * numrec; p < endp; p += reclen) { // PRC RsrcEntryType
        if (p + 10 > X.Sz()) { /*debug('lookahead shorted out');*/ return }
        var hkhd = X.SA(p, 4), hkid = X.U16(p + 4, _BE), hkofs = X.U32(p + 6, _BE);
        if (!isWithin(hkofs, p, X.Sz()) || !/\w{3,}/.test(hkhd) || uniq.indexOf(hkofs) >= 0) return;
        uniq.push(hkofs); if (m < hkofs) { m = hkofs; mhk = hkhd }
        if (hkhd === 'tver') sVersion = 'v' + X.SA(m, 0x100).trim()
    }
    else if (tp === 'PDB') for (reclen = 8, endp = p + reclen * numrec; p < endp; p += reclen) { // PDB RecordEntryType
        if (p + 8 > X.Sz()) return;
        var hkofs = X.U32(p, _BE), hkattr = X.U8(p + 4), hkid = X.U24(p + 5, _BE); //the ID is unique. Not in reality though
        if (!isWithin(hkofs, p, X.Sz()) || uniq.indexOf(hkofs) >= 0) return;
        uniq.push(hkofs); if (m < hkofs) m = hkofs
    }
    //else TODO parse PQA reclist
    if (appinfo && !isWithin(appinfo, 0x4E + reclen * numrec, X.Sz())
        || sortinfo && !isWithin(sortinfo, 0x4C + reclen * numrec, X.Sz())
        || appinfo && sortinfo && sortinfo < appinfo) {
		/*debug('appinfo/sortinfo bad address');*/ return
    }

    if (sVersion != '') sVersion = tp_.appendS(sVersion, ' '); else sVersion = tp_;
    if (appinfo && Math.abs(appinfo - p) > 0x10) {
        //_l2r('Palm OS file',p,'appinfo should be '+Hex(appinfo));
        return
    }
    if (sus > 3) return;
    if (tp == 'PRC')
        if (['taic', 'tAIN', 'tver', 'tSTR'].indexOf(mhk) >= 0)
            sz = outSz(X.fSig(m, -1, '00') + 1); //text fields, most end with 0. Some do have 0 padding...
        else if (mhk == 'tSTL') {
            var ll = X.U8(m + 2, _BE), z; m += 4;
            for (i = 0; i < ll && m <= X.Sz(); i++) {
                z = X.fSig(m, TOEOF, "00");
                if (z < 0) { bad = bad.addIfNone('!short'); break }
                m = z + 1
            }
            sz = outSz(m)
        }
        else if (['pref'].indexOf(mhk) >= 0) sz = outSz(m + 10);
        else if (X.c("'RIFF'........'WAVE'", m)) sz = outSz(m + X.U32(m + 4) + 8);
        else {
            sz = Hex(Math.max(m, p, appinfo, sortinfo)) + '+' + mhk;
            if (mhk === 'code' && !X.c("4E75", X.Sz() - 2)) bad = bad.addIfNone("!noRTSatEoF")
        }
    else // PDB? PQA?
        sz = Math.max(m, p) + '+' + mhk;

    sName = "Palm OS file (." + tp + ")"; bDetected = true;
    if (sus) bad += '!sus×' + sus;
    if (bad != '') sVersion = sVersion.appendS('malformed' + bad, '/');
    if (X.isVerbose()) {
        sOption(name); sOption(by, 'by:'); sOption(dt, 'lastmod' + (modnum > 1 ? ' (×' + modnum + ')' : '') + ':');
        sOption(Hex(idseed), "idseed:")
        if (appinfo) sOption('appinfo'); if (sortinfo) sOption('sortinfo'); sOption(numrec, 'res:'); sOption(sz, 'sz:')
    }

    return result()
}
/* beautify ignore:end */
