1 /*
2  * miscellaneous string utility functions
3  *
4  * Copyright (c) 2001-2004 the xdvik development team
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to
8  * deal in the Software without restriction, including without limitation the
9  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10  * sell copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19  * IN NO EVENT SHALL PAUL VOJTA OR ANY OTHER AUTHOR OF THIS SOFTWARE BE
20  * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22  * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23  */
24 
25 /*  #define MYDEBUG 1 */
26 
27 #include <string.h>
28 #include <stdio.h>
29 #include <locale.h>
30 #include <ctype.h>
31 #include "xdvi-config.h"
32 #include "xdvi.h"
33 #include "string-utils.h"
34 #include "util.h"
35 
36 /*------------------------------------------------------------
37  *  str_is_prefix
38  *
39  *  Purpose:
40  *	Return True if <str1> is a prefix of <str2>, else False.
41  *
42  *	If `case_sensitive' is set to False, it performs matching
43  *	in a case-insensitive manner; in that case, str1 should
44  *	be lowercase.
45  *------------------------------------------------------------*/
46 
47 Boolean
str_is_prefix(const char * str1,const char * str2,Boolean case_sensitive)48 str_is_prefix(const char *str1, const char *str2, Boolean case_sensitive)
49 {
50     int i;
51 
52     for (i = 0; *str1 != '\0' && *str2 != '\0'; i++) {
53 	if ((case_sensitive && *str1 != *str2) ||
54 	    (!case_sensitive && *str1 != tolower((int)*str2)))
55 	    return False;
56 	str1++;
57 	str2++;
58     }
59     return *str1 == '\0';
60 }
61 
62 
63 
64 /*------------------------------------------------------------
65  *  str_is_suffix
66  *
67  *  Purpose:
68  *	Return True if str1 is a suffix of str2, else False.
69  *	E.g. returns true if str1 = ".tex", str2 = "test.tex".
70  *
71  *	If `case_sensitive' is set to False, it performs matching
72  *	in a case-insensitive manner; in that case, str1 should
73  *	be lowercase.
74  *------------------------------------------------------------*/
75 
76 Boolean
str_is_suffix(const char * str1,const char * str2,Boolean case_sensitive)77 str_is_suffix(const char *str1, const char *str2, Boolean case_sensitive)
78 {
79     int len1 = strlen(str1);
80     int len2 = strlen(str2);
81 
82     while (len2 > len1) {
83 	str2++;
84 	len2--;
85     }
86     if (case_sensitive)
87 	return strcmp(str1, str2) == 0;
88     else
89 	/* also compare terminating 0; second string
90 	   is assumed to be all-lowercase! */
91 	return memicmp(str2, str1, len1 + 1) == 0;
92 }
93 
94 /*
95  * Like strstr(), but does case-insensitive matching: Brute-force algorithm
96  * to find the first occurrence of subsrtring `needle' (which should be all-lowercase)
97  * in string `haystack', ignoring case in (i.e. lowercasing) haystack. The terminating
98  * '\0' characters are not compared.
99  * Returns a pointer to the beginning of the substring if found, NULL else.
100  *
101  * This code was derived from public domain code (Stristr.c on www.snippets.org).
102  * Currently unused.
103  */
104 char *
my_stristr(const char * haystack,const char * needle)105 my_stristr(const char *haystack, const char *needle)
106 {
107     const char *curr;
108 
109     for (curr = haystack; *curr != '\0'; curr++) {
110 	const char *ptr1, *ptr2;
111 	/* search for first character */
112 	for (; ((*curr != '\0') && (tolower((int)*curr) != *needle)); curr++) { ; }
113 
114 	if (*curr == '\0') /* not found */
115 	    return NULL;
116 
117 	/* now compare other characters */
118 	ptr1 = needle;
119 	ptr2 = curr;
120 	while (tolower((int)*ptr2) == *ptr1) {
121 	    ptr2++;
122 	    ptr1++;
123 	    /* success if at end of needle */
124 	    if (*ptr1 == '\0')
125 		return (char *)curr;
126 	}
127     }
128     return NULL;
129 }
130 
131 /*
132   expand filename to include full path name;
133   returns result in a freshly allocated string.
134 */
135 char *
expand_filename(const char * filename,expandPathTypeT type)136 expand_filename(const char *filename, expandPathTypeT type)
137 {
138     char *path_name = NULL;
139 
140     if (*filename == '/') /* already an absolute path */
141 	return xstrdup(filename);
142 
143     if (type == USE_CWD_PATH) {
144 	size_t path_name_len = 512;
145 	size_t len = strlen(filename) + 1;
146 
147 	/* append to cwd if it's not already a full path */
148 	if (filename[0] != '/') {
149 	    for (;;) {
150 		char *tmp;
151 		path_name = xrealloc(path_name, path_name_len);
152 		if ((tmp = getcwd(path_name, path_name_len)) == NULL && errno == ERANGE) {
153 		    path_name_len *= 2;
154 		}
155 		else {
156 		    path_name = tmp;
157 		    break;
158 		}
159 	    }
160 	    len += strlen(path_name) + 1;	/* 1 for '/' */
161 	    path_name = xrealloc(path_name, len);
162 	    strcat(path_name, "/");
163 	    strcat(path_name, filename);
164 	}
165 
166 	TRACE_HTEX((stderr,
167 		    "Expanding filename |%s| with CWD gives |%s|",
168 		    filename, path_name == NULL ? "<NULL>" : path_name));
169 	return path_name ? path_name : xstrdup(filename);
170     }
171     else {
172 	ASSERT(globals.dvi_file.dirname != NULL, "globals.dvi_file.dirname should have been initialized before");
173 	path_name = xstrdup(globals.dvi_file.dirname);
174 	path_name = xstrcat(path_name, filename);
175 	TRACE_HTEX((stderr,
176 		    "Expanding filename |%s| with globals.dvi_file.dirname |%s| gives |%s|",
177 		    filename, globals.dvi_file.dirname, path_name));
178 	return path_name;
179     }
180 }
181 
182 /* expand filename to include `.dvi' extension and full path name;
183    returns malloc()ed string (caller is responsible for free()ing).
184 */
185 char *
filename_append_dvi(const char * filename)186 filename_append_dvi(const char *filename)
187 {
188     char *expanded_filename = NULL;
189     const char *orig_filename = filename;
190     size_t len;
191     char *p;
192 
193     /* skip over `file:' prefix if present */
194     if (str_is_prefix("file:", filename, True)) {
195 	filename += strlen("file:");
196 	if (str_is_prefix("//", filename, True)) { /* is there a hostname following? */
197 	    char *tmp = strchr(filename+2, '/'); /* skip over host name */
198 	    if (tmp == NULL) {
199 		XDVI_WARNING((stderr, "Malformed hostname part in filename `%s'; not expanding file name",
200 			      orig_filename));
201 	    }
202 	    else {
203 		/* deal with multiple `//' after "file://localhost";
204 		   while the RFC seems to suggest that file://localhost/foo/bar defines a path
205 		   `foo/bar', most browsers (and actually, also libwww) will treat this path as absolute:
206 		   `/foo/bar'. So we treat
207 		   file://localhost//foo/bar
208 		   and
209 		   file://localhost/foo/bar
210 		   as equivalent.
211 		*/
212 		while (*(tmp+1) == '/')
213 		    tmp++;
214 		filename = tmp;
215 	    }
216 	}
217     }
218 
219     len = strlen(filename) + 5; /* 5 in case we need to add `.dvi\0' */
220 
221     expanded_filename = xmalloc(len);
222 
223     strcpy(expanded_filename, filename);
224 
225     /* Append ".dvi" extension if no extension is present.
226        Only look at the filename part when trying to find a `.'.
227     */
228     if ((p = strrchr(expanded_filename, '/')) == NULL) {
229 	p = expanded_filename;
230     }
231     if ((p = strrchr(p, '.')) == NULL) {
232 	TRACE_HTEX((stderr, "appending .dvi extension to filename |%s|", expanded_filename));
233 	strcat(expanded_filename, ".dvi");
234     }
235     return expanded_filename;
236 }
237 
238 char *
expand_filename_append_dvi(const char * filename,expandPathTypeT type,Boolean must_exist)239 expand_filename_append_dvi(const char *filename, expandPathTypeT type, Boolean must_exist)
240 {
241     char canonical_path[MAXPATHLEN + 1];
242     char *normalized_fname = filename_append_dvi(filename);
243     char *expanded_fname = expand_filename(normalized_fname, type);
244     if (must_exist) {
245 	char *canonical_name = REALPATH(expanded_fname, canonical_path);
246 	free(normalized_fname);
247 	free(expanded_fname);
248 	return xstrdup(canonical_name);
249     }
250     else {
251 	free(normalized_fname);
252 	return expanded_fname;
253     }
254 }
255 
256 char *
format_arg(const char * fmt,const char * arg,int * match)257 format_arg(const char *fmt, const char *arg, int *match)
258 {
259     char *tmp = xmalloc(strlen(fmt) + strlen(arg) + 1);
260     if (strchr(fmt, '%') != NULL) {
261 	sprintf(tmp, fmt, arg);
262 	*match = 1;
263     }
264     else {
265 	strcpy(tmp, fmt);
266 	/* NOTE: don't reset *match to 0, leave that to caller */
267     }
268     return tmp;
269 }
270 
271 /* escape single `%' characters in arg and return newly allocated string */
272 char *
escape_format_arg(const char * arg)273 escape_format_arg(const char *arg)
274 {
275     char *ret, *ptr;
276     ASSERT(arg != NULL, "");
277     ret = xmalloc(strlen(arg) * 2 + 1); /* more than enuff */
278 
279     ptr = ret;
280     while (*arg != '\0') {
281 	/* need to escape? */
282 	if (*arg == '%') { /* && (ptr == ret
283 			    || (ptr > ret && *(arg - 1) != '%'))) { */
284 	    *ptr++ = '%';
285 	}
286 	*ptr++ = *arg++;
287     }
288     *ptr = '\0';
289 
290     /* trim return buffer */
291     return xrealloc(ret, strlen(ret) + 1);
292 }
293 
294 char *
unquote_arg(const char * fmt,const char * arg,int * match,int * len)295 unquote_arg(const char *fmt, const char *arg, int *match, int *len)
296 {
297     char *ptr;
298     char c;
299 
300     c = fmt[0];
301     fmt++;
302     if ((ptr = strchr(fmt, c)) != NULL) {
303 	*ptr++ = '\0';
304 	while (*ptr == ' ' || *ptr == '\t') {
305 	    ptr++;
306 	}
307 	*len = ptr - fmt;
308 	return format_arg(fmt, arg, match);
309     }
310     else {
311 	*len = strlen(fmt);
312 	XDVI_WARNING((stderr, "Ignoring lonesome quote in string %s", fmt - 1));
313 	return format_arg(fmt, arg, match);
314     }
315 }
316 
317 /* Chop `source' into chunks separated by `sep', and save these
318  * to newly allocated return list. The end of the list is indicated
319  * by a NULL element (i.e. the returned list will always contain at
320  * least 1 element). The caller is responsible for free()ing the returned
321  * list.
322  *
323  * If `do_unquote' is True, separators inside single or double quotation marks will not be
324  * treated as boundaries. The quotation marks surrounding the chunk will be removed
325  * as well in that case.
326  */
327 char **
get_separated_list(const char * source,const char * sep,Boolean do_unquote)328 get_separated_list(const char *source, const char *sep, Boolean do_unquote)
329 {
330     /* allocate at least list of size 1, for NULL termination below */
331     char **chunks = xmalloc(sizeof *chunks);
332     const char *b_ptr, *e_ptr;
333     size_t i = 0;
334 
335     b_ptr = source;
336 
337     while (*b_ptr != '\0' && strchr(sep, *b_ptr) != NULL)
338 	b_ptr++;
339 
340     for (i = 0; *b_ptr != '\0'; i++) {
341 	char *quote;
342 	char quotechar = 0;
343 	size_t len, chunk_len, alloc_len = 0;
344 
345 	if ((len = strcspn(b_ptr, sep)) == 0) /* at end */
346 	    break;
347 
348 	/* check for quoted chunk, in which case we don't want to treat
349 	   spaces as chunk separators */
350 	if (do_unquote && (quote = strchr("'\"", *b_ptr)) != NULL) {
351 	    const char *curr = b_ptr + 1;
352 	    quotechar = *quote;
353 
354 	    for (;;) { /* find end of quote */
355 		char *maybe_end = strchr(curr, quotechar);
356 		if (maybe_end != NULL) {
357 		    if (maybe_end - curr > 0 &&
358 			*(maybe_end - 1) == '\\') { /* escaped quote */
359 			curr = ++maybe_end;
360 		    }
361 		    else { /* found */
362 			b_ptr++;
363 			len = maybe_end - b_ptr;
364 			break;
365 		    }
366 		}
367 		else { /* not found end - warn, and forget about this quote */
368 		    XDVI_WARNING((stderr, "Unmatched quote character in string `%s'", b_ptr));
369 		    break;
370 		}
371 	    }
372 	}
373 
374 	e_ptr = b_ptr + len;
375 	chunk_len = e_ptr - b_ptr;
376 	while (i + 1 >= alloc_len) {
377 	    alloc_len++;
378 	}
379 	chunks = xrealloc(chunks, alloc_len * sizeof *chunks);
380 	chunks[i] = xmalloc(chunk_len + 1);
381 	memcpy(chunks[i], b_ptr, chunk_len);
382 	chunks[i][chunk_len] = '\0';
383 
384 	/* skip trailing quotechar and spaces */
385 	b_ptr = e_ptr;
386 	if (do_unquote && quotechar != 0 && *b_ptr == quotechar)
387 	    b_ptr++;
388 	while (*b_ptr != '\0' && strchr(sep, *b_ptr) != NULL)
389 	    b_ptr++;
390     }
391     /* terminate list with NULL element */
392     chunks[i] = NULL;
393     return chunks;
394 }
395 
396 const char *
find_format_str(const char * input,const char * fmt)397 find_format_str(const char *input, const char *fmt)
398 {
399     const char *ptr = input;
400     while ((ptr = strstr(ptr, fmt)) != NULL) {
401 	if (ptr > input && *(ptr - 1) == '\\') {
402 	    ptr++;
403 	    continue;
404 	}
405 	else
406 	    return ptr;
407     }
408     return NULL;
409 }
410 
411 /* return directory component of `path', or NULL if path is a filename only */
412 char *
get_dir_component(const char * path)413 get_dir_component(const char *path)
414 {
415     char *ret = NULL;
416     char *p;
417 
418     if ((p = strrchr(path, '/')) != NULL) {
419 	/* copy, chop off filename (but not the '/') */
420 	ret = xstrdup(path);
421 	*(ret + (p - path) + 1) = '\0';
422 	TRACE_CLIENT((stderr, "get_dir_component of |%s| is |%s|\n", path, ret));
423     }
424     return ret;
425 }
426 
427 /*
428   If `src' is an absolute path, compare it with `target', ignoring `.tex' extension in
429   both strings. Else, compare the concatenation of `src' with `dvi_path' and `target',
430   in the same way.
431   Since efficiency is a concern here, we don't copy any strings; instead, we loop through
432   `src' and `target' from the end of both strings (which makes expanding `../' easier, and
433   will terminate earlier for non-equal files), replacing `src' with `dvi_path' when
434   dropping off the beginning of `src'.
435 */
436 Boolean
src_compare(const char * src,int src_len,const char * target,const char * dvi_path,size_t dvi_path_len)437 src_compare(const char *src, int src_len, const char *target, const char *dvi_path, size_t dvi_path_len)
438 {
439     int i, j;
440     Boolean matched = True;
441 
442     /* This macro sets the `src' pointer to `dvi_path' after having
443        dropped off the beginning of src, or returns False if dvi_path
444        is NULL (which either means that `src' was an absolute path, or
445        that the dvi_path has been used up already).
446     */
447 #define CHK_USE_DIR(i)								\
448 	if (i == -1) {								\
449 	    if (dvi_path == NULL) /* already done */				\
450 		return False;							\
451 	    src = dvi_path;							\
452 	    dvi_path = NULL;							\
453 	    i = dvi_path_len - 1;						\
454 	    MYTRACE((stderr, "swapped, now using: |%s| of len %d", src, i));	\
455 	}
456 
457     /* ignore path in both filenames if one of them does not contain a path */
458     {
459 	char *src_p = strrchr(src, '/');
460 	char *target_p = strrchr(target, '/');
461 
462 	if (src_p == NULL) {
463 	    dvi_path = NULL;
464 	    if (target_p != NULL)
465 		target = target_p + 1;
466 	}
467 
468 	if (target_p == NULL) {
469 	    dvi_path = NULL;
470 	    if (src_p != NULL)
471 		src = src_p + 1;
472 	}
473     }
474 
475     /* don't prepend dvi_path if src is absolute */
476     if (*src == '/') {
477 	dvi_path = NULL;
478     }
479 
480     /* skip `.tex' suffix in strings if present */
481     i = src_len;
482     MYTRACE((stderr, "len of |%s| %d", src, i));
483     if (i >= 4 && strcmp(src + (i - 4), ".tex") == 0) {
484 	MYTRACE((stderr, "src |%s| has .tex suffix!", src));
485 	i -= 4;
486     }
487 
488     j = strlen(target);
489     MYTRACE((stderr, "len of |%s| %d", target, j));
490     if (j >= 4 && strcmp(target + (j - 4), ".tex") == 0) {
491 	MYTRACE((stderr, "target |%s| has .tex suffix!", target));
492 	j -= 4;
493     }
494 
495     /* start with index of last char */
496     i--;
497     j--;
498 
499     while (i >= 0 && j >= 0) {
500 	int skip_dirs = 0;
501 	/*  	fprintf(stderr, "check: %d[%c]\n", i, src[i]); */
502 	while (src[i] == '.' && src[i + 1] == '/') { /* normalize `../' and `./' */
503 	    MYTRACE((stderr, "check2: %d[%c]", i, src[i]));
504 
505 	    if (i >= 2 && src[i - 1] == '.' && src[i - 2] == '/') {
506 		MYTRACE((stderr, "case /.."));
507 		i -= 3;
508 		skip_dirs++;
509 	    }
510 	    else if (i == 1) { /* `../' or `/./' at start of src */
511 		if (src[0] == '.') { /* `../' */
512 		    MYTRACE((stderr, "../ at start"));
513 		    i -= 2;
514 		    skip_dirs++;
515 		}
516 		else if (src[0] == '/') { /* `/./' */
517 		    MYTRACE((stderr, "/./ at start"));
518 		    i -= 1;
519 		}
520 		else /* something else */
521 		    break;
522 	    }
523 	    else if (i == 0 || (i > 1 && src[i - 1] == '/')) { /* ./ at start, or /./ somewhere */
524 		i -= 1;
525 	    }
526 	    else /* might be `abc./' or `abc../' (strange but legal) */
527 		break;
528 
529 	    CHK_USE_DIR(i);
530 	    while (i >= 0 && src[i] == '/') i--;
531 	    CHK_USE_DIR(i);
532 
533 	    while (src[i] != '.' && skip_dirs-- > 0) { /* unless there are subsequent `../' */
534 		/* skip directories backwards */
535 		while (i >= 0 && src[i] != '/') {
536 		    MYTRACE((stderr, "non-slash: %d,%c", i, src[i]));
537 		    i--;
538 		}
539 		CHK_USE_DIR(i);
540 		while (i >= 0 && src[i] == '/') {
541 		    MYTRACE((stderr, "slash: %d,%c", i, src[i]));
542 		    i--;
543 		}
544 		CHK_USE_DIR(i);
545 		MYTRACE((stderr, "skipped backwards: %d,%c", i, src[i]));
546 	    }
547 	}
548 
549 	/* skip multiple '/'. No need to fall off the beginning of src and use
550 	   CHK_USE_DIR() here, since with (multiple) '/' at the beginning, src
551 	   must be an absolute path. */
552 	while (i > 0 && src[i] == '/' && src[i - 1] == '/') {
553 	    i--;
554 	}
555 
556 	MYTRACE((stderr, "comparing: %d[%c] - %d[%c]", i, src[i], j, target[j]));
557 	if (src[i] != target[j]) {
558 	    matched = False;
559 	    break;
560 	}
561 	if (i == 0 && j == 0) /* at start of both strings */
562 	    break;
563 	i--;
564 	j--;
565 	CHK_USE_DIR(i);
566     }
567 
568     if (i != 0 || j != 0) /* not at start of both strings, no match */
569 	return False;
570     else
571 	return matched;
572 #undef CHK_USE_DIR
573 }
574 
575 
576 /*
577   Contributed by ZLB: Return a canonicalized version of path (with `../'
578   and `./' resolved and `//' reduced to '/') in a freshly malloc()ed
579   buffer, which the caller is responsible for free()ing. Cannot fail.
580 */
581 char *
canonicalize_path(const char * path)582 canonicalize_path(const char *path)
583 {
584     char *p, *q, *start;
585     char c;
586     size_t len = strlen(path);
587 
588     assert(path != NULL);
589     assert(*path == '/');
590 
591     start = q = p = xstrdup(path);
592 
593     /* len is the length of string */
594     while (p < start + len) {
595 	if ((c = p[1]) == '/') {
596 	    /* remove multiple '/' in pathname */
597 	    memmove(p + 1, p + 2, len - (p + 2 - start) + 1);
598 	    len--;
599 	    continue;
600 	}
601 	else if (c == '.') {
602 	    if ((c = p[2]) == '/') {
603 		/* p = '/.' in pathname */
604 		memmove(p + 1, p + 3, len - (p + 3 - start) + 1);
605 		len -= 2;
606 		continue;
607 	    }
608 	    else if (c == '.' && ((c = p[3]) == '/' || c == '\0')) {
609 		/* p == "/.." */
610 		memmove(q, p + 3, len - (p + 3 - start) + 1);
611 		len -= (p - q) + 3;
612 		p = q;
613 		/* check if the new dirname at p is "//" or './' or '../' */
614 		if ((c = p[1]) == '/')
615 		    continue;
616 		else if (c == '.') {
617 		    if ((c = p[2]) == '/')
618 			continue;
619 		    else if (c == '.' && ((c = p[3]) == '/' || c == '\0')) {
620 			while (--q >= start && *q != '/');
621 			if (q < start)
622 			    q = start;
623 			continue;
624 		    }
625 		}
626 	    }
627 	}
628 
629 	/* search next '/' */
630 	q = p;
631 	while (++p <= start + len && *p != '/');
632     }
633 
634     return start;
635 }
636 
637 /* Escape all of the following characters in str:
638    ` \ ; ( )
639    making it safe to pass str to a shell. Return result in a newly
640    allocated string, which the caller is responsible to free() after use.
641 */
642 char *
shell_escape_string(const char * str)643 shell_escape_string(const char *str)
644 {
645     size_t len = strlen(str);
646     char *new_str = xmalloc(len * 2 + 1); /* safe amount, since each char will be doubled at most */
647 
648     const char *src_ptr = str;
649     char *target_ptr = new_str;
650     while (*src_ptr != '\0') {
651 	if (*src_ptr == '\\'
652 	    || *src_ptr == '`'
653 	    || *src_ptr == '('
654 	    || *src_ptr == ')'
655 	    || *src_ptr == ';') {
656 #if 0
657 	    /* only if not yet escaped? */
658  	    && (src_ptr == str || (src_ptr > str && *(src_ptr - 1) != '\\'))) {
659 #endif
660 	    *target_ptr++ = '\\';
661 	}
662 	*target_ptr++ = *src_ptr++;
663     }
664     *target_ptr = '\0'; /* terminate */
665     return new_str;
666 }
667 
668 /* Get a pointer to the extension of the filename 'fname'
669    (ie. into the existing string),
670    or NULL if fname doens't have an extension.
671  */
672 const char *
673 get_extension(const char *fname)
674 {
675     char *sep, *tmp;
676     /* does filename have a directory component?
677        If so, be careful with dots within this component.
678     */
679     if ((sep = strrchr(fname, '/')) != NULL) {
680 	tmp = sep;
681 	if ((sep = strrchr(tmp, '.')) != NULL) {
682 	    return sep;
683 	}
684 	else {
685 	    return NULL;
686 	}
687     }
688     else if ((sep = strrchr(fname, '.')) != NULL) {
689 	return sep;
690     }
691     else {
692 	return NULL;
693     }
694 }
695 
696 void
697 replace_extension(const char *fname, const char *extension,
698 		  char *buf, size_t buf_len)
699 {
700     char *sep;
701     if ((sep = strrchr(fname, '.')) != NULL) {
702 	size_t len = strlen(extension);
703 	if (len + (sep - fname) > buf_len)
704 	    return;
705 	strncpy(buf, fname, sep - fname);
706 	strcpy(buf + (sep - fname), extension);
707     }
708     return;
709 }
710 
711 #if 0
712 /*
713  * Estimate the string length needed for %p conversion. Currently unused,
714  * since we use the more general VSNPRINTF() approach.
715  */
716 #define PTR_CONVERSION_LEN_GUESS sizeof(void *) * CHAR_BIT
717 #endif
718 
719 
720 /*
721  * Return a formatted string in newly allocated memory.
722  */
723 char *
724 get_string_va(const char *fmt, ...)
725 {
726     char *buf = NULL;
727     XDVI_GET_STRING_ARGP(buf, fmt);
728     return buf;
729 }
730 
731 /* Wrapper for atof() for strtod()-like error checking,
732  * with an XDVI_WARNING if the conversion of <str> wasn't complete.
733  */
734 double
735 my_atof(const char *str)
736 {
737     char *ptr;
738     double f;
739 
740     f = strtod(str, (char **)&ptr);
741     if (*ptr != '\0') {
742 	XDVI_WARNING((stderr, "strtod: incomplete conversion of %s to %f", str, f));
743     }
744     return f;
745 }
746 
747 /* return length of a string representation of the integer n */
748 int
749 length_of_int(int n)
750 {
751     int ret = 1;
752 
753     if (n < 0) {
754 	ret++;
755 	n *= -1;
756     }
757     while (n >= 10) {
758 	n /= 10;
759 	ret++;
760     }
761 
762     return ret;
763 }
764 
765 Boolean
766 is_spaces_only(const char *ptr)
767 {
768     for (; *ptr; ptr++) {
769 	if (!isspace((int)*ptr))
770 	    return False;
771     }
772     return True;
773 }
774