logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/

df.c (11818B)


  1. // utils-std: Collection of commonly available Unix tools
  2. // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
  3. // SPDX-License-Identifier: MPL-2.0
  4. #define _POSIX_C_SOURCE 202405L
  5. #define _DEFAULT_SOURCE // mntent in glibc 2.19+
  6. #include "../config.h"
  7. #include "../lib/reallocarray.h"
  8. #include "../libutils/getopt_nolong.h"
  9. #include "../libutils/humanize.h"
  10. #include <ctype.h> // iscntrl, isspace
  11. #include <errno.h> // errno
  12. #include <mntent.h>
  13. #include <stdbool.h>
  14. #include <stdint.h>
  15. #include <stdio.h> // printf
  16. #include <stdlib.h> // abort, exit
  17. #include <string.h> // strerror
  18. #include <sys/stat.h> // stat, dev_t
  19. #include <sys/statvfs.h>
  20. #include <unistd.h> // getopt
  21. #ifdef HAS_GETOPT_LONG
  22. #include <getopt.h>
  23. #endif
  24. #define STR(s) #s
  25. /*
  26. * Rough idea of builtin filesystems in Linux 6.12.31:
  27. * $ grep -RI '\bregister_filesystem(' /usr/src/linux/fs/ | wc -l
  28. * 79
  29. */
  30. #define DF_MNT_TYPE_MAX 128
  31. // Grabbed from OpenRC rc-functions.sh
  32. // clang-format off
  33. static const char *net_fs_list[] = {
  34. "afs", "ceph", "cifs", "coda", "davfs", "fuse", "fuse.glusterfs",
  35. "fuse.sshfs", "gfs", "glusterfs", "lustre", "ncpfs", "nfs", "nfs4",
  36. "ocfs2", "shfs", "smbfs"
  37. };
  38. // clang-format on
  39. blksize_t forced_bsize = 0;
  40. const char *argv0 = "df";
  41. // Replaces control characters and whitespaces with '?' in-place (no allocation)
  42. static void
  43. static_escape(char *str)
  44. {
  45. for(size_t i = 0; i < strlen(str); i++)
  46. if(iscntrl(str[i]) || isspace(str[i])) str[i] = '?';
  47. }
  48. int
  49. main(int argc, char *argv[])
  50. {
  51. bool opt_P = false, opt_h = false, opt_a = false, opt_T = false, opt_l = false, opt_i = false;
  52. int fs_col_width = 20;
  53. dev_t *devices = NULL;
  54. size_t excluded_count = 0;
  55. static char *excluded[DF_MNT_TYPE_MAX];
  56. size_t only_count = 0;
  57. static char *only[DF_MNT_TYPE_MAX];
  58. #ifdef HAS_GETOPT_LONG
  59. // Strictly for GNUisms compatibility so no long-only options
  60. // clang-format off
  61. static struct option opts[] = {
  62. {"all", no_argument, NULL, 'a'},
  63. {"human-readable", no_argument, NULL, 'h'},
  64. {"inodes", no_argument, NULL, 'i'},
  65. {"local", no_argument, NULL, 'l'},
  66. {"portability", no_argument, NULL, 'P'},
  67. {"type", required_argument, NULL, 't'},
  68. {"exclude-type", required_argument, NULL, 'x'},
  69. {0, 0, 0, 0},
  70. };
  71. // clang-format on
  72. // Need + as first character to get POSIX-style option parsing
  73. for(int c = -1; (c = getopt_long(argc, argv, "+:ahilPkTt:x:", opts, NULL)) != -1;)
  74. #else
  75. for(int c = -1; (c = getopt_nolong(argc, argv, ":ahilPkTt:x:")) != -1;)
  76. #endif
  77. {
  78. switch(c)
  79. {
  80. case 'a':
  81. opt_a = true;
  82. break;
  83. case 'P':
  84. if(forced_bsize == 0) forced_bsize = 512;
  85. opt_P = true;
  86. break;
  87. case 'k':
  88. forced_bsize = 1024;
  89. break;
  90. case 'h':
  91. opt_h = true;
  92. fs_col_width = 30;
  93. break;
  94. case 'i':
  95. opt_i = true;
  96. break;
  97. case 'l':
  98. opt_l = true;
  99. break;
  100. case 'T':
  101. opt_T = true;
  102. break;
  103. case 't':
  104. if(only_count >= (DF_MNT_TYPE_MAX - 1))
  105. {
  106. fprintf(
  107. stderr,
  108. "%s: error: Can only select up to " STR(DF_MNT_TYPE_MAX) " mnt_types via option `-t`\n",
  109. argv0);
  110. return 1;
  111. }
  112. only[only_count++] = optarg;
  113. break;
  114. case 'x':
  115. if(excluded_count >= (DF_MNT_TYPE_MAX - 1))
  116. {
  117. fprintf(
  118. stderr,
  119. "%s: error: Can only select up to " STR(DF_MNT_TYPE_MAX) " mnt_types via option `-x`\n",
  120. argv0);
  121. return 1;
  122. }
  123. excluded[excluded_count++] = optarg;
  124. break;
  125. case ':':
  126. fprintf(stderr, "%s: error: Missing operand for option: '-%c'\n", argv0, optopt);
  127. return 1;
  128. case '?':
  129. GETOPT_UNKNOWN_OPT
  130. return 1;
  131. }
  132. }
  133. if(opt_P && opt_T)
  134. {
  135. fprintf(stderr, "%s: error: Options -P and -T are incompatible\n", argv0);
  136. return 1;
  137. }
  138. if(opt_P) opt_h = false;
  139. argc -= optind;
  140. argv += optind;
  141. int args_left = argc != 0 ? argc : 1;
  142. bool tty_out = isatty(1);
  143. int col_width = tty_out ? 10 : 0;
  144. if(!tty_out)
  145. {
  146. errno = 0;
  147. fs_col_width = 0;
  148. }
  149. dev_t *arg_devs = NULL;
  150. if(argc > 0)
  151. {
  152. arg_devs = calloc(argc, sizeof(dev_t));
  153. if(arg_devs == NULL)
  154. {
  155. fprintf(stderr, "%s: error: Failed to allocate arg_devs array: %s\n", argv0, strerror(errno));
  156. return 1;
  157. }
  158. }
  159. for(int i = 0; i < argc; i++)
  160. {
  161. struct stat file_stats;
  162. if(stat(argv[i], &file_stats) != 0)
  163. {
  164. fprintf(stderr,
  165. "%s: error: Failed getting status for file '%s': %s\n",
  166. argv0,
  167. argv[i],
  168. strerror(errno));
  169. goto error;
  170. }
  171. arg_devs[i] = file_stats.st_dev;
  172. }
  173. // Begin: Print header
  174. printf("%-*s ", fs_col_width, "Filesystem");
  175. if(opt_T) printf("%*s ", col_width, "Type");
  176. if(opt_i)
  177. {
  178. printf("%*s %*s %*s ", col_width, "Inodes", col_width, "Iused", col_width, "IFree");
  179. if(opt_P)
  180. printf("IUse%% Mounted on\n");
  181. else
  182. printf("IUse%% Mountpoint\n");
  183. }
  184. else
  185. {
  186. // cast to (long int) as POSIX.1-2024 defines blksize_t as signed
  187. // integer no longer than `long`
  188. if(forced_bsize != 0)
  189. printf("%ld-blocks ", (long int)forced_bsize);
  190. else
  191. printf("%*s ", col_width, "Total");
  192. printf("%*s %*s ", col_width, "Used", col_width, "Available");
  193. if(opt_P)
  194. printf("Capacity Mounted on\n");
  195. else
  196. printf("Use%% Mountpoint\n");
  197. }
  198. // End: Print header
  199. FILE *mounted = setmntent(MOUNTED, "r");
  200. if(mounted == NULL)
  201. {
  202. fprintf(stderr,
  203. "%s: error: Failed opening setmntent(\"" MOUNTED "\", \"r\"): %s\n",
  204. argv0,
  205. strerror(errno));
  206. goto error;
  207. }
  208. // Linux has a configurable limit of maximum mounts which defaults to 100_000
  209. // See /proc/sys/fs/mount-max introduced in Linux 4.9 for CVE-2016-6213
  210. // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=d29216842a85c7970c536108e093963f02714498
  211. //
  212. // Which is way too much to be either stack-allocated or statically allocated:
  213. // 100000*sizeof(dev_t) = 100000*8 bytes ≈ 781.25 KiB
  214. // So reallocarray to the rescue
  215. size_t devices_found = 0;
  216. size_t devices_len = 100;
  217. devices = reallocarray(NULL, devices_len, sizeof(dev_t));
  218. if(!devices)
  219. {
  220. fprintf(stderr,
  221. "%s: error: Failed to allocate memory for storing (%zd) devices: %s\n",
  222. argv0,
  223. devices_len,
  224. strerror(errno));
  225. goto error;
  226. }
  227. // Even with argc>0 we still need to go over mntent for the filesystem mountpoint and type
  228. while(args_left > 0)
  229. {
  230. struct mntent *mntent = getmntent(mounted);
  231. if(mntent == NULL) break;
  232. if(excluded_count > 0)
  233. {
  234. bool exclude = false;
  235. for(size_t i = 0; i < excluded_count; i++)
  236. if(strcmp(excluded[i], mntent->mnt_type) == 0)
  237. {
  238. exclude = true;
  239. break;
  240. }
  241. if(exclude) continue;
  242. }
  243. if(only_count > 0)
  244. {
  245. bool include = false;
  246. for(size_t i = 0; i < only_count; i++)
  247. if(strcmp(only[i], mntent->mnt_type) == 0)
  248. {
  249. include = true;
  250. break;
  251. }
  252. if(!include) continue;
  253. }
  254. if(opt_l)
  255. {
  256. bool remote = false;
  257. for(size_t i = 0; i < sizeof(net_fs_list) / sizeof(char *); i++)
  258. if(strcmp(net_fs_list[i], mntent->mnt_type) == 0)
  259. {
  260. remote = true;
  261. break;
  262. }
  263. if(remote) continue;
  264. }
  265. struct stat file_stats;
  266. if(!opt_a || argc > 0)
  267. {
  268. if(stat(mntent->mnt_dir, &file_stats) != 0)
  269. {
  270. fprintf(stderr,
  271. "%s: warning: Failed getting status for file '%s': %s\n",
  272. argv0,
  273. mntent->mnt_dir,
  274. strerror(errno));
  275. errno = 0;
  276. }
  277. }
  278. if(argc > 0)
  279. {
  280. bool found = false;
  281. for(int i = 0; i < argc; i++)
  282. if(arg_devs[i] == file_stats.st_dev)
  283. {
  284. found = true;
  285. break;
  286. }
  287. if(!found) continue;
  288. args_left--;
  289. }
  290. if(!opt_a)
  291. {
  292. bool dupe = false;
  293. for(size_t i = 0; i < devices_found; i++)
  294. if(devices[i] == file_stats.st_dev)
  295. {
  296. dupe = true;
  297. break;
  298. }
  299. if(dupe) continue;
  300. if(devices_found + 1 >= devices_len)
  301. {
  302. devices_len *= 2;
  303. if(devices_len <= 0) abort();
  304. devices = reallocarray(devices, devices_len, sizeof(dev_t));
  305. if(!devices)
  306. {
  307. fprintf(stderr,
  308. "%s: error: Failed to allocate memory for storing (%zd) devices: %s\n",
  309. argv0,
  310. devices_len,
  311. strerror(errno));
  312. goto error;
  313. }
  314. }
  315. devices[devices_found++] = file_stats.st_dev;
  316. }
  317. // Note: musl prior to 1.2.5 has broken getmntent when octal sequences and carriage return is used
  318. // https://git.musl-libc.org/cgit/musl/commit/src/misc/mntent.c?id=f314e133929b6379eccc632bef32eaebb66a7335
  319. // https://git.musl-libc.org/cgit/musl/commit/src/misc/mntent.c?id=ee1d39bc1573c1ae49ee6b658938b56bbef95a6c
  320. struct statvfs stats;
  321. if(statvfs(mntent->mnt_dir, &stats) != 0)
  322. {
  323. fprintf(stderr,
  324. "%s: warning: Failed getting statistics for filesystem '%s': %s\n",
  325. argv0,
  326. mntent->mnt_dir,
  327. strerror(errno));
  328. errno = 0;
  329. static_escape(mntent->mnt_fsname);
  330. static_escape(mntent->mnt_dir);
  331. if(opt_a)
  332. {
  333. printf("%-*s ", fs_col_width, mntent->mnt_fsname);
  334. if(opt_T) printf("%*s ", col_width, mntent->mnt_type);
  335. printf("%*s ", col_width, "-"); // total
  336. printf("%*s ", col_width, "-"); // used
  337. printf("%*s ", col_width, "-"); // free
  338. printf("%*s ", tty_out ? 3 : 0, "-"); // percent
  339. printf("%s\n", mntent->mnt_dir);
  340. }
  341. continue;
  342. }
  343. // Skip null filesystems
  344. if(!opt_a && stats.f_blocks == 0) continue;
  345. // Needs to be done after calling statvfs(3) and stat(3)
  346. static_escape(mntent->mnt_fsname);
  347. static_escape(mntent->mnt_dir);
  348. printf("%-*s ", fs_col_width, mntent->mnt_fsname);
  349. if(opt_T) printf("%*s ", col_width, mntent->mnt_type);
  350. if(opt_i)
  351. {
  352. intmax_t used = stats.f_files - stats.f_ffree;
  353. intmax_t percent = (used / stats.f_files) * 100;
  354. if(opt_h && !opt_P)
  355. {
  356. struct si_scale total_scl = dtosi(stats.f_files, false);
  357. struct si_scale used_scl = dtosi(used, false);
  358. struct si_scale free_scl = dtosi(stats.f_ffree, false);
  359. int width_num = tty_out ? col_width - 1 : 0;
  360. int width_pre = tty_out ? 1 : 0;
  361. printf("%*.2f%-*s ", width_num, total_scl.number, width_pre, total_scl.prefix);
  362. printf("%*.2f%-*s ", width_num, used_scl.number, width_pre, used_scl.prefix);
  363. printf("%*.2f%-*s ", width_num, free_scl.number, width_pre, free_scl.prefix);
  364. }
  365. else
  366. {
  367. printf("%*jd ", col_width, (intmax_t)stats.f_files);
  368. printf("%*jd ", col_width, used);
  369. printf("%*jd ", col_width, (intmax_t)stats.f_ffree);
  370. }
  371. printf("%*jd%% ", tty_out ? 4 : 0, percent);
  372. printf("%s\n", mntent->mnt_dir);
  373. continue;
  374. }
  375. intmax_t percent = 0;
  376. intmax_t total = stats.f_frsize * (stats.f_blocks != 0 ? stats.f_blocks : 1);
  377. intmax_t free = stats.f_bfree * (stats.f_bsize != 0 ? stats.f_bsize : 1);
  378. intmax_t used = total - free;
  379. if(used + free)
  380. {
  381. percent = (used * 100) / (used + free);
  382. if(used * 100 != percent * (used + free)) percent++;
  383. }
  384. if(forced_bsize != 0)
  385. {
  386. total /= forced_bsize;
  387. free /= forced_bsize;
  388. used /= forced_bsize;
  389. }
  390. if(opt_h && !opt_P)
  391. {
  392. struct si_scale total_scl = dtosi(total, true);
  393. struct si_scale used_scl = dtosi(used, true);
  394. struct si_scale free_scl = dtosi(free, true);
  395. int width_num = tty_out ? col_width - 3 : 0;
  396. int width_pre = tty_out ? 3 : 0;
  397. printf("%*.2f%-*s ", width_num, total_scl.number, width_pre, total_scl.prefix);
  398. printf("%*.2f%-*s ", width_num, used_scl.number, width_pre, used_scl.prefix);
  399. printf("%*.2f%-*s ", width_num, free_scl.number, width_pre, free_scl.prefix);
  400. }
  401. else
  402. {
  403. printf("%*jd ", col_width, total);
  404. printf("%*jd ", col_width, used);
  405. printf("%*jd ", col_width, free);
  406. }
  407. printf("%*jd%% ", tty_out ? 3 : 0, percent);
  408. printf("%s\n", mntent->mnt_dir);
  409. }
  410. endmntent(mounted);
  411. free(devices);
  412. free(arg_devs);
  413. return 0;
  414. error:
  415. free(devices);
  416. free(arg_devs);
  417. return 1;
  418. }