mirror of
				https://github.com/RfidResearchGroup/proxmark3.git
				synced 2025-10-31 08:26:15 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			503 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			503 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /* 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);
 | |
| }
 |