diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-02-28 19:39:06 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-02-28 19:39:06 +0300 |
| commit | 3f1e89c0ebb036173f6bb421c77b631a4ad710b4 (patch) | |
| tree | 001aec89afffc5a8f45dab0bec060ff1aa8f2025 | |
| download | Project-Tick-3f1e89c0ebb036173f6bb421c77b631a4ad710b4.tar.gz Project-Tick-3f1e89c0ebb036173f6bb421c77b631a4ad710b4.zip | |
init Standalone musl-libc-based Linux port of FreeBSD `df` for Project Tick BSD/Linux Distribution.
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
| -rw-r--r-- | .gitignore | 25 | ||||
| -rw-r--r-- | GNUmakefile | 35 | ||||
| -rw-r--r-- | Makefile | 8 | ||||
| -rw-r--r-- | Makefile.depend | 17 | ||||
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | df.1 | 298 | ||||
| -rw-r--r-- | df.c | 1349 | ||||
| -rw-r--r-- | tests/test.sh | 179 |
8 files changed, 1937 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000000..cc4425bc3c --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,35 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -D_FILE_OFFSET_BITS=64 +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/df +OBJS := $(OBJDIR)/df.o + +.PHONY: all clean dirs test status + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/df.o: $(CURDIR)/df.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/df.c" -o "$@" + +test: $(TARGET) + DF_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(CURDIR)/build" "$(CURDIR)/out" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..d00b17c6df --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +PACKAGE=runtime + +PROG= df +SRCS= df.c + +LIBADD= xo util + +.include <bsd.prog.mk> diff --git a/Makefile.depend b/Makefile.depend new file mode 100644 index 0000000000..b915f0b12d --- /dev/null +++ b/Makefile.depend @@ -0,0 +1,17 @@ +# Autogenerated - do NOT edit! + +DIRDEPS = \ + include \ + include/xlocale \ + lib/${CSU_DIR} \ + lib/libc \ + lib/libcompiler_rt \ + lib/libutil \ + lib/libxo/libxo \ + + +.include <dirdeps.mk> + +.if ${DEP_RELDIR} == ${_DEP_RELDIR} +# local dependencies - needed for -jN in clean tree +.endif diff --git a/README.md b/README.md new file mode 100644 index 0000000000..94526e2e1d --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# df + +Standalone musl-libc-based Linux port of FreeBSD `df` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Notes + +- Port strategy is Linux-native mount and stat collection, not a FreeBSD ABI shim. +- Mount enumeration uses `/proc/self/mountinfo`; capacity and inode counts use `statvfs(3)` on each selected mountpoint. +- FreeBSD `getmntinfo(3)`, `sysctl(vfs.conflist)`, `getbsize(3)`, `humanize_number(3)`, and `libxo` are removed from the standalone port. +- `-l` is implemented with Linux-oriented remote-filesystem detection based on mount type, `_netdev`, and remote-style sources such as `host:/path` or `//server/share`. +- Linux has no equivalent of FreeBSD's cached `statfs` refresh control, so `-n` fails with an explicit error instead of silently degrading. +- Linux also has no `MNT_IGNORE` mount flag equivalent; as a result `-a` is accepted but has no additional effect in this port. @@ -0,0 +1,298 @@ +.\"- +.\" Copyright (c) 1989, 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" Copyright (c) 2026 +.\" Project Tick. All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd July 16, 2025 +.Dt DF 1 +.Os +.Sh NAME +.Nm df +.Nd display free disk space +.Sh SYNOPSIS +.Nm +.Op Fl b | g | H | h | k | m | P +.Op Fl acilnT +.Op Fl \&, +.Op Fl t Ar type +.Op Ar file | filesystem ... +.Sh DESCRIPTION +The +.Nm +utility +displays statistics about the amount of free disk space on the specified +mounted +.Ar file system +or on the file system of which +.Ar file +is a part. +By default block counts are displayed with an assumed block size of +512 bytes. +If neither a file or a file system operand is specified, +statistics for all mounted file systems are displayed +(subject to the +.Fl t +option below). +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl a +Accepted for command-line compatibility. +Linux does not expose a direct equivalent of FreeBSD's +.Dv MNT_IGNORE +mount flag in +.Pa /proc/self/mountinfo , +so this option has no additional effect in this port. +.It Fl b +Explicitly use 512 byte blocks, overriding any +.Ev BLOCKSIZE +specification from the environment. +This is the same as the +.Fl P +option. +The +.Fl k +option overrides this option. +.It Fl c +Display a grand total. +.It Fl g +Use 1073741824 byte (1 Gibibyte) blocks rather than the default. +This overrides any +.Ev BLOCKSIZE +specification from the environment. +.It Fl h +.Dq Human-readable +output. +Use unit suffixes: Byte, Kibibyte, Mebibyte, Gibibyte, Tebibyte and +Pebibyte (based on powers of 1024) in order to reduce the number of +digits to four or fewer. +.It Fl H , Fl Fl si +Same as +.Fl h +but based on powers of 1000. +.It Fl i +Include statistics on the number of free and used inodes. +In conjunction with the +.Fl h +or +.Fl H +options, the number of inodes is scaled by powers of 1000. +In case the filesystem has no inodes then +.Sq - +is displayed instead of the usage percentage. +.It Fl k +Use 1024 byte (1 Kibibyte) blocks rather than the default. +This overrides the +.Fl P +option and any +.Ev BLOCKSIZE +specification from the environment. +.It Fl l +Select a locally-mounted file system for display. +If used in combination with the +.Fl t Ar type +option, file system types will be added or excluded according to the +parameters of that option. +This Linux port classifies remote file systems by mount type, the +.Dq _netdev +mount option, and remote-style sources such as +.Dq host:/path +or +.Dq //server/share . +.It Fl m +Use 1048576 byte (1 Mebibyte) blocks rather than the default. +This overrides any +.Ev BLOCKSIZE +specification from the environment. +.It Fl n +This flag is recognized but rejected on Linux. +The standalone Linux port reads mount metadata from +.Pa /proc/self/mountinfo +and refreshes filesystem counters with +.Xr statvfs 3 +for each selected mountpoint, and Linux does not provide an equivalent +cached-statistics mode for this utility. +.It Fl P +Explicitly use 512 byte blocks, overriding any +.Ev BLOCKSIZE +specification from the environment. +This is the same as the +.Fl b +option. +The +.Fl k +option overrides this option. +.It Fl t Ar type +Select file systems to display. +More than one type may be specified in a comma separated list. +The list of file system types can be prefixed with +.Dq no +to specify the file system types for which action should +.Em not +be taken. +If used in combination with the +.Fl l +option, the parameters of this option will modify the list of +locally-mounted file systems selected by the +.Fl l +option. +For example, the +.Nm +command: +.Bd -literal -offset indent +df -t nonfs,nullfs +.Ed +.Pp +lists all file systems except those of type NFS and NULLFS. +On Linux, mounted filesystem types can be inspected via +.Pa /proc/self/mountinfo . +.It Fl T +Include file system type. +.It Fl , +(Comma) Print sizes grouped and separated by thousands using the +non-monetary separator returned by +.Xr localeconv 3 , +typically a comma or period. +If no locale is set, or the locale does not have a non-monetary separator, this +option has no effect. +.El +.Sh ENVIRONMENT +.Bl -tag -width BLOCKSIZE +.It Ev BLOCKSIZE +Specifies the units in which to report block counts. +This Linux port accepts units of bytes or numbers scaled with the letters +.Em k +(for multiples of 1024 bytes), +.Em m +(for multiples of 1048576 bytes) or +.Em g +(for gibibytes). +The allowed range is 512 bytes to 1 GB. +If the value is outside, it will be set to the appropriate limit. +.El +.Sh EXAMPLES +Show human readable free disk space for all mount points including file system +type: +.Bd -literal -offset indent +$ df -ahT +Filesystem Type Size Used Avail Capacity Mounted on +/dev/ada1p2 ufs 213G 152G 44G 78% / +devfs devfs 1.0K 1.0K 0B 100% /dev +/dev/ada0p1 ufs 1.8T 168G 1.5T 10% /data +linsysfs linsysfs 4.0K 4.0K 0B 100% /compat/linux/sys +/dev/da0 msdosfs 7.6G 424M 7.2G 5% /mnt/usb +.Ed +.Pp +Show inode statistics except for devfs or linsysfs file systems. +Note that the +.Dq no +prefix affects all the file systems in the list and the +.Fl t +option can be specified only once: +.Bd -literal -offset indent +$ df -i -t nodevfs,linsysfs +Filesystem 1K-blocks Used Avail Capacity iused ifree %iused +Mounted on +/dev/ada1p2 223235736 159618992 45757888 78% 1657590 27234568 6% / +/dev/ada0p1 1892163184 176319420 1564470712 10% 1319710 243300576 1% +/data +/dev/da0 7989888 433664 7556224 5% 0 0 100% +/mnt/usb +.Ed +.Pp +Show human readable information for the file system containing the file +.Pa /etc/rc.conf : +.Bd -literal -offset indent +$ df -h /etc/rc.conf +Filesystem Size Used Avail Capacity Mounted on +/dev/ada1p2 213G 152G 44G 78% / +.Ed +.Pp +Same as above but specifying some file system: +.Bd -literal -offset indent +$ df -h /dev/ada1p2 +Filesystem Size Used Avail Capacity Mounted on +/dev/ada1p2 213G 152G 44G 78% / +.Ed +.Sh NOTES +For non-Unix file systems, the reported values of used and free inodes +may have a different meaning than that of used and available files and +directories. +An example is msdosfs, which in the case of FAT12 or FAT16 file systems +reports the number of available and free root directory entries instead +of inodes +.Po +where 1 to 21 such directory entries are required to store +each file or directory name or disk label +.Pc . +.Sh SEE ALSO +.Xr lsvfs 1 , +.Xr quota 1 , +.Xr fstatfs 2 , +.Xr statfs 2 , +.Xr statvfs 3 , +.Xr localeconv 3 , +.Xr fstab 5 , +.Xr mount 8 , +.Xr pstat 8 , +.Xr quot 8 , +.Xr swapinfo 8 +.Sh STANDARDS +With the exception of most options, +the +.Nm +utility conforms to +.St -p1003.1-2004 , +which defines only the +.Fl k , P +and +.Fl t +options. +.Sh HISTORY +A +.Nm +command appeared in +.At v1 . +.Sh BUGS +The +.Fl n +flag is ignored if a file or file system is specified. +Also, if a mount +point is not accessible by the user, it is possible that the file system +information could be stale. +.Pp +The +.Fl b +and +.Fl P +options are identical. +The former comes from the BSD tradition, and the latter is required +for +.St -p1003.1-2004 +conformity. @@ -0,0 +1,1349 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1980, 1990, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * (c) UNIX System Laboratories, Inc. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * All or some portions of this file are derived from material licensed + * to the University of California by American Telephone and Telegraph + * Co. or Unix System Laboratories, Inc. and are reproduced herein with + * the permission of UNIX System Laboratories, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#define _POSIX_C_SOURCE 200809L +#define _XOPEN_SOURCE 700 +#define _FILE_OFFSET_BITS 64 + +#include <ctype.h> +#include <errno.h> +#include <getopt.h> +#include <limits.h> +#include <locale.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/sysmacros.h> +#include <sysexits.h> +#include <unistd.h> + +#define MOUNTINFO_PATH "/proc/self/mountinfo" +#define DEFAULT_BLOCK_SIZE 512ULL +#define MAX_BLOCK_SIZE (1ULL << 30) + +enum display_mode { + DISPLAY_BLOCKS = 0, + DISPLAY_HUMAN_1024, + DISPLAY_HUMAN_1000, +}; + +struct string_list { + char **items; + size_t len; + bool invert; + bool present; +}; + +struct options { + bool aflag; + bool cflag; + bool iflag; + bool kflag_seen; + bool lflag; + bool thousands; + bool Tflag; + enum display_mode mode; + bool block_size_override; + uint64_t block_size; + struct string_list type_filter; +}; + +struct mount_entry { + char *fstype; + char *options; + char *source; + char *target; + dev_t dev; + bool is_remote; +}; + +struct mount_table { + struct mount_entry *entries; + size_t len; + size_t cap; +}; + +struct row { + const struct mount_entry *mount; + bool is_total; + uint64_t total_bytes; + uint64_t used_bytes; + uint64_t avail_bytes; + uint64_t total_inodes; + uint64_t free_inodes; +}; + +struct row_list { + struct row *items; + size_t len; + size_t cap; +}; + +struct column_widths { + int avail; + int capacity; + int fstype; + int ifree; + int iused; + int mount_source; + int size; + int used; +}; + +static void append_mount(struct mount_table *table, struct mount_entry entry); +static void append_row(struct row_list *rows, struct row row); +static bool contains_token(const char *csv, const char *token); +static uint64_t divide_saturated(uint64_t value, uint64_t divisor); +static void format_block_header(char *buf, size_t buflen, uint64_t block_size); +static void format_human(char *buf, size_t buflen, uint64_t value, bool si_units, + bool bytes); +static void format_integer(char *buf, size_t buflen, uint64_t value, + const char *separator); +static void free_mount_table(struct mount_table *table); +static void free_row_list(struct row_list *rows); +static void free_string_list(struct string_list *list); +static const struct mount_entry *find_mount_for_path(const struct mount_table *table, + const char *path); +static const struct mount_entry *find_mount_for_source(const struct mount_table *table, + const char *source); +static int load_mounts(struct mount_table *table); +static int parse_block_size_env(void); +static int parse_mount_line(char *line, struct mount_entry *entry); +static int parse_options(int argc, char *argv[], struct options *options); +static int parse_type_filter(const char *arg, struct string_list *list); +static bool path_is_within_mount(const char *path, const char *mountpoint); +static int populate_all_rows(const struct options *options, + const struct mount_table *table, struct row_list *rows); +static int populate_operand_rows(const struct options *options, + const struct mount_table *table, char *const *operands, int operand_count, + struct row_list *rows); +static void print_rows(const struct options *options, const struct row_list *rows); +static int resolve_operand(const struct mount_table *table, const char *operand, + const struct mount_entry **mount, char **canonical_path); +static bool row_selected(const struct options *options, + const struct mount_entry *mount); +static int row_statvfs(const struct mount_entry *mount, struct row *row); +static int scan_mountinfo(FILE *stream, struct mount_table *table); +static void set_default_options(struct options *options); +static char *strdup_or_die(const char *value); +static bool string_list_contains(const struct string_list *list, const char *value); +static bool is_remote_mount(const char *fstype, const char *source, + const char *options); +static char *unescape_mount_field(const char *value); +static void usage(void); +static void update_widths(const struct options *options, struct column_widths *widths, + const struct row *row); +static uint64_t clamp_product(uint64_t lhs, uint64_t rhs); +static uint64_t clamp_sum(uint64_t lhs, uint64_t rhs); +static uint64_t row_fragment_size(const struct statvfs *stats); +static char *split_next_field(char **cursor, char delimiter); + +static void +append_mount(struct mount_table *table, struct mount_entry entry) +{ + struct mount_entry *new_entries; + size_t new_cap; + + if (table->len < table->cap) { + table->entries[table->len++] = entry; + return; + } + + new_cap = table->cap == 0 ? 32 : table->cap * 2; + if (new_cap < table->cap) { + fprintf(stderr, "df: mount table too large\n"); + exit(EX_OSERR); + } + new_entries = realloc(table->entries, new_cap * sizeof(*table->entries)); + if (new_entries == NULL) { + fprintf(stderr, "df: realloc failed\n"); + exit(EX_OSERR); + } + table->entries = new_entries; + table->cap = new_cap; + table->entries[table->len++] = entry; +} + +static void +append_row(struct row_list *rows, struct row row) +{ + struct row *new_items; + size_t new_cap; + + if (rows->len < rows->cap) { + rows->items[rows->len++] = row; + return; + } + + new_cap = rows->cap == 0 ? 16 : rows->cap * 2; + if (new_cap < rows->cap) { + fprintf(stderr, "df: row table too large\n"); + exit(EX_OSERR); + } + new_items = realloc(rows->items, new_cap * sizeof(*rows->items)); + if (new_items == NULL) { + fprintf(stderr, "df: realloc failed\n"); + exit(EX_OSERR); + } + rows->items = new_items; + rows->cap = new_cap; + rows->items[rows->len++] = row; +} + +static bool +contains_token(const char *csv, const char *token) +{ + const char *cursor; + size_t token_len; + + if (csv == NULL || token == NULL) + return false; + + token_len = strlen(token); + for (cursor = csv; *cursor != '\0'; ) { + const char *end; + size_t len; + + end = strchr(cursor, ','); + if (end == NULL) + end = cursor + strlen(cursor); + len = (size_t)(end - cursor); + if (len == token_len && strncmp(cursor, token, len) == 0) + return true; + if (*end == '\0') + break; + cursor = end + 1; + } + + return false; +} + +static uint64_t +divide_saturated(uint64_t value, uint64_t divisor) +{ + if (divisor == 0) + return 0; + return value / divisor; +} + +static void +format_block_header(char *buf, size_t buflen, uint64_t block_size) +{ + const char *suffix; + uint64_t units; + + if (block_size == DEFAULT_BLOCK_SIZE) { + snprintf(buf, buflen, "512-blocks"); + return; + } + + if (block_size % (1ULL << 30) == 0) { + suffix = "G"; + units = block_size / (1ULL << 30); + } else if (block_size % (1ULL << 20) == 0) { + suffix = "M"; + units = block_size / (1ULL << 20); + } else if (block_size % 1024ULL == 0) { + suffix = "K"; + units = block_size / 1024ULL; + } else { + suffix = ""; + units = block_size; + } + + snprintf(buf, buflen, "%ju%s-blocks", (uintmax_t)units, suffix); +} + +static void +format_human(char *buf, size_t buflen, uint64_t value, bool si_units, bool bytes) +{ + static const char *const byte_units[] = { "B", "K", "M", "G", "T", "P", "E" }; + static const char *const count_units[] = { "", "K", "M", "G", "T", "P", "E" }; + const char *const *units; + double scaled; + unsigned base; + size_t unit_index; + + base = si_units ? 1000U : 1024U; + units = bytes ? byte_units : count_units; + scaled = (double)value; + unit_index = 0; + + while (scaled >= (double)base && unit_index < 6) { + scaled /= (double)base; + unit_index++; + } + + if (unit_index == 0) { + snprintf(buf, buflen, "%ju%s", (uintmax_t)value, units[unit_index]); + return; + } + + if (scaled < 10.0) + snprintf(buf, buflen, "%.1f%s", scaled, units[unit_index]); + else + snprintf(buf, buflen, "%.0f%s", scaled, units[unit_index]); +} + +static void +format_integer(char *buf, size_t buflen, uint64_t value, const char *separator) +{ + char digits[64]; + char reversed[96]; + size_t digit_count; + size_t out_len; + size_t i; + size_t sep_len; + + sep_len = separator == NULL ? 0 : strlen(separator); + snprintf(digits, sizeof(digits), "%ju", (uintmax_t)value); + if (sep_len == 0) { + snprintf(buf, buflen, "%s", digits); + return; + } + + digit_count = strlen(digits); + out_len = 0; + for (i = 0; i < digit_count; i++) { + if (i != 0 && i % 3 == 0) { + size_t j; + + for (j = 0; j < sep_len; j++) { + if (out_len + 1 >= sizeof(reversed)) + break; + reversed[out_len++] = separator[sep_len - j - 1]; + } + } + if (out_len + 1 >= sizeof(reversed)) + break; + reversed[out_len++] = digits[digit_count - i - 1]; + } + for (i = 0; i < out_len && i + 1 < buflen; i++) + buf[i] = reversed[out_len - i - 1]; + buf[i] = '\0'; +} + +static void +free_mount_table(struct mount_table *table) +{ + size_t i; + + for (i = 0; i < table->len; i++) { + free(table->entries[i].fstype); + free(table->entries[i].options); + free(table->entries[i].source); + free(table->entries[i].target); + } + free(table->entries); + table->entries = NULL; + table->len = 0; + table->cap = 0; +} + +static void +free_row_list(struct row_list *rows) +{ + free(rows->items); + rows->items = NULL; + rows->len = 0; + rows->cap = 0; +} + +static void +free_string_list(struct string_list *list) +{ + size_t i; + + for (i = 0; i < list->len; i++) + free(list->items[i]); + free(list->items); + list->items = NULL; + list->len = 0; + list->invert = false; + list->present = false; +} + +static const struct mount_entry * +find_mount_for_path(const struct mount_table *table, const char *path) +{ + const struct mount_entry *best; + size_t best_len; + size_t i; + + best = NULL; + best_len = 0; + for (i = 0; i < table->len; i++) { + size_t mount_len; + + if (!path_is_within_mount(path, table->entries[i].target)) + continue; + mount_len = strlen(table->entries[i].target); + if (mount_len >= best_len) { + best = &table->entries[i]; + best_len = mount_len; + } + } + + return best; +} + +static const struct mount_entry * +find_mount_for_source(const struct mount_table *table, const char *source) +{ + size_t i; + + for (i = 0; i < table->len; i++) { + if (strcmp(table->entries[i].source, source) == 0) + return &table->entries[i]; + } + + return NULL; +} + +static int +load_mounts(struct mount_table *table) +{ + FILE *stream; + int rc; + + stream = fopen(MOUNTINFO_PATH, "re"); + if (stream == NULL) { + fprintf(stderr, "df: %s: %s\n", MOUNTINFO_PATH, strerror(errno)); + return -1; + } + + rc = scan_mountinfo(stream, table); + fclose(stream); + return rc; +} + +static int +parse_block_size_env(void) +{ + char *endptr; + const char *value; + uint64_t multiplier; + uint64_t numeric; + unsigned long long parsed; + + value = getenv("BLOCKSIZE"); + if (value == NULL || *value == '\0') + return (int)DEFAULT_BLOCK_SIZE; + + errno = 0; + parsed = strtoull(value, &endptr, 10); + if (errno != 0 || endptr == value) + return (int)DEFAULT_BLOCK_SIZE; + + multiplier = 1; + if (*endptr != '\0') { + if (endptr[1] != '\0') + return (int)DEFAULT_BLOCK_SIZE; + switch (*endptr) { + case 'k': + case 'K': + multiplier = 1024ULL; + break; + case 'm': + case 'M': + multiplier = 1ULL << 20; + break; + case 'g': + case 'G': + multiplier = 1ULL << 30; + break; + default: + return (int)DEFAULT_BLOCK_SIZE; + } + } + + numeric = (uint64_t)parsed; + if (numeric != 0 && multiplier > UINT64_MAX / numeric) + numeric = MAX_BLOCK_SIZE; + else + numeric *= multiplier; + + if (numeric < DEFAULT_BLOCK_SIZE) + numeric = DEFAULT_BLOCK_SIZE; + if (numeric > MAX_BLOCK_SIZE) + numeric = MAX_BLOCK_SIZE; + + return (int)numeric; +} + +static int +parse_mount_line(char *line, struct mount_entry *entry) +{ + char *cursor; + char *dash; + char *fields[6]; + char *right_fields[3]; + char *mount_options; + char *super_options; + char *combined; + unsigned long major_id; + unsigned long minor_id; + size_t i; + + memset(entry, 0, sizeof(*entry)); + dash = strstr(line, " - "); + if (dash == NULL) + return -1; + *dash = '\0'; + cursor = line; + for (i = 0; i < 6; i++) { + fields[i] = split_next_field(&cursor, ' '); + if (fields[i] == NULL) + return -1; + } + cursor = dash + 3; + for (i = 0; i < 3; i++) { + right_fields[i] = split_next_field(&cursor, ' '); + if (right_fields[i] == NULL) + return -1; + } + + if (sscanf(fields[2], "%lu:%lu", &major_id, &minor_id) != 2) + return -1; + + entry->target = unescape_mount_field(fields[4]); + entry->fstype = strdup_or_die(right_fields[0]); + entry->source = unescape_mount_field(right_fields[1]); + mount_options = fields[5]; + super_options = right_fields[2]; + combined = malloc(strlen(mount_options) + strlen(super_options) + 2); + if (combined == NULL) { + fprintf(stderr, "df: malloc failed\n"); + exit(EX_OSERR); + } + snprintf(combined, strlen(mount_options) + strlen(super_options) + 2, + "%s,%s", mount_options, super_options); + entry->options = combined; + entry->dev = makedev(major_id, minor_id); + entry->is_remote = is_remote_mount(entry->fstype, entry->source, + entry->options); + return 0; +} + +static int +parse_options(int argc, char *argv[], struct options *options) +{ + static const struct option long_options[] = { + { "si", no_argument, NULL, 'H' }, + { NULL, 0, NULL, 0 }, + }; + int ch; + + while ((ch = getopt_long(argc, argv, "+abcgHhiklmnPt:T,", long_options, + NULL)) != -1) { + switch (ch) { + case 'a': + options->aflag = true; + break; + case 'b': + case 'P': + if (!options->kflag_seen) { + options->mode = DISPLAY_BLOCKS; + options->block_size_override = true; + options->block_size = DEFAULT_BLOCK_SIZE; + } + break; + case 'c': + options->cflag = true; + break; + case 'g': + options->mode = DISPLAY_BLOCKS; + options->block_size_override = true; + options->block_size = 1ULL << 30; + break; + case 'H': + options->mode = DISPLAY_HUMAN_1000; + options->block_size_override = false; + break; + case 'h': + options->mode = DISPLAY_HUMAN_1024; + options->block_size_override = false; + break; + case 'i': + options->iflag = true; + break; + case 'k': + options->kflag_seen = true; + options->mode = DISPLAY_BLOCKS; + options->block_size_override = true; + options->block_size = 1024; + break; + case 'l': + options->lflag = true; + break; + case 'm': + options->mode = DISPLAY_BLOCKS; + options->block_size_override = true; + options->block_size = 1ULL << 20; + break; + case 'n': + fprintf(stderr, "df: option -n is not supported on Linux\n"); + return -1; + case 't': + if (options->type_filter.present) { + fprintf(stderr, "df: only one -t option may be specified\n"); + return -1; + } + if (parse_type_filter(optarg, &options->type_filter) != 0) + return -1; + break; + case 'T': + options->Tflag = true; + break; + case ',': + options->thousands = true; + break; + default: + usage(); + } + } + + if (options->mode == DISPLAY_BLOCKS && !options->block_size_override) + options->block_size = (uint64_t)parse_block_size_env(); + + return 0; +} + +static int +parse_type_filter(const char *arg, struct string_list *list) +{ + char *copy; + char *token; + char *cursor; + + if (arg == NULL) { + fprintf(stderr, "df: missing filesystem type list\n"); + return -1; + } + + copy = strdup_or_die(arg); + cursor = copy; + list->invert = false; + if (strncmp(cursor, "no", 2) == 0) { + list->invert = true; + cursor += 2; + } + if (*cursor == '\0') { + fprintf(stderr, "df: empty filesystem type list: %s\n", arg); + free(copy); + return -1; + } + + while ((token = split_next_field(&cursor, ',')) != NULL) { + char **items; + + if (*token == '\0') { + fprintf(stderr, "df: empty filesystem type in list: %s\n", arg); + free(copy); + return -1; + } + items = realloc(list->items, (list->len + 1) * sizeof(*list->items)); + if (items == NULL) { + fprintf(stderr, "df: realloc failed\n"); + free(copy); + exit(EX_OSERR); + } + list->items = items; + list->items[list->len++] = strdup_or_die(token); + } + free(copy); + list->present = true; + return 0; +} + +static bool +path_is_within_mount(const char *path, const char *mountpoint) +{ + size_t mount_len; + + if (strcmp(mountpoint, "/") == 0) + return path[0] == '/'; + + mount_len = strlen(mountpoint); + if (strncmp(path, mountpoint, mount_len) != 0) + return false; + if (path[mount_len] == '\0' || path[mount_len] == '/') + return true; + return false; +} + +static int +populate_all_rows(const struct options *options, const struct mount_table *table, + struct row_list *rows) +{ + int rv; + size_t i; + + rv = 0; + for (i = 0; i < table->len; i++) { + struct row row; + + if (!row_selected(options, &table->entries[i])) + continue; + if (row_statvfs(&table->entries[i], &row) != 0) { + fprintf(stderr, "df: %s: %s\n", table->entries[i].target, + strerror(errno)); + rv = 1; + continue; + } + append_row(rows, row); + } + + return rv; +} + +static int +populate_operand_rows(const struct options *options, const struct mount_table *table, + char *const *operands, int operand_count, struct row_list *rows) +{ + int rv; + int i; + + rv = 0; + for (i = 0; i < operand_count; i++) { + char *canonical_path; + const struct mount_entry *mount; + struct row row; + + canonical_path = NULL; + if (resolve_operand(table, operands[i], &mount, &canonical_path) != 0) { + rv = 1; + free(canonical_path); + continue; + } + if (!row_selected(options, mount)) { + fprintf(stderr, + "df: %s: filtered out by current filesystem selection\n", + operands[i]); + rv = 1; + free(canonical_path); + continue; + } + if (row_statvfs(mount, &row) != 0) { + fprintf(stderr, "df: %s: %s\n", mount->target, strerror(errno)); + rv = 1; + free(canonical_path); + continue; + } + append_row(rows, row); + free(canonical_path); + } + + return rv; +} + +static void +print_rows(const struct options *options, const struct row_list *rows) +{ + struct column_widths widths; + char header[32]; + char size_buf[64]; + char used_buf[64]; + char avail_buf[64]; + char iused_buf[64]; + char ifree_buf[64]; + char inode_pct[16]; + char percent_buf[16]; + const char *separator; + size_t i; + + if (rows->len == 0) + return; + + memset(&widths, 0, sizeof(widths)); + for (i = 0; i < rows->len; i++) + update_widths(options, &widths, &rows->items[i]); + + if (options->mode == DISPLAY_BLOCKS) + format_block_header(header, sizeof(header), options->block_size); + else + snprintf(header, sizeof(header), "Size"); + if (widths.mount_source < (int)strlen("Filesystem")) + widths.mount_source = (int)strlen("Filesystem"); + if (options->Tflag && widths.fstype < (int)strlen("Type")) + widths.fstype = (int)strlen("Type"); + if (widths.size < (int)strlen(header)) + widths.size = (int)strlen(header); + if (widths.used < (int)strlen("Used")) + widths.used = (int)strlen("Used"); + if (widths.avail < (int)strlen("Avail")) + widths.avail = (int)strlen("Avail"); + if (widths.capacity < (int)strlen("Capacity")) + widths.capacity = (int)strlen("Capacity"); + if (options->iflag) { + if (widths.iused < (int)strlen("iused")) + widths.iused = (int)strlen("iused"); + if (widths.ifree < (int)strlen("ifree")) + widths.ifree = (int)strlen("ifree"); + } + + printf("%-*s", widths.mount_source, "Filesystem"); + if (options->Tflag) + printf(" %-*s", widths.fstype, "Type"); + printf(" %*s %*s %*s %*s", widths.size, header, widths.used, "Used", + widths.avail, "Avail", widths.capacity, "Capacity"); + if (options->iflag) + printf(" %*s %*s %6s", widths.iused, "iused", widths.ifree, "ifree", + "%iused"); + printf(" Mounted on\n"); + + separator = options->thousands ? localeconv()->thousands_sep : ""; + if (separator == NULL) + separator = ""; + + for (i = 0; i < rows->len; i++) { + uint64_t used_inodes; + const char *mount_source; + const char *mount_target; + const char *fstype; + double percent; + + mount_source = rows->items[i].is_total ? "total" : rows->items[i].mount->source; + mount_target = rows->items[i].is_total ? "" : rows->items[i].mount->target; + fstype = rows->items[i].is_total ? "" : rows->items[i].mount->fstype; + + if (options->mode == DISPLAY_BLOCKS) { + format_integer(size_buf, sizeof(size_buf), + divide_saturated(rows->items[i].total_bytes, options->block_size), + separator); + format_integer(used_buf, sizeof(used_buf), + divide_saturated(rows->items[i].used_bytes, options->block_size), + separator); + format_integer(avail_buf, sizeof(avail_buf), + divide_saturated(rows->items[i].avail_bytes, options->block_size), + separator); + } else { + format_human(size_buf, sizeof(size_buf), rows->items[i].total_bytes, + options->mode == DISPLAY_HUMAN_1000, true); + format_human(used_buf, sizeof(used_buf), rows->items[i].used_bytes, + options->mode == DISPLAY_HUMAN_1000, true); + format_human(avail_buf, sizeof(avail_buf), rows->items[i].avail_bytes, + options->mode == DISPLAY_HUMAN_1000, true); + } + + if (rows->items[i].used_bytes + rows->items[i].avail_bytes == 0) { + snprintf(percent_buf, sizeof(percent_buf), "100%%"); + } else { + percent = ((double)rows->items[i].used_bytes * 100.0) / + (double)(rows->items[i].used_bytes + rows->items[i].avail_bytes); + snprintf(percent_buf, sizeof(percent_buf), "%.0f%%", percent); + } + + printf("%-*s", widths.mount_source, mount_source); + if (options->Tflag) + printf(" %-*s", widths.fstype, fstype); + printf(" %*s %*s %*s %*s", widths.size, size_buf, widths.used, used_buf, + widths.avail, avail_buf, widths.capacity, percent_buf); + + if (options->iflag) { + used_inodes = rows->items[i].total_inodes >= rows->items[i].free_inodes ? + rows->items[i].total_inodes - rows->items[i].free_inodes : 0; + if (options->mode == DISPLAY_BLOCKS) { + format_integer(iused_buf, sizeof(iused_buf), used_inodes, + separator); + format_integer(ifree_buf, sizeof(ifree_buf), + rows->items[i].free_inodes, separator); + } else { + format_human(iused_buf, sizeof(iused_buf), used_inodes, true, + false); + format_human(ifree_buf, sizeof(ifree_buf), + rows->items[i].free_inodes, true, false); + } + if (rows->items[i].total_inodes == 0) + snprintf(inode_pct, sizeof(inode_pct), "-"); + else + snprintf(inode_pct, sizeof(inode_pct), "%.0f%%", + ((double)used_inodes * 100.0) / + (double)rows->items[i].total_inodes); + printf(" %*s %*s %6s", widths.iused, iused_buf, widths.ifree, + ifree_buf, inode_pct); + } + + if (mount_target[0] != '\0') + printf(" %s", mount_target); + printf("\n"); + } +} + +static int +resolve_operand(const struct mount_table *table, const char *operand, + const struct mount_entry **mount, char **canonical_path) +{ + struct stat st; + char *resolved; + + if (stat(operand, &st) != 0) { + *mount = find_mount_for_source(table, operand); + if (*mount != NULL) + return 0; + fprintf(stderr, "df: %s: %s\n", operand, strerror(errno)); + return -1; + } + + if (S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode)) { + *mount = find_mount_for_source(table, operand); + if (*mount == NULL) { + fprintf(stderr, "df: %s: not mounted\n", operand); + return -1; + } + return 0; + } + + resolved = realpath(operand, NULL); + if (resolved == NULL) { + fprintf(stderr, "df: %s: %s\n", operand, strerror(errno)); + return -1; + } + + *mount = find_mount_for_path(table, resolved); + if (*mount == NULL) { + fprintf(stderr, "df: %s: no mount entry found\n", operand); + free(resolved); + return -1; + } + + *canonical_path = resolved; + return 0; +} + +static bool +row_selected(const struct options *options, const struct mount_entry *mount) +{ + bool type_match; + + if (!options->type_filter.present) + return !options->lflag || !mount->is_remote; + + type_match = string_list_contains(&options->type_filter, mount->fstype); + if (options->type_filter.invert) + return (!options->lflag || !mount->is_remote) && !type_match; + if (options->lflag) + return !mount->is_remote || type_match; + return type_match; +} + +static int +row_statvfs(const struct mount_entry *mount, struct row *row) +{ + struct statvfs stats; + uint64_t fragment_size; + uint64_t blocks; + uint64_t bfree; + uint64_t bavail; + + if (statvfs(mount->target, &stats) != 0) + return -1; + + fragment_size = row_fragment_size(&stats); + blocks = (uint64_t)stats.f_blocks; + bfree = (uint64_t)stats.f_bfree; + bavail = (uint64_t)stats.f_bavail; + if (blocks < bfree) + bfree = blocks; + + row->mount = mount; + row->is_total = false; + row->total_bytes = clamp_product(blocks, fragment_size); + row->used_bytes = clamp_product(blocks - bfree, fragment_size); + row->avail_bytes = clamp_product(bavail, fragment_size); + row->total_inodes = (uint64_t)stats.f_files; + row->free_inodes = (uint64_t)stats.f_ffree; + return 0; +} + +static int +scan_mountinfo(FILE *stream, struct mount_table *table) +{ + char *line; + size_t linecap; + ssize_t linelen; + + line = NULL; + linecap = 0; + while ((linelen = getline(&line, &linecap, stream)) != -1) { + struct mount_entry entry; + + if (linelen > 0 && line[linelen - 1] == '\n') + line[linelen - 1] = '\0'; + if (parse_mount_line(line, &entry) != 0) { + fprintf(stderr, "df: could not parse %s\n", MOUNTINFO_PATH); + free(line); + return -1; + } + append_mount(table, entry); + } + free(line); + if (ferror(stream)) { + fprintf(stderr, "df: %s: %s\n", MOUNTINFO_PATH, strerror(errno)); + return -1; + } + return 0; +} + +static void +set_default_options(struct options *options) +{ + memset(options, 0, sizeof(*options)); + options->mode = DISPLAY_BLOCKS; + options->block_size = DEFAULT_BLOCK_SIZE; +} + +static char * +strdup_or_die(const char *value) +{ + char *copy; + + copy = strdup(value); + if (copy == NULL) { + fprintf(stderr, "df: strdup failed\n"); + exit(EX_OSERR); + } + return copy; +} + +static bool +string_list_contains(const struct string_list *list, const char *value) +{ + size_t i; + + for (i = 0; i < list->len; i++) { + if (strcmp(list->items[i], value) == 0) + return true; + } + + return false; +} + +static bool +is_remote_mount(const char *fstype, const char *source, const char *options) +{ + static const char *const remote_types[] = { + "9p", + "acfs", + "afs", + "ceph", + "cifs", + "coda", + "davfs", + "fuse.glusterfs", + "fuse.sshfs", + "gfs", + "gfs2", + "glusterfs", + "lustre", + "ncp", + "ncpfs", + "nfs", + "nfs4", + "ocfs2", + "smb3", + "smbfs", + "sshfs", + }; + size_t i; + + if (contains_token(options, "_netdev")) + return true; + for (i = 0; i < sizeof(remote_types) / sizeof(remote_types[0]); i++) { + if (strcmp(fstype, remote_types[i]) == 0) + return true; + } + if (strncmp(source, "//", 2) == 0) + return true; + if (source[0] != '/' && strstr(source, ":/") != NULL) + return true; + return false; +} + +static char * +unescape_mount_field(const char *value) +{ + char *out; + size_t in_len; + size_t i; + size_t j; + + in_len = strlen(value); + out = malloc(in_len + 1); + if (out == NULL) { + fprintf(stderr, "df: malloc failed\n"); + exit(EX_OSERR); + } + + for (i = 0, j = 0; i < in_len; i++) { + if (value[i] == '\\' && i + 3 < in_len && + isdigit((unsigned char)value[i + 1]) && + isdigit((unsigned char)value[i + 2]) && + isdigit((unsigned char)value[i + 3])) { + unsigned digit; + + digit = (unsigned)(value[i + 1] - '0') * 64U + + (unsigned)(value[i + 2] - '0') * 8U + + (unsigned)(value[i + 3] - '0'); + out[j++] = (char)digit; + i += 3; + continue; + } + out[j++] = value[i]; + } + out[j] = '\0'; + return out; +} + +static void +usage(void) +{ + fprintf(stderr, + "usage: df [-b | -g | -H | -h | -k | -m | -P] [-acilT] [-t type] [-,]\n" + " [file | filesystem ...]\n"); + exit(EX_USAGE); +} + +static void +update_widths(const struct options *options, struct column_widths *widths, + const struct row *row) +{ + char buf[64]; + char human_buf[32]; + uint64_t used_inodes; + int len; + + len = (int)strlen(row->is_total ? "total" : row->mount->source); + if (len > widths->mount_source) + widths->mount_source = len; + if (options->Tflag) { + len = row->is_total ? 0 : (int)strlen(row->mount->fstype); + if (len > widths->fstype) + widths->fstype = len; + } + + if (options->mode == DISPLAY_BLOCKS) { + format_integer(buf, sizeof(buf), + divide_saturated(row->total_bytes, options->block_size), + options->thousands ? localeconv()->thousands_sep : ""); + len = (int)strlen(buf); + if (len > widths->size) + widths->size = len; + format_integer(buf, sizeof(buf), + divide_saturated(row->used_bytes, options->block_size), + options->thousands ? localeconv()->thousands_sep : ""); + len = (int)strlen(buf); + if (len > widths->used) + widths->used = len; + format_integer(buf, sizeof(buf), + divide_saturated(row->avail_bytes, options->block_size), + options->thousands ? localeconv()->thousands_sep : ""); + len = (int)strlen(buf); + if (len > widths->avail) + widths->avail = len; + } else { + format_human(human_buf, sizeof(human_buf), row->total_bytes, + options->mode == DISPLAY_HUMAN_1000, true); + len = (int)strlen(human_buf); + if (len > widths->size) + widths->size = len; + format_human(human_buf, sizeof(human_buf), row->used_bytes, + options->mode == DISPLAY_HUMAN_1000, true); + len = (int)strlen(human_buf); + if (len > widths->used) + widths->used = len; + format_human(human_buf, sizeof(human_buf), row->avail_bytes, + options->mode == DISPLAY_HUMAN_1000, true); + len = (int)strlen(human_buf); + if (len > widths->avail) + widths->avail = len; + } + + if (row->used_bytes + row->avail_bytes == 0) + len = (int)strlen("100%"); + else + len = 4; + if (len > widths->capacity) + widths->capacity = len; + + if (!options->iflag) + return; + + used_inodes = row->total_inodes >= row->free_inodes ? + row->total_inodes - row->free_inodes : 0; + if (options->mode == DISPLAY_BLOCKS) { + format_integer(buf, sizeof(buf), used_inodes, + options->thousands ? localeconv()->thousands_sep : ""); + len = (int)strlen(buf); + if (len > widths->iused) + widths->iused = len; + format_integer(buf, sizeof(buf), row->free_inodes, + options->thousands ? localeconv()->thousands_sep : ""); + len = (int)strlen(buf); + if (len > widths->ifree) + widths->ifree = len; + } else { + format_human(human_buf, sizeof(human_buf), used_inodes, true, false); + len = (int)strlen(human_buf); + if (len > widths->iused) + widths->iused = len; + format_human(human_buf, sizeof(human_buf), row->free_inodes, true, + false); + len = (int)strlen(human_buf); + if (len > widths->ifree) + widths->ifree = len; + } +} + +static uint64_t +clamp_product(uint64_t lhs, uint64_t rhs) +{ + if (lhs == 0 || rhs == 0) + return 0; + if (lhs > UINT64_MAX / rhs) + return UINT64_MAX; + return lhs * rhs; +} + +static uint64_t +clamp_sum(uint64_t lhs, uint64_t rhs) +{ + if (UINT64_MAX - lhs < rhs) + return UINT64_MAX; + return lhs + rhs; +} + +static uint64_t +row_fragment_size(const struct statvfs *stats) +{ + if (stats->f_frsize != 0) + return (uint64_t)stats->f_frsize; + if (stats->f_bsize != 0) + return (uint64_t)stats->f_bsize; + return DEFAULT_BLOCK_SIZE; +} + +static char * +split_next_field(char **cursor, char delimiter) +{ + char *field; + char *end; + + if (cursor == NULL || *cursor == NULL) + return NULL; + + while (**cursor == delimiter) + (*cursor)++; + if (**cursor == '\0') + return NULL; + + field = *cursor; + end = strchr(field, delimiter); + if (end == NULL) { + *cursor = field + strlen(field); + return field; + } + + *end = '\0'; + *cursor = end + 1; + return field; +} + +int +main(int argc, char *argv[]) +{ + struct mount_table table; + struct options options; + struct row total_row; + struct row_list rows; + int rv; + size_t i; + + (void)setlocale(LC_ALL, ""); + memset(&table, 0, sizeof(table)); + memset(&rows, 0, sizeof(rows)); + memset(&total_row, 0, sizeof(total_row)); + + set_default_options(&options); + if (parse_options(argc, argv, &options) != 0) { + free_string_list(&options.type_filter); + return EXIT_FAILURE; + } + + if (load_mounts(&table) != 0) { + free_string_list(&options.type_filter); + return EXIT_FAILURE; + } + + rv = 0; + if (optind == argc) + rv = populate_all_rows(&options, &table, &rows); + else + rv = populate_operand_rows(&options, &table, argv + optind, + argc - optind, &rows); + + if (options.cflag && rows.len > 0) { + total_row.is_total = true; + for (i = 0; i < rows.len; i++) { + total_row.total_bytes = clamp_sum(total_row.total_bytes, + rows.items[i].total_bytes); + total_row.used_bytes = clamp_sum(total_row.used_bytes, + rows.items[i].used_bytes); + total_row.avail_bytes = clamp_sum(total_row.avail_bytes, + rows.items[i].avail_bytes); + total_row.total_inodes = clamp_sum(total_row.total_inodes, + rows.items[i].total_inodes); + total_row.free_inodes = clamp_sum(total_row.free_inodes, + rows.items[i].free_inodes); + } + append_row(&rows, total_row); + } + + print_rows(&options, &rows); + + free_row_list(&rows); + free_mount_table(&table); + free_string_list(&options.type_filter); + return rv == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test.sh b/tests/test.sh new file mode 100644 index 0000000000..ebb683f5a9 --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,179 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +DF_BIN=${DF_BIN:-"$ROOT/out/df"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/df-test.XXXXXX") +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_match() { + pattern=$1 + text=$2 + message=$3 + printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$message" +} + +assert_eq() { + expected=$1 + actual=$2 + message=$3 + [ "$expected" = "$actual" ] || fail "$message: expected [$expected] got [$actual]" +} + +assert_row_fields() { + line=$1 + expected_first=$2 + expected_last=$3 + min_fields=$4 + message=$5 + printf '%s\n' "$line" | awk -v first="$expected_first" -v last="$expected_last" -v min_fields="$min_fields" ' + { + if (NF < min_fields) + exit 1 + if ($1 != first) + exit 1 + if ($NF != last) + exit 1 + } + ' || fail "$message" +} + +expected_mount_line() { + path=$1 + awk -v target="$path" ' + function unescape(value, out, i, octal) { + out = "" + for (i = 1; i <= length(value); i++) { + if (substr(value, i, 1) == "\\" && i + 3 <= length(value) && + substr(value, i + 1, 3) ~ /^[0-7]{3}$/) { + octal = substr(value, i + 1, 3) + out = out sprintf("%c", (substr(octal, 1, 1) + 0) * 64 + (substr(octal, 2, 1) + 0) * 8 + (substr(octal, 3, 1) + 0)) + i += 3 + } else { + out = out substr(value, i, 1) + } + } + return out + } + function within(path, mountpoint, n) { + if (mountpoint == "/") + return substr(path, 1, 1) == "/" + n = length(mountpoint) + return substr(path, 1, n) == mountpoint && + (length(path) == n || substr(path, n + 1, 1) == "/") + } + { + split($0, halves, " - ") + split(halves[1], left, " ") + split(halves[2], right, " ") + mountpoint = unescape(left[5]) + if (!within(target, mountpoint)) + next + if (length(mountpoint) >= best_len) { + best_len = length(mountpoint) + best_src = unescape(right[2]) + best_type = right[1] + best_target = mountpoint + } + } + END { + if (best_len == 0) + exit 1 + printf "%s\t%s\t%s\n", best_src, best_type, best_target + } + ' /proc/self/mountinfo +} + +parse_source() { + printf '%s\n' "$1" | cut -f1 +} + +parse_type() { + printf '%s\n' "$1" | cut -f2 +} + +parse_target() { + printf '%s\n' "$1" | cut -f3 +} + +[ -x "$DF_BIN" ] || fail "missing binary: $DF_BIN" + +export LC_ALL=C + +current_path=$(pwd -P) +mount_info=$(expected_mount_line "$current_path") || fail "could not resolve current mount" +expected_source=$(parse_source "$mount_info") +expected_type=$(parse_type "$mount_info") +expected_target=$(parse_target "$mount_info") + +usage_output=$("$DF_BIN" -z 2>&1 || true) +assert_match '^usage: df ' "$usage_output" "usage output missing" + +unsupported_output=$("$DF_BIN" -n 2>&1 || true) +assert_eq "df: option -n is not supported on Linux" "$unsupported_output" "unsupported -n check failed" + +missing_output=$("$DF_BIN" "$WORKDIR/does-not-exist" 2>&1 || true) +assert_match "^df: $WORKDIR/does-not-exist: " "$missing_output" "missing operand error missing" + +invalid_type_output=$("$DF_BIN" -t '' 2>&1 || true) +assert_eq "df: empty filesystem type list: " "$invalid_type_output" "empty -t list check failed" + +default_output=$("$DF_BIN" -P .) +printf '%s\n' "$default_output" > "$WORKDIR/default.out" +header_line=$(sed -n '1p' "$WORKDIR/default.out") +data_line=$(sed -n '2p' "$WORKDIR/default.out") +assert_match '^Filesystem +512-blocks +Used +Avail +Capacity +Mounted on$' "$header_line" "default header mismatch" +assert_row_fields "$data_line" "$expected_source" "$expected_target" 6 "default row mismatch" + +type_output=$("$DF_BIN" -PT .) +printf '%s\n' "$type_output" > "$WORKDIR/type.out" +assert_match '^Filesystem +Type +512-blocks +Used +Avail +Capacity +Mounted on$' "$(sed -n '1p' "$WORKDIR/type.out")" "type header mismatch" +printf '%s\n' "$(sed -n '2p' "$WORKDIR/type.out")" | awk -v first="$expected_source" -v type="$expected_type" -v last="$expected_target" ' + { + if (NF < 7 || $1 != first || $2 != type || $NF != last) + exit 1 + } +' || fail "type row mismatch" + +inode_output=$("$DF_BIN" -Pi .) +printf '%s\n' "$inode_output" > "$WORKDIR/inode.out" +assert_match '^Filesystem +512-blocks +Used +Avail +Capacity +iused +ifree +%iused +Mounted on$' "$(sed -n '1p' "$WORKDIR/inode.out")" "inode header mismatch" +printf '%s\n' "$(sed -n '2p' "$WORKDIR/inode.out")" | awk -v first="$expected_source" -v last="$expected_target" ' + { + if (NF < 9 || $1 != first || $NF != last) + exit 1 + if ($(NF - 1) != "-" && $(NF - 1) !~ /^[0-9]+%$/) + exit 1 + } +' || fail "inode row mismatch" + +human_output=$("$DF_BIN" -h .) +printf '%s\n' "$human_output" > "$WORKDIR/human.out" +assert_match '^Filesystem +Size +Used +Avail +Capacity +Mounted on$' "$(sed -n '1p' "$WORKDIR/human.out")" "human header mismatch" +assert_row_fields "$(sed -n '2p' "$WORKDIR/human.out")" "$expected_source" "$expected_target" 6 "human row mismatch" + +si_output=$("$DF_BIN" --si .) +assert_match '^Filesystem +Size +Used +Avail +Capacity +Mounted on$' "$(printf '%s\n' "$si_output" | sed -n '1p')" "si header mismatch" + +filtered_output=$("$DF_BIN" -t definitely-not-a-real-fstype . 2>&1 || true) +assert_eq "df: .: filtered out by current filesystem selection" "$filtered_output" "filtered operand error missing" + +type_filter_output=$("$DF_BIN" -t "$expected_type" -P .) +assert_match "^Filesystem +512-blocks +Used +Avail +Capacity +Mounted on$" "$(printf '%s\n' "$type_filter_output" | sed -n '1p')" "type filter header mismatch" +assert_row_fields "$(printf '%s\n' "$type_filter_output" | sed -n '2p')" "$expected_source" "$expected_target" 6 "type filter row missing" + +total_output=$("$DF_BIN" -c -P .) +printf '%s\n' "$total_output" > "$WORKDIR/total.out" +assert_match '^Filesystem +512-blocks +Used +Avail +Capacity +Mounted on$' "$(sed -n '1p' "$WORKDIR/total.out")" "total header mismatch" +assert_match "^total +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+%$" "$(sed -n '3p' "$WORKDIR/total.out")" "total row missing" + +default_again=$("$DF_BIN" -a -P .) +assert_eq "$default_output" "$default_again" "-a should be a no-op on Linux" + +printf '%s\n' "PASS" |
