summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-02-28 19:39:06 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-02-28 19:39:06 +0300
commit3f1e89c0ebb036173f6bb421c77b631a4ad710b4 (patch)
tree001aec89afffc5a8f45dab0bec060ff1aa8f2025
downloadProject-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--.gitignore25
-rw-r--r--GNUmakefile35
-rw-r--r--Makefile8
-rw-r--r--Makefile.depend17
-rw-r--r--README.md26
-rw-r--r--df.1298
-rw-r--r--df.c1349
-rw-r--r--tests/test.sh179
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.
diff --git a/df.1 b/df.1
new file mode 100644
index 0000000000..cd8d562a40
--- /dev/null
+++ b/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/df.c b/df.c
new file mode 100644
index 0000000000..c6b839d449
--- /dev/null
+++ b/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/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"