//     ___       ___
//   _/  /_______\  \_     ___ ___ __ _                       _ __ ___ ___
//__//  / _______ \  \\___/                                               \___
//_/ | '  \__ __/  ` | \_/     © Copyright 1999-2000, Christopher Page     \__
// \ | |    | |__  | | / \   Released as Free Software under the GNU GPL   /
//  >| .    |  _/  . |<   >--- --- -- -                       - -- --- ---<
// / \  \   | |   /  / \ /   This file is part of the VersionCopy source   \
// \  \  \_/   \_/  /  / \  and it is released under the GNU GPL. Please   /
//  \  \           /  /   \   read the "COPYING" file which should have   /
// //\  \_________/  /\\ //\    been included in the distribution arc.   /
//- --\   _______   /-- - --\      for full details of the license      /-----
//-----\_/       \_/---------\   ___________________________________   /------
//                            \_/                                   \_/
//
// Description:
//
//  VersionCopy version aware copy command
//
// Functions:
//
//  BOOL IsDirectory        (STRPTR TestName)
//  STRPTR FindSubBuffer    (UBYTE *Source, UBYTE * Pattern, LONG SourceLen, LONG PatternLen)
//  VersionData *FindVersion(BPTR Source, struct FileInfoBlock *SourceData)
//  void FreeVersion        (struct VersionData *OldData)
//  LONG FileCopy           (STRPTR SourceName, STRPTR TargetName, ULONG BufferSize)
//  void CopyAttributes     (struct FileInfoBlock *SourceInfo, STRPTR TargetName)
//  LONG VersionCopy        (struct FileInfoBlock *SourceFIB, struct FileInfoBlock *TargetFIB, STRPTR SourceName, STRPTR TargetName, ULONG BufferSize)
//
// Detail:
//
//  VersionCopy is a cut-down "Copy" command which is aware of the version information
//  within the file being copied. Before copying the source file it checks whether the
//  destination already exists and, if it does, the version numbers of the source and
//  destination are compared. The source is only copied over the destination if the
//  source version or revision is greater than that of the destination. Unlike Copy,
//  wildcards and directory copy are not supported - indeed these facilities would be
//  somewhat meaningless for the applications this command is intended.
//
// Fold Markers:
//
//  Start: /*GFS*/
//    End: /*GFE*/

#include<exec/exec.h>
#include<dos/dos.h>

#include<clib/exec_protos.h>
#include<clib/dos_protos.h>

#include<ctype.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include"VersionCopy_rev.h"

const char Copyright[] = "© copyright 2000 Chris Page";
const char Version[]   = VERSTAG;

// Typing saver! :)
#define ShowMessage(TextData) if(!((BOOL)ArgResult[OPT_QUIET])) printf("%s\n", TextData)

// Stuff for ReadArgs()
#define ARGS_TEMPLATE  "FROM/A,TO/A,QUIET/S,BUF=BUFFER/K/N,CLONE/S,DATE/S,COM/S,NOPRO/S,IGNORENAME/S"
#define OPT_FROM       0
#define OPT_TO         1
#define OPT_QUIET      2
#define OPT_BUFFER     3
#define OPT_CLONE      4
#define OPT_DATE       5
#define OPT_COM        6
#define OPT_NOPRO      7
#define OPT_IGNORE     8
#define OPT_COUNT      9

// Note this does not consider dates!
struct VersionData
{
    STRPTR Name    ; // Name given after $VER:, NULL if not found
    LONG   Version ; // Version number
    LONG   Revision; // Revision, 0 if no revision given.
};

// Prototype type things..
       BOOL IsDirectory        (STRPTR TestName);
       STRPTR FindSubBuffer    (UBYTE *Source, UBYTE * Pattern, LONG SourceLen, LONG PatternLen);
struct VersionData *FindVersion(BPTR Source, struct FileInfoBlock *SourceData);
       void FreeVersion        (struct VersionData *OldData);
       LONG FileCopy           (STRPTR SourceName, STRPTR TargetName, ULONG BufferSize);
       void CopyAttributes     (struct FileInfoBlock *SourceInfo, STRPTR TargetName);
       LONG VersionCopy        (struct FileInfoBlock *SourceFIB, struct FileInfoBlock *TargetFIB, STRPTR SourceName, STRPTR TargetName, ULONG BufferSize);


// Globals...
char  WorkBuffer[80]       ;  // Yeah, this is nasty but it saves some messing about later
ULONG ArgResult [OPT_COUNT];  // ReadArgs store.


/* BOOL IsDirectory(STRPTR)                                                  */
/* -=-=-=-=-=-=-=-=-=-=-=-=                                                  */
/* If the specified filename is actually a directory this returns TRUE.      */
/*                                                                           */
/* Parameters:                                                               */
/*   TestName       Name of the object to identify.                          */

/*GFS*/  BOOL IsDirectory(STRPTR TestName)
{
    struct FileInfoBlock *TestFIB ;
           BPTR           TestLock;
           BOOL           IsDir   = TRUE;

    if(TestFIB = AllocDosObject(DOS_FIB, NULL)) {
        if(TestLock = Lock(TestName, SHARED_LOCK)) {
            Examine(TestLock, TestFIB);

            IsDir = (TestFIB -> fib_DirEntryType >= 0);

            UnLock(TestLock);
        } else {
            IsDir = FALSE;
        }
        FreeDosObject(DOS_FIB, TestFIB);
    }

    return(IsDir);
}/*GFE*/


/* STRPTR FindSubBuffer(UBYTE *, UBYTE *, LONG, LONG)                        */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=                        */
/* This is basically a version of strstr which will work for non-string data */
/* (ie: buffers containing arbitrary data, not just NULL-terminated strings) */
/*                                                                           */
/* Parameters:                                                               */
/*     Source       Buffer to scan                                           */
/*    Pattern       Pattern to find in Source                                */
/*  SourceLen       Length of the source buffer in bytes                     */
/* PatternLen       Length of the pattern in bytes                           */

/*GFS*/  STRPTR FindSubBuffer(UBYTE *Source, UBYTE * Pattern, LONG SourceLen, LONG PatternLen)
{
    LONG SourcePos = 0;

    while(SourcePos <= SourceLen) {
        if(!memcmp(&Source[SourcePos], Pattern, PatternLen)) {
            return(&Source[SourcePos]);
        } else {
            SourcePos ++;
        }
    }

    return(NULL);
}/*GFE*/


/* VersionData *FindVersion(BPTR, FileInfoBlock *)                           */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-                           */
/* This routine loads the file specified by the provided file handle into a  */
/* memory buffer and searches it for the $VER: string that identifies the    */
/* start of an Amiga version string. If it finds this the name, version and  */
/* revision are copied into a VersionData structure and returned. Use FreeVec*/
/* to release the name buffer and VersionData structure returned by this     */
/* If this returns NULL then IoErr() will contain a code indicating why      */
/*                                                                           */
/* Parameters:                                                               */
/*     Source       File handle for the file to load                         */
/* SourceData       FIB filled in by calling ExamineFH() on the source file  */

/*GFS*/  struct VersionData *FindVersion(BPTR Source, struct FileInfoBlock *SourceData)
{
    struct VersionData *NewData;
           STRPTR       Buffer ;
           STRPTR       Search ;
           STRPTR       Start  ;
           BOOL         Spaced    = FALSE;
           BOOL         Complete  = FALSE;
           LONG         ErrorCode = 0;

           // This is a hack to get around vbcc not putting the Version string near
           // the start of the file. If FindBuffer[] = "$VER:" then Version VersionCopy
           // gets confused between the $VER: for FindBuffer and the one for the version

           char         FindBuffer[] = " VER:";
           FindBuffer[0] = '$';

    if(Buffer = AllocVec(SourceData -> fib_Size, MEMF_ANY)) {
        Read(Source, Buffer, SourceData -> fib_Size);

        Search = FindSubBuffer(Buffer, FindBuffer, SourceData -> fib_Size - 6, 6);

        if(Search) {
            // Skip the version header
            Search += 6;

            // Skip any leading whitespace
            while(*Search && (*Search == ' ')) Search++;

            // Remember the start location
            Start = Search;

            // Look for the first digit after a space
            do {
                if(isdigit(*Search) && Spaced) {
                    Complete = TRUE;
                } else {
                    Spaced = (*Search == ' ');
                    Search++;
                }
            } while(!Complete && *Search);

            if(NewData = AllocVec(sizeof(struct VersionData), MEMF_ANY|MEMF_CLEAR)) {

                // Allocate space for the name and copy it.
                if(NewData -> Name = AllocVec(Search - Start, MEMF_ANY|MEMF_CLEAR)) {
                    CopyMem(Start, NewData -> Name, Search - Start - 1);

                    // got a version number?
                    if(Complete) {
                        NewData -> Version = atol(Search);

                        // Try to locate the revision
                        while(*Search && (*Search != '.')) Search++;

                        if(*Search == '.') {
                            Search++;
                            Start = Search;

                            // whack a NULL in at the cirst non-digit encountered
                            while(isdigit(*Search)) Search++;
                            *Search = '\0';

                            NewData -> Revision = atol(Start);
                        }

                        FreeVec(Buffer);
                        SetIoErr(0);
                        return(NewData);
                    }
                } else {
                    ErrorCode = ERROR_NO_FREE_STORE;
                    ShowMessage("Unable to allocate version name buffer");
                }
                FreeVec(NewData);
            } else {
                ErrorCode = ERROR_NO_FREE_STORE;
                ShowMessage("Unable to allocate version data");
            }
        } else {
            ErrorCode = ERROR_OBJECT_WRONG_TYPE;
            if(!((BOOL)ArgResult[OPT_QUIET])) printf("%s does not contain version information\n", SourceData -> fib_FileName);
        }

        FreeVec(Buffer);
    } else {
        ErrorCode = ERROR_NO_FREE_STORE;
        ShowMessage("Unable to allocate search buffer");
    }

    SetIoErr(ErrorCode);
    return(NULL);
}/*GFE*/


/* void FreeVersion(VersionData *)                                           */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-                                           */
/* Small routine to free the specified VersionData structure - if the pointer*/
/* is not NULL then the Name field is freed (if required) and the the struct */
/* itself is released.                                                       */
/*                                                                           */
/* Parameters:                                                               */
/*    OldData       The VersionData structure to free.                       */

/*GFS*/  void FreeVersion(struct VersionData *OldData)
{
    if(OldData) {
        if(OldData -> Name) FreeVec(OldData -> Name);
        FreeVec(OldData);
    }
}/*GFE*/


/* LONG FileCopy(STRPTR, STRPTR, ULONG)                                      */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=                                      */
/* This does the actual duplication of the source file, copying BufferSize   */
/* chunks of the file until the whole file is copied or the user does a      */
/* CTRL-C to abort it the copy. Errors in reading and writing are detected.  */
/*                                                                           */
/* Parameters:                                                               */
/* SourceName       Name of the file to copy                                 */
/* TargetName       Name of the destination file                             */
/* BufferSize       Size of the copy buffer in bytes.                        */

/*GFS*/  LONG FileCopy(STRPTR SourceName, STRPTR TargetName, ULONG BufferSize)
{
    BPTR   SourceFile, DestFile    ;
    UBYTE *CopyBuffer              ;
    LONG   CopyLength              ;
    LONG   WriteLength             ;
    LONG   ReturnCode = RETURN_WARN;

    if(CopyBuffer = (UBYTE *)AllocVec(BufferSize, MEMF_ANY)) {
        if(SourceFile = Open(SourceName, MODE_OLDFILE)) {
            if(DestFile = Open(TargetName, MODE_NEWFILE)) {

                // Read & write BufferSize chunks of the file, allowing user abort and
                // filesystem full handling..
                do {
                    WriteLength = 0;
                    if(CopyLength  = Read(SourceFile, CopyBuffer, BufferSize)) {

                        // Allow user abort..
                        if(!(SetSignal(0, 0) & SIGBREAKF_CTRL_C)) {
                            WriteLength = Write(DestFile  , CopyBuffer, CopyLength);
                        }
                    }
                } while((CopyLength > 0) && (WriteLength == CopyLength));

                // should catch the last error
                if((CopyLength < 0) || (WriteLength != CopyLength)) ReturnCode = IoErr();
                Close(DestFile);

                // Failed to write the last read, erase destination..
                if((WriteLength != CopyLength) && (CopyLength > 0)) {
                    DeleteFile(TargetName);

                    if(ReturnCode && !(BOOL)ArgResult[OPT_QUIET]) {
                        Fault(ReturnCode, NULL, WorkBuffer, 80);
                        printf("Unable to write %s: %s\n", TargetName, WorkBuffer);
                    }

                    ShowMessage("Removed incomplete file...\n");
                }
            } else {
                ReturnCode = IoErr();

                if(!((BOOL)ArgResult[OPT_QUIET])) {
                    Fault(ReturnCode, NULL, WorkBuffer, 80);
                    printf("Unable to open %s for writing: %s\n", TargetName, WorkBuffer);
                }
            }

            Close(SourceFile);
        // This one shouldn't really happen but anyway....
        } else {
            ReturnCode = IoErr();

            if(!((BOOL)ArgResult[OPT_QUIET])) {
                Fault(ReturnCode, NULL, WorkBuffer, 80);
                printf("Unable to open %s for reading: %s\n", TargetName, WorkBuffer);
            }
        }

        FreeVec(CopyBuffer);
    } else {
        ReturnCode = ERROR_NO_FREE_STORE;
        ShowMessage("Unable to allocate the copy buffer");
    }

    return(ReturnCode);
}/*GFE*/


/* void CopyAttributes(FileInfoblock *, STRPTR)                              */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=                              */
/* If the file has been copied then some attributes of the source may need   */
/* to be copied to the target: if the clone has been activated then all the  */
/* file attributes of the source need to be copied, otherwise the only attr. */
/* copied by default is the protection bits - this cane be disabled with a   */
/* flag and other attributes can be activated with the DATE and COM args..   */
/*                                                                           */
/* Parameters:                                                               */
/* SourceInfo       FileInfoBlock containing the source attributes           */
/* TargetName       Name of the target file (must exist!!)                   */

/*GFS*/  void CopyAttributes(struct FileInfoBlock *SourceInfo, STRPTR TargetName)
{
    if((BOOL)ArgResult[OPT_CLONE]) {
        SetComment   (TargetName, SourceInfo -> fib_Comment);
        SetFileDate  (TargetName, &SourceInfo -> fib_Date);
        SetProtection(TargetName, SourceInfo -> fib_Protection);
    } else {
        if((BOOL)ArgResult[OPT_COM] )     SetComment   (TargetName, SourceInfo -> fib_Comment);
        if((BOOL)ArgResult[OPT_DATE])     SetFileDate  (TargetName, &SourceInfo -> fib_Date);
        if(!((BOOL)ArgResult[OPT_NOPRO])) SetProtection(TargetName, SourceInfo -> fib_Protection);
    }
}/*GFE*/


/* LONG VersionCopy(FileInfoBlock *, FileInfoBlock *, STRPTR, STRPTR, ULONG) */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
/* This checks the source and target versions and copies the source over the */
/* target if the target version is lower than the source or the target has   */
/* no version information.                                                   */
/*                                                                           */
/* Parameters:                                                               */
/*  SourceFIB       FileInfoBlock containing the source attributes           */
/*  TargetFIB       FileInfoBlock containing the target attributes           */
/* SourceName       Filename of the source file                              */
/* TargetName       Filename of the target                                   */
/* BufferSize       Size of the copy buffer in bytes                         */

/*GFS*/  LONG VersionCopy(struct FileInfoBlock *SourceFIB, struct FileInfoBlock *TargetFIB, STRPTR SourceName, STRPTR TargetName, ULONG BufferSize)
{
    struct VersionData   *SourceVers;
    struct VersionData   *TargetVers;
           BPTR           SourceFile;
           BPTR           TargetFile;
           LONG           ErrorCode = RETURN_WARN;

    // Open the source and get it's version - these must both succeed..
    if(SourceFile = Open(SourceName, MODE_OLDFILE)) {
        if(SourceVers = FindVersion(SourceFile, SourceFIB)) {

            // Open the target & get version info if possible...
            if(TargetFile = Open(TargetName, MODE_OLDFILE)) {
                if(TargetVers = FindVersion(TargetFile, TargetFIB)) {

                    // Don't need these any more...
                    Close(SourceFile);
                    Close(TargetFile);
                    SourceFile = 0L;

                    // If the names match, or ignore has been set, then pass through..
                    if((BOOL)ArgResult[OPT_IGNORE] || !strcmp(SourceVers -> Name, TargetVers -> Name)) {

                        // Check the version and revision
                        if((SourceVers -> Version > TargetVers -> Version) ||
                           ((SourceVers -> Version == TargetVers -> Version) &&
                            (SourceVers -> Revision > TargetVers -> Revision))) {

                            // Source is newer - copy it.
                            ErrorCode = FileCopy(SourceName, TargetName, BufferSize);
                        } else {

                            // Neil says he wants an OK for this case...
                            ErrorCode = RETURN_OK;
                            ShowMessage("Target is up to date");
                        }
                    } else {
                        ErrorCode = ERROR_OBJECT_WRONG_TYPE;
                        ShowMessage("Version names in source and target do not match");
                    }

                    FreeVersion(TargetVers);

                // No version info in the file.. just write over it.
                } else {

                    // Don't need these now..
                    Close(TargetFile);
                    Close(SourceFile);
                    SourceFile = 0L;

                    // Target doesn't contain any version, straight copy
                    ErrorCode = FileCopy((STRPTR)ArgResult[OPT_FROM], TargetName, BufferSize);

                    // If the copy was successful then copy the file attributes
                    if(ErrorCode == RETURN_WARN) CopyAttributes(SourceFIB, TargetName);
                }
            } else {

                // Hmm.. opening the target failed. We know it exists (or we wouldn't be here)
                // but it won't open so something screwy is going on..
                ErrorCode = IoErr();
                if(!((BOOL)ArgResult[OPT_QUIET])) {
                    Fault(ErrorCode, NULL, WorkBuffer, 80);
                    printf("Unable to open target: %s\n", WorkBuffer);
                }
            }

            FreeVersion(SourceVers);
        } else {
            ErrorCode = IoErr();
        }

        if(SourceFile) Close(SourceFile);
    } else {
        ErrorCode = IoErr();
        if(!((BOOL)ArgResult[OPT_QUIET])) {
            Fault(ErrorCode, NULL, WorkBuffer, 80);
            printf("Unable to open source: %s\n", WorkBuffer);
        }
    }

    return(ErrorCode);
}/*GFE*/


int main(int argc, char **argv)
{
    struct RDArgs        *ArgsData  ;
    struct FileInfoBlock *SourceFIB ;
    struct FileInfoBlock *TargetFIB ;
           BPTR           SourceLock;
           BPTR           TargetLock;
           STRPTR         TargetName;
           STRPTR         TempString;
           BOOL           FreeTarget = FALSE;
           ULONG          TargetSize = 0L;
           ULONG          BufferSize = 0L;

           LONG           ErrorCode  = 0L;

    // Can't do anything if the user stuffs up the command line...
    if(ArgsData = ReadArgs(ARGS_TEMPLATE, ArgResult, NULL)) {

        // Work out how big the copy buffer should be
        if(ArgResult[OPT_BUFFER]) {
            BufferSize = (*(ULONG *)ArgResult[OPT_BUFFER]) * 512;
        }

        if(BufferSize == 0) BufferSize = 102400;

        // Allocate the source fib, lock the source and examine it
        if(SourceFIB = AllocDosObject(DOS_FIB, NULL)) {
            if(SourceLock = Lock((STRPTR)ArgResult[OPT_FROM], SHARED_LOCK)) {
                Examine(SourceLock, SourceFIB);

                // Only continue if the source is a file (no directory copy in this version...)
                if(SourceFIB -> fib_DirEntryType < 0) {

                    // Quick hack to make sure we have a valid destination name..
                    if(strlen((STRPTR)ArgResult[OPT_TO]) == 0) {
                        TargetName = FilePart((STRPTR)ArgResult[OPT_FROM]);
                    } else {
                        // User has specified a destination (ie: not "") but is it a dir?
                        TargetName = (STRPTR)ArgResult[OPT_TO];

                        // If the target is a directory we need to add the filename...
                        if(IsDirectory(TargetName)) {
                            TempString = FilePart((STRPTR)ArgResult[OPT_FROM]);
                            TargetSize = strlen(TargetName) + strlen(TempString) + 3;

                            // Allocate space for the new name
                            if(TargetName = AllocVec(TargetSize, MEMF_ANY)) {
                                strcpy(TargetName, (STRPTR)ArgResult[OPT_TO]);
                                AddPart(TargetName, TempString, TargetSize);
                                FreeTarget = TRUE;
                            } else {
                                ErrorCode = ERROR_NO_FREE_STORE;
                                ShowMessage("Unable to allocate target name buffer");
                            }
                        }
                    }

                    if(TargetName) {

                        // Alloc fib, lock and examine as usual...
                        if(TargetFIB = AllocDosObject(DOS_FIB, NULL)) {
                            if(TargetLock = Lock(TargetName, SHARED_LOCK)) {
                                Examine(TargetLock, TargetFIB);

                                // Don't need these no more...
                                UnLock(TargetLock);
                                UnLock(SourceLock);
                                SourceLock = 0L;

                                // Copy only if the version wills it..
                                ErrorCode = VersionCopy(SourceFIB, TargetFIB, (STRPTR)ArgResult[OPT_FROM], TargetName, BufferSize);

                                // If the copy was successful then copy the file attributes
                                if(ErrorCode == RETURN_WARN) CopyAttributes(SourceFIB, TargetName);

                            } else {
                                ErrorCode = IoErr();

                                // Can't lock the target - is it just that it doesn't exist....
                                if(ErrorCode == ERROR_OBJECT_NOT_FOUND) {

                                    // Target doesn't exist, straight copy
                                    ErrorCode = FileCopy((STRPTR)ArgResult[OPT_FROM], TargetName, BufferSize);

                                    // If the copy was successful then copy the file attributes
                                    if(ErrorCode == RETURN_WARN) CopyAttributes(SourceFIB, TargetName);

                                } else {

                                    // .... or is it something worse?
                                    if(!((BOOL)ArgResult[OPT_QUIET])) {
                                        Fault(ErrorCode, NULL, WorkBuffer, 80);
                                        printf("Target not locked: (%ld) %s\n", ErrorCode, WorkBuffer);
                                    }
                                }
                            }
                            FreeDosObject(DOS_FIB, TargetFIB);
                        }

                        if(FreeTarget) FreeVec(TargetName);
                    }
                } else {
                    ErrorCode = ERROR_OBJECT_WRONG_TYPE;
                    ShowMessage("The source is not a file");
                }

                if(SourceLock) UnLock(SourceLock);
            } else {
                ErrorCode = IoErr();
                if(!((BOOL)ArgResult[OPT_QUIET])) {
                    Fault(ErrorCode, NULL, WorkBuffer, 80);
                    printf("Unable to open %s: %s\n", (STRPTR)ArgResult[OPT_FROM], WorkBuffer);
                }
            }

            FreeDosObject(DOS_FIB, SourceFIB);
        } else {
            ErrorCode = ERROR_NO_FREE_STORE;
            ShowMessage("Unable to allocate source FIB\n");
        }

        FreeArgs(ArgsData);
    } else {
        printf("%s: required argument missing\n", argv[0]);
        ErrorCode = ERROR_REQUIRED_ARG_MISSING;
    }

    exit(ErrorCode);
}







