xref: /freebsd/lib/libbsdconf/bsdconf_string.c (revision 3fe5961a0b708da599d42cbb6b5e4f030c28ea45)
1 /*
2  * Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
3  * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
4  *
5  * SPDX-License-Identifier: BSD-2-Clause
6  */
7 
8 #include <ctype.h>
9 #include <errno.h>
10 #include <stdlib.h>
11 #include <string.h>
12 
13 #include "bsdconf_internal.h"
14 
15 /*
16  * Counts the number of occurrences of one string that appear in the source
17  * string. Return value is the total count.
18  *
19  * An example use would be if you need to know how large a buffer needs to be
20  * for a bsdconf_replaceall() series.
21  */
22 unsigned int
bsdconf_strcount(const char * source,const char * find)23 bsdconf_strcount(const char *source, const char *find)
24 {
25 	const char *p;
26 	size_t flen;
27 	unsigned int n = 0;
28 
29 	if (source == NULL || find == NULL || *source == '\0' || *find == '\0')
30 		return (0);
31 
32 	flen = strlen(find);
33 	for (p = source; (p = strstr(p, find)) != NULL; p += flen)
34 		n++;
35 
36 	return (n);
37 }
38 
39 /*
40  * Replaces all occurrences of `find' in `buf' with `replace'.
41  *
42  * `buf' must point to a mutable buffer of at least `buflen' bytes (including
43  * space for the terminating NUL). A string constant will not compile as the
44  * first argument without a cast; do not cast one in. A local or global
45  * non-const array is fine.
46  *
47  * The result is always built in a temporary buffer and copied back, so the
48  * same path is taken whether `replace' is longer or shorter than `find'.
49  * Pass a `buflen' large enough for the expanded result (bsdconf_strcount()
50  * can size it); if the result would not fit, -1 is returned with errno set
51  * to ENOSPC and `buf' is left unmodified. On success the return value is
52  * the length (in bytes) of the result, not counting the terminating NUL.
53  *
54  * When an error occurs, -1 is returned and the global variable errno is set
55  * accordingly.
56  */
57 int
bsdconf_replaceall(char * buf,size_t buflen,const char * find,const char * replace)58 bsdconf_replaceall(char *buf, size_t buflen, const char *find,
59     const char *replace)
60 {
61 	char *dst;
62 	char *out;
63 	const char *hit;
64 	const char *src;
65 	size_t flen, need, rlen, slen;
66 	unsigned int n;
67 
68 	if (buf == NULL)
69 		return (0);
70 	if (find == NULL)
71 		return ((int)strlen(buf));
72 
73 	slen = strlen(buf);
74 	flen = strlen(find);
75 	rlen = replace != NULL ? strlen(replace) : 0;
76 
77 	if (slen == 0 || flen == 0 || slen < flen)
78 		return ((int)slen);
79 
80 	n = bsdconf_strcount(buf, find);
81 	if (n == 0)
82 		return ((int)slen);
83 
84 	if (rlen >= flen)
85 		need = slen + (size_t)n * (rlen - flen) + 1;
86 	else
87 		need = slen - (size_t)n * (flen - rlen) + 1;
88 	if (need > buflen) {
89 		errno = ENOSPC;
90 		return (-1);
91 	}
92 
93 	if ((out = malloc(need)) == NULL)
94 		return (-1);
95 
96 	dst = out;
97 	src = buf;
98 	while ((hit = strstr(src, find)) != NULL) {
99 		memcpy(dst, src, (size_t)(hit - src));
100 		dst += hit - src;
101 		if (rlen > 0) {
102 			memcpy(dst, replace, rlen);
103 			dst += rlen;
104 		}
105 		src = hit + flen;
106 	}
107 	memcpy(dst, src, strlen(src) + 1);
108 	memcpy(buf, out, need);
109 	free(out);
110 
111 	return ((int)(need - 1));
112 }
113 
114 /*
115  * Unexpands (collapses) C-style escape sequences in `src' into `dst'.
116  *
117  * The result is never longer than the input, so `dst' may be the same buffer
118  * as `src' (cf. strunvis(3)). Do not pass a string constant as `dst'; a local
119  * or global non-const array is fine.
120  *
121  * Interpreted sequences are:
122  *
123  * 	\NNN	character with octal value NNN (1 to 3 digits)
124  * 	\a	alert (BEL)
125  * 	\b	backspace
126  * 	\f	form feed
127  * 	\n	new line
128  * 	\r	carriage return
129  * 	\t	horizontal tab
130  * 	\v	vertical tab
131  * 	\xNN	byte with hexadecimal value NN (1 to 2 digits)
132  *
133  * All other sequences are unescaped (ie. '\"' and '\#'). A trailing backslash
134  * or a `\x' with no following hex digits is emitted verbatim (the backslash
135  * is dropped for `\x', leaving `x').
136  */
137 void
bsdconf_strunexpand(char * dst,const char * src)138 bsdconf_strunexpand(char *dst, const char *src)
139 {
140 	char *d;
141 	const char *s;
142 	unsigned int n, v;
143 	unsigned char c;
144 
145 	d = dst;
146 	s = src;
147 
148 	/*
149 	 * Loop until we hit the end of the input. The stop condition must
150 	 * track the input cursor (s): collapsing an escape advances s ahead
151 	 * of the output cursor (d), so once the two diverge *d no longer
152 	 * reflects where the input terminates.
153 	 */
154 	while (*s != '\0') {
155 		if (*s != '\\') {
156 			*d++ = *s++;
157 			continue;
158 		}
159 
160 		/*
161 		 * A backslash at the very end of the string escapes nothing
162 		 * (there is no next character); emit it verbatim and stop
163 		 * rather than read past the terminator.
164 		 */
165 		if (*(s + 1) == '\0') {
166 			*d++ = *s++;
167 			break;
168 		}
169 
170 		/* Replace the backslash with the correct character */
171 		s++;
172 		switch (*s) {
173 		case 'a': *d = '\a'; break; /* bell/alert (BEL) */
174 		case 'b': *d = '\b'; break; /* backspace */
175 		case 'f': *d = '\f'; break; /* form feed */
176 		case 'n': *d = '\n'; break; /* new line */
177 		case 'r': *d = '\r'; break; /* carriage return */
178 		case 't': *d = '\t'; break; /* horizontal tab */
179 		case 'v': *d = '\v'; break; /* vertical tab */
180 		case 'x': /* hex value (1 to 2 digits)(\xNN) */
181 			v = 0;
182 			n = 0;
183 			while (n < 2) {
184 				c = (unsigned char)*(s + 1);
185 				if (c >= '0' && c <= '9')
186 					v = (v << 4) + (c - '0');
187 				else if (c >= 'A' && c <= 'F')
188 					v = (v << 4) + (c - 'A' + 10);
189 				else if (c >= 'a' && c <= 'f')
190 					v = (v << 4) + (c - 'a' + 10);
191 				else
192 					break;
193 				s++;
194 				n++;
195 			}
196 			/* \x with no digits: emit the 'x' (unknown escape) */
197 			*d = (n == 0) ? 'x' : (char)v;
198 			break;
199 		default: /* octal (\NNN, 1 to 3 digits) or unknown sequence */
200 			if (*s >= '0' && *s <= '7') {
201 				v = (unsigned int)(*s - '0');
202 				n = 1;
203 				while (n < 3 && *(s + 1) >= '0' &&
204 				    *(s + 1) <= '7') {
205 					s++;
206 					v = (v << 3) +
207 					    (unsigned int)(*s - '0');
208 					n++;
209 				}
210 				*d = (char)v;
211 			} else
212 				*d = *s;
213 			break;
214 		}
215 
216 		/* Increment to next offset, possible next escape sequence */
217 		d++;
218 		s++;
219 	}
220 
221 	/*
222 	 * Terminate at the (possibly earlier) output cursor. When any
223 	 * escape was collapsed the string shrank, so the trailing bytes
224 	 * between d and s are now stale and must be cut off here.
225 	 */
226 	*d = '\0';
227 }
228 
229 /*
230  * Convert a string to lower case. Pass a mutable buffer (a local or global
231  * non-const array is fine); do not pass a string constant.
232  */
233 void
bsdconf_strtolower(char * buf)234 bsdconf_strtolower(char *buf)
235 {
236 	char *p = buf;
237 
238 	if (buf == NULL)
239 		return;
240 
241 	while (*p != '\0') {
242 		*p = (char)tolower((unsigned char)*p);
243 		p++;
244 	}
245 }
246