1/*
2 * Copyright (C) 1984-2023  Mark Nudelman
3 *
4 * You may distribute under the terms of either the GNU General Public
5 * License or the Less License, as specified in the README file.
6 *
7 * For more information, see the README file.
8 */
9
10
11/*
12 * Routines to mess around with filenames (and files).
13 * Much of this is very OS dependent.
14 */
15
16#include "less.h"
17#include "lglob.h"
18#if MSDOS_COMPILER
19#include <dos.h>
20#if MSDOS_COMPILER==WIN32C && !defined(_MSC_VER)
21#include <dir.h>
22#endif
23#if MSDOS_COMPILER==DJGPPC
24#include <glob.h>
25#include <dir.h>
26#define _MAX_PATH      PATH_MAX
27#endif
28#endif
29#ifdef _OSK
30#include <rbf.h>
31#ifndef _OSK_MWC32
32#include <modes.h>
33#endif
34#endif
35
36#if HAVE_STAT
37#include <sys/stat.h>
38#ifndef S_ISDIR
39#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)
40#endif
41#ifndef S_ISREG
42#define S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)
43#endif
44#endif
45
46extern int force_open;
47extern int secure;
48extern int use_lessopen;
49extern int ctldisp;
50extern int utf_mode;
51extern IFILE curr_ifile;
52extern IFILE old_ifile;
53#if SPACES_IN_FILENAMES
54extern char openquote;
55extern char closequote;
56#endif
57#if HAVE_STAT_INO
58extern ino_t curr_ino;
59extern dev_t curr_dev;
60#endif
61
62/*
63 * Remove quotes around a filename.
64 */
65public char * shell_unquote(char *str)
66{
67	char *name;
68	char *p;
69
70	name = p = (char *) ecalloc(strlen(str)+1, sizeof(char));
71	if (*str == openquote)
72	{
73		str++;
74		while (*str != '\0')
75		{
76			if (*str == closequote)
77			{
78				if (str[1] != closequote)
79					break;
80				str++;
81			}
82			*p++ = *str++;
83		}
84	} else
85	{
86		char *esc = get_meta_escape();
87		int esclen = (int) strlen(esc);
88		while (*str != '\0')
89		{
90			if (esclen > 0 && strncmp(str, esc, esclen) == 0)
91				str += esclen;
92			*p++ = *str++;
93		}
94	}
95	*p = '\0';
96	return (name);
97}
98
99/*
100 * Get the shell's escape character.
101 */
102public char * get_meta_escape(void)
103{
104	char *s;
105
106	s = lgetenv("LESSMETAESCAPE");
107	if (s == NULL)
108		s = DEF_METAESCAPE;
109	return (s);
110}
111
112/*
113 * Get the characters which the shell considers to be "metacharacters".
114 */
115static char * metachars(void)
116{
117	static char *mchars = NULL;
118
119	if (mchars == NULL)
120	{
121		mchars = lgetenv("LESSMETACHARS");
122		if (mchars == NULL)
123			mchars = DEF_METACHARS;
124	}
125	return (mchars);
126}
127
128/*
129 * Is this a shell metacharacter?
130 */
131static int metachar(char c)
132{
133	return (strchr(metachars(), c) != NULL);
134}
135
136/*
137 * Insert a backslash before each metacharacter in a string.
138 */
139public char * shell_quote(char *s)
140{
141	char *p;
142	char *newstr;
143	int len;
144	char *esc = get_meta_escape();
145	int esclen = (int) strlen(esc);
146	int use_quotes = 0;
147	int have_quotes = 0;
148
149	/*
150	 * Determine how big a string we need to allocate.
151	 */
152	len = 1; /* Trailing null byte */
153	for (p = s;  *p != '\0';  p++)
154	{
155		len++;
156		if (*p == openquote || *p == closequote)
157			have_quotes = 1;
158		if (metachar(*p))
159		{
160			if (esclen == 0)
161			{
162				/*
163				 * We've got a metachar, but this shell
164				 * doesn't support escape chars.  Use quotes.
165				 */
166				use_quotes = 1;
167			} else
168			{
169				/*
170				 * Allow space for the escape char.
171				 */
172				len += esclen;
173			}
174		}
175	}
176	if (use_quotes)
177	{
178		if (have_quotes)
179			/*
180			 * We can't quote a string that contains quotes.
181			 */
182			return (NULL);
183		len = (int) strlen(s) + 3;
184	}
185	/*
186	 * Allocate and construct the new string.
187	 */
188	newstr = p = (char *) ecalloc(len, sizeof(char));
189	if (use_quotes)
190	{
191		SNPRINTF3(newstr, len, "%c%s%c", openquote, s, closequote);
192	} else
193	{
194		while (*s != '\0')
195		{
196			if (metachar(*s))
197			{
198				/*
199				 * Add the escape char.
200				 */
201				strcpy(p, esc);
202				p += esclen;
203			}
204			*p++ = *s++;
205		}
206		*p = '\0';
207	}
208	return (newstr);
209}
210
211/*
212 * Return a pathname that points to a specified file in a specified directory.
213 * Return NULL if the file does not exist in the directory.
214 */
215public char * dirfile(char *dirname, char *filename, int must_exist)
216{
217	char *pathname;
218	int len;
219	int f;
220
221	if (dirname == NULL || *dirname == '\0')
222		return (NULL);
223	/*
224	 * Construct the full pathname.
225	 */
226	len = (int) (strlen(dirname) + strlen(filename) + 2);
227	pathname = (char *) calloc(len, sizeof(char));
228	if (pathname == NULL)
229		return (NULL);
230	SNPRINTF3(pathname, len, "%s%s%s", dirname, PATHNAME_SEP, filename);
231	if (must_exist)
232	{
233		/*
234		 * Make sure the file exists.
235		 */
236		f = open(pathname, OPEN_READ);
237		if (f < 0)
238		{
239			free(pathname);
240			pathname = NULL;
241		} else
242		{
243			close(f);
244		}
245	}
246	return (pathname);
247}
248
249/*
250 * Return the full pathname of the given file in the "home directory".
251 */
252public char * homefile(char *filename)
253{
254	char *pathname;
255
256	/* Try $HOME/filename. */
257	pathname = dirfile(lgetenv("HOME"), filename, 1);
258	if (pathname != NULL)
259		return (pathname);
260#if OS2
261	/* Try $INIT/filename. */
262	pathname = dirfile(lgetenv("INIT"), filename, 1);
263	if (pathname != NULL)
264		return (pathname);
265#endif
266#if MSDOS_COMPILER || OS2
267	/* Look for the file anywhere on search path. */
268	pathname = (char *) ecalloc(_MAX_PATH, sizeof(char));
269#if MSDOS_COMPILER==DJGPPC
270	{
271		char *res = searchpath(filename);
272		if (res == 0)
273			*pathname = '\0';
274		else
275			strcpy(pathname, res);
276	}
277#else
278	_searchenv(filename, "PATH", pathname);
279#endif
280	if (*pathname != '\0')
281		return (pathname);
282	free(pathname);
283#endif
284	return (NULL);
285}
286
287/*
288 * Expand a string, substituting any "%" with the current filename,
289 * and any "#" with the previous filename.
290 * But a string of N "%"s is just replaced with N-1 "%"s.
291 * Likewise for a string of N "#"s.
292 * {{ This is a lot of work just to support % and #. }}
293 */
294public char * fexpand(char *s)
295{
296	char *fr, *to;
297	int n;
298	char *e;
299	IFILE ifile;
300
301#define fchar_ifile(c) \
302	((c) == '%' ? curr_ifile : \
303	 (c) == '#' ? old_ifile : NULL_IFILE)
304
305	/*
306	 * Make one pass to see how big a buffer we
307	 * need to allocate for the expanded string.
308	 */
309	n = 0;
310	for (fr = s;  *fr != '\0';  fr++)
311	{
312		switch (*fr)
313		{
314		case '%':
315		case '#':
316			if (fr > s && fr[-1] == *fr)
317			{
318				/*
319				 * Second (or later) char in a string
320				 * of identical chars.  Treat as normal.
321				 */
322				n++;
323			} else if (fr[1] != *fr)
324			{
325				/*
326				 * Single char (not repeated).  Treat specially.
327				 */
328				ifile = fchar_ifile(*fr);
329				if (ifile == NULL_IFILE)
330					n++;
331				else
332					n += (int) strlen(get_filename(ifile));
333			}
334			/*
335			 * Else it is the first char in a string of
336			 * identical chars.  Just discard it.
337			 */
338			break;
339		default:
340			n++;
341			break;
342		}
343	}
344
345	e = (char *) ecalloc(n+1, sizeof(char));
346
347	/*
348	 * Now copy the string, expanding any "%" or "#".
349	 */
350	to = e;
351	for (fr = s;  *fr != '\0';  fr++)
352	{
353		switch (*fr)
354		{
355		case '%':
356		case '#':
357			if (fr > s && fr[-1] == *fr)
358			{
359				*to++ = *fr;
360			} else if (fr[1] != *fr)
361			{
362				ifile = fchar_ifile(*fr);
363				if (ifile == NULL_IFILE)
364					*to++ = *fr;
365				else
366				{
367					strcpy(to, get_filename(ifile));
368					to += strlen(to);
369				}
370			}
371			break;
372		default:
373			*to++ = *fr;
374			break;
375		}
376	}
377	*to = '\0';
378	return (e);
379}
380
381
382#if TAB_COMPLETE_FILENAME
383
384/*
385 * Return a blank-separated list of filenames which "complete"
386 * the given string.
387 */
388public char * fcomplete(char *s)
389{
390	char *fpat;
391	char *qs;
392
393	if (secure)
394		return (NULL);
395	/*
396	 * Complete the filename "s" by globbing "s*".
397	 */
398#if MSDOS_COMPILER && (MSDOS_COMPILER == MSOFTC || MSDOS_COMPILER == BORLANDC)
399	/*
400	 * But in DOS, we have to glob "s*.*".
401	 * But if the final component of the filename already has
402	 * a dot in it, just do "s*".
403	 * (Thus, "FILE" is globbed as "FILE*.*",
404	 *  but "FILE.A" is globbed as "FILE.A*").
405	 */
406	{
407		char *slash;
408		int len;
409		for (slash = s+strlen(s)-1;  slash > s;  slash--)
410			if (*slash == *PATHNAME_SEP || *slash == '/')
411				break;
412		len = (int) strlen(s) + 4;
413		fpat = (char *) ecalloc(len, sizeof(char));
414		if (strchr(slash, '.') == NULL)
415			SNPRINTF1(fpat, len, "%s*.*", s);
416		else
417			SNPRINTF1(fpat, len, "%s*", s);
418	}
419#else
420	{
421	int len = (int) strlen(s) + 2;
422	fpat = (char *) ecalloc(len, sizeof(char));
423	SNPRINTF1(fpat, len, "%s*", s);
424	}
425#endif
426	qs = lglob(fpat);
427	s = shell_unquote(qs);
428	if (strcmp(s,fpat) == 0)
429	{
430		/*
431		 * The filename didn't expand.
432		 */
433		free(qs);
434		qs = NULL;
435	}
436	free(s);
437	free(fpat);
438	return (qs);
439}
440#endif
441
442/*
443 * Try to determine if a file is "binary".
444 * This is just a guess, and we need not try too hard to make it accurate.
445 */
446public int bin_file(int f)
447{
448	int n;
449	int bin_count = 0;
450	char data[256];
451	char* p;
452	char* edata;
453
454	if (!seekable(f))
455		return (0);
456	if (lseek(f, (off_t)0, SEEK_SET) == BAD_LSEEK)
457		return (0);
458	n = read(f, data, sizeof(data));
459	if (n <= 0)
460		return (0);
461	edata = &data[n];
462	for (p = data;  p < edata;  )
463	{
464		if (utf_mode && !is_utf8_well_formed(p, edata-p))
465		{
466			bin_count++;
467			utf_skip_to_lead(&p, edata);
468		} else
469		{
470			LWCHAR c = step_char(&p, +1, edata);
471			struct ansi_state *pansi;
472			if (ctldisp == OPT_ONPLUS && (pansi = ansi_start(c)) != NULL)
473			{
474				skip_ansi(pansi, &p, edata);
475				ansi_done(pansi);
476			} else if (binary_char(c))
477				bin_count++;
478		}
479	}
480	/*
481	 * Call it a binary file if there are more than 5 binary characters
482	 * in the first 256 bytes of the file.
483	 */
484	return (bin_count > 5);
485}
486
487/*
488 * Try to determine the size of a file by seeking to the end.
489 */
490static POSITION seek_filesize(int f)
491{
492	off_t spos;
493
494	spos = lseek(f, (off_t)0, SEEK_END);
495	if (spos == BAD_LSEEK)
496		return (NULL_POSITION);
497	return ((POSITION) spos);
498}
499
500#if HAVE_POPEN
501/*
502 * Read a string from a file.
503 * Return a pointer to the string in memory.
504 */
505static char * readfd(FILE *fd)
506{
507	int len;
508	int ch;
509	char *buf;
510	char *p;
511
512	/*
513	 * Make a guess about how many chars in the string
514	 * and allocate a buffer to hold it.
515	 */
516	len = 100;
517	buf = (char *) ecalloc(len, sizeof(char));
518	for (p = buf;  ;  p++)
519	{
520		if ((ch = getc(fd)) == '\n' || ch == EOF)
521			break;
522		if (p - buf >= len-1)
523		{
524			/*
525			 * The string is too big to fit in the buffer we have.
526			 * Allocate a new buffer, twice as big.
527			 */
528			len *= 2;
529			*p = '\0';
530			p = (char *) ecalloc(len, sizeof(char));
531			strcpy(p, buf);
532			free(buf);
533			buf = p;
534			p = buf + strlen(buf);
535		}
536		*p = ch;
537	}
538	*p = '\0';
539	return (buf);
540}
541
542/*
543 * Execute a shell command.
544 * Return a pointer to a pipe connected to the shell command's standard output.
545 */
546static FILE * shellcmd(char *cmd)
547{
548	FILE *fd;
549
550#if HAVE_SHELL
551	char *shell;
552
553	shell = lgetenv("SHELL");
554	if (!isnullenv(shell))
555	{
556		char *scmd;
557		char *esccmd;
558
559		/*
560		 * Read the output of <$SHELL -c cmd>.
561		 * Escape any metacharacters in the command.
562		 */
563		esccmd = shell_quote(cmd);
564		if (esccmd == NULL)
565		{
566			fd = popen(cmd, "r");
567		} else
568		{
569			int len = (int) (strlen(shell) + strlen(esccmd) + 5);
570			scmd = (char *) ecalloc(len, sizeof(char));
571			SNPRINTF3(scmd, len, "%s %s %s", shell, shell_coption(), esccmd);
572			free(esccmd);
573			fd = popen(scmd, "r");
574			free(scmd);
575		}
576	} else
577#endif
578	{
579		fd = popen(cmd, "r");
580	}
581	/*
582	 * Redirection in `popen' might have messed with the
583	 * standard devices.  Restore binary input mode.
584	 */
585	SET_BINARY(0);
586	return (fd);
587}
588
589#endif /* HAVE_POPEN */
590
591
592/*
593 * Expand a filename, doing any system-specific metacharacter substitutions.
594 */
595public char * lglob(char *filename)
596{
597	char *gfilename;
598
599	filename = fexpand(filename);
600	if (secure)
601		return (filename);
602
603#ifdef DECL_GLOB_LIST
604{
605	/*
606	 * The globbing function returns a list of names.
607	 */
608	int length;
609	char *p;
610	char *qfilename;
611	DECL_GLOB_LIST(list)
612
613	GLOB_LIST(filename, list);
614	if (GLOB_LIST_FAILED(list))
615	{
616		return (filename);
617	}
618	length = 1; /* Room for trailing null byte */
619	for (SCAN_GLOB_LIST(list, p))
620	{
621		INIT_GLOB_LIST(list, p);
622		qfilename = shell_quote(p);
623		if (qfilename != NULL)
624		{
625			length += strlen(qfilename) + 1;
626			free(qfilename);
627		}
628	}
629	gfilename = (char *) ecalloc(length, sizeof(char));
630	for (SCAN_GLOB_LIST(list, p))
631	{
632		INIT_GLOB_LIST(list, p);
633		qfilename = shell_quote(p);
634		if (qfilename != NULL)
635		{
636			sprintf(gfilename + strlen(gfilename), "%s ", qfilename);
637			free(qfilename);
638		}
639	}
640	/*
641	 * Overwrite the final trailing space with a null terminator.
642	 */
643	*--p = '\0';
644	GLOB_LIST_DONE(list);
645}
646#else
647#ifdef DECL_GLOB_NAME
648{
649	/*
650	 * The globbing function returns a single name, and
651	 * is called multiple times to walk thru all names.
652	 */
653	char *p;
654	int len;
655	int n;
656	char *pfilename;
657	char *qfilename;
658	DECL_GLOB_NAME(fnd,drive,dir,fname,ext,handle)
659
660	GLOB_FIRST_NAME(filename, &fnd, handle);
661	if (GLOB_FIRST_FAILED(handle))
662	{
663		return (filename);
664	}
665
666	_splitpath(filename, drive, dir, fname, ext);
667	len = 100;
668	gfilename = (char *) ecalloc(len, sizeof(char));
669	p = gfilename;
670	do {
671		n = (int) (strlen(drive) + strlen(dir) + strlen(fnd.GLOB_NAME) + 1);
672		pfilename = (char *) ecalloc(n, sizeof(char));
673		SNPRINTF3(pfilename, n, "%s%s%s", drive, dir, fnd.GLOB_NAME);
674		qfilename = shell_quote(pfilename);
675		free(pfilename);
676		if (qfilename != NULL)
677		{
678			n = (int) strlen(qfilename);
679			while (p - gfilename + n + 2 >= len)
680			{
681				/*
682				 * No room in current buffer.
683				 * Allocate a bigger one.
684				 */
685				len *= 2;
686				*p = '\0';
687				p = (char *) ecalloc(len, sizeof(char));
688				strcpy(p, gfilename);
689				free(gfilename);
690				gfilename = p;
691				p = gfilename + strlen(gfilename);
692			}
693			strcpy(p, qfilename);
694			free(qfilename);
695			p += n;
696			*p++ = ' ';
697		}
698	} while (GLOB_NEXT_NAME(handle, &fnd) == 0);
699
700	/*
701	 * Overwrite the final trailing space with a null terminator.
702	 */
703	*--p = '\0';
704	GLOB_NAME_DONE(handle);
705}
706#else
707#if HAVE_POPEN
708{
709	/*
710	 * We get the shell to glob the filename for us by passing
711	 * an "echo" command to the shell and reading its output.
712	 */
713	FILE *fd;
714	char *s;
715	char *lessecho;
716	char *cmd;
717	char *esc;
718	int len;
719
720	esc = get_meta_escape();
721	if (strlen(esc) == 0)
722		esc = "-";
723	esc = shell_quote(esc);
724	if (esc == NULL)
725	{
726		return (filename);
727	}
728	lessecho = lgetenv("LESSECHO");
729	if (isnullenv(lessecho))
730		lessecho = "lessecho";
731	/*
732	 * Invoke lessecho, and read its output (a globbed list of filenames).
733	 */
734	len = (int) (strlen(lessecho) + strlen(filename) + (7*strlen(metachars())) + 24);
735	cmd = (char *) ecalloc(len, sizeof(char));
736	SNPRINTF4(cmd, len, "%s -p0x%x -d0x%x -e%s ", lessecho,
737		(unsigned char) openquote, (unsigned char) closequote, esc);
738	free(esc);
739	for (s = metachars();  *s != '\0';  s++)
740		sprintf(cmd + strlen(cmd), "-n0x%x ", (unsigned char) *s);
741	sprintf(cmd + strlen(cmd), "-- %s", filename);
742	fd = shellcmd(cmd);
743	free(cmd);
744	if (fd == NULL)
745	{
746		/*
747		 * Cannot create the pipe.
748		 * Just return the original (fexpanded) filename.
749		 */
750		return (filename);
751	}
752	gfilename = readfd(fd);
753	pclose(fd);
754	if (*gfilename == '\0')
755	{
756		free(gfilename);
757		return (filename);
758	}
759}
760#else
761	/*
762	 * No globbing functions at all.  Just use the fexpanded filename.
763	 */
764	gfilename = save(filename);
765#endif
766#endif
767#endif
768	free(filename);
769	return (gfilename);
770}
771
772/*
773 * Does path not represent something in the file system?
774 */
775public int is_fake_pathname(char *path)
776{
777	return (strcmp(path, "-") == 0 ||
778	        strcmp(path, FAKE_HELPFILE) == 0 || strcmp(path, FAKE_EMPTYFILE) == 0);
779}
780
781/*
782 * Return canonical pathname.
783 */
784public char * lrealpath(char *path)
785{
786	if (!is_fake_pathname(path))
787	{
788#if HAVE_REALPATH
789		char rpath[PATH_MAX];
790		if (realpath(path, rpath) != NULL)
791			return (save(rpath));
792#endif
793	}
794	return (save(path));
795}
796
797#if HAVE_POPEN
798/*
799 * Return number of %s escapes in a string.
800 * Return a large number if there are any other % escapes besides %s.
801 */
802static int num_pct_s(char *lessopen)
803{
804	int num = 0;
805
806	while (*lessopen != '\0')
807	{
808		if (*lessopen == '%')
809		{
810			if (lessopen[1] == '%')
811				++lessopen;
812			else if (lessopen[1] == 's')
813				++num;
814			else
815				return (999);
816		}
817		++lessopen;
818	}
819	return (num);
820}
821#endif
822
823/*
824 * See if we should open a "replacement file"
825 * instead of the file we're about to open.
826 */
827public char * open_altfile(char *filename, int *pf, void **pfd)
828{
829#if !HAVE_POPEN
830	return (NULL);
831#else
832	char *lessopen;
833	char *qfilename;
834	char *cmd;
835	int len;
836	FILE *fd;
837#if HAVE_FILENO
838	int returnfd = 0;
839#endif
840
841	if (!use_lessopen || secure)
842		return (NULL);
843	ch_ungetchar(-1);
844	if ((lessopen = lgetenv("LESSOPEN")) == NULL)
845		return (NULL);
846	while (*lessopen == '|')
847	{
848		/*
849		 * If LESSOPEN starts with a |, it indicates
850		 * a "pipe preprocessor".
851		 */
852#if !HAVE_FILENO
853		error("LESSOPEN pipe is not supported", NULL_PARG);
854		return (NULL);
855#else
856		lessopen++;
857		returnfd++;
858#endif
859	}
860	if (*lessopen == '-')
861	{
862		/*
863		 * Lessopen preprocessor will accept "-" as a filename.
864		 */
865		lessopen++;
866	} else
867	{
868		if (strcmp(filename, "-") == 0)
869			return (NULL);
870	}
871	if (num_pct_s(lessopen) != 1)
872	{
873		error("LESSOPEN ignored: must contain exactly one %%s", NULL_PARG);
874		return (NULL);
875	}
876
877	qfilename = shell_quote(filename);
878	len = (int) (strlen(lessopen) + strlen(qfilename) + 2);
879	cmd = (char *) ecalloc(len, sizeof(char));
880	SNPRINTF1(cmd, len, lessopen, qfilename);
881	free(qfilename);
882	fd = shellcmd(cmd);
883	free(cmd);
884	if (fd == NULL)
885	{
886		/*
887		 * Cannot create the pipe.
888		 */
889		return (NULL);
890	}
891#if HAVE_FILENO
892	if (returnfd)
893	{
894		char c;
895		int f;
896
897		/*
898		 * The alt file is a pipe. Read one char
899		 * to see if the pipe will produce any data.
900		 * If it does, push the char back on the pipe.
901		 */
902		f = fileno(fd);
903		SET_BINARY(f);
904		if (read(f, &c, 1) != 1)
905		{
906			/*
907			 * Pipe is empty.
908			 * If more than 1 pipe char was specified,
909			 * the exit status tells whether the file itself
910			 * is empty, or if there is no alt file.
911			 * If only one pipe char, just assume no alt file.
912			 */
913			int status = pclose(fd);
914			if (returnfd > 1 && status == 0) {
915				/* File is empty. */
916				*pfd = NULL;
917				*pf = -1;
918				return (save(FAKE_EMPTYFILE));
919			}
920			/* No alt file. */
921			return (NULL);
922		}
923		/* Alt pipe contains data, so use it. */
924		ch_ungetchar(c);
925		*pfd = (void *) fd;
926		*pf = f;
927		return (save("-"));
928	}
929#endif
930	/* The alt file is a regular file. Read its name from LESSOPEN. */
931	cmd = readfd(fd);
932	pclose(fd);
933	if (*cmd == '\0')
934	{
935		/*
936		 * Pipe is empty.  This means there is no alt file.
937		 */
938		free(cmd);
939		return (NULL);
940	}
941	return (cmd);
942#endif /* HAVE_POPEN */
943}
944
945/*
946 * Close a replacement file.
947 */
948public void close_altfile(char *altfilename, char *filename)
949{
950#if HAVE_POPEN
951	char *lessclose;
952	char *qfilename;
953	char *qaltfilename;
954	FILE *fd;
955	char *cmd;
956	int len;
957
958	if (secure)
959		return;
960	ch_ungetchar(-1);
961	if ((lessclose = lgetenv("LESSCLOSE")) == NULL)
962		return;
963	if (num_pct_s(lessclose) > 2)
964	{
965		error("LESSCLOSE ignored; must contain no more than 2 %%s", NULL_PARG);
966		return;
967	}
968	qfilename = shell_quote(filename);
969	qaltfilename = shell_quote(altfilename);
970	len = (int) (strlen(lessclose) + strlen(qfilename) + strlen(qaltfilename) + 2);
971	cmd = (char *) ecalloc(len, sizeof(char));
972	SNPRINTF2(cmd, len, lessclose, qfilename, qaltfilename);
973	free(qaltfilename);
974	free(qfilename);
975	fd = shellcmd(cmd);
976	free(cmd);
977	if (fd != NULL)
978		pclose(fd);
979#endif
980}
981
982/*
983 * Is the specified file a directory?
984 */
985public int is_dir(char *filename)
986{
987	int isdir = 0;
988
989#if HAVE_STAT
990{
991	int r;
992	struct stat statbuf;
993
994	r = stat(filename, &statbuf);
995	isdir = (r >= 0 && S_ISDIR(statbuf.st_mode));
996}
997#else
998#ifdef _OSK
999{
1000	int f;
1001
1002	f = open(filename, S_IREAD | S_IFDIR);
1003	if (f >= 0)
1004		close(f);
1005	isdir = (f >= 0);
1006}
1007#endif
1008#endif
1009	return (isdir);
1010}
1011
1012/*
1013 * Returns NULL if the file can be opened and
1014 * is an ordinary file, otherwise an error message
1015 * (if it cannot be opened or is a directory, etc.)
1016 */
1017public char * bad_file(char *filename)
1018{
1019	char *m = NULL;
1020
1021	if (!force_open && is_dir(filename))
1022	{
1023		static char is_a_dir[] = " is a directory";
1024
1025		m = (char *) ecalloc(strlen(filename) + sizeof(is_a_dir),
1026			sizeof(char));
1027		strcpy(m, filename);
1028		strcat(m, is_a_dir);
1029	} else
1030	{
1031#if HAVE_STAT
1032		int r;
1033		struct stat statbuf;
1034
1035		r = stat(filename, &statbuf);
1036		if (r < 0)
1037		{
1038			m = errno_message(filename);
1039		} else if (force_open)
1040		{
1041			m = NULL;
1042		} else if (!S_ISREG(statbuf.st_mode))
1043		{
1044			static char not_reg[] = " is not a regular file (use -f to see it)";
1045			m = (char *) ecalloc(strlen(filename) + sizeof(not_reg),
1046				sizeof(char));
1047			strcpy(m, filename);
1048			strcat(m, not_reg);
1049		}
1050#endif
1051	}
1052	return (m);
1053}
1054
1055/*
1056 * Return the size of a file, as cheaply as possible.
1057 * In Unix, we can stat the file.
1058 */
1059public POSITION filesize(int f)
1060{
1061#if HAVE_STAT
1062	struct stat statbuf;
1063
1064	if (fstat(f, &statbuf) >= 0)
1065		return ((POSITION) statbuf.st_size);
1066#else
1067#ifdef _OSK
1068	long size;
1069
1070	if ((size = (long) _gs_size(f)) >= 0)
1071		return ((POSITION) size);
1072#endif
1073#endif
1074	return (seek_filesize(f));
1075}
1076
1077public int curr_ifile_changed(void)
1078{
1079#if HAVE_STAT_INO
1080	/*
1081	 * If the file's i-number or device has changed,
1082	 * or if the file is smaller than it previously was,
1083	 * the file must be different.
1084	 */
1085	struct stat st;
1086	POSITION curr_pos = ch_tell();
1087	int r = stat(get_filename(curr_ifile), &st);
1088	if (r == 0 && (st.st_ino != curr_ino ||
1089		st.st_dev != curr_dev ||
1090		(curr_pos != NULL_POSITION && st.st_size < curr_pos)))
1091		return (TRUE);
1092#endif
1093	return (FALSE);
1094}
1095
1096/*
1097 *
1098 */
1099public char * shell_coption(void)
1100{
1101	return ("-c");
1102}
1103
1104/*
1105 * Return last component of a pathname.
1106 */
1107public char * last_component(char *name)
1108{
1109	char *slash;
1110
1111	for (slash = name + strlen(name);  slash > name; )
1112	{
1113		--slash;
1114		if (*slash == *PATHNAME_SEP || *slash == '/')
1115			return (slash + 1);
1116	}
1117	return (name);
1118}
1119