XDG Basedirs interface in a single header file
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

440 lines
8.9KB

  1. #ifndef SIMPLE_XDG_BDIRS_H
  2. #define SIMPLE_XDG_BDIRS_H
  3. #include <errno.h>
  4. #include <stdbool.h>
  5. #include <stdint.h>
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. #include <unistd.h>
  10. /* one of these types is passed to each of the functions below, denoting the
  11. * category of file to which the functions actions ought to correspond.
  12. * - `SIMPLE_XDG_BDIRS_DATA` is used for persistent data created by a program
  13. * - `SIMPLE_XDG_BDIRS_CONFIG` is used for a program's configuration files
  14. * - `SIMPLE_XDG_BDIRS_CACHE` is used for transient data which may be deleted
  15. * without warning by the system
  16. * - `SIMPLE_XDG_BDIRS_RUNTIME` is used for "runtime files", such as lock files
  17. * or sockets
  18. */
  19. enum simple_xdg_bdirs_ftype {
  20. SIMPLE_XDG_BDIRS_DATA,
  21. SIMPLE_XDG_BDIRS_CONFIG,
  22. SIMPLE_XDG_BDIRS_CACHE,
  23. SIMPLE_XDG_BDIRS_RUNTIME,
  24. };
  25. /* this function finds and returns a single string that is the path of a
  26. * directory into which all files of type `type` ought to be written.
  27. *
  28. * because the location of this directory is determined at runtime, and in
  29. * particular because it relies on the state of the filesystem and environment
  30. * variables, it is possible that no good candidate directory will be found, in
  31. * which case `NULL` will be returned.
  32. *
  33. * if a string is returned, it is guaranteed to always have a trailing '/',
  34. * meaning that a relative path can be appended directly without fear of
  35. * unintended results, like '/etx/xdgherbstluftwm'
  36. *
  37. * errors:
  38. * ENOENT: no candidate dir found
  39. * EINVAL: bad argument
  40. * EOVERFLOW: extremely-large string encountered (unlikely)
  41. * ENOMEM: malloc err
  42. */
  43. static char*
  44. simple_xdg_bdirs_write_dir(enum simple_xdg_bdirs_ftype type)
  45. {
  46. char *home = getenv("HOME");
  47. char *xdg_data_home;
  48. char *xdg_config_home;
  49. char *xdg_cache_home;
  50. char *xdg_runtime_dir;
  51. char *empty = "";
  52. char *s1, *s2, *r;
  53. size_t l1, l2;
  54. /* get components */
  55. switch (type) {
  56. case SIMPLE_XDG_BDIRS_DATA:
  57. xdg_data_home = getenv("XDG_DATA_HOME");
  58. if (xdg_data_home && xdg_data_home[0]) {
  59. s1 = empty;
  60. s2 = xdg_data_home;
  61. } else if (home && home[0]) {
  62. s1 = home;
  63. s2 = "/.local/share";
  64. } else {
  65. errno = ENOENT;
  66. return NULL;
  67. }
  68. break;
  69. case SIMPLE_XDG_BDIRS_CONFIG:
  70. xdg_config_home = getenv("XDG_CONFIG_HOME");
  71. if (xdg_config_home && xdg_config_home[0]) {
  72. s1 = empty;
  73. s2 = xdg_config_home;
  74. } else if (home && home[0]) {
  75. s1 = home;
  76. s2 = "/.config";
  77. } else {
  78. errno = ENOENT;
  79. return NULL;
  80. }
  81. break;
  82. case SIMPLE_XDG_BDIRS_CACHE:
  83. xdg_cache_home = getenv("XDG_CACHE_HOME");
  84. if (xdg_cache_home && xdg_cache_home[0]) {
  85. s1 = empty;
  86. s2 = xdg_cache_home;
  87. } else if (home && home[0]) {
  88. s1 = home;
  89. s2 = "/.cache";
  90. } else {
  91. errno = ENOENT;
  92. return NULL;
  93. }
  94. break;
  95. case SIMPLE_XDG_BDIRS_RUNTIME:
  96. xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
  97. if (xdg_runtime_dir && xdg_runtime_dir[0]) {
  98. s1 = empty;
  99. s2 = xdg_runtime_dir;
  100. } else {
  101. errno = ENOENT;
  102. return NULL;
  103. }
  104. break;
  105. default:
  106. errno = EINVAL;
  107. return NULL;
  108. }
  109. /* get s1 part length */
  110. for (l1 = 0; s1[l1]; l1++) {
  111. if (l1 == SIZE_MAX) {
  112. errno = EOVERFLOW;
  113. return NULL;
  114. }
  115. }
  116. /* get s2 part length */
  117. for (l2 = 0; s2[l2]; l1++, l2++) {
  118. if (l1 == SIZE_MAX) {
  119. errno = EOVERFLOW;
  120. return NULL;
  121. }
  122. }
  123. /* extra 2 for '/' and '\0' */
  124. for (l2 = 0; l2 < 2; l1++, l2++) {
  125. if (l1 == SIZE_MAX) {
  126. errno = EOVERFLOW;
  127. return NULL;
  128. }
  129. }
  130. /* build str */
  131. r = malloc(l1 * sizeof(char));
  132. if (r == NULL) {
  133. errno = ENOMEM;
  134. return NULL;
  135. }
  136. for (l1 = 0; s1[l1]; l1++)
  137. r[l1] = s1[l1];
  138. for (l2 = 0; s2[l2]; l1++, l2++)
  139. r[l1] = s2[l2];
  140. r[l1] = '/';
  141. /* slash dedupe */
  142. for (l2 = 0; l2 < l1; l2++) {
  143. if (r[l2] == '/' && r[l2 + 1] == '/') {
  144. memmove(r + l2, r + l2 + 1, l1 - l2 - 1);
  145. l1--;
  146. l2--;
  147. }
  148. }
  149. r[l1 + 1] = '\0';
  150. return r;
  151. }
  152. /* this function finds and returns a null-terminated array of strings that are
  153. * the paths, in order, where one ought to search for files of type `type` when
  154. * reading.
  155. *
  156. * because some filetypes have only a single, environment-determined read dir,
  157. * it's possible that none will be found, in which case `NULL` will be
  158. * returned.
  159. *
  160. * returned strings are guaranteed to always have a trailing '/', meaning that
  161. * a relative path can be appended directly without fear of unintended results,
  162. * like '/etx/xdgherbstluftwm'
  163. *
  164. * errors:
  165. * ENOENT: no candidate dirs found
  166. * EINVAL: bad argument
  167. * EOVERFLOW: extremely-large string encountered (unlikely)
  168. * ENOMEM: malloc err
  169. */
  170. static char**
  171. simple_xdg_bdirs_read_dirs(enum simple_xdg_bdirs_ftype type)
  172. {
  173. char *s1 = NULL, *s2 = NULL;
  174. bool flag;
  175. size_t l1, l2, l3, l4;
  176. char **r = NULL, **cur;
  177. /* cache and runtime are the same as write_dir */
  178. switch (type) {
  179. case SIMPLE_XDG_BDIRS_CACHE:
  180. r = malloc(2 * sizeof(char*));
  181. if (r == NULL) {
  182. errno = ENOMEM;
  183. return NULL;
  184. }
  185. r[1] = NULL;
  186. r[0] = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_CACHE);
  187. if (r[0] == NULL)
  188. goto abort;
  189. return r;
  190. case SIMPLE_XDG_BDIRS_RUNTIME:
  191. r = malloc(2 * sizeof(char*));
  192. if (r == NULL) {
  193. errno = ENOMEM;
  194. return NULL;
  195. }
  196. r[1] = NULL;
  197. r[0] = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_RUNTIME);
  198. if (r[0] == NULL)
  199. goto abort;
  200. return r;
  201. /* data and config are write_dir + fallbacks */
  202. case SIMPLE_XDG_BDIRS_DATA:
  203. s1 = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_DATA);
  204. s2 = getenv("XDG_DATA_DIRS");
  205. if (!s2 || !s2[0])
  206. s2 = "/usr/local/share:/usr/share";
  207. break;
  208. case SIMPLE_XDG_BDIRS_CONFIG:
  209. s1 = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_CONFIG);
  210. s2 = getenv("XDG_CONFIG_DIRS");
  211. if (!s2 || !s2[0])
  212. s2 = "/etc/xdg";
  213. break;
  214. default:
  215. errno = EINVAL;
  216. return NULL;
  217. }
  218. /* count number of fallback dirs */
  219. for (flag = true, l1 = 0, l2 = 1; s2[l1]; l1++) {
  220. if (l1 == SIZE_MAX) {
  221. errno = EOVERFLOW;
  222. goto abort;
  223. }
  224. if (s2[l1] == ':') {
  225. if (!flag) {
  226. l2++;
  227. flag = true;
  228. }
  229. } else {
  230. flag = false;
  231. }
  232. }
  233. /* make r */
  234. r = malloc((l2 + (s1 != NULL) + 1) * sizeof(char*));
  235. if (r == NULL) {
  236. errno = ENOMEM;
  237. goto abort;
  238. }
  239. /* insert write_dir, if it was found */
  240. if (s1 != NULL) {
  241. r[0] = s1;
  242. cur = r + 1;
  243. } else {
  244. cur = r;
  245. }
  246. for (l1 = 0, l2 = 0; s2[l1];) {
  247. /* skip over multiple ':' */
  248. for (; s2[l2] == ':'; l2++);
  249. /* check for done */
  250. l1 = l2;
  251. if (s2[l1] == '\0')
  252. break;
  253. /* find the end of the current dir in the string */
  254. for (; s2[l2] && s2[l2] != ':'; l2++);
  255. /* copy the current string into r */
  256. *cur = malloc((l2 - l1 + 2) * sizeof(char));
  257. if (*cur == NULL) {
  258. errno = ENOMEM;
  259. goto abort;
  260. }
  261. if (l2 - l1 != 0)
  262. strncpy(*cur, s2 + l1, l2 - l1);
  263. (*cur)[l2 - l1] = '/';
  264. (*cur)[l2 - l1 + 1] = '\0';
  265. /* slash dedupe */
  266. l3 = l2 - l1;
  267. for (l4 = 0; l4 < l3; l4++) {
  268. if ((*cur)[l4] == '/' && (*cur)[l4 + 1] == '/') {
  269. memmove(*cur + l4, *cur + l4 + 1, l3 - l4);
  270. l3--;
  271. l4--;
  272. }
  273. }
  274. (*cur)[l3 + 1] = '\0';
  275. cur++;
  276. }
  277. *cur = NULL;
  278. /* done ^_^ */
  279. return r;
  280. abort:
  281. /* whoops, something went wrong :c */
  282. if (s1 != NULL)
  283. free(s1);
  284. if (r == NULL)
  285. return NULL;
  286. for (cur = r; *cur != NULL; cur++)
  287. free(cur);
  288. free(r);
  289. return NULL;
  290. }
  291. static char*
  292. sub_simple_xdg_bdirs_join(const char *s1, const char *s2)
  293. {
  294. size_t l1, l2, l3;
  295. char *r;
  296. l1 = strlen(s1);
  297. l2 = strlen(s2);
  298. for (l3 = l1; l3 != SIZE_MAX && l2 != 0; l3++, l2--);
  299. if (l3 == SIZE_MAX) {
  300. errno = EOVERFLOW;
  301. return NULL;
  302. }
  303. r = malloc((l3 + 1) * sizeof(char));
  304. if (r == NULL) {
  305. errno = ENOMEM;
  306. return NULL;
  307. }
  308. strcpy(r, s1);
  309. strcpy(r + l1, s2);
  310. return r;
  311. }
  312. /* take a relative path and set of directories returned from
  313. * `simple_xdg_bdirs_read_dirs` and return either a full path to an existing
  314. * file or, on error, NULL
  315. *
  316. * errors
  317. * ENOENT: target file not found or readable
  318. * EINVAL: bad argument
  319. * EOVERFLOW: extremely-large string encountered (unlikely)
  320. * ENOMEM: malloc err
  321. */
  322. static inline char*
  323. simple_xdg_bdirs_fullpath_read(const char *rel_path, char **read_dirs)
  324. {
  325. char **cur, *path;
  326. if (rel_path == NULL && read_dirs == NULL) {
  327. errno = EINVAL;
  328. return NULL;
  329. }
  330. for (cur = read_dirs; *cur != NULL; cur++) {
  331. path = sub_simple_xdg_bdirs_join(*cur, rel_path);
  332. if (path == NULL)
  333. return NULL;
  334. if (!access(path, R_OK)) {
  335. return path;
  336. }
  337. free(path);
  338. }
  339. errno = ENOENT;
  340. return NULL;
  341. }
  342. /* take a relative path and a write directory returned from
  343. * `simple_xdg_bdirs_write_dir` and return either a full path to the targeted
  344. * write path or, on error, NULL
  345. *
  346. * errors
  347. * ENOENT: write_dir is not found or not write accessible
  348. * EINVAL: bad argument
  349. * EOVERFLOW: extremely-large string encountered (unlikely)
  350. * ENOMEM: malloc err
  351. */
  352. static inline char*
  353. simple_xdg_bdirs_fullpath_write(const char *rel_path, char *write_dir)
  354. {
  355. if (rel_path == NULL && write_dir == NULL) {
  356. errno = EINVAL;
  357. return NULL;
  358. }
  359. if (access(write_dir, W_OK)) {
  360. errno = ENOENT;
  361. return NULL;
  362. }
  363. return sub_simple_xdg_bdirs_join(write_dir, rel_path);
  364. }
  365. #endif