/*++

Copyright (c) 1990-2000 Microsoft Corporation

Module Name:

        Replace

Abstract:

        Replace utility

Author:

        Ramon Juan San Andres (ramonsa) 01-May-1991

Revision History:

--*/

#include "ulib.hxx"
#include "arrayit.hxx"
#include "dir.hxx"
#include "filter.hxx"
#include "file.hxx"
#include "fsnode.hxx"
#include "stream.hxx"
#include "substrng.hxx"
#include "system.hxx"
#include "replace.hxx"

//
//      Pattern that matches all files in a directory
//
#define         MATCH_ALL_FILES         "*.*"


#define CTRL_C          (WCHAR)3



//
//      Size of buffers to hold path strings
//
#define         INITIAL_PATHSTRING_BUFFER_SIZE  MAX_PATH


VOID __cdecl
main (
        )

/*++

Routine Description:

        Main function of the Replace utility

Arguments:

    None.

Return Value:

    None.

Notes:

--*/

{
    //
    //      Initialize stuff
    //
    DEFINE_CLASS_DESCRIPTOR( REPLACE );

    //
    //      Now do the replacement
    //
    {
            REPLACE Replace;

            //
            //      Initialize the Replace object.
            //
            Replace.Initialize();

            //
            //      Do our thing
            //
            Replace.DoReplace();
    }
}




DEFINE_CONSTRUCTOR( REPLACE,    PROGRAM );



BOOLEAN
REPLACE::Initialize (
        )

/*++

Routine Description:

        Initializes the REPLACE object

Arguments:

    None.

Return Value:

        TRUE if initialized, FALSE otherwise

Notes:

--*/

{
        //
        //      Initialize program object
        //
        PROGRAM::Initialize( REPLACE_MESSAGE_USAGE );

        //
        //      Allocate global structures and initialize them
        //
        InitializeThings();

        //
        //      Parse the arguments
        //
        SetArguments();

        return TRUE;
}

REPLACE::~REPLACE (
        )

/*++

Routine Description:

        Destructs a REPLACE object

Arguments:

    None.

Return Value:

    None.

Notes:

--*/

{
    //
    //      Deallocate the global structures previously allocated
    //
    DeallocateThings();

    //
    //      Exit without error
    //
    DisplayMessageAndExit( 0, NULL, EXIT_NORMAL );

}

VOID
REPLACE::InitializeThings (
        )

/*++

Routine Description:

        Initializes the global variables that need initialization

Arguments:

    None.

Return Value:

    None.

Notes:

--*/

{

    //
        //      Initialize the path string buffers
        //
    _PathString1 = (LPWSTR)MALLOC( INITIAL_PATHSTRING_BUFFER_SIZE );
    _PathString2 = (LPWSTR)MALLOC( INITIAL_PATHSTRING_BUFFER_SIZE );

    _PathString1Size = _PathString2Size = INITIAL_PATHSTRING_BUFFER_SIZE;

    _Keyboard = NEW KEYBOARD;

    if ( !_PathString1 || !_PathString2 || !_Keyboard ) {
            DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
    }

    //
    //      initialize the keyboard and set ctrl-c handling
    //
    _Keyboard->Initialize();
    _Keyboard->EnableBreakHandling();

    //
    //      Initialize our data
    //
    _SourcePath             =       NULL;
    _DestinationPath        =       NULL;
    _FilesAdded             =       0;
    _FilesReplaced          =       0;
    _SourceDirectory        =       NULL;
    _Pattern                        =       NULL;
    _FilesInSrc             =       NULL;

    _AddSwitch = FALSE;   // use by DisplayMessageAndExit before any
                          // of those boolean _*Switch is being initialized

}

VOID
REPLACE::DeallocateThings (
        )

/*++

Routine Description:

        Deallocates the stuff that was initialized in InitializeThings()

Arguments:

    None.

Return Value:

    None.

Notes:

--*/

{

    DELETE( _FilesInSrc );
    DELETE( _SourceDirectory );
    DELETE( _Pattern );
    DELETE( _Keyboard );

    FREE( _PathString1 );
    FREE( _PathString2 );

}

BOOLEAN
REPLACE::DoReplace (
        )

/*++

Routine Description:

        This is the function that performs the Replace.

Arguments:

    None.

Return Value:

        TRUE

Notes:

--*/

{

    PFSN_DIRECTORY  DestinationDirectory;
    FSN_FILTER              Filter;
    WCHAR                   Char;

    //
    //      Get the source directory object and the pattern that we will use
    //      for file matching.
    //
    GetDirectoryAndPattern( _SourcePath, &_SourceDirectory, &_Pattern );

    DebugPtrAssert( _SourceDirectory );
    DebugPtrAssert( _Pattern );

    //
    //      Get the destination directory
    //
    GetDirectory( _DestinationPath, &DestinationDirectory );

    DebugPtrAssert( DestinationDirectory );

    //
    //      Wait if requested
    //
    if ( _WaitSwitch ) {

            DisplayMessage( REPLACE_MESSAGE_PRESS_ANY_KEY );
            AbortIfCtrlC();

            //
            //      All input is in raw mode.
            //
            _Keyboard->DisableLineMode();
            GetStandardInput()->ReadChar( &Char );
            _Keyboard->EnableLineMode();

            GetStandardOutput()->WriteChar( Char );
            GetStandardOutput()->WriteChar( (WCHAR)'\r');
            GetStandardOutput()->WriteChar( (WCHAR)'\n');

            //
            //      Check for ctrl-c
            //
            if ( Char == CTRL_C ) {
                    exit ( EXIT_PATH_NOT_FOUND );
            }
    }

    //
    //      Get an array containing all the files in the source directory
    //      that match the pattern.
    //
    //      This is so that Replacer() does not have to get the same
    //      information over and over when the Subdir switch is set.
    //
    _FilesInSrc = GetFileArray( _SourceDirectory, _Pattern );
    DebugPtrAssert( _FilesInSrc );

    if ( _SubdirSwitch ) {

            //
            //      First, replace the files in the directory specified
            //
            Replacer( this, DestinationDirectory, NULL );

            Filter.Initialize();
            Filter.SetAttributes( (FSN_ATTRIBUTE)FILE_ATTRIBUTE_DIRECTORY );

            //
            //      Now traverse the destination directory, calling the
            //      replacer function for each subdirectory.
            //
            DestinationDirectory->Traverse( this,
                                                                            &Filter,
                                                                            NULL,
                                                                            REPLACE::Replacer );
    } else {

            //
            //      Call the replace function, which takes care of replacements
            //
            Replacer(this,  DestinationDirectory, NULL );
    }
    DELETE( DestinationDirectory );

    return TRUE;

}

VOID
REPLACE::GetDirectoryAndPattern(
   IN    PPATH       Path,
   OUT   PFSN_DIRECTORY *Directory,
    OUT     PWSTRING        *Pattern
   )

/*++

Routine Description:

        Given a path, this function obtains a directory object and a pattern.

        Normally, the pattern is the filename portion of the path, but if the
        entire path refers to a directory, then the pattern is "*.*"

Arguments:

        Path            -       Supplies pointer to path
        Directory       -       Supplies pointer to pointer to directory
        Pattern         -       Supplies pointer to pointer to pattern

Return Value:

        TRUE

Notes:

--*/

{
    PATH           TmpPath;
    PWSTRING            Name;
    PFSN_DIRECTORY    Dir;
    PWSTRING            Ptrn;

    DebugAssert( Path );
    DebugAssert( Directory );
    DebugAssert( Pattern );

    //
    // If the name passed is a directory, it is an error.
    // Otherwise, split the path into Directory and Pattern
    // portions.
    //
    Dir = SYSTEM::QueryDirectory( Path );

    if ( Dir ||
         (Name = Path->QueryName()) == NULL ||
         (Ptrn = Name->QueryString()) == NULL ) {

        DisplayMessageAndExit( REPLACE_ERROR_NO_FILES_FOUND,
                               Path->GetPathString(),
                               EXIT_FILE_NOT_FOUND );

    } else {

        // We're finished with Name.
        //
        DELETE( Name );

        //
        // Get the directory
        //
        TmpPath.Initialize( Path, TRUE );
        TmpPath.TruncateBase();

        Dir = SYSTEM::QueryDirectory( &TmpPath );

        if ( !Dir ) {

            DisplayMessageAndExit( REPLACE_ERROR_PATH_NOT_FOUND,
                                   Path->GetPathString(),
                                   EXIT_PATH_NOT_FOUND );
        }

        *Directory = Dir;
        *Pattern   = Ptrn;
    }
}

VOID
REPLACE::GetDirectory(
        IN              PCPATH                  Path,
        OUT     PFSN_DIRECTORY  *Directory
        )

/*++

Routine Description:

        Makes a directory out of a path.

Arguments:

        Path            -       Supplies pointer to path
        Directory       -       Supplies pointer to pointer to directory

Return Value:

        TRUE

Notes:

--*/

{
    PFSN_DIRECTORY  Dir;


    if ( !(Dir = SYSTEM::QueryDirectory( Path )) ) {

            DisplayMessageAndExit( REPLACE_ERROR_PATH_NOT_FOUND,
                                                       Path->GetPathString(),
                                                       EXIT_PATH_NOT_FOUND );
    }

    *Directory = Dir;

}

PARRAY
REPLACE::GetFileArray(
   IN    PFSN_DIRECTORY Directory,
    IN      PWSTRING        Pattern
   )

/*++

Routine Description:

        Gets an array of those files in a directory matching a pattern.

Arguments:

        Directory       -       Supplies pointer to directory
        Pattern         -       Supplies pointer to pattern

Return Value:

        Pointer to the array of files

Notes:

--*/
{

    PARRAY          Array;
    FSN_FILTER      Filter;

    DebugPtrAssert( Directory );
    DebugPtrAssert( Pattern );

    Filter.Initialize();
    Filter.SetFileName( Pattern );
    Filter.SetAttributes( (FSN_ATTRIBUTE)0, (FSN_ATTRIBUTE)0, (FSN_ATTRIBUTE)FILE_ATTRIBUTE_DIRECTORY );

    Array = Directory->QueryFsnodeArray( &Filter );

    DebugPtrAssert( Array );

    return Array;

}

BOOLEAN
REPLACE::Replacer (
        IN              PVOID           This,
        IN OUT  PFSNODE         DirectoryNode,
        IN              PPATH           DummyPath
        )
/*++

Routine Description:

        This is the heart of Replace. Given a destination directory, it
        performs the replacement/additions according to the global switches
        and the SourceDirectory and Pattern.

Arguments:

        This                    -       Supplies pointer to the REPLACE object
        Node                    -       Supplies pointer to the directory node.
        DummyPath               -       Required by FSN_DIRECTORY::Traverse(), must be
                                                NULL.

Return Value:

        BOOLEAN -       TRUE if operation successful.
                                FALSE otherwise

Notes:

--*/

{

    DebugAssert( DummyPath == NULL );
    DebugAssert( DirectoryNode->IsDirectory() );

    ((PREPLACE)This)->AbortIfCtrlC();

    if ( ((PREPLACE)This)->_AddSwitch ) {
        return ((PREPLACE)This)->AddFiles( (PFSN_DIRECTORY)DirectoryNode );
    } else {
        return ((PREPLACE)This)->ReplaceFiles( (PFSN_DIRECTORY)DirectoryNode );
    }
}

BOOLEAN
REPLACE::AddFiles (
        IN OUT  PFSN_DIRECTORY  DestinationDirectory
        )
/*++

Routine Description:

        Adds those      files from the SourceDirectory that match Pattern to the
        DestinationDirectory. The array of files is already in the
        FilesInSrc array.

Arguments:

        DestinationDirectory    -       Supplies pointer to destination
                                                                directory.

Return Value:

        BOOLEAN -       TRUE if operation successful.
                                FALSE otherwise

Notes:

--*/

{

    PARRAY_ITERATOR   Iterator;
    PFSN_FILE         File;
    PFSN_FILE         FileToCreate;
    PATH              DestinationPath;
    PWSTRING          Name;

    DebugPtrAssert( DestinationDirectory );
    DebugPtrAssert( _FilesInSrc );

    //
    //      Get destination path
    //
    DestinationPath.Initialize( DestinationDirectory->GetPath() );

    //
    //      Obtain an iterator for going thru the files
    //
    Iterator = ( PARRAY_ITERATOR )_FilesInSrc->QueryIterator( );
    if (Iterator == NULL) {
        DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
        return FALSE;       // help lint
    }

    //
    //      For each file in the array, see if it exists in the destination
    //      directory, and if it does not, then copy it.
    //
    while ( File =  (PFSN_FILE)Iterator->GetNext() ) {

        DebugAssert( !(((PFSNODE)File)->IsDirectory()) );

        Name = File->QueryName();
        if (Name == NULL) {
            DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
            return FALSE;       // help lint
        }

        //
        //      Form the path in the target file
        //
        DestinationPath.AppendBase( Name );

        DELETE( Name );

        //
        //      See if the file exists
        //
        FileToCreate = SYSTEM::QueryFile( &DestinationPath );

        //
        //      If the file does not exist, then it has to be added
        //
        if ( !FileToCreate ) {

            if ( !_PromptSwitch || Prompt( REPLACE_MESSAGE_ADD_YES_NO, &DestinationPath ) ) {

                DisplayMessage( REPLACE_MESSAGE_ADDING, NORMAL_MESSAGE, "%W", DestinationPath.GetPathString() );

                CopyTheFile( File->GetPath(), &DestinationPath );

                _FilesAdded++;
            }

        }

        DELETE( FileToCreate );

        //
        //      Set the destination path back to what it originally was
        //      ( i.e. directory specification, no file ).
        //
        DestinationPath.TruncateBase();

    }
    DELETE( Iterator );

    return TRUE;
}

BOOLEAN
REPLACE::ReplaceFiles (
        IN OUT  PFSN_DIRECTORY  DestinationDirectory
        )
/*++

Routine Description:

        Replaces those files in the DestinationDirectory that match Pattern
        by the corresponding files in SourceDirectory.

Arguments:

        DestinationDirectory    -       Supplies pointer to destination
                                                                directory.

Return Value:

        BOOLEAN -       TRUE if operation successful.
                        FALSE otherwise

Notes:

--*/

{

    PARRAY_ITERATOR     Iterator;
    PFSN_FILE           File;
    PFSN_FILE           FileToReplace;
    PATH                DestinationPath;
    PWSTRING            Name;
    PTIMEINFO           TimeSrc;
    PTIMEINFO           TimeDst;
    BOOLEAN             Proceed = TRUE;

    DebugPtrAssert( DestinationDirectory );
    DebugPtrAssert( _FilesInSrc );

    //
    //      Get destination path
    //
    DestinationPath.Initialize( DestinationDirectory->GetPath() );

    //
    //      Obtain an iterator for going thru the files
    //
    Iterator = ( PARRAY_ITERATOR )_FilesInSrc->QueryIterator( );
    if (Iterator == NULL) {
        DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
        return FALSE;       // help lint
    }

    //
    //      For each file in the array, see if it exists in the destination
    //      directory, and if it does, replace it
    //
    while ( File =  (PFSN_FILE)Iterator->GetNext() ) {

        AbortIfCtrlC();

        DebugAssert( !(((PFSNODE)File)->IsDirectory()) );

        Name = File->QueryName();
        if (Name == NULL) {
            DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
            return FALSE;       // help lint
        }

        //
        //      Form the path in the target file
        //
        DestinationPath.AppendBase( Name );

        DELETE( Name );

        //
        //      See if the file exists
        //
        FileToReplace = SYSTEM::QueryFile( &DestinationPath );


        if ( FileToReplace ) {

            //
            //      If the CompareTime switch is set, then we only proceed if
            //      the destination file is older than the source file.
            //
            if ( _CompareTimeSwitch ) {

                TimeSrc = File->QueryTimeInfo();
                TimeDst = FileToReplace->QueryTimeInfo();

                if (TimeSrc == NULL) {
                    DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
                    return FALSE;       // help lint
                }
                if (TimeDst == NULL) {
                    DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );
                    return FALSE;       // help lint
                }

                Proceed = *TimeDst < *TimeSrc;

                DELETE( TimeSrc );
                DELETE( TimeDst );

            }

            if ( Proceed ) {

                //
                //      We replace the file if it is NOT read-only
                //      (unless the ReadOnly switch is set )
                //
                if ( _ReadOnlySwitch || !(FileToReplace->IsReadOnly()) ) {

                    if ( !_PromptSwitch || Prompt( REPLACE_MESSAGE_REPLACE_YES_NO, &DestinationPath ) ) {

                        DisplayMessage( REPLACE_MESSAGE_REPLACING, NORMAL_MESSAGE, "%W", DestinationPath.GetPathString() );

                        //
                        //      If the file is read-only, we reset the read-only attribute
                        //      before copying.
                        //
                        if ( FileToReplace->IsReadOnly() ) {
                            FileToReplace->ResetReadOnlyAttribute();
                        }

                        CopyTheFile( File->GetPath(), &DestinationPath );

                        _FilesReplaced++;
                    }
                } else {

                    //
                    //      The file is read-only but the ReadOnly flag was
                    //      not set, we error out.
                    //
                    DisplayMessageAndExit( REPLACE_ERROR_ACCESS_DENIED,
                                           DestinationPath.GetPathString(),
                                           EXIT_ACCESS_DENIED );
                }
            }
        }

        DELETE( FileToReplace );

        //
        //      Set the destination path back to what it originally was
        //      ( i.e. directory specification, no file name part ).
        //
        DestinationPath.TruncateBase();

    }

    DELETE( Iterator );

    return TRUE;
}

BOOLEAN
REPLACE::Prompt (
        IN      MSGID           MessageId,
        IN      PCPATH          Path
        )

/*++

Routine Description:

        Gets confirmation from the user about a file to be added/replaced

Arguments:

        MessageId       -       Supplies the Id of the message to use for prompting
        Path            -       Supplies path to use as parameter for the message.

Return Value:

        BOOLEAN -       TRUE if the user confirmed the add/replace
                                FALSE otherwise

--*/

{

    DisplayMessage( MessageId, NORMAL_MESSAGE, "%W", Path->GetPathString() );
    return _Message.IsYesResponse();

}

BOOLEAN
REPLACE::CopyTheFile (
        IN      PCPATH          SrcPath,
        IN      PCPATH          DstPath
        )

/*++

Routine Description:

        Copies a file

Arguments:

        SrcPath         -       Supplies path of source file
        DstFile         -       Supplies path of destination file

Return Value:

        BOOLEAN -       TRUE

--*/

{

    ULONG   Size;

    DebugPtrAssert( SrcPath );
    DebugPtrAssert( DstPath );

    //
    //      Make sure that the buffers are big enough to hold the
    //      paths
    //
    Size = (SrcPath->GetPathString()->QueryChCount() + 1) * 2;
    if ( Size > _PathString1Size ) {
    _PathString1 = (LPWSTR)REALLOC( _PathString1, (unsigned int)Size );
            DebugPtrAssert( _PathString1 );
            _PathString1Size = Size;
    }

    Size = (DstPath->GetPathString()->QueryChCount() + 1) * 2;
    if ( Size > _PathString2Size ) {
    _PathString2 = (LPWSTR)REALLOC( _PathString2, (unsigned int)Size );
            DebugPtrAssert( _PathString2 );
            _PathString2Size = Size;
    }

    if ( !_PathString1 || !_PathString2 ) {
            DisplayMessageAndExit( REPLACE_ERROR_NO_MEMORY, NULL, EXIT_NO_MEMORY );

    }

    //
    //  Convert the paths to LPWSTR so that we can call CopyFile()
    //
    SrcPath->GetPathString()->QueryWSTR( 0, TO_END, _PathString1, _PathString1Size/sizeof(WCHAR) );
    DstPath->GetPathString()->QueryWSTR( 0, TO_END, _PathString2, _PathString2Size/sizeof(WCHAR) );

    //
    //      Now do the copy
    //
    if ( !CopyFile( _PathString1, _PathString2, FALSE ) ) {

            ExitWithError( GetLastError() );

    }

    return TRUE;

}