canfigger v0.3.3
Lightweight config file parser library
Loading...
Searching...
No Matches
canfigger.c
1/*
2This file is part of canfigger<https://github.com/andy5995/canfigger>
3
4MIT License
5
6Copyright (c) 2024 Andy Alt(arch_stanton5995@proton.me)
7
8Permission is hereby granted, free of charge, to any person obtaining a copy
9of this software and associated documentation files (the "Software"), to deal
10in the Software without restriction, including without limitation the rights
11to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12copies of the Software, and to permit persons to whom the Software is
13furnished to do so, subject to the following conditions:
14
15The above copyright notice and this permission notice shall be included in all
16copies or substantial portions of the Software.
17
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24SOFTWARE.
25*/
26
27#ifndef _WIN32
28/* stat() and getuid() are hidden by -std=c99 without this; canfigger_runtime_dir
29 needs them to check that the runtime directory is really ours. */
30#ifndef _POSIX_C_SOURCE
31#define _POSIX_C_SOURCE 200809L
32#endif
33#endif
34
35#include <ctype.h> // isspace()
36#include <errno.h>
37#include <stdbool.h>
38#include <stdio.h>
39#include <stdlib.h> // free(), malloc()
40#include <string.h>
41
42#ifdef _WIN32
43#include <windows.h>
44#include <shlobj.h>
45#else
46#include <sys/stat.h>
47#include <sys/types.h>
48#include <unistd.h>
49#endif
50
51// This is only required for version info and can be removed
52// if you're copying the canfigger source files to use as
53// an embedded library with your own project (i.e., not building
54// canfigger with the build system it's shipped with).
55#include "canfigger_version.h"
56
57#include "canfigger.h"
58
59static char *grab_str_segment(char *a, char **dest, const int c);
60
62struct line
63{
64 size_t len;
65 char *start;
66 char *end;
67};
71static void *
72malloc_wrap(size_t size)
73{
74 void *retval = malloc(size);
75 if (retval)
76 return retval;
77
78 perror("canfigger: malloc");
79
80 return NULL;
81}
82
83
84/* Copy the first @p n bytes of @p src into a new NUL-terminated string. @p n
85 is always the length to copy: it once doubled as a "copy the whole string"
86 sentinel when 0, which made an empty field (",," or "= value") swallow the
87 rest of the line. */
88static char *
89strclone(const char *src, size_t n)
90{
91 char *dest = malloc_wrap(n + 1);
92 if (!dest)
93 return NULL;
94
95 memcpy(dest, src, n);
96 dest[n] = '\0';
97 return dest;
98}
99
100
101/* Copy all of @p src. Kept separate from strclone() so a whole-string copy
102 and a copy of a counted length can never be confused again. */
103static char *
104strclone_str(const char *src)
105{
106 return strclone(src, strlen(src));
107}
108
109
110void
111canfigger_free_current_attr_str_advance(struct attributes *attributes,
112 char **attr)
113{
114 if (!attributes)
115 {
116 *attr = NULL;
117 return;
118 }
119
121 free(attributes->current);
122
123 if (!attributes->iter_ptr)
124 {
125 free(attributes->current);
126 attributes->current = NULL;
127 *attr = NULL;
128 return;
129 }
130
131 attributes->iter_ptr = grab_str_segment(attributes->iter_ptr,
132 &attributes->current, '\n');
133
134 /* A trailing delimiter ("a, b,") leaves an empty final segment. Treat it as
135 the end, as earlier releases did, rather than as one more attribute. An
136 empty attribute anywhere else ("a,,b") is returned as "". */
137 if (attributes->current && *attributes->current == '\0'
138 && !attributes->iter_ptr)
139 {
140 free(attributes->current);
141 attributes->current = NULL;
142 }
143
144 /* current is also NULL when the copy in grab_str_segment() failed; the
145 caller then sees the end of the list, which the header documents. */
146 *attr = attributes->current;
147 return;
148}
149
150
151void
152canfigger_free_current_key_node_advance(struct Canfigger **node)
153{
154 if (*node)
155 {
156 if ((*node)->attributes)
157 {
158 if ((*node)->attributes->current)
159 {
160 free((*node)->attributes->current);
161 (*node)->attributes->current = NULL;
162 }
163
164 if ((*node)->attributes->str)
165 {
166 free((*node)->attributes->str);
167 (*node)->attributes->str = NULL;
168 }
169
170 free((*node)->attributes);
171 (*node)->attributes = NULL;
172 }
173
174 if ((*node)->value)
175 {
176 free((*node)->value);
177 (*node)->value = NULL;
178 }
179
180 free((*node)->key);
181 (*node)->key = NULL;
182
183 struct Canfigger *temp_node = (*node)->next;
184 free(*node);
185 *node = temp_node;
186 }
187
188 return;
189}
190
191
192void
194{
195 if (*node)
196 {
197 while (*node)
199 }
200
201 return;
202}
203
204
205static void
206truncate_whitespace(char *str)
207{
208 if (!str)
209 return;
210
211 char *pos_0 = str;
212 /* Advance pointer until NULL terminator is found
213 * Don't try to use strchr() because you'll get a different
214 * result if the pointer is already at '\0'. */
215 while (*str != '\0')
216 str++;
217
218 /* set pointer to segment preceding NULL terminator */
219 if (str != pos_0)
220 str--;
221 else
222 return;
223
224 while (isspace((unsigned char) *str))
225 {
226 *str = '\0';
227 if (str != pos_0)
228 str--;
229 else
230 break;
231 }
232
233 return;
234}
235
236
237static char *
238grab_str_segment(char *a, char **dest, const int c)
239{
240 /* Spaces and tabs only, not isspace(): attribute iteration calls this with
241 '\n' as the delimiter, and skipping it would swallow an empty attribute
242 ("a,,b"). */
243 while (*a == ' ' || *a == '\t')
244 a++;
245
246 char *b = strchr(a, c);
247 if (!b)
248 {
249 *dest = strclone_str(a);
250 return b;
251 }
252
253 size_t len = b - a;
254 *dest = strclone(a, len);
255 if (!*dest)
256 return NULL;
257
258 truncate_whitespace(*dest);
259 return b + 1;
260}
261
262static void
263add_key_node(struct Canfigger **root, struct Canfigger **cur_node)
264{
265 struct Canfigger *tmp_node = malloc_wrap(sizeof(struct Canfigger));
266 if (!tmp_node)
267 return;
268
269 /* The node is linked in before its fields are filled, so they must be safe
270 to free from the start: a failure part-way through the line leaves it for
271 canfigger_free_list() to release along with the rest of the list. */
272 tmp_node->key = NULL;
273 tmp_node->value = NULL;
274 tmp_node->attributes = NULL;
275 tmp_node->next = NULL;
276
277 if (*root)
278 (*cur_node)->next = tmp_node;
279 else
280 *root = tmp_node;
281
282 *cur_node = tmp_node;
283
284 return;
285}
286
287
288static char *
289read_entire_file(const char *filename)
290{
291 char *buffer = NULL;
292 long file_size;
293 size_t n_bytes;
294
295 FILE *fp = fopen(filename, "rb");
296 if (!fp)
297 {
298 fprintf(stderr, "canfigger: Failed to open %s: %s\n", filename,
299 strerror(errno));
300 return NULL;
301 }
302
303 if (fseek(fp, 0, SEEK_END) != 0 || (file_size = ftell(fp)) < 0
304 || fseek(fp, 0, SEEK_SET) != 0)
305 {
306 fprintf(stderr, "canfigger: Failed to determine size of %s: %s\n",
307 filename, strerror(errno));
308 goto done;
309 }
310
311 buffer = malloc_wrap(file_size + 1);
312 if (!buffer)
313 goto done;
314
315 n_bytes = fread(buffer, 1, file_size, fp);
316 if (n_bytes != (size_t) file_size)
317 {
318 if (ferror(fp))
319 fprintf(stderr, "canfigger: Error reading %s: %s\n", filename,
320 strerror(errno));
321 else
322 fprintf(stderr,
323 "canfigger: Partial read of %s: expected %ld bytes, got %zu bytes\n",
324 filename, file_size, n_bytes);
325 free(buffer);
326 buffer = NULL;
327 }
328 else
329 buffer[file_size] = '\0';
330
331done:
332 fclose(fp);
333 return buffer;
334}
335
336
337struct Canfigger *
338canfigger_parse_file(const char *file, const int delimiter)
339{
340 struct Canfigger *root = NULL, *cur_node = NULL;
341
342 char *file_contents = read_entire_file(file);
343 if (file_contents == NULL)
344 return NULL;
345
346 /* Skip UTF-8 BOM (EF BB BF) if present. Some editors (especially on Windows)
347 * prepend these three bytes silently; without this check they would be
348 * prepended to the first key, corrupting any strcmp against it.
349 * Array accesses are safe: read_entire_file allocates file_size+1 bytes with
350 * a null terminator, and && short-circuits — [1] is only read when [0]
351 * matched 0xEF (so at least 1 file byte exists), and [2] only when [1]
352 * matched 0xBB (so at least 2 exist). */
353 char *parse_start = file_contents;
354 if ((unsigned char) parse_start[0] == 0xEF &&
355 (unsigned char) parse_start[1] == 0xBB &&
356 (unsigned char) parse_start[2] == 0xBF)
357 parse_start += 3;
358
359 struct line line;
360 line.start = parse_start;
361
362 bool node_complete = false;
363
364 for (;;)
365 {
366 line.end = strchr(line.start, '\n');
367 line.len =
368 line.end ? (size_t) (line.end - line.start) : strlen(line.start);
369
370 /* End of file with no remaining content */
371 if (line.len == 0 && !line.end)
372 break;
373
374 char *tmp_line = malloc_wrap(line.len + 1);
375 if (!tmp_line)
376 {
377 canfigger_free_list(&root);
378 free(file_contents);
379 return NULL;
380 }
381
382 memcpy(tmp_line, line.start, line.len);
383 tmp_line[line.len] = '\0';
384 line.start = line.end ? line.end + 1 : line.start + line.len;
385
386 char *line_ptr = tmp_line;
387 truncate_whitespace(line_ptr);
388
389 while (isspace((unsigned char) *line_ptr))
390 line_ptr++;
391
392 if (*line_ptr == '\0' || *line_ptr == '#' || *line_ptr == '[')
393 {
394 free(tmp_line);
395 if (!line.end)
396 break;
397 continue;
398 }
399
400 node_complete = false;
401 struct Canfigger *prev_node = cur_node;
402 add_key_node(&root, &cur_node);
403 if (cur_node == prev_node)
404 {
405 free(tmp_line);
406 break;
407 }
408
409 // Get key
410 line_ptr = grab_str_segment(line_ptr, &cur_node->key, '=');
411 if (!cur_node->key)
412 {
413 free(tmp_line);
414 break;
415 }
416
417 // Get value
418 if (line_ptr)
419 {
420 line_ptr = grab_str_segment(line_ptr, &cur_node->value, delimiter);
421 if (!cur_node->value)
422 {
423 free(tmp_line);
424 break;
425 }
426 }
427
428 // Handle attributes
429 if (line_ptr)
430 {
431 cur_node->attributes = malloc_wrap(sizeof(struct attributes));
432 if (!cur_node->attributes)
433 {
434 free(tmp_line);
435 break;
436 }
437
438 struct attributes *attr_ptr = cur_node->attributes;
439 attr_ptr->current = NULL;
440
441 attr_ptr->str = strclone_str(line_ptr);
442 if (!attr_ptr->str)
443 {
444 free(tmp_line);
445 break;
446 }
447
448 attr_ptr->iter_ptr = attr_ptr->str;
449
450 // Change the delimiter, which will be used later
451 // in canfigger_free_current_attr_str_advance()
452 char *delimiter_ptr = strchr(attr_ptr->iter_ptr, delimiter);
453 while (delimiter_ptr)
454 {
455 *delimiter_ptr = '\n';
456 delimiter_ptr = strchr(delimiter_ptr, delimiter);
457 }
458 }
459
460 node_complete = true;
461 free(tmp_line);
462 if (!line.end)
463 break;
464 }
465
466 if (!root)
467 {
468 free(file_contents);
469 return NULL;
470 }
471
472 if (!node_complete)
473 {
474 free(file_contents);
475 canfigger_free_list(&root);
476 return NULL;
477 }
478
479 free(file_contents);
480 return root;
481}
482
483
484#ifndef _WIN32
485static char *
486xdg_base_dir(const char *xdg_env, const char *fallback)
487{
488 const char *base = getenv(xdg_env);
489 /* The basedir spec requires these to be absolute and says a relative value is
490 invalid and must be ignored -- resolving it against the current directory
491 would put the file somewhere that depends on where the program was
492 started. Fall through to the $HOME default instead. */
493 if (base && *base == '/')
494 return strclone_str(base);
495
496 const char *home = getenv("HOME");
497 if (!home || !*home)
498 return NULL;
499
500 size_t len = strlen(home) + 1 + strlen(fallback) + 1;
501 char *result = malloc_wrap(len);
502 if (!result)
503 return NULL;
504 snprintf(result, len, "%s/%s", home, fallback);
505 return result;
506}
507#endif
508
509
510/* All the Windows helpers below go through SHGetFolderPathA, which is both
511 deprecated and ANSI-only. This is accepted rather than overlooked: when the
512 profile path cannot be represented in the active code page (a non-ASCII
513 account name on a non-UTF-8 system) the call fails and the helper returns
514 NULL, which is a clean failure rather than a corrupt path. Its replacement,
515 SHGetKnownFolderPath, hands back UTF-16, and a UTF-8 char * built from that
516 is not something MSVC's fopen will open -- so moving to it means a parallel
517 wide-char API, not a drop-in swap. An application shipping a UTF-8
518 active-code-page manifest (Windows 10 1903+) gets UTF-8 paths from this code
519 as it stands. */
520#ifdef _WIN32
521/* @p subdir may be NULL. It exists because Windows has no separate cache or
522 state root: everything below LOCAL_APPDATA would otherwise land on the same
523 path, so canfigger_cache_dir() and canfigger_state_dir() would return exactly
524 what canfigger_data_dir() does and a file written through one would be the
525 file written through another. The subdirectory keeps them distinct, matching
526 what Qt's QStandardPaths does for the same reason. */
527static char *
528dir_for_appname(const char *appname, int csidl, const char *subdir)
529{
530 if (!appname || *appname == '\0')
531 return NULL;
532
533 char base[MAX_PATH];
534 if (FAILED(SHGetFolderPathA(NULL, csidl, NULL, 0, base)))
535 return NULL;
536
537 size_t len = strlen(base) + 1 + strlen(appname) + 1;
538 if (subdir)
539 len += strlen(subdir) + 1;
540 char *result = malloc_wrap(len);
541 if (!result)
542 return NULL;
543 if (subdir)
544 snprintf(result, len, "%s\\%s\\%s", base, appname, subdir);
545 else
546 snprintf(result, len, "%s\\%s", base, appname);
547 return result;
548}
549#else
550static char *
551dir_for_appname(const char *appname, const char *xdg_env,
552 const char *xdg_fallback)
553{
554 if (!appname || *appname == '\0')
555 return NULL;
556
557 char *base = xdg_base_dir(xdg_env, xdg_fallback);
558 if (!base)
559 return NULL;
560 char *result = canfigger_path_join(base, appname);
561 free(base);
562 return result;
563}
564#endif
565
566
567char *
568canfigger_config_dir(const char *appname)
569{
570#ifdef _WIN32
571 return dir_for_appname(appname, CSIDL_APPDATA, NULL);
572#else
573 return dir_for_appname(appname, "XDG_CONFIG_HOME", ".config");
574#endif
575}
576
577
578char *
579canfigger_config_file(const char *filename)
580{
581 if (!filename || !*filename)
582 return NULL;
583
584#ifdef _WIN32
585 char base[MAX_PATH];
586 if (FAILED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, base)))
587 return NULL;
588
589 size_t len = strlen(base) + 1 + strlen(filename) + 1;
590 char *result = malloc_wrap(len);
591 if (!result)
592 return NULL;
593 snprintf(result, len, "%s\\%s", base, filename);
594 return result;
595#else
596 char *base = xdg_base_dir("XDG_CONFIG_HOME", ".config");
597 if (!base)
598 return NULL;
599 char *result = canfigger_path_join(base, filename);
600 free(base);
601 return result;
602#endif
603}
604
605
606char *
607canfigger_data_dir(const char *appname)
608{
609#ifdef _WIN32
610 return dir_for_appname(appname, CSIDL_LOCAL_APPDATA, NULL);
611#else
612 return dir_for_appname(appname, "XDG_DATA_HOME", ".local/share");
613#endif
614}
615
616
617char *
618canfigger_cache_dir(const char *appname)
619{
620#ifdef _WIN32
621 return dir_for_appname(appname, CSIDL_LOCAL_APPDATA, "Cache");
622#else
623 return dir_for_appname(appname, "XDG_CACHE_HOME", ".cache");
624#endif
625}
626
627
628char *
629canfigger_state_dir(const char *appname)
630{
631#ifdef _WIN32
632 return dir_for_appname(appname, CSIDL_LOCAL_APPDATA, "State");
633#else
634 return dir_for_appname(appname, "XDG_STATE_HOME", ".local/state");
635#endif
636}
637
638
639char *
640canfigger_runtime_dir(const char *appname)
641{
642 if (!appname || *appname == '\0')
643 return NULL;
644
645#ifdef _WIN32
646 /* Windows has no runtime-directory concept. Returning NULL rather than a
647 LOCAL_APPDATA path keeps the contract honest: callers asking for a runtime
648 directory want one that goes away with the session, and a path under
649 LOCAL_APPDATA does not. Let them choose their own fallback. */
650 return NULL;
651#else
652 const char *base = getenv("XDG_RUNTIME_DIR");
653 if (!base || *base != '/')
654 return NULL;
655
656 /* The spec does not merely name this directory, it states what it must be:
657 owned by the user, accessible to nobody else, mode 0700. A directory that
658 fails those checks is not a safe place for the sockets, lock files and
659 short-lived secrets this is used for, so treat it as absent and let the
660 caller fall back rather than handing back a path that looks usable. */
661 struct stat st;
662 if (stat(base, &st) != 0)
663 return NULL;
664 if (!S_ISDIR(st.st_mode))
665 return NULL;
666 if (st.st_uid != getuid())
667 return NULL;
668 if ((st.st_mode & 07777) != 0700)
669 return NULL;
670
671 return canfigger_path_join(base, appname);
672#endif
673}
674
675
676#ifdef _WIN32
677/* Windows has no search *path*: ProgramData is the one system-wide location for
678 settings and data shared by every user, so the list always holds exactly one
679 entry. It is still handed back as a list so a caller can walk
680 canfigger_config_dirs() / canfigger_data_dirs() the same way on both
681 platforms instead of bracketing the loop in #ifdefs. */
682static char **
683common_appdata_list(void)
684{
685 char base[MAX_PATH];
686 if (FAILED(SHGetFolderPathA(NULL, CSIDL_COMMON_APPDATA, NULL, 0, base)))
687 return NULL;
688
689 char **list = malloc_wrap(2 * sizeof(*list));
690 if (!list)
691 return NULL;
692
693 list[0] = strclone_str(base);
694 if (!list[0])
695 {
696 free(list);
697 return NULL;
698 }
699 list[1] = NULL;
700 return list;
701}
702#endif
703
704
705#ifndef _WIN32
706/* Split a colon-separated XDG search path into a NULL-terminated array. */
707static char **
708xdg_dir_list(const char *xdg_env, const char *fallback)
709{
710 const char *value = getenv(xdg_env);
711 if (!value || *value == '\0')
712 value = fallback;
713
714 size_t max = 2;
715 const char *p;
716 for (p = value; *p != '\0'; p++)
717 if (*p == ':')
718 max++;
719
720 char **list = malloc_wrap(max * sizeof(*list));
721 if (!list)
722 return NULL;
723
724 size_t n = 0;
725 const char *start = value;
726 for (;;)
727 {
728 const char *end = strchr(start, ':');
729 size_t len = end ? (size_t) (end - start) : strlen(start);
730
731 /* Same rule as xdg_base_dir: a relative entry is invalid per the spec, and
732 an empty one ("a::b") is not a path. Skip both rather than failing the
733 whole list -- one bad entry in a search path should not cost the caller
734 the good ones. */
735 if (len > 0 && *start == '/')
736 {
737 list[n] = strclone(start, len);
738 if (!list[n])
739 {
740 list[n] = NULL;
741 canfigger_free_dirs(list);
742 return NULL;
743 }
744 n++;
745 }
746
747 if (!end)
748 break;
749 start = end + 1;
750 }
751
752 list[n] = NULL;
753 return list;
754}
755#endif
756
757
758char **
759canfigger_config_dirs(void)
760{
761#ifdef _WIN32
762 return common_appdata_list();
763#else
764 return xdg_dir_list("XDG_CONFIG_DIRS", "/etc/xdg");
765#endif
766}
767
768
769char **
770canfigger_data_dirs(void)
771{
772#ifdef _WIN32
773 return common_appdata_list();
774#else
775 return xdg_dir_list("XDG_DATA_DIRS", "/usr/local/share:/usr/share");
776#endif
777}
778
779
780void
781canfigger_free_dirs(char **dirs)
782{
783 if (!dirs)
784 return;
785
786 size_t i;
787 for (i = 0; dirs[i] != NULL; i++)
788 free(dirs[i]);
789 free(dirs);
790}
791
792
793char *
794canfigger_path_join(const char *dir, const char *file)
795{
796 if (!dir || !*dir || !file || !*file)
797 return NULL;
798
799 size_t dirlen = strlen(dir);
800 size_t filelen = strlen(file);
801 bool needs_sep = dirlen > 0 && dir[dirlen - 1] != '/'
802 && dir[dirlen - 1] != '\\';
803 size_t total = dirlen + (needs_sep ? 1 : 0) + filelen + 1;
804
805 char *result = malloc_wrap(total);
806 if (!result)
807 return NULL;
808
809#ifdef _WIN32
810 const char sep = '\\';
811#else
812 const char sep = '/';
813#endif
814
815 if (needs_sep)
816 snprintf(result, total, "%s%c%s", dir, sep, file);
817 else
818 snprintf(result, total, "%s%s", dir, file);
819
820 return result;
821}
822
823
824/* Does the path name an existing regular file? A directory of the right name is
825 not a config file, so it must not stop the search short of one further down
826 the path. */
827static bool
828path_is_file(const char *path)
829{
830#ifdef _WIN32
831 DWORD attr = GetFileAttributesA(path);
832 return attr != INVALID_FILE_ATTRIBUTES
833 && !(attr & FILE_ATTRIBUTE_DIRECTORY);
834#else
835 struct stat st;
836 return stat(path, &st) == 0 && S_ISREG(st.st_mode);
837#endif
838}
839
840
841char *
842canfigger_find_config_file(const char *appname, const char *filename)
843{
844 if (!filename || !*filename)
845 return NULL;
846
847 char *candidate = NULL;
848
849 /* The user's own config always wins, whatever the system copies hold. */
850 if (appname && *appname)
851 {
852 char *dir = canfigger_config_dir(appname);
853 if (dir)
854 {
855 candidate = canfigger_path_join(dir, filename);
856 free(dir);
857 }
858 }
859 else
860 candidate = canfigger_config_file(filename);
861
862 if (candidate && path_is_file(candidate))
863 return candidate;
864 free(candidate);
865
866 char **dirs = canfigger_config_dirs();
867 if (!dirs)
868 return NULL;
869
870 char *found = NULL;
871 size_t i;
872 for (i = 0; dirs[i] != NULL && !found; i++)
873 {
874 char *base = NULL;
875 if (appname && *appname)
876 {
877 base = canfigger_path_join(dirs[i], appname);
878 if (!base)
879 continue;
880 }
881
882 candidate = canfigger_path_join(base ? base : dirs[i], filename);
883 free(base);
884
885 if (candidate && path_is_file(candidate))
886 found = candidate;
887 else
888 free(candidate);
889 }
890
891 canfigger_free_dirs(dirs);
892 return found;
893}
894
895
896/* Keyed by enum canfigger_user_dir so the rows cannot silently shift if the
897 enum is ever reordered. A new enumerator with no row here still compiles: the
898 entry is a null pointer, which canfigger_user_dir() turns into its ordinary
899 NULL return rather than a crash. */
900#ifndef _WIN32
901static const char *const user_dir_keys[] = {
902 [CANFIGGER_USER_DIR_DESKTOP] = "XDG_DESKTOP_DIR",
903 [CANFIGGER_USER_DIR_DOWNLOAD] = "XDG_DOWNLOAD_DIR",
904 [CANFIGGER_USER_DIR_TEMPLATES] = "XDG_TEMPLATES_DIR",
905 [CANFIGGER_USER_DIR_PUBLICSHARE] = "XDG_PUBLICSHARE_DIR",
906 [CANFIGGER_USER_DIR_DOCUMENTS] = "XDG_DOCUMENTS_DIR",
907 [CANFIGGER_USER_DIR_MUSIC] = "XDG_MUSIC_DIR",
908 [CANFIGGER_USER_DIR_PICTURES] = "XDG_PICTURES_DIR",
909 [CANFIGGER_USER_DIR_VIDEOS] = "XDG_VIDEOS_DIR"
910};
911
912
913/* user-dirs.dirs is a shell fragment sourced by the desktop session, not a
914 canfigger file, so it cannot go through canfigger_parse_file(): its values
915 are double-quoted, may contain backslash escapes, and the "$HOME/" prefix has
916 to be expanded. This mirrors the reference xdg-user-dir-lookup implementation
917 -- deliberately, since matching whatever the desktop itself resolves matters
918 more here than being lenient. */
919static char *
920user_dir_from_file(const char *key)
921{
922 char *path = canfigger_config_file("user-dirs.dirs");
923 if (!path)
924 return NULL;
925
926 FILE *fp = fopen(path, "r");
927 free(path);
928 if (!fp)
929 return NULL;
930
931 static const char home_prefix[] = "$HOME/";
932 const char *home = getenv("HOME");
933 size_t keylen = strlen(key);
934 char line[1024];
935 char *result = NULL;
936
937 while (!result && fgets(line, sizeof line, fp) != NULL)
938 {
939 char *p = line;
940 while (isspace((unsigned char) *p))
941 p++;
942 if (strncmp(p, key, keylen) != 0)
943 continue;
944
945 p += keylen;
946 while (isspace((unsigned char) *p))
947 p++;
948 if (*p != '=')
949 continue;
950 p++;
951 while (isspace((unsigned char) *p))
952 p++;
953 if (*p != '"')
954 continue;
955 p++;
956
957 bool relative_to_home = false;
958 if (strncmp(p, home_prefix, sizeof home_prefix - 1) == 0)
959 {
960 relative_to_home = true;
961 p += sizeof home_prefix - 1;
962 }
963 /* Anything neither absolute nor under $HOME is unusable: an entry like
964 "Desktop" would resolve against the current directory. */
965 else if (*p != '/')
966 break;
967
968 size_t prefix = 0;
969 if (relative_to_home)
970 {
971 if (!home || !*home)
972 break;
973 prefix = strlen(home) + 1;
974 }
975
976 char *out = malloc_wrap(prefix + strlen(p) + 1);
977 if (!out)
978 break;
979 if (relative_to_home)
980 snprintf(out, prefix + 1, "%s/", home);
981
982 char *d = out + prefix;
983 while (*p != '\0' && *p != '"')
984 {
985 if (*p == '\\' && *(p + 1) != '\0')
986 p++;
987 *d++ = *p++;
988 }
989 *d = '\0';
990
991 /* An empty value (XDG_DESKTOP_DIR="") disables the directory; treat it as
992 absent so the caller gets the $HOME fallback rather than "". */
993 if (*out == '\0')
994 {
995 free(out);
996 break;
997 }
998 result = out;
999 }
1000
1001 fclose(fp);
1002 return result;
1003}
1004#else
1005/* Keyed by enum canfigger_user_dir, as user_dir_keys[] is. CSIDL_PROFILE stands
1006 in for Downloads, which has no CSIDL at all (it arrived with the Vista-era
1007 KNOWNFOLDERID API); the name is appended below. Unlike the POSIX table a
1008 missing row is not detectable here -- 0 is CSIDL_DESKTOP -- so every
1009 enumerator needs one. */
1010static const int user_dir_csidl[] = {
1011 [CANFIGGER_USER_DIR_DESKTOP] = CSIDL_DESKTOPDIRECTORY,
1012 [CANFIGGER_USER_DIR_DOWNLOAD] = CSIDL_PROFILE,
1013 [CANFIGGER_USER_DIR_TEMPLATES] = CSIDL_TEMPLATES,
1014 [CANFIGGER_USER_DIR_PUBLICSHARE] = CSIDL_COMMON_DOCUMENTS,
1015 [CANFIGGER_USER_DIR_DOCUMENTS] = CSIDL_PERSONAL,
1016 [CANFIGGER_USER_DIR_MUSIC] = CSIDL_MYMUSIC,
1017 [CANFIGGER_USER_DIR_PICTURES] = CSIDL_MYPICTURES,
1018 [CANFIGGER_USER_DIR_VIDEOS] = CSIDL_MYVIDEO
1019};
1020#endif
1021
1022
1023char *
1025{
1026#ifdef _WIN32
1027 if ((unsigned) which >= sizeof user_dir_csidl / sizeof *user_dir_csidl)
1028 return NULL;
1029
1030 char base[MAX_PATH];
1031 if (FAILED(SHGetFolderPathA(NULL, user_dir_csidl[which], NULL, 0, base)))
1032 return NULL;
1033
1034 if (which == CANFIGGER_USER_DIR_DOWNLOAD)
1035 return canfigger_path_join(base, "Downloads");
1036 return strclone_str(base);
1037#else
1038 if ((unsigned) which >= sizeof user_dir_keys / sizeof *user_dir_keys)
1039 return NULL;
1040
1041 if (!user_dir_keys[which])
1042 return NULL;
1043
1044 char *dir = user_dir_from_file(user_dir_keys[which]);
1045 if (dir)
1046 return dir;
1047
1048 const char *home = getenv("HOME");
1049 if (!home || !*home)
1050 return NULL;
1051
1052 /* The reference implementation falls back to $HOME for every type but the
1053 desktop, which keeps its historical $HOME/Desktop default. */
1054 if (which == CANFIGGER_USER_DIR_DESKTOP)
1055 return canfigger_path_join(home, "Desktop");
1056 return strclone_str(home);
1057#endif
1058}
Public API for the Canfigger configuration file parser.
struct Canfigger * next
Definition canfigger.h:167
char * canfigger_config_file(const char *filename)
Return the path to a config file in the platform base config directory.
Definition canfigger.c:579
struct Canfigger * canfigger_parse_file(const char *file, const int delimiter)
Parse a configuration file into a linked list of key-value nodes.
Definition canfigger.c:338
char * canfigger_state_dir(const char *appname)
Return the platform state directory for an application.
Definition canfigger.c:629
char * canfigger_cache_dir(const char *appname)
Return the platform cache directory for an application.
Definition canfigger.c:618
char * canfigger_data_dir(const char *appname)
Return the platform data directory for an application.
Definition canfigger.c:607
char * str
Definition canfigger.h:140
canfigger_user_dir
The well-known user directories, as named by the XDG user directories specification.
Definition canfigger.h:480
char * current
Definition canfigger.h:143
char * iter_ptr
Definition canfigger.h:146
void canfigger_free_list(struct Canfigger **node)
Free all remaining nodes in the list.
Definition canfigger.c:193
char * key
Definition canfigger.h:160
char * value
Definition canfigger.h:163
char * canfigger_config_dir(const char *appname)
Return the platform config directory for an application.
Definition canfigger.c:568
char * canfigger_path_join(const char *dir, const char *file)
Join a directory path and a filename with the platform separator.
Definition canfigger.c:794
void canfigger_free_current_key_node_advance(struct Canfigger **node)
Free the current node and advance the list pointer to the next node.
Definition canfigger.c:152
char * canfigger_runtime_dir(const char *appname)
Return the runtime directory for an application, or NULL.
Definition canfigger.c:640
struct attributes * attributes
Definition canfigger.h:165
A single node in the parsed configuration linked list.
Definition canfigger.h:158
Internal iteration state for a node's attribute list.
Definition canfigger.h:137