/*
 * Tool to manipulate cabinet files
 *
 * Copyright 2011 Alexandre Julliard
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "config.h"
#include "wine/port.h"

#include <stdio.h>
#include <stdlib.h>

#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#include "fci.h"
#include "fdi.h"

#include "wine/unicode.h"
#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(cabarc);

/* from msvcrt */
#ifndef _O_RDONLY
#define _O_RDONLY      0
#define _O_WRONLY      1
#define _O_RDWR        2
#define _O_APPEND      0x0008
#define _O_RANDOM      0x0010
#define _O_SEQUENTIAL  0x0020
#define _O_TEMPORARY   0x0040
#define _O_NOINHERIT   0x0080
#define _O_CREAT       0x0100
#define _O_TRUNC       0x0200
#define _O_EXCL        0x0400
#define _O_SHORT_LIVED 0x1000
#define _O_TEXT        0x4000
#define _O_BINARY      0x8000
#endif

#ifndef _O_ACCMODE
#define _O_ACCMODE     (_O_RDONLY|_O_WRONLY|_O_RDWR)
#endif

#ifndef _SH_COMPAT
#define _SH_COMPAT     0x00
#define _SH_DENYRW     0x10
#define _SH_DENYWR     0x20
#define _SH_DENYRD     0x30
#define _SH_DENYNO     0x40
#endif

#ifndef _A_RDONLY
#define _A_RDONLY      0x01
#define _A_HIDDEN      0x02
#define _A_SYSTEM      0x04
#define _A_ARCH        0x20
#endif

/* command-line options */
static int opt_cabinet_size = CB_MAX_DISK;
static int opt_cabinet_id;
static int opt_compression = tcompTYPE_MSZIP;
static BOOL opt_recurse;
static BOOL opt_preserve_paths;
static int opt_reserve_space;
static int opt_verbose;
static char *opt_cab_file;
static WCHAR *opt_dest_dir;
static WCHAR **opt_files;

static void * CDECL cab_alloc( ULONG size )
{
    return HeapAlloc( GetProcessHeap(), 0, size );
}

static void CDECL cab_free( void *ptr )
{
    HeapFree( GetProcessHeap(), 0, ptr );
}

static WCHAR *strdupAtoW( UINT cp, const char *str )
{
    WCHAR *ret = NULL;
    if (str)
    {
        DWORD len = MultiByteToWideChar( cp, 0, str, -1, NULL, 0 );
        if ((ret = cab_alloc( len * sizeof(WCHAR) )))
            MultiByteToWideChar( cp, 0, str, -1, ret, len );
    }
    return ret;
}

static char *strdupWtoA( UINT cp, const WCHAR *str )
{
    char *ret = NULL;
    if (str)
    {
        DWORD len = WideCharToMultiByte( cp, 0, str, -1, NULL, 0, NULL, NULL );
        if ((ret = cab_alloc( len )))
            WideCharToMultiByte( cp, 0, str, -1, ret, len, NULL, NULL );
    }
    return ret;
}

/* format a cabinet name by replacing the '*' wildcard by the cabinet id */
static BOOL format_cab_name( char *dest, int id, const char *name )
{
    const char *num = strchr( name, '*' );
    int len;

    if (!num)
    {
        if (id == 1)
        {
            strcpy( dest, name );
            return TRUE;
        }
        WINE_MESSAGE( "cabarc: Cabinet name must contain a '*' character\n" );
        return FALSE;
    }
    len = num - name;
    memcpy( dest, name, len );
    len += sprintf( dest + len, "%u", id );
    lstrcpynA( dest + len, num + 1, CB_MAX_CABINET_NAME - len );
    return TRUE;
}

static int CDECL fci_file_placed( CCAB *cab, char *file, LONG size, BOOL continuation, void *ptr )
{
    if (!continuation && opt_verbose) printf( "adding %s\n", file );
    return 0;
}

static INT_PTR CDECL fci_open( char *file, int oflag, int pmode, int *err, void *ptr )
{
    DWORD creation = 0, sharing = 0;
    int ioflag = 0;
    HANDLE handle;

    switch (oflag & _O_ACCMODE)
    {
    case _O_RDONLY: ioflag |= GENERIC_READ; break;
    case _O_WRONLY: ioflag |= GENERIC_WRITE; break;
    case _O_RDWR:   ioflag |= GENERIC_READ | GENERIC_WRITE; break;
    }

    if (oflag & _O_CREAT)
    {
        if (oflag & _O_EXCL) creation = CREATE_NEW;
        else if (oflag & _O_TRUNC) creation = CREATE_ALWAYS;
        else creation = OPEN_ALWAYS;
    }
    else
    {
        if (oflag & _O_TRUNC) creation = TRUNCATE_EXISTING;
        else creation = OPEN_EXISTING;
    }

    switch (pmode & 0x70)
    {
    case _SH_DENYRW: sharing = 0; break;
    case _SH_DENYWR: sharing = FILE_SHARE_READ; break;
    case _SH_DENYRD: sharing = FILE_SHARE_WRITE; break;
    default:         sharing = FILE_SHARE_READ | FILE_SHARE_WRITE; break;
    }

    handle = CreateFileA( file, ioflag, sharing, NULL, creation, FILE_ATTRIBUTE_NORMAL, NULL );
    if (handle == INVALID_HANDLE_VALUE) *err = GetLastError();
    return (INT_PTR)handle;
}

static UINT CDECL fci_read( INT_PTR hf, void *pv, UINT cb, int *err, void *ptr )
{
    DWORD num_read;

    if (!ReadFile( (HANDLE)hf, pv, cb, &num_read, NULL ))
    {
        *err = GetLastError();
        return -1;
    }
    return num_read;
}

static UINT CDECL fci_write( INT_PTR hf, void *pv, UINT cb, int *err, void *ptr )
{
    DWORD written;

    if (!WriteFile( (HANDLE) hf, pv, cb, &written, NULL ))
    {
        *err = GetLastError();
        return -1;
    }
    return written;
}

static int CDECL fci_close( INT_PTR hf, int *err, void *ptr )
{
    if (!CloseHandle( (HANDLE)hf ))
    {
        *err = GetLastError();
        return -1;
    }
    return 0;
}

static LONG CDECL fci_lseek( INT_PTR hf, LONG dist, int seektype, int *err, void *ptr )
{
    DWORD ret;

    ret = SetFilePointer( (HANDLE)hf, dist, NULL, seektype );
    if (ret == INVALID_SET_FILE_POINTER && GetLastError())
    {
        *err = GetLastError();
        return -1;
    }
    return ret;
}

static int CDECL fci_delete( char *file, int *err, void *ptr )
{
    if (!DeleteFileA( file ))
    {
        *err = GetLastError();
        return -1;
    }
    return 0;
}

static BOOL CDECL fci_get_temp( char *name, int size, void *ptr )
{
    char path[MAX_PATH];

    if (!GetTempPathA( MAX_PATH, path )) return FALSE;
    if (!GetTempFileNameA( path, "cab", 0, name )) return FALSE;
    DeleteFileA( name );
    return TRUE;
}

static BOOL CDECL fci_get_next_cab( CCAB *cab, ULONG prev_size, void *ptr )
{
    return format_cab_name( cab->szCab, cab->iCab + 1, opt_cab_file );
}

static LONG CDECL fci_status( UINT type, ULONG cb1, ULONG cb2, void *ptr )
{
    return 0;
}

static INT_PTR CDECL fci_get_open_info( char *name, USHORT *date, USHORT *time,
                                        USHORT *attribs, int *err, void *ptr )
{
    HANDLE handle;
    BY_HANDLE_FILE_INFORMATION info;
    WCHAR *p, *nameW = strdupAtoW( CP_UTF8, name );

    handle = CreateFileW( nameW, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
                          NULL, OPEN_EXISTING, 0, NULL );
    if (handle == INVALID_HANDLE_VALUE)
    {
        *err = GetLastError();
        WINE_ERR( "failed to open %s: error %u\n", wine_dbgstr_w(nameW), *err );
        cab_free( nameW );
        return -1;
    }
    if (!GetFileInformationByHandle( handle, &info ))
    {
        *err = GetLastError();
        CloseHandle( handle );
        cab_free( nameW );
        return -1;
    }
    FileTimeToDosDateTime( &info.ftLastWriteTime, date, time );
    *attribs = info.dwFileAttributes & (_A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_ARCH);
    for (p = nameW; *p; p++) if (*p >= 0x80) break;
    if (*p) *attribs |= _A_NAME_IS_UTF;
    cab_free( nameW );
    return (INT_PTR)handle;
}

static INT_PTR CDECL fdi_open( char *file, int oflag, int pmode )
{
    int err;
    return fci_open( file, oflag, pmode, &err, NULL );
}

static UINT CDECL fdi_read( INT_PTR hf, void *pv, UINT cb )
{
    int err;
    return fci_read( hf, pv, cb, &err, NULL );
}

static UINT CDECL fdi_write( INT_PTR hf, void *pv, UINT cb )
{
    int err;
    return fci_write( hf, pv, cb, &err, NULL );
}

static int CDECL fdi_close( INT_PTR hf )
{
    int err;
    return fci_close( hf, &err, NULL );
}

static LONG CDECL fdi_lseek( INT_PTR hf, LONG dist, int whence )
{
    int err;
    return fci_lseek( hf, dist, whence, &err, NULL );
}


/* create directories leading to a given file */
static void create_directories( const WCHAR *name )
{
    WCHAR *path, *p;

    /* create the directory/directories */
    path = cab_alloc( (strlenW(name) + 1) * sizeof(WCHAR) );
    strcpyW(path, name);

    p = strchrW(path, '\\');
    while (p != NULL)
    {
        *p = 0;
        if (!CreateDirectoryW( path, NULL ))
            WINE_TRACE("Couldn't create directory %s - error: %d\n", wine_dbgstr_w(path), GetLastError());
        *p = '\\';
        p = strchrW(p+1, '\\');
    }
    cab_free( path );
}

/* check if file name matches against one of the files specification */
static BOOL match_files( const WCHAR *name )
{
    int i;

    if (!*opt_files) return TRUE;
    for (i = 0; opt_files[i]; i++)
    {
        unsigned int len = strlenW( opt_files[i] );
        /* FIXME: do smarter matching, and wildcards */
        if (!len) continue;
        if (strncmpiW( name, opt_files[i], len )) continue;
        if (opt_files[i][len - 1] == '\\' || !name[len] || name[len] == '\\') return TRUE;
    }
    return FALSE;
}

static INT_PTR CDECL list_notify( FDINOTIFICATIONTYPE fdint, PFDINOTIFICATION pfdin )
{
    WCHAR *nameW;

    switch (fdint)
    {
    case fdintCABINET_INFO:
        return 0;
    case fdintCOPY_FILE:
        nameW = strdupAtoW( (pfdin->attribs & _A_NAME_IS_UTF) ? CP_UTF8 : CP_ACP, pfdin->psz1 );
        if (match_files( nameW ))
        {
            char *nameU = strdupWtoA( CP_UNIXCP, nameW );
            if (opt_verbose)
            {
                char attrs[] = "rxash";
                if (!(pfdin->attribs & _A_RDONLY)) attrs[0] = '-';
                if (!(pfdin->attribs & _A_EXEC))   attrs[1] = '-';
                if (!(pfdin->attribs & _A_ARCH))   attrs[2] = '-';
                if (!(pfdin->attribs & _A_SYSTEM)) attrs[3] = '-';
                if (!(pfdin->attribs & _A_HIDDEN)) attrs[4] = '-';
                printf( " %s %9u  %04u/%02u/%02u %02u:%02u:%02u  ", attrs, pfdin->cb,
                        (pfdin->date >> 9) + 1980, (pfdin->date >> 5) & 0x0f, pfdin->date & 0x1f,
                        pfdin->time >> 11, (pfdin->time >> 5) & 0x3f, (pfdin->time & 0x1f) * 2 );
            }
            printf( "%s\n", nameU );
            cab_free( nameU );
        }
        cab_free( nameW );
        return 0;
    default:
        WINE_FIXME( "Unexpected notification type %d.\n", fdint );
        return 0;
    }
}

static int list_cabinet( char *cab_dir )
{
    ERF erf;
    int ret = 0;
    HFDI fdi = FDICreate( cab_alloc, cab_free, fdi_open, fdi_read,
                          fdi_write, fdi_close, fdi_lseek, cpuUNKNOWN, &erf );

    if (!FDICopy( fdi, opt_cab_file, cab_dir, 0, list_notify, NULL, NULL )) ret = GetLastError();
    FDIDestroy( fdi );
    return ret;
}

static INT_PTR CDECL extract_notify( FDINOTIFICATIONTYPE fdint, PFDINOTIFICATION pfdin )
{
    WCHAR *file, *nameW, *path = NULL;
    INT_PTR ret;

    switch (fdint)
    {
    case fdintCABINET_INFO:
        return 0;

    case fdintCOPY_FILE:
        nameW = strdupAtoW( (pfdin->attribs & _A_NAME_IS_UTF) ? CP_UTF8 : CP_ACP, pfdin->psz1 );
        if (opt_preserve_paths)
        {
            file = nameW;
            while (*file == '\\') file++;  /* remove leading backslashes */
        }
        else
        {
            if ((file = strrchrW( nameW, '\\' ))) file++;
            else file = nameW;
        }

        if (opt_dest_dir)
        {
            path = cab_alloc( (strlenW(opt_dest_dir) + strlenW(file) + 1) * sizeof(WCHAR) );
            strcpyW( path, opt_dest_dir );
            strcatW( path, file );
        }
        else path = file;

        if (match_files( file ))
        {
            if (opt_verbose)
            {
                char *nameU = strdupWtoA( CP_UNIXCP, path );
                printf( "extracting %s\n", nameU );
                cab_free( nameU );
            }
            create_directories( path );
            /* FIXME: check for existing file and overwrite mode */
            ret = (INT_PTR)CreateFileW( path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
                                        NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
        }
        else ret = 0;

        cab_free( nameW );
        if (path != file) cab_free( path );
        return ret;

    case fdintCLOSE_FILE_INFO:
        CloseHandle( (HANDLE)pfdin->hf );
        return 0;

    case fdintNEXT_CABINET:
        WINE_TRACE("Next cab: status %u, path '%s', file '%s'\n", pfdin->fdie, pfdin->psz3, pfdin->psz1);
        return pfdin->fdie == FDIERROR_NONE ? 0 : -1;

    case fdintENUMERATE:
        return 0;

    default:
        WINE_FIXME( "Unexpected notification type %d.\n", fdint );
        return 0;
    }
}

static int extract_cabinet( char *cab_dir )
{
    ERF erf;
    int ret = 0;
    HFDI fdi = FDICreate( cab_alloc, cab_free, fdi_open, fdi_read,
                          fdi_write, fdi_close, fdi_lseek, cpuUNKNOWN, &erf );

    if (!FDICopy( fdi, opt_cab_file, cab_dir, 0, extract_notify, NULL, NULL ))
    {
        ret = GetLastError();
        WINE_WARN("FDICopy() failed: code %u\n", ret);
    }
    FDIDestroy( fdi );
    return ret;
}

static BOOL add_file( HFCI fci, WCHAR *name )
{
    BOOL ret;
    char *filename, *path = strdupWtoA( CP_UTF8, name );

    if (!opt_preserve_paths)
    {
        if ((filename = strrchr( path, '\\' ))) filename++;
        else filename = path;
    }
    else
    {
        filename = path;
        while (*filename == '\\') filename++;  /* remove leading backslashes */
    }
    ret = FCIAddFile( fci, path, filename, FALSE,
                      fci_get_next_cab, fci_status, fci_get_open_info, opt_compression );
    cab_free( path );
    return ret;
}

static BOOL add_directory( HFCI fci, WCHAR *dir )
{
    static const WCHAR wildcardW[] = {'*',0};
    WCHAR *p, *buffer;
    HANDLE handle;
    WIN32_FIND_DATAW data;
    BOOL ret = TRUE;

    if (!(buffer = cab_alloc( (strlenW(dir) + MAX_PATH + 2) * sizeof(WCHAR) ))) return FALSE;
    strcpyW( buffer, dir );
    p = buffer + strlenW( buffer );
    if (p > buffer && p[-1] != '\\') *p++ = '\\';
    strcpyW( p, wildcardW );

    if ((handle = FindFirstFileW( buffer, &data )) != INVALID_HANDLE_VALUE)
    {
        do
        {
            if (data.cFileName[0] == '.' && !data.cFileName[1]) continue;
            if (data.cFileName[0] == '.' && data.cFileName[1] == '.' && !data.cFileName[2]) continue;
            if (data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) continue;

            strcpyW( p, data.cFileName );
            if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                ret = add_directory( fci, buffer );
            else
                ret = add_file( fci, buffer );
            if (!ret) break;
        } while (FindNextFileW( handle, &data ));
        FindClose( handle );
    }
    cab_free( buffer );
    return TRUE;
}

static BOOL add_file_or_directory( HFCI fci, WCHAR *name )
{
    DWORD attr = GetFileAttributesW( name );

    if (attr == INVALID_FILE_ATTRIBUTES)
    {
        WINE_MESSAGE( "cannot open %s\n", wine_dbgstr_w(name) );
        return FALSE;
    }
    if (attr & FILE_ATTRIBUTE_DIRECTORY)
    {
        if (opt_recurse) return add_directory( fci, name );
        WINE_MESSAGE( "cabarc: Cannot add %s, it's a directory (use -r for recursive add)\n",
                      wine_dbgstr_w(name) );
        return FALSE;
    }
    return add_file( fci, name );
}

static int new_cabinet( char *cab_dir )
{
    static const WCHAR plusW[] = {'+',0};
    WCHAR **file;
    ERF erf;
    BOOL ret = FALSE;
    HFCI fci;
    CCAB cab;

    cab.cb                = opt_cabinet_size;
    cab.cbFolderThresh    = CB_MAX_DISK;
    cab.cbReserveCFHeader = opt_reserve_space;
    cab.cbReserveCFFolder = 0;
    cab.cbReserveCFData   = 0;
    cab.iCab              = 0;
    cab.iDisk             = 0;
    cab.setID             = opt_cabinet_id;
    cab.szDisk[0]         = 0;

    strcpy( cab.szCabPath, cab_dir );
    strcat( cab.szCabPath, "\\" );
    format_cab_name( cab.szCab, 1, opt_cab_file );

    fci = FCICreate( &erf, fci_file_placed, cab_alloc, cab_free,fci_open, fci_read,
                     fci_write, fci_close, fci_lseek, fci_delete, fci_get_temp, &cab, NULL );

    for (file = opt_files; *file; file++)
    {
        if (!strcmpW( *file, plusW ))
            FCIFlushFolder( fci, fci_get_next_cab, fci_status );
        else
            if (!(ret = add_file_or_directory( fci, *file ))) break;
    }

    if (ret)
    {
        if (!(ret = FCIFlushCabinet( fci, FALSE, fci_get_next_cab, fci_status )))
            WINE_MESSAGE( "cabarc: Failed to create cabinet %s\n", wine_dbgstr_a(opt_cab_file) );
    }
    FCIDestroy( fci );
    return !ret;
}

static void usage( void )
{
    WINE_MESSAGE(
        "Usage: cabarc [options] command file.cab [files...] [dest_dir\\]\n"
        "\nCommands:\n"
        "   L   List the contents of the cabinet\n"
        "   N   Create a new cabinet\n"
        "   X   Extract files from the cabinet into dest_dir\n"
        "\nOptions:\n"
        "  -d size  Set maximum disk size\n"
        "  -h       Display this help\n"
        "  -i id    Set cabinet id\n"
        "  -m type  Set compression type (mszip|none)\n"
        "  -p       Preserve directory names\n"
        "  -r       Recurse into directories\n"
        "  -s size  Reserve space in the cabinet header\n"
        "  -v       More verbose output\n" );
}

int wmain( int argc, WCHAR *argv[] )
{
    static const WCHAR noneW[] = {'n','o','n','e',0};
    static const WCHAR mszipW[] = {'m','s','z','i','p',0};

    WCHAR *p, *command;
    char buffer[MAX_PATH];
    char filename[MAX_PATH];
    char *cab_file, *file_part;
    int i;

    while (argv[1] && argv[1][0] == '-')
    {
        switch (argv[1][1])
        {
        case 'd':
            argv++; argc--;
            opt_cabinet_size = atoiW( argv[1] );
            if (opt_cabinet_size < 50000)
            {
                WINE_MESSAGE( "cabarc: Cabinet size must be at least 50000\n" );
                return 1;
            }
            break;
        case 'h':
            usage();
            return 0;
        case 'i':
            argv++; argc--;
            opt_cabinet_id = atoiW( argv[1] );
            break;
        case 'm':
            argv++; argc--;
            if (!strcmpiW( argv[1], noneW )) opt_compression = tcompTYPE_NONE;
            else if (!strcmpiW( argv[1], mszipW )) opt_compression = tcompTYPE_MSZIP;
            else
            {
                char *arg = strdupWtoA( CP_ACP, argv[1] );
                WINE_MESSAGE( "cabarc: Unknown compression type '%s'\n", arg);
                return 1;
            }
            break;
        case 'p':
            opt_preserve_paths = TRUE;
            break;
        case 'r':
            opt_recurse = TRUE;
            break;
        case 's':
            argv++; argc--;
            opt_reserve_space = atoiW( argv[1] );
            break;
        case 'v':
            opt_verbose++;
            break;
        default:
            usage();
            return 1;
        }
        argv++; argc--;
    }

    command = argv[1];
    if (argc < 3 || !command[0] || command[1])
    {
        usage();
        return 1;
    }
    cab_file = strdupWtoA( CP_ACP, argv[2] );
    argv += 2;
    argc -= 2;

    if (!GetFullPathNameA( cab_file, MAX_PATH, buffer, &file_part ) || !file_part)
    {
        WINE_ERR( "cannot get full name for %s\n", wine_dbgstr_a( cab_file ));
        return 1;
    }
    strcpy(filename, file_part);
    file_part[0] = 0;

    /* map slash to backslash in all file arguments */
    for (i = 1; i < argc; i++)
        for (p = argv[i]; *p; p++)
            if (*p == '/') *p = '\\';
    opt_files = argv + 1;
    opt_cab_file = filename;

    switch (*command)
    {
    case 'l':
    case 'L':
        return list_cabinet( buffer );
    case 'n':
    case 'N':
        return new_cabinet( buffer );
    case 'x':
    case 'X':
        if (argc > 1)  /* check for destination dir as last argument */
        {
            WCHAR *last = argv[argc - 1];
            if (last[0] && last[strlenW(last) - 1] == '\\')
            {
                opt_dest_dir = last;
                argv[--argc] = NULL;
            }
        }
        WINE_TRACE("Extracting file(s) from cabinet %s\n", wine_dbgstr_a(cab_file));
        return extract_cabinet( buffer );
    default:
        usage();
        return 1;
    }
}
