/********************************************************************** * * undel -- a file undelete utility * * Written in GNU c by Trevor Blight, Jan 93 * * Portions of this program are based (loosely) on the program RESCUE2, * published in "The MS-DOS Developer's Guide", 2nd ed., by Howard W. Sams & Co. * * This software may be used freely for any purpose whatsoever. * Please enhance and return it to the public domain. * ***********************************************************************/ #include #include #include #include #include #include #include #include #include #include #include "undel.h" #define MAX_CLUSTERS 4078 /* max nr clusters for 12 bit FAT */ #define CHAIN_END 1 /* used by get_cluster() to indicate eof */ typedef unsigned char BYTE; typedef unsigned short WORD; typedef unsigned long LONG; typedef struct { char name[8]; /* name */ char ext[3]; /* extension */ BYTE attrib; /* attribute */ BYTE reserved[10]; WORD time; /* time: hhhhh mmm - mmm sssss */ WORD date; /* date: yyyyyyy m - mmm ddddd */ WORD cluster; /* starting cluster (Intel format) */ LONG fsize; /* total size in bytes (Intel format) */ } DENTRY; BYTE *FatBuffer; /* FAT table buffer address */ DENTRY *dirBuffer; /* directory buffer address */ DENTRY *dirend; int wi_handle; /* window handle */ /*** forward declarations ***/ PRIVATE void win_tidy( void ); PRIVATE BOOL fill_dta( WORD srch_att ); PRIVATE void undelete_file( const char *pathp ); PRIVATE WORD next_cluster( const WORD clust ); PRIVATE BOOL match( const char *sname, const char *fname ); PRIVATE WORD swap_word( const WORD word ); PRIVATE LONG swap_long( const LONG l ); PRIVATE void fnmcpy( char* d, const char *s ); PRIVATE int find_subdir( WORD dnum, const char *pathp, WORD *snump ); /*** sector <--> cluster conversion macros ***/ #define sector_of(cl) ((cl - 2)*bpbp->clsiz + bpbp->datrec) #define cluster_of(s) (2 + (s - bpbp->datrec)/bpbp->clsiz) /******************************************************** * * call this function at end of program to tidy up AES windows * *******************************************************/ PRIVATE void win_tidy( void ) { wind_close(wi_handle); wind_delete(wi_handle); appl_exit(); } /* win_tidy() */ /********************************************************* * * main entry point * *********************************************************/ int main(void) /* ignore params */ { #define WI_KIND 0 char dname[80]; /* directory name buffer */ int dnum; /* drive nr, A: = 0, etc */ int r; /* fsel return value */ int sextn; char fname[64]; int xdesk,ydesk,hdesk,wdesk; char extn3str[4]; char extn4str[4]; appl_init(); wind_get(0, WF_WORKXYWH, &xdesk, &ydesk, &wdesk, &hdesk); wi_handle = wind_create(WI_KIND, xdesk, ydesk, wdesk, hdesk); if( wi_handle < 0 ) { form_alert(1, "[1][too many windows open][QUIT]" ); appl_exit(); exit(1); /* quit */ } /* if */ wind_open(wi_handle,xdesk,ydesk,wdesk,hdesk); if (atexit(win_tidy) != 0 ) { form_alert(1, "[1][can't arrange tidy up at exit][QUIT]" ); win_tidy(); exit(1); } /* if */ graf_mouse(ARROW, 0x0L); strcpy (dname, "\\"); dnum = -1; sextn = 0x0001; /* select extension 1 */ strcpy( extn3str, "c " ); strcpy( extn4str, "h " ); r = fsel("Select a file to undelete ...", /* I display title for box */ dname, /* IO initial path spec \ ... \ no drive; returns selected path */ &dnum, /* IO selects drive -1=default 0=A: .. 5=F: */ &sextn, /* IO bitmap selected extn boxes : * 0x0001 = extn0 0x0002 = extn1 0x0004 = extn2 * 0x0008 = extn3 0x0010 = extn4 */ "* ", /* I extension text not including "." for each of 1st 3 extn boxes */ "PRG", /* NOTE for all 5 boxes this should be 3 chars long, even if spaces */ "DOC", extn3str, extn4str, /* IO extension text for last 2 extn boxes (editable) */ fname); /* O returns complete path and file */ if (r != 0) { undelete_file( fname ); } /* if */ return 0; /* successful completion */ } /* main */ /******************************************************** * * replacement for Fsfirst() & Fsnext() to recognise deleted files * ********************************************************/ struct BPB { short recsiz; /* physical sector size in bytes */ short clsiz; /* cluster size in sectors */ short clsizb; /* cluster size in bytes */ short rdlen; /* root directory length in sectors */ short fsiz; /* FAT size in sectors */ short fatrec; /* second FAT starts at this sector */ short datrec; /* data sectors start here */ short numcl; /* nr of data clusters on disk */ short bflags; /* flags */ }; /*** custom dta buffer for d_First() & d_Next() ***/ struct DTA { WORD dta_sector; DENTRY *dta_dirBuffer; DENTRY *dta_entryp; DENTRY *dta_dirend; unsigned short dta_srchattr; char dta_buf[4]; char dta_drv; char dta_attribute; unsigned short dta_time; unsigned short dta_date; long dta_size; char dta_name[14]; }; PRIVATE struct BPB *bpbp; PRIVATE struct DTA *dtap; short d_First(char *drvp, char *pathp, short attr) { WORD dnum; /* drive nr, A: = 0, etc */ int error; WORD snum; /* sector number */ dnum = toupper(*drvp) - 'A'; if( (Drvmap() & (1 << dnum)) == 0 ) { return ENXIO; } /* if */ if( *pathp == '\\' ) { pathp++; } /* if */ /*** get FAT size, root directory sector, etc from Bios Parameter Block... ***/ if( (bpbp = (struct BPB *)Getbpb(dnum)) == 0 ) { char emsg[80]; sprintf( emsg, "[1][can't get parameters for drive %c:][QUIT]", 'A'+dnum); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ /*** allocate memory for directory buffers & FAT tables ***/ if( ((dirBuffer = (DENTRY *)malloc(bpbp->clsizb)) == NULL OR (FatBuffer = (BYTE *)malloc(bpbp->fsiz * bpbp->recsiz)) == NULL) ) { form_alert(1, "[1][can't allocate enough memory][QUIT]" ); exit(1); /* quit */ } /* if */ dirend = &dirBuffer[bpbp->clsizb/sizeof(*dirBuffer)]; /*** read in FAT ***/ error = Rwabs( 0 /* read mode */, FatBuffer, bpbp->fsiz, bpbp->fatrec - bpbp->fsiz, /* first FAT starts here */ dnum ); if (error != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading FAT on drive %c][QUIT]", strerror(-error), dnum + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ if( (error = find_subdir( dnum, pathp, &snum )) != 0 ) { return error; } /* if */ if( (attr & FA_DIR) == 0 ) { char emsg[80]; sprintf( emsg, "[1][path is not a directory:|%s][QUIT]", pathp ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ /******************************************************* * * snum is the sector which has the directory entries * *******************************************************/ error = Rwabs( 0 /* read mode */, dirBuffer, bpbp->clsiz, snum, dnum ); if (error != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|directory entries on drive %c][QUIT]", strerror(-error), dnum + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ /*** fill the dta buffer with the first directory entry in dirBuffer ***/ dtap = (struct DTA *)Fgetdta(); dtap->dta_sector = snum; dtap->dta_dirBuffer = dirBuffer; dtap->dta_entryp = dirBuffer; dtap->dta_dirend = dirend; dtap->dta_drv = dnum; return fill_dta(0) ? 0 : -ENOENT; } /* d_First() */ short d_Next() { return fill_dta(0) ? 0 : -ENMFILES; /* OK ? */ } /* d_Next() */ /************************************************************* * * copy next subdirectory entry into dta * *************************************************************/ PRIVATE BOOL fill_dta( WORD srch_att ) { DENTRY *dptr; /* current directory entry */ WORD cluster; int error; struct DTA *dtap; dtap = (struct DTA *)Fgetdta(); dptr = dtap->dta_entryp++; if( dptr >= dtap->dta_dirend ) { /*** all entries in this cluster exhausted, get next cluster ***/ if( dtap->dta_sector >= bpbp->datrec ) { /*** this is a subdirectory ***/ cluster = cluster_of( dtap->dta_sector ); if( (cluster = next_cluster( cluster )) <= CHAIN_END ) { return( FALSE ); /* no more files */ } else { dtap->dta_sector = sector_of( cluster ); } /* if */ } else if( dtap->dta_sector >= bpbp->fatrec + bpbp->fsiz ) { /*** this is the root directory ***/ dtap->dta_sector += bpbp->clsiz; /* next cluster in root dir */ if( dtap->dta_sector >= bpbp->datrec ) { return( FALSE ); /* no more files */ } /* if */ } else { /*** this shouldn't happen ***/ char emsg[80]; sprintf( emsg, "[1][error while reading FAT on drive %c][QUIT]", dtap->dta_drv + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ error = Rwabs( 0 /* read mode */, dtap->dta_dirBuffer, bpbp->clsiz, dtap->dta_sector, dtap->dta_drv ); if (error != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|directory entries on drive %c][QUIT]", strerror(-error), dtap->dta_drv + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ dtap->dta_entryp = dtap->dta_dirBuffer; dptr = dtap->dta_entryp++; } /* if */ if( dptr->name[0] == '\0' ) { return( FALSE ); /* no more files */ } /* if */ dtap->dta_attribute = dptr->attrib; dtap->dta_time = dptr->time; dtap->dta_date = dptr->date; dtap->dta_size = dptr->fsize; fnmcpy( dtap->dta_name, dptr->name ); assert( strlen(dtap->dta_name) < sizeof(dtap->dta_name) ); /* in case overflow */ return TRUE; } /* fill_dta() */ /************************************************************* * * find the first sector for a pathname * (the sector contains the subdirectory list) * **************************************************************/ PRIVATE int find_subdir( WORD dnum, const char *pathp, WORD *snump ) { WORD snum; WORD attr; WORD cluster; int error; DENTRY *dptr; /* current directory entry */ /*** follow the chain of directory entries to match path name ***/ snum = bpbp->fatrec + bpbp->fsiz; /* root directory starts here */ if( *pathp == '\\' ) { pathp++; } /* if */ while( *pathp != '\0' ) { /* loop thru all subdirectories in the path */ error = Rwabs( 0 /* read mode */, dirBuffer, bpbp->clsiz, snum, dnum ); if (error != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|directory entries on drive %c][QUIT]", strerror(-error), dnum + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ dptr = dirBuffer; /*** loop thru all directory entries until the name is found ***/ while( ((dptr->attrib & FA_DIR) == 0) /* not a directory ? */ OR NOT match( pathp, dptr->name ) ) { /* names don't match ? */ if( ++dptr >= dirend ) { /*** name not found in this cluster, get next cluster ***/ if( snum >= bpbp->datrec ) { /*** this is a subdirectory ***/ cluster = cluster_of( snum ); if( (cluster = next_cluster( cluster )) <= CHAIN_END ) { return( -EPATH ); /* path not found */ } else { snum = sector_of( cluster ); } /* if */ } else if( snum >= bpbp->fatrec + bpbp->fsiz ) { /*** this is the root directory ***/ snum += bpbp->clsiz; /* next cluster in root dir */ if( snum >= bpbp->datrec ) { return( -EPATH ); /* path not found */ } /* if */ } else { /*** this shouldn't happen ***/ char emsg[80]; sprintf( emsg, "[1][error while reading|directory on drive %c][QUIT]", dnum + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ error = Rwabs( 0 /* read mode */, dirBuffer, bpbp->clsiz, snum, dnum ); if (error != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|directory entries on drive %c][QUIT]", strerror(-error), dnum + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ dptr = dirBuffer; } /* if */ } /* while */ /*** the next subdirectory name has been found ***/ snum = sector_of( swap_word(dptr->cluster) ); attr = dptr->attrib; while( (*pathp != '\0') AND (*pathp != '\\') ) { pathp++; } /* while */ if( *pathp == '\\' ) { pathp++; } /* if */ } /* while */ *snump = snum; return 0; } /* find_subdir() */ /************************************************************** * * undelete_file * pathp -- points to full pathname of file * eg A:\..dir_path..\file_name * * - find the directory entry of the file * - assume the file contents are in the empty clusters following the starting * cluster of the file. * - build an image of the file in data_buffer * - write out data_buffer to a new file * ***************************************************************/ PRIVATE void undelete_file( const char *pathp ) { WORD snum; /* sector number of directory entry */ WORD dnum; /* disk drive number */ WORD cluster; WORD current; /* current cluster in chain of deleted file */ WORD previous; /* unused ??? */ unsigned nrcl; /* number of clusters in deleted file */ void *data_buffer; /* put copy of restored file here */ void *datap; /* points into data_buffer */ int i; int error; /* error return code for various functions */ char fname[13]; /* name of file to be undeleted */ char dir_path[80]; /* directory path of file to be undeleted */ long file_size; /* size of the file */ DENTRY *dptr; /* points to a copy of the directory entry of file */ int fh; /* file handle for restored copy of file */ char restored_fn[80]; /* name of restored file */ char Path[80]; /* used for fsel_input() */ int ExitButton; /* used for fsel_input() */ long bytes_written; /* returned by Fwrite() */ assert( pathp[1] == ':' ); dnum = toupper(*pathp) - 'A'; if( (Drvmap() & (1 << dnum)) == 0 ) { char emsg[80]; sprintf( emsg, "[1][drive %c: doesn't exist!][QUIT]", *pathp ); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ /*** split pathp into directory path & filename path ***/ strcpy (dir_path, pathp+2); i = strlen( dir_path ); while( i >= 0 ) { if( dir_path[i] == '\\' ) { strcpy(fname, dir_path+i+1); dir_path[i+1] = '\0'; break; } /* if */ i--; } /* while */ if( find_subdir( dnum, dir_path, &snum ) != 0 ) { char emsg[80]; sprintf( emsg, "[1][can't find file|%s][QUIT]", fname); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ /*** look for file name in directory sector ***/ if (Rwabs( 0 /* read mode */, dirBuffer, bpbp->clsiz, snum, dnum ) != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|directory entries on drive %c][QUIT]", strerror(-error), dtap->dta_drv + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ dptr = dirBuffer; while( NOT match( fname, dptr->name ) ) { dptr++; /* point to next name */ if( dptr >= dirend ) { /*** all entries in this cluster exhausted, get next cluster ***/ if( snum >= bpbp->datrec ) { /*** this is a subdirectory ***/ cluster = cluster_of( snum ); if( (cluster = next_cluster( cluster )) <= CHAIN_END ) { char emsg[80]; sprintf( emsg, "[1][can't find file|\"%s\"][QUIT]", fname); form_alert(1, emsg ); exit(1); /* quit */ } else { snum = sector_of( cluster ); } /* if */ } else if( snum >= bpbp->fatrec + bpbp->fsiz ) { /*** this is the root directory ***/ snum += bpbp->clsiz; /* next cluster in root dir */ if( snum >= bpbp->datrec ) { char emsg[80]; sprintf( emsg, "[1][can't find file|\"%s\"][QUIT]", fname); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ } else { /*** this shouldn't happen ***/ char emsg[80]; sprintf( emsg, "[1][error while reading|root directory on drive %c][QUIT]", dnum + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ if (Rwabs(0 /* read mode */, dirBuffer, bpbp->clsiz, snum, dnum) != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|directory entries on drive %c][QUIT]", strerror(-error), dtap->dta_drv + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ dptr = dirBuffer; } /* if */ if( dptr->name[0] == '\0' ) { char emsg[80]; sprintf( emsg, "[1][can't find file|\"%s\"][QUIT]", fname); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ } /* while */ /******************************************************** * * found the file to be undeleted: * - dptr points to a copy of the directory entry * ********************************************************/ if( (char)dptr->name[0] != (char)0xe5) { char emsg[80]; sprintf( emsg, "[1][file is not deleted|\"%s\"][QUIT]", pathp); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ current = previous = swap_word(dptr->cluster); if( next_cluster(current) != 0 ) { char emsg[80]; sprintf( emsg, "[1][\"%s\"|has been overwritten][QUIT]", pathp); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ if( dptr->attrib == FA_DIR ) { nrcl = 0; /* directories have file size = 0 */ } else { file_size = swap_long(dptr->fsize); nrcl = (file_size + bpbp->clsizb - 1)/bpbp->clsizb; } /* if */ if( (datap = data_buffer = malloc( nrcl*bpbp->clsizb )) == NULL ) { form_alert(1, "[1][can't allocate enough memory][QUIT]" ); exit(1); /* quit */ } /* if */ while( nrcl > 0 ) { if( current > bpbp->numcl ) { char emsg[80]; sprintf( emsg, "[1][can't recover all of|\"%s\"][Carry On][QUIT]", pathp ); if( form_alert(1, emsg ) == 2 ) { exit(1); /* quit */ } /* if */ file_size = (datap-data_buffer)*sizeof(*datap); } /* if */ if( next_cluster( current ) == 0 ) { error = Rwabs( 0 /* read mode */, datap, bpbp->clsiz, sector_of(current), dnum ); if (error != 0) { char emsg[80]; sprintf( emsg, "[1][%s|while reading|file sectors on drive %c][QUIT]", strerror(-error), dtap->dta_drv + 'A' ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ datap += bpbp->clsizb; previous = current; nrcl--; } /* if */ current++; } /* while */ /*** now write the file out ***/ strcpy(Path, "A:\\*.*"); Path[0] += Dgetdrv(); if( fsel_input(Path, restored_fn, &ExitButton) == 0 ) { form_alert(1, "[1]can't select a name|for restored file[oh dear]"); } /* if */ if( ExitButton == 0 ) { /* user has cancelled !!! */ exit(1); } /* if */ /*** merge Path & restored file name ***/ i = strlen( Path ); while( i >= 0 ) { if( Path[i] == '\\' ) { strcpy(Path+i+1, restored_fn); strcpy(restored_fn, Path); break; } /* if */ i--; } /* while */ if( (fh = Fopen( restored_fn, 1)) < 0 ) { if( fh != -ENOENT ) { char emsg[80]; sprintf( emsg, "[1][%s|while opening %s][QUIT]", strerror(-fh), restored_fn ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ if( (fh = Fcreate( restored_fn, 0)) < 0 ) { char emsg[80]; sprintf( emsg, "[1][%s|while creating %s][QUIT]", strerror(-fh), restored_fn ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ } /* if */ if( (bytes_written = Fwrite(fh, file_size, data_buffer)) < 0 ) { char emsg[80]; error = (int)bytes_written; sprintf( emsg, "[1][%s|while writing %s][QUIT]", strerror(-error), restored_fn ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ if( (error = Fclose(fh)) != 0 ) { char emsg[80]; sprintf( emsg, "[1][%s|while trying to close %s][QUIT]", strerror(-error), restored_fn ); form_alert(1, emsg); exit(1); /* quit */ } /* if */ } /* undelete_file() */ /************************************************************** * * find next cluster in a chain of FAT entries ... * * FAT entries: 12-bit 16-bit meaning * ------ ------ ------- * 000 0000 free cluster * 001 0001 shouldn't happen * 002 - FEF 0002 - 7FFF next cluster nr * 8000 - FFEF shouldn't happen * FF0 - FF7 FFF0 - FFF7 bad cluster * FF8 - FFF FFF8 - FFFF end of chain * * max nr clusters for 12 bit FAT = 4078 * **************************************************************/ PRIVATE WORD next_cluster( const WORD clust ) { int index; /* index into FAT */ WORD newclust; /* next cluster */ /*** if nr clusters on disk > MAX_CLUSTERS use 16-bit FATs ***/ if( bpbp->numcl > MAX_CLUSTERS ) { /*** 16-bit FATs ***/ index = clust; newclust = FatBuffer[index] + (FatBuffer[index+1] << 8); /* byte swap */ if( newclust >= 0xfff8 ) { return CHAIN_END; } else if( newclust >= 0x8000 ) { /*** there is a file system problem if this happens ***/ char emsg[128]; sprintf(emsg, "[1][file system problem|bad cluster chain|" "at cluster %1d, value is %1d][QUIT]", (int)clust, (int)newclust ); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ } else { /*** 12-bit FATs ***/ index = clust + (clust >> 1); newclust = FatBuffer[index] + (FatBuffer[index+1] << 8); /* byte swap */ if( clust & 1) { /* is clust odd ?? */ newclust >>= 4; } /* if */ newclust &= 0x0fff; if( newclust >= 0xff8 ) { return CHAIN_END; } else if( newclust >= 0x0ff0 ) { /*** there is a file system problem if this happens ***/ char emsg[128]; sprintf(emsg, "[1][file system problem|bad cluster chain|" "at cluster %1d, value is %1d][QUIT]", (int)clust, (int)newclust ); form_alert(1, emsg ); exit(1); /* quit */ } /* if */ } /* if */ return newclust; } /* next_cluster() */ /******************************************************** * * check names for a match. * fname in format 'nnnnnnnneee', ie 8 chars name + 3 chars extension * sname is a null terminated string. * match up to first directory separator (ie '\') in sname * return TRUE if match found, FALSE otherwise * ********************************************************/ PRIVATE BOOL match( const char *sname, /* search match name */ const char *fname /* file or directory name */ ) { const char *fext; /* file or dir extension */ fext = fname + 8; while( fname < fext+3 ) { if( *fname == *sname ) { fname++; sname++; } else { /*** check why names differ ***/ switch( *sname++ ) { case '.': if( (*fname == ' ') OR (fname == fext) ) { fname = fext; } else { return FALSE; } /* if */ break; case '\\': case '\0': if( *fname == ' ' ) { return TRUE; } /* if */ break; default: return FALSE; } /* switch */ } /* if */ } /* while */ return TRUE; } /* match() */ /******************************* * * swap bytes the bytes in a word argument * *******************************/ PRIVATE WORD swap_word( const WORD word ) { WORD r; ((char *)&r)[0] = ((char *)&word)[1]; ((char *)&r)[1] = ((char *)&word)[0]; return r; } /* swap_word() */ PRIVATE LONG swap_long( const LONG l ) { LONG r; ((char *)&r)[0] = ((char *)&l)[3]; ((char *)&r)[1] = ((char *)&l)[2]; ((char *)&r)[2] = ((char *)&l)[1]; ((char *)&r)[3] = ((char *)&l)[0]; return r; } /* swap_word() */ /******************************************** * * copy filename from directory entry to dta * ********************************************/ PRIVATE void fnmcpy(char* d, const char *s) { int i; /* character counter */ const char *extp; /* pointer to extension */ extp = s+8; i = 8; /* copy up to 8 chars for name */ while( (i > 0) AND ((unsigned)*s > ' ') ) { /* stop at '\0' or ' ' */ *d++ = *s++; i--; } /* while */ if( (unsigned)*extp > ' ' ) { *d++ = '.'; i = 3; /* copy up to 3 chars for extension */ while( (i > 0) AND ((unsigned)*extp > ' ') ) { *d++ = *extp++; i--; } /* while */ } /* if */ *d = '\0'; } /* fnmcpy() */ /************************* end of undel.c *************************/