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