xref: /openbsd/usr.bin/less/filename.c (revision 083f8ce1)
1 /*
2  * Copyright (C) 1984-2012  Mark Nudelman
3  * Modified for use with illumos by Garrett D'Amore.
4  * Copyright 2014 Garrett D'Amore <garrett@damore.org>
5  *
6  * You may distribute under the terms of either the GNU General Public
7  * License or the Less License, as specified in the README file.
8  *
9  * For more information, see the README file.
10  */
11 
12 /*
13  * Routines to mess around with filenames (and files).
14  * Much of this is very OS dependent.
15  *
16  * Modified for illumos/POSIX -- it uses native glob(3C) rather than
17  * popen to a shell to perform the expansion.
18  */
19 
20 #include <sys/stat.h>
21 
22 #include <glob.h>
23 #include <stdarg.h>
24 
25 #include "less.h"
26 
27 extern int force_open;
28 extern int secure;
29 extern int ctldisp;
30 extern int utf_mode;
31 extern IFILE curr_ifile;
32 extern IFILE old_ifile;
33 extern char openquote;
34 extern char closequote;
35 
36 /*
37  * Remove quotes around a filename.
38  */
39 char *
shell_unquote(char * str)40 shell_unquote(char *str)
41 {
42 	char *name;
43 	char *p;
44 
45 	name = p = ecalloc(strlen(str)+1, sizeof (char));
46 	if (*str == openquote) {
47 		str++;
48 		while (*str != '\0') {
49 			if (*str == closequote) {
50 				if (str[1] != closequote)
51 					break;
52 				str++;
53 			}
54 			*p++ = *str++;
55 		}
56 	} else {
57 		char *esc = get_meta_escape();
58 		int esclen = strlen(esc);
59 		while (*str != '\0') {
60 			if (esclen > 0 && strncmp(str, esc, esclen) == 0)
61 				str += esclen;
62 			*p++ = *str++;
63 		}
64 	}
65 	*p = '\0';
66 	return (name);
67 }
68 
69 /*
70  * Get the shell's escape character.
71  */
72 char *
get_meta_escape(void)73 get_meta_escape(void)
74 {
75 	char *s;
76 
77 	s = lgetenv("LESSMETAESCAPE");
78 	if (s == NULL)
79 		s = "\\";
80 	return (s);
81 }
82 
83 /*
84  * Get the characters which the shell considers to be "metacharacters".
85  */
86 static char *
metachars(void)87 metachars(void)
88 {
89 	static char *mchars = NULL;
90 
91 	if (mchars == NULL) {
92 		mchars = lgetenv("LESSMETACHARS");
93 		if (mchars == NULL)
94 			mchars = DEF_METACHARS;
95 	}
96 	return (mchars);
97 }
98 
99 /*
100  * Is this a shell metacharacter?
101  */
102 static int
metachar(char c)103 metachar(char c)
104 {
105 	return (strchr(metachars(), c) != NULL);
106 }
107 
108 /*
109  * Must use quotes rather than escape characters for this meta character.
110  */
111 static int
must_quote(char c)112 must_quote(char c)
113 {
114 	return (c == '\n');
115 }
116 
117 /*
118  * Insert a backslash before each metacharacter in a string.
119  */
120 char *
shell_quote(const char * s)121 shell_quote(const char *s)
122 {
123 	const char *p;
124 	char *r;
125 	char *newstr;
126 	int len;
127 	char *esc = get_meta_escape();
128 	int esclen = strlen(esc);
129 	int use_quotes = 0;
130 	int have_quotes = 0;
131 
132 	/*
133 	 * Determine how big a string we need to allocate.
134 	 */
135 	len = 1; /* Trailing null byte */
136 	for (p = s; *p != '\0'; p++) {
137 		len++;
138 		if (*p == openquote || *p == closequote)
139 			have_quotes = 1;
140 		if (metachar(*p)) {
141 			if (esclen == 0) {
142 				/*
143 				 * We've got a metachar, but this shell
144 				 * doesn't support escape chars.  Use quotes.
145 				 */
146 				use_quotes = 1;
147 			} else if (must_quote(*p)) {
148 				/* Opening quote + character + closing quote. */
149 				len += 3;
150 			} else {
151 				/*
152 				 * Allow space for the escape char.
153 				 */
154 				len += esclen;
155 			}
156 		}
157 	}
158 	/*
159 	 * Allocate and construct the new string.
160 	 */
161 	if (use_quotes) {
162 		/* We can't quote a string that contains quotes. */
163 		if (have_quotes)
164 			return (NULL);
165 		newstr  = easprintf("%c%s%c", openquote, s, closequote);
166 	} else {
167 		newstr = r = ecalloc(len, sizeof (char));
168 		while (*s != '\0') {
169 			if (!metachar(*s)) {
170 				*r++ = *s++;
171 			} else if (must_quote(*s)) {
172 				/* Surround the character with quotes. */
173 				*r++ = openquote;
174 				*r++ = *s++;
175 				*r++ = closequote;
176 			} else {
177 				/* Escape the character. */
178 				(void) strlcpy(r, esc, newstr + len - p);
179 				r += esclen;
180 				*r++ = *s++;
181 			}
182 		}
183 		*r = '\0';
184 	}
185 	return (newstr);
186 }
187 
188 /*
189  * Return a pathname that points to a specified file in a specified directory.
190  * Return NULL if the file does not exist in the directory.
191  */
192 static char *
dirfile(const char * dirname,const char * filename)193 dirfile(const char *dirname, const char *filename)
194 {
195 	char *pathname;
196 	char *qpathname;
197 	int f;
198 
199 	if (dirname == NULL || *dirname == '\0')
200 		return (NULL);
201 	/*
202 	 * Construct the full pathname.
203 	 */
204 	pathname = easprintf("%s/%s", dirname, filename);
205 	/*
206 	 * Make sure the file exists.
207 	 */
208 	qpathname = shell_unquote(pathname);
209 	f = open(qpathname, O_RDONLY);
210 	if (f == -1) {
211 		free(pathname);
212 		pathname = NULL;
213 	} else {
214 		(void) close(f);
215 	}
216 	free(qpathname);
217 	return (pathname);
218 }
219 
220 /*
221  * Return the full pathname of the given file in the "home directory".
222  */
223 char *
homefile(char * filename)224 homefile(char *filename)
225 {
226 	return (dirfile(lgetenv("HOME"), filename));
227 }
228 
229 /*
230  * Expand a string, substituting any "%" with the current filename,
231  * and any "#" with the previous filename.
232  * But a string of N "%"s is just replaced with N-1 "%"s.
233  * Likewise for a string of N "#"s.
234  * {{ This is a lot of work just to support % and #. }}
235  */
236 char *
fexpand(char * s)237 fexpand(char *s)
238 {
239 	char *fr, *to;
240 	int n;
241 	char *e;
242 	IFILE ifile;
243 
244 #define	fchar_ifile(c) \
245 	((c) == '%' ? curr_ifile : (c) == '#' ? old_ifile : NULL)
246 
247 	/*
248 	 * Make one pass to see how big a buffer we
249 	 * need to allocate for the expanded string.
250 	 */
251 	n = 0;
252 	for (fr = s; *fr != '\0'; fr++) {
253 		switch (*fr) {
254 		case '%':
255 		case '#':
256 			if (fr > s && fr[-1] == *fr) {
257 				/*
258 				 * Second (or later) char in a string
259 				 * of identical chars.  Treat as normal.
260 				 */
261 				n++;
262 			} else if (fr[1] != *fr) {
263 				/*
264 				 * Single char (not repeated).  Treat specially.
265 				 */
266 				ifile = fchar_ifile(*fr);
267 				if (ifile == NULL)
268 					n++;
269 				else
270 					n += strlen(get_filename(ifile));
271 			}
272 			/*
273 			 * Else it is the first char in a string of
274 			 * identical chars.  Just discard it.
275 			 */
276 			break;
277 		default:
278 			n++;
279 			break;
280 		}
281 	}
282 
283 	e = ecalloc(n+1, sizeof (char));
284 
285 	/*
286 	 * Now copy the string, expanding any "%" or "#".
287 	 */
288 	to = e;
289 	for (fr = s; *fr != '\0'; fr++) {
290 		switch (*fr) {
291 		case '%':
292 		case '#':
293 			if (fr > s && fr[-1] == *fr) {
294 				*to++ = *fr;
295 			} else if (fr[1] != *fr) {
296 				ifile = fchar_ifile(*fr);
297 				if (ifile == NULL) {
298 					*to++ = *fr;
299 				} else {
300 					(void) strlcpy(to, get_filename(ifile),
301 					    e + n + 1 - to);
302 					to += strlen(to);
303 				}
304 			}
305 			break;
306 		default:
307 			*to++ = *fr;
308 			break;
309 		}
310 	}
311 	*to = '\0';
312 	return (e);
313 }
314 
315 /*
316  * Return a blank-separated list of filenames which "complete"
317  * the given string.
318  */
319 char *
fcomplete(char * s)320 fcomplete(char *s)
321 {
322 	char *fpat;
323 	char *qs;
324 
325 	if (secure)
326 		return (NULL);
327 	/*
328 	 * Complete the filename "s" by globbing "s*".
329 	 */
330 	fpat =  easprintf("%s*", s);
331 
332 	qs = lglob(fpat);
333 	s = shell_unquote(qs);
334 	if (strcmp(s, fpat) == 0) {
335 		/*
336 		 * The filename didn't expand.
337 		 */
338 		free(qs);
339 		qs = NULL;
340 	}
341 	free(s);
342 	free(fpat);
343 	return (qs);
344 }
345 
346 /*
347  * Try to determine if a file is "binary".
348  * This is just a guess, and we need not try too hard to make it accurate.
349  */
350 int
bin_file(int f)351 bin_file(int f)
352 {
353 	char data[256];
354 	ssize_t i, n;
355 	wchar_t ch;
356 	int bin_count, len;
357 
358 	if (!seekable(f))
359 		return (0);
360 	if (lseek(f, (off_t)0, SEEK_SET) == (off_t)-1)
361 		return (0);
362 	n = read(f, data, sizeof (data));
363 	bin_count = 0;
364 	for (i = 0; i < n; i += len) {
365 		len = mbtowc(&ch, data + i, n - i);
366 		if (len <= 0) {
367 			bin_count++;
368 			len = 1;
369 		} else if (iswprint(ch) == 0 && iswspace(ch) == 0 &&
370 		    data[i] != '\b' &&
371 		    (ctldisp != OPT_ONPLUS || data[i] != ESC))
372 			bin_count++;
373 	}
374 	/*
375 	 * Call it a binary file if there are more than 5 binary characters
376 	 * in the first 256 bytes of the file.
377 	 */
378 	return (bin_count > 5);
379 }
380 
381 /*
382  * Expand a filename, doing any system-specific metacharacter substitutions.
383  */
384 char *
lglob(char * filename)385 lglob(char *filename)
386 {
387 	char *gfilename;
388 	char *ofilename;
389 	glob_t list;
390 	int i;
391 	int length;
392 	char *p;
393 	char *qfilename;
394 
395 	ofilename = fexpand(filename);
396 	if (secure)
397 		return (ofilename);
398 	filename = shell_unquote(ofilename);
399 
400 	/*
401 	 * The globbing function returns a list of names.
402 	 */
403 
404 #ifndef	GLOB_TILDE
405 #define	GLOB_TILDE	0
406 #endif
407 #ifndef	GLOB_LIMIT
408 #define	GLOB_LIMIT	0
409 #endif
410 	if (glob(filename, GLOB_TILDE | GLOB_LIMIT, NULL, &list) != 0) {
411 		free(filename);
412 		return (ofilename);
413 	}
414 	length = 1; /* Room for trailing null byte */
415 	for (i = 0; i < list.gl_pathc; i++) {
416 		p = list.gl_pathv[i];
417 		qfilename = shell_quote(p);
418 		if (qfilename != NULL) {
419 			length += strlen(qfilename) + 1;
420 			free(qfilename);
421 		}
422 	}
423 	gfilename = ecalloc(length, sizeof (char));
424 	for (i = 0; i < list.gl_pathc; i++) {
425 		p = list.gl_pathv[i];
426 		qfilename = shell_quote(p);
427 		if (qfilename != NULL) {
428 			if (i != 0) {
429 				(void) strlcat(gfilename, " ", length);
430 			}
431 			(void) strlcat(gfilename, qfilename, length);
432 			free(qfilename);
433 		}
434 	}
435 	globfree(&list);
436 	free(filename);
437 	free(ofilename);
438 	return (gfilename);
439 }
440 
441 /*
442  * Is the specified file a directory?
443  */
444 int
is_dir(char * filename)445 is_dir(char *filename)
446 {
447 	int isdir = 0;
448 	int r;
449 	struct stat statbuf;
450 
451 	filename = shell_unquote(filename);
452 
453 	r = stat(filename, &statbuf);
454 	isdir = (r >= 0 && S_ISDIR(statbuf.st_mode));
455 	free(filename);
456 	return (isdir);
457 }
458 
459 /*
460  * Returns NULL if the file can be opened and
461  * is an ordinary file, otherwise an error message
462  * (if it cannot be opened or is a directory, etc.)
463  */
464 char *
bad_file(char * filename)465 bad_file(char *filename)
466 {
467 	char *m = NULL;
468 
469 	filename = shell_unquote(filename);
470 	if (!force_open && is_dir(filename)) {
471 		m = easprintf("%s is a directory", filename);
472 	} else {
473 		int r;
474 		struct stat statbuf;
475 
476 		r = stat(filename, &statbuf);
477 		if (r == -1) {
478 			m = errno_message(filename);
479 		} else if (force_open) {
480 			m = NULL;
481 		} else if (!S_ISREG(statbuf.st_mode)) {
482 			m = easprintf("%s is not a regular file (use -f to "
483 			    "see it)", filename);
484 		}
485 	}
486 	free(filename);
487 	return (m);
488 }
489 
490 /*
491  * Return the size of a file, as cheaply as possible.
492  */
493 off_t
filesize(int f)494 filesize(int f)
495 {
496 	struct stat statbuf;
497 
498 	if (fstat(f, &statbuf) >= 0)
499 		return (statbuf.st_size);
500 	return (-1);
501 }
502 
503 /*
504  * Return last component of a pathname.
505  */
506 char *
last_component(char * name)507 last_component(char *name)
508 {
509 	char *slash;
510 
511 	for (slash = name + strlen(name); slash > name; ) {
512 		--slash;
513 		if (*slash == '/')
514 			return (slash + 1);
515 	}
516 	return (name);
517 }
518