/*****************************************************************************
 * Author:   Valient Gough <vgough@pobox.com>
 *
 *****************************************************************************
 * Copyright (c) 2004, Valient Gough
 * 
 * This program is free software; you can distribute it and/or modify it under 
 * the terms of the GNU General Public License (GPL), as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 General Public License for
 * more details.
 */


#include "encfs.h"

#include "config.h"

#include "FileUtils.h"
#include "Cipher.h"

#include <rlog/rlog.h>
#include <rlog/StdioNode.h>
#include <rlog/RLogChannel.h>

#include <iostream>
#include <string>

#include "i18n.h"

#ifdef HAVE_SSL
#define NO_DES
#include <openssl/ssl.h>
#endif

using namespace rlog;
using namespace std;


static int showInfo( int argc, char **argv );
static int showVersion( int argc, char **argv );
static int chpasswd( int argc, char **argv );
static int cmd_ls( int argc, char **argv );
static int cmd_decode( int argc, char **argv );
static int cmd_showcruft( int argc, char **argv );

struct CommandOpts
{
    const char *name;
    int minOptions;
    int maxOptions;
    int (*func)(int argc, char **argv);
    const char *argStr;
    const char *usageStr;
} commands[] = 
{
    {"info", 1, 1, showInfo, "(root dir)", 
	// xgroup(usage)
	gettext_noop("  -- show information (Default command)")},
    {"passwd", 1, 1, chpasswd, "(root dir)",
	// xgroup(usage)
	gettext_noop("  -- change password for volume")},
    {"ls", 1, 2, cmd_ls, 0,0},
    {"showcruft", 1, 1, cmd_showcruft, "(root dir)",
	// xgroup(usage)
        gettext_noop("  -- show undecodable filenames in the volume")},
    {"decode", 1, 2, cmd_decode, "(root dir) encoded-name",
	// xgroup(usage)
        gettext_noop("  -- decodes name and prints plaintext version")}, 
    {"--version", 0, 0, showVersion, "", 
	// xgroup(usage)
	gettext_noop("  -- print version number and exit")},
    {0,0,0,0,0,0}
};



static
void usage(const char *name)
{
    cerr << autosprintf(_("encfsctl version %s"), VERSION) << "\n"
	<< _("Usage:\n") 
	// displays usage commands, eg "./encfs (root dir) ..."
	// xgroup(usage)
	<< autosprintf(_("%s (root dir)\n"
	"  -- displays information about the filesystem, or \n"), name);

    int offset = 0;
    while(commands[offset].name != 0)
    {
	if( commands[offset].argStr != 0 )
	{
	    cerr << name << " " << commands[offset].name << " " 
		<< commands[offset].argStr << "\n"
		<< gettext( commands[offset].usageStr ) << "\n";
	}
	++offset;
    }

    cerr << "\n"
	// xgroup(usage)
	<< autosprintf(_("Example: \n%s info ~/.crypt\n"), name)
	<< "\n";
}

static bool checkDir( string &rootDir )
{
    if( !isDirectory( rootDir.c_str() ))
    {
	cerr << autosprintf(_("directory %s does not exist.\n"),
		rootDir.c_str());
	return false;
    }
    if(rootDir[ rootDir.length()-1 ] != '/')
	rootDir.append("/");

    return true;
}

static int showVersion( int argc, char **argv )
{
    (void)argc;
    (void)argv;
    // xgroup(usage)
    cerr << autosprintf(_("encfsctl version %s"), VERSION) << "\n";

    return EXIT_SUCCESS;
}

static int showInfo( int argc, char **argv )
{
    (void)argc;
    string rootDir = argv[1];
    if( !checkDir( rootDir ))
	return EXIT_FAILURE;

    EncFSConfig config;
    ConfigType type = readConfig( rootDir, &config );

    // show information stored in config..
    switch(type)
    {
    case Config_None:
	// xgroup(diag)
	cout << _("Unable to load or parse config file\n");
	return EXIT_FAILURE;
    case Config_Prehistoric:
	// xgroup(diag)
	cout << _("A really old EncFS filesystem was found. \n"
	    "It is not supported in this EncFS build.\n");
	return EXIT_FAILURE;
    case Config_V3:
	// xgroup(diag)
	cout << "\n" << autosprintf(_("Version 3 configuration; "
	    "created by %s\n"), config.creator.c_str());
	break;
    case Config_V4:
	// xgroup(diag)
	cout << "\n" << autosprintf(_("Version 4 configuration; "
	    "created by %s\n"), config.creator.c_str());
	break;
    case Config_V5:
	// xgroup(diag)
	cout << "\n" << autosprintf(_("Version 5 configuration; "
	    "created by %s (revision %i)\n"), config.creator.c_str(),
		config.subVersion);
	break;
    }

    showFSInfo( config );

    return EXIT_SUCCESS;
}

static int cmd_decode( int argc, char **argv )
{
    string rootDir = argv[1];
    if(!checkDir( rootDir ))
	return EXIT_FAILURE;

    const bool idleTracking = false;
    const bool forceCreate = false;
    const bool checkKey = false;
    const bool forceDecode = false;
    Ptr<EncFS_Root> rootInfo = initFS( rootDir, idleTracking, forceCreate,
	    checkKey, forceDecode, string());

    if(rootInfo.isNull())
    {
	// xgroup(diag)
	cerr << _("Unable to initialize encrypted filesystem - check path.\n");
	return EXIT_FAILURE;
    }

    if( argc > 2 )
    {
	string name = rootInfo->root->plainPath( argv[2] );
	cout << name << "\n";
    } else
    {
	do
	{
	    string name;
	    cin >> name;
	    if(name.empty())
		break;

	    name = rootInfo->root->plainPath( name.c_str() );
	    cout << name << "\n";
	} while(1);
    }
    return EXIT_SUCCESS;
}

static int cmd_ls( int argc, char **argv )
{
    (void)argc;
    string rootDir = argv[1];
    if(!checkDir( rootDir ))
	return EXIT_FAILURE;

    const bool idleTracking = false;
    const bool forceCreate = false;
    const bool checkKey = false;
    const bool forceDecode = false;
    Ptr<EncFS_Root> rootInfo = initFS( rootDir, idleTracking, forceCreate,
	    checkKey, forceDecode, string());

    if(rootInfo.isNull())
    {
	cerr << _("Unable to initialize encrypted filesystem - check path.\n");
	return EXIT_FAILURE;
    }

    // show files in directory
    {
	DirTraverse dt = rootInfo->root->openDir("/");
	if(dt.valid())
	{
	    for(string name = dt.nextPlaintextName(); !name.empty(); 
		    name = dt.nextPlaintextName())
	    {
		Ptr<FileNode> fnode = 
		    rootInfo->root->lookupNode( name.c_str(), "encfsctl-ls" );
		struct stat stbuf;
		fnode->getAttr( &stbuf );

		struct tm stm;
		localtime_r( &stbuf.st_mtime, &stm );
		stm.tm_year += 1900;
		// TODO: when I add "%s" to the end and name.c_str(), I get a
		// seg fault from within strlen.  Why ???
		printf("%11i %4i-%02i-%02i %02i:%02i:%02i %s\n",
			int(stbuf.st_size), 
			int(stm.tm_year), int(stm.tm_mon), int(stm.tm_mday),
			int(stm.tm_hour), int(stm.tm_min), int(stm.tm_sec),
			name.c_str());
	    }
	}
    }

    return EXIT_SUCCESS;
}


int showcruft( const Ptr<EncFS_Root> &rootInfo, const char *dirName )
{
    int found = 0;
    DirTraverse dt = rootInfo->root->openDir( dirName );
    if(dt.valid())
    {
	bool showedDir = false;
	for(string name = dt.nextInvalid(); !name.empty(); 
		name = dt.nextInvalid())
	{
	    string cpath = rootInfo->root->cipherPath( dirName );
	    cpath += '/';
	    cpath += name;

	    if(!showedDir)
	    {
		// just before showing a list of files in a directory
		cout << autosprintf(_("In directory %s: \n"), dirName);
		showedDir = true;
	    }
	    ++found;
	    cout << cpath << "\n";
	}

	// now go back and look for directories to recurse into..
	dt = rootInfo->root->openDir( dirName );
	if(dt.valid())
	{
	    for(string name = dt.nextPlaintextName(); !name.empty(); 
		    name = dt.nextPlaintextName())
	    {
		if( name == "." || name == "..")
		    continue;

		string plainPath = dirName;
		plainPath += '/';
		plainPath += name;

		string cpath = rootInfo->root->cipherPath( plainPath.c_str() );

		if(isDirectory( cpath.c_str() ))
		    found += showcruft( rootInfo, plainPath.c_str() );
	    }
	}
    }

    return found;
}

/*
    iterate recursively through the filesystem and print out names of files
    which have filenames which cannot be decoded with the given key..
*/
static int cmd_showcruft( int argc, char **argv )
{
    (void)argc;
    string rootDir = argv[1];
    if(!checkDir( rootDir ))
	return EXIT_FAILURE;

    const bool idleTracking = false;
    const bool forceCreate = false;
    const bool checkKey = false;
    const bool forceDecode = false;
    Ptr<EncFS_Root> rootInfo = initFS( rootDir, idleTracking, forceCreate,
	    checkKey, forceDecode, string());

    if(rootInfo.isNull())
    {
	cerr << _("Unable to initialize encrypted filesystem - check path.\n");
	return EXIT_FAILURE;
    }

    int filesFound = showcruft( rootInfo, "/" );

    cerr << autosprintf(
	    ngettext("Found %i invalid file.", "Found %i invalid files.", 
		filesFound), filesFound) << "\n";

    return EXIT_SUCCESS;
}

static int chpasswd( int argc, char **argv )
{
    (void)argc;
    string rootDir = argv[1];
    if( !checkDir( rootDir ))
	return EXIT_FAILURE;

    EncFSConfig config;
    ConfigType cfgType = readConfig( rootDir, &config );

    if(cfgType == Config_None)
    {
	cout << _("Unable to load or parse config file\n");
	return EXIT_FAILURE;
    }

    // instanciate proper cipher
    Ptr<Cipher> cipher = Cipher::New( config.cipherIface, config.keySize );
    if(cipher.isNull())
    {
	cout << autosprintf(_("Unable to find specified cipher \"%s\"\n"),
		config.cipherIface.name().c_str());
	return EXIT_FAILURE;
    }

    // ask for existing password
    cout << _("Enter current Encfs password\n");
    CipherKey userKey = getUserKey( cipher );
    if(userKey.isNull())
	return EXIT_FAILURE;

    rAssert( (int)config.keyData.length() == cipher->encodedKeySize() );


    // decode volume key using user key -- at this point we detect an incorrect
    // password if the key checksum does not match (causing readKey to fail).
    CipherKey volumeKey = cipher->readKey(
	    (unsigned char*)config.keyData.data(), userKey );

    if(volumeKey.isNull())
    {
	cout << _("Invalid password\n");
	return EXIT_FAILURE;
    }

    // Now, get New user key..
    userKey.reset();
    cout << _("Enter new Encfs password\n");
    userKey = getNewUserKey( cipher );

    // re-encode the volume key using the new user key and write it out..
    int result = EXIT_FAILURE;
    if(!userKey.isNull())
    {
	int encodedKeySize = cipher->encodedKeySize();
	unsigned char *keyBuf = new unsigned char[ encodedKeySize ];

	// encode volume key with new user key
	cipher->writeKey( volumeKey, keyBuf, userKey );
	userKey.reset();

	config.keyData.assign( (char*)keyBuf, encodedKeySize );
	delete[] keyBuf;

	if(saveConfig( cfgType, rootDir, &config ))
	{
	    // password modified -- changes volume key of filesystem..
	    cout << _("Volume Key successfully updated.\n");
	    result = EXIT_SUCCESS;
	} else
	{
	    cout << _("Error saving modified config file.\n");
	}
    }

    volumeKey.reset();

    return result;
}


int main(int argc, char **argv)
{
    RLogInit( argc, argv );

#ifdef LOCALEDIR
    setlocale( LC_ALL, "" );
    bindtextdomain( PACKAGE, LOCALEDIR );
    textdomain( PACKAGE );
#endif

#ifdef HAVE_SSL
    SSL_load_error_strings();
    SSL_library_init();
#endif

    StdioNode *slog = new StdioNode( STDERR_FILENO );
    slog->subscribeTo( GetGlobalChannel("error") );
    slog->subscribeTo( GetGlobalChannel("warning") );
#ifndef NO_DEBUG
    slog->subscribeTo( GetGlobalChannel("debug") );
#endif

    if(argc < 2)
    {
	usage( argv[0] );
	return EXIT_FAILURE;
    }

    if(argc == 2 && !(*argv[1] == '-' && *(argv[1]+1) == '-'))
    {
	// default command when only 1 argument given -- treat the argument as
	// a directory..
	return showInfo( argc, argv );
    } else
    {
	// find the specified command
	int offset = 0;
	while(commands[offset].name != 0)
	{
	    if(!strcmp( argv[1], commands[offset].name ))
		break;
	    ++offset;
	}

	if(commands[offset].name == 0)
	{
	    cerr << autosprintf(_("invalid command: \"%s\""), argv[1]) << endl;
	} else
	{
	    if((argc-2 < commands[offset].minOptions) || 
		    (argc-2 > commands[offset].maxOptions))
	    {
		cerr << autosprintf(
			_("Incorrect number of arguments for command \"%s\""), 
			argv[1]) << endl;
	    } else
		return (*commands[offset].func)( argc-1, argv+1 );
	}
    }

    return EXIT_FAILURE;
}


