diff options
Diffstat (limited to 'corebinutils/df')
| -rw-r--r-- | corebinutils/df/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/df/GNUmakefile | 35 | ||||
| -rw-r--r-- | corebinutils/df/LICENSE | 32 | ||||
| -rw-r--r-- | corebinutils/df/LICENSES/BSD-3-Clause.txt | 11 | ||||
| -rw-r--r-- | corebinutils/df/README.md | 26 | ||||
| -rw-r--r-- | corebinutils/df/df.1 | 298 | ||||
| -rw-r--r-- | corebinutils/df/df.c | 1349 | ||||
| -rw-r--r-- | corebinutils/df/tests/test.sh | 179 |
8 files changed, 1955 insertions, 0 deletions
diff --git a/corebinutils/df/.gitignore b/corebinutils/df/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/df/.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/corebinutils/df/GNUmakefile b/corebinutils/df/GNUmakefile new file mode 100644 index 0000000000..cc4425bc3c --- /dev/null +++ b/corebinutils/df/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/corebinutils/df/LICENSE b/corebinutils/df/LICENSE new file mode 100644 index 0000000000..d593353c1b --- /dev/null +++ b/corebinutils/df/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 1980, 1990, 1993, 1994 + The Regents of the University of California. All rights reserved. + +Copyright (c) 2026 + Project Tick. All rights reserved. + +This code is derived from software contributed to Berkeley by +Kevin Fall. + +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.
\ No newline at end of file diff --git a/corebinutils/df/LICENSES/BSD-3-Clause.txt b/corebinutils/df/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..ea890afbc7 --- /dev/null +++ b/corebinutils/df/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) <year> <owner>. + +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 copyright holder 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. diff --git a/corebinutils/df/README.md b/corebinutils/df/README.md new file mode 100644 index 0000000000..94526e2e1d --- /dev/null +++ b/corebinutils/df/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. diff --git a/corebinutils/df/df.1 b/corebinutils/df/df.1 new file mode 100644 index 0000000000..cd8d562a40 --- /dev/null +++ b/corebinutils/df/df.1 @@ -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. diff --git a/corebinutils/df/df.c b/corebinutils/df/df.c new file mode 100644 index 0000000000..c6b839d449 --- /dev/null +++ b/corebinutils/df/df.c @@ -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/corebinutils/df/tests/test.sh b/corebinutils/df/tests/test.sh new file mode 100644 index 0000000000..ebb683f5a9 --- /dev/null +++ b/corebinutils/df/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" |
