/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "signtool.h"
#include <prmem.h>
#include <prio.h>
#include <prenv.h>

static int javascript_fn(char *relpath, char *basedir, char *reldir,
                         char *filename, void *arg);
static int extract_js(char *filename);
static int copyinto(char *from, char *to);
static PRStatus ensureExists(char *base, char *path);
static int make_dirs(char *path, PRInt32 file_perms);

static char *jartree = NULL;
static int idOrdinal;
static PRBool dumpParse = PR_FALSE;

static char *event_handlers[] = {
    "onAbort",
    "onBlur",
    "onChange",
    "onClick",
    "onDblClick",
    "onDragDrop",
    "onError",
    "onFocus",
    "onKeyDown",
    "onKeyPress",
    "onKeyUp",
    "onLoad",
    "onMouseDown",
    "onMouseMove",
    "onMouseOut",
    "onMouseOver",
    "onMouseUp",
    "onMove",
    "onReset",
    "onResize",
    "onSelect",
    "onSubmit",
    "onUnload"
};

static int num_handlers = 23;

/*
 *  I n l i n e J a v a S c r i p t
 *
 *  Javascript signing. Instead of passing an archive to signtool,
 *  a directory containing html files is given. Archives are created
 *  from the archive= and src= tag attributes inside the html,
 *  as appropriate. Then the archives are signed.
 *
 */
int
InlineJavaScript(char *dir, PRBool recurse)
{
    jartree = dir;
    if (verbosity >= 0) {
        PR_fprintf(outputFD, "\nGenerating inline signatures from HTML files in: %s\n",
                   dir);
    }
    if (PR_GetEnvSecure("SIGNTOOL_DUMP_PARSE")) {
        dumpParse = PR_TRUE;
    }

    return foreach (dir, "", javascript_fn, recurse, PR_FALSE /*include dirs*/,
                    (void *)NULL);
}

/************************************************************************
 *
 * j a v a s c r i p t _ f n
 */
static int
javascript_fn(char *relpath, char *basedir, char *reldir, char *filename, void *arg)
{
    char fullname[FNSIZE];

    /* only process inline scripts from .htm, .html, and .shtml*/

    if (!(PL_strcaserstr(filename, ".htm") == filename + strlen(filename) - 4) &&
        !(PL_strcaserstr(filename, ".html") == filename + strlen(filename) - 5) &&
        !(PL_strcaserstr(filename, ".shtml") == filename + strlen(filename) - 6)) {
        return 0;
    }

    /* don't process scripts that signtool has already
     extracted (those that are inside .arc directories) */

    if (PL_strcaserstr(filename, ".arc") == filename + strlen(filename) - 4)
        return 0;

    if (verbosity >= 0) {
        PR_fprintf(outputFD, "Processing HTML file: %s\n", relpath);
    }

    /* reset firstArchive at top of each HTML file */

    /* skip directories that contain extracted scripts */

    if (PL_strcaserstr(reldir, ".arc") == reldir + strlen(reldir) - 4)
        return 0;

    sprintf(fullname, "%s/%s", basedir, relpath);
    return extract_js(fullname);
}

/*===========================================================================
 =
 = D A T A   S T R U C T U R E S
 =
*/
typedef enum {
    TEXT_HTML_STATE = 0,
    SCRIPT_HTML_STATE
}

HTML_STATE;

typedef enum {
    /* we start in the start state */
    START_STATE,

    /* We are looking for or reading in an attribute */
    GET_ATT_STATE,

    /* We're burning ws before finding an attribute */
    PRE_ATT_WS_STATE,

    /* We're burning ws after an attribute.  Looking for an '='. */
    POST_ATT_WS_STATE,

    /* We're burning ws after an '=', waiting for a value */
    PRE_VAL_WS_STATE,

    /* We're reading in a value */
    GET_VALUE_STATE,

    /* We're reading in a value that's inside quotes */
    GET_QUOTED_VAL_STATE,

    /* We've encountered the closing '>' */
    DONE_STATE,

    /* Error state */
    ERR_STATE
}

TAG_STATE;

typedef struct AVPair_Str {
    char *attribute;
    char *value;
    unsigned int valueLine; /* the line that the value ends on */
    struct AVPair_Str *next;
} AVPair;

typedef enum {
    APPLET_TAG,
    SCRIPT_TAG,
    LINK_TAG,
    STYLE_TAG,
    COMMENT_TAG,
    OTHER_TAG
}

TAG_TYPE;

typedef struct {
    TAG_TYPE type;
    AVPair *attList;
    AVPair *attListTail;
    char *text;
} TagItem;

typedef enum {
    TAG_ITEM,
    TEXT_ITEM
}

ITEM_TYPE;

typedef struct HTMLItem_Str {
    unsigned int startLine;
    unsigned int endLine;
    ITEM_TYPE type;
    union {
        TagItem *tag;
        char *text;
    } item;
    struct HTMLItem_Str *next;
} HTMLItem;

typedef struct {
    PRFileDesc *fd;
    PRInt32 curIndex;
    PRBool IsEOF;
#define FILE_BUFFER_BUFSIZE 512
    char buf[FILE_BUFFER_BUFSIZE];
    PRInt32 startOffset;
    PRInt32 maxIndex;
    unsigned int lineNum;
} FileBuffer;

/*===========================================================================
 =
 = F U N C T I O N S
 =
*/
static HTMLItem *CreateTextItem(char *text, unsigned int startline,
                                unsigned int endline);
static HTMLItem *CreateTagItem(TagItem *ti, unsigned int startline,
                               unsigned int endline);
static TagItem *ProcessTag(FileBuffer *fb, char **errStr);
static void DestroyHTMLItem(HTMLItem *item);
static void DestroyTagItem(TagItem *ti);
static TAG_TYPE GetTagType(char *att);
static FileBuffer *FB_Create(PRFileDesc *fd);
static int FB_GetChar(FileBuffer *fb);
static PRInt32 FB_GetPointer(FileBuffer *fb);
static PRInt32 FB_GetRange(FileBuffer *fb, PRInt32 start, PRInt32 end,
                           char **buf);
static unsigned int FB_GetLineNum(FileBuffer *fb);
static void FB_Destroy(FileBuffer *fb);
static void PrintTagItem(PRFileDesc *fd, TagItem *ti);
static void PrintHTMLStream(PRFileDesc *fd, HTMLItem *head);

/************************************************************************
 *
 * C r e a t e T e x t I t e m
 */
static HTMLItem *
CreateTextItem(char *text, unsigned int startline, unsigned int endline)
{
    HTMLItem *item;

    item = PR_Malloc(sizeof(HTMLItem));
    if (!item) {
        return NULL;
    }

    item->type = TEXT_ITEM;
    item->item.text = text;
    item->next = NULL;
    item->startLine = startline;
    item->endLine = endline;

    return item;
}

/************************************************************************
 *
 * C r e a t e T a g I t e m
 */
static HTMLItem *
CreateTagItem(TagItem *ti, unsigned int startline, unsigned int endline)
{
    HTMLItem *item;

    item = PR_Malloc(sizeof(HTMLItem));
    if (!item) {
        return NULL;
    }

    item->type = TAG_ITEM;
    item->item.tag = ti;
    item->next = NULL;
    item->startLine = startline;
    item->endLine = endline;

    return item;
}

static PRBool
isAttChar(int c)
{
    return (isalnum(c) || c == '/' || c == '-');
}

/************************************************************************
 *
 * P r o c e s s T a g
 */
static TagItem *
ProcessTag(FileBuffer *fb, char **errStr)
{
    TAG_STATE state;
    PRInt32 startText, startID, curPos;
    PRBool firstAtt;
    int curchar;
    TagItem *ti = NULL;
    AVPair *curPair = NULL;
    char quotechar = '\0';
    unsigned int linenum;
    unsigned int startline;

    state = START_STATE;

    startID = FB_GetPointer(fb);
    startText = startID;
    firstAtt = PR_TRUE;

    ti = (TagItem *)PR_Malloc(sizeof(TagItem));
    if (!ti)
        out_of_memory();
    ti->type = OTHER_TAG;
    ti->attList = NULL;
    ti->attListTail = NULL;
    ti->text = NULL;

    startline = FB_GetLineNum(fb);

    while (state != DONE_STATE && state != ERR_STATE) {
        linenum = FB_GetLineNum(fb);
        curchar = FB_GetChar(fb);
        if (curchar == EOF) {
            *errStr = PR_smprintf(
                "line %d: Unexpected end-of-file while parsing tag starting at line %d.\n",
                linenum, startline);
            state = ERR_STATE;
            continue;
        }

        switch (state) {
            case START_STATE:
                if (curchar == '!') {
                    /*
                     * SGML tag or comment
                     * Here's the general rule for SGML tags.  Everything from
                     * <! to > is the tag.  Inside the tag, comments are
                     * delimited with --.  So we are looking for the first '>'
                     * that is not commented out, that is, not inside a pair
                     * of --: <!DOCTYPE --this is a comment >(psyche!)   -->
                     */

                    PRBool inComment = PR_FALSE;
                    short hyphenCount = 0; /* number of consecutive hyphens */

                    while (1) {
                        linenum = FB_GetLineNum(fb);
                        curchar = FB_GetChar(fb);
                        if (curchar == EOF) {
                            /* Uh oh, EOF inside comment */
                            *errStr = PR_smprintf(
                                "line %d: Unexpected end-of-file inside comment starting at line %d.\n",
                                linenum, startline);
                            state = ERR_STATE;
                            break;
                        }
                        if (curchar == '-') {
                            if (hyphenCount == 1) {
                                /* This is a comment delimiter */
                                inComment = !inComment;
                                hyphenCount = 0;
                            } else {
                                /* beginning of a comment delimiter? */
                                hyphenCount = 1;
                            }
                        } else if (curchar == '>') {
                            if (!inComment) {
                                /* This is the end of the tag */
                                state = DONE_STATE;
                                break;
                            } else {
                                /* The > is inside a comment, so it's not
                                                         * really the end of the tag */
                                hyphenCount = 0;
                            }
                        } else {
                            hyphenCount = 0;
                        }
                    }
                    ti->type = COMMENT_TAG;
                    break;
                }
            /* fall through */
            case GET_ATT_STATE:
                if (isspace(curchar) || curchar == '=' || curchar == '>') {
                    /* end of the current attribute */
                    curPos = FB_GetPointer(fb) - 2;
                    if (curPos >= startID) {
                        /* We have an attribute */
                        curPair = (AVPair *)PR_Malloc(sizeof(AVPair));
                        if (!curPair)
                            out_of_memory();
                        curPair->value = NULL;
                        curPair->next = NULL;
                        FB_GetRange(fb, startID, curPos,
                                    &curPair->attribute);

                        /* Stick this attribute on the list */
                        if (ti->attListTail) {
                            ti->attListTail->next = curPair;
                            ti->attListTail = curPair;
                        } else {
                            ti->attList = ti->attListTail =
                                curPair;
                        }

                        /* If this is the first attribute, find the type of tag
                         * based on it. Also, start saving the text of the tag. */
                        if (firstAtt) {
                            ti->type = GetTagType(curPair->attribute);
                            startText = FB_GetPointer(fb) -
                                        1;
                            firstAtt = PR_FALSE;
                        }
                    } else {
                        if (curchar == '=') {
                            /* If we don't have any attribute but we do have an
                         * equal sign, that's an error */
                            *errStr = PR_smprintf("line %d: Malformed tag starting at line %d.\n",
                                                  linenum, startline);
                            state = ERR_STATE;
                            break;
                        }
                    }

                    /* Compute next state */
                    if (curchar == '=') {
                        startID = FB_GetPointer(fb);
                        state = PRE_VAL_WS_STATE;
                    } else if (curchar == '>') {
                        state = DONE_STATE;
                    } else if (curPair) {
                        state = POST_ATT_WS_STATE;
                    } else {
                        state = PRE_ATT_WS_STATE;
                    }
                } else if (isAttChar(curchar)) {
                    /* Just another char in the attribute. Do nothing */
                    state = GET_ATT_STATE;
                } else {
                    /* bogus char */
                    *errStr = PR_smprintf("line %d: Bogus chararacter '%c' in tag.\n",
                                          linenum, curchar);
                    state = ERR_STATE;
                    break;
                }
                break;
            case PRE_ATT_WS_STATE:
                if (curchar == '>') {
                    state = DONE_STATE;
                } else if (isspace(curchar)) {
                    /* more whitespace, do nothing */
                } else if (isAttChar(curchar)) {
                    /* starting another attribute */
                    startID = FB_GetPointer(fb) - 1;
                    state = GET_ATT_STATE;
                } else {
                    /* bogus char */
                    *errStr = PR_smprintf("line %d: Bogus character '%c' in tag.\n",
                                          linenum, curchar);
                    state = ERR_STATE;
                    break;
                }
                break;
            case POST_ATT_WS_STATE:
                if (curchar == '>') {
                    state = DONE_STATE;
                } else if (isspace(curchar)) {
                    /* more whitespace, do nothing */
                } else if (isAttChar(curchar)) {
                    /* starting another attribute */
                    startID = FB_GetPointer(fb) - 1;
                    state = GET_ATT_STATE;
                } else if (curchar == '=') {
                    /* there was whitespace between the attribute and its equal
                     * sign, which means there's a value coming up */
                    state = PRE_VAL_WS_STATE;
                } else {
                    /* bogus char */
                    *errStr = PR_smprintf("line %d: Bogus character '%c' in tag.\n",
                                          linenum, curchar);
                    state = ERR_STATE;
                    break;
                }
                break;
            case PRE_VAL_WS_STATE:
                if (curchar == '>') {
                    /* premature end-of-tag (sounds like a personal problem). */
                    *errStr = PR_smprintf(
                        "line %d: End of tag while waiting for value.\n",
                        linenum);
                    state = ERR_STATE;
                    break;
                } else if (isspace(curchar)) {
                    /* more whitespace, do nothing */
                    break;
                } else {
                    /* this must be some sort of value. Fall through
                                 * to GET_VALUE_STATE */
                    startID = FB_GetPointer(fb) - 1;
                    state = GET_VALUE_STATE;
                }
            /* Fall through if we didn't break on '>' or whitespace */
            case GET_VALUE_STATE:
                if (isspace(curchar) || curchar == '>') {
                    /* end of value */
                    curPos = FB_GetPointer(fb) - 2;
                    if (curPos >= startID) {
                        /* Grab the value */
                        FB_GetRange(fb, startID, curPos,
                                    &curPair->value);
                        curPair->valueLine = linenum;
                    } else {
                        /* empty value, leave as NULL */
                    }
                    if (isspace(curchar)) {
                        state = PRE_ATT_WS_STATE;
                    } else {
                        state = DONE_STATE;
                    }
                } else if (curchar == '\"' || curchar == '\'') {
                    /* quoted value.  Start recording the value inside the quote*/
                    startID = FB_GetPointer(fb);
                    state = GET_QUOTED_VAL_STATE;
                    PORT_Assert(quotechar == '\0');
                    quotechar = curchar; /* look for matching quote type */
                } else {
                    /* just more value */
                }
                break;
            case GET_QUOTED_VAL_STATE:
                PORT_Assert(quotechar != '\0');
                if (curchar == quotechar) {
                    /* end of quoted value */
                    curPos = FB_GetPointer(fb) - 2;
                    if (curPos >= startID) {
                        /* Grab the value */
                        FB_GetRange(fb, startID, curPos,
                                    &curPair->value);
                        curPair->valueLine = linenum;
                    } else {
                        /* empty value, leave it as NULL */
                    }
                    state = GET_ATT_STATE;
                    quotechar = '\0';
                    startID = FB_GetPointer(fb);
                } else {
                    /* more quoted value, continue */
                }
                break;
            case DONE_STATE:
            case ERR_STATE:
            default:; /* should never get here */
        }
    }

    if (state == DONE_STATE) {
        /* Get the text of the tag */
        curPos = FB_GetPointer(fb) - 1;
        FB_GetRange(fb, startText, curPos, &ti->text);

        /* Return the tag */
        return ti;
    }

    /* Uh oh, an error.  Kill the tag item*/
    DestroyTagItem(ti);
    return NULL;
}

/************************************************************************
 *
 * D e s t r o y H T M L I t e m
 */
static void
DestroyHTMLItem(HTMLItem *item)
{
    if (item->type == TAG_ITEM) {
        DestroyTagItem(item->item.tag);
    } else {
        if (item->item.text) {
            PR_Free(item->item.text);
        }
    }
}

/************************************************************************
 *
 * D e s t r o y T a g I t e m
 */
static void
DestroyTagItem(TagItem *ti)
{
    AVPair *temp;

    if (ti->text) {
        PR_Free(ti->text);
        ti->text = NULL;
    }

    while (ti->attList) {
        temp = ti->attList;
        ti->attList = ti->attList->next;

        if (temp->attribute) {
            PR_Free(temp->attribute);
            temp->attribute = NULL;
        }
        if (temp->value) {
            PR_Free(temp->value);
            temp->value = NULL;
        }
        PR_Free(temp);
    }

    PR_Free(ti);
}

/************************************************************************
 *
 * G e t T a g T y p e
 */
static TAG_TYPE
GetTagType(char *att)
{
    if (!PORT_Strcasecmp(att, "APPLET")) {
        return APPLET_TAG;
    }
    if (!PORT_Strcasecmp(att, "SCRIPT")) {
        return SCRIPT_TAG;
    }
    if (!PORT_Strcasecmp(att, "LINK")) {
        return LINK_TAG;
    }
    if (!PORT_Strcasecmp(att, "STYLE")) {
        return STYLE_TAG;
    }
    return OTHER_TAG;
}

/************************************************************************
 *
 * F B _ C r e a t e
 */
static FileBuffer *
FB_Create(PRFileDesc *fd)
{
    FileBuffer *fb;
    PRInt32 amountRead;
    PRInt32 storedOffset;

    fb = (FileBuffer *)PR_Malloc(sizeof(FileBuffer));
    fb->fd = fd;
    storedOffset = PR_Seek(fd, 0, PR_SEEK_CUR);
    PR_Seek(fd, 0, PR_SEEK_SET);
    fb->startOffset = 0;
    amountRead = PR_Read(fd, fb->buf, FILE_BUFFER_BUFSIZE);
    if (amountRead == -1)
        goto loser;
    fb->maxIndex = amountRead - 1;
    fb->curIndex = 0;
    fb->IsEOF = (fb->curIndex > fb->maxIndex) ? PR_TRUE : PR_FALSE;
    fb->lineNum = 1;

    PR_Seek(fd, storedOffset, PR_SEEK_SET);
    return fb;
loser:
    PR_Seek(fd, storedOffset, PR_SEEK_SET);
    PR_Free(fb);
    return NULL;
}

/************************************************************************
 *
 * F B _ G e t C h a r
 */
static int
FB_GetChar(FileBuffer *fb)
{
    PRInt32 storedOffset;
    PRInt32 amountRead;
    int retval = -1;

    if (fb->IsEOF) {
        return EOF;
    }

    storedOffset = PR_Seek(fb->fd, 0, PR_SEEK_CUR);

    retval = (unsigned char)fb->buf[fb->curIndex++];
    if (retval == '\n')
        fb->lineNum++;

    if (fb->curIndex > fb->maxIndex) {
        /* We're at the end of the buffer. Try to get some new data from the
                 * file */
        fb->startOffset += fb->maxIndex + 1;
        PR_Seek(fb->fd, fb->startOffset, PR_SEEK_SET);
        amountRead = PR_Read(fb->fd, fb->buf, FILE_BUFFER_BUFSIZE);
        if (amountRead == -1)
            goto loser;
        fb->maxIndex = amountRead - 1;
        fb->curIndex = 0;
    }

    fb->IsEOF = (fb->curIndex > fb->maxIndex) ? PR_TRUE : PR_FALSE;

loser:
    PR_Seek(fb->fd, storedOffset, PR_SEEK_SET);
    return retval;
}

/************************************************************************
 *
 * F B _ G e t L i n e N u m
 *
 */
static unsigned int
FB_GetLineNum(FileBuffer *fb)
{
    return fb->lineNum;
}

/************************************************************************
 *
 * F B _ G e t P o i n t e r
 *
 */
static PRInt32
FB_GetPointer(FileBuffer *fb)
{
    return fb->startOffset + fb->curIndex;
}

/************************************************************************
 *
 * F B _ G e t R a n g e
 *
 */
static PRInt32
FB_GetRange(FileBuffer *fb, PRInt32 start, PRInt32 end, char **buf)
{
    PRInt32 amountRead;
    PRInt32 storedOffset;

    *buf = PR_Malloc(end - start + 2);
    if (*buf == NULL) {
        return 0;
    }

    storedOffset = PR_Seek(fb->fd, 0, PR_SEEK_CUR);
    PR_Seek(fb->fd, start, PR_SEEK_SET);
    amountRead = PR_Read(fb->fd, *buf, end - start + 1);
    PR_Seek(fb->fd, storedOffset, PR_SEEK_SET);
    if (amountRead == -1) {
        PR_Free(*buf);
        *buf = NULL;
        return 0;
    }

    (*buf)[end - start + 1] = '\0';
    return amountRead;
}

/************************************************************************
 *
 * F B _ D e s t r o y
 *
 */
static void
FB_Destroy(FileBuffer *fb)
{
    if (fb) {
        PR_Free(fb);
    }
}

/************************************************************************
 *
 * P r i n t T a g I t e m
 *
 */
static void
PrintTagItem(PRFileDesc *fd, TagItem *ti)
{
    AVPair *pair;

    PR_fprintf(fd, "TAG:\n----\nType: ");
    switch (ti->type) {
        case APPLET_TAG:
            PR_fprintf(fd, "applet\n");
            break;
        case SCRIPT_TAG:
            PR_fprintf(fd, "script\n");
            break;
        case LINK_TAG:
            PR_fprintf(fd, "link\n");
            break;
        case STYLE_TAG:
            PR_fprintf(fd, "style\n");
            break;
        case COMMENT_TAG:
            PR_fprintf(fd, "comment\n");
            break;
        case OTHER_TAG:
        default:
            PR_fprintf(fd, "other\n");
            break;
    }

    PR_fprintf(fd, "Attributes:\n");
    for (pair = ti->attList; pair; pair = pair->next) {
        PR_fprintf(fd, "\t%s=%s\n", pair->attribute,
                   pair->value ? pair->value : "");
    }
    PR_fprintf(fd, "Text:%s\n", ti->text ? ti->text : "");

    PR_fprintf(fd, "---End of tag---\n");
}

/************************************************************************
 *
 * P r i n t H T M L S t r e a m
 *
 */
static void
PrintHTMLStream(PRFileDesc *fd, HTMLItem *head)
{
    while (head) {
        if (head->type == TAG_ITEM) {
            PrintTagItem(fd, head->item.tag);
        } else {
            PR_fprintf(fd, "\nTEXT:\n-----\n%s\n-----\n\n", head->item.text);
        }
        head = head->next;
    }
}

/************************************************************************
 *
 * S a v e I n l i n e S c r i p t
 *
 */
static int
SaveInlineScript(char *text, char *id, char *basedir, char *archiveDir)
{
    char *filename = NULL;
    PRFileDesc *fd = NULL;
    int retval = -1;
    PRInt32 writeLen;
    char *ilDir = NULL;

    if (!text || !id || !archiveDir) {
        return -1;
    }

    if (dumpParse) {
        PR_fprintf(outputFD, "SaveInlineScript: text=%s, id=%s, \n"
                             "basedir=%s, archiveDir=%s\n",
                   text, id, basedir, archiveDir);
    }

    /* Make sure the archive directory is around */
    if (ensureExists(basedir, archiveDir) != PR_SUCCESS) {
        PR_fprintf(errorFD,
                   "ERROR: Unable to create archive directory %s.\n", archiveDir);
        errorCount++;
        return -1;
    }

    /* Make sure the inline script directory is around */
    ilDir = PR_smprintf("%s/inlineScripts", archiveDir);
    scriptdir = "inlineScripts";
    if (ensureExists(basedir, ilDir) != PR_SUCCESS) {
        PR_fprintf(errorFD,
                   "ERROR: Unable to create directory %s.\n", ilDir);
        errorCount++;
        return -1;
    }

    filename = PR_smprintf("%s/%s/%s", basedir, ilDir, id);

    /* If the file already exists, give a warning, then blow it away */
    if (PR_Access(filename, PR_ACCESS_EXISTS) == PR_SUCCESS) {
        PR_fprintf(errorFD,
                   "warning: file \"%s\" already exists--will overwrite.\n",
                   filename);
        warningCount++;
        if (rm_dash_r(filename)) {
            PR_fprintf(errorFD, "ERROR: Unable to delete %s.\n", filename);
            errorCount++;
            goto finish;
        }
    }

    /* Write text into file with name id */
    fd = PR_Open(filename, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0777);
    if (!fd) {
        PR_fprintf(errorFD, "ERROR: Unable to create file \"%s\".\n",
                   filename);
        errorCount++;
        goto finish;
    }
    writeLen = strlen(text);
    if (PR_Write(fd, text, writeLen) != writeLen) {
        PR_fprintf(errorFD, "ERROR: Unable to write to file \"%s\".\n",
                   filename);
        errorCount++;
        goto finish;
    }

    retval = 0;
finish:
    if (filename) {
        PR_smprintf_free(filename);
    }
    if (ilDir) {
        PR_smprintf_free(ilDir);
    }
    if (fd) {
        PR_Close(fd);
    }
    return retval;
}

/************************************************************************
 *
 * S a v e U n n a m a b l e S c r i p t
 *
 */
static int
SaveUnnamableScript(char *text, char *basedir, char *archiveDir,
                    char *HTMLfilename)
{
    char *id = NULL;
    char *ext = NULL;
    char *start = NULL;
    int retval = -1;

    if (!text || !archiveDir || !HTMLfilename) {
        return -1;
    }

    if (dumpParse) {
        PR_fprintf(outputFD, "SaveUnnamableScript: text=%s, basedir=%s,\n"
                             "archiveDir=%s, filename=%s\n",
                   text, basedir, archiveDir,
                   HTMLfilename);
    }

    /* Construct the filename */
    ext = PL_strrchr(HTMLfilename, '.');
    if (ext) {
        *ext = '\0';
    }
    for (start = HTMLfilename; strpbrk(start, "/\\");
         start = strpbrk(start, "/\\") + 1)
        /* do nothing */;
    if (*start == '\0')
        start = HTMLfilename;
    id = PR_smprintf("_%s%d", start, idOrdinal++);
    if (ext) {
        *ext = '.';
    }

    /* Now call SaveInlineScript to do the work */
    retval = SaveInlineScript(text, id, basedir, archiveDir);

    PR_Free(id);

    return retval;
}

/************************************************************************
 *
 * S a v e S o u r c e
 *
 */
static int
SaveSource(char *src, char *codebase, char *basedir, char *archiveDir)
{
    char *from = NULL, *to = NULL;
    int retval = -1;
    char *arcDir = NULL;

    if (!src || !archiveDir) {
        return -1;
    }

    if (dumpParse) {
        PR_fprintf(outputFD, "SaveSource: src=%s, codebase=%s, basedir=%s,\n"
                             "archiveDir=%s\n",
                   src, codebase, basedir, archiveDir);
    }

    if (codebase) {
        arcDir = PR_smprintf("%s/%s/%s/", basedir, codebase, archiveDir);
    } else {
        arcDir = PR_smprintf("%s/%s/", basedir, archiveDir);
    }

    if (codebase) {
        from = PR_smprintf("%s/%s/%s", basedir, codebase, src);
        to = PR_smprintf("%s%s", arcDir, src);
    } else {
        from = PR_smprintf("%s/%s", basedir, src);
        to = PR_smprintf("%s%s", arcDir, src);
    }

    if (make_dirs(to, 0777)) {
        PR_fprintf(errorFD,
                   "ERROR: Unable to create archive directory %s.\n", archiveDir);
        errorCount++;
        goto finish;
    }

    retval = copyinto(from, to);
finish:
    if (from)
        PR_Free(from);
    if (to)
        PR_Free(to);
    if (arcDir)
        PR_Free(arcDir);
    return retval;
}

/************************************************************************
 *
 * T a g T y p e T o S t r i n g
 *
 */
char *
TagTypeToString(TAG_TYPE type)
{
    switch (type) {
        case APPLET_TAG:
            return "APPLET";
        case SCRIPT_TAG:
            return "SCRIPT";
        case LINK_TAG:
            return "LINK";
        case STYLE_TAG:
            return "STYLE";
        default:
            break;
    }
    return "unknown";
}

/************************************************************************
 *
 * e x t r a c t _ j s
 *
 */
static int
extract_js(char *filename)
{
    PRFileDesc *fd = NULL;
    FileBuffer *fb = NULL;
    HTMLItem *head = NULL;
    HTMLItem *tail = NULL;
    HTMLItem *curitem = NULL;
    HTMLItem *styleList = NULL;
    HTMLItem *styleListTail = NULL;
    HTMLItem *entityList = NULL;
    HTMLItem *entityListTail = NULL;
    TagItem *tagp = NULL;
    char *text = NULL;
    char *tagerr = NULL;
    char *archiveDir = NULL;
    char *firstArchiveDir = NULL;
    char *basedir = NULL;
    PRInt32 textStart;
    PRInt32 curOffset;
    HTML_STATE state;
    int curchar;
    int retval = -1;
    unsigned int linenum, startLine;

    /* Initialize the implicit ID counter for each file */
    idOrdinal = 0;

    /*
     * First, parse the HTML into a stream of tags and text.
     */

    fd = PR_Open(filename, PR_RDONLY, 0);
    if (!fd) {
        PR_fprintf(errorFD, "Unable to open %s for reading.\n", filename);
        errorCount++;
        return -1;
    }

    /* Construct base directory of filename. */
    {
        char *cp;

        basedir = PL_strdup(filename);

        /* Remove trailing slashes */
        while ((cp = PL_strprbrk(basedir, "/\\")) ==
               (basedir + strlen(basedir) - 1)) {
            *cp = '\0';
        }

        /* Now remove everything from the last slash (which will be followed
         * by a filename) to the end */
        cp = PL_strprbrk(basedir, "/\\");
        if (cp) {
            *cp = '\0';
        }
    }

    state = TEXT_HTML_STATE;

    fb = FB_Create(fd);

    textStart = 0;
    startLine = 0;
    while (linenum = FB_GetLineNum(fb), (curchar = FB_GetChar(fb)) !=
                                            EOF) {
        switch (state) {
            case TEXT_HTML_STATE:
                if (curchar == '<') {
                    /*
                     * Found a tag
                     */
                    /* Save the text so far to a new text item */
                    curOffset = FB_GetPointer(fb) - 2;
                    if (curOffset >= textStart) {
                        if (FB_GetRange(fb, textStart, curOffset,
                                        &text) !=
                            curOffset - textStart + 1) {
                            PR_fprintf(errorFD,
                                       "Unable to read from %s.\n",
                                       filename);
                            errorCount++;
                            goto loser;
                        }
                        /* little fudge here.  If the first character on a line
                         * is '<', meaning a new tag, the preceding text item
                         * actually ends on the previous line.  In this case
                         * we will be saying that the text segment ends on the
                         * next line. I don't think this matters for text items. */
                        curitem = CreateTextItem(text, startLine,
                                                 linenum);
                        text = NULL;
                        if (tail == NULL) {
                            head = tail = curitem;
                        } else {
                            tail->next = curitem;
                            tail = curitem;
                        }
                    }

                    /* Process the tag */
                    tagp = ProcessTag(fb, &tagerr);
                    if (!tagp) {
                        if (tagerr) {
                            PR_fprintf(errorFD, "Error in file %s: %s\n",
                                       filename, tagerr);
                            errorCount++;
                        } else {
                            PR_fprintf(errorFD,
                                       "Error in file %s, in tag starting at line %d\n",
                                       filename, linenum);
                            errorCount++;
                        }
                        goto loser;
                    }
                    /* Add the tag to the list */
                    curitem = CreateTagItem(tagp, linenum, FB_GetLineNum(fb));
                    if (tail == NULL) {
                        head = tail = curitem;
                    } else {
                        tail->next = curitem;
                        tail = curitem;
                    }

                    /* What's the next state */
                    if (tagp->type == SCRIPT_TAG) {
                        state = SCRIPT_HTML_STATE;
                    }

                    /* Start recording text from the new offset */
                    textStart = FB_GetPointer(fb);
                    startLine = FB_GetLineNum(fb);
                } else {
                    /* regular character.  Next! */
                }
                break;
            case SCRIPT_HTML_STATE:
                if (curchar == '<') {
                    char *cp;
                    /*
                     * If this is a </script> tag, then we're at the end of the
                     * script.  Otherwise, ignore
                     */
                    curOffset = FB_GetPointer(fb) - 1;
                    cp = NULL;
                    if (FB_GetRange(fb, curOffset, curOffset + 8, &cp) != 9) {
                        if (cp) {
                            PR_Free(cp);
                            cp = NULL;
                        }
                    } else {
                        /* compare the strings */
                        if (!PORT_Strncasecmp(cp, "</script>", 9)) {
                            /* This is the end of the script. Record the text. */
                            curOffset--;
                            if (curOffset >= textStart) {
                                if (FB_GetRange(fb, textStart, curOffset, &text) !=
                                    curOffset - textStart + 1) {
                                    PR_fprintf(errorFD, "Unable to read from %s.\n",
                                               filename);
                                    errorCount++;
                                    goto loser;
                                }
                                curitem = CreateTextItem(text, startLine, linenum);
                                text = NULL;
                                if (tail == NULL) {
                                    head = tail = curitem;
                                } else {
                                    tail->next = curitem;
                                    tail = curitem;
                                }
                            }

                            /* Now parse the /script tag and put it on the list */
                            tagp = ProcessTag(fb, &tagerr);
                            if (!tagp) {
                                if (tagerr) {
                                    PR_fprintf(errorFD, "Error in file %s: %s\n",
                                               filename, tagerr);
                                } else {
                                    PR_fprintf(errorFD,
                                               "Error in file %s, in tag starting at"
                                               " line %d\n",
                                               filename, linenum);
                                }
                                errorCount++;
                                goto loser;
                            }
                            curitem = CreateTagItem(tagp, linenum,
                                                    FB_GetLineNum(fb));
                            if (tail == NULL) {
                                head = tail = curitem;
                            } else {
                                tail->next = curitem;
                                tail = curitem;
                            }

                            /* go back to text state */
                            state = TEXT_HTML_STATE;

                            textStart = FB_GetPointer(fb);
                            startLine = FB_GetLineNum(fb);
                        }
                    }
                }
                break;
        }
    }

    /* End of the file.  Wrap up any remaining text */
    if (state == SCRIPT_HTML_STATE) {
        if (tail && tail->type == TAG_ITEM) {
            PR_fprintf(errorFD, "ERROR: <SCRIPT> tag at %s:%d is not followed "
                                "by a </SCRIPT> tag.\n",
                       filename, tail->startLine);
        } else {
            PR_fprintf(errorFD, "ERROR: <SCRIPT> tag in file %s is not followed"
                                " by a </SCRIPT tag.\n",
                       filename);
        }
        errorCount++;
        goto loser;
    }
    curOffset = FB_GetPointer(fb) - 1;
    if (curOffset >= textStart) {
        text = NULL;
        if (FB_GetRange(fb, textStart, curOffset, &text) !=
            curOffset - textStart + 1) {
            PR_fprintf(errorFD, "Unable to read from %s.\n", filename);
            errorCount++;
            goto loser;
        }
        curitem = CreateTextItem(text, startLine, linenum);
        text = NULL;
        if (tail == NULL) {
            head = tail = curitem;
        } else {
            tail->next = curitem;
            tail = curitem;
        }
    }

    if (dumpParse) {
        PrintHTMLStream(outputFD, head);
    }

    /*
     * Now we have a stream of tags and text.  Go through and deal with each.
     */
    for (curitem = head; curitem; curitem = curitem->next) {
        TagItem *tagp = NULL;
        AVPair *pairp = NULL;
        char *src = NULL, *id = NULL, *codebase = NULL;
        PRBool hasEventHandler = PR_FALSE;
        int i;

        /* Reset archive directory for each tag */
        if (archiveDir) {
            PR_Free(archiveDir);
            archiveDir = NULL;
        }

        /* We only analyze tags */
        if (curitem->type != TAG_ITEM) {
            continue;
        }

        tagp = curitem->item.tag;

        /* go through the attributes to get information */
        for (pairp = tagp->attList; pairp; pairp = pairp->next) {

            /* ARCHIVE= */
            if (!PL_strcasecmp(pairp->attribute, "archive")) {
                if (archiveDir) {
                    /* Duplicate attribute.  Print warning */
                    PR_fprintf(errorFD,
                               "warning: \"%s\" attribute overwrites previous attribute"
                               " in tag starting at %s:%d.\n",
                               pairp->attribute, filename, curitem->startLine);
                    warningCount++;
                    PR_Free(archiveDir);
                }
                archiveDir = PL_strdup(pairp->value);

                /* Substiture ".arc" for ".jar" */
                if ((PL_strlen(archiveDir) < 4) ||
                    PL_strcasecmp((archiveDir + strlen(archiveDir) - 4),
                                  ".jar")) {
                    PR_fprintf(errorFD,
                               "warning: ARCHIVE attribute should end in \".jar\" in tag"
                               " starting on %s:%d.\n",
                               filename, curitem->startLine);
                    warningCount++;
                    PR_Free(archiveDir);
                    archiveDir = PR_smprintf("%s.arc", archiveDir);
                } else {
                    PL_strcpy(archiveDir + strlen(archiveDir) - 4, ".arc");
                }

                /* Record the first archive.  This will be used later if
                 * the archive is not specified */
                if (firstArchiveDir == NULL) {
                    firstArchiveDir = PL_strdup(archiveDir);
                }
            }
            /* CODEBASE= */
            else if (!PL_strcasecmp(pairp->attribute, "codebase")) {
                if (codebase) {
                    /* Duplicate attribute.  Print warning */
                    PR_fprintf(errorFD,
                               "warning: \"%s\" attribute overwrites previous attribute"
                               " in tag staring at %s:%d.\n",
                               pairp->attribute, filename, curitem->startLine);
                    warningCount++;
                }
                codebase = pairp->value;
            }
            /* SRC= and HREF= */
            else if (!PORT_Strcasecmp(pairp->attribute, "src") ||
                     !PORT_Strcasecmp(pairp->attribute, "href")) {
                if (src) {
                    /* Duplicate attribute.  Print warning */
                    PR_fprintf(errorFD,
                               "warning: \"%s\" attribute overwrites previous attribute"
                               " in tag staring at %s:%d.\n",
                               pairp->attribute, filename, curitem->startLine);
                    warningCount++;
                }
                src = pairp->value;
            }
            /* CODE= */
            else if (!PORT_Strcasecmp(pairp->attribute, "code")) {
                /*!!!XXX Change PORT to PL all over this code !!! */
                if (src) {
                    /* Duplicate attribute.  Print warning */
                    PR_fprintf(errorFD,
                               "warning: \"%s\" attribute overwrites previous attribute"
                               " ,in tag staring at %s:%d.\n",
                               pairp->attribute, filename, curitem->startLine);
                    warningCount++;
                }
                src = pairp->value;

                /* Append a .class if one is not already present */
                if ((PL_strlen(src) < 6) ||
                    PL_strcasecmp((src + PL_strlen(src) - 6), ".class")) {
                    src = PR_smprintf("%s.class", src);
                    /* Put this string back into the data structure so it
                     * will be deallocated properly */
                    PR_Free(pairp->value);
                    pairp->value = src;
                }
            }
            /* ID= */
            else if (!PL_strcasecmp(pairp->attribute, "id")) {
                if (id) {
                    /* Duplicate attribute.  Print warning */
                    PR_fprintf(errorFD,
                               "warning: \"%s\" attribute overwrites previous attribute"
                               " in tag staring at %s:%d.\n",
                               pairp->attribute, filename, curitem->startLine);
                    warningCount++;
                }
                id = pairp->value;
            }

            /* STYLE= */
            /* style= attributes, along with JS entities, are stored into
             * files with dynamically generated names. The filenames are
             * based on the order in which the text is found in the file.
             * All JS entities on all lines up to and including the line
             * containing the end of the tag that has this style= attribute
             * will be processed before this style=attribute.  So we need
             * to record the line that this _tag_ (not the attribute) ends on.
             */
            else if (!PL_strcasecmp(pairp->attribute, "style") && pairp->value) {
                HTMLItem *styleItem;
                /* Put this item on the style list */
                styleItem = CreateTextItem(PL_strdup(pairp->value),
                                           curitem->startLine, curitem->endLine);
                if (styleListTail == NULL) {
                    styleList = styleListTail = styleItem;
                } else {
                    styleListTail->next = styleItem;
                    styleListTail = styleItem;
                }
            }
            /* Event handlers */
            else {
                for (i = 0; i < num_handlers; i++) {
                    if (!PL_strcasecmp(event_handlers[i], pairp->attribute)) {
                        hasEventHandler = PR_TRUE;
                        break;
                    }
                }
            }

            /* JS Entity */
            {
                char *entityStart, *entityEnd;
                HTMLItem *entityItem;

                /* go through each JavaScript entity ( &{...}; ) and store it
                 * in the entityList.  The important thing is to record what
                 * line number it's on, so we can get it in the right order
                 * in relation to style= attributes.
                 * Apparently, these can't flow across lines, so the start and
                 * end line will be the same.  That helps matters.
                 */
                entityEnd = pairp->value;
                while (entityEnd &&
                       (entityStart = PL_strstr(entityEnd, "&{")) /*}*/ != NULL) {
                    entityStart += 2; /* point at beginning of actual entity */
                    entityEnd = PL_strchr(entityStart, '}');
                    if (entityEnd) {
                        /* Put this item on the entity list */
                        *entityEnd = '\0';
                        entityItem = CreateTextItem(PL_strdup(entityStart),
                                                    pairp->valueLine, pairp->valueLine);
                        *entityEnd = /* { */ '}';
                        if (entityListTail) {
                            entityListTail->next = entityItem;
                            entityListTail = entityItem;
                        } else {
                            entityList = entityListTail = entityItem;
                        }
                    }
                }
            }
        }

        /* If no archive was supplied, we use the first one of the file */
        if (!archiveDir && firstArchiveDir) {
            archiveDir = PL_strdup(firstArchiveDir);
        }

        /* If we have an event handler, we need to archive this tag */
        if (hasEventHandler) {
            if (!id) {
                PR_fprintf(errorFD,
                           "warning: tag starting at %s:%d has event handler but"
                           " no ID attribute.  The tag will not be signed.\n",
                           filename, curitem->startLine);
                warningCount++;
            } else if (!archiveDir) {
                PR_fprintf(errorFD,
                           "warning: tag starting at %s:%d has event handler but"
                           " no ARCHIVE attribute.  The tag will not be signed.\n",
                           filename, curitem->startLine);
                warningCount++;
            } else {
                if (SaveInlineScript(tagp->text, id, basedir, archiveDir)) {
                    goto loser;
                }
            }
        }

        switch (tagp->type) {
            case APPLET_TAG:
                if (!src) {
                    PR_fprintf(errorFD,
                               "error: APPLET tag starting on %s:%d has no CODE "
                               "attribute.\n",
                               filename, curitem->startLine);
                    errorCount++;
                    goto loser;
                } else if (!archiveDir) {
                    PR_fprintf(errorFD,
                               "error: APPLET tag starting on %s:%d has no ARCHIVE "
                               "attribute.\n",
                               filename, curitem->startLine);
                    errorCount++;
                    goto loser;
                } else {
                    if (SaveSource(src, codebase, basedir, archiveDir)) {
                        goto loser;
                    }
                }
                break;
            case SCRIPT_TAG:
            case LINK_TAG:
            case STYLE_TAG:
                if (!archiveDir) {
                    PR_fprintf(errorFD,
                               "error: %s tag starting on %s:%d has no ARCHIVE "
                               "attribute.\n",
                               TagTypeToString(tagp->type),
                               filename, curitem->startLine);
                    errorCount++;
                    goto loser;
                } else if (src) {
                    if (SaveSource(src, codebase, basedir, archiveDir)) {
                        goto loser;
                    }
                } else if (id) {
                    /* Save the next text item */
                    if (!curitem->next || (curitem->next->type !=
                                           TEXT_ITEM)) {
                        PR_fprintf(errorFD,
                                   "warning: %s tag starting on %s:%d is not followed"
                                   " by script text.\n",
                                   TagTypeToString(tagp->type),
                                   filename, curitem->startLine);
                        warningCount++;
                        /* just create empty file */
                        if (SaveInlineScript("", id, basedir, archiveDir)) {
                            goto loser;
                        }
                    } else {
                        curitem = curitem->next;
                        if (SaveInlineScript(curitem->item.text,
                                             id, basedir,
                                             archiveDir)) {
                            goto loser;
                        }
                    }
                } else {
                    /* No src or id tag--warning */
                    PR_fprintf(errorFD,
                               "warning: %s tag starting on %s:%d has no SRC or"
                               " ID attributes.  Will not sign.\n",
                               TagTypeToString(tagp->type), filename, curitem->startLine);
                    warningCount++;
                }
                break;
            default:
                /* do nothing for other tags */
                break;
        }
    }

    /* Now deal with all the unnamable scripts */
    if (firstArchiveDir) {
        HTMLItem *style, *entity;

        /* Go through the lists of JS entities and style attributes.  Do them
         * in chronological order within a list.  Pick the list with the lower
         * endLine. In case of a tie, entities come first.
         */
        style = styleList;
        entity = entityList;
        while (style || entity) {
            if (!entity || (style && (style->endLine < entity->endLine))) {
                /* Process style */
                SaveUnnamableScript(style->item.text, basedir, firstArchiveDir,
                                    filename);
                style = style->next;
            } else {
                /* Process entity */
                SaveUnnamableScript(entity->item.text, basedir, firstArchiveDir,
                                    filename);
                entity = entity->next;
            }
        }
    }

    retval = 0;
loser:
    /* Blow away the stream */
    while (head) {
        curitem = head;
        head = head->next;
        DestroyHTMLItem(curitem);
    }
    while (styleList) {
        curitem = styleList;
        styleList = styleList->next;
        DestroyHTMLItem(curitem);
    }
    while (entityList) {
        curitem = entityList;
        entityList = entityList->next;
        DestroyHTMLItem(curitem);
    }
    if (text) {
        PR_Free(text);
        text = NULL;
    }
    if (fb) {
        FB_Destroy(fb);
        fb = NULL;
    }
    if (fd) {
        PR_Close(fd);
    }
    if (tagerr) {
        PR_smprintf_free(tagerr);
        tagerr = NULL;
    }
    if (archiveDir) {
        PR_Free(archiveDir);
        archiveDir = NULL;
    }
    if (firstArchiveDir) {
        PR_Free(firstArchiveDir);
        firstArchiveDir = NULL;
    }
    if (entityListTail) {
        PR_Free(entityListTail);
    }
    if (curitem) {
        PR_Free(curitem);
    }
    if (basedir) {
        PR_Free(basedir);
    }
    return retval;
}

/**********************************************************************
 *
 * e n s u r e E x i s t s
 *
 * Check for existence of indicated directory.  If it doesn't exist,
 * it will be created.
 * Returns PR_SUCCESS if the directory is present, PR_FAILURE otherwise.
 */
static PRStatus
ensureExists(char *base, char *path)
{
    char fn[FNSIZE];
    PRDir *dir;
    sprintf(fn, "%s/%s", base, path);

    /*PR_fprintf(outputFD, "Trying to open directory %s.\n", fn);*/

    if ((dir = PR_OpenDir(fn))) {
        PR_CloseDir(dir);
        return PR_SUCCESS;
    }
    return PR_MkDir(fn, 0777);
}

/***************************************************************************
 *
 * m a k e _ d i r s
 *
 * Ensure that the directory portion of the path exists.  This may require
 * making the directory, and its parent, and its parent's parent, etc.
 */
static int
make_dirs(char *path, int file_perms)
{
    char *Path;
    char *start;
    char *sep;
    int ret = 0;
    PRFileInfo info;

    if (!path) {
        return 0;
    }

    Path = PL_strdup(path);
    if (!Path) {
        return 0;
    }

    start = strpbrk(Path, "/\\");
    if (!start) {
        goto loser;
    }
    start++; /* start right after first slash */

    /* Each time through the loop add one more directory. */
    while ((sep = strpbrk(start, "/\\"))) {
        *sep = '\0';

        if (PR_GetFileInfo(Path, &info) != PR_SUCCESS) {
            /* No such dir, we have to create it */
            if (PR_MkDir(Path, file_perms) != PR_SUCCESS) {
                PR_fprintf(errorFD, "ERROR: Unable to create directory %s.\n",
                           Path);
                errorCount++;
                ret = -1;
                goto loser;
            }
        } else {
            /* something exists by this name, make sure it's a directory */
            if (info.type != PR_FILE_DIRECTORY) {
                PR_fprintf(errorFD, "ERROR: Unable to create directory %s.\n",
                           Path);
                errorCount++;
                ret = -1;
                goto loser;
            }
        }

        start = sep + 1; /* start after the next slash */
        *sep = '/';
    }

loser:
    PR_Free(Path);
    return ret;
}

/*
 *  c o p y i n t o
 *
 *  Function to copy file "from" to path "to".
 *
 */
static int
copyinto(char *from, char *to)
{
    PRInt32 num;
    char buf[BUFSIZ];
    PRFileDesc *infp = NULL, *outfp = NULL;
    int retval = -1;

    if ((infp = PR_Open(from, PR_RDONLY, 0777)) == NULL) {
        PR_fprintf(errorFD, "ERROR: Unable to open \"%s\" for reading.\n",
                   from);
        errorCount++;
        goto finish;
    }

    /* If to already exists, print a warning before deleting it */
    if (PR_Access(to, PR_ACCESS_EXISTS) == PR_SUCCESS) {
        PR_fprintf(errorFD, "warning: %s already exists--will overwrite\n", to);
        warningCount++;
        if (rm_dash_r(to)) {
            PR_fprintf(errorFD,
                       "ERROR: Unable to remove %s.\n", to);
            errorCount++;
            goto finish;
        }
    }

    if ((outfp = PR_Open(to, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0777)) ==
        NULL) {
        char *errBuf = NULL;

        errBuf = PR_Malloc(PR_GetErrorTextLength() + 1);
        PR_fprintf(errorFD, "ERROR: Unable to open \"%s\" for writing.\n", to);
        if (PR_GetErrorText(errBuf)) {
            PR_fprintf(errorFD, "Cause: %s\n", errBuf);
        }
        if (errBuf) {
            PR_Free(errBuf);
        }
        errorCount++;
        goto finish;
    }

    while ((num = PR_Read(infp, buf, BUFSIZ)) > 0) {
        if (PR_Write(outfp, buf, num) != num) {
            PR_fprintf(errorFD, "ERROR: Error writing to %s.\n", to);
            errorCount++;
            goto finish;
        }
    }

    retval = 0;
finish:
    if (infp)
        PR_Close(infp);
    if (outfp)
        PR_Close(outfp);

    return retval;
}