logo

utils-std

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

df.c (11770B)


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