/*  MikMod example player
	(c) 1998, 1999 Miodrag Vallat and others - see file AUTHORS for
	complete list.

	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 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.
 
	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., 59 Temple Place - Suite 330, Boston, MA
	02111-1307, USA.
*/

/*==============================================================================

  $Id: marchive.c,v 1.4 2003/09/25 16:38:42 raph Exp $

  Archive support

  These routines are used to detect different archive/compression formats
  and decompress/de-archive the mods from them if necessary.

==============================================================================*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifndef HAVE_FNMATCH_H
#include "fnmatch.h"
#else
#include <fnmatch.h>
#endif
#include <ctype.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#if !defined(__OS2__)&&!defined(__EMX__)
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#include <pwd.h>
#include <sys/wait.h>
#endif

#include <mikmod.h>
#include "player.h"
#include "mutilities.h"

/*========== Archives */

/*
	The following table describes how MikMod should deal with archives.

	The first two fields are for identification. The code will consider that
	a given file is a recognized archive if a signature is found at a fixed
	location in the file. The first field is the offset into the archive of
	the signature, and the second field points to the signature to check (an
	8-bit string).

	The third field contains the name of the program to invoke to list and
	extract the archive.

	The fourth and fifth fields are the first two arguments which will be
	given to the program when it is invoked to extract a file from the archive.
	The command which will be invoked is :
		archiver_command extract_arg1 extract_arg2 archive_file file_to_extract
	If only one argument is needed, set extract_arg2 to NULL.

	The sixth and seventh fields are the first two arguments which will be
	given to the program when it is invoked to list the contents of the
	archive.
	The command which will be invoked is :
		archiver_command list_arg1 list_arg2 archive_file
	If only one argument is needed, set list_arg2 to NULL.

	For the special case of mono-file archives (gzip and bzip2 compressed
	files, for example), set *both* arguments to NULL. In this case, the code
	will determine the contents of the file without having to invoke the
	list function of the archiver. This is necessary since bzip2 has no list
	function, and the only way to get the archive contents is to test it,
	which can be a really slow process.

	The last field is the column, in the archive listing output, where the
	filenames begin (starting from zero for the leftmost column). A good
	archiver will put them last on line, so they can embed spaces and be as
	long as necessary.

	The constant MAXCOLUMN later should be set to the highest column number
	found in the table. When adding new entries to the table, be sure to
	compile a debug version, it will check at runtime if your MAXCOLUMN value
	is correct.
	
	The OS/2 variant of the table is shorter, since there is no need to
	make the two extract and list arguments separate, so only one string is
	needed in both cases, and for mono-file archives, the listing arguments
	string is set to NULL.

*/

typedef struct {
	int location;	/* <0 -> file extensions are checked */
	char *marker;
	char *command;
#if !defined(__OS2__)&&!defined(__EMX__)
	char *extrargs,*extrargs2;
	char *listargs,*listargs2;
#else
	char *extrargs;
	char *listargs;
#endif
	unsigned int nameoffset;
} ARCHIVE;

/* use similar signature idea to "file" to see what format we have... */
static	char pksignature[]={'P','K',0x03,0x04,0};
static	char zoosignature[]={0xdc,0xa7,0xc4,0xfd,0};
static	char rarsignature[]={'R','a','r','!',0};
static	char gzsignature[]={0x1f,0x8b,0};
static	char bzip2signature[]={'B','Z','h',0};
static	char tarsignature[]={'u','s','t','a','r',0};
#if !defined(__OS2__)&&!defined(__EMX__)
static	char lhsignature[]={'-','l','h',0};
static	char lzsignature[]={'-','l','z',0};
#endif

/* interesting file extensions */
#if !defined(__OS2__)&&!defined(__EMX__)
static	char targzextension[]=".TAR.GZ .TAZ .TGZ";
static	char tarbzip2extension[]=".TAR.BZ2 .TBZ .TBZ2";
#endif

static ARCHIVE MA_archiver[]={
	/* location, marker, command, extract and view args, filenames column */
#if !defined(__OS2__)&&!defined(__EMX__)
/* PKzip archive */
	{	0,	pksignature,	"unzip",	"-pqq", NULL,	"-vqq", NULL,	58},
/* zoo */
	{	20,	zoosignature,	"zoo",		"xpq", NULL,	"lfq", NULL,		0},
/* rar */
	{	0,	rarsignature,	"unrar",	"p", "-inul",	"v", "-c-",		1},
/* lharc */
	{	2,	lhsignature,	"lha",		"pq", NULL,		"vvq", NULL,	0},
	{	2,	lzsignature,	"lha",		"pq", NULL,		"vvq", NULL,	0},
/* tar */
	{	257,tarsignature,	"tar",		"-xOf", NULL,	"-tf", NULL,	0},
/* tar.gz */
	{	-1,	targzextension,	"tar",		"-xOzf", NULL,	"-tzf", NULL,	0},
/* tar.bz2 */
	{	-1,	tarbzip2extension,	"tar",	"--use-compress-program=bzip2",
			"-xOf",		"--use-compress-program=bzip2",	"-tf",			0},
/* gzip */
	{	0,	gzsignature,	"gzip",		"-dqc", NULL,	NULL, NULL,		0},
/* bzip2 */
	{	0,	bzip2signature,	"bzip2",	"-dc", NULL,	NULL, NULL,		0},
/* needed to end the array... */
	{	0,	NULL,			NULL,		NULL, NULL,		NULL, NULL,		0}
#else
/* PKzip archive */
	{	0,	pksignature,	"unzip",	"-pqq",		"-lqq",		41},
/* zoo */
	{	20,	zoosignature,	"zoo",		"xpq",		"lq",		47},
	{	0,	rarsignature,	"unrar",	"p -inul",	"v -c-",	1},
/* tar */
	{	257,tarsignature,	"tar",		"-xOf",		"-tRf",		16},
/* gzip */
	{	0,	gzsignature,	"gzip",		"-dqc",		NULL,		27},
/* bzip2 */
	{	0,	bzip2signature,	"bzip2",	"-dc",		NULL,		0},
/* needed to end the array... */
	{	0,	NULL,			NULL,		NULL,		NULL,		0}
#endif
};

/* rightmost column position */
#if defined(__OS2__)||defined(__EMX__)
#define MAXCOLUMN 47
#else
#define MAXCOLUMN 58
#endif

/* largest signature length */
#define MAXSIGNATURE 16

/* module filenames patterns */
static CHAR* modulepatterns[]={
	"*.669",
	"*.[Aa][Mm][Ff]",
	"*.[Aa][Pp][Uu][Nn]",
	"*.[Dd][Ss][Mm]",
	"*.[Ff][Aa][Rr]",
	"*.[Gg][Dd][Mm]",
	"*.[Ii][Mm][Ff]",
	"*.[Ii][Tt]",
	"*.[Mm][Ee][Dd]",
	"*.[Mm][Oo][Dd]",
	"*.[Mm][Tt][Mm]",
	"*.[Nn][Ss][Tt]",	/* noisetracker */
	"*.[Ss]3[Mm]",
	"*.[Ss][Tt][Mm]",
	"*.[Ss][Tt][Xx]",
	"*.[Uu][Ll][Tt]",
	"*.[Uu][Nn][Ii]",
	"*.[Xx][Mm]",
	NULL
};

/* On the Amiga (Aminet) the module type seems often to be used as prefix */
static CHAR* prefixmodulepatterns[]={
	"669.*",
	"[Aa][Mm][Ff].*",
	"[Aa][Pp][Uu][Nn].*",
	"[Dd][Ss][Mm].*",
	"[Ff][Aa][Rr].*",
	"[Gg][Dd][Mm].*",
	"[Ii][Mm][Ff].*",
	"[Ii][Tt].*",
	"[Mm][Ee][Dd].*",
	"[Mm][Oo][Dd].*",
	"[Mm][Tt][Mm].*",
	"[Nn][Ss][Tt].*",
	"[Ss]3[Mm].*",
	"[Ss][Tt][Mm].*",
	"[Ss][Tt][Xx].*",
	"[Uu][Ll][Tt].*",
	"[Uu][Nn][Ii].*",
	"[Xx][Mm].*",
	NULL
};

#if !defined(__OS2__)&&!defined(__EMX__)
/* Drop all root privileges we might have */
BOOL DropPrivileges(void)
{
	if (!geteuid()) {
		if (getuid()) {
			/* we are setuid root -> drop setuid to become the real user */
			if (setuid(getuid()))
				return 1;
		} else {
			/* we are run as root -> drop all and become user 'nobody' */
			struct passwd *nobody;
			int uid;

			if (!(nobody=getpwnam("nobody")))
				return 1; /* no such user ? */
			uid=nobody->pw_uid;
			if (!uid) /* user 'nobody' has root privileges ? weird... */
				return 1;
			if (setuid(uid))
				return 1;
		}
	}
	return 0;
}
#endif

/* Determines if a filename matches a module filename pattern */
static BOOL MA_isModuleFilename(CHAR* filename)
{
	int t;

	for (t=0;modulepatterns[t];t++)
		if (!fnmatch(modulepatterns[t],filename,FNM_NOESCAPE))
			return 1;
	return 0;
}

/* The same, but also checks for prefix names */
static BOOL MA_isModuleFilename2(CHAR* filename)
{
	int t;

	/* a filename may not start with '-' since this could be abused to
	   feed another option to the archiver */
	if (filename[0]=='-')
		return 0;
	for (t=0;modulepatterns[t];t++)
		if (!fnmatch(modulepatterns[t],filename,FNM_NOESCAPE))
			return 1;
	for (t=0;prefixmodulepatterns[t];t++)
		if (!fnmatch(prefixmodulepatterns[t],filename,FNM_NOESCAPE))
			return 1;
	return 0;
}

/* Determines if a filename extension matches an archive filename extension
   pattern */
static BOOL MA_MatchExtension(CHAR *archive,CHAR *ends)
{
	CHAR *pos=ends;
	int nr,arch_nr;

	do {
		while ((*pos)&&(*pos!=' ')) pos++;
		nr=pos-ends;
		arch_nr=strlen(archive);
		while ((nr>0)&&(arch_nr>0)&&
		       (toupper((int)archive[arch_nr-1])==*(ends+nr-1)))
			nr--,arch_nr--;
		if (nr<=0) return 1;

		pos++;
		ends=pos;
	} while (*(pos-1));
	return 0;
}

/* Tests if 'filename' has the signature 'header-string' at offset
   'header_location' */
static int MA_identify(CHAR* filename,int header_location,CHAR* header_string)
{
	FILE *fp;
	CHAR id[MAXSIGNATURE+1];

	if (header_location<0) {
		/* check extension of file rather than signature */

		return MA_MatchExtension(filename,header_string);
	} else {
		/* check in-file signature */

#ifdef MIKMOD_DEBUG
		if (strlen(header_string)>MAXSIGNATURE) {
			fputs("Error: wrong constant MAXSIGNATURE in MikMod source\n",stderr);
			return 0;
		}
#endif
		if (!(fp=fopen(filename,"rb")))
			return 0;

		fseek(fp,header_location,SEEK_SET);

		if (!fread(id,strlen(header_string),1,fp)) {
			fclose(fp);
			return 0;
		}
		if (!memcmp(id,(CHAR*)header_string,strlen(header_string))) {
			fclose(fp);
			return 1;
		}
		fclose(fp);
		return 0;
	}
}

/* Extracts the file 'file' from the archive 'arc' */
CHAR* MA_dearchive(CHAR* arc,CHAR* file)
{
	int t;
	CHAR *tmp_file;

	/* not an archive file... */
	if ((!arc)||(!arc[0]))
		return strdup(file);

	tmp_file=get_tmp_name();

	for (t=0;MA_archiver[t].command;t++) 
		if (MA_identify(arc,MA_archiver[t].location,MA_archiver[t].marker)) {
			/* display "extracting" message, as this may take some time... */
			display_extractbanner();
#if defined(__OS2__)||defined(__EMX__)
			/* extracting, the non-Unix way */
		{
			CHAR command_buff[PATH_MAX<<1];

			if (MA_archiver[t].listargs)
				/* multi-file archive : need to give filename to extract */
#ifdef HAVE_SNPRINTF
				snprintf(command_buff,PATH_MAX<<1,"%s %s \"%s\" \"%s\" >\"%s\"",
				        MA_archiver[t].command,MA_archiver[t].extrargs,
				        arc,file,tmp_file);
#else
				sprintf(command_buff,"%s %s \"%s\" \"%s\" >\"%s\"",
				        MA_archiver[t].command,MA_archiver[t].extrargs,
				        arc,file,tmp_file);
#endif
			else
				/* single-file archive */
#ifdef HAVE_SNPRINTF
				snprintf(command_buff,PATH_MAX<<1,"%s %s \"%s\" >\"%s\"",
				        MA_archiver[t].command,MA_archiver[t].extrargs,
				        arc,tmp_file);
#else
				sprintf(command_buff,"%s %s \"%s\" >\"%s\"",
				        MA_archiver[t].command,MA_archiver[t].extrargs,
				        arc,tmp_file);
#endif

			system(command_buff);
		}
#else
			/* extracting, the Unix way */
		{
			pid_t pid;
			int status,fd;

			switch (pid=fork()) {
				case -1: /* fork failed */
					return NULL;
					break;
				case 0: /* fork succeeded, child process code */
					/* if we have root privileges, drop them */
					if (DropPrivileges()) exit(0);
					fd=open(tmp_file,O_WRONLY|O_CREAT|O_TRUNC,S_IREAD|S_IWRITE);
					if (fd!=-1) {
						char *argv[6];
						int index=2;

						close(0);close(1);close(2);
						dup2(fd,1);
						signal(SIGINT,SIG_DFL);signal(SIGQUIT,SIG_DFL);

	 					argv[0]=MA_archiver[t].command;
						argv[1]=MA_archiver[t].extrargs;
						if (MA_archiver[t].extrargs2)
							argv[index++]=MA_archiver[t].extrargs2;
						argv[index++]=arc;
						if (MA_archiver[t].listargs)
							argv[index++]=file;
						argv[index++]=NULL;
						execvp(MA_archiver[t].command,argv);

						close(fd);
						unlink(tmp_file);
					}
					exit(0);
					break;
				default: /* fork succeeded, main process code */
					waitpid(pid,&status,0);
					if (!WIFEXITED(status)) {
						unlink(tmp_file);
						return NULL;
					}
					break;
			}
		}
#endif
			break;
		}

	return tmp_file;
}

/* Examines file 'filename' to add modules to the playlist 'pl' */
void MA_FindFiles(PLAYLIST* pl,CHAR* filename)
{
	int t;
	int archive=0;
	CHAR string[PATH_MAX+(MAXCOLUMN+1)+1];
	struct stat statbuf;

#ifdef MIKMOD_DEBUG
	for (t=0;MA_archiver[t].command;t++)
		if (MA_archiver[t].nameoffset>MAXCOLUMN) {
			fputs("Error: wrong constant MAXCOLUMN in MikMod source\n",stderr);
			return;
		}
#endif

	if (stat(filename,&statbuf))
		return; /* File does not exist or not enough access rights */

	if ((S_ISDIR(statbuf.st_mode))||(S_ISCHR(statbuf.st_mode))
#ifndef __EMX__
	    ||(S_ISBLK(statbuf.st_mode))
#endif
	   )
		return; /* Directories and devices can't be modules... */

	/* if filename looks like a playlist, load as a playlist */
	if (PL_isPlaylistFilename(filename))
		if (PL_Load(pl,filename))
			return;

	/* if filename looks like a module and not like an archive, don't try to
	   unzip the file...*/
	if (MA_isModuleFilename(filename)) {
		PL_Add(pl,filename,NULL,0,0);
		return;
	}

	for (t=0;MA_archiver[t].command;t++) 
		if (MA_identify(filename,MA_archiver[t].location,MA_archiver[t].marker)) {
			archive=t+1;
			break;
		}

	if (archive--) {
		if (MA_archiver[t].listargs) {
			/* multi-file archive, need to invoke list function */
#if defined(__OS2__)||defined(__EMX__)
/* Archive display, the non-Unix way */
			FILE *file;

#ifdef HAVE_SNPRINTF
			sprintf(string,PATH_MAX+(MAXCOLUMN+1)+1,"%s %s \"%s\"",MA_archiver[archive].command,
			        MA_archiver[archive].listargs,filename);
#else
			sprintf(string,"%s %s \"%s\"",MA_archiver[archive].command,
			        MA_archiver[archive].listargs,filename);
#endif
#ifdef __WATCOMC__
			file=_popen(string,"r");
#else
			file=popen(string,"r");
#endif
			fgets(string,PATH_MAX+MAXCOLUMN+1,file);
			while (!feof(file)) {
				string[strlen(string)-1]=0;
				if (!MA_archiver[archive].nameoffset) {
					for (t=0;string[t]!=' ';t++);
					string[t]=0;
				}
				if (MA_isModuleFilename2(string+MA_archiver[archive].nameoffset))
					PL_Add(pl,(string+MA_archiver[archive].nameoffset),filename,0,0);
				fgets(string,PATH_MAX,file);
			}
#ifdef __WATCOMC__
			_pclose(file);
#else
			pclose(file);
#endif
#else
/* Archive display, the Unix way */
			int fd[2];

			if (!pipe(fd)) {
				pid_t pid;
				FILE *file;
				int status,s;

				switch (pid=fork()) {
					case -1: /* fork failed */
						break;
					case 0: /* fork succeeded, child process code */
					{
						char *argv[5];
						int index=2;

						/* if we have root privileges, drop them */
						if (DropPrivileges()) exit(0);
						close(0);close(1);close(2);
						dup2(fd[1],1);dup2(fd[1],2);
						signal(SIGINT,SIG_DFL);signal(SIGQUIT,SIG_DFL);

	 					argv[0]=MA_archiver[archive].command;
						argv[1]=MA_archiver[archive].listargs;
						if (MA_archiver[archive].listargs2)
							argv[index++]=MA_archiver[archive].listargs2;
						argv[index++]=filename;
						argv[index++]=NULL;

						execvp(MA_archiver[t].command,argv);

						close(fd[1]);
						exit(0);
					}
						break;
					default: /* fork succeeded, main process code */

						close(fd[1]);
						if (!(file=fdopen(fd[0],"r"))) {
							close(fd[1]);
							waitpid(pid,&status,0);
							break;
						}
						/* read from the pipe */
						while (fgets(string,PATH_MAX+MAXCOLUMN+1,file)) {
							s=strlen(string)-1;
							if (string[s]=='\n') string[s]='\0';
							/* remove trailing whitespace from the filename */
							for (s=0,t=0;string[t++];)
								if (string[t]!=' ')
									s=t;
							string[s]='\0';
							if (MA_isModuleFilename2(string+MA_archiver[archive].nameoffset))
								PL_Add(pl,(string+MA_archiver[archive].nameoffset),filename,0,0);
						}
						fclose(file);
						waitpid(pid,&status,0);
						break;
				}
			}
#endif
		} else {
			/* single-file archive, guess the name */
			CHAR *dot,*slash,*spare;

			dot=strrchr(filename,'.');
			slash=strrchr(filename,PATH_SEP);

			if (!slash)
				slash=filename;
			else
				slash++;
			if (!dot) for (dot=slash;*dot;dot++);

			spare=malloc((1+dot-slash)*sizeof(CHAR));
			if (spare) {
				strncpy(spare,slash,dot-slash);spare[dot-slash]=0;

				if (MA_isModuleFilename2(spare)) PL_Add(pl,spare,filename,0,0);
				free(spare);
			}
		}
	} else
		PL_Add(pl,filename,NULL,0,0);
}

/* ex:set ts=4: */
