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