/*
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 1, 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.

** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/*
 * Author : Alexandre Parenteau <aubonbeurre@hotmail.com> --- December 1997
 */

/*
 * TextBinary.cpp --- utilities to ckeck if files are binary or text
 */

#include "stdafx.h"

#ifdef WIN32
#	include <sys/stat.h>
#	include <direct.h>
#	include <io.h>
#endif /* WIN32 */

#ifdef TARGET_OS_MAC
#	include <Resources.h>
#	include <unistd.h>
#	include <sys/types.h>
#	include <sys/stat.h>
#	include "MacMisc.h"
#endif /* TARGET_OS_MAC */

#ifdef HAVE_UNISTD_H
#	include <unistd.h>
#endif

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

#include "TextBinary.h"
#include "FileTraversal.h"
#include "AppConsole.h"
#include "CPStr.h"
#include "CvsPrefs.h"

#if !defined(S_ISLNK) && defined(S_IFLNK)
#	if defined(S_IFMT)
#		define	S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#	else
#		define S_ISLNK(m) ((m) & S_IFLNK)
#	endif
#endif

#define MAX_TEST_SIZE 100000

#ifdef WIN32
#	ifdef _DEBUG
#	define new DEBUG_NEW
#	undef THIS_FILE
	static char THIS_FILE[] = __FILE__;
#	endif
#endif /* WIN32 */

static CStaticAllocT<char> sBuf;
static int sBufSize;

static kTextBinTYPE FillBuffer(const char *arg, const char *dir, const UFSSpec * spec, bool bin)
{
	sBuf.AdjustSize(MAX_TEST_SIZE);
	
	if(chdir(dir) != 0)
	{
		cvs_err("Unable to chdir to ""%s"" (error %d)\n", dir, errno);
		return kFileMissing;
	}
	
	FILE *samp = fopen(arg, bin ? "rb" : "r");
	if(samp == 0L)
	{
		return kFileMissing;
	}
	
	sBufSize = fread(sBuf, sizeof(char), MAX_TEST_SIZE, samp);
	if(sBufSize < 0)
	{
		cvs_err("Unable to read ""%s"" (error %d)\n", arg, errno);
		return kFileMissing;
	}
	
	fclose(samp);
	return kFileIsOK;
}

static kTextBinTYPE TestBuffer(const char *arg, const char *dir, const UFSSpec * spec, kFileType expectType)
{
	if(sBufSize == 0)
		return kFileIsOK;
	
	bool isUnicode = false;
#ifdef WIN32
	isUnicode = gCvsPrefs.WhichVersion() == kWin32 ? false : IsTextUnicode(sBuf, sBufSize, NULL) == TRUE;
#endif /* WIN32 */

	// Windows : fread skips the \r, so we don't care.
	// Mac : MSL exchanges when reading the \r and \n,
	// so we look for \n
	// Others : default to \n
	
	const unsigned char nativeNL = '\n';
	const unsigned char nonnativeNL = '\r';
	
	float percent = 0;
	int numBinChars = 0;
	int numTextChars = 0;

	if( !isUnicode )
	{
		// figure if it is binary
		int i;
		unsigned char c;
		char *tmp;
		
		for(tmp = sBuf, i = 0; i < sBufSize; i++)
		{
			c = (unsigned char)*tmp++;
			if((c < 0x20 && c != nativeNL && c != nonnativeNL && c != '\t') || (c >= 0x80
#ifdef TARGET_OS_MAC
				&& c != (unsigned char)'Ñ' && c != (unsigned char)'¥' /* powerplant */
#endif /* TARGET_OS_MAC */		
				))
				numBinChars++;
			else
				numTextChars++;
		}
		
		percent = (float)numBinChars / (float)sBufSize;
	}
	
	// more than 5% binary makes binary
	bool isbin = percent >= 0.05;

	switch( expectType )
	{
	case kFileTypeText:
		if( isUnicode )
		{
			return kTextIsUnicode;
		}
		else if( isbin )
		{
			return kTextIsBinary;
		}
		break;
	case kFileTypeBin:
		if( isUnicode )
		{
			return kBinIsUnicode;
		}
		else if( !isbin )
		{
			return kBinIsText;
		}
		break;
	case kFileTypeUnicode:
		if( !isUnicode )
		{
			return isbin ? kUnicodeIsBinary : kUnicodeIsText;
		}
		break;
	default:
#ifdef WIN32
		ASSERT(FALSE); // uknown type
#endif
		break;
	}

	// for the text files, check if the new lines are good
	if(kFileTypeText == expectType)
	{
		int i;
		unsigned char c;
		char *tmp;
		int numNonNative = 0, numNative = 0;
		for(tmp = sBuf, i = 0; i < sBufSize; i++)
		{
			c = (unsigned char)*tmp++;
			if(c == nonnativeNL)
				numNonNative++;
			else if(c == nativeNL)
				numNative++;
		}
		if(numNative == 0)
		{
			if(numNonNative > 0)
				return kTextWrongLF;
		}
		else
		{
			percent = (float)numNonNative / (float)numNative;
			if(percent >= 0.1)
				return kTextWrongLF;
		}
		
		if(numBinChars > 0)
			return kTextEscapeChar;
	}
	
	return kFileIsOK;
}

#if TARGET_RT_MAC_CFM
static kTextBinTYPE GetMacSig(const char *arg, const char *dir, const UFSSpec * spec, bool expectbin)
{
	CInfoPBRec info;

	info.dirInfo.ioVRefNum 		= spec->vRefNum;
	info.dirInfo.ioDrDirID 		= spec->parID;
	info.dirInfo.ioNamePtr 		= (StringPtr)spec->name;
	info.dirInfo.ioFDirIndex 	= 0;
	info.dirInfo.ioACUser 		= 0;
		
	OSErr err = PBGetCatInfoSync(&info);
	if(err != noErr || (info.dirInfo.ioFlAttrib & 0x10)/*folder*/)
	{
		cvs_err("Unable to get info on ""%s"" (error %d)\n", arg, err);
		return kFileMissing;
	}
	
	if(info.hFileInfo.ioFlFndrInfo.fdFlags & (1 << 15))
	{
		return kFileIsAlias;
	}
	
	if(expectbin)
	{
		if(info.hFileInfo.ioFlFndrInfo.fdType == 'TEXT')
			return kBinWrongSig;
	}
	else
	{
		if(info.hFileInfo.ioFlRLgLen > 0 && info.hFileInfo.ioFlLgLen == 0)
			return kTextIsBinary;
			
		
		if(info.hFileInfo.ioFlFndrInfo.fdType != 'TEXT')
			return kTextWrongSig;
	}
	
	return kFileIsOK;
}
#endif /* TARGET_RT_MAC_CFM */

#if TARGET_RT_MAC_MACHO
static kTextBinTYPE GetMacSig(const char *arg, const char *dir, const UFSSpec * spec, bool expectbin)
{
	FSCatalogInfo info;
	FInfo finfo;
	
	OSErr err = FSGetCatalogInfo(spec, kFSCatInfoNodeFlags | kFSCatInfoFinderInfo |
		kFSCatInfoDataSizes | kFSCatInfoRsrcSizes, &info, NULL, NULL, NULL);
	if(err != noErr || (info.nodeFlags & kFSNodeIsDirectoryMask))
	{
		cvs_err("Unable to get info on ""%s"" (error %d)\n", arg, err);
		return kFileMissing;
	}
	
	memcpy(&finfo, &info.finderInfo, 2 * sizeof(OSType));
	
	if(expectbin)
	{
		if(finfo.fdType == 'TEXT')
			return kBinWrongSig;
	}
	else
	{
		if(info.rsrcPhysicalSize > 0 && info.dataPhysicalSize == 0)
			return kTextIsBinary;
			
		if(finfo.fdType != 'TEXT' && finfo.fdType != 0)
			return kTextWrongSig;
	}
	
	return kFileIsOK;
}
#endif /* TARGET_RT_MAC_MACHO */


static kTextBinTYPE TestValidName(const char *filename)
{
	kTextBinTYPE result = kFileIsOK;

	if(strchr(filename, '/') != 0L)
		result = kFileInvalidName;
	if(strchr(filename, '\n') != 0L)
		result = kFileInvalidName;

#ifdef TARGET_OS_MAC
	if(strchr(filename, '\r') != 0L)
		result = kFileInvalidName;
	if(strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0)
		result = kFileInvalidName;
	if(strcmp(filename, "CVS") != 0 && stricmp(filename, "cvs") == 0)
		result = kFileInvalidName;
#endif /* !TARGET_OS_MAC */

	return result;
}

static kTextBinTYPE TestFileIsAlias(const char *arg, const char *dir, const UFSSpec * spec)
{
	kTextBinTYPE result = kFileIsOK;

#ifdef S_ISLNK
	CPStr fullname;
	struct stat sb;

	fullname = dir;
	if(!fullname.endsWith(kPathDelimiter))
		fullname << kPathDelimiter;
	fullname << arg;

	if (stat(fullname, &sb) == -1)
		result = kFileMissing;
	else if(S_ISLNK(sb.st_mode))
		result = kFileIsAlias;
#endif /* S_ISLNK */

	return result;
}

kTextBinTYPE FileIsText(const char *arg, const char *dir, const UFSSpec * spec)
{
	kTextBinTYPE state;

#ifdef TARGET_OS_MAC
	state = GetMacSig(arg, dir, spec, false);
	if(state != kFileIsOK)
		return state;
#endif /* TARGET_OS_MAC */
	
	state = FillBuffer(arg, dir, spec, false);
	if(state != kFileIsOK)
		return state;
	
	if(sBufSize == 0)
		return kFileIsOK;

	state = TestBuffer(arg, dir, spec, kFileTypeText);
	if(state != kFileIsOK)
		return state;

	state = TestValidName(arg);
	if(state != kFileIsOK)
		return state;
	
	state = TestFileIsAlias(arg, dir, spec);
	if(state != kFileIsOK)
		return state;

	return kFileIsOK;
}

kTextBinTYPE FileIsBinary(const char *arg, const char *dir, const UFSSpec * spec)
{
	kTextBinTYPE state;

	state = FillBuffer(arg, dir, spec, true);
	if(state != kFileIsOK)
		return state;
	
	state = TestBuffer(arg, dir, spec, kFileTypeBin);

#ifdef TARGET_OS_MAC
	kTextBinTYPE sigstate = GetMacSig(arg, dir, spec, true);
	if(sigstate != kFileIsOK)
		return state;
	if(state != kFileIsOK && state != kBinIsText)
		return state;
#else /* !TARGET_OS_MAC */
	if(state != kFileIsOK)
		return state;
#endif /* !TARGET_OS_MAC */

	state = TestValidName(arg);
	if(state != kFileIsOK)
		return state;
	
	state = TestFileIsAlias(arg, dir, spec);
	if(state != kFileIsOK)
		return state;

	return kFileIsOK;
}

kTextBinTYPE FileIsUnicode(const char *arg, const char *dir, const UFSSpec * spec /*= 0L*/)
{
	kTextBinTYPE state;

	state = FillBuffer(arg, dir, spec, true);
	if(state != kFileIsOK)
		return state;
	
	state = TestBuffer(arg, dir, spec, kFileTypeUnicode);

#ifdef TARGET_OS_MAC
	kTextBinTYPE sigstate = GetMacSig(arg, dir, spec, true);
	if(sigstate != kFileIsOK)
		return state;
	if(state != kFileIsOK && state != kBinIsText)
		return state;
#else /* !TARGET_OS_MAC */
	if(state != kFileIsOK)
		return state;
#endif /* !TARGET_OS_MAC */

	state = TestValidName(arg);
	if(state != kFileIsOK)
		return state;
	
	state = TestFileIsAlias(arg, dir, spec);
	if(state != kFileIsOK)
		return state;

	return kFileIsOK;
}

/*!
	Split the path into the up-directory and the folder
	\param dir Path to be split
	\param uppath Return up-directory
	\param folder Return folder or file
	\param dontSkipDelimiter Set to true to prevent skipping the delimiter if the path to be split ends with one
	\return true on success, false otherwise
	\note Passing NULL is considered an error, passing empty string returns true
*/
bool SplitPath(const char* dir, CStr& uppath, CStr& folder, bool dontSkipDelimiter /*= false*/)
{
	char newDir[MAX_PATH * 2] = "";
	char oldDir[MAX_PATH * 2] = "";
	char newFile[MAX_PATH] = "";

	if( !dir )
	{
		// Null pointer
		return false;
	}

	int dirLen = strlen(dir);
	if( dirLen > 0 )
	{
		strcpy(oldDir, dir);
		
		if( !dontSkipDelimiter && (dir[dirLen - 1] == kPathDelimiter) )
		{
			oldDir[dirLen - 1] = '\0';
			--dirLen;
		}
		
		const char* tmp = oldDir;
		const char* prev = oldDir;
		
		while( (tmp = strchr(tmp, kPathDelimiter)) != 0L )
		{
			prev = ++tmp;
		}
		
		strcpy(newDir, oldDir);
		newDir[prev - oldDir] = '\0';
		strcpy(newFile, prev);
	}
		
	uppath = newDir;
	folder = newFile;

	return true;
}

/*!
	Find a unique name for a temporary file
	\param file Return temporary file name
	\param prefix Prefix of the filename
	\param extension Extension of the filename
	\param create Set to true to create the file
	\return true on success, false otherwise
*/
bool MakeTmpFile(CStr& file, const char* prefix, const char* extension /*= 0L*/, bool create /*= false*/)
{
	static int count = 0;
	char filename[MAX_PATH];

	CStr tmpPath;
	CStr ext;
	CStr prf;

	if( extension != 0L && extension[0] != '\0' )
	{
		if( extension[0] != '.' )
			ext = ".";

		ext << extension;
	}

	if( prefix != 0L && prefix[0] != '\0' )
	{
		prf = prefix;
	}
	else
	{
		prf = "cvsgui";
	}

#ifdef WIN32
	char *ptr;
	ptr = getenv(_T("TMP"));
	if( ptr != 0L && _access(ptr, 0) != -1 )
	{
		tmpPath = ptr;
	}
	else
	{
		tmpPath = ".";
	}
#endif /* !WIN32 */
#ifdef TARGET_OS_MAC
	UFSSpec theFolder;
	OSErr err;
	
	if( (err = MacGetTempFolder(theFolder, tmpPath)) != noErr )
	{
		cvs_err("Unable to locate the preferences folder (error %d) !\n", err);
		return 0L;
	}
	
	for( ; ; ) 
	{
		sprintf(filename, "%s%d%s", (const char*)prf, count, (const char*)ext);
		if( ::strlen(filename) <= 31 )
			break;
		
		if( prf.length() == 0 ) 
		{
			cvs_err("Unable to truncate temp file !\n");
			return 0L;
		}
		
		prf.set (prf, prf.length () - 1);
	}
#endif /* !TARGET_OS_MAC */
#if qUnix
	tmpPath = "/tmp";
#endif

	struct stat sb;
	if( stat(tmpPath, &sb) == -1 || !S_ISDIR(sb.st_mode) )
	{
		cvs_err("Unable to access '%s' (error %d) !\n", (const char*)tmpPath, errno);
		return false;
	}

	if( !tmpPath.endsWith(kPathDelimiter) )
		tmpPath << kPathDelimiter;

	int maxtry = 10000;
	while( true )
	{
		sprintf(filename, "%s%d%s", (const char*)prf, count++, (const char*)ext);
		file = tmpPath;
		file << filename;

		if( stat(file, &sb) == -1 && errno == ENOENT )
		{
			if( create )
			{
				FILE* stream = fopen(file, "w");
				if( stream )
				{
					fclose(stream);
				}
			}
		
			return true;
		}

		if( maxtry-- == 0 )
		{
			cvs_err("Time out to access '%s' !\n", (const char*)tmpPath);
			break;
		}
	}

	return false;
}

/*!
	Extract the extension from the filename
	\param file File name to extract extension from
	\param base Return the base of the filename
	\param ext Return the file's extension
*/
void GetExtension(const char* file, CStr& base, CStr& ext)
{
	char *tmp = strrchr(file, '.');
	
	if( tmp == 0L )
	{
		ext = "";
		base = file;
	}
	else
	{
		ext = tmp + 1;
		base = file;
		base[tmp - file] = '\0';
	}
}

/*!
	Parse a command like 'user=who&host=cvs'
	\param cmd Command
	\param key Key
	\param value Return value
	\return	true on success, false otherwise
*/
bool GetEnvValue(const char* cmd, const char* key, UStr& value)
{
	const char* tmp = strstr(cmd, key);
	if( tmp == 0L )
		return false;

	tmp = strchr(tmp, '=');
	if( tmp == 0L )
		return false;

	tmp++;

	const char* tmp2 = strchr(tmp, '&');
	if( tmp2 == 0L )
		value = tmp;
	else
		value.set(tmp, tmp2 - tmp);

	return true;
}

/*!
	Convert a command line (possibly quoted) into regular argc, argv arguments
	\param cmdLine Command line
	\param argv Return argv arguments
	\return	The count of arguments (argc)
*/
int StringToArgv(const char* cmdLine, char** argv)
{
	int argc = 0;
	
	char Quote = '\0';				/*	Contains the quote character if we are inside "" or '', or \0 otherwise */ 
	const char*	walker1 = cmdLine;	/*	walker1 traverses the original command line */
	char* walker2;					/*	walker2 traverses the parsed copy of the command line */
	char theChar;					/*	theChar is the character we are currently parsing */
	char arguments[MAX_CMD_LEN];	/*	Will contain a parsed copy of the command line */
	
	/* Copy into arguments */
	walker2 = arguments;
	
	/* Parse command line to \0 */										
	while( (theChar = *walker1++) != '\0' ) 
	{
		/* Skip leading space before an argument */					
		if( theChar == ' ' )
			continue;
		
		/* Set argv to point to the space where we copy the next argument */
		argv [argc++] = walker2;								
		
		/* If we filled argv, skip the rest */															
		if( argc > MAX_ARGS)
		{
			argc--;
			break;
		}

		do
		{
			/* Parse an argument skip \s */
			if( theChar == '\\' && *walker1++ )
			{
				theChar = *walker1++;
			}
			else
			{
				/* When we find a quote... */
				if( theChar == '"' || theChar == '\'' )  
				{
					/* If we are not inside quotes already, */
					if( Quote == '\0' )
					{
						/* Remember we are inside quotes now */						
						Quote = theChar;
						
						/* Go to the next character */						
						continue;
					}
					
					/* If we are already inside that kind of quotes */
					if( theChar == Quote )
					{
						/* Close quotes */
						Quote = '\0';							
						
						/* Go to the next character */						
						continue;
					}
				}
			}
			
			/* Copy the character into args */
			*walker2++ = theChar;								
																
			/* Repeat until we reach the end of arguments or we reach a space that is not inside quotes */															
		}while( (*walker1 != '\0') && (((theChar = *walker1++) != ' ') || (Quote != '\0')) );
		
		/* Null-terminate the last argument */
		*walker2++ = 0;											
	}

	return argc;
}
