/* reveng.c
 * Greg Cook, 23/Feb/2019
 */

/* CRC RevEng: arbitrary-precision CRC calculator and algorithm finder
 * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018,
 * 2019  Gregory Cook
 *
 * This file is part of CRC RevEng.
 *
 * CRC RevEng is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * CRC RevEng is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with CRC RevEng.  If not, see <https://www.gnu.org/licenses/>.
 */

/* 2013-09-16: calini(), calout() work on shortest argument
 * 2013-06-11: added sequence number to uprog() calls
 * 2013-02-08: added polynomial range search
 * 2013-01-18: refactored model checking to pshres(); renamed chkres()
 * 2012-05-24: efficiently build Init contribution string
 * 2012-05-24: removed broken search for crossed-endian algorithms
 * 2012-05-23: rewrote engini() after Ewing; removed modini()
 * 2011-01-17: fixed ANSI C warnings
 * 2011-01-08: fixed calini(), modini() caters for crossed-endian algos
 * 2011-01-04: renamed functions, added calini(), factored pshres();
 *         rewrote engini() and implemented quick Init search
 * 2011-01-01: reveng() initialises terminating entry, addparms()
 *         initialises all fields
 * 2010-12-26: renamed CRC RevEng. right results, rejects polys faster
 * 2010-12-24: completed, first tests (unsuccessful)
 * 2010-12-21: completed modulate(), partial sketch of reveng()
 * 2010-12-19: started reveng
 */

/* reveng() can in theory be modified to search for polynomials shorter
 * than the full width as well, but this imposes a heavy time burden on
 * the full width search, which is the primary use case, as well as
 * complicating the search range function introduced in version 1.1.0.
 * It is more effective to search for each shorter width directly.
 */

#include <stdlib.h>

#define FILE void
#include "reveng.h"

static poly_t *modpol(const poly_t init, int rflags, int args, const poly_t *argpolys);
static void engini(int *resc, model_t **result, const poly_t divisor, int flags, int args, const poly_t *argpolys);
static void calout(int *resc, model_t **result, const poly_t divisor, const poly_t init, int flags, int args, const poly_t *argpolys);
static void calini(int *resc, model_t **result, const poly_t divisor, int flags, const poly_t xorout, int args, const poly_t *argpolys);
static void chkres(int *resc, model_t **result, const poly_t divisor, const poly_t init, int flags, const poly_t xorout, int args, const poly_t *argpolys);

static const poly_t pzero = PZERO;

model_t *
reveng(const model_t *guess, const poly_t qpoly, int rflags, int args, const poly_t *argpolys) {
    /* Complete the parameters of a model by calculation or brute search. */
    poly_t *pworks, *wptr, rem, gpoly;
    model_t *result = NULL, *rptr;
    int resc = 0;
    unsigned long spin = 0, seq = 0;

    if (~rflags & R_HAVEP) {
        /* The poly is not known.
         * Produce a list of differences between the arguments.
         */
        pworks = modpol(guess->init, rflags, args, argpolys);
        if (!pworks || !plen(*pworks)) {
            free(pworks);
            goto requit;
        }
        /* Initialise the guessed poly to the starting value. */
        gpoly = pclone(guess->spoly);
        /* Clear the least significant term, to be set in the
         * loop. qpoly does not need fixing as it is only
         * compared with odd polys.
         */
        if (plen(gpoly))
            pshift(&gpoly, gpoly, 0UL, 0UL, plen(gpoly) - 1UL, 1UL);

        while (piter(&gpoly) && (~rflags & R_HAVEQ || pcmp(&gpoly, &qpoly) < 0)) {
            /* For each possible poly of this size, try
             * dividing all the differences in the list.
             */
            if (!(spin++ & R_SPMASK)) {
                uprog(gpoly, guess->flags, seq++);
            }
            for (wptr = pworks; plen(*wptr); ++wptr) {
                /* straight divide message by poly, don't multiply by x^n */
                rem = pcrc(*wptr, gpoly, pzero, pzero, 0);
                if (ptst(rem)) {
                    pfree(&rem);
                    break;
                } else
                    pfree(&rem);
            }
            /* If gpoly divides all the differences, it is a
             * candidate.  Search for an Init value for this
             * poly or if Init is known, log the result.
             */
            if (!plen(*wptr)) {
                /* gpoly is a candidate poly */
                if (rflags & R_HAVEI && rflags & R_HAVEX)
                    chkres(&resc, &result, gpoly, guess->init, guess->flags, guess->xorout, args, argpolys);
                else if (rflags & R_HAVEI)
                    calout(&resc, &result, gpoly, guess->init, guess->flags, args, argpolys);
                else if (rflags & R_HAVEX)
                    calini(&resc, &result, gpoly, guess->flags, guess->xorout, args, argpolys);
                else
                    engini(&resc, &result, gpoly, guess->flags, args, argpolys);
            }
            if (!piter(&gpoly))
                break;
        }
        /* Finished with gpoly and the differences list, free them.
         */
        pfree(&gpoly);
        for (wptr = pworks; plen(*wptr); ++wptr)
            pfree(wptr);
        free(pworks);
    } else if (rflags & R_HAVEI && rflags & R_HAVEX)
        /* All parameters are known!  Submit the result if we get here */
        chkres(&resc, &result, guess->spoly, guess->init, guess->flags, guess->xorout, args, argpolys);
    else if (rflags & R_HAVEI)
        /* Poly and Init are known, calculate XorOut */
        calout(&resc, &result, guess->spoly, guess->init, guess->flags, args, argpolys);
    else if (rflags & R_HAVEX)
        /* Poly and XorOut are known, calculate Init */
        calini(&resc, &result, guess->spoly, guess->flags, guess->xorout, args, argpolys);
    else
        /* Poly is known but not Init; search for Init. */
        engini(&resc, &result, guess->spoly, guess->flags, args, argpolys);

requit:
    if (!(result = realloc(result, ++resc * sizeof(model_t)))) {
        uerror("cannot reallocate result array");
        return NULL;
    }
    rptr = result + resc - 1;
    rptr->spoly  = pzero;
    rptr->init   = pzero;
    rptr->flags  = 0;
    rptr->xorout = pzero;
    rptr->check  = pzero;
    rptr->magic  = pzero;
    rptr->name   = NULL;

    return (result);
}

static poly_t *
modpol(const poly_t init, int rflags, int args, const poly_t *argpolys) {
    /* Produce, in ascending length order, a list of differences
     * between the arguments in the list by summing pairs of arguments.
     * If R_HAVEI is not set in rflags, only pairs of equal length are
     * summed.
     * Otherwise, sums of right-aligned pairs are also returned, with
     * the supplied init poly added to the leftmost terms of each
     * poly of the pair.
     */
    poly_t work, swap, *result, *rptr, *iptr;
    const poly_t *aptr, *bptr, *eptr = argpolys + args;
    unsigned long alen, blen;

    if (args < 2) return (NULL);

    result = calloc(((((args - 1) * args) >> 1) + 1) * sizeof(poly_t), sizeof(char));
    if (!result)
        uerror("cannot allocate memory for codeword table");

    rptr = result;

    for (aptr = argpolys; aptr < eptr; ++aptr) {
        alen = plen(*aptr);
        for (bptr = aptr + 1; bptr < eptr; ++bptr) {
            blen = plen(*bptr);
            if (alen == blen) {
                work = pclone(*aptr);
                psum(&work, *bptr, 0UL);
            } else if (rflags & R_HAVEI && alen < blen) {
                work = pclone(*bptr);
                psum(&work, *aptr, blen - alen);
                psum(&work, init, 0UL);
                psum(&work, init, blen - alen);
            } else if (rflags & R_HAVEI /* && alen > blen */) {
                work = pclone(*aptr);
                psum(&work, *bptr, alen - blen);
                psum(&work, init, 0UL);
                psum(&work, init, alen - blen);
            } else
                work = pzero;

            if (plen(work))
                pnorm(&work);
            if ((blen = plen(work))) {
                /* insert work into result[] in ascending order of length */
                for (iptr = result; iptr < rptr; ++iptr) {
                    if (plen(work) < plen(*iptr)) {
                        swap = *iptr;
                        *iptr = work;
                        work = swap;
                    } else if (plen(*iptr) == blen && !pcmp(&work, iptr)) {
                        pfree(&work);
                        work = *--rptr;
                        break;
                    }
                }
                *rptr++ = work;
            }
        }
    }
    *rptr = pzero;
    return (result);
}

static void
engini(int *resc, model_t **result, const poly_t divisor, int flags, int args, const poly_t *argpolys) {
    /* Search for init values implied by the arguments.
     * Method from: Ewing, Gregory C. (March 2010).
     * "Reverse-Engineering a CRC Algorithm". Christchurch:
     * University of Canterbury.
     * <http://www.cosc.canterbury.ac.nz/greg.ewing/essays/
     * CRC-Reverse-Engineering.html>
     */
    poly_t apoly = PZERO, bpoly, pone = PZERO, *mat, *jptr;
    const poly_t *aptr, *bptr, *iptr;
    unsigned long alen, blen, dlen, ilen, i, j;
    int cy;

    dlen = plen(divisor);

    /* Allocate the CRC matrix */
    mat = (poly_t *) calloc((dlen << 1) * sizeof(poly_t), sizeof(char));
    if (!mat)
        uerror("cannot allocate memory for CRC matrix");

    /* Find arguments of the two shortest lengths */
    alen = blen = plen(*(aptr = bptr = iptr = argpolys));
    for (++iptr; iptr < argpolys + args; ++iptr) {
        ilen = plen(*iptr);
        if (ilen < alen) {
            bptr = aptr;
            blen = alen;
            aptr = iptr;
            alen = ilen;
        } else if (ilen > alen && (aptr == bptr || ilen < blen)) {
            bptr = iptr;
            blen = ilen;
        }
    }
    if (aptr == bptr) {
        /* if no arguments are suitable, calculate Init with an
         * assumed XorOut of 0.  Create a padded XorOut
         */
        palloc(&apoly, dlen);
        calini(resc, result, divisor, flags, apoly, args, argpolys);
        pfree(&apoly);
        free(mat);
        return;
    }

    /* Find the potential contribution of the bottom bit of Init */
    palloc(&pone, 1UL);
    piter(&pone);
    if (blen < (dlen << 1)) {
        palloc(&apoly, dlen); /* >= 1 */
        psum(&apoly, pone, (dlen << 1) - 1UL - blen); /* >= 0 */
        psum(&apoly, pone, (dlen << 1) - 1UL - alen); /* >= 1 */
    } else {
        palloc(&apoly, blen - dlen + 1UL); /* > dlen */
        psum(&apoly, pone, 0UL);
        psum(&apoly, pone, blen - alen); /* >= 1 */
    }
    if (plen(apoly) > dlen) {
        mat[dlen] = pcrc(apoly, divisor, pzero, pzero, 0);
        pfree(&apoly);
    } else {
        mat[dlen] = apoly;
    }

    /* Find the actual contribution of Init */
    apoly = pcrc(*aptr, divisor, pzero, pzero, 0);
    bpoly = pcrc(*bptr, divisor, pzero, apoly, 0);

    /* Populate the matrix */
    palloc(&apoly, 1UL);
    for (jptr = mat; jptr < mat + dlen; ++jptr)
        *jptr = pzero;
    for (iptr = jptr++; jptr < mat + (dlen << 1); iptr = jptr++)
        * jptr = pcrc(apoly, divisor, *iptr, pzero, P_MULXN);
    pfree(&apoly);

    /* Transpose the matrix, augment with the Init contribution
     * and convert to row echelon form
     */
    for (i = 0UL; i < dlen; ++i) {
        apoly = pzero;
        iptr = mat + (dlen << 1);
        for (j = 0UL; j < dlen; ++j)
            ppaste(&apoly, *--iptr, i, j, j + 1UL, dlen + 1UL);
        if (ptst(apoly))
            ppaste(&apoly, bpoly, i, dlen, dlen + 1UL, dlen + 1UL);
        j = pfirst(apoly);
        while (j < dlen && !pident(mat[j], pzero)) {
            psum(&apoly, mat[j], 0UL); /* pfirst(apoly) > j */
            j = pfirst(apoly);
        }
        if (j < dlen)
            mat[j] = apoly; /* pident(mat[j], pzero) || pfirst(mat[j]) == j */
        else
            pfree(&apoly);
    }
    palloc(&bpoly, dlen + 1UL);
    psum(&bpoly, pone, dlen);

    /* Iterate through all solutions */
    do {
        /* Solve the matrix by Gaussian elimination.
         * The parity of the result, masked by each row, should be even.
         */
        cy = 1;
        apoly = pclone(bpoly);
        jptr = mat + dlen;
        for (i = 0UL; i < dlen; ++i) {
            /* Compute next bit of Init */
            if (pmpar(apoly, *--jptr))
                psum(&apoly, pone, dlen - 1UL - i);
            /* Toggle each zero row with carry, for next iteration */
            if (cy) {
                if (pident(*jptr, pzero)) {
                    /* 0 to 1, no carry */
                    *jptr = bpoly;
                    cy = 0;
                } else if (pident(*jptr, bpoly)) {
                    /* 1 to 0, carry forward */
                    *jptr = pzero;
                }
            }
        }

        /* Trim the augment mask bit */
        praloc(&apoly, dlen);

        /* Test the Init value and add to results if correct */
        calout(resc, result, divisor, apoly, flags, args, argpolys);
        pfree(&apoly);
    } while (!cy);
    pfree(&pone);
    pfree(&bpoly);

    /* Free the matrix. */
    for (jptr = mat; jptr < mat + (dlen << 1); ++jptr)
        pfree(jptr);
    free(mat);
}

static void
calout(int *resc, model_t **result, const poly_t divisor, const poly_t init, int flags, int args, const poly_t *argpolys) {
    /* Calculate Xorout, check it against all the arguments and
     * add to results if consistent.
     */
    poly_t xorout;
    const poly_t *aptr, *iptr;
    unsigned long alen, ilen;

    if (args < 1) return;

    /* find argument of the shortest length */
    alen = plen(*(aptr = iptr = argpolys));
    for (++iptr; iptr < argpolys + args; ++iptr) {
        ilen = plen(*iptr);
        if (ilen < alen) {
            aptr = iptr;
            alen = ilen;
        }
    }

    xorout = pcrc(*aptr, divisor, init, pzero, 0);
    /* On little-endian algorithms, the calculations yield
     * the reverse of the actual xorout: in the Williams
     * model, the refout stage intervenes between init and
     * xorout.
     */
    if (flags & P_REFOUT)
        prev(&xorout);

    /* Submit the model to the results table.
     * Could skip the shortest argument but we wish to check our
     * calculation.
     */
    chkres(resc, result, divisor, init, flags, xorout, args, argpolys);
    pfree(&xorout);
}

static void
calini(int *resc, model_t **result, const poly_t divisor, int flags, const poly_t xorout, int args, const poly_t *argpolys) {
    /* Calculate Init, check it against all the arguments and add to
     * results if consistent.
     */
    poly_t rcpdiv, rxor, arg, init;
    const poly_t *aptr, *iptr;
    unsigned long alen, ilen;

    if (args < 1) return;

    /* find argument of the shortest length */
    alen = plen(*(aptr = iptr = argpolys));
    for (++iptr; iptr < argpolys + args; ++iptr) {
        ilen = plen(*iptr);
        if (ilen < alen) {
            aptr = iptr;
            alen = ilen;
        }
    }

    rcpdiv = pclone(divisor);
    prcp(&rcpdiv);
    /* If the algorithm is reflected, an ordinary CRC requires the
     * model's XorOut to be reversed, as XorOut follows the RefOut
     * stage.  To reverse the CRC calculation we need rxor to be the
     * mirror image of the forward XorOut.
     */
    rxor = pclone(xorout);
    if (~flags & P_REFOUT)
        prev(&rxor);
    arg = pclone(*aptr);
    prev(&arg);

    init = pcrc(arg, rcpdiv, rxor, pzero, 0);
    pfree(&arg);
    pfree(&rxor);
    pfree(&rcpdiv);
    prev(&init);

    /* Submit the model to the results table.
     * Could skip the shortest argument but we wish to check our
     * calculation.
     */
    chkres(resc, result, divisor, init, flags, xorout, args, argpolys);
    pfree(&init);
}

static void
chkres(int *resc, model_t **result, const poly_t divisor, const poly_t init, int flags, const poly_t xorout, int args, const poly_t *argpolys) {
    /* Checks a model against the argument list, and adds to the
     * external results table if consistent.
     * Extends the result array and updates the external pointer if
     * necessary.
     */
    model_t *rptr;
    poly_t xor, crc;
    const poly_t *aptr = argpolys, *const eptr = argpolys + args;

    /* If the algorithm is reflected, an ordinary CRC requires the
     * model's XorOut to be reversed, as XorOut follows the RefOut
     * stage.
     */
    xor = pclone(xorout);
    if (flags & P_REFOUT)
        prev(&xor);

    for (; aptr < eptr; ++aptr) {
        crc = pcrc(*aptr, divisor, init, xor, 0);
        if (ptst(crc)) {
            pfree(&crc);
            break;
        } else {
            pfree(&crc);
        }
    }
    pfree(&xor);
    if (aptr != eptr) return;

    *result = realloc(*result, ++*resc * sizeof(model_t));
    if (!*result) {
        uerror("cannot reallocate result array");
        return;
    }

    rptr = *result + *resc - 1;
    rptr->spoly  = pclone(divisor);
    rptr->init   = pclone(init);
    rptr->flags  = flags;
    rptr->xorout = pclone(xorout);
    rptr->check  = pzero;
    rptr->magic  = pzero;
    rptr->name   = NULL;

    /* compute check value for this model */
    mcheck(rptr);

    /* callback to notify new model */
    ufound(rptr);
}
