MPFileCopy.c

/*
    File:       MPFileCopy.c
 
    Contains:   An HFS Plus APIs copying engine, which optionally uses pre-emptive threads.
 
    Written by: Quinn
 
    Copyright:  Copyright © 1999 by Apple Computer, Inc., All Rights Reserved.
 
                You may incorporate this Apple sample source code into your program(s) without
                restriction. This Apple sample source code has been provided "AS IS" and the
                responsibility for its operation is yours. You are not permitted to redistribute
                this Apple sample source code as "Apple sample source code" after having made
                changes. If you're going to re-distribute the source, we require that you make
                it clear in the source that the code was descended from Apple sample source
                code, but that you've made changes.
 
    Change History (most recent first):
 
*/
 
/////////////////////////////////////////////////////////////////
 
// MIB Setup
 
#include "MoreSetup.h"
 
// Mac OS Interfaces
 
#include <Files.h>
#include <Multiprocessing.h>
#include <Gestalt.h>
#include <Events.h>
#include <DriverGestalt.h>
#include <Navigation.h>
#include <UTCUtils.h>
 
// Standard C interfaces
 
#include <stdio.h>
#include <stddef.h>
#include <string.h>
 
// Wacky C interfaces (-:
 
#include <SIOUX.h>
 
// Project interfaces
 
#include "TradDriverLoaderLib.h"
#include "MoreInterfaceLib.h"
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- Testing Flags -----
 
// Set kNeverUseMPTasks to true when debugging with a debugger
// that doesn't handle debugging MP tasks.
 
enum {
    kNeverUseMPTasks = false
};
 
// kSpecialCaseClassicForks controls whether the classic
// (resource and data) forks are copied by special case code
// (true) or by the general purpose code (false).  This allows
// you to test the general purpose code on current systems
// which don't support named forks other than the classic ones.
 
enum {
    kSpecialCaseClassicForks = true
};
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- Tunable Parameters -----
 
// The following constants control the behaviour of the copy engine.
 
// BufferSizeForThisVolumeSpeed
 
enum {
//  kDefaultCopyBufferSize =   2L * 1024 * 1024,            // Fast be not very responsive.
    kDefaultCopyBufferSize = 256L * 1024,                   // Slower, but can still use machine.
    kMaximumCopyBufferSize =   2L * 1024 * 1024,
    kMinimumCopyBufferSize = 1024
};
 
// CalculateForksToCopy
 
enum {
    kExpectedForkCount     = 10             // Number of non-classic forks we expect.
};
 
// CopyFolder
 
enum {
    kMaximumFolderDepth     = 100,          // Arbitrary limit needed for stack allocation, see below.
    kFolderItemsPerBulkCall = 20            // Trades File Manager efficiency versus memory usage, see below.
};
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- Utilities -----
 
static OSStatus VolumeContainingFSRef(const FSRef *itemRef, FSVolumeRefNum *volume)
{
    OSStatus err;
    FSCatalogInfo catInfo;
    
    err = FSGetCatalogInfo(itemRef, kFSCatInfoVolume, &catInfo, nil, nil, nil);
    if (err == noErr) {
        *volume = catInfo.volume;
    }
    return err;
}
 
static OSStatus IsFileSharingRunning(Boolean *fileSharingIsRunning)
    // Determines whether File Sharing is running.  It does this in
    // the most paranoid way, by checking to see whether the process
    // is running.  I might be able to just get away with checking
    // whether the volume is being shared, but that would require
    // determining exactly what's causing the problem.  I decided
    // to just be paranoid and say "if the File Sharing process is 
    // around, we act as if File Sharing is running".
{
    OSStatus err;
    Boolean done;
    ProcessInfoRec processInfo;
    ProcessSerialNumber psn;
    
    MoreAssertQ(fileSharingIsRunning != nil);
    *fileSharingIsRunning = false;
    
    // Iterate through the process list.
    
    psn.highLongOfPSN = 0;
    psn.lowLongOfPSN = kNoProcess;
    done = false;
    do {
        err = GetNextProcess(&psn);
        if (err == noErr) {
        
            // Get information on this specific process.
            // If the signature matches File Sharing, 
            // set *fileSharingIsRunning and leave.
            
            MoreBlockZero(&processInfo, sizeof(processInfo));
            processInfo.processInfoLength = sizeof(processInfo);
            err = GetProcessInformation(&psn, &processInfo);
            if (err == noErr && processInfo.processSignature == 'hhgg') {
                *fileSharingIsRunning = true;
                done = true;
            }
        } else if (err == procNotFound) {
            done = true;
            err = noErr;
        }
    } while (err == noErr & ! done);
 
    return err;
}
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- Transfer Buffer Size -----
 
static OSStatus GetFileSystemInfo(FSVolumeRefNum vRefNum,
                                    Boolean *async, 
                                    Boolean *localVolume, 
                                    UInt32  *volumeBytesPerSecond)
    // Returns information about the volume specified by vRefNum.
    // This information allows us to customise our copy engine
    // appropriately.  Customisation includes the size of the
    // copy buffer we allocate, whether we test for asynchronicity
    // by calling the low-level driver, and so on.
{
    OSStatus err;
    GetVolParmsInfoBuffer volParms;
    HParamBlockRec hpb;
    UInt32 bytesPerSecond;
    
    hpb.ioParam.ioNamePtr = nil;
    hpb.ioParam.ioVRefNum = vRefNum;
    hpb.ioParam.ioBuffer = (Ptr) &volParms;
    hpb.ioParam.ioReqCount = sizeof(volParms);
 
    err = PBHGetVolParmsSync(&hpb);
    if (err == noErr) {
        
        // Version 1 of the GetVolParmsInfoBuffer included the vMAttrib
        // field, so we don't really need to test ioActCount.  A noErr
        // result indicates that we have the info we need.  This is
        // just a paranoia check.
        
        MoreAssertQ(hpb.ioParam.ioActCount >= offsetof(GetVolParmsInfoBuffer, vMVolumeGrade));
        
        if (async != nil) {
            *async = ((volParms.vMAttrib & (1 << bSupportsAsyncRequests)) != 0);
        }
        if (localVolume != nil) {
            *localVolume = (volParms.vMServerAdr == 0);
        }
 
        // On the other hand, vMVolumeGrade was not introduced until
        // version 2 of the GetVolParmsInfoBuffer, so we have to explicitly
        // test whether we got a useful value.
        
        if (volumeBytesPerSecond != nil) {
            bytesPerSecond = 0;
            if (hpb.ioParam.ioActCount >= offsetof(GetVolParmsInfoBuffer, vMForeignPrivID)) {
                if (volParms.vMVolumeGrade <= 0) {
                    bytesPerSecond = -volParms.vMVolumeGrade;
                }
            }
            *volumeBytesPerSecond = bytesPerSecond;
        }
    }
    return err;
}
 
static ByteCount BufferSizeForThisVolumeSpeed(UInt32 volumeBytesPerSecond)
    // Calculate an appropriate copy buffer size based on the volumes
    // rated speed.  Our target is to use a buffer that takes 0.25
    // seconds to fill.  This is necessary because the volume might be
    // mounted over a very slow link (like ARA), and if we do a 256 KB
    // read over an ARA link we'll block the File Manager queue for
    // so long that other clients (who might have innocently just
    // called PBGetCatInfoSync) will block for a noticable amount of time.
    //
    // Note that volumeBytesPerSecond might be 0, in which case we assume
    // some default value.
{
    ByteCount bufferSize;
    
    if (volumeBytesPerSecond == 0) {
        bufferSize = kDefaultCopyBufferSize;
    } else {
        // We want to issue a single read that takes 0.25 of a second,
        // so devide the bytes per second by 4.
        bufferSize = volumeBytesPerSecond / 4;
    }
    
    // Round bufferSize down to 512 byte boundary.
    
    bufferSize &= ~0x01FF;
    
    // Clip to sensible limits.
    
    if (bufferSize < kMinimumCopyBufferSize) {
        bufferSize = kMinimumCopyBufferSize;
    } else if (bufferSize > kMaximumCopyBufferSize) {
        bufferSize = kMaximumCopyBufferSize;
    }
    return bufferSize;
}
 
static ByteCount BufferSizeForThisVolume(FSVolumeRefNum vRefNum)
    // This routine calculates the appropriate buffer size for
    // the given vRefNum.  It's a simple composition of GetFileSystemInfo
    // BufferSizeForThisVolumeSpeed.
{
    ByteCount volumeBytesPerSecond;
    
    if ( GetFileSystemInfo(vRefNum, nil, nil, &volumeBytesPerSecond) != noErr ) {
        volumeBytesPerSecond = 0;
    }
    return BufferSizeForThisVolumeSpeed(volumeBytesPerSecond);
}
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- True Async Tests -----
 
// The following code is use to determine whether the File Manager should
// be called asynchronously or not by testing certain low-level functionality.
// We do this because certain machines, like the Power Macintosh 6500, which
// should support asynchronous I/O, don't do so in all circumstances.  Instead,
// all local hard disk I/O is done synchronously.  This causes us problems, 
// as described below.
//
// When you do synchronous I/O from an MP task, the actual I/O is done
// asynchronously from a 'software interrupt' (technically known as a
// SWI, pronounced "swee").  This SWI is just like a hardware
// interrupt in that it can interrupt the current foreground task at any
// instruction boundary (as long as the interrupt is not masked).  It also
// (currently) executes with interrupts masked.
//
// If the underlying hardware does not work asynchronously, executing
// asynchronous File Manager I/O from this SWI results in a very poor
// user experience.  Specifically, interrupts remain masked for the duration
// of the I/O, which causes the mouse to jerk horribly.
//
// So, our goal is to not do File Manager operations from MP tasks on machines
// that have this problem.  We detect this in a three stage process, implemented
// by the IsVolumeTrulyAsync routine.
//
// 1. Determine whether the volume's file system is asynchronous.  Some
//    external file systems blow up if you call them asynchronously.
//
// 2. Determine whether the device driver thinks it runs asynchronously.   Some
//    older hard disk device drivers just don't support asynchronous operations.
//
// 3. If the volume is local, determine whether the driver is really running
//    asynchronously.  This is important on machines like the Power Macintosh
//    6500, where the device driver is asynchronous but the ATA Manager isn't.
//    We do this by issuing a large PBReadAsync and checking to see whether
//    the I/O completion runs before the PBReadAsync returns.
//
// If all three of these checks give the green light on both the source and
// destination volumes, we run the copying engine from an MP task.  If they 
// don't, we do everything from system task level code.
//
// Note that somes of these checks aren't possible within a Carbon application.
// This won't be a problem on Mac OS X (because all drivers are async there)
// but it could be a problem for Carbon applications on traditional Mac OS.
// I don't have a good solution to that problem at this time.
 
#if !TARGET_API_MAC_CARBON
 
// The following code implements part 3 of the true asynchronicity testing
// algorithm, described above.  We 
 
volatile static Boolean gEarlyCompletion;
 
static IOCompletionUPP gMyCompletionUPP;        // points to MyCompletion
 
static void MyCompletion(ParmBlkPtr paramBlock)
    // The I/O completion routine for the large read request we
    // use for a true test of asynchronicity.
{
    #pragma unused(paramBlock)
    
    gEarlyCompletion = true;
}
 
static OSStatus IsDeviceTrulyAsync(Boolean *async, UInt32 volumeBytesPerSecond, DriverRefNum refNum, SInt16 driveNum)
    // This routine implements part 3 of the true asynchronicity testing
    // algorithm, described above.
    //
    // We make the assumption that the device size is greater
    // than or equal to kMaximumCopyBufferSize, which isn't necessarily
    // true.  Should probably revisit that decision at some time.
{
    OSStatus err;
    Ptr copyBuffer;
    ParamBlockRec pb;
    ByteCount bufferSize;
 
    // Calculate what we would consider to be a "large" request.
    
    bufferSize = BufferSizeForThisVolumeSpeed(volumeBytesPerSecond);
 
    MoreAssertQ(gMyCompletionUPP != nil);
 
    // Create the test buffer.
    
    copyBuffer = NewPtr(bufferSize);
    err = MemError();
    if (err == noErr) {
        gEarlyCompletion = false;
        
        // Issue the request.
        
        pb.ioParam.ioCompletion = gMyCompletionUPP;
        pb.ioParam.ioVRefNum    = driveNum;
        pb.ioParam.ioRefNum     = refNum;
        pb.ioParam.ioBuffer     = copyBuffer;
        pb.ioParam.ioReqCount   = bufferSize;
        pb.ioParam.ioPosMode    = fsFromStart;
        pb.ioParam.ioPosOffset  = 0;
 
        err = PBReadAsync(&pb);
        
        // Did we complete early.  Note that this sort of code often
        // needs to use an atomic test and set, but in this case that's
        // not necessary because MyCompletion doesn't ever read
        // gEarlyCompletion.
        
        *async = ! gEarlyCompletion;
        
        // Wait for the I/O to actually complete.
        
        while ( pb.ioParam.ioResult > 0 ) {
            // do nothing
        }
 
        // Clean up.
        
        DisposePtr( copyBuffer );
        MoreAssertQ(MemError() == noErr);
    }
    return err;
}
 
static OSStatus IsDeviceAsync(FSVolumeRefNum vRefNum, Boolean *async, DriverRefNum *refNum, SInt16 *driveNum)
    // This routine asks the device driver whether it thinks it's asynchronous
    // using Driver Gestalt.
{
    OSStatus err;
    HParamBlockRec hpb;
    DriverGestaltParam pb;
    DriverFlags flags;
    
    // First find the driver and drive on which the volume is mounted.
    
    hpb.volumeParam.ioNamePtr = nil;
    hpb.volumeParam.ioVRefNum = vRefNum;
    hpb.volumeParam.ioVolIndex = 0;
    err = PBHGetVInfoSync(&hpb);
 
    // Special case for offline volumes.  We're not going anywhere near them.
    
    if (err == noErr) {
        if (hpb.volumeParam.ioVDrvInfo == 0) {
            err = offLinErr;
        }
    }
    if (err == noErr) {
        *refNum   = hpb.volumeParam.ioVDRefNum;
        *driveNum = hpb.volumeParam.ioVDrvInfo;
    }
    
    // Then check whether the driver support Driver Gestalt.
    
    if (err == noErr) {
        err = TradGetDriverInformation(*refNum, nil, &flags, nil, nil);
    }
    if (err == noErr) {
        if (TradDriverGestaltIsOn(flags)) {
            // If the driver supports Driver Gestalt, we ask it
            // whether it thinks it supports async.
            
            pb.ioVRefNum = *driveNum;
            pb.ioCRefNum = *refNum;
            pb.csCode = kDriverGestaltCode;
            pb.driverGestaltSelector = kdgSync;
            err = PBStatusSync( (ParamBlockRec *) &pb);
            if (err == noErr) {
                *async = ! (GetDriverGestaltSyncResponse(&pb)->behavesSynchronously);
            }
        } else {
            // If the driver doesn't support Driver Gestalt,
            // we return true.  This will cause the caller
            // to try a real test based on I/O.  We do this
            // because certain really old drivers, like ".Sony",
            // are async even though they don't support Driver
            // Gestalt.
            
            *async = true;
        }
    }
    return err;
}
 
#endif
 
static OSStatus IsVolumeTrulyAsync(FSVolumeRefNum vRefNum, Boolean *async)
    // This routine sets *async to true if the volume can, within
    // the bounds of our ability to tell, operate asynchronously.
    // The algorithm is described above.
{
    OSStatus err;
    Boolean localVolume;
    UInt32  volumeBytesPerSecond;
 
    err = GetFileSystemInfo(vRefNum, async, &localVolume, &volumeBytesPerSecond);
    #if TARGET_API_MAC_CARBON
        // To a first approximation, all device drivers are async on Carbon.
        // Actually, we still have the Power Mac 6500 problem on Carbon on
        // traditional Mac OS, but I'm going to ignore that for the moment.
    #else
        {
            DriverRefNum refNum;
            SInt16 driveNum;
 
            if (err == noErr && *async) {
                err = IsDeviceAsync(vRefNum, async, &refNum, &driveNum);
            }
            if (err == noErr && *async && localVolume) {
                err = IsDeviceTrulyAsync(async, volumeBytesPerSecond, refNum, driveNum);
            }
        }
    #endif
    return err;
}
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- Copy Engine -----
 
struct CopyParams {
    void            *copyBuffer;
    ByteCount       copyBufferSize;
    UInt32          recursionLimit;
    UInt32          currentDepth;
    Boolean         copyingToDropFolder;
    Boolean         copyingToLocalVolume;
};
typedef struct CopyParams CopyParams;
 
// The tasks stack size is a combination of two factors:
//
// 1. Non-recursive overhead, about 3 KB, which is the
//    sum of the frame sizes of the various non-recursive
//    routines in the maximum depth copy engine call chain.
//
// 2. Recursive overhead, about 200 bytes per recursion level,
//    which is the frame size of the CopyFolder routine.
//
// With the current numbers, this works out to less than 24 KB,
// which is not a problem.
 
enum {
    kCopyFolderFrameSize = 208,
    
    kCopyFileTaskStackSize = (3 * 1024) + (kMaximumFolderDepth * kCopyFolderFrameSize)
};                      
 
static UTCDateTime gMagicBusyCreationDate;      // a UTCDateTime equivalent of kMagicBusyCreationDate.
 
static HFSUniStr255 gDataForkName;
static HFSUniStr255 gRsrcForkName;
 
static OSStatus CopyFork(const FSRef *source, SInt64 sourceForkSize, const FSRef *dest, 
                            SInt16 forkDestRefNum,
                            ConstHFSUniStr255Param forkName,
                            const CopyParams *params)
    // Copies the fork whose name is forkName from source to dest.
    // A refnum for the destination fork may be supplied in forkDestRefNum.
    // If forkDestRefNum is 0, we must open the destination fork ourselves,
    // otherwise it has been opened for us and we shouldn't close it.
    //
    // Frame Size: 128
{
    OSStatus err;
    OSStatus err2;
    SInt16 sourceRef;
    SInt16 destRef;
    SInt64 bytesRemaining;
    SInt64 bytesToReadThisTime;
    SInt64 bytesToWriteThisTime;
    
    // If we haven't been passed in a forkDestRefNum (which basically
    // means we're copying into a non-drop folder), create the destination
    // fork.  We have to do this regardless of whether sourceForkSize is 
    // 0, because we want to preserve empty forks.
    
    if (forkDestRefNum == 0) {
        err = FSCreateFork(dest, forkName->length, forkName->unicode);
        
        // Mac OS 9.0 has a bug (in the AppleShare external file system,
        // I think) [2410374] that causes FSCreateFork to return an errFSForkExists
        // error even though the fork is empty.  The following code swallows
        // the error (which is harmless) in that case.
        
        if (err == errFSForkExists && !params->copyingToLocalVolume) {
            err = noErr;
        }
    }
 
    // The remainder of this code only applies if there is actual data
    // in the source fork.
    
    if (err == noErr && sourceForkSize != 0) {
 
        // Prepare for failure.
        
        sourceRef = 0;
        destRef   = 0;
 
        // Open up the destination fork, if we're asked to, otherwise
        // just use the passed in forkDestRefNum.
        
        if (forkDestRefNum == 0) {
            err = FSOpenFork(dest, forkName->length, forkName->unicode, fsWrPerm, &destRef);
        } else {
            destRef = forkDestRefNum;
        }
        
        // Open up the source fork.
        
        if (err == noErr) {
            err = FSOpenFork(source, forkName->length, forkName->unicode, fsRdPerm, &sourceRef);
        }
 
        // Here we create space for the entire fork on the destination volume.
        // FSAllocateFork has the right semantics on both traditional Mac OS
        // and Mac OS X.  On traditional Mac OS it will allocate space for the
        // file in one hit without any other special action.  On Mac OS X,
        // FSAllocateFork is preferable to FSSetForkSize because it prevents
        // the system from zero filling the bytes that were added to the end
        // of the fork (which would be waste becasue we're about to write over
        // those bytes anyway.
        
        if (err == noErr) {
            err = FSAllocateFork(destRef, kFSAllocNoRoundUpMask, fsFromStart, 0, sourceForkSize, nil);
        }
 
        // Copy the file from the source to the destination in chunks of
        // no more than params->copyBufferSize bytes.  This is fairly
        // boring code except for the bytesToReadThisTime/bytesToWriteThisTime
        // distinction.  On the last chunk, we round bytesToWriteThisTime
        // up to the next 512 byte boundary and then, after we exit the loop,
        // we set the file's EOF back to the real location (if the fork size
        // is not a multiple of 512 bytes).
        // 
        // This technique works around a 'bug' in the traditional Mac OS File Manager,
        // where the File Manager will put the last 512-byte block of a large write into
        // the cache (even if we specifically request no caching) if that block is not
        // full. If the block goes into the cache it will eventually have to be
        // flushed, which causes sub-optimal disk performance.
        //
        // This is only done if the destination volume is local.  For a network
        // volume, it's better to just write the last bytes directly.
        //
        // This is extreme over-optimisation given the other limits of this
        // sample, but I will hopefully get to the other limits eventually.
        
        bytesRemaining = sourceForkSize;
        while (err == noErr && bytesRemaining != 0) {
            if (bytesRemaining > params->copyBufferSize) {
                bytesToReadThisTime  = params->copyBufferSize;
                bytesToWriteThisTime = bytesToReadThisTime;
            } else {
                bytesToReadThisTime  = bytesRemaining;
                if (params->copyingToLocalVolume) {
                    bytesToWriteThisTime = (bytesRemaining + 0x01FF) & ~0x01FF;
                } else {
                    bytesToWriteThisTime = bytesRemaining;
                }
            }
            err = FSReadFork(sourceRef, fsAtMark + noCacheMask, 0, bytesToReadThisTime, params->copyBuffer, nil);
            if (err == noErr) {
                err = FSWriteFork(destRef, fsAtMark + noCacheMask, 0, bytesToWriteThisTime, params->copyBuffer, nil);
            }
            if (err == noErr) {
                bytesRemaining -= bytesToReadThisTime;
            }
        }
        if (err == noErr && (params->copyingToLocalVolume && ((sourceForkSize & 0x01FF) != 0)) ) {
            err = FSSetForkSize(destRef, fsFromStart, sourceForkSize);
        }
 
        // Clean up.
        
        if (sourceRef != 0) {
            err2 = FSCloseFork(sourceRef);
            MoreAssertQ(err2 == noErr);
            if (err == noErr) {
                err = err2;
            }
        }
 
        // Only close destRef if we were asked to open it (ie forkDestRefNum == 0) and
        // we actually managed to open it (ie destRef != 0).
        
        if (forkDestRefNum == 0 && destRef != 0) {
            err2 = FSCloseFork(destRef);
            MoreAssertQ(err2 == noErr);
            if (err == noErr) {
                err = err2;
            }
        }
    }
 
    return err;
}
 
// The ForkTracker data structure holds information about a specific fork,
// specifically the name and the refnum.  We use this to build a list of
// all the forks before we start copying them.  We need to do this because,
// if we're copying into a drop folder, we must open all the forks before
// we start copying data into any of them.
//
// For debug builds, we pad the ForkTracker record out to be a multiple of
// 8 bytes long.  This is because the debug builds use MPGetAllocatedBlockSize
// in many assertions, but MPGetAllocatedBlockSize only returns the size of
// the block to an 8 byte resolution.  Rather than second guess its results,
// we just ensure that the ForkTracker element size for debug build is
// a multiple of 8.  Also, we have to use 68K alignment because that's
// the only guaranteed way to force a specific structure layout.
 
#pragma options align=mac68k
 
struct ForkTracker {
    HFSUniStr255    forkName;
    SInt64          forkSize;
    SInt16          forkDestRefNum;
    #if MORE_DEBUG
        SInt16       pad[3];
    #endif
};
typedef struct ForkTracker ForkTracker;
typedef ForkTracker *ForkTrackerPtr;
 
#pragma options align=reset
 
static Boolean IdenticalHFSUniStr255(const HFSUniStr255 *lhs, const HFSUniStr255 *rhs)
    // MP Note:  I can call memcmp here because it's implement in 
    // a library that's statically linked in to our application and
    // I checked the code in that library and it doesn't break the
    // rules for pre-emptive tasks.
    //
    // Frame Size: 80
{
    return (lhs->length == rhs->length)
        && (memcmp(lhs->unicode, rhs->unicode, lhs->length * sizeof(UniChar)) == 0);
}
 
static OSStatus CalculateForksToCopy(const FSRef *source,
                                        SInt64 *dataForkSize,
                                        SInt64 *rsrcForkSize,
                                        ForkTrackerPtr *otherForksParam,
                                        ItemCount      *otherForksCountParam)
    // This routine determines the list of forks that a file has.
    // dataFork is set if the file has a data fork.
    // rsrcFork is set if the file has a resource fork.
    // otherForksParam is set to point to a memory block allocated with
    // MPAllocAligned if the file has forks beyond the resource and data
    // forks.  You must free that block with MPFree.  otherForksCountParam
    // is set to the count of the number of forks in the otherForksParam
    // array.  This count does *not* include the resource and data forks.
    //
    // Frame Size: 656
{
    OSStatus        err;
    Boolean         done;
    CatPositionRec  iterator;
    HFSUniStr255    thisForkName;
    SInt64          thisForkSize;
    ForkTrackerPtr  otherForks;
    ItemCount       otherForksCount;
    ItemCount       otherForksMemoryBlockCount;
    #if MORE_DEBUG
        Boolean         doneDataFork = false;
        Boolean         doneRsrcFork = false;
    #endif
    
    MoreAssertQ(source != nil);
    MoreAssertQ(dataForkSize != nil);
    MoreAssertQ(rsrcForkSize != nil);
    MoreAssertQ(otherForksParam != nil);
    MoreAssertQ(*otherForksParam == nil);
    MoreAssertQ(otherForksCountParam != nil);
    MoreAssertQ(*otherForksCountParam == 0);
    
    MoreAssertQ(sizeof(ForkTracker) % 8 == 0);      // See note above.
    
    *dataForkSize = 0;
    *rsrcForkSize = 0;
    
    otherForks = nil;
    otherForksCount = 0;
 
    // Iterate through the list of forks, processing each fork name in turn.
    
    iterator.initialize = 0;
    done = false;
    do {
        err = FSIterateForks(source, &iterator, &thisForkName, &thisForkSize, nil);
        if (err == errFSNoMoreItems) {
            err = noErr;
            done = true;
        } else if (err == noErr) {
            if ( kSpecialCaseClassicForks && IdenticalHFSUniStr255(&thisForkName, &gDataForkName) ) {
                #if MORE_DEBUG
                    // only one data fork, please
                    MoreAssertQ(doneDataFork == false);
                    doneDataFork = true;
                #endif
                *dataForkSize = thisForkSize;
            } else if ( kSpecialCaseClassicForks && IdenticalHFSUniStr255(&thisForkName, &gRsrcForkName) ) {
                #if MORE_DEBUG
                    // only one resource fork, please
                    MoreAssertQ(doneRsrcFork == false);
                    doneRsrcFork = true;
                #endif
                *rsrcForkSize = thisForkSize;
            } else {
 
                // We've found a fork other than the resource and data forks.
                // We have to add it to the otherForks array.  But the array
                // a) may not have been created yet, and b) may not contain
                // enough elements to hold the new fork.
                
                if (otherForks == nil) {
                
                    // The array hasn't been allocated yet, allocate it.
                    
                    otherForksMemoryBlockCount = kExpectedForkCount;
                    otherForks = MPAllocateAligned(sizeof(ForkTracker) * kExpectedForkCount, kMPAllocateDefaultAligned, kNilOptions);
                    if (otherForks == nil) {
                        err = memFullErr;
                    }
                } else {
                
                    // If the array doesn't contain enough elements, grow it.
                    
                    if (otherForksCount == otherForksMemoryBlockCount) {
                        ForkTrackerPtr newOtherForks;
                        
                        newOtherForks = MPAllocateAligned(sizeof(ForkTracker) * (otherForksCount + kExpectedForkCount), kMPAllocateDefaultAligned, kNilOptions);
                        if (otherForks == nil) {
                            err = memFullErr;
                        } else {
                            BlockMoveData(otherForks, newOtherForks, sizeof(ForkTracker) * otherForksCount);
                            otherForksMemoryBlockCount += kExpectedForkCount;
                            MPFree(otherForks);
                            otherForks = newOtherForks;
                        }
                    }
                }
                
                // If we have no error, we know we have space in the otherForks
                // array to place the new fork.  Put it there and increment the
                // count of forks.
                
                if (err == noErr) {
                    MoreAssertQ(otherForks != nil);
                    MoreAssertQ(otherForksCount < otherForksMemoryBlockCount);
                    MoreAssertQ(MPGetAllocatedBlockSize(otherForks) == sizeof(ForkTracker) * otherForksMemoryBlockCount);
                    
                    BlockMoveData(&thisForkName, &otherForks[otherForksCount].forkName, sizeof(thisForkName));
                    otherForks[otherForksCount].forkSize = thisForkSize;
                    otherForks[otherForksCount].forkDestRefNum = 0;
                    otherForksCount += 1;
                }
            }
        }
    } while (err == noErr && ! done);
    
    // Clean up.
    
    if (err != noErr) {
        if (otherForks != nil) {
            MPFree(otherForks);
            otherForks = nil;
        }
        otherForksCount = 0;
    }
    *otherForksParam = otherForks;
    *otherForksCountParam = otherForksCount;
 
    // I had to break up the post-conditions into multiple statements
    // because otherwise they overflow the string length limit of the pre-processor.
    // Jeff Rohl would be proud.
    
    if (err == noErr) {
        // Success post-condition
        MoreAssertQ(   ((otherForks == nil) && (otherForksCount == 0)) 
                    || ((otherForks != nil) && (otherForksCount <= otherForksMemoryBlockCount) && (MPGetAllocatedBlockSize(otherForks) == sizeof(ForkTracker) * otherForksMemoryBlockCount)));
    } else {
        // Failure post-condition
        MoreAssertQ((otherForks == nil) && (otherForksCount == 0));
    }
 
    return err; 
}
 
static OSStatus OpenAllForks(const FSRef *dest, Boolean openDataFork, Boolean openRsrcFork, 
                            SInt16 *dataRefNum, SInt16 *rsrcRefNum, ForkTrackerPtr otherForks, ItemCount otherForksCount)
    // Open all the forks of the file.  We need to do this when we're copying
    // into a drop folder, where you must open all the forks before starting
    // to write to any of them.
    //
    // IMPORTANT:  If it fails, this routine won't close forks that opened successfully.
    // You must call CloseAllForks regardless of whether this routine returns an error.
    //
    // Frame Size: 96
{
    OSStatus  err;
    ItemCount thisForkIndex;
 
    MoreAssertQ(dataRefNum != nil); 
    MoreAssertQ(rsrcRefNum != nil); 
    MoreAssertQ(*dataRefNum == 0);
    MoreAssertQ(*rsrcRefNum == 0);
    MoreAssertQ((otherForks == nil) || (otherForksCount > 0));
 
    // Open the resource and data forks as a special case.
    
    err = noErr;
    if (openDataFork) {
        // Data fork never needs to be created, so I don't have to FSCreateFork it here.
        err = FSOpenFork(dest, gDataForkName.length, gDataForkName.unicode, fsWrPerm, dataRefNum);
    }
    if (err == noErr && openRsrcFork) {
        // Resource fork never needs to be created, so I don't have to FSCreateFork it here.
        err = FSOpenFork(dest, gRsrcForkName.length, gRsrcForkName.unicode, fsWrPerm, rsrcRefNum);
    }
    
    // Open the other forks.
    
    if (err == noErr) {
        for (thisForkIndex = 0; thisForkIndex < otherForksCount; thisForkIndex++) {
            MoreAssertQ(otherForks[thisForkIndex].forkDestRefNum == 0);
 
            // Create the fork.  Swallow afpAccessDenied because this operation
            // causes the external file system compatibility shim in Mac OS 9 to
            // generate a GetCatInfo request to the AppleShare external file system,
            // which in turn causes an AFP GetFileDirParms request on the wire,
            // which the AFP server bounces with afpAccessDenied because the file
            // is in a drop folder.  As there's no native support for non-classic
            // forks in current AFP, there's no way I can decide how I should
            // handle this in a non-test case.  So I just swallow the error and
            // hope that when native AFP support arrives, the right thing will happen.
            
            err = FSCreateFork(dest, otherForks[thisForkIndex].forkName.length, otherForks[thisForkIndex].forkName.unicode);
            if (err == noErr || err == afpAccessDenied) {
                err = noErr;
            }
            
            // Previously I avoided opening up the fork if the fork if the
            // length was empty, but that confused CopyFork into thinking
            // this wasn't a drop folder copy, so I decided to simply avoid
            // this trivial optimisation.  In drop folders, we always open
            // all forks.
            
            if (err == noErr) {
                err = FSOpenFork(dest, otherForks[thisForkIndex].forkName.length, otherForks[thisForkIndex].forkName.unicode, fsWrPerm, &otherForks[thisForkIndex].forkDestRefNum);
            }
            if (err != noErr) {
                break;
            }
        }
    }
    return err;
}
 
static OSStatus CloseAllForks(SInt16 dataRefNum, SInt16 rsrcRefNum, ForkTrackerPtr otherForks, ItemCount otherForksCount)
    // Close all the forks that might have been opened by OpenAllForks.
    //
    // Frame Size: 96
{
    OSStatus  err;
    OSStatus  err2;
    ItemCount thisForkIndex;
 
    MoreAssertQ((otherForks == nil) || (otherForksCount > 0));
 
    err = noErr;
    if (dataRefNum != 0) {
        err2 = FSCloseFork(dataRefNum);
        MoreAssertQ(err2 == noErr);
        if (err == noErr) {
            err = err2;
        }
    }
    if (rsrcRefNum != 0) {
        err2 = FSCloseFork(rsrcRefNum);
        MoreAssertQ(err2 == noErr);
        if (err == noErr) {
            err = err2;
        }
    }
    for (thisForkIndex = 0; thisForkIndex < otherForksCount; thisForkIndex++) {
        if (otherForks[thisForkIndex].forkDestRefNum != 0) {
            err2 = FSCloseFork(otherForks[thisForkIndex].forkDestRefNum);
            MoreAssertQ(err2 == noErr);
            if (err == noErr) {
                err = err2;
            }
        }
    }
    return err;
}
 
static OSStatus CopyItemsForks(const FSRef *source, const FSRef *dest, CopyParams *params)
    // Copy all the folks of a file or folder from source to dest.
    //
    // Frame Size: 112 (+ 656 in sub-routines)
{
    OSStatus        err;
    OSStatus        err2;
    SInt64          dataForkSize;
    SInt64          rsrcForkSize;
    ForkTrackerPtr  otherForks;
    ItemCount       otherForksCount;
    SInt16          dataRefNum;
    SInt16          rsrcRefNum;
    ItemCount       thisForkIndex;
    
    dataRefNum = 0;
    rsrcRefNum = 0;
    otherForks = nil;
    otherForksCount = 0;
    
    // First determine the list of forks that the source has.
    
    err = CalculateForksToCopy(source, &dataForkSize, &rsrcForkSize, &otherForks, &otherForksCount);
    if (err == noErr) {
    
        // If we're copying into a drop folder, open up all of those forks.
        // We have to do this because, once we've starting writing to a fork
        // in a drop folder, we can't open any more forks.
        //
        // We only do this if we're copying into a drop folder in order
        // to conserve FCBs in the more common, non-drop folder case.
        
        if (params->copyingToDropFolder) {
            err = OpenAllForks(dest, (dataForkSize != 0), (rsrcForkSize != 0), &dataRefNum, &rsrcRefNum, otherForks, otherForksCount);
        }
        
        // Copy each fork.
        
        if (err == noErr && (dataForkSize != 0)) {
            err = CopyFork(source, dataForkSize, dest, dataRefNum, &gDataForkName, params);
        }
        if (err == noErr && (rsrcForkSize != 0)) {
            err = CopyFork(source, rsrcForkSize, dest, rsrcRefNum, &gRsrcForkName, params);
        }
        if (err == noErr) {
            for (thisForkIndex = 0; thisForkIndex < otherForksCount; thisForkIndex++) {
                err = CopyFork(source, otherForks[thisForkIndex].forkSize, 
                                dest, otherForks[thisForkIndex].forkDestRefNum, &otherForks[thisForkIndex].forkName, params);
                if (err != noErr) {
                    break;
                }
            }
        }
        
        // Close any forks that might be left open.  Note that we have to call
        // this regardless of an error.  Also note that this only closes forks
        // that were opened by OpenAllForks.  If we're not copying into a drop
        // folder, the forks are opened and closed by CopyFork.
        
        err2 = CloseAllForks(dataRefNum, rsrcRefNum, otherForks, otherForksCount);
        if (err == noErr) {
            err = err2;
        }
    }
 
    // Clean up.
    
    if (otherForks != nil) {
        MPFree(otherForks);
    }
 
    return err;
}
 
static volatile UInt32 gState = 0;      // Used only during debugging.
 
static Boolean gFileSharingIsRunning;
 
static OSStatus CopyFile(const FSRef *source, FSCatalogInfo *sourceCatInfo,
                            const FSRef *destDir, ConstHFSUniStr255Param destName,
                            CopyParams *params)
    // Copies a file referenced by source to the directory referenced by
    // destDir.  destName is the name the file should be given in the
    // destination directory.  sourceCatInfo is the catalogue info of
    // the file, which is passed in as an optimisation (we could get it
    // by doing a FSGetCatalogInfo but the caller has already done that
    // so we might as well take advantage of that).
    //
    // Frame Size: 176 (+ 112 + 656 in sub-routines)
{
    OSStatus err;
    OSStatus junk;
    FSRef    dest;
    OSType   originalFileType;
    UInt16   originalNodeFlags;
    UTCDateTime originalCreateDate;
 
    // If we're copying to a drop folder, we won't be able to reset this
    // information once the copy is done, so we don't mess it up in
    // the first place.  We still clear the locked bit though; items dropped
    // into a drop folder always become unlocked.
    
    if (!params->copyingToDropFolder) {
        // Remember and clear the file's type, so the Finder doesn't
        // look at the file until we're done.
        
        originalFileType = ((FInfo *) &sourceCatInfo->finderInfo)->fdType;
        ((FInfo *) &sourceCatInfo->finderInfo)->fdType = kFirstMagicBusyFiletype;
 
        // Set the file's creation date to kMagicBusyCreationDate,
        // remembering the old value for restoration later.
        
        originalCreateDate = sourceCatInfo->createDate;
        sourceCatInfo->createDate = gMagicBusyCreationDate;
 
        // Remember and clear the file's locked status, so that we can
        // actually write the forks we're about to create.
        
        originalNodeFlags = sourceCatInfo->nodeFlags;
    }
    sourceCatInfo->nodeFlags &= ~kFSNodeLockedMask;
    
    // Create the file in destDir.
    
    // In Mac OS 9.0, File Sharing messes up calls to FSCreateFileUnicode such that,
    // if you pass in sourceCatInfo, the file will be created with a blank file
    // type and creator code.  [2397324]  I work around this by, if File
    // Sharing is running, splitting the create into two parts: first create the file,
    // then set the catalogue info.  This defeats the purpose of the unified call (ie
    // the operation isn't atomic) but them's the breaks.  Also, I found that if you
    // set the catalogue info directly after creating the file, you trigger another
    // aspect of this File Sharing problem and you end up with weird zombie files that
    // are half locked and half unlocked.  To solve this I don't set any catalogue
    // info here and set all the info at the end of the routine, where normally I just
    // restore the modification dates etc.
    //
    // This should be OK because the FSCreateFileUnicode creates a file with a zero
    // fdType, which the Finder will ignore.
    //
    // I'm scared now.
    
    if ( ! gFileSharingIsRunning || params->copyingToDropFolder) {
        // File sharing not running, let's do this the right way.
        err = FSCreateFileUnicode(destDir, destName->length, destName->unicode, kFSCatInfoSettableInfo, sourceCatInfo, &dest, nil);
    } else {
        // File sharing is running, use the workaround.
        err = FSCreateFileUnicode(destDir, destName->length, destName->unicode, kFSCatInfoNone, nil, &dest, nil);
    }
    
    // Iterate through the forks, copying each fork.
    
    if (err == noErr) {
        err = CopyItemsForks(source, &dest, params);
 
        // Restore the original file type, creation and modification dates, and
        // locked status.  This is one of the places where we need to handle drop
        // folders as a special case because this FSSetCatalogInfo will fail for
        // an item in a drop folder, so we don't even attempt it.
        //
        // Also, if File Sharing is running we use this point to set up all
        // the catalogue info.  
        
        if (err == noErr && !params->copyingToDropFolder) {
            ((FInfo *) &sourceCatInfo->finderInfo)->fdType = originalFileType;
            sourceCatInfo->createDate = originalCreateDate;
            sourceCatInfo->nodeFlags  = originalNodeFlags;
            if (gFileSharingIsRunning) {
                err = FSSetCatalogInfo(&dest, kFSCatInfoCreateDate
                                            | kFSCatInfoAttrMod 
                                            | kFSCatInfoContentMod 
                                            | kFSCatInfoFinderInfo 
                                            | kFSCatInfoNodeFlags, sourceCatInfo);
            } else {
                err = FSSetCatalogInfo(&dest, kFSCatInfoSettableInfo, sourceCatInfo);
            }
        }
        
        // If we created the file and the copy failed, try to clean up by
        // deleting the file we created.  We do this because, while it's
        // possible for the copy to fail halfway through and we don't really
        // clean up that well, we *really* don't wan't any half-created
        // files being left around.
        //
        // Note that there are cases where the assert can fire which are not
        // errors (for example, if the  destination is in a drop folder) but
        // I'll leave it in anyway because I'm interested in discovering those
        // cases.  Note that, if this fires and we're running MP, current versions
        // of MacsBug won't catch the exception and the MP task will terminate
        // with an kMPTaskAbortedErr error.
        
        if (err != noErr) {
            junk = FSDeleteObject(&dest);
            MoreAssertQ(junk == noErr);
        }
    }
    
    return err;
}
 
static OSStatus CopyFolder(const FSRef *source, FSCatalogInfo *sourceCatInfo,
                            const FSRef *destDir, ConstHFSUniStr255Param destName,
                            CopyParams *params)
    // Copies a folder referenced by source to the directory referenced by
    // destDir.  destName is the name the folder should be given in the
    // destination directory.  sourceCatInfo is the catalogue info of
    // the folder, which is passed in as an optimisation (we could get it
    // by doing a FSGetCatalogInfo but the caller has already done that
    // so we might as well take advantage of that).
    //
    // IMPORTANT:  This routine calls itself recursively, so adding local
    // variables can significantly effect the stack usage of the operation.
    // If you add local variables, remember to update the constant
    // kCopyFolderFrameSize to be greater than or equal to the real frame size.
    //
    // Frame Size: 208 (see kCopyFolderFrameSize) (+ 176 + 112 + 656 in sub-routines)
{
    OSStatus      err;                  // stack,  4 bytes
    OSStatus      junk;                 // stack,  4 bytes
    FSRef         newDir;               // stack, 80 bytes
    FSIterator    iterator;             // stack,  4 bytes
    ItemCount     foundItems;           // stack,  4 bytes
    FSCatalogInfo *foundCatInfos;       // heap, 144 bytes per element
    FSRef         *foundFSRefs;         // heap,  80 bytes per element
    HFSUniStr255  *foundNames;          // heap, 512 bytes per element
    Boolean       done;                 // stack,  4 bytes
    ItemCount     i;                    // stack,  4 bytes
    UTCDateTime   originalCreateDate;   // stack,  8 bytes
 
    // Check that we haven't run off the bottom of our stack.
    
    err = noErr;
    params->currentDepth += 1;
    if (params->currentDepth > params->recursionLimit) {
        err = -1;
    }
 
    // Allocate memory for temporary buffers.  
    //
    // 736 bytes per element per kFolderItemsPerBulkCall (currently 20) per folder nesting depth
    
    if (err == noErr) {
        foundCatInfos = MPAllocateAligned(kFolderItemsPerBulkCall * sizeof(FSCatalogInfo),
                                            kMPAllocateDefaultAligned, kNilOptions);
        foundFSRefs   = MPAllocateAligned(kFolderItemsPerBulkCall * sizeof(FSRef),
                                            kMPAllocateDefaultAligned, kNilOptions);
        foundNames    = MPAllocateAligned(kFolderItemsPerBulkCall * sizeof(HFSUniStr255),
                                            kMPAllocateDefaultAligned, kNilOptions);
        if (foundCatInfos == nil || foundFSRefs == nil || foundNames == nil) {
            err = memFullErr;
        }
    }
 
    // Create the destination directory.
    
    if (err == noErr) {
 
        // Set the folder's creation date to kMagicBusyCreationDate
        // so that the Finder doesn't mess with the folder while
        // it's copying.  We remember the old value for restoration
        // later.  We only do this if we're not copying to a drop
        // folder, because if we are copying to a drop folder we don't
        // have the opportunity to reset the information at the end of
        // this routine.
        
        if (!params->copyingToDropFolder) {
            originalCreateDate = sourceCatInfo->createDate;
            sourceCatInfo->createDate = gMagicBusyCreationDate;
        }
 
        err = FSCreateDirectoryUnicode(destDir, destName->length, destName->unicode,
                 kFSCatInfoSettableInfo,
                 sourceCatInfo,
                 &newDir, nil, nil);
    }
 
    // With the new APIs, folders can have forks as well as files.  Before
    // we start copying items in the folder, we
        
    if (err == noErr) {
        err = CopyItemsForks(source, &newDir, params);
    }
             
    // Iterate through the source code directory, copying each item to the newly
    // created directory.
    
    if (err == noErr) {
        err = FSOpenIterator(source, kFSIterateFlat, &iterator);
        if (err == noErr) {
        
            // Repeatedly call GetCatalogInfoBulk to get the contents
            // of the directory, and copy the contents.
            //
            // Note that we call GetCatalogInfoBulk with kFSCatInfoSettableInfo,
            // not kFSCatInfoGettableInfo, because there's no point getting
            // information that we can't set back when we're creating the copied item.
            
            done = false;
            do {
                err = FSGetCatalogInfoBulk(iterator, kFolderItemsPerBulkCall,
                                            &foundItems, nil,
                                            kFSCatInfoSettableInfo,
                                            foundCatInfos, foundFSRefs, nil, foundNames);
                if (err == errFSNoMoreItems) {
                    // We ran off the end of the directory.  Record that we're
                    // done, but set err to noErr so that we process any partial
                    // results.
                    done = true;            
                    err = noErr;
                }
                if (err == noErr) {
                    for (i = 0; i < foundItems; i++) {
                        if (foundCatInfos[i].nodeFlags & kFSNodeIsDirectoryMask) {
                            err = CopyFolder(&foundFSRefs[i], &foundCatInfos[i], &newDir, &foundNames[i], params);
                        } else {
                            err = CopyFile(&foundFSRefs[i], &foundCatInfos[i], &newDir, &foundNames[i], params);
                        }
                        if (err != noErr) {
                            break;
                        }
                    }
                }
            } while (err == noErr && !done);
            
            junk = FSCloseIterator(iterator);
            MoreAssertQ(junk == noErr);
        }
    }
 
    // Reset the modification dates, except when copying to a drop folder
    // where this won't work.
 
    if (err == noErr && !params->copyingToDropFolder) {
        sourceCatInfo->createDate = originalCreateDate;
        err = FSSetCatalogInfo(&newDir,   kFSCatInfoCreateDate
                                        | kFSCatInfoAttrMod 
                                        | kFSCatInfoContentMod, sourceCatInfo);
    }
 
    // Clean up.
    
    if (foundCatInfos != nil) {
        MPFree(foundCatInfos);
    }   
    if (foundFSRefs != nil) {
        MPFree(foundFSRefs);
    }   
    if (foundNames != nil) {
        MPFree(foundNames);
    }   
    params->currentDepth -= 1;
    
    return err;
}
 
enum {
    kDestInsideSourceErr = -1234
};
 
static OSStatus CheckForDestInsideSource(const FSRef *source, const FSRef *destDir)
    // Determines whether the destination directory is equal to the source
    // item, or whether it's nested inside the source item.  Returns a
    // kDestInsideSourceErr if that's the case.  We do this to prevent
    // endless recursion while copying.
    //
    // Frame Size: 288
{
    OSStatus err;
    FSRef thisDir;
    Boolean done;
    FSCatalogInfo thisDirInfo;
    
    thisDir = *destDir;
    done = false;
    do {
        err = FSCompareFSRefs(source, &thisDir);
        if (err == noErr) {
            err = kDestInsideSourceErr;
        } else if (err == diffVolErr) {
            err = noErr;
            done = true;
        } else if (err == errFSRefsDifferent) {
        
            // This is somewhat tricky.  We can ask for the parent of thisDir
            // by setting the parentRef parameter to FSGetCatalogInfo but, if
            // thisDir is the volume's FSRef, this will give us back junk.
            // So we also ask for the parent's dir ID to be returned in the
            // FSCatalogInfo record, and then check that against the node
            // ID of the root's parent (ie 1).  If we match that, we've made
            // it to the top of the hierarchy without hitting source, so
            // we leave with no error.
            
            err = FSGetCatalogInfo(&thisDir, kFSCatInfoParentDirID, &thisDirInfo, nil, nil, &thisDir);
            if (err == noErr) {
                if (thisDirInfo.parentDirID == fsRtParID) {
                    done = true;
                }
            }
        }
    } while ( err == noErr && ! done );
    
    return err;
}
 
enum {
    kPrivilegesMask  = kioACUserNoSeeFolderMask | kioACUserNoSeeFilesMask | kioACUserNoMakeChangesMask,
    kDropFolderValue = kioACUserNoSeeFolderMask | kioACUserNoSeeFilesMask
};
 
static OSStatus CopyItemTopLevel(const FSRef *source, const FSRef *destDir,
                                    void *copyBuffer, ByteCount copyBufferSize)
    // This routine acts as the top level of the copy engine.  It exists
    // to a) present a nicer API than the various recursive routines, and
    // b) minimise the local variables in the recursive routines.
    //
    // Frame Size: 752
{
    OSStatus err;
    FSCatalogInfo   tmpCatInfo;
    HFSUniStr255    destName;
    CopyParams      params;
    FSVolumeRefNum  destVRefNum;
    
    params.copyBuffer     = copyBuffer;
    params.copyBufferSize = copyBufferSize;
    params.recursionLimit = kMaximumFolderDepth;
    params.currentDepth   = 0;
 
    // Get info on the destination to determine whether it is a drop folder.
    
    err = FSGetCatalogInfo(destDir, kFSCatInfoUserPrivs, &tmpCatInfo, nil, nil, nil);
    if (err == noErr) {
        params.copyingToDropFolder = ((tmpCatInfo.userPrivileges & kPrivilegesMask) == kDropFolderValue);
        
        // Get info on the source to decide whether to kick off a file or
        // directory copy.
 
        err = FSGetCatalogInfo(source, kFSCatInfoSettableInfo, &tmpCatInfo, &destName, nil, nil);
    }
    
    // Get info on the destination to determine whether it is on a network volume.
    
    if (err == noErr) {
        err = VolumeContainingFSRef(destDir, &destVRefNum);
    }
    if (err == noErr) {
        err = GetFileSystemInfo(destVRefNum, nil, &params.copyingToLocalVolume, nil);
    }
    
    if (err == noErr) {
 
        // Clear the "inited" bit so that the Finder positions the icon for us.
        // We do this here because we only want to clear the "inited" bit for the
        // top level item.
    
        ((FInfo *)(tmpCatInfo.finderInfo))->fdFlags &= ~kHasBeenInited;
 
        if (tmpCatInfo.nodeFlags & kFSNodeIsDirectoryMask) {
            err = CheckForDestInsideSource(source, destDir);
            if (err == noErr) {
                err = CopyFolder(source, &tmpCatInfo, destDir, &destName, &params);
            }
        } else {
            err = CopyFile(source, &tmpCatInfo, destDir, &destName, &params);
        }
    }
    return err;
}
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- MP Stuff -----
 
static Boolean MPFileManagerAvailable(void)
    // Check to see whether we can use the File Manager from a
    // pre-emptive thread.  If gestaltMPCallableAPIsAttr isn't
    // available in your "Gestalt.h", update to the very latest.
{
    SInt32 response;
    return MPLibraryIsLoaded() 
            && Gestalt(gestaltMPCallableAPIsAttr, &response) == noErr 
            && (response & (1 << gestaltMPFileManager)) != 0;
}
 
// MP tasks take a single parameter when they start, but we need
// to pass a bunch of parameters to the task, so we put them
// in a structure.
 
struct CopyTaskParams {
    const FSRef *source;
    const FSRef *destDir;
    void *copyBuffer;
    ByteCount copyBufferSize;
};
typedef struct CopyTaskParams CopyTaskParams;
 
static OSStatus CopyFileTask(const CopyTaskParams *params)
    // This is the start routine of the MP task.  It simply
    // unpacks the parameters from the CopyTaskParams structure
    // and calls the copy engine.
    //
    // Frame Size: 64
{
    OSStatus err;
    
    err = CopyItemTopLevel(params->source, params->destDir, params->copyBuffer, params->copyBufferSize);
    return err;
}
 
static OSStatus CopyItemTopLevelMP(const FSRef *source, const FSRef *destDir,
                            void *copyBuffer, ByteCount copyBufferSize)
    // This routine is a direct analogue of CopyFileTopLevel, except it
    // executes the copy in an MP task.  The foreground process than
    // simply sits and waits for the task to complete, printing "."s
    // every now and again.  This is not a particularly useful application
    // of multi-processing (-: but it's still pretty cool.  For a start,
    // you can hold down the mouse button on a menu and the copy keeps
    // going.
    //
    // In a real application, this routine would probably be executed
    // by co-operative code that would drive the status window that
    // shows the user the status of the copy.  Pressing the Cancel
    // button on the status window would set a variable in the
    // CopyTaskParams that the MP task would periodically check.
    // [Calling MPTerminateTask would probably be a bad idea because
    // there'd be no way for the task to dispose of the memory that
    // it's allocated during the copy.]
{
    OSStatus err;
    OSStatus junk;
    MPQueueID terminationQueue;
    CopyTaskParams params;
    MPTaskID theCopyTask;
    Boolean done;
    OSStatus terminationErr;
    UInt32 timeLastPrinted;
    EventRecord event;
    
    terminationErr = noErr;             // Just to make for more obvious debugging.
 
    // Fill out the CopyTaskParams.
    
    params.source = source;
    params.destDir = destDir;
    params.copyBuffer = copyBuffer;
    params.copyBufferSize = copyBufferSize;
 
    // Create the termination queue and the task itself.
    
    err = MPCreateQueue(&terminationQueue);
    if (err == noErr) {
        err = MPCreateTask( (TaskProc) CopyFileTask, &params,
                            kCopyFileTaskStackSize,
                            terminationQueue,
                            nil, nil,   // no termination parameters
                            0,          // no task options
                            &theCopyTask);
        
        // Now wait for the copying task to complete, printing "."s
        // while we wait.
        
        if (err == noErr) {
            done = false;
            timeLastPrinted = TickCount();
            do {
                err = MPWaitOnQueue(terminationQueue,
                            nil, nil,   // not interested in these terminator parameters
                            (void **) &terminationErr,
                            kDurationImmediate);
                if (err == noErr) {
                    done = true;
                    err = terminationErr;
                } else if (err == kMPTimeoutErr) {
                    if (TickCount() > timeLastPrinted + 10) {
                        printf(".");
                        fflush(stdout);
                        timeLastPrinted = TickCount();
                    }
                    (void) WaitNextEvent(everyEvent, &event, 10, nil);
                    (void) SIOUXHandleOneEvent(&event);
                    err = noErr;
                }
            } while (err == noErr && ! done );
        }
        
        junk = MPDeleteQueue(terminationQueue);
        MoreAssertQ(junk == noErr);
    }
 
    return err;
}
 
/////////////////////////////////////////////////////////////////
#pragma mark ----- Mainline -----
 
static OSStatus ExtractSingleItem(const NavReplyRecord *reply, FSRef *item)
    // This item extracts a single FSRef from a NavReplyRecord.
    // Nav makes it really easy to support 'odoc', but a real pain
    // to support other things.  *sigh*
{
    OSStatus err;
    SInt32 itemCount;
    FSSpec fss;
    AEKeyword junkKeyword;
    DescType junkType;
    Size junkSize;
    
    MoreAssertQ((AECountItems(&reply->selection, &itemCount) == noErr) && (itemCount == 1));
    
    err = AEGetNthPtr(&reply->selection, 1, typeFSS, &junkKeyword, &junkType, &fss, sizeof(fss), &junkSize);
    if (err == noErr) {
        MoreAssertQ(junkType == typeFSS);
        MoreAssertQ(junkSize == sizeof(FSSpec));
        
        // We call FSMakeFSSpec because sometimes Nav is braindead
        // and gives us an invalid FSSpec (where the name is empty).
        // While FSpMakeFSRef seems to handle that (and the file system
        // engineers assure me that that will keep working (at least
        // on traditional Mac OS) because of the potential for breaking
        // existing applications), I'm still wary of doing this so
        // I regularise the FSSpec by feeding it through FSMakeFSSpec.
        
        if (fss.name[0] == 0) {
            err = FSMakeFSSpec(fss.vRefNum, fss.parID, fss.name, &fss);
        }
        if (err == noErr) {
            err = FSpMakeFSRef(&fss, item);
        }
    }
    return err;
}
 
static OSStatus ChooseSourceAndDest(FSRef *source, FSRef *dest)
    // Use Navigation Services (because I'm a good Apple employee)
    // to choose a source file and a destination folder,
    // and return them as FSRefs.
{
    OSStatus err;
    OSStatus junk;
    NavDialogOptions options;
    NavReplyRecord reply;
    
    err = noErr;
    if (! NavServicesAvailable() ) {
        printf("Navigation Services is required.\n");
        err = -1;
    }
    if (err == noErr) {
        err = NavGetDefaultDialogOptions(&options);
    }
    
    // Get the source item.
    
    if (err == noErr) {
        options.dialogOptionFlags &= ~kNavAllowMultipleFiles;
        err = NavChooseObject(nil, &reply, &options, nil, nil, nil);
    }
    if (err == noErr) {
        err = ExtractSingleItem(&reply, source);
        
        junk = NavDisposeReply(&reply);
        MoreAssertQ(junk == noErr);
    }
    
    // Get the dest folder.
    
    if (err == noErr) {
        err = NavChooseFolder(nil, &reply, &options, nil, nil, nil);
    }
    if (err == noErr) {
        err = ExtractSingleItem(&reply, dest);
        
        junk = NavDisposeReply(&reply);
        MoreAssertQ(junk == noErr);
    }
    
    // Would be nice if I made the destination name unique at this point.
    // Too hard for the moment.
    
    return err;
}
 
void main(void)
    // The main line.  Calls the copy engine to copy an item
    // named "Source" in the same directory as the application
    // to a folder named "Dest" in the same directory.
{
    OSStatus err;
    FSRef source;
    FSVolumeRefNum sourceVol;
    FSRef dest;
    FSVolumeRefNum destVol;
    Ptr copyBuffer;
    Boolean runPreemptive;
    ByteCount bufferSize;
    
    printf("Hello Cruel World!\n");
    printf("MPFileCopy.c\n");
    
    // gState is just for debugging.
    
    printf("&gState = $%08x\n", &gState);
    
    // We flush stdout so that the SIOUX window comes up, which initialises
    // the toolbox for us, which we're going to need as soon as we
    // call ChooseSourceAndDest, which calls Navigation Services.
    
    fflush(stdout);
 
    #if !TARGET_API_MAC_CARBON
        gMyCompletionUPP = NewIOCompletionProc(MyCompletion);
        MoreAssert(gMyCompletionUPP != nil);
    #endif
 
    copyBuffer = nil;
    
    // Get source and dest objects using Nav.
    
    err = ChooseSourceAndDest(&source, &dest);
 
    // Decide whether we can run using MP tasks or not.
    
    if (err == noErr) {
        err = VolumeContainingFSRef(&source, &sourceVol);
    }
    if (err == noErr) {
        err = VolumeContainingFSRef(&dest, &destVol);
    }
    runPreemptive = ! kNeverUseMPTasks && MPFileManagerAvailable();
    if (err == noErr && runPreemptive) {
        err = IsVolumeTrulyAsync(sourceVol, &runPreemptive);
        if (err == noErr && runPreemptive && destVol != sourceVol) {
            err = IsVolumeTrulyAsync(destVol, &runPreemptive);
        }
    }
    
    // Allocate the copy buffer.  We do this late because we
    // need to know the source and destination volumes to
    // judge the copy buffer size.
    
    if (err == noErr) {
        bufferSize = BufferSizeForThisVolume(sourceVol);
        if (destVol != sourceVol) {
            ByteCount tmp;
            
            tmp = BufferSizeForThisVolume(destVol);
            if (tmp < bufferSize) {
                bufferSize = tmp;
            }
        }
        copyBuffer = NewPtr(bufferSize);
        err = MemError();
    }
    
    // Under Mac OS 9.0, File Sharing's patches cause FSCreateFileUnicode to go
    // astray.  We determine whether it's running now and then test the gFileSharingIsRunning
    // flag later to see whether we need to use the workaround.
    
    if (err == noErr) {
        err = IsFileSharingRunning(&gFileSharingIsRunning);
    }
    
    // The copy engine is going to set each item's creation date
    // to kMagicBusyCreationDate while it's copying the item.
    // But kMagicBusyCreationDate is an old-style 32-bit date/time,
    // while the HFS Plus APIs use the new 64-bit date/time.  So
    // we have to call a happy UTC utilities routine to convert from
    // the local time kMagicBusyCreationDate to a UTCDateTime
    // gMagicBusyCreationDate, which the File Manager will store
    // on disk and which the Finder we read back using the old
    // APIs, whereupon the File Manager will convert it back
    // to local time (and hopefully get the kMagicBusyCreationDate
    // back!).
    
    if (err == noErr) {
        gMagicBusyCreationDate.highSeconds = 0;
        gMagicBusyCreationDate.fraction = 0;
        err = ConvertLocalTimeToUTC(kMagicBusyCreationDate, &gMagicBusyCreationDate.lowSeconds);
    }
    
    // Get the constant names for the resource and data fork, which
    // we're going to need inside the copy engine.
    
    if (err == noErr) {
        err = FSGetDataForkName(&gDataForkName);
    }
    if (err == noErr) {
        err = FSGetResourceForkName(&gRsrcForkName);
    }
    
    // Now call copy engine.
    
    if (err == noErr) {
        if ( runPreemptive ) {
            printf("Copying using pre-emptive thread.\n");
            fflush(stdout);
            err = CopyItemTopLevelMP(&source, &dest, copyBuffer, GetPtrSize(copyBuffer));
        } else {
            printf("Copying using co-operative main thread.\n");
            fflush(stdout);
            err = CopyItemTopLevel(&source, &dest, copyBuffer, GetPtrSize(copyBuffer));
        }
    }
    
    // Clean up.
    
    if (copyBuffer != nil) {
        DisposePtr(copyBuffer);
        MoreAssertQ(MemError() == noErr);
    }
    
    if (err == noErr) {
        printf("\nSuccess.\n");
    } else {
        printf("\nFailed with error %ld.\n", err);
    }
    printf("Done.  Press command-Q to Quit.\n");
}