adtools/clib2

View on GitHub
library/unistd_execve.c

Summary

Maintainability
Test Coverage
/*
 * $Id: unistd_execve.c,v 1.13 2006-10-02 07:16:06 obarthel Exp $
 *
 * :ts=4
 *
 * Portable ISO 'C' (1994) runtime library for the Amiga computer
 * Copyright (c) 2002-2015 by Olaf Barthel <obarthel (at) gmx.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Neither the name of Olaf Barthel nor the names of contributors
 *     may be used to endorse or promote products derived from this
 *     software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef _UNISTD_HEADERS_H
#include "unistd_headers.h"
#endif /* _UNISTD_HEADERS_H */

/****************************************************************************/

#include <dos/stdio.h>

/****************************************************************************/

/* The following is not part of the ISO 'C' (1994) standard. */

/****************************************************************************/

/* This gets handed around when trying to locate a program or a script
   interpreter which knows how to do the job. */
struct program_info
{
    struct Segment *    resident_command;    /* If not NULL, points to a valid
                                               resident command */
    BPTR                home_dir;            /* If not ZERO refers to the directory
                                               in which the command to be executed
                                               can be found */
    BPTR                segment_list;        /* If not ZERO refers to a command
                                               loaded into memory */
    char *                program_name;        /* Points to the name of the command */
    char *                interpreter_name;    /* If not NULL the name of the command
                                               interpreter to use */
    char *                interpreter_args;    /* If not NULL these are additional
                                               arguments to be passedto the command
                                               interpreter */
};

/****************************************************************************/

/* Try to find a resident command by name; returns a pointer to the Segment
   data structure ready to use, or NULL if none could be found */
static struct Segment *
find_resident_command(const char * command_name)
{
    struct Segment * seg;

    /* This must be done under Forbid() since dos.library does not have
       a more sophisticated arbitration method for this yet... */
    Forbid();

    seg = FindSegment((STRPTR)command_name,NULL,FALSE);
    if(seg == NULL)
        seg = FindSegment((STRPTR)command_name,NULL,TRUE);

    if(seg != NULL)
    {
        /* Check if that's a disabled command or something else. */
        if((seg->seg_UC < 0) && ((seg->seg_UC > CMD_INTERNAL) || (seg->seg_UC <= CMD_DISABLED)))
        {
            seg = NULL;
        }
        else
        {
            /* Unless it's a built-in command, mark it as having another user. */
            if(seg->seg_UC >= 0)
                seg->seg_UC++;
        }
    }

    Permit();

    return(seg);
}

/****************************************************************************/

/* Try to read the first line of a script file */
static int
get_first_script_line(const char * path,char ** line_ptr)
{
    BPTR script_file;
    int result = -1;
    char * script_line = NULL;
    size_t script_line_size = 0;
    size_t script_line_length = 0;
    LONG c;

    (*line_ptr) = NULL;

    script_file = Open((STRPTR)path,MODE_OLDFILE);
    if(script_file == ZERO)
    {
        __set_errno(__translate_io_error_to_errno(IoErr()));
        goto out;
    }

    /* Make file access a little more robust. */
    SetVBuf(script_file,NULL,BUF_LINE,1024);

    while((c = FGetC(script_file)) != -1)
    {
        /* Still enough room in the buffer? We always reserve
           enough memory for the next character and a NUL
           to terminate the string with. */
        if(script_line_length + 2 > script_line_size)
        {
            char * new_script_line;

            /* Give the user a chance to bail out. */
            if(__check_abort_enabled && (SetSignal(0,0) & __break_signal_mask) != 0)
            {
                __set_errno(EAGAIN);
                goto out;
            }

            /* Always reserve a little more memory than needed,
               and one extra byte to allow us to to NUL-terminate
               the string. */
            new_script_line = realloc(script_line,script_line_length + 10);
            if(new_script_line == NULL)
            {
                __set_errno(ENOMEM);
                goto out;
            }

            script_line            = new_script_line;
            script_line_size    = script_line_length + 10;
        }

        /* Stop when we hit a line feed or an unprintable character */
        if(c == '\n' || (c < ' ' && c != '\t' && c != '\r') || (c >= 128 && c < 160))
            break;

        script_line[script_line_length++] = c;
    }

    /* Check for read error */
    if(c == -1 && IoErr() != 0)
    {
        __set_errno(__translate_io_error_to_errno(IoErr()));
        goto out;
    }

    /* Provide for NUL-termination. */
    if(script_line_size > 0)
    {
        /* Also strip all trailing blank spaces; that includes
           line feed and carriage return characters. */
        while(script_line_length > 0 && isspace(script_line[script_line_length-1]))
            script_line_length--;
    }

    script_line[script_line_length] = '\0';

    (*line_ptr) = script_line;
    script_line = NULL;

    result = 0;

 out:

    if(script_file != ZERO)
        Close(script_file);

    if(script_line != NULL)
        free(script_line);

    return(result);
}

/****************************************************************************/

/* Release all the resources allocate for the program information, as produced
   by the find_command() function */
static void
free_program_info(struct program_info * pi)
{
    if(pi != NULL)
    {
        if(pi->resident_command != NULL)
        {
            Forbid();

            if(pi->resident_command->seg_UC > 0)
                pi->resident_command->seg_UC--;

            Permit();
        }

        if(pi->interpreter_name != NULL)
            free(pi->interpreter_name);

        if(pi->interpreter_args != NULL)
            free(pi->interpreter_args);

        if(pi->program_name != NULL)
            free(pi->program_name);

        if(pi->home_dir != ZERO)
            UnLock(pi->home_dir);

        if(pi->segment_list != ZERO)
            UnLoadSeg(pi->segment_list);

        free(pi);
    }
}

/****************************************************************************/

/* Try to find a command by name; if the name does not include any path
   information, try the dos.library resident command list */
static int
find_command(const char * path,struct program_info ** result_ptr)
{
    struct name_translation_info nti;
    char * script_line = NULL;
    struct program_info * pi;
    APTR old_window_ptr;
    int result = -1;
    BPTR old_dir = ZERO;
    BOOL found_path_separator;
    BOOL found_volume_separator;
    const char *p;
    int error;
    char c;

    (*result_ptr) = NULL;

    /* We don't want to show any dos.library requesters while we
       are looking */
    old_window_ptr = __set_process_window((APTR)-1);

    pi = malloc(sizeof(*pi));
    if(pi == NULL)
    {
        __set_errno(ENOMEM);
        goto out;
    }

    memset(pi,0,sizeof(*pi));

    error = __translate_unix_to_amiga_path_name(&path,&nti);
    if(error != 0)
    {
        __set_errno(error);
        goto out;
    }

    /* Check if the path name uses separator characters, which
       indicate that the command should be located along a
       relative or absolute path. */
    found_path_separator = found_volume_separator = FALSE;

    p = path;

    while((c = (*p++)) != '\0')
    {
        if(c == '/')
            found_path_separator = TRUE;

        if(c == ':')
            found_volume_separator = TRUE;
    }

    /* No relative or absolute path given? Try the resident command list. */
    if(!found_path_separator && !found_volume_separator)
    {
        /* Try to find the command on the resident list */
        pi->resident_command = find_resident_command(path);
        if(pi->resident_command != NULL)
        {
            pi->program_name = strdup(path);
            if(pi->program_name == NULL)
            {
                __set_errno(ENOMEM);
                goto out;
            }
        }
    }

    /* No resident command found? Try the file system. */
    if(pi->resident_command == NULL)
    {
        struct MsgPort * file_system;
        struct DevProc * dvp = NULL;
        BOOL done = FALSE;
        LONG io_err;

        /* Now for the simple stuff. Find a command or command script file
           under the path name given. Handle multi-volume assignments, such as
           referring to "C:", gracefully */
        file_system = GetFileSysTask();

        do
        {
            /* Give the user a chance to bail out. */
            if(__check_abort_enabled && (SetSignal(0,0) & __break_signal_mask) != 0)
            {
                error = EAGAIN;
                break;
            }

            if(found_volume_separator)
            {
                dvp = GetDeviceProc((STRPTR)path,dvp);
                if(dvp != NULL)
                {
                    SetFileSysTask(dvp->dvp_Port);

                    old_dir = CurrentDir(dvp->dvp_Lock);
                }
            }

            /* First try: let's assume that that the file is
               executable */
            pi->segment_list = LoadSeg((STRPTR)path);
            if(pi->segment_list != ZERO)
            {
                /* Also remember the name of the command */
                pi->program_name = strdup(path);
                if(pi->program_name != NULL)
                    done = TRUE;
                else
                    error = ENOMEM;
            }

            io_err = IoErr();

            /* If that didn't work and we might be dealing with a script
               file, have a closer look at it. */
            if(error == 0 && !done && (io_err == ERROR_OBJECT_NOT_FOUND || io_err == ERROR_OBJECT_WRONG_TYPE || io_err == ERROR_BAD_HUNK))
            {
                /* Could that be an ARexx or shell script? */
                if(get_first_script_line(path,&script_line) == 0)
                {
                    if(strncmp(script_line,"/*",2) == SAME)
                    {
                        /* That's an ARexx script (or a 'C'/C++/Java program, but we support only ARexx here) */
                        pi->interpreter_name = strdup("RX");
                        if(pi->interpreter_name != NULL)
                            done = TRUE;
                        else
                            error = ENOMEM;
                    }
                    else if (strncmp(script_line,"#!",2) == SAME)
                    {
                        char * name;
                        char * args;

                        /* That's probably a shell script */
                        name = &script_line[2];
                        while((*name) != '\0' && isspace(*name))
                            name++;

                        /* Do we have a command name? */
                        if((*name) != '\0')
                        {
                            /* Find out if there are any script parameters */
                            args = name;
                            while((*args) != '\0' && !isspace(*args))
                                args++;

                            if((*args) != '\0')
                            {
                                (*args++) = '\0';

                                while((*args) != '\0' && isspace(*args))
                                    args++;
                            }

                            /* Remember the parameters, if any */
                            if((*args) != '\0')
                            {
                                pi->interpreter_args = strdup(args);
                                if(pi->interpreter_args == NULL)
                                    error = ENOMEM;
                            }

                            /* And remember the interpreter name. */
                            if(error == 0)
                            {
                                pi->interpreter_name = strdup(name);
                                if(pi->interpreter_name != NULL)
                                    done = TRUE;
                                else
                                    error = ENOMEM;
                            }
                        }
                    }

                    free(script_line);
                    script_line = NULL;
                }
            }

            /* If that still didn't work, check if the file has
               the "script" protection bit set. */
            if(error == 0 && !done)
            {
                BPTR file_lock;

                file_lock = Lock((STRPTR)path,SHARED_LOCK);
                if(file_lock != ZERO)
                {
                    D_S(struct FileInfoBlock,fib);

                    if(Examine(file_lock,fib))
                    {
                        if(fib->fib_Protection & FIBF_SCRIPT)
                        {
                            /* That's an AmigaDOS script */
                            pi->interpreter_name = strdup("Execute");
                            if(pi->interpreter_name != NULL)
                                done = TRUE;
                            else
                                error = ENOMEM;
                        }
                    }
                    else
                    {
                        error = __translate_io_error_to_errno(IoErr());
                    }

                    UnLock(file_lock);
                }
            }

            /* If we found what we came for, also try to get a lock on
               the command/script home directory, so that "PROGDIR:"
               will work. */
            if(done)
            {
                BPTR file_lock;

                /* Remember where that file came from so that
                   "PROGDIR:" will work. */
                file_lock = Lock((STRPTR)path,SHARED_LOCK);
                if(file_lock != ZERO)
                {
                    pi->home_dir = ParentDir(file_lock);
                    if(pi->home_dir == ZERO)
                        error = __translate_io_error_to_errno(IoErr());

                    UnLock(file_lock);
                }
                else
                {
                    error = __translate_io_error_to_errno(IoErr());
                }
            }

            if(dvp != NULL)
                CurrentDir(old_dir);
        }
        while(!done && error == 0 && dvp != NULL && (dvp->dvp_Flags & DVPF_ASSIGN));

        SetFileSysTask(file_system);

        if(dvp != NULL)
            FreeDeviceProc(dvp);

        if(error == 0 && !done)
            error = ENOENT;

        if(error != 0)
        {
            __set_errno(error);
            goto out;
        }
    }

    (*result_ptr) = pi;
    pi = NULL;

    result = 0;

 out:

    if(script_line != NULL)
        free(script_line);

    if(pi != NULL)
        free_program_info(pi);

    __set_process_window(old_window_ptr);

    return(result);
}

/****************************************************************************/

/* Scan the string, looking for characters which need to be
   escape with a '*' if that string is to be quoted and the
   contents should remain in the same form */
static size_t
count_extra_escape_chars(const char * string,size_t len)
{
    size_t count = 0;
    size_t i;
    char c;

    for(i = 0 ; i < len ; i++)
    {
        c = (*string++);
        if(c == '\"' || c == '*' || c == '\n')
            count++;
    }

    return(count);
}

/****************************************************************************/

/* Scan a string for characters which may require that the string
   should be quoted */
STATIC BOOL
string_needs_quoting(const char * string,size_t len)
{
    BOOL result = FALSE;
    size_t i;
    char c;

    for(i = 0 ; i < len ; i++)
    {
        c = (*string++);
        if(c == ' ' || ((unsigned char)c) == 0xA0 || c == '\t' || c == '\n' || c == '\"')
        {
            result = TRUE;
            break;
        }
    }

    return(result);
}

/****************************************************************************/

/* Figure out how many characters would go into a string composed of
   individual arguments. This takes into account the lengths of
   the individual argument strings, the separator characters, the
   quote characters and any escape characters. */
static size_t
get_arg_string_length(char *const argv[])
{
    size_t result = 0;
    size_t i,len;
    char * s;

    /* The first argv[] element is skipped; it does not contain part of
       the command line but holds the name of the program to be run. */
    for(i = 1 ; argv[i] != NULL ; i++)
    {
        s = (char *)argv[i];

        len = strlen(s);
        if(len > 0)
        {
            if((*s) != '\"')
            {
                if(string_needs_quoting(s,len))
                    len += 1 + count_extra_escape_chars(s,len) + 1;
            }

            if(result == 0)
                result = len;
            else
                result = result + 1 + len;
        }
    }

    return(result);
}

/****************************************************************************/

/* Put together an argument string from a list of individual
   components, quoting characters, escape characters and
   separator characters. You're supposed to have enough memory
   reserved for the whole string to fit */
static void
build_arg_string(char *const argv[],char * arg_string)
{
    BOOL first_char = TRUE;
    size_t i,j,len;
    char * s;

    /* The first argv[] element is skipped; it does not contain part of
       the command line but holds the name of the program to be run. */
    for(i = 1 ; argv[i] != NULL ; i++)
    {
        s = (char *)argv[i];

        len = strlen(s);
        if(len > 0)
        {
            if(first_char)
                first_char = FALSE;
            else
                (*arg_string++) = ' ';

            if((*s) != '\"' && string_needs_quoting(s,len))
            {
                (*arg_string++) = '\"';

                for(j = 0 ; j < len ; j++)
                {
                    if(s[j] == '\"' || s[j] == '*')
                    {
                        (*arg_string++) = '*';
                        (*arg_string++) = s[j];
                    }
                    else if (s[j] == '\n')
                    {
                        (*arg_string++) = '*';
                        (*arg_string++) = 'N';
                    }
                    else
                    {
                        (*arg_string++) = s[j];
                    }
                }

                (*arg_string++) = '\"';
            }
            else
            {
                memcpy(arg_string,s,len);
                arg_string += len;
            }
        }
    }
}

/****************************************************************************/

/* NOTE: This is not an execve() function which works like you might expect it
         to do on a Unix-like system. Specifically, the command which will
         be executed does not replace the currently running program. That
         command will be executed first, and when it has returned the currently
         running program will exit. Also, if you need to redirect the standard
         input/output/error streams you will have to do this using the
         equivalent AmigaDOS functions (Open(), SelectInput(), SelectOutput()
         and SelectErrorOutput()), restoring these streams before your program
         exits. What this execve() function does is very similar to how the
         built-in AmigaDOS shell works, but it is much more limited in what
         it can do. */

int
execve(const char *path, char *const argv[], char *const envp[])
{
    char old_program_name[256];
    int result = -1;
    struct program_info * pi;
    char * arg_string = NULL;
    size_t arg_string_len = 0;
    size_t parameter_string_len;
    BOOL success = FALSE;
    BOOL clean_up_env = FALSE;
    BPTR old_dir;

    /* We begin by trying to find the command to execute */
    if(find_command((char *)path,&pi) != 0)
        goto out;

    /* We'll need to know how much memory to reserve for the
       parameters anyway */
    parameter_string_len = get_arg_string_length(argv);

    /* Do we have to use a script interpreter? */
    if(pi->interpreter_name != NULL)
    {
        struct program_info * pi_interpreter;
        size_t path_len = strlen(path);

        /* Now try to find the command corresponding to the
           interpreter given */
        if(find_command(pi->interpreter_name,&pi_interpreter) != 0)
            goto out;

        /* We only try to resolve the name once. If this is still
           not a command we can launch, we chicken out */
        if(pi_interpreter->interpreter_name != NULL)
        {
            free_program_info(pi_interpreter);
            goto out;
        }

        /* Just remember the arguments that need to be passed
           to the interpreter */
        pi_interpreter->interpreter_args = pi->interpreter_args;
        pi->interpreter_args = NULL;

        free_program_info(pi);
        pi = pi_interpreter;

        /* Reserve as much memory as is required for the
           interpreter's parameters and the command's
           arguments */
        if(pi->interpreter_args != NULL)
        {
            size_t interpreter_args_len = strlen(pi->interpreter_args);

            arg_string = malloc(interpreter_args_len + 1 + path_len + 1 + parameter_string_len + 1 + 1);
            if(arg_string == NULL)
            {
                __set_errno(ENOMEM);
                goto out;
            }

            memcpy(arg_string,pi->interpreter_args,interpreter_args_len);
            arg_string_len = interpreter_args_len;
            arg_string[arg_string_len++] = ' ';

            memcpy(&arg_string[arg_string_len],path,path_len);
            arg_string_len += path_len;
            arg_string[arg_string_len++] = ' ';
        }
        else
        {
            arg_string = malloc(path_len + 1 + parameter_string_len + 1 + 1);
            if(arg_string == NULL)
            {
                __set_errno(ENOMEM);
                goto out;
            }

            memcpy(arg_string,path,path_len);
            arg_string_len = path_len;
            arg_string[arg_string_len++] = ' ';
        }
    }
    else
    {
        arg_string = malloc(parameter_string_len + 1 + 1);
        if(arg_string == NULL)
        {
            __set_errno(ENOMEM);
            goto out;
        }
    }

    /* Any command parameters to take care of? */
    if(parameter_string_len > 0)
    {
        build_arg_string(argv,&arg_string[arg_string_len]);
        arg_string_len += parameter_string_len;
    }

    /* Add the terminating new line character and a NUL,
       to be nice... */
    arg_string[arg_string_len++]    = '\n';
    arg_string[arg_string_len]        = '\0';

    /* Almost ready: prepare the environment data so that it can
       be used by the command to be executed. */
    if(envp != NULL)
    {
        if(__execve_environ_init(envp) != 0)
            goto out;

        clean_up_env = TRUE;
    }

    /* Change the shell's program name */
    GetProgramName(old_program_name,sizeof(old_program_name));
    SetProgramName(pi->program_name);

    /* Change the command's home directory, so that "PROGDIR:"
       can be used */
    old_dir = SetProgramDir(pi->home_dir);

    /* Reset the break signal before the program starts */
    SetSignal(0,SIGBREAKF_CTRL_C);

    /* Now try to run the program with the accumulated parameters */
    result = RunCommand((pi->resident_command != NULL) ? pi->resident_command->seg_Seg : pi->segment_list,Cli()->cli_DefaultStack * sizeof(LONG),arg_string,arg_string_len);

    /* Restore the home directory */
    SetProgramDir(old_dir);

    /* Restore the program name */
    SetProgramName(old_program_name);

    /* Did we launch the program? */
    if(result == -1)
    {
        __set_errno(__translate_io_error_to_errno(IoErr()));
        goto out;
    }

    /* Looks good, doesn't it? */
    success = TRUE;

 out:

    /* Clean up... */
    if(clean_up_env)
        __execve_environ_exit(envp);

    if(pi != NULL)
        free_program_info(pi);

    if(arg_string != NULL)
        free(arg_string);

    /* If things went well, we can actually quit now. */
    if(success)
        __execve_exit(result);

    /* Just in case somebody wants to quit... */
    __check_abort();

    return(result);
}