xref: /freebsd/contrib/pkgconf/libpkgconf/path.c (revision f5dc2263ab1be8a35a7e27e82103f9ccd41ae584)
1 /*
2  * path.c
3  * filesystem path management
4  *
5  * SPDX-License-Identifier: pkgconf
6  *
7  * Copyright (c) 2016 pkgconf authors (see AUTHORS).
8  *
9  * Permission to use, copy, modify, and/or distribute this software for any
10  * purpose with or without fee is hereby granted, provided that the above
11  * copyright notice and this permission notice appear in all copies.
12  *
13  * This software is provided 'as is' and without any warranty, express or
14  * implied.  In no event shall the authors be liable for any damages arising
15  * from the use of this software.
16  */
17 
18 #include <libpkgconf/config.h>
19 #include <libpkgconf/stdinc.h>
20 #include <libpkgconf/libpkgconf.h>
21 #include <libpkgconf/path.h>
22 
23 #if defined(HAVE_SYS_STAT_H) && ! defined(_WIN32)
24 # include <sys/stat.h>
25 # define PKGCONF_CACHE_INODES
26 #endif
27 
28 static bool
29 #ifdef PKGCONF_CACHE_INODES
30 path_list_contains_entry(const pkgconf_buffer_t *text, pkgconf_list_t *dirlist, struct stat *st)
31 #else
32 path_list_contains_entry(const pkgconf_buffer_t *text, pkgconf_list_t *dirlist)
33 #endif
34 {
35 	pkgconf_node_t *n;
36 
37 	PKGCONF_FOREACH_LIST_ENTRY(dirlist->head, n)
38 	{
39 		pkgconf_path_t *pn = n->data;
40 
41 #ifdef PKGCONF_CACHE_INODES
42 		if (pn->handle_device == (void *)(intptr_t)st->st_dev && pn->handle_path == (void *)(intptr_t)st->st_ino)
43 			return true;
44 #endif
45 
46 		if (!strcmp(pkgconf_buffer_str(text), pn->path))
47 			return true;
48 	}
49 
50 	return false;
51 }
52 
53 /*
54  * !doc
55  *
56  * libpkgconf `path` module
57  * ========================
58  *
59  * The `path` module provides functions for manipulating lists of paths in a cross-platform manner.  Notably,
60  * it is used by the `pkgconf client` to parse the ``PKG_CONFIG_PATH``, ``PKG_CONFIG_LIBDIR`` and related environment
61  * variables.
62  */
63 
64 static pkgconf_path_t *
65 prepare_path_node(const char *text, pkgconf_list_t *dirlist, bool filter)
66 {
67 	pkgconf_path_t *node;
68 	pkgconf_buffer_t pathbuf = PKGCONF_BUFFER_INITIALIZER;
69 
70 	pkgconf_buffer_append(&pathbuf, text);
71 	pkgconf_path_relocate(&pathbuf);
72 
73 #ifdef PKGCONF_CACHE_INODES
74 	struct stat st;
75 
76 	if (filter)
77 	{
78 		if (lstat(pkgconf_buffer_str(&pathbuf), &st) == -1)
79 		{
80 			pkgconf_buffer_finalize(&pathbuf);
81 			return NULL;
82 		}
83 
84 		if (S_ISLNK(st.st_mode))
85 		{
86 			char realpathbuf[PKGCONF_ITEM_SIZE * 4];
87 			char *linkdest = realpath(pkgconf_buffer_str(&pathbuf), realpathbuf);
88 
89 			if (linkdest != NULL && stat(linkdest, &st) == -1)
90 			{
91 				pkgconf_buffer_finalize(&pathbuf);
92 				return NULL;
93 			}
94 		}
95 
96 		if (path_list_contains_entry(&pathbuf, dirlist, &st))
97 		{
98 			pkgconf_buffer_finalize(&pathbuf);
99 			return NULL;
100 		}
101 	}
102 #else
103 	if (filter && path_list_contains_entry(&pathbuf, dirlist))
104 	{
105 		pkgconf_buffer_finalize(&pathbuf);
106 		return NULL;
107 	}
108 #endif
109 
110 	node = calloc(1, sizeof(pkgconf_path_t));
111 	if (node == NULL)
112 	{
113 		pkgconf_buffer_finalize(&pathbuf);
114 		return NULL;
115 	}
116 
117 	node->path = pkgconf_buffer_freeze(&pathbuf);
118 	if (node->path == NULL)
119 	{
120 		free(node);
121 		return NULL;
122 	}
123 
124 #ifdef PKGCONF_CACHE_INODES
125 	if (filter)
126 	{
127 		node->handle_path = (void *)(intptr_t) st.st_ino;
128 		node->handle_device = (void *)(intptr_t) st.st_dev;
129 	}
130 #endif
131 
132 	return node;
133 }
134 
135 /*
136  * !doc
137  *
138  * .. c:function:: void pkgconf_path_add(const char *text, pkgconf_list_t *dirlist)
139  *
140  *    Adds a path node to a path list.  If the path is already in the list, do nothing.
141  *
142  *    :param char* text: The path text to add as a path node.
143  *    :param pkgconf_list_t* dirlist: The path list to add the path node to.
144  *    :param bool filter: Whether to perform duplicate filtering.
145  *    :return: nothing
146  */
147 void
148 pkgconf_path_add(const char *text, pkgconf_list_t *dirlist, bool filter)
149 {
150 	pkgconf_path_t *node = prepare_path_node(text, dirlist, filter);
151 	if (node == NULL)
152 		return;
153 
154 	pkgconf_node_insert_tail(&node->lnode, node, dirlist);
155 }
156 
157 /*
158  * !doc
159  *
160  * .. c:function:: void pkgconf_path_prepend(const char *text, pkgconf_list_t *dirlist)
161  *
162  *    Prepends a path node to a path list.  If the path is already in the list, do nothing.
163  *
164  *    :param char* text: The path text to add as a path node.
165  *    :param pkgconf_list_t* dirlist: The path list to add the path node to.
166  *    :param bool filter: Whether to perform duplicate filtering.
167  *    :return: nothing
168  */
169 void
170 pkgconf_path_prepend(const char *text, pkgconf_list_t *dirlist, bool filter)
171 {
172 	pkgconf_path_t *node = prepare_path_node(text, dirlist, filter);
173 	if (node == NULL)
174 		return;
175 
176 	pkgconf_node_insert(&node->lnode, node, dirlist);
177 }
178 
179 /*
180  * !doc
181  *
182  * .. c:function:: size_t pkgconf_path_split(const char *text, pkgconf_list_t *dirlist)
183  *
184  *    Splits a given text input and inserts paths into a path list.
185  *
186  *    :param char* text: The path text to split and add as path nodes.
187  *    :param pkgconf_list_t* dirlist: The path list to have the path nodes added to.
188  *    :param bool filter: Whether to perform duplicate filtering.
189  *    :return: number of path nodes added to the path list
190  *    :rtype: size_t
191  */
192 size_t
193 pkgconf_path_split(const char *text, pkgconf_list_t *dirlist, bool filter)
194 {
195 	size_t count = 0;
196 	char *workbuf, *p, *iter;
197 
198 	if (text == NULL)
199 		return 0;
200 
201 	iter = workbuf = strdup(text);
202 	if (workbuf == NULL)
203 		return 0;
204 
205 	while ((p = strtok(iter, PKG_CONFIG_PATH_SEP_S)) != NULL)
206 	{
207 		pkgconf_path_add(p, dirlist, filter);
208 
209 		count++, iter = NULL;
210 	}
211 	free(workbuf);
212 
213 	return count;
214 }
215 
216 /*
217  * !doc
218  *
219  * .. c:function:: size_t pkgconf_path_build_from_environ(const char *envvarname, const char *fallback, pkgconf_list_t *dirlist)
220  *
221  *    Adds the paths specified in an environment variable to a path list.  If the environment variable is not set,
222  *    an optional default set of paths is added.
223  *
224  *    :param pkgconf_client_t* client: The client to use for environmental variable lookup (can be NULL).
225  *    :param char* envvarname: The environment variable to look up.
226  *    :param char* fallback: The fallback paths to use if the environment variable is not set.
227  *    :param pkgconf_list_t* dirlist: The path list to add the path nodes to.
228  *    :param bool filter: Whether to perform duplicate filtering.
229  *    :return: number of path nodes added to the path list
230  *    :rtype: size_t
231  */
232 size_t
233 pkgconf_path_build_from_environ(const pkgconf_client_t *client, const char *envvarname, const char *fallback, pkgconf_list_t *dirlist, bool filter)
234 {
235 	const char *data;
236 
237 	data = pkgconf_client_getenv(client, envvarname);
238 	if (data != NULL)
239 		return pkgconf_path_split(data, dirlist, filter);
240 
241 	if (fallback != NULL)
242 		return pkgconf_path_split(fallback, dirlist, filter);
243 
244 	/* no fallback and no environment variable, thusly no nodes added */
245 	return 0;
246 }
247 
248 /*
249  * !doc
250  *
251  * .. c:function:: bool pkgconf_path_match_list(const char *path, const pkgconf_list_t *dirlist)
252  *
253  *    Checks whether a path has a matching prefix in a path list.
254  *
255  *    :param char* path: The path to check against a path list.
256  *    :param pkgconf_list_t* dirlist: The path list to check the path against.
257  *    :return: true if the path list has a matching prefix, otherwise false
258  *    :rtype: bool
259  */
260 bool
261 pkgconf_path_match_list(const char *path, const pkgconf_list_t *dirlist)
262 {
263 	pkgconf_node_t *n = NULL;
264 	pkgconf_buffer_t relocated = PKGCONF_BUFFER_INITIALIZER;
265 	const char *cpath = path;
266 
267 	if (path == NULL)
268 		return false;
269 
270 	pkgconf_buffer_append(&relocated, path);
271 	cpath = pkgconf_buffer_str(&relocated);
272 
273 	if (pkgconf_path_relocate(&relocated))
274 		cpath = pkgconf_buffer_str(&relocated);
275 
276 	if (cpath == NULL)
277 	{
278 		pkgconf_buffer_finalize(&relocated);
279 		return false;
280 	}
281 
282 	PKGCONF_FOREACH_LIST_ENTRY(dirlist->head, n)
283 	{
284 		pkgconf_path_t *pnode = n->data;
285 
286 		if (!strcmp(pnode->path, cpath))
287 		{
288 			pkgconf_buffer_finalize(&relocated);
289 			return true;
290 		}
291 	}
292 
293 	pkgconf_buffer_finalize(&relocated);
294 	return false;
295 }
296 
297 /*
298  * !doc
299  *
300  * .. c:function:: void pkgconf_path_copy_list(pkgconf_list_t *dst, const pkgconf_list_t *src)
301  *
302  *    Copies a path list to another path list.
303  *
304  *    :param pkgconf_list_t* dst: The path list to copy to.
305  *    :param pkgconf_list_t* src: The path list to copy from.
306  *    :return: nothing
307  */
308 void
309 pkgconf_path_copy_list(pkgconf_list_t *dst, const pkgconf_list_t *src)
310 {
311 	pkgconf_node_t *n;
312 
313 	PKGCONF_FOREACH_LIST_ENTRY(src->head, n)
314 	{
315 		pkgconf_path_t *srcpath = n->data, *path;
316 
317 		path = calloc(1, sizeof(pkgconf_path_t));
318 		if (path == NULL)
319 			continue;
320 
321 		path->path = strdup(srcpath->path);
322 		if (path->path == NULL)
323 		{
324 			free(path);
325 			continue;
326 		}
327 
328 #ifdef PKGCONF_CACHE_INODES
329 		path->handle_path = srcpath->handle_path;
330 		path->handle_device = srcpath->handle_device;
331 #endif
332 
333 		pkgconf_node_insert_tail(&path->lnode, path, dst);
334 	}
335 }
336 
337 /*
338  * !doc
339  *
340  * .. c:function:: void pkgconf_path_prepend_list(pkgconf_list_t *dst, const pkgconf_list_t *src)
341  *
342  *    Copies a path list to another path list.
343  *
344  *    :param pkgconf_list_t* dst: The path list to copy to.
345  *    :param pkgconf_list_t* src: The path list to copy from.
346  *    :return: nothing
347  */
348 void
349 pkgconf_path_prepend_list(pkgconf_list_t *dst, const pkgconf_list_t *src)
350 {
351 	pkgconf_node_t *n;
352 
353 	PKGCONF_FOREACH_LIST_ENTRY(src->head, n)
354 	{
355 		pkgconf_path_t *srcpath = n->data, *path;
356 
357 		path = calloc(1, sizeof(pkgconf_path_t));
358 		if (path == NULL)
359 			continue;
360 
361 		path->path = strdup(srcpath->path);
362 		if (path->path == NULL)
363 		{
364 			free(path);
365 			continue;
366 		}
367 
368 #ifdef PKGCONF_CACHE_INODES
369 		path->handle_path = srcpath->handle_path;
370 		path->handle_device = srcpath->handle_device;
371 #endif
372 
373 		pkgconf_node_insert(&path->lnode, path, dst);
374 	}
375 }
376 
377 /*
378  * !doc
379  *
380  * .. c:function:: void pkgconf_path_free(pkgconf_list_t *dirlist)
381  *
382  *    Releases any path nodes attached to the given path list.
383  *
384  *    :param pkgconf_list_t* dirlist: The path list to clean up.
385  *    :return: nothing
386  */
387 void
388 pkgconf_path_free(pkgconf_list_t *dirlist)
389 {
390 	pkgconf_node_t *n, *tn;
391 
392 	PKGCONF_FOREACH_LIST_ENTRY_SAFE(dirlist->head, tn, n)
393 	{
394 		pkgconf_path_t *pnode = n->data;
395 
396 		free(pnode->path);
397 		free(pnode);
398 	}
399 
400 	pkgconf_list_zero(dirlist);
401 }
402 
403 static char *
404 normpath(const pkgconf_buffer_t *pathbuf)
405 {
406 	if (!pathbuf || pkgconf_buffer_len(pathbuf) == 0)
407 		return NULL;
408 
409 	const char *path = pkgconf_buffer_str(pathbuf);
410 	char *copy = strdup(path);
411 	if (NULL == copy)
412 		return NULL;
413 	char *ptr = copy;
414 
415 	for (int ii = 0; copy[ii]; ii++)
416 	{
417 		*ptr++ = path[ii];
418 		if ('/' == path[ii])
419 		{
420 			ii++;
421 			while ('/' == path[ii])
422 				ii++;
423 			ii--;
424 		}
425 	}
426 	*ptr = '\0';
427 
428 	return copy;
429 }
430 
431 /*
432  * !doc
433  *
434  * .. c:function:: bool pkgconf_path_relocate(pkgconf_buffer_t *buf)
435  *
436  *    Relocates a path, possibly calling normpath() on it.
437  *
438  *    :param pkgconf_buffer_t* buf: The path to relocate.
439  *    :return: true on success, false on error
440  *    :rtype: bool
441  */
442 bool
443 pkgconf_path_relocate(pkgconf_buffer_t *buf)
444 {
445 	char *tmpbuf;
446 
447 	if ((tmpbuf = normpath(buf)) != NULL)
448 	{
449 		pkgconf_buffer_reset(buf);
450 		pkgconf_buffer_append(buf, tmpbuf);
451 		free(tmpbuf);
452 	}
453 
454 	return true;
455 }
456 
457 /*
458  * !doc
459  *
460  * .. c:function:: bool pkgconf_path_trim_basename(pkgconf_buffer_t *buf)
461  *
462  *    Trims the basename from a path.
463  *
464  *    :param pkgconf_buffer_t* buf: The path to trim.
465  *    :return: true if a separator was found and the path was trimmed, false otherwise
466  *    :rtype: bool
467  */
468 bool
469 pkgconf_path_trim_basename(pkgconf_buffer_t *buf)
470 {
471 	char *sep;
472 
473 	if (!pkgconf_buffer_len(buf))
474 		return false;
475 
476 	sep = strrchr(buf->base, PKG_DIR_SEP_S);
477 #ifdef _WIN32
478 	char *sep2 = strrchr(buf->base, '/');
479 	if (sep2 != NULL && (sep == NULL || sep2 > sep))
480 		sep = sep2;
481 #endif
482 
483 	if (sep != NULL)
484 	{
485 		*sep = '\0';
486 		buf->end = sep;
487 		return true;
488 	}
489 
490 	return false;
491 }
492 
493 /*
494  * !doc
495  *
496  * .. c:function:: const char *pkgconf_path_find_basename(const char *path)
497  *
498  *    Finds the basename from a path.
499  *
500  *    :param char* path: The path to find the basename from.
501  *    :return: a pointer to the basename
502  *    :rtype: const char *
503  */
504 const char *
505 pkgconf_path_find_basename(const char *path)
506 {
507 	const char *sep;
508 
509 	sep = strrchr(path, PKG_DIR_SEP_S);
510 #ifdef _WIN32
511 	const char *sep2 = strrchr(path, '/');
512 	if (sep2 != NULL && (sep == NULL || sep2 > sep))
513 		sep = sep2;
514 #endif
515 
516 	if (sep != NULL)
517 		return sep + 1;
518 
519 	return path;
520 }
521 
522 #ifdef _WIN32
523 #define PKG_CONFIG_REG_KEY "Software\\pkgconfig\\PKG_CONFIG_PATH"
524 /*
525  * !doc
526  *
527  * .. c:function:: void pkgconf_path_build_from_registry(HKEY hKey, pkgconf_list_t *dir_list, bool filter)
528  *
529  *    Adds paths to a directory list discovered from a given registry key.
530  *
531  *    .. warning::
532  *       The Windows registry search path mechanism is deprecated and will be
533  *       removed in pkgconf 3.1.  Use ``PKG_CONFIG_PATH`` or configure search
534  *       paths explicitly instead.  Avoid using this function directly in new
535  *       code.
536  *
537  *    :param pkgconf_client_t* client: pkgconf client
538  *    :param HKEY hKey: The registry key to enumerate.
539  *    :param pkgconf_list_t* dir_list: The directory list to append enumerated paths to.
540  *    :param bool filter: Whether duplicate paths should be filtered.
541  *    :return: number of path nodes added to the list
542  *    :rtype: size_t
543  */
544 size_t
545 pkgconf_path_build_from_registry(pkgconf_client_t *client, void *hKey, pkgconf_list_t *dir_list, bool filter)
546 {
547 	HKEY key;
548 	int i = 0;
549 	size_t added = 0;
550 
551 	char buf[16384]; /* per registry limits */
552 	DWORD bufsize = sizeof buf;
553 	if (RegOpenKeyEx(hKey, PKG_CONFIG_REG_KEY,
554 				0, KEY_READ, &key) != ERROR_SUCCESS)
555 		return 0;
556 
557 	pkgconf_warn(client,
558 		"WARNING: support for reading PKG_CONFIG_PATH from the Windows registry "
559 		"is deprecated and will be removed in pkgconf 3.1\n");
560 
561 	while (RegEnumValue(key, i++, buf, &bufsize, NULL, NULL, NULL, NULL)
562 			== ERROR_SUCCESS)
563 	{
564 		char pathbuf[PKGCONF_ITEM_SIZE];
565 		DWORD type;
566 		DWORD pathbuflen = sizeof pathbuf;
567 
568 		if (RegQueryValueEx(key, buf, NULL, &type, (LPBYTE) pathbuf, &pathbuflen)
569 				== ERROR_SUCCESS && type == REG_SZ)
570 		{
571 			pkgconf_path_add(pathbuf, dir_list, filter);
572 			added++;
573 		}
574 
575 		bufsize = sizeof buf;
576 	}
577 
578 	RegCloseKey(key);
579 	return added;
580 }
581 #endif
582 
583 bool
584 pkgconf_path_is_plausible(const pkgconf_buffer_t *buf)
585 {
586 	const char *s;
587 
588 	if (buf == NULL)
589 		return false;
590 
591 	s = pkgconf_buffer_str(buf);
592 	if (s == NULL)
593 		return false;
594 
595 	/* skip leading whitespace */
596 	while (*s != '\0' && isspace((unsigned char)*s))
597 		s++;
598 
599 	if (*s == '\0')
600 		return false;
601 
602 	/* POSIX absolute path */
603 	if (*s == '/')
604 		return true;
605 
606 	/* ./ or ../ relative path */
607 	if (s[0] == '.' && (s[1] == '/' || s[1] == '\\'))
608 		return true;
609 
610 	if (s[0] == '.' && s[1] == '.' && (s[2] == '/' || s[2] == '\\'))
611 		return true;
612 
613 	/* Windows drive path: C:/... or C:\... */
614 	if (isalpha((unsigned char)s[0]) && s[1] == ':' && (s[2] == '/' || s[2] == '\\'))
615 		return true;
616 
617 	/* anything with a path separator seems plausible, for example "Program Files/MySDK" */
618 	for (const char *p = s; *p != '\0'; p++)
619 	{
620 		if (*p == '/' || *p == '\\')
621 			return true;
622 	}
623 
624 	return false;
625 }
626