home *** CD-ROM | disk | FTP | other *** search
/ Geek Gadgets 1 / ADE-1.bin / ade-dist / ncftp-2.3.0-src.tgz / tar.out / contrib / ncftp / Xfer.c < prev    next >
C/C++ Source or Header  |  1996-09-28  |  17KB  |  605 lines

  1. /* Xfer.c */
  2.  
  3. #include "Sys.h"
  4.  
  5. #ifndef NeXT
  6. #ifndef _POSIX_SOURCE
  7. #    define _POSIX_SOURCE 1
  8. #endif
  9. #endif
  10.  
  11. #ifndef _POSIX_C_SOURCE
  12. #    define _POSIX_C_SOURCE 3    /* For solaris only? */
  13. #endif
  14.  
  15. #include <signal.h>
  16. #include <setjmp.h>
  17. #include <errno.h>
  18.  
  19. #define _xfer_c_ 1
  20.  
  21. #include "Util.h"
  22. #include "Main.h"
  23. #include "Xfer.h"
  24. #include "RCmd.h"
  25. #include "FTP.h"
  26. #include "Progress.h"
  27.  
  28. /* Large buffer to hold blocks of data during transferring. */
  29. char *gXferBuf = NULL;
  30.  
  31. char *gSecondaryBuf = NULL;
  32.  
  33. /* Size of the transfer buffer.  */
  34. size_t gXferBufSize = kXferBufSize;
  35.  
  36. /* Stores whether we had an interrupt occur during the transfer. */
  37. int gXferAbortFlag = 0;
  38.  
  39. char *gSecondaryBufPtr;
  40. char *gSecondaryBufLimit;
  41.  
  42. int gUsingBufferGets;
  43.  
  44. jmp_buf gXferTimeoutJmp;
  45. int gNumReadTimeouts;        /* Number of timeouts occurred during reads. */
  46. int gNumWriteTimeouts;        /* Number of timeouts occurred during writes. */
  47. int gConsecutiveTimeouts;    /* Number of timeouts in a row. */
  48. int gTransferTimedOut;        /* Flag to tell if transfer aborted with TOs. */
  49. int gBlockTimeoutLen;        /* How long we give I/O to complete. */
  50.  
  51. extern int gDebug;
  52. extern int gStdout;
  53. extern int gNetworkTimeout;
  54.  
  55.  
  56. void InitXferBuffer(void)
  57. {
  58.     /* Try allocating a big block of data.  If we fail, try halving
  59.      * the size and try again.
  60.      */ 
  61.     gSecondaryBuf = NULL;
  62.     gXferBuf = NULL;
  63.     for ( ; (gXferBufSize > (size_t) 256); gXferBufSize = gXferBufSize / (size_t) 2) {
  64.         gXferBuf = (char *) malloc(gXferBufSize);
  65.         if (gXferBuf != NULL) {
  66.             gSecondaryBuf = (char *) malloc(gXferBufSize);
  67.             if (gSecondaryBuf == NULL) {
  68.                 free(gXferBuf);
  69.             } else {
  70.                 /* Allocated both buffers. */
  71.                 return;
  72.             }
  73.         }
  74.     }
  75.     fprintf(stderr, "No memory for transfer buffer.\n");
  76.     Exit(kExitOutOfMemory);
  77. }    /* InitXferBuffer */
  78.  
  79.  
  80.  
  81.  
  82. #ifndef ReadOrTimeout
  83. static void IOAlarm(int sigNum)
  84. {
  85.     alarm(0);
  86.     longjmp(gXferTimeoutJmp, 1);
  87. }    /* IOAlarm */
  88. #endif
  89.  
  90.  
  91.  
  92.  
  93. void ResetBlockTimeout(void)
  94. {
  95.     gBlockTimeoutLen = gNetworkTimeout / kMaxConsecTimeOuts;
  96.     if (gBlockTimeoutLen < 2)
  97.         gBlockTimeoutLen = 2;
  98. }    /* ResetBlockTimeout */
  99.  
  100.  
  101.  
  102. #ifndef ReadOrTimeout
  103. int ReadOrTimeout(const int f, char *buf, const size_t bufSize)
  104. {
  105.     int nr;
  106.     VSig_t oa;
  107.     
  108.     oa = (VSig_t) SIGNAL(SIGALRM, IOAlarm);
  109.     if (setjmp(gXferTimeoutJmp) != 0) {
  110.         (void) SIGNAL(SIGALRM, oa);
  111.         /* If the read didn't finish in x seconds, we'll return
  112.          * a timeout error.  We also double the timeout length, so the
  113.          * read has more time to complete, since if it didn't finish
  114.          * in x seconds, there's no reason to assume the next attempt
  115.          * will finish in x seconds either.
  116.          */
  117.         gBlockTimeoutLen = gBlockTimeoutLen * 2;
  118.         return (kTimeoutErr);
  119.     }
  120.     alarm((unsigned int) gBlockTimeoutLen);
  121.     nr = (int) read(f, buf, bufSize);
  122.     alarm(0);
  123.     (void) SIGNAL(SIGALRM, oa);
  124.     return (nr);
  125. }    /* ReadOrTimeout */
  126. #endif    /* ReadOrTimeout */
  127.  
  128.  
  129.  
  130.  
  131. #ifndef WriteOrTimeout
  132. int WriteOrTimeout(const int f, char *buf, const size_t bufSize)
  133. {
  134.     int nr;
  135.     VSig_t oa;
  136.     
  137.     oa = (VSig_t) SIGNAL(SIGALRM, IOAlarm);
  138.     if (setjmp(gXferTimeoutJmp) != 0) {
  139.         (void) SIGNAL(SIGALRM, oa);
  140.         /* If the write didn't finish in x seconds, we'll return
  141.          * a timeout error.  We also double the timeout length, so the
  142.          * write has more time to complete, since if it didn't finish
  143.          * in x seconds, there's no reason to assume the next attempt
  144.          * will finish in x seconds either.
  145.          */
  146.         gBlockTimeoutLen = gBlockTimeoutLen * 2;
  147.         return (kTimeoutErr);
  148.     }
  149.     alarm((unsigned int) gBlockTimeoutLen);
  150.     nr = (int) write(f, buf, bufSize);
  151.     alarm(0);
  152.     (void) SIGNAL(SIGALRM, oa);
  153.     return (nr);
  154. }    /* WriteOrTimeout */
  155. #endif    /* WriteOrTimeout */
  156.  
  157.  
  158.  
  159.  
  160. int BufferGets(char *buf, size_t bufsize, XferSpecPtr xp)
  161. {
  162.     int err;
  163.     char *src;
  164.     char *dst;
  165.     char *dstlim;
  166.     int len;
  167.     int nr;
  168.  
  169.     gUsingBufferGets = 1;
  170.     err = 0;
  171.     dst = buf;
  172.     dstlim = dst + bufsize - 1;        /* Leave room for NUL. */
  173.     src = gSecondaryBufPtr;
  174.     for ( ; dst < dstlim; ) {
  175.         if (src >= gSecondaryBufLimit) {
  176.             /* Fill the buffer. */
  177.  
  178. /* Don't need to poll it here.  The routines that use BufferGets don't
  179.  * need any special processing during timeouts (i.e. progress reports),
  180.  * so go ahead and just let it block until there is data to read.
  181.  */
  182.             nr = (int) read(xp->inStream, gSecondaryBuf, gXferBufSize);
  183.             if (nr == 0) {
  184.                 /* EOF. */
  185.                 goto done;
  186.             } else if (nr < 0) {
  187.                 /* Error. */
  188.                 err = -1;
  189.                 goto done;
  190.             }
  191.             gSecondaryBufPtr = gSecondaryBuf;
  192.             gSecondaryBufLimit = gSecondaryBuf + nr;
  193.             src = gSecondaryBufPtr;
  194.         }
  195.         if (*src == '\r') {
  196.             ++src;
  197.         } else {
  198.             if (*src == '\n') {
  199.                 *dst++ = *src++;
  200.                 goto done;
  201.             }
  202.             *dst++ = *src++;
  203.         }
  204.     }
  205.  
  206. done:
  207.     gSecondaryBufPtr = src;
  208.     *dst = '\0';
  209.     len = (int) (dst - buf);
  210.     if (err < 0)
  211.         return (err);
  212.     return (len);
  213. }    /* BufferGets */
  214.  
  215.  
  216.  
  217.  
  218. /* We get here upon a signal we can handle during transfers. */
  219. void XferSigHandler(int sigNum)
  220. {
  221.     gXferAbortFlag = sigNum;
  222. #if 1
  223.     /* Not a good thing to do in general from a signal handler... */
  224.     TraceMsg("XferSigHandler: SIG %d.\n", sigNum);
  225. #endif
  226.     return;
  227. }    /* XferSigHandler */
  228.  
  229.  
  230.  
  231.  
  232. /* This initializes a transfer information block to zeroes, and
  233.  * also initializes the two Response blocks.
  234.  */
  235. XferSpecPtr InitXferSpec(void)
  236. {
  237.     XferSpecPtr xp;
  238.     
  239.     xp = (XferSpecPtr) calloc(SZ(1), sizeof(XferSpec));
  240.     if (xp == NULL)
  241.         OutOfMemory();
  242.     xp->cmdResp = InitResponse();
  243.     xp->xferResp = InitResponse();
  244.     return (xp);
  245. }    /* InitXferSpec */
  246.  
  247.  
  248.  
  249.  
  250. /* Disposes the transfer information block, and the responses within it. */
  251. void DoneWithXferSpec(XferSpecPtr xp)
  252. {
  253.     DoneWithResponse(xp->cmdResp);
  254.     DoneWithResponse(xp->xferResp);
  255.     CLEARXFERSPEC(xp);
  256.     free(xp);
  257. }    /* DoneWithXferSpec */
  258.  
  259.  
  260.  
  261.  
  262. void AbortDataTransfer(XferSpecPtr xp)
  263. {
  264. #ifdef SUCK_ABORT
  265.     longstring buf;
  266. #endif
  267.  
  268.     DebugMsg("Start Abort\n");
  269.  
  270. #ifndef SUCK_ABORT
  271.     SendTelnetInterrupt();    /* Probably could get by w/o doing this. */
  272.     
  273.     /* If we aborted too late, and the server already sent the whole thing,
  274.      * it will just respond a 226 Transfer completed to our ABOR.  But
  275.      * if we actually aborted, we'll get a 426 reply instead, then the
  276.      * server will send another 226 reply.  So if we get a 426 we'll
  277.      * print that quick and get rid of it by NULLing it out;  RDataCmd()
  278.      * will then do its usual GetResponse and get the pending 226.
  279.      * If we get the 226 here, we don't want RDataCmd() to try and get
  280.      * another response.  It will check to see if there is already is
  281.      * one, and if so, not get a response.
  282.      */
  283.     (void) RCmd(xp->xferResp, "ABOR");
  284.     
  285.     if (xp->xferResp->code == 426) {
  286.         TraceMsg("(426) Aborted in time.\n");
  287.         ReInitResponse(xp->xferResp);
  288.     }
  289. #else
  290.     /* Fake it by just reading until EOF. */
  291.     while (read(xp->inStream, buf, sizeof(buf) > 0)
  292.         ;
  293. #endif
  294.     CloseDataConnection();
  295.  
  296.     DebugMsg("End Abort\n");
  297. }    /* AbortDataTransfer */
  298.  
  299.  
  300.  
  301.  
  302. /* We take advantage of POSIX's signal routines.  One problem that I couldn't
  303.  * resolve was if we were interrupted in the middle of a write(), the
  304.  * program space was corrupted.  For example, while fetching a file, I
  305.  * would hit interrupt, and many times the program would just choke, leaving
  306.  * stdout munged so subsequent printf's would print a few characters of
  307.  * garbage.
  308.  *
  309.  * As a work-around, we block the signals until the current i/o operation
  310.  * completes, then abort.  Use POSIX signals instead of even more #ifdefs
  311.  * for System V's sighold/sigrlse and BSD's sigblock, because recent versions
  312.  * of System V and BSD both support POSIX.
  313.  */
  314.  
  315. int DataTransfer(XferSpecPtr xp)
  316. {
  317.     GetBlockProc        get;
  318.     PutBlockProc        put;
  319.     int                    in, out;
  320.     long                nRead, nPut;
  321.     Sig_t                origIntr, origPipe;
  322. #ifdef POSIX_SIGNALS
  323.     sigset_t            blockSet, origSet;
  324. #endif
  325.  
  326.     get = xp->getBlock;
  327.     put = xp->putBlock;
  328.     in = xp->inStream;
  329.     out = xp->outStream;
  330.     gXferAbortFlag = 0;
  331.  
  332.     /* In case we happen to use the secondary I/O buffer (i.e. want to use
  333.      * BufferGets), this line sets the buffer pointer so that the first thing
  334.      * BufferGets will do is reset and fill the buffer using real I/O.
  335.      */
  336.     gSecondaryBufPtr = gSecondaryBuf + gXferBufSize;
  337.     gUsingBufferGets = 0;
  338.  
  339.     /* Always call StartProgress, because that initializes the logging
  340.      * stuff too.
  341.      */
  342.     StartProgress(xp);
  343.     
  344.     gNumReadTimeouts = gNumWriteTimeouts = 0;
  345.     gConsecutiveTimeouts = 0;
  346.     gTransferTimedOut = 0;
  347.     
  348. #ifdef POSIX_SIGNALS
  349.     sigemptyset(&blockSet);
  350.     sigaddset(&blockSet, SIGINT);
  351.     sigprocmask(0, NULL, &origSet);
  352. #endif
  353.  
  354.     origIntr = SIGNAL(SIGINT, XferSigHandler);
  355.     origPipe = SIGNAL(SIGPIPE, XferSigHandler);
  356.  
  357.     for (errno = 0; ; ) {
  358.     
  359.         /*** Read-block loop *********************************************/
  360.         
  361.         for (gConsecutiveTimeouts = 0;
  362.             gConsecutiveTimeouts < kMaxConsecTimeOuts;
  363.             gConsecutiveTimeouts++)
  364.         {        
  365.             if (gXferAbortFlag > 0) {
  366. abortRead:
  367.                 SIGNAL(SIGINT, SIG_IGN);    /* Don't interrupt while aborting. */
  368.                 SIGNAL(SIGPIPE, SIG_IGN);
  369.                 /* It's important to make sure that the local file gets it's times
  370.                  * set correctly, so that reget works like it should.  When we
  371.                  * call AbortDataTransfer, often the server just hangs up, and
  372.                  * in that case we longjmp someplace else.
  373.                  */
  374.                 if ((xp->netMode == kNetReading) && (xp->outStream != gStdout))
  375.                     SetLocalFileTimes(xp->doUTime, xp->remoteModTime, xp->localFileName);
  376.                 if (gXferAbortFlag == SIGPIPE) {
  377.                     if (gDebug)
  378.                         EPrintF("\r** Broken pipe **\n");
  379.                 } else if (gXferAbortFlag == SIGINT) {
  380.                     EPrintF("\r** Aborting Transfer **\n");
  381.                 } else {
  382.                     EPrintF("\r** Aborting Transfer (%d) **\n", gXferAbortFlag);
  383.                 }
  384.                 DebugMsg("Read timeouts: %d;  Write timeouts: %d.\n",
  385.                     gNumReadTimeouts, gNumWriteTimeouts);
  386.                 AbortDataTransfer(xp);
  387.                 goto doneXfer;    /* Not reached. */
  388.             }
  389.     
  390.             /* Read the block. */
  391. #ifdef POSIX_SIGNALS
  392.             sigprocmask(SIG_BLOCK, &blockSet, NULL);
  393. #endif
  394.     
  395.             /* The GetBlockProc should time-out as needed, and return
  396.              * kTimeoutErr when that happens.
  397.              */
  398.             nRead = (*get)(gXferBuf, gXferBufSize, xp);
  399.             
  400. #ifdef POSIX_SIGNALS
  401.             sigprocmask(SIG_UNBLOCK, &blockSet, NULL);
  402. #endif
  403.     
  404.             if (nRead > 0L)
  405.                 goto readOkay;        /* Got some data, so done for now. */
  406.             
  407.             if (nRead == 0L)
  408.                 goto doneXfer;        /* End of transmission. */
  409.             
  410.             if (nRead == -1L) {
  411.                 /* A generic error. */
  412.                 if (errno != EINTR) {
  413.                     Error(kDoPerror, "Error occurred during read!\n");
  414.                     goto abortRead;
  415.                     /* Note: You often get EINTR when you use ^Z. */
  416.                 }
  417.             }
  418.             
  419.             /* else nRead == kTimeoutErr */
  420.                     
  421.             /* Add one to the global counter for this data transfer. */
  422.             ++gNumReadTimeouts;
  423.  
  424.             /* While we're waiting, do a progress report. */
  425.             if (xp->doReports)
  426.                 ProgressReport(xp, kForceUpdate);
  427.         }    /* end read-block loop */
  428.  
  429.         /* We only get here if we finished the loop above, which
  430.          * means we hit our limit of consecutive timeouts.
  431.          *
  432.          * Our timeout length is really a small "sub-timeout" so
  433.          * that we can squeeze in progress reports.  If we
  434.          * hit X sub-timeouts in a row, we really have reached
  435.          * our network timeout, so we should abort.
  436.          */
  437.         gTransferTimedOut = 1;
  438.         Error(kDontPerror, "Timed-out while trying to read data.\n");
  439.         goto abortRead;
  440.  
  441. readOkay:
  442.         xp->bytesTransferred += nRead;
  443.  
  444.  
  445.         /*** Write-block loop **********************************************/
  446.         
  447.         for (gConsecutiveTimeouts = 0;
  448.             gConsecutiveTimeouts < kMaxConsecTimeOuts;
  449.             gConsecutiveTimeouts++)
  450.         {
  451.             /* This is the same glob of code as for the abortRead part. */
  452.             if (gXferAbortFlag > 0) {
  453. abortWrite:
  454.                 SIGNAL(SIGINT, SIG_IGN);    /* Don't interrupt while aborting. */
  455.                 SIGNAL(SIGPIPE, SIG_IGN);
  456.                 /* It's important to make sure that the local file gets it's times
  457.                  * set correctly, so that reget works like it should.  When we
  458.                  * call AbortDataTransfer, often the server just hangs up, and
  459.                  * in that case we longjmp someplace else.
  460.                  */
  461.                 if ((xp->netMode == kNetReading) && (xp->outStream != gStdout))
  462.                     SetLocalFileTimes(xp->doUTime, xp->remoteModTime, xp->localFileName);
  463.                 if (gXferAbortFlag == SIGPIPE) {
  464.                     if (gDebug)
  465.                         EPrintF("\r** Broken pipe **\n");
  466.                 } else if (gXferAbortFlag == SIGINT) {
  467.                     EPrintF("\r** Aborting Transfer **\n");
  468.                 } else {
  469.                     EPrintF("\r** Aborting Transfer (%d) **\n", gXferAbortFlag);
  470.                 }
  471.                 DebugMsg("Read timeouts: %d;  Write timeouts: %d.\n",
  472.                     gNumReadTimeouts, gNumWriteTimeouts);
  473.                 AbortDataTransfer(xp);
  474.                 goto doneXfer;    /* Not reached. */
  475.             }
  476.  
  477.             /* Write the block. */
  478. #ifdef POSIX_SIGNALS
  479.             sigprocmask(SIG_BLOCK, &blockSet, NULL);
  480. #endif
  481.             nPut = (*put)(gXferBuf, (size_t) nRead, xp);
  482. #ifdef POSIX_SIGNALS
  483.             sigprocmask(SIG_UNBLOCK, &blockSet, NULL);
  484. #endif
  485.  
  486.             if (nPut > 0L)
  487.                 goto writeOkay;    /* Write succeeded. */
  488.             
  489.             if (nPut < -1L) {                
  490.                 /* Add one to the global counter for this data transfer. */
  491.                 ++gNumWriteTimeouts;
  492.     
  493.                 /* While we're waiting, do a progress report. */
  494.                 if (xp->doReports)
  495.                     ProgressReport(xp, kForceUpdate);
  496.             } else /* if (nPut == -1L or 0L) */ {
  497.                 if (errno == EPIPE) {
  498.                     goto abortWrite;
  499.                 } else if (errno != EINTR) {
  500.                     Error(kDoPerror, "Error occurred during write!\n");
  501.                     goto abortWrite;
  502.                 } /* else EINTR, fall-through and retry write. */
  503.             }
  504.             /* else nPut == kTimeoutErr, which we just continue. */
  505.         }    /* End write-block loop */
  506.  
  507.         /* We only get here if we finished the loop above, which
  508.          * means we hit our limit of consecutive timeouts.
  509.          */
  510.         gTransferTimedOut = 1;
  511.         Error(kDontPerror, "Timed-out while trying to write data.\n");
  512.         goto abortWrite;
  513.         
  514. writeOkay:
  515.         /* Check to see if we need to have a transfer progress report. */
  516.         if (xp->doReports)
  517.             ProgressReport(xp, kOptionalUpdate);
  518.     }
  519.  
  520. doneXfer:
  521.     /* Always call EndProgress, because that does logging too. */
  522.     EndProgress(xp);
  523.  
  524.     (void) SIGNAL(SIGINT, origIntr);
  525.     (void) SIGNAL(SIGPIPE, origPipe);
  526. #ifdef POSIX_SIGNALS
  527.     (void) sigprocmask(SIG_SETMASK, &origSet, NULL);
  528. #endif
  529.  
  530.     if (gNumReadTimeouts + gNumWriteTimeouts > 0)
  531.         DebugMsg(
  532.             "Read timeouts: %d;  Write timeouts: %d;  Block timeout len: %d.\n",
  533.             gNumReadTimeouts, gNumWriteTimeouts, gBlockTimeoutLen);
  534.  
  535.     return (0);
  536. }    /* DataTransfer */
  537.  
  538. /*****************************************************************************
  539.  
  540. How the XferSpec is used.
  541. -------------------------
  542.  
  543. Because there are different ways to transfer a file, and different things to
  544. do with the transfer data, it's difficult to have one central transferring
  545. facility without having a lot of "if (this) else if (that)'s."  The other way
  546. would be to have one function for each task, at the expense of redundancy and
  547. inconvenience when general changes would have to be made for each one.
  548.  
  549. I chose to keep things really general, and make heavy use of function
  550. pointers and parameter blocks.  I've declared a parameter block which I call
  551. the XferSpec for lack of a catchy name.  It looks intimidating, considering
  552. the number of fields it has, but I've designed it so you can fill in a
  553. minimum number of fields and leave the others blank.
  554.  
  555. The key here are the function pointers.  The GetBlockProc and PutBlockProc
  556. are the heart of the XferSpec, and are also declared in Xfer.h. This makes it
  557. possible to generalize the transfer process, without compromising power.  In
  558. fact, I can do much more with these than I could hacking the BSD ftp code to
  559. special case everything.  You can design your transfer task by changing
  560. writing the transfer functions to do what you want with the data.  You will
  561. see many examples of different ways to munge data using GetBlockProcs and
  562. PutBlockProcs, in Get.c, List.c, Glob.c, Put.c, and more.
  563.  
  564. The other pretty important part of the XferSpec are the responses.  A
  565. transfer actually consists of two responses, a preliminary response, and a
  566. concluding response.  RCmd.c's RDataCmd takes care of getting those filled
  567. in, and you probably won't care about them from there.  The important part is
  568. getting the XferSpec set up and handing it over to RDataCmd, which in turn
  569. calls DataTransfer when the time comes.
  570.  
  571. Filling in the XferSpec should be straight forward.  If this is an actual
  572. file transfer, where a file is being moved, you should fill in a few extra
  573. fields so that transfer progress is reported to the user.  You should not do
  574. that if you aren't doing a file transfer.  The only time you wouldn't want to
  575. do that, is if you are doing a directory listing.  You don't want progress
  576. reports for that.
  577.  
  578. The required fields are:
  579.     int                    netMode;
  580.     GetBlockProc        getBlock;
  581.     PutBlockProc        putBlock;
  582.     int                    inStream;
  583.     int                    outStream;
  584.  
  585. Use the netMode field to tell what you're doing -- reading from the network or
  586. writing to it.  Accordingly, you need to fill in either inStream *OR*
  587. outStream.  RDataCmd looks at netMode and fills in inStream if you're reading
  588. from the network, or outStream if you're writing to it.  You then need to
  589. specify the other one, which is the file you're transferring on the local
  590. side.
  591.  
  592. The getBlock and putBlock fields are pointers to functions are declared by
  593. you.  Your functions should write the data according to the parameters given
  594. to them, and return the number of bytes handled.  The GetBlockProc should
  595. return -1L only if a non-end-of-file error occurred.
  596.  
  597. Once you've filled in the XferSpecPtr, which you got by calling InitXferSpec,
  598. you can give it to RDataCmd and let it do the rest.  When it finishes, you'll
  599. get back a full XferSpec, but you'll probably not care about that and want to
  600. dispose of it with DoneWithXferSpec.
  601.  
  602. *****************************************************************************/
  603.  
  604. /* eof */
  605.