/******************************* LICENSE **************************************
* Any code in this file may be redistributed or modified under the terms of
* the GNU General Public License as published by the Free Software
* Foundation; version 2 of the license.
****************************** END LICENSE ***********************************/

/******************************************************************************
* Author:
* Andrew Smith, http://littlesvr.ca/misc/contactandrew.php
*
* Copyright 2005-2007 Andrew Smith <andrew-smith@mail.ru>
*
* Contributors:
*
******************************************************************************/

#include <stdbool.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <stdio.h>

#include "bk.h"
#include "bkInternal.h"
#include "bkExtract.h"
#include "bkPath.h"
#include "bkError.h"
#include "bkMisc.h"

/*******************************************************************************
* bk_extract_boot_record()
* Extracts the el torito boot record to the file destPathAndName, with
* permissions destFilePerms.
* */
int bk_extract_boot_record(VolInfo* volInfo, const char* destPathAndName,
                           unsigned destFilePerms)
{
    int srcFile; /* returned by open() */
    bool srcFileWasOpened;
    int destFile; /* returned by open() */
    int rc;

    if (volInfo->bootMediaType == BOOT_MEDIA_NONE)
        return BKERROR_EXTRACT_ABSENT_BOOT_RECORD;

    if (volInfo->bootMediaType != BOOT_MEDIA_NO_EMULATION &&
            volInfo->bootMediaType != BOOT_MEDIA_1_2_FLOPPY &&
            volInfo->bootMediaType != BOOT_MEDIA_1_44_FLOPPY &&
            volInfo->bootMediaType != BOOT_MEDIA_2_88_FLOPPY) {
        return BKERROR_EXTRACT_UNKNOWN_BOOT_MEDIA;
    }

    /* SET source file (open if needed) */
    if (volInfo->bootRecordIsVisible)
        /* boot record is a file in the tree */
    {
        if (volInfo->bootRecordOnImage->onImage) {
            srcFile = volInfo->imageForReading;
            lseek(volInfo->imageForReading, volInfo->bootRecordOnImage->position, SEEK_SET);
            srcFileWasOpened = false;
        } else {
            srcFile = open(volInfo->bootRecordOnImage->pathAndName, O_RDONLY);
            if (srcFile == -1)
                return BKERROR_OPEN_READ_FAILED;
            srcFileWasOpened = true;
        }
    } else
        /* boot record is not a file in the tree */
    {
        if (volInfo->bootRecordIsOnImage) {
            srcFile = volInfo->imageForReading;
            lseek(volInfo->imageForReading, volInfo->bootRecordOffset, SEEK_SET);
            srcFileWasOpened = false;
        } else {
            srcFile = open(volInfo->bootRecordPathAndName, O_RDONLY);
            if (srcFile == -1)
                return BKERROR_OPEN_READ_FAILED;
            srcFileWasOpened = true;
        }
    }
    /* END SET source file (open if needed) */

    destFile = open(destPathAndName, O_WRONLY | O_CREAT | O_TRUNC,
                    destFilePerms);
    if (destFile == -1) {
        if (srcFileWasOpened)
            close(srcFile);
        return BKERROR_OPEN_WRITE_FAILED;
    }

    rc = copyByteBlock(volInfo, srcFile, destFile, volInfo->bootRecordSize);
    if (rc <= 0) {
        if (srcFileWasOpened)
            close(srcFile);
        close(destFile);
        return rc;
    }

    close(destFile);

    if (srcFileWasOpened)
        close(srcFile);

    return 1;
}

int bk_extract(VolInfo* volInfo, const char* srcPathAndName,
               const char* destDir, bool keepPermissions,
               void(*progressFunction)(VolInfo*))
{
    return bk_extract_as(volInfo, srcPathAndName, destDir, NULL,
                         keepPermissions, progressFunction);
}

int bk_extract_as(VolInfo* volInfo, const char* srcPathAndName,
                  const char* destDir, const char* nameToUse,
                  bool keepPermissions, void(*progressFunction)(VolInfo*))
{
    int rc;
    NewPath srcPath;
    BkDir* parentDir;
    bool dirFound;

    volInfo->progressFunction = progressFunction;
    volInfo->stopOperation = false;

    rc = makeNewPathFromString(srcPathAndName, &srcPath);
    if (rc <= 0) {
        freePathContents(&srcPath);
        return rc;
    }

    if (srcPath.numChildren == 0) {
        freePathContents(&srcPath);
        return BKERROR_EXTRACT_ROOT;
    }

    /* i want the parent directory */
    srcPath.numChildren--;
    dirFound = findDirByNewPath(&srcPath, &(volInfo->dirTree), &parentDir);
    srcPath.numChildren++;
    if (!dirFound) {
        freePathContents(&srcPath);
        return BKERROR_DIR_NOT_FOUND_ON_IMAGE;
    }

    rc = extract(volInfo, parentDir, srcPath.children[srcPath.numChildren - 1],
                 destDir, nameToUse, keepPermissions);

    freePathContents(&srcPath);

    if (rc <= 0) {
        return rc;
    }

    return 1;
}

int copyByteBlock(VolInfo* volInfo, int src, int dest, unsigned numBytes)
{
    int rc;
    int count;
    int numBlocks;
    int sizeLastBlock;

    numBlocks = numBytes / READ_WRITE_BUFFER_SIZE;
    sizeLastBlock = numBytes % READ_WRITE_BUFFER_SIZE;

    maybeUpdateProgress(volInfo);
    if (volInfo->stopOperation)
        return BKERROR_OPER_CANCELED_BY_USER;

    for (count = 0; count < numBlocks; count++) {
        maybeUpdateProgress(volInfo);
        if (volInfo->stopOperation)
            return BKERROR_OPER_CANCELED_BY_USER;

        rc = read(src, volInfo->readWriteBuffer, READ_WRITE_BUFFER_SIZE);
        if (rc != READ_WRITE_BUFFER_SIZE)
            return BKERROR_READ_GENERIC;
        rc = write(dest, volInfo->readWriteBuffer, READ_WRITE_BUFFER_SIZE);
        if (rc <= 0)
            return rc;
    }

    if (sizeLastBlock > 0) {
        rc = read(src, volInfo->readWriteBuffer, sizeLastBlock);
        if (rc != sizeLastBlock)
            return BKERROR_READ_GENERIC;
        rc = write(dest, volInfo->readWriteBuffer, sizeLastBlock);
        if (rc <= 0)
            return rc;
    }

    return 1;
}

int extract(VolInfo* volInfo, BkDir* parentDir, char* nameToExtract,
            const char* destDir, const char* nameToUse, bool keepPermissions)
{
    BkFileBase* child;
    int rc;

    child = parentDir->children;
    while (child != NULL) {
        if (volInfo->stopOperation)
            return BKERROR_OPER_CANCELED_BY_USER;

        if (strcmp(child->name, nameToExtract) == 0) {
            if (IS_DIR(child->posixFileMode)) {
                rc = extractDir(volInfo, BK_DIR_PTR(child), destDir,
                                nameToUse, keepPermissions);
            } else if (IS_REG_FILE(child->posixFileMode)) {
                rc = extractFile(volInfo, BK_FILE_PTR(child), destDir,
                                 nameToUse, keepPermissions);
            } else if (IS_SYMLINK(child->posixFileMode)) {
                rc = extractSymlink(BK_SYMLINK_PTR(child), destDir,
                                    nameToUse);
            } else {
                printf("trying to extract something that's not a file, "
                       "symlink or directory, ignored\n"); fflush(NULL);
            }

            if (rc <= 0) {
                bool goOn;

                if (volInfo->warningCbk != NULL && !volInfo->stopOperation)
                    /* perhaps the user wants to ignore this failure */
                {
                    snprintf(volInfo->warningMessage, BK_WARNING_MAX_LEN,
                             "Failed to extract item '%s': '%s'",
                             child->name,
                             bk_get_error_string(rc));
                    goOn = volInfo->warningCbk(volInfo->warningMessage);
                    rc = BKWARNING_OPER_PARTLY_FAILED;
                } else
                    goOn = false;

                if (!goOn) {
                    volInfo->stopOperation = true;
                    return rc;
                }
            }
        }

        child = child->next;
    }

    return 1;
}

int extractDir(VolInfo* volInfo, BkDir* srcDir, const char* destDir,
               const char* nameToUse, bool keepPermissions)
{
    int rc;
    BkFileBase* child;

    /* vars to create destination dir */
    char* newDestDir;
    unsigned destDirPerms;

    /* CREATE destination dir on filesystem */
    /* 1 for '\0' */
    if (nameToUse == NULL)
        newDestDir = malloc(strlen(destDir) + strlen(BK_BASE_PTR(srcDir)->name) + 2);
    else
        newDestDir = malloc(strlen(destDir) + strlen(nameToUse) + 2);
    if (newDestDir == NULL)
        return BKERROR_OUT_OF_MEMORY;

    strcpy(newDestDir, destDir);
    if (destDir[strlen(destDir) - 1] != '/')
        strcat(newDestDir, "/");
    if (nameToUse == NULL)
        strcat(newDestDir, BK_BASE_PTR(srcDir)->name);
    else
        strcat(newDestDir, nameToUse);

    if (keepPermissions)
        destDirPerms = BK_BASE_PTR(BK_BASE_PTR(srcDir))->posixFileMode;
    else
        destDirPerms = volInfo->posixDirDefaults;
    /* want to make sure user has write and execute permissions to new directory
    * so that can extract stuff into it */
    destDirPerms |= 0300;

    if (access(newDestDir, F_OK) == 0) {
        free(newDestDir);
        return BKERROR_DUPLICATE_EXTRACT;
    }

    rc = mkdir(newDestDir, destDirPerms);
    if (rc == -1) {
        free(newDestDir);
        return BKERROR_MKDIR_FAILED;
    }
    /* END CREATE destination dir on filesystem */

    /* EXTRACT children */
    child = srcDir->children;
    while (child != NULL) {
        rc = extract(volInfo, srcDir, child->name, newDestDir,
                     NULL, keepPermissions);
        if (rc <= 0) {
            free(newDestDir);
            return rc;
        }

        child = child->next;
    }
    /* END EXTRACT children */

    free(newDestDir);

    return 1;
}

int extractFile(VolInfo* volInfo, BkFile* srcFileInTree, const char* destDir,
                const char* nameToUse, bool keepPermissions)
{
    int srcFile;
    bool srcFileWasOpened;
    char* destPathAndName;
    unsigned destFilePerms;
    int destFile; /* returned by open() */
    int rc;

    if (srcFileInTree->onImage) {
        srcFile = volInfo->imageForReading;
        lseek(volInfo->imageForReading, srcFileInTree->position, SEEK_SET);
        srcFileWasOpened = false;
    } else {
        srcFile = open(srcFileInTree->pathAndName, O_RDONLY);
        if (srcFile == -1)
            return BKERROR_OPEN_READ_FAILED;
        srcFileWasOpened = true;

        /* UPDATE the file's size, in case it's changed since we added it */
        struct stat statStruct;

        rc = stat(srcFileInTree->pathAndName, &statStruct);
        if (rc != 0) {
            if (srcFileWasOpened)
                close(srcFile);
            return BKERROR_STAT_FAILED;
        }

        srcFileInTree->size = statStruct.st_size;
        /* UPDATE the file's size, in case it's changed since we added it */
    }

    if (nameToUse == NULL)
        destPathAndName = malloc(strlen(destDir) +
                                 strlen(BK_BASE_PTR(srcFileInTree)->name) + 2);
    else
        destPathAndName = malloc(strlen(destDir) + strlen(nameToUse) + 2);
    if (destPathAndName == NULL) {
        if (srcFileWasOpened)
            close(srcFile);
        return BKERROR_OUT_OF_MEMORY;
    }

    strcpy(destPathAndName, destDir);
    if (destDir[strlen(destDir) - 1] != '/')
        strcat(destPathAndName, "/");
    if (nameToUse == NULL)
        strcat(destPathAndName, BK_BASE_PTR(srcFileInTree)->name);
    else
        strcat(destPathAndName, nameToUse);

    if (access(destPathAndName, F_OK) == 0) {
        if (srcFileWasOpened)
            close(srcFile);
        free(destPathAndName);
        return BKERROR_DUPLICATE_EXTRACT;
    }

    /* WRITE file */
    if (keepPermissions)
        destFilePerms = BK_BASE_PTR(srcFileInTree)->posixFileMode;
    else
        destFilePerms = volInfo->posixFileDefaults;

    destFile = open(destPathAndName, O_WRONLY | O_CREAT | O_TRUNC, destFilePerms);
    if (destFile == -1) {
        if (srcFileWasOpened)
            close(srcFile);
        free(destPathAndName);
        return BKERROR_OPEN_WRITE_FAILED;
    }

    free(destPathAndName);

    rc = copyByteBlock(volInfo, srcFile, destFile, srcFileInTree->size);
    if (rc < 0) {
        close(destFile);
        if (srcFileWasOpened)
            close(srcFile);
        return rc;
    }

    close(destFile);
    if (destFile == -1) {
        if (srcFileWasOpened)
            close(srcFile);
        return BKERROR_EXOTIC;
    }
    /* END WRITE file */

    if (srcFileWasOpened)
        close(srcFile);

    return 1;
}

int extractSymlink(BkSymLink* srcLink, const char* destDir,
                   const char* nameToUse)
{
    char* destPathAndName;
    int rc;

    if (nameToUse == NULL)
        destPathAndName = malloc(strlen(destDir) +
                                 strlen(BK_BASE_PTR(srcLink)->name) + 2);
    else
        destPathAndName = malloc(strlen(destDir) + strlen(nameToUse) + 2);
    if (destPathAndName == NULL)
        return BKERROR_OUT_OF_MEMORY;

    strcpy(destPathAndName, destDir);
    if (destDir[strlen(destDir) - 1] != '/')
        strcat(destPathAndName, "/");
    if (nameToUse == NULL)
        strcat(destPathAndName, BK_BASE_PTR(srcLink)->name);
    else
        strcat(destPathAndName, nameToUse);

    if (access(destPathAndName, F_OK) == 0) {
        free(destPathAndName);
        return BKERROR_DUPLICATE_EXTRACT;
    }

    rc = symlink(srcLink->target, destPathAndName);
    if (rc == -1) {
        free(destPathAndName);
        return BKERROR_CREATE_SYMLINK_FAILED;
    }

    free(destPathAndName);

    return 1;
}
