Blob


1 /*
2 * Copyright (c) 2023 Benjamin Stürz <benni@stuerz.xyz>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16 #define _XOPEN_SOURCE 700
17 #define _BSD_SOURCE 1
18 #define DKTYPENAMES
19 #define WSDEBUG 0
20 #include <stddef.h>
21 #include <dev/biovar.h>
22 #include <sys/cdefs.h>
23 #include <sys/types.h>
24 #include <sys/disklabel.h>
25 #include <sys/sysctl.h>
26 #include <sys/ioctl.h>
27 #include <sys/mount.h>
28 #include <sys/dkio.h>
29 #include <inttypes.h>
30 #include <stdbool.h>
31 #include <string.h>
32 #include <stdarg.h>
33 #include <stdint.h>
34 #include <stdlib.h>
35 #include <unistd.h>
36 #include <libgen.h>
37 #include <stdio.h>
38 #include <fcntl.h>
39 #include <ctype.h>
40 #include <errno.h>
41 #include <util.h>
42 #include <err.h>
44 #define PREFIX0_ASCII "|-"
45 #define PREFIX0_UNICODE "├─"
46 #define PREFIX1_ASCII "`-"
47 #define PREFIX1_UNICODE "└─"
48 #define PREFIX2_ASCII "| `-"
49 #define PREFIX2_UNICODE "│ └─"
51 static const char *prefix0, *prefix1, *prefix2;
53 static int diskcount (void)
54 {
55 const int mib[2] = { CTL_HW, HW_DISKCOUNT };
56 int diskcount;
57 size_t len = sizeof diskcount;
59 if (sysctl (mib, 2, &diskcount, &len, NULL, 0) == -1)
60 err (1, "sysctl(hw.diskcount)");
62 return diskcount;
63 }
65 static char *disknames (void)
66 {
67 const int num = diskcount ();
68 const int mib[2] = { CTL_HW, HW_DISKNAMES };
69 size_t len = 32 * num;
70 char *buffer = malloc (len);
72 if (sysctl (mib, 2, buffer, &len, NULL, 0) == -1)
73 err (1, "sysctl(hw.disknames)");
75 return buffer;
76 }
78 static char *stripdisk (char *n)
79 {
80 const char sufx[] = " disk";
81 const size_t ln = strnlen (n, 16);
82 const size_t ls = sizeof sufx - 1;
84 if (memcmp (n + ln - ls, sufx, ls) == 0) {
85 n[ln - ls] = '\0';
86 }
88 return n;
89 }
91 static void print_size (uint64_t sz)
92 {
93 const struct unit {
94 char sym;
95 uint64_t factor;
96 } units[] = {
97 { 'P', 1ull << 50 },
98 { 'T', 1ull << 40 },
99 { 'G', 1ull << 30 },
100 { 'M', 1ull << 20 },
101 { 'K', 1ull << 10 },
102 { '0', 0 },
103 };
105 char sym = 'B';
106 uint64_t factor = 1;
108 for (const struct unit *u = &units[0]; u->factor; ++u) {
109 if (sz >= (u->factor * 9 / 10)) {
110 sym = u->sym;
111 factor = u->factor;
112 break;
116 const unsigned scaled10 = sz * 10 / factor;
117 const unsigned scaled = sz / factor;
118 if (scaled10 >= 1000) {
119 printf ("%u", scaled);
120 } else if (scaled10 >= 100) {
121 printf (" %u", scaled);
122 } else {
123 printf ("%u.%u", scaled, scaled10 % 10);
126 putchar (sym);
127 putchar (' ');
130 enum {
131 FIELD_NAME = 0x01,
132 FIELD_DUID = 0x02,
133 FIELD_SIZE = 0x04,
134 FIELD_USED = 0x08,
135 FIELD_FREE = 0x10,
136 FIELD_TYPE = 0x20,
137 FIELD_COMMENT = 0x40,
139 FIELD_DEFAULT = FIELD_NAME | FIELD_SIZE | FIELD_TYPE | FIELD_COMMENT,
140 };
142 enum {
143 OPT_NOHEADER = 0x01,
144 OPT_NOBIO = 0x02,
145 };
147 struct my_diskinfo;
149 struct my_partinfo {
150 char letter;
151 uint64_t size;
152 uint64_t fssize;
153 uint64_t free;
154 const char *fstype;
155 const char *mount;
156 const struct my_diskinfo *sub; // If this is part of a RAID
157 const char *raidstatus; // Only available if (sub != NULL)
158 };
160 struct my_diskinfo {
161 char type[16];
162 char label[16];
163 char name[8];
164 uint64_t size;
165 uint64_t used;
166 u_char duid[8];
167 uint8_t num_parts;
168 struct my_partinfo parts[MAXPARTITIONS];
169 const char *raidstatus; // If this is a RAID device
170 };
172 struct padding {
173 int name;
174 /* duid = 8 */
175 /* size = 4 */
176 /* used = 4 */
177 /* free = 4 */
178 int type;
179 int comment;
180 };
182 static void print_header (int fields, const struct padding *p)
184 if (fields & FIELD_NAME)
185 printf ("%-*s ", p->name, "NAME");
187 if (fields & FIELD_DUID)
188 printf ("%-18s ", "DUID");
190 if (fields & FIELD_SIZE)
191 printf ("%-4s ", "SIZE");
193 if (fields & FIELD_USED)
194 printf ("%-4s ", "USED");
196 if (fields & FIELD_FREE)
197 printf ("%-4s ", "FREE");
199 if (fields & FIELD_TYPE)
200 printf ("%-*s ", p->type, "TYPE");
202 if (fields & FIELD_COMMENT)
203 printf ("%-*s ", p->comment, "COMMENT");
205 #if WSDEBUG
206 putchar ('X');
207 #endif
209 putchar ('\n');
212 static void print_duid (const u_char *duid)
214 for (size_t i = 0; i < 8; ++i) {
215 printf ("%02x", duid[i]);
219 static void print_disk (const struct my_diskinfo *, int, int, const char *, const struct padding *);
220 static void print_part (
221 const struct my_diskinfo *disk,
222 const struct my_partinfo *part,
223 int fields,
224 int options,
225 bool last,
226 const struct padding *p
227 ) {
228 if (fields & FIELD_NAME) {
229 const char *prefix = last ? prefix1 : prefix0;
230 printf (
231 "%s%s%c%-*s ",
232 prefix,
233 disk->name,
234 part->letter,
235 p->name - 2 - (int)strlen (disk->name) - 1,
236 ""
237 );
240 if (fields & FIELD_DUID) {
241 print_duid (disk->duid);
242 printf (".%c ", part->letter);
245 if (fields & FIELD_SIZE)
246 print_size (part->size);
248 if (fields & FIELD_USED) {
249 if (part->fssize) {
250 print_size (part->fssize - part->free);
251 } else {
252 printf (" N/A ");
256 if (fields & FIELD_FREE) {
257 if (part->fssize) {
258 print_size (part->free);
259 } else {
260 printf (" N/A ");
264 if (fields & FIELD_TYPE)
265 printf ("%-*s ", p->type, part->fstype);
267 if (fields & FIELD_COMMENT)
268 printf ("%-*s ", p->comment, part->mount ? part->mount : "");
270 #if WSDEBUG
271 putchar ('X');
272 #endif
274 putchar ('\n');
276 if (part->sub) {
277 print_disk (part->sub, fields, options, part->raidstatus, p);
281 static void print_disk (
282 const struct my_diskinfo *disk,
283 int fields,
284 int options,
285 const char *raidstatus,
286 const struct padding *p
287 ) {
288 if (fields & FIELD_NAME) {
289 const char *prefix = raidstatus ? prefix2 : "";
291 printf (
292 "%s%-*s ",
293 prefix,
294 p->name - (raidstatus ? 4 : 0),
295 disk->name
296 );
299 if (fields & FIELD_DUID) {
300 print_duid (disk->duid);
301 printf (" ");
304 if (fields & FIELD_SIZE)
305 print_size (disk->size);
307 if (fields & FIELD_USED)
308 print_size (disk->used);
310 if (fields & FIELD_FREE)
311 print_size (disk->size - disk->used);
313 if (fields & FIELD_TYPE)
314 printf ("%-*.16s ", p->type, disk->type);
316 if (fields & FIELD_COMMENT) {
317 if (raidstatus) {
318 printf ("%-*s ", p->comment, raidstatus);
319 } else if (disk->raidstatus) {
320 printf (
321 "%.16s (%s)%*s ",
322 disk->label,
323 disk->raidstatus,
324 p->comment - (int)strnlen (disk->label, sizeof disk->label) - (int)strlen (disk->raidstatus) - 3,
325 ""
326 );
327 } else {
328 printf ("%-*.16s ", p->comment, disk->label);
332 #if WSDEBUG
333 putchar ('X');
334 #endif
336 putchar ('\n');
338 if (!raidstatus) {
339 for (uint8_t i = 0; i < disk->num_parts; ++i)
340 print_part (disk, &disk->parts[i], fields, options, i == (disk->num_parts - 1), p);
344 static const struct statfs *find_mount (const char *dev)
346 static struct statfs *mounts = NULL;
347 static int n_mounts;
349 if (!mounts) {
350 n_mounts = getmntinfo (&mounts, MNT_NOWAIT);
351 if (n_mounts == 0)
352 err (1, "getmntinfo()");
355 for (int i = 0; i < n_mounts; ++i) {
356 if (!strcmp (dev, mounts[i].f_mntfromname))
357 return &mounts[i];
360 return NULL;
363 static int read_disk (const char *name, struct my_diskinfo *disk)
365 struct disklabel label;
366 char *ppath, *letter;
368 memset (disk, 0, sizeof *disk);
370 { // Read disklabel.
371 size_t len;
372 int fd;
374 fd = opendev (name, O_RDONLY, OPENDEV_PART | OPENDEV_BLCK, &ppath);
375 if (fd < 0) {
376 warn ("read_disk(): opendev(%s)", name);
377 return -1;
380 if (ioctl (fd, DIOCGDINFO, &label) < 0) {
381 warn ("read_disk(): ioctl(%s, DIOCGDINFO)", name);
382 close (fd);
383 return -1;
385 close (fd);
387 len = strlen (ppath);
388 letter = ppath + len - 1;
391 strlcpy (disk->name, name, sizeof disk->name);
392 disk->size = DL_GETDSIZE (&label) * label.d_secsize;
393 memcpy (disk->type, label.d_typename, sizeof disk->type);
394 stripdisk (disk->type);
395 memcpy (disk->label, label.d_packname, sizeof disk->label);
396 memcpy (disk->duid, label.d_uid, sizeof disk->duid);
397 disk->num_parts = 0;
399 for (uint16_t i = 0; i < label.d_npartitions; ++i) {
400 const struct partition *p = &label.d_partitions[i];
401 const struct statfs *mnt;
402 struct my_partinfo part;
404 memset (&part, 0, sizeof part);
406 part.size = DL_GETPSIZE (p) * label.d_secsize;
408 if (!part.size)
409 continue;
411 part.letter = 'a' + i;
412 *letter = part.letter;
413 mnt = find_mount (ppath);
415 if (mnt) {
416 const uint64_t bs = mnt->f_bsize;
417 part.mount = mnt->f_mntonname;
418 part.fssize = mnt->f_blocks * bs;
419 part.free = mnt->f_bfree * bs;
422 part.fstype = fstypenames[p->p_fstype];
423 if (i != 2)
424 disk->used += part.size;
425 disk->parts[disk->num_parts++] = part;
428 return 0;
431 static const char *bd_statusstr (int status) {
432 switch (status) {
433 case BIOC_SDONLINE: return BIOC_SDONLINE_S;
434 case BIOC_SDOFFLINE: return BIOC_SDOFFLINE_S;
435 case BIOC_SDFAILED: return BIOC_SDFAILED_S;
436 case BIOC_SDREBUILD: return BIOC_SDREBUILD_S;
437 case BIOC_SDHOTSPARE: return BIOC_SDHOTSPARE_S;
438 case BIOC_SDUNUSED: return BIOC_SDUNUSED_S;
439 case BIOC_SDSCRUB: return BIOC_SDSCRUB_S;
440 case BIOC_SDINVALID: return BIOC_SDINVALID_S;
441 default: return "Unknown";
445 static const char *bv_statusstr (int status) {
446 switch (status) {
447 case BIOC_SVONLINE: return BIOC_SVONLINE_S;
448 case BIOC_SVOFFLINE: return BIOC_SVOFFLINE_S;
449 case BIOC_SVDEGRADED: return BIOC_SVDEGRADED_S;
450 case BIOC_SVBUILDING: return BIOC_SVBUILDING_S;
451 case BIOC_SVSCRUB: return BIOC_SVSCRUB_S;
452 case BIOC_SVREBUILD: return BIOC_SVREBUILD_S;
453 case BIOC_SVINVALID: return BIOC_SVINVALID_S;
454 default: return "Unknown";
458 static void read_raid (
459 struct my_diskinfo *disk,
460 struct my_diskinfo *disks,
461 size_t num_disks
462 ) {
463 struct bioc_inq bi;
464 int fd;
466 fd = opendev (disk->name, O_RDONLY, OPENDEV_PART | OPENDEV_BLCK, NULL);
467 if (fd < 0) {
468 warn ("read_raid(): opendev(%s)", disk->name);
469 return;
472 memset (&bi, 0, sizeof bi);
474 if (ioctl (fd, BIOCINQ, &bi) == -1)
475 goto ret;
477 for (int i = 0; i < bi.bi_novol; ++i) {
478 struct bioc_vol bv;
480 memset (&bv, 0, sizeof bv);
481 memcpy (&bv.bv_bio, &bi.bi_bio, sizeof bi.bi_bio);
482 bv.bv_volid = i;
484 if (ioctl (fd, BIOCVOL, &bv) == -1) {
485 warn ("read_raid(%s): BIOCVOL(%d)", disk->name, i);
486 continue;
489 if (strcmp (disk->name, bv.bv_dev) != 0)
490 continue;
492 disk->raidstatus = bv_statusstr (bv.bv_status);
494 for (int j = 0; j < bv.bv_nodisk; ++j) {
495 struct bioc_disk bd;
496 size_t len_vendor;
497 char letter;
499 memset (&bd, 0, sizeof bd);
500 memcpy (&bd.bd_bio, &bi.bi_bio, sizeof bi.bi_bio);
501 bd.bd_volid = i;
502 bd.bd_diskid = j;
504 if (ioctl (fd, BIOCDISK, &bd) == -1) {
505 warn ("read_raid(%s): BIOCDISK(%d, %d)", disk->name, i, j);
506 continue;
509 len_vendor = strlen (bd.bd_vendor);
510 if (len_vendor < 4 || len_vendor > 8) {
511 warnx ("read_raid(%s): unexpected vendor string: %.32s", disk->name, bd.bd_vendor);
512 continue;
514 letter = bd.bd_vendor[len_vendor - 1];
516 for (size_t k = 0; k < num_disks; ++k) {
517 if (!memcmp (bd.bd_vendor, disks[k].name, 3)) {
518 for (size_t l = 0; l < disks[k].num_parts; ++l) {
519 if (letter == disks[k].parts[l].letter) {
520 disks[k].parts[l].sub = disk;
521 disks[k].parts[l].raidstatus = bd_statusstr(bd.bd_status);
522 goto found;
527 found:;
530 ret:
531 close (fd);
534 static int usage (void)
536 fputs ("Usage: lsblk [-abinUuV] [disk...]\n", stderr);
537 return 1;
540 static void pad_update (int *pad, int newval)
542 if (newval > *pad)
543 *pad = newval;
546 static void pad_disk (struct padding *p, const struct my_diskinfo *disk)
548 size_t len_disk;
549 int comment;
551 len_disk = strnlen (disk->name, sizeof disk->name);
552 comment = strnlen (disk->label, sizeof disk->label);
553 if (disk->raidstatus)
554 comment += strlen (disk->raidstatus) + 3;
556 pad_update (&p->name, len_disk);
557 pad_update (&p->type, strnlen (disk->type, sizeof disk->type));
558 pad_update (&p->comment, comment);
560 for (int i = 0; i < disk->num_parts; ++i) {
561 const struct my_partinfo *part = &disk->parts[i];
563 pad_update (&p->name, len_disk + 3);
564 pad_update (&p->type, strlen (part->fstype));
566 if (part->sub) {
567 pad_update (&p->name, strnlen (part->sub->name, sizeof part->sub->name) + 4);
568 pad_update (&p->comment, strlen (part->raidstatus));
569 } else if (part->mount) {
570 pad_update (&p->comment, strlen (part->mount));
576 static int compare_disk (const void *p1, const void *p2)
578 const struct my_diskinfo *d1 = p1;
579 const struct my_diskinfo *d2 = p2;
581 return strcmp (d1->name, d2->name);
584 int main (int argc, char *argv[])
586 int option;
587 int fields = FIELD_DEFAULT;
588 int options = 0;
589 int ret = 0;
590 bool ascii = false;
592 if (unveil ("/dev", "r") == -1)
593 err (1, "unveil(/dev)");
595 if (unveil (NULL, NULL) == -1)
596 err (1, "unveil()");
598 while ((option = getopt (argc, argv, ":abinUuV")) != -1) {
599 switch (option) {
600 case 'a':
601 fields = -1;
602 break;
603 case 'b':
604 options |= OPT_NOBIO;
605 break;
606 case 'i':
607 ascii = true;
608 break;
609 case 'n':
610 options |= OPT_NOHEADER;
611 break;
612 case 'U':
613 fields |= FIELD_DUID;
614 break;
615 case 'u':
616 fields |= FIELD_USED | FIELD_FREE;
617 break;
618 case 'V':
619 puts ("lsblk-" VERSION);
620 return 0;
621 default:
622 return usage ();
626 argv += optind;
627 argc -= optind;
629 char *names = argc == 0 ? disknames () : NULL;
631 if (pledge ("stdio rpath disklabel", NULL) == -1)
632 err (1, "pledge()");
634 prefix0 = ascii ? PREFIX0_ASCII : PREFIX0_UNICODE;
635 prefix1 = ascii ? PREFIX1_ASCII : PREFIX1_UNICODE;
636 prefix2 = ascii ? PREFIX2_ASCII : PREFIX2_UNICODE;
638 size_t cap_disks;
639 if (argc == 0) {
640 const char *s = names;
641 cap_disks = 1;
642 while ((s = strchr (s, ',')) != NULL)
643 ++cap_disks, ++s;
644 } else {
645 cap_disks = argc;
648 struct my_diskinfo *disks = calloc (cap_disks, sizeof (struct my_diskinfo));
649 size_t num_disks = 0;
651 if (argc == 0) {
652 for (char *name; (name = strsep (&names, ",")) != NULL; ) {
653 char *colon = strchr (name, ':');
654 struct my_diskinfo disk;
656 if (colon)
657 *colon = '\0';
658 if (read_disk (name, &disk) == 0) {
659 disks[num_disks++] = disk;
660 } else {
661 ret = 1;
664 if (colon)
665 *colon = ':';
667 } else {
668 for (int i = 0; i < argc; ++i) {
669 char *name = basename (argv[i]);
670 char *last = name + strlen (name) - 1;
671 struct my_diskinfo disk;
673 if (isalpha (*last)) {
674 warnx ("%s: specifying a partition is not supported, using the drive.", name);
675 *last = '\0';
677 if (read_disk (name, &disk) == 0) {
678 disks[num_disks++] = disk;
679 } else {
680 ret = 1;
685 free (names);
687 if (num_disks == 0)
688 return ret;
690 mergesort (disks, num_disks, sizeof *disks, compare_disk);
692 if (!(options & OPT_NOBIO)) {
693 for (size_t i = 0; i < num_disks; ++i) {
694 read_raid (&disks[i], disks, num_disks);
698 struct padding p = {
699 .name = strlen ("NAME"),
700 .type = strlen ("TYPE"),
701 .comment = strlen ("COMMENT"),
702 };
704 for (size_t i = 0; i < num_disks; ++i)
705 pad_disk (&p, &disks[i]);
707 if (!(options & OPT_NOHEADER))
708 print_header (fields, &p);
710 for (size_t i = 0; i < num_disks; ++i) {
711 print_disk (&disks[i], fields, options, NULL, &p);
714 return ret;
717 /* vim: set tabstop=4 shiftwidth=4 expandtab: */