/*
**  Copyright 2000-2004 University of Illinois Board of Trustees
**  Copyright 2000-2004 Mark D. Roth
**  All rights reserved.
**
**  ftplist.c - FTP directory parsing code
**
**  Mark D. Roth <roth@feep.net>
*/

#include <internal.h>

#include <stdio.h>
#include <errno.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>

#ifdef STDC_HEADERS
# include <string.h>
# include <stdlib.h>
#endif

/* needed for strcasecmp() on some platforms */
#include <strings.h>


#ifdef DEBUG
static void
_print_file_info(file_info_t *fip)
{
	printf("fi_filename\t\t= \"%s\"\n", fip->fi_filename);
	printf("fi_linkto\t\t= \"%s\"\n", fip->fi_linkto);
	printf("fs_mode\t\t\t= %04o\n", (unsigned int)fip->fi_stat.fs_mode);
	printf("fs_nlink\t\t= %hu\n", (unsigned short)fip->fi_stat.fs_nlink);
	printf("fs_username\t\t= \"%s\"\n", fip->fi_stat.fs_username);
	printf("fs_groupname\t\t= \"%s\"\n", fip->fi_stat.fs_groupname);
	printf("fs_size\t\t\t= %lu\n", (unsigned long)fip->fi_stat.fs_size);
	printf("fs_mtime\t\t= %s", asctime(localtime(&fip->fi_stat.fs_mtime)));
}
#endif


/*
** _ftp_list_parse_choose() - choose a list-parsing function based on
**                            the first line returned by the server
** arguments:
**	ftp		FTP handle
**	buf		line to be parsed
*/
static void
_ftp_list_parse_choose(FTP *ftp, char *buf)
{
	int i;

#ifdef DEBUG
	printf("==> _ftp_list_parse_choose(\"%s\")\n", buf);
#endif

	/*
	** for MLST, we know what to use,
	** so we don't even bother looking at buf
	*/
	if (BIT_ISSET(ftp->ftp_flags, FTP_FLAG_USE_MLST)
	    && BIT_ISSET(ftp->ftp_features, FTP_FEAT_MLST))
	{
		ftp->ftp_list_parse_function = _ftp_list_parse_mlsd;
#ifdef DEBUG
		printf("<== _ftp_list_parse_choose(): using MLST parsing\n");
#endif
		return;
	}

	/* if it starts with a '+', assume EPLF */
	if (buf[0] == '+')
	{
		ftp->ftp_list_parse_function = _ftp_list_parse_eplf;
#ifdef DEBUG
		printf("<== _ftp_list_parse_choose(): using EPLF parsing\n");
#endif
		return;
	}

	/* if there's no whitespace, assume it's a filename-only listing */
	if (strpbrk(buf, " \t") == NULL)
	{
		ftp->ftp_list_parse_function = _ftp_list_parse_dummy;
#ifdef DEBUG
		printf("<== _ftp_list_parse_choose(): using dummy parsing\n");
#endif
		return;
	}

	/* if it starts with a date in the form MM-DD-YY, assume NT */
	if (sscanf(buf, "%2d-%2d-%2d", &i, &i, &i) == 3)
	{
		ftp->ftp_list_parse_function = _ftp_list_parse_nt;
#ifdef DEBUG
		printf("<== _ftp_list_parse_choose(): using WinNT parsing\n");
#endif
		return;
	}

	/*
	** if we haven't identified it as any other type,
	** assume UNIX-style directories by default
	*/
	ftp->ftp_list_parse_function = _ftp_list_parse_unix;
#ifdef DEBUG
	printf("<== _ftp_list_parse_choose(): using UNIX parsing\n");
#endif
}


/*
** _ftp_list() - request, read, and parse a directory list from the server
** Returns:
**	(number of entries)	success
**	-1 (and sets errno)	failure
*/
int
_ftp_list(FTP *ftp, char *path, fget_hash_t *files_h,
	  enum dcent_types preferred_type, unsigned long flags)
{
	char buf[FTPBUFSIZE];
	int retval = 0, code = 0, numignored = 0, save_errno;
	unsigned short in_desired_cwd = 0;
	char *new_cwd, *cp, *cp2;
	char orig_path[MAXPATHLEN] = "";
	file_info_t *fip;

#ifdef DEBUG
	printf("==> _ftp_list(ftp=0x%lx (%s), path=\"%s\", "
	       "files_h=0x%lx, preferred_type=%d, flags=%lu)\n",
	       ftp, ftp->ftp_host, path, files_h, preferred_type, flags);
#endif

	/*
	** for MLSD or MLST, we don't need to mess with CWD
	*/
	if (BIT_ISSET(ftp->ftp_flags, FTP_FLAG_USE_MLST)
	    && BIT_ISSET(ftp->ftp_features, FTP_FEAT_MLST))
	{
		/*
		** make sure the server knows which facts we want returned
		*/
		if (_ftp_opts_mlsd(ftp) == -1)
			return -1;

		/*
		** for MLSD, set up the command and skip down
		** to where we open the ftp-data connection
		*/
		if (preferred_type == DCENT_DIR)
		{
			snprintf(buf, sizeof(buf), "MLSD %s", path);
			goto open_data_connection;
		}

		/*
		** MLST is handled differently, because there's no
		** ftp-data connection
		*/

		if (_ftp_send_command(ftp, "MLST %s", path) == -1)
		{
#ifdef DEBUG
			printf("    _ftp_list(): _ftp_send_command(\"%s\"): "
			       "%s\n", buf, strerror(errno));
#endif
			return -1;
		}

		if (_ftp_get_response(ftp, &code, buf, sizeof(buf)) == -1)
		{
#ifdef DEBUG
			printf("    _ftp_list(): _ftp_get_response(): %s\n",
			       strerror(errno));
#endif
			return -1;
		}

		if (code != 250)
		{
			errno = ENOENT;
			return -1;
		}

		/* find line starting with a space */
		cp = strstr(buf, "\n ");
		if (cp == NULL)
		{
			errno = EINVAL;
			return -1;
		}

		/* skip to beginning of MLST data */
		cp += 2;

		/* find next newline */
		cp2 = strchr(cp, '\n');
		if (cp2 == NULL)
		{
			errno = EINVAL;
			return -1;
		}
		*cp2 = '\0';

		fip = (file_info_t *)calloc(1, sizeof(file_info_t));
		if (fip == NULL)
			return -1;

		/* parse the entry */
		switch (_ftp_list_parse_mlsd(ftp, cp, fip))
		{
		/*
		** _ftp_list_parse_mlsd() never returns FLP_ERROR
		*/
		case FLP_IGNORE:
		default:
			/* ignore line */
			free(fip);
			errno = EINVAL;
			return -1;

		case FLP_VALID:
			/* normal file */
			fget_hash_add(files_h, fip);
		}

		/* return number of entries, which is always 1 */
		return 1;
	}

	/*
	** not using MLSx, so construct LIST request
	*/

	/*
	** if the path is "/", always treat it as a DCENT_DIR request,
	** since there is no parent directory to check for a DCENT_FILE
	*/
	if (strcmp(path, "/") == 0)
		preferred_type = DCENT_DIR;

	/*
	** for files, change to the parent directory
	** for directories, change to the directory itself
	*/
	if (preferred_type == DCENT_FILE)
		new_cwd = dirname(path);
	else
		new_cwd = path;

	/*
	** if we're not already in the desired directory,
	** save original path and send CWD request
	*/
	if (strcmp(ftp_getcwd(ftp), new_cwd) == 0)
		in_desired_cwd = 1;
	else
	{
		strlcpy(orig_path, ftp_getcwd(ftp), sizeof(orig_path));
		if (ftp_chdir(ftp, new_cwd) == -1)
		{
#ifdef DEBUG
			printf("    _ftp_list(): ftp_chdir(\"%s\"): %s\n",
			       new_cwd, strerror(errno));
#endif
			/*
			** if we're looking for a directory but we can't
			** CWD to the specified path, then fail
			**
			** FIXME: is this too much of an assumption?
			** is it possible for CWD to fail for a directory,
			** but still have LIST succeed?
			*/
			if (preferred_type == DCENT_DIR)
				return -1;

			/*
			** also fail immediately if we got ECONNRESET
			*/
			if (errno == ECONNRESET)
				return -1;

			/*
			** it could be EACCES or ENOENT
			** (and if we got ENOENT, it might really be an
			** ENOTDIR situation)
			** either way, we'll try passing the path name to
			** the LIST request
			*/

			/*
			** clear buffer so that we know not to CWD back below
			*/
			orig_path[0] = '\0';
		}
		else
			in_desired_cwd = 1;
	}

	/*
	** construct list command
	*/
	strlcpy(buf, "LIST", sizeof(buf));

	/*
	** add appropriate ls(1) options,
	** unless server doesn't grok them
	*/
	if (! BIT_ISSET(flags, FL_TRY_NO_LS_OPTS)
	    && ! BIT_ISSET(ftp->ftp_features, FTP_FEAT_NO_LIST_LS_OPTS))
		strlcat(buf,
			(preferred_type == DCENT_FILE
			 ? " -ld"
			 : " -la"),
			sizeof(buf));

	/*
	** add the pathname to be listed
	*/
	if (preferred_type == DCENT_FILE)
	{
		strlcat(buf, " ", sizeof(buf));

		/*
		** if we're already in the parent directory, just use
		** the basename; otherwise, use the full path
		*/
		if (in_desired_cwd)
			strlcat(buf, basename(path), sizeof(buf));
		else
			strlcat(buf, path, sizeof(buf));
	}
	else	/* preferred_type == DCENT_DIR */
	{
		/*
		** if we're not already in the directory to be listed,
		** use the full path; otherwise, don't use any argument
		*/
		if (! in_desired_cwd)
		{
			strlcat(buf, " ", sizeof(buf));
			strlcat(buf, path, sizeof(buf));
		}
	}

  open_data_connection:
	/* for MLSD or LIST requests, use a data connection */
	if (_ftp_data_connect(ftp, "%s", buf) == -1)
	{
		retval = -1;
		goto done;
	}

	/*
	** if there are send and receive hooks set for the control
	** connection, use them for the file listing as well
	*/
	fget_netio_set_options(ftp->ftp_data,
			       NETIO_OPT_SEND_HOOK,	ftp->ftp_send_hook,
			       NETIO_OPT_RECV_HOOK,	ftp->ftp_recv_hook,
			       NETIO_OPT_HOOK_HANDLE,	ftp,
			       NETIO_OPT_HOOK_DATA,	ftp->ftp_hook_data,
			       0);

	/* read each line */
	while (fget_netio_read_line(ftp->ftp_data,
				    ftp->ftp_io_timeout,
				    buf, sizeof(buf)) > 0)
	{
		/* choose a list parsing function, if needed */
		if (ftp->ftp_list_parse_function == NULL)
			_ftp_list_parse_choose(ftp, buf);

		/* allocate structure for new entry */
		fip = (file_info_t *)calloc(1, sizeof(file_info_t));
		if (fip == NULL)
			return -1;

		/*
		** call the parsing function to generate
		** an ftpstat structure
		*/
		switch ((*(ftp->ftp_list_parse_function))(ftp, buf, fip))
		{
		case FLP_ERROR:
			/* error (sets errno) */
			save_errno = errno;
			free(fip);
			_ftp_data_close(ftp);
			errno = save_errno;
			retval = -1;
			goto done;

		case FLP_IGNORE:
		default:
			/* ignore line */
			free(fip);
			numignored++;
			break;

		case FLP_VALID:
			/* normal file */
			fget_hash_add(files_h, fip);
		}
	}

	if (_ftp_data_close(ftp) == -1)
	{
		/* if something other than 226 was returned, return ENOENT */
		if (errno == EINVAL)
			errno = ENOENT;

		retval = -1;
		goto done;
	}

	/* no lines returned, which means ENOENT */
	if ((fget_hash_nents(files_h) + numignored) == 0)
	{
		errno = ENOENT;
		retval = -1;
		goto done;
	}

	/* no errors, so return number of entries found */
	retval = fget_hash_nents(files_h);

  done:
	/*
	** for MLSD, return immediately
	** (no need to try again with ls(1) options
	** or CWD back to where we belong)
	*/
	if (BIT_ISSET(ftp->ftp_flags, FTP_FLAG_USE_MLST)
	    && BIT_ISSET(ftp->ftp_features, FTP_FEAT_MLST))
		return retval;

	/*
	** if we failed with ls(1) options, try again without them
	*/
	if (! BIT_ISSET(flags, FL_TRY_NO_LS_OPTS)
	    && ! BIT_ISSET(ftp->ftp_features, FTP_FEAT_NO_LIST_LS_OPTS)
	    && retval == -1
	    && errno != ECONNRESET)
	{
		retval = _ftp_list(ftp, path, files_h,
				   preferred_type, flags|FL_TRY_NO_LS_OPTS);

		/* if it succeeded, don't use ls(1) options from now on */
		if (retval == 0)
			BIT_SET(ftp->ftp_features, FTP_FEAT_NO_LIST_LS_OPTS);
	}

	/*
	** change back to original working directory
	*/
	if (orig_path[0] != '\0'
	    && (retval != -1 || errno != ECONNRESET)
	    && ftp_chdir(ftp, orig_path) == -1)
		return -1;

#ifdef DEBUG
	printf("<== _ftp_list(): returning %d\n", retval);
#endif
	return retval;
}




syntax highlighted by Code2HTML, v. 0.9.1