xref: /freebsd/contrib/pkgconf/libpkgconf/license.c (revision 592efe252472a3385acf36b1f49ecf710a7f3d9c)
1 /*
2  * license.c
3  * Evaluate SPDX license expressions
4  *
5  * SPDX-License-Identifier: pkgconf
6  *
7  * Copyright (c) 2026 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/stdinc.h>
19 #include <libpkgconf/libpkgconf.h>
20 
license_sanitize_string(const char * s,pkgconf_buffer_t * buf)21 static void license_sanitize_string(const char *s, pkgconf_buffer_t *buf)
22 {
23 	unsigned int i = 0;
24 	/*
25 	 * Allowed chars are:
26 	 * - a-z
27 	 * - 0-9
28 	 * - chars '-', '+', '.', '(' and ')'
29 	 */
30 	for (i = 0; i < strlen(s); i ++)
31 	{
32 		if (isalnum((unsigned char) s[i]) || s[i] == '-' || s[i] == '+' || s[i] == '(' || s[i] == ')' || s[i] == '.' || s[i] == ':')
33 		{
34 			pkgconf_buffer_push_byte(buf, s[i]);
35 		}
36 	}
37 }
38 
39 /*
40  * !doc
41  *
42  * .. c:function:: void pkgconf_license_copy_list(const pkgconf_client_t *client, pkgconf_list_t *list, const pkgconf_list_t *base)
43  *
44  *    Copies a `license list` to another `license list`
45  *
46  *    :param pkgconf_client_t* client: The pkgconf client being accessed.
47  *    :param pkgconf_list_t* list: The list the fragments are being added to.
48  *    :param pkgconf_list_t* base: The list the fragments are being copied from.
49  *    :return: nothing
50  */
51 void
pkgconf_license_copy_list(const pkgconf_client_t * client,pkgconf_list_t * list,const pkgconf_list_t * base)52 pkgconf_license_copy_list(const pkgconf_client_t *client, pkgconf_list_t *list, const pkgconf_list_t *base)
53 {
54 	pkgconf_node_t *node;
55 	(void) client;
56 
57 	PKGCONF_FOREACH_LIST_ENTRY(base->head, node)
58 	{
59 		pkgconf_license_t *license = node->data;
60 		pkgconf_license_t *cpy_license = calloc(1, sizeof(pkgconf_license_t));
61 
62 		if (cpy_license == NULL)
63 			continue;
64 
65 		cpy_license->type = license->type;
66 
67 		if (license->data != NULL)
68 		{
69 			cpy_license->data = strdup(license->data);
70 		}
71 
72 		pkgconf_node_insert_tail(&cpy_license->iter, cpy_license, list);
73 	}
74 }
75 
76 /*
77  * !doc
78  *
79  * .. c:function:: void pkgconf_license_free(pkgconf_list_t *list)
80  *
81  *    Delete an entire `license list`.
82  *
83  *    :param pkgconf_list_t* list: The `license list` to delete.
84  *    :return: nothing
85  */
86 void
pkgconf_license_free(pkgconf_list_t * list)87 pkgconf_license_free(pkgconf_list_t *list)
88 {
89 	if (!list)
90 		return;
91 
92 	pkgconf_node_t *node, *next;
93 
94 	PKGCONF_FOREACH_LIST_ENTRY_SAFE(list->head, next, node)
95 	{
96 		pkgconf_license_t *license = node->data;
97 		free(license->data);
98 		free(license);
99 	}
100 }
101 
102 /*
103  * !doc
104  *
105  * .. c:function:: void pkgconf_license_insert(pkgconf_client_t *client, pkgconf_list_t *list, unsigned char type, const char *data)
106  *
107  *    Adds a `license` of text to a `license list` directly without interpreting it.
108  *
109  *    :param pkgconf_client_t* client: The pkgconf client being accessed.
110  *    :param pkgconf_list_t* list: The fragment list.
111  *    :param char type: The type of the license.
112  *    :param char* data: The data of the license
113  *    :return: nothing
114  */
115 void
pkgconf_license_insert(pkgconf_client_t * client,pkgconf_list_t * list,unsigned char type,const char * data)116 pkgconf_license_insert(pkgconf_client_t *client, pkgconf_list_t *list, unsigned char type, const char *data)
117 {
118 	(void) client;
119 
120 	pkgconf_license_t *license;
121 
122 	license = calloc(1, sizeof(pkgconf_license_t));
123 	if (!license)
124 	{
125 		pkgconf_error(client, "pkgconf_license_insert: out of memory");
126 		return;
127 	}
128 	license->type = type;
129 	license->data = strdup(data);
130 
131 	pkgconf_node_insert_tail(&license->iter, license, list);
132 }
133 
134 /*
135  * !doc
136  *
137  * .. c:function:: pkgconf_license_evaluate_str(pkgconf_client_t *client, pkgconf_list_t *license_list, const char *expression, unsigned int flags)
138  *
139  * Evaluates SPDX expression strings like:
140  * - BSD-3-Clause
141  * - LGPL-2.1-only OR MIT
142  * - LGPL-2.1-only OR MIT OR BSD-3-Clause
143  * - ISC AND (BSD-3-Clause AND BSD-2-Clause)
144  *
145  * Function parses and sanitizes license strings. Also adding multiple
146  * 'License:'-keys is supported like:
147  * License: BSD-3-Clause
148  * License: BSD-2-Clause
149  *
150  * Which will evaluate to: BSD-3-Clause AND BSD-2-Clause
151  *
152  * :param pkgconf_client_t* client: The client object that owns the package this dependency list belongs to.
153  * :param pkgconf_list_t* license_list: The dependency list to populate with dependency nodes.
154  * :param char* expression: The dependency data to parse.
155  * :param uint flags: Any flags to attach to the dependency nodes.
156  * :return: nothing
157  */
158 void
pkgconf_license_evaluate_str(pkgconf_client_t * client,pkgconf_list_t * license_list,const char * expression,unsigned int flags)159 pkgconf_license_evaluate_str(pkgconf_client_t *client, pkgconf_list_t *license_list, const char *expression, unsigned int flags)
160 {
161 	pkgconf_buffer_t out_buffer = PKGCONF_BUFFER_INITIALIZER;
162 	size_t buf_size = 0;
163 	char *cur_word = NULL;
164 	int i, ret, argc;
165 	char **argv;
166 	size_t string_len = 0;
167 
168 	(void)flags;
169 
170 	if (expression == NULL)
171 		return;
172 
173 	buf_size = strlen(expression) + 1;
174 	ret = pkgconf_argv_split(expression, &argc, &argv);
175 	if (!ret)
176 	{
177 		/* This is not the first License:
178 		 * so add AND
179 		 */
180 		if (license_list->head)
181 		{
182 			pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_AND, "AND");
183 		}
184 
185 		i = 0;
186 		while (i < argc)
187 		{
188 			string_len = strnlen(argv[i], buf_size);
189 			cur_word = argv[i];
190 			if (string_len >= 1)
191 			{
192 				license_sanitize_string(cur_word, &out_buffer);
193 				if (!pkgconf_buffer_len(&out_buffer))
194 				{
195 					i ++;
196 					pkgconf_buffer_finalize(&out_buffer);
197 					continue;
198 				}
199 
200 				cur_word = (char *)pkgconf_buffer_str(&out_buffer);
201 				size_t cur_len = strnlen(cur_word, buf_size);
202 				if (!cur_len)
203 				{
204 					i ++;
205 					pkgconf_buffer_finalize(&out_buffer);
206 					continue;
207 				}
208 
209 				if (cur_word[0] == '(')
210 				{
211 					pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_BRACKET_OPEN, "(");
212 					/* If there is more after '(' like '(BSD-2-Clause'
213 					 * Then append rest to fragments as license.
214 					 * This is expression like GPL-2.0-only OR (BSD-2-Clause AND ISC)
215 					 */
216 					if (cur_len >= 2)
217 					{
218 						cur_word ++;
219 						pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_EXPRESSION, cur_word);
220 					}
221 				}
222 				else if (cur_word[cur_len - 1] == ')')
223 				{
224 					if (cur_len >= 2)
225 					{
226 						cur_word[cur_len - 1] = 0x00;
227 						pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_EXPRESSION, cur_word);
228 					}
229 					pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_BRACKET_CLOSE, ")");
230 				}
231 				else if (!strcasecmp(cur_word, "and"))
232 				{
233 					pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_AND, "AND");
234 				}
235 				else if (!strcasecmp(cur_word, "or"))
236 				{
237 					pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_OR, "OR");
238 				}
239 				else if (!strcasecmp(cur_word, "with"))
240 				{
241 					pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_WITH, "WITH");
242 				}
243 				else
244 				{
245 					pkgconf_license_insert(client, license_list, PKGCONF_LICENSE_EXPRESSION, cur_word);
246 				}
247 				pkgconf_buffer_finalize(&out_buffer);
248 			}
249 			i++;
250 		}
251 
252 		pkgconf_argv_free(argv);
253 	}
254 }
255 
256 /*
257  * !doc
258  *
259  * .. c:function:: pkgconf_license_evaluate_str(pkgconf_client_t *client, pkgconf_list_t *license_list, const char *expression, unsigned int flags)
260  *
261  * Evaluates SPDX expression strings like:
262  * - BSD-3-Clause
263  * - LGPL-2.1-only OR MIT
264  * - LGPL-2.1-only OR MIT OR BSD-3-Clause
265  * - ISC AND (BSD-3-Clause AND BSD-2-Clause)
266  *
267  * Function parses and sanitizes license strings. Also adding multiple
268  * 'License:'-keys is supported like:
269  * License: BSD-3-Clause
270  * License: BSD-2-Clause
271  *
272  * Which will evaluate to: BSD-3-Clause AND BSD-2-Clause
273  *
274  * :param pkgconf_client_t* client: The client object that owns the package this dependency list belongs to.
275  * :param pkgconf_list_t* license_list: The dependency list to populate with dependency nodes.
276  * :param char* expression: The dependency data to parse.
277  * :param uint flags: Any flags to attach to the dependency nodes.
278  * :return: nothing
279  */
280 void
pkgconf_license_evaluate(pkgconf_client_t * client,pkgconf_pkg_t * pkg,pkgconf_list_t * license_list,const char * license_str,unsigned int flags)281 pkgconf_license_evaluate(pkgconf_client_t *client, pkgconf_pkg_t *pkg, pkgconf_list_t *license_list, const char *license_str, unsigned int flags)
282 {
283 	char *license_expression = pkgconf_bytecode_eval_str(client, &pkg->vars, license_str, NULL);
284 
285 	pkgconf_license_evaluate_str(client, license_list, license_expression, flags);
286 	free(license_expression);
287 }
288 
289 /*
290  * !doc
291  *
292  * .. c:function:: void pkgconf_dependency_parse_str(pkgconf_list_t *deplist_head, const char *depends)
293  *
294  * Renders license fragments back to SPDX expression string. Tries
295  * to keep output as close as possible to original input
296  *
297  * :param pkgconf_client_t* client: The client object that owns the package this dependency list belongs to.
298  * :param pkgconf_list_t* deplist_head: The dependency list to populate with dependency nodes.
299  * :param char* depends: The dependency data to parse.
300  * :param uint flags: Any flags to attach to the dependency nodes.
301  * :return: nothing
302  */
303 void
pkgconf_license_render(pkgconf_client_t * client,const pkgconf_list_t * list,pkgconf_buffer_t * buf)304 pkgconf_license_render(pkgconf_client_t *client, const pkgconf_list_t *list, pkgconf_buffer_t *buf)
305 {
306 	const pkgconf_buffer_t *frag_string = NULL;
307 	pkgconf_node_t *node;
308 	bool is_delim = true;
309 
310 	(void)client;
311 
312 	PKGCONF_FOREACH_LIST_ENTRY(list->head, node)
313 	{
314 		pkgconf_license_t *license = node->data;
315 		is_delim = true;
316 		frag_string = PKGCONF_BUFFER_FROM_STR(license->data);
317 		pkgconf_buffer_append(buf, pkgconf_buffer_str_or_empty(frag_string));
318 
319 		if (license->type == PKGCONF_LICENSE_BRACKET_OPEN || (node->next != NULL && ((const pkgconf_license_t *)node->next->data)->type == PKGCONF_LICENSE_BRACKET_CLOSE))
320 		{
321 			is_delim = false;
322 		}
323 
324 		if (node->next != NULL && is_delim)
325 		{
326 			pkgconf_buffer_push_byte(buf, ' ');
327 		}
328 	}
329 }
330