/*
** 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 macintosh
#	include <Resources.h>
#	include <unistd.h>
#	include <sys/types.h>
#	include <sys/stat.h>
#	include "MacMisc.h"
#endif /* macintosh */

#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);
#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 macintosh
				&& c != (unsigned char)'Ñ' && c != (unsigned char)'¥' /* powerplant */
#endif /* macintosh */		
				))
				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 != '    ')
			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 macintosh
	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 /* !macintosh */

	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 macintosh
	state = GetMacSig(arg, dir, spec, false);
	if(state != kFileIsOK)
		return state;
#endif /* macintosh */
	
	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 macintosh
	kTextBinTYPE sigstate = GetMacSig(arg, dir, spec, true);
	if(sigstate != kFileIsOK)
		return state;
	if(state != kFileIsOK && state != kBinIsText)
		return state;
#else /* !macintosh */
	if(state != kFileIsOK)
		return state;
#endif /* !macintosh */

	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 macintosh
	kTextBinTYPE sigstate = GetMacSig(arg, dir, spec, true);
	if(sigstate != kFileIsOK)
		return state;
	if(state != kFileIsOK && state != kBinIsText)
		return state;
#else /* !macintosh */
	if(state != kFileIsOK)
		return state;
#endif /* !macintosh */

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

	return kFileIsOK;
}

bool SplitPath(const char *dir, CStr & uppath, CStr & folder)
{
	char newdir[512];
	char olddir[512];
	char newfile[256];
	int lendir = strlen(dir);
	strcpy(olddir, dir);
	if(dir[lendir - 1] == kPathDelimiter)
	{
		olddir[lendir - 1] = '\0';
		--lendir;
	}
	const char *tmp = olddir, *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;
}

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';
	}
}

bool MakeTmpFile(CStr & file, const char *prefix, const char *extension /*= 0L*/, bool create /*= false*/)
{
	static int count = 0;
	char filename[256];

	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 macintosh
	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;
			} // if
		
		prf.set (prf, prf.length () - 1);
		} // for
#endif /* !macintosh */
#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(1)
	{
		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;
}

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;
}

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 */
	
	walker2 = arguments;										/*	copy into arguments */
	while ((theChar = *walker1++) != '\0') {					/*	parse command line to \0 */
		if (theChar == ' ')										/*	skip leading space before an argument */
			continue;		
		argv [argc++] = walker2;								/*	set argv to point to the space
																	where we copy the next argument */
		if (argc > MAX_ARGS) {									/*	if we filled argv, skip the rest */
			argc--;
			break;
		}
		do {													/*	parse an argument */
			if (theChar == '\\' && *walker1++)					/*	skip \s */
				theChar = *walker1++;
			else
				if ((theChar == '"') || (theChar == '\'')) {	/*	when we find a quote... */
					if (Quote == '\0') {						/*	if we are not inside quotes already, */
						Quote = theChar;						/*	remember we are inside quotes now */
						continue;								/*	go to the next character */
					}
					if (theChar == Quote) {						/*	if we are already inside that kind of quotes */
						Quote = '\0';							/*	close quotes */
						continue;								/*	go to the next character */
					}
				}
			*walker2++ = theChar;								/*	copy the character into args */
																/*	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')));
		*walker2++ = 0;											/* null-terminate the last argument */
	}

	return argc;
}
