/*-M- Trash.c -- Move file(s) to trashcan directory/ies or remove them.
 *
 * Description: 
 *  Uses the environmental variable TRASH would should contain a 
 *  colon-delimited list of "trashcan" directories in which to move files.
 *  The system adminstrator should routinely remove the files from
 *  the trashcan directories.
 *
 *  The big advantage of moving trash files to trashcan directories is
 *  that, for directories with many files, it is much faster.
 *  Secondary advantages:
 *  -- can often rename directories that cannot be removed due to
 *      an ownership problem of a file within
 *  -- recovery from unintended removal is possible.
 *
 *  This source code is used both as library routine and as program.
 *  As a program, it must be compiled with -DDO_PROGRAM and includes
 *  everything needed apart from standard unix/linux C library.
 *  As a library routine, it uses a few allocparse features.
 *
 *  CAVEATS:
 *
 *  There are a number of modes that can be set through Set routines below.
 *  The defaults may be acceptable for most uses.
 *  In large programs, there may be conflicts between different uses
 *  of Trash fncs requiring different modes... be careful.
 */
/*
 * Change Log:  (in reverse cronological order, please)
 *      Date        Who         What
 *      2003/01/07     ted       Created.
 */

#ifdef DO_PROGRAM
/*--- HelpLines -- documentation printed with -h option and in .phelp file */
const char *HelpLines[] = 
{
"NAME   ",
"    trash -- move file(s) into trashcan directory or remove them  ",
"   ",
"SYNOPSIS   ",
"    trash [<option>]... <file>...   -- trash the files   ",
"    trash -h   -- for this message ",
"   ",
"OPTIONS ",
"   -h  -- for this help message  ",
"   -s  -- suppress all diagnostics except for total failure ",
"       to remove a file (but see -f) and error exit messages. ",
"   -v  -- verbose (default) ",
"   -f  -- exit 0 even if can't remove files. ",
"       This also suppresses diagnostics about removal failure.",
"   -rm -- remove if we can't rename (default)  ",
"   -no-rm -- do NOT remove if we can't rename ",
"   -same-name -- try first to rename using same filename; ",
"       if this fails, then use a made up new name. ",
"       Rename to same name is failed if file by this name exists ",
"       already, but this check is prone to race conditions. ",
"   -no-same-name -- use only made up new names in trashcan dir. ",
"       This is the default. ",
"   -nfs-safe -- ignore .nfs* files when doing removals. ",
"       This is default and avoids some problems with the dirty details ",
"       of some nfs provided filesystems.  ",
"   -no-nfs-safe -- don't treat .nfs* files specially.  ",
"   -   -- all following arguments are to be treated as files to remove ",
"           (otherwise args beginning with `-' are options).  ",
"   ",
"DESCRIPTION   ",
"  ",
"The trash program may be used as an optimized equivalent to the ",
"traditional unix `rm -rf' to remove files / directories. ",
"The optimization occurs when files (including directories) ",
"can be renamed into a trashcan directory (which must be on the ",
"same filesystem i.e. same disk partition).  ",
"For removing a directory with many files, it is far faster to simply ",
"rename it into a trashcan directory. ",
"Of course, it must eventually be removed from the trashcan directory ",
"to avoid filling up the fileysystem.  ",
"In addition to speed, there are two other advantages: ",
"-- It is possible to rename directories which cannot be removed by ",
"a non-root user due to the presence in the directory of files owned ",
"by another user.  ",
"-- Recovery of files that were unintentially trashed may be simplified. ",
"  ",
"The trash program attempts renames only into trashcan directories ",
"that are on the same filesystem as the file to be removed. ",
"If the trash program cannot rename away a file, it attempts to ",
"(recursively) remove it. ",
"Unlike some damaged versions of rm, the trash program first attempts ",
"to make directories writeable before removing files from them. ",
"  ",
"The trash program knows the locations of trashcan directories from the ",
"environmental variable TRASH which should contain a colon-delimited ",
"set of paths for the trashcan directories (same format as for PATH). ",
"If this env. variable is missing, then it can only do actual deletions. ",
"  ",
"It is recommended that the system administrator set the value for TRASH ",
"in the commonly used shell startup files, and keep this in synch with ",
"trashcan directories on each filesystem, and with a root cron job run  ",
"e.g. nightly to remove contents of the trashcan directories. ",
"It is also possible for an individual user to do something similar, ",
"but without root permission it may sometimes be impossible to remove ",
"all of the contents of a trashcan directory.  ",
"   ",
"When renaming into a trashcan directory, the default is to make up a ",
"new name (and to print this name to stderr). ",
"The new name is composed of various information which put together is ",
"likely to be unique, and a check for existing file is made to avoid ",
"overwriting; still race conditions are possible. ",
"The current format for made up names is:  ",
"       YYYYMMDD.hhmmss.prog.pid.iter.filename  ",
"Where: YYYY is year, MM is month 01-12, DD is day 01-31, hh is hour 00-23, ",
"   mm is minute 00-59, ss is second 00-59, ",
"   prog is name of program that trashed the file, if known (limit 20 chars), ",
"   pid is process id on of program on computer it ran on, ",
"   iter is added for uniqueness, and filename is filename of renamed file ",
"   (limit 30 chars).  ",
"   ",
0           /* terminator -- must be last */
};
#endif /* DO_PROGRAM */


/*---------- Includes --------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdarg.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/file.h>
#include <dirent.h>
#include <sys/time.h>
#include <time.h>

#define MAX_PATH_LENGTH 1024    /* should be defined somewhere, but... */
#define IS_DIRECTORY( mode )  ( ( mode & S_IFMT ) == S_IFDIR )
#define IS_REGULAR( mode )    ( ( mode & S_IFMT ) == S_IFREG )
#define IS_SOFT_LINK( mode )  ( ( mode & S_IFMT ) == S_IFLNK )


/*--- TrashMode... global modes affecting behavior
*/
static int TrashModeRemove = 1;     /* nonzero to remove if we can't rename */
static int TrashModeSameName = 0;   /* nonzero keep same filename if possible */
static int TrashModeVerbose = 1;       /* nonzero to print what we're doing */
static int TrashModeNfsSafe = 1;    /* nonzero to not remove .nfs files */

/*--- TrashIter, TrashTimeString --
*   part of unique renaming scheme. Each successive rename
*   uses a new iteration number as part of filename.
*   The TrashTimeString is set first time that this code is used in a process.
*/
static unsigned TrashIter;
static char TrashTimeString[80];

/*-F- TrashModeRemoveSet -- set mode for Trash() to remove file if renames fail.
*   The default mode is to do so.
*/
void TrashModeRemoveSet( int RemoveIfRenameFails )
{
    TrashModeRemove = RemoveIfRenameFails;
}

/*-F- TrashModeSameNameSet -- set mode for Trash fncs to use same filename
*   if possible within the trash directory. If not possible, a madeup name
*   is used.
*/
void TrashModeSameNameSet( int UseSameNameIfPossible )
{
    TrashModeSameName = UseSameNameIfPossible;
}

/*-F- TrashModeVerboseSet -- turn on verbose messages (typically 1 per 
*   trashed file or directory).
*/
void TrashModeVerboseSet( int Verbose )
{
    TrashModeVerbose = Verbose;
}


#ifdef DO_PROGRAM   /* provide standalone diagnostics and exit handling */
static char * TrashErrExitProgramName = NULL; /* Can be set by program */
#else /* DO_PROGRAM */
/* Here for allocparse library generation */
#include <allocparseX.h>
#include <ErrExit.c.h>
#define TrashErrExitProgramName ErrExitProgramName
#endif /* DO_PROGRAM */

/*--- TrashErr... -- handy error routines
*/
#define STDERR  2               /* error file no. (by convention) */
static void TrashErrExit(
        const char * Message, /* message to put to stderr; printf like */
        ...             /* printf-like args */
        )
{
    int NChars; /* Temporary count */
    char FormatBuf[2000];
    va_list ArgP;
    int ErrCode = 1;        /* exit status to use */

    if( Message != NULL )        /* print nothing if no message */
    {
        if( TrashErrExitProgramName != NULL ) /* print program name if one */
        {
            NChars = strlen( TrashErrExitProgramName );
            if( NChars > 0 )            /* Print program name */
                    (void) write( STDERR, TrashErrExitProgramName, NChars );

            (void) write( STDERR, ": ", 2 );  /* Print colon space */
        }

        /* Print Message */
        va_start( ArgP, Message );
        vsprintf(  FormatBuf, Message, ArgP );
        va_end( ArgP );
        NChars = strlen( FormatBuf );
        if( NChars > 0 )        /* Print message */
                (void) write( STDERR, FormatBuf, NChars );

        (void) write( STDERR, "\n", 1 );    /* terminating new line */
    }

    exit( ErrCode );            /* never returns */
}
static void TrashErrPrint(
        const char * Message, /* message to put to stderr; printf like */
        ...             /* printf-like args */
        )
{
    int NChars; /* Temporary count */
    char FormatBuf[2000];
    va_list ArgP;

    if ( ! TrashModeVerbose ) return;

    if( Message != NULL )        /* print nothing if no message */
    {
        if( TrashErrExitProgramName != NULL ) /* print program name if one */
        {
            NChars = strlen( TrashErrExitProgramName );
            if( NChars > 0 )            /* Print program name */
                    (void) write( STDERR, TrashErrExitProgramName, NChars );

            (void) write( STDERR, ": ", 2 );  /* Print colon space */
        }

        /* Print Message */
        va_start( ArgP, Message );
        vsprintf(  FormatBuf, Message, ArgP );
        va_end( ArgP );
        NChars = strlen( FormatBuf );
        if( NChars > 0 )        /* Print message */
                (void) write( STDERR, FormatBuf, NChars );

        (void) write( STDERR, "\n", 1 );    /* terminating new line */
    }
}

/*--- TrashDirs -- list of trash directories
*/
static struct TrashDir
{
    const char *Path;     /* where to find trash directory */
    struct stat StatBuf;        /* info from stat() for that Path */
} *TrashDirs;
static int NTrashDirs;     /* how many in TrashDirs[] */
static int TrashDirIsInit; /* nonzero after init */

/*-F- TrashInit -- first time init's for Trash fncs.
*   Calling this is optional since it will be taken care of automatically,
*   but might be preferred for some programs.
*/
void TrashInit(void)
{
    const char *EnvVar;
    int EnvVarLen;
    char *Copy;
    char *Ptr;
    char *NextPtr = NULL;
    int NPaths;

    if ( TrashDirIsInit ) return;
    TrashDirIsInit = 1; /* once only */

    EnvVar = getenv("TRASH");
    NTrashDirs = 0;
    if ( ! EnvVar ) 
    {
        TrashErrPrint(
            "Environmental variable TRASH not set -- can't use trashcan dirs.");
        return;
    }
    EnvVarLen = strlen(EnvVar);
    Copy = malloc(EnvVarLen+1);
    if ( ! Copy ) TrashErrExit("malloc failure.");
    strcpy( Copy, EnvVar );

    for ( NPaths = 0, Ptr = Copy; (NextPtr = strchr(Ptr,':')) != NULL;
        NPaths++, Ptr = NextPtr+1 ) {;}
    NPaths++;   /* one more if no terminator */
    TrashDirs = malloc( NPaths*sizeof(struct TrashDir) );
    if ( ! TrashDirs ) TrashErrExit("malloc failure.");
    for ( Ptr = Copy;  ; Ptr = NextPtr+1 ) 
    {
        NextPtr = strchr(Ptr,':');
        if ( NextPtr ) *NextPtr = 0;   /* null terminate */
        if ( Ptr[0] )
        {
            TrashDirs[NTrashDirs].Path = Ptr;
            if ( lstat( Ptr, &TrashDirs[NTrashDirs].StatBuf ) == 0 )
            {
                NTrashDirs++;
            }
            else
            {
                TrashErrPrint(
                    "Directory in env. var TRASH is not accessible: %s", Ptr );
            }
        }
        if ( ! NextPtr ) break;
    }
}

/*-F- TrashRenameLow -- cover for rename()
*   Reports errors according to current verbosity modes.
*   Returns nonzero in case rename failed (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*/
int TrashRenameLow( const char *FilePath, const char *NewPath )
{
    int Error = 1;
    struct stat StatBuf;
    if ( lstat( NewPath, &StatBuf ) == 0 )
    {   /* existing file */
        TrashErrPrint("Retry needed: Existing file is called: %s", NewPath );
        return 1;
    }
    if ( lstat( FilePath, &StatBuf ) != 0 )
    {
        Error = errno;
        if ( Error == ENOENT )
        {
            TrashErrPrint("Ignoring non-existent file: %s", FilePath );
            return 0;
        }
    }
    else
    {
        /* Some unices prohibit the renaming away of readonly directories
        *   from beneath a writeable directory.
        *   Try to patch this up using chmod.
        *   Note that chmod will fail if we are not the owner...
        *
        *   We will also get an error if the directory containing
        *   our file to delete is readonly, but we deliberately do NOT
        *   try to change that.
        */
        if ( S_ISDIR(StatBuf.st_mode) && (StatBuf.st_mode & 0700) != 0700 )
        {
            (void) chmod( FilePath, StatBuf.st_mode | 0700 );
        }
    }
    if ( rename( FilePath, NewPath ) == 0 )
    {
        TrashErrPrint("Renamed `%s' to `%s'", FilePath, NewPath );
        Error = 0;
    }
    else
    {
        Error = errno;
        if ( Error == ENOENT )
        {   /* no such source */
            /* this COULD be due to the trash directory suddenly disappearing,
            *   but that is very unlikely. */
            Error = 0;
            TrashErrPrint("Ignoring non-existent file: %s", FilePath );
        }
        else
        {
            TrashErrPrint("Rename of `%s' to `%s' failed due to error %d (%s)", 
                FilePath, NewPath,
                Error, strerror(Error) );
            Error = 1;
        }
    }
    return Error;
}

/*-F- TrashRenameFileName -- attempt to rename file to 
*   given trash can dir and filename therein.
*   Returns nonzero in case rename failed (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*/
int TrashRenameFileName( 
    const char *FilePath, 
    const char *TrashCanPath, 
    const char *FileName 
    )
{
    int Error = 1;
    int Len = strlen(TrashCanPath)+1+strlen(FileName);
    char * NewPath = malloc(Len+1);
    if ( ! NewPath ) TrashErrExit("malloc failure");
    strcpy( NewPath, TrashCanPath );
    strcat( NewPath, "/");
    strcat( NewPath, FileName );
    Error = TrashRenameLow( FilePath, NewPath );
    free(NewPath);
    return Error;
}

/*-F- TrashRenameNewName -- attempt to rename file to
*   given trash can dir using the Iter'th made up name.
*   Iter can be any integer.
*   This will fail if the made up name is in use,
*   as can happen due to race conditions (in spite of 
*   all precautions); retries are advised!
*   Returns nonzero in case rename failed (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*
*   BUGS: it can sometimes happen that one rename obliterates
*   a previous one, in spite of all precautions; this current
*   algorithm provides no absolute prevention of this
*   in the case of simple files (or empty directories)
*   but non-empty directories should be exempt from this problem.
*   This is only an issue if one wanted to recover a file.
*/
int TrashRenameNewName( 
    const char *FilePath, 
    const char *TrashCanPath
    )
{
    /* It seems to be pretty difficult to reliably generate
    *   unique names, and even harder to totally avoid
    *   race conditions in using names.
    *   The rename() system call, sadly, will replace an existing
    *   file (permissions permitting).... not what is desired.
    *   There is no equivalent to the O_EXCL option of open().
    *
    *   an alternate approach that could work is to
    *   use mkstemp to create a unique file in the trashcan dir,
    *   and then assume that we can rename to a name BASED on the
    *   name of the "temp file" without harm...
    *   this works only if the "temp file" is left around for
    *   as long as the renamed file...
    */

    char FileName[200];
    #define FromFileMax 30
    char FromFileName[FromFileMax+1];
    int Len;
    int Error = 1;
    char *NewPath;
    unsigned Iter = TrashIter++;
    const char *ProgramName;

    {   /* extract last filename component as possible */
        /* But if someone says "trash directory/." or "trash ."
        *   then don't try to do anything overly smart, but don't
        *   do anything overly dumb either
        */
        int StartIndex;     /* begin char in FilePath */
        int EndIndex;       /* one past last char */
        EndIndex = strlen(FilePath);
        for (;;)
        {
            for ( StartIndex = EndIndex-1; 
                StartIndex >= 0 && FilePath[StartIndex] != '/'; 
                StartIndex-- ) {;}
            StartIndex++;
            if ( StartIndex == 0 || StartIndex != EndIndex ) break;
            /* Here if StartIndex == EndIndex, i.e. slash at end */
            EndIndex--;
        }
        if ( EndIndex-StartIndex > 30 ) EndIndex = StartIndex+30;
        if ( (EndIndex-StartIndex) > 0 )
            memcpy( FromFileName, FilePath+StartIndex, (EndIndex-StartIndex) );
        FromFileName[(EndIndex-StartIndex)] = 0;
    }

    ProgramName = TrashErrExitProgramName;
    if ( ! ProgramName ) ProgramName = "Trash";
    if ( strrchr(ProgramName,'/') ) ProgramName = strrchr(ProgramName,'/')+1;

    /* By putting the program time at the front
    *   then the latest renames will appear last in the directory listing
    *   which might be helpful; and all trashes by same program may
    *   be together.
    *   Of course, the current time (in seconds) has some uniqueness,
    *   although it may be shared with others from the same process.
    *   Put process id second.  Of course, process ids are unique
    *   only on a single processor, and only at a given time...
    *   but the chance of two processes with same pid doing trash to the
    *   same trash directory in the same second is pretty small.
    *   Hopefully Iter saves the day... no absolute gaurantees of course...
    *   We put some, perhaps all, of the from file at the end to help
    *   in identification; unfortunately, this is not a lot of help for
    *   uniqueness for cases like e.g. removing a bunch of subdirectories
    *   with the same name.
    */
    if ( ! TrashTimeString[0] )
    {
        struct timeval TimeVal;
        struct tm *LocalTime;       /* see ctime(3) */

        gettimeofday(&TimeVal,NULL);
        LocalTime = localtime( (long *) &TimeVal.tv_sec );
        sprintf(TrashTimeString, "%d%02d%02d.%02d%02d%02d",
            (LocalTime->tm_year+1900) ,
            (LocalTime->tm_mon+1)     , /* add one to get 1-12 */
            (LocalTime->tm_mday)      , /* this begins with 1 as is */
            (LocalTime->tm_hour)      ,
            (LocalTime->tm_min)       ,
            (LocalTime->tm_sec)       );
    }
    sprintf(FileName,"%s.%.20s.%u.%u.%.*s",
        TrashTimeString,
        ProgramName, 
        (unsigned) getpid(),           /* of this process */
        Iter,               /* different each time for this process */
        FromFileMax,
        FromFileName 
        );

    Len = strlen(TrashCanPath)+1+strlen(FileName);
    NewPath = malloc(Len+1);
    if ( ! NewPath ) TrashErrExit("malloc failure");
    strcpy( NewPath, TrashCanPath );
    strcat( NewPath, "/");
    strcat( NewPath, FileName );
    Error = TrashRenameLow( FilePath, NewPath );
    free(NewPath);
    return Error;
}

/*-F- TrashRenameToDir -- rename a file away into given trash can dir
*   using currently selected modes.
*   Returns nonzero in case all renames fail (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*/
int TrashRenameToDir( 
    const char *FilePath,
    const char *TrashCanPath
    )
{
    const char *FileName;
    int Try;

    /* Try to keep the same name(?) */
    if ( TrashModeSameName )
    {
        FileName = strrchr(FilePath,'/');
        if ( FileName ) FileName++;
        else FileName = FilePath;
        if ( TrashRenameFileName( FilePath, TrashCanPath, FileName ) == 0 ) 
            return 0;
    }

    /* If that doesn't work, do new name */
    for ( Try = 0; Try <= 9; Try++ )
    {
        if ( TrashRenameNewName( FilePath, TrashCanPath ) == 0 ) return 0;
    }

    return 1;
}

/*-F- TrashRename -- rename a file into a trash can dir from TRASH env. var.
*   Returns nonzero in case all renames fail (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*/
int TrashRename( const char *FilePath )
{
    struct stat StatBuf;
    int IDir = 0;
    struct TrashDir *DirP = NULL;
    int NMatch = 0;
    int Error = 1;

    TrashInit();

    if ( lstat( FilePath, &StatBuf ) != 0 )
    {
        Error = errno;
        if ( Error == ENOENT )
        {
            TrashErrPrint("Ignoring nonexistent file: %s", FilePath );
            return 0;   /* not an error... */
        }
        else
        {
            TrashErrPrint("Access failure %d (%s) to: %s", 
                Error, strerror(Error), FilePath );
            return 1;
        }
    }
    if ( NTrashDirs == 0 ) return 1;    /* don't need duplicate msg */
    for ( IDir = 0; IDir < NTrashDirs; IDir++ )
    {
        DirP = &TrashDirs[IDir];
        if ( DirP->StatBuf.st_dev == StatBuf.st_dev ) 
        {
            NMatch++;
            if ( TrashRenameToDir( FilePath, DirP->Path ) == 0 ) return 0;
        }
    }
    if ( NMatch == 0 )
        TrashErrPrint("No directory in TRASH env. variable matches file system of: %s",
            FilePath );
    else TrashErrPrint("Giving up on renaming: %s", FilePath );
    return Error;
}

/*-F- TrashRemove -- yet another recursive file remove subroutine.
*   Returns nonzero in case remove fails (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*/
int TrashRemove( const char *FilePath )
{
    int StringLen;
    int Error = 0;
    struct stat StatBuf;   /* for lstat call */

    /* Check arguments for validity */
    if ( ! FilePath || ! *FilePath ) return 0;  /* ignore... */

    /* Compute some basic parameters and do more checking */
    StringLen = strlen( FilePath );
    if ( StringLen > MAX_PATH_LENGTH ) 
    {
        TrashErrPrint("Path too long : `%.500s'......", FilePath );
        return 1;
    }

    /* Get status of source */
    if ( lstat( FilePath, &StatBuf ) < 0) Error = errno;

    /* Perform basic check on source */
    if ( Error != 0 ) 
    {
        if ( Error == ENOENT ) return 0;   /* doesn't exist... ? */
        TrashErrPrint("Access error %d (%s) to: %s",
            Error, strerror(Error), FilePath );
    }

    /* 
    *   remove files beneath directory
    */
    if ( IS_DIRECTORY( StatBuf.st_mode ) )
    {
        DIR *DirFile;   /* pointer to directory read structure */
        struct dirent *DirEntryP;       /* pointer to dir. entry */
        char SubFilePath[MAX_PATH_LENGTH+1];  /* full path of sub */
        int Len;

        /* Whoa! We can't remove a directory unless we first remove
        *   what is under it, and in order to do THAT, the directory
        *   must be readable, writeable AND executable.
        *   This may be already the case, or may require chmod.
        *   The chmod may not be required, but doing so is harmless;
        *   the chmod may fail (e.g. not owner) and yet not be needed.
        *   There is no point in setting other bits besides user rwx,
        *   because if we're not owner we can't change anything anyway,
        *   and if we are owner, we don't need the other bits,
        *   and should we abort along the way here, we might leave
        *   a security hole.
        */
        (void) chmod( FilePath, StatBuf.st_mode | 0700 );

        DirFile = opendir( FilePath );
        if ( DirFile == (DIR *) NULL ) 
        {
            Error = errno;
            TrashErrPrint("Directory read error %d (%s) on: %s",
                Error, strerror(Error), FilePath );
        }
        while ( ( DirEntryP = readdir( DirFile ) ) != NULL )
        {               /* until end of directory */
            int NameLen;
            char * Name = DirEntryP->d_name;
            #if defined(ATOOL_TARGET_sun4) || defined(ATOOL_TARGET_alpha)       /* where d_name not null terminated */
            NameLen = DirEntryP->d_namlen;
            #else /* no d_namlen; d_name should be null term */
            NameLen = strlen(Name);
            #endif

            if ( NameLen <= 0 ) continue; /* null entry*/
            if ( Name[0] == '.' )
            {
               if ( NameLen == 1 ) continue;        /* ignore "." */
               if ( Name[1] == '.' &&
                   NameLen == 2 ) continue;     /* ignore ".." */
               if ( TrashModeNfsSafe && NameLen >= 4 && 
                    Name[1] == 'n' &&
                    Name[2] == 'f' &&
                    Name[3] == 's' ) continue;  /* ignore nfs temp files */
            }
            /* construct full path name of sub file */
            (void) strcpy( SubFilePath, FilePath );
            Len = strlen(SubFilePath);
            if ( Len+1+NameLen > MAX_PATH_LENGTH ) 
            {
                TrashErrPrint("Path too long at: %s/%*s", FilePath, NameLen, Name);
                return 1;
            }
            SubFilePath[Len++] = '/';
            strncpy( SubFilePath+Len, Name, NameLen );
            Len += NameLen;
            SubFilePath[Len] = 0;   /* null terminate */
            /* and remove beneath (new) target directory */
            Error = TrashRemove( SubFilePath );
            if ( Error ) return Error;
        }
        (void) closedir( DirFile );    /* free it up */

        if ( rmdir( FilePath ) ) 
        {
            Error = errno;
            TrashErrPrint("Directory remove error %d (%s): %s", 
                Error, strerror(Error), FilePath );
            return 1;
        }
    }
    else
    {   /* remove it */
        if ( unlink( FilePath ) ) 
        {
            Error = errno;
            TrashErrPrint("File remove error %d (%s): %s", 
                Error, strerror(Error), FilePath );
            return 1;
        }
    }

    return 0;   /* success */
}

/*-F- TrashParentIsReadonly -- returns nonzero if parent of file does
*   not give us permission to write it... thus we should leave
*   the file alone...
*/
int TrashParentIsReadonly(const char *FilePath) 
{
    char PathBuf[4000];
    char *Slash;

    if ( strlen(FilePath) > sizeof(PathBuf)-8 )
    {
        TrashErrPrint("Path too long.");
        return 1;
    }
    strcpy( PathBuf, FilePath );
    for (;;)
    {
        Slash = strrchr( PathBuf, '/' );
        if ( Slash == PathBuf )
        {   /* the root directory... */
            PathBuf[1] = 0;
            break;
        }
        else
        if ( Slash )
        {
            *Slash++ = 0; /* chop off ending part */
            /* handle stuff like:   name/   name/././.  etc.  */
            if ( ! Slash[0] || (Slash[0] == '.' && Slash[1] == 0 ) ) {;}
            else break;
        }
        else
        {
            /* just a simple name. Parent is "." unless name is "." or ".."
            *   which we'll ignore...
            */
            strcpy(PathBuf,".");
            break;
        }
    }
    if ( access( PathBuf, X_OK|W_OK ) )
    {
        /* sadly for us, access works using permissions of the "real"
        *   user id only.... so it is inaccurate for setuid programs.
        */
        if ( getuid() != geteuid() )
        {
            return 0;   /* can't tell using access() ... perhaps its ok? */
        }
        TrashErrPrint("Errno %d for access of %s",
            errno, PathBuf );
        return 1;   /* no access */
    }
    return 0;
}


/*-F- Trash -- rename into trashcan dir (or if that fails, remove acc. to mode)
*   a given file or directory.
*   Returns nonzero in case all attempts fail (except if file does not
*   exist, which is treated as a success); returns 0 for success.
*/
int Trash( 
    const char *FilePath 
    )
{
    if ( TrashParentIsReadonly(FilePath) )
    {
        TrashErrPrint("Parent directory of `%s' is not writeable by us!", 
            FilePath );
        return 1;
    }
    if ( TrashRename(FilePath) )
    {
        if ( TrashModeRemove )
        {
            TrashErrPrint("Doing direct remove instead for: %s", FilePath );
            if ( TrashRemove(FilePath) ) return 1;
        }
        else return 1;
    }
    return 0;   /* success */
}


/*=========================== Standalone Program code =====================*/

#ifdef DO_PROGRAM

static int TrashModeForceSuccess = 0;  /* nonzero to ignore file remove errors for exit */
static int TrashModeArgsAreFiles = 0;   /* nonzero to treat all args as files */

/*--- main program 
*/
int main( 
    int argc, 
    char **argv 
    )
{
    char *Arg;
    int NFail = 0;
    int NFiles = 0;

    TrashErrExitProgramName = *argv++;
    while ( (Arg = *argv++) != NULL )
    {
        if ( ! Arg[0] ) continue;
        if ( Arg[0] != '-' || TrashModeArgsAreFiles )
        {
            NFiles++;
            if ( Trash(Arg) ) 
            {   /* could not get rid of the file */
                if ( ! TrashModeForceSuccess ) 
                {   /* if error generation not suppressed... */
                    int ModeSave = TrashModeVerbose;
                    NFail++;
                    /* Even in silent mode, print this message unless Force mode
                    *   is used.
                    */
                    TrashModeVerbose = 1;
                    TrashErrPrint("Could not rename or remove: %s", Arg );
                    TrashModeVerbose = ModeSave;    /* restore */
                }
            }
        }
        else
        if ( ! strcmp(Arg, "-h") || ! strcmp(Arg, "--help") ) 
        {
            int HelpLineN;
            for ( HelpLineN = 0; HelpLines[HelpLineN]; HelpLineN++ )
                printf("%s\n", HelpLines[HelpLineN] );
            exit(0);
        }
        else
        if ( ! strcmp(Arg, "-") ) TrashModeArgsAreFiles = 1;
        else
        if ( ! strcmp(Arg, "-rm" ) ) TrashModeRemove = 1;
        else
        if ( ! strcmp(Arg, "-no-rm" ) ) TrashModeRemove = 0;
        else
        if ( ! strcmp(Arg, "-same-name" ) ) TrashModeSameName = 1;
        else
        if ( ! strcmp(Arg, "-no-same-name" ) ) TrashModeSameName = 0;
        else
        if ( ! strcmp(Arg, "-s" ) ) TrashModeVerbose = 0;
        else
        if ( ! strcmp(Arg, "-v" ) ) TrashModeVerbose = 1;
        else
        if ( ! strcmp(Arg, "-f" ) ) TrashModeForceSuccess = 1;
        else
        if ( ! strcmp(Arg, "-nfs-safe" ) ) TrashModeNfsSafe = 1;
        else
        if ( ! strcmp(Arg, "-no-nfs-safe" ) ) TrashModeNfsSafe = 0;
        else
        {
            TrashErrPrint("Unknown arg (use -h for help): %s",  Arg );
        }
    }
    if ( NFiles == 0 ) TrashErrPrint("No files. (Use -h for help).");
    if ( TrashModeForceSuccess || NFail == 0 ) exit(0);
    if ( NFail ) TrashErrExit("Aborting due to errors.");
    exit(0);
    return 0;
}
#endif /* DO_PROGRAM */

