diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:26:58 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:26:58 +0300 |
| commit | 4328365a80faebd5963112936b3d5daf0440d6e8 (patch) | |
| tree | f50bc9edec18ffa8c77445fcb3619113bc85eff5 /corebinutils/ls | |
| parent | ec6a123cffbe492c576ec1ad545d5296321a86e1 (diff) | |
| parent | 06b170dd48138a26fdfe1b822ba9846a26a2fa0f (diff) | |
| download | Project-Tick-4328365a80faebd5963112936b3d5daf0440d6e8.tar.gz Project-Tick-4328365a80faebd5963112936b3d5daf0440d6e8.zip | |
Add 'corebinutils/ls/' from commit '06b170dd48138a26fdfe1b822ba9846a26a2fa0f'
git-subtree-dir: corebinutils/ls
git-subtree-mainline: ec6a123cffbe492c576ec1ad545d5296321a86e1
git-subtree-split: 06b170dd48138a26fdfe1b822ba9846a26a2fa0f
Diffstat (limited to 'corebinutils/ls')
| -rw-r--r-- | corebinutils/ls/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/ls/GNUmakefile | 45 | ||||
| -rw-r--r-- | corebinutils/ls/LICENSE | 32 | ||||
| -rw-r--r-- | corebinutils/ls/LICENSES/BSD-3-Clause.txt | 11 | ||||
| -rw-r--r-- | corebinutils/ls/README.md | 45 | ||||
| -rw-r--r-- | corebinutils/ls/cmp.c | 173 | ||||
| -rw-r--r-- | corebinutils/ls/extern.h | 70 | ||||
| -rw-r--r-- | corebinutils/ls/ls.1 | 792 | ||||
| -rw-r--r-- | corebinutils/ls/ls.c | 776 | ||||
| -rw-r--r-- | corebinutils/ls/ls.h | 180 | ||||
| -rw-r--r-- | corebinutils/ls/print.c | 352 | ||||
| -rwxr-xr-x | corebinutils/ls/tests/test.sh | 292 | ||||
| -rw-r--r-- | corebinutils/ls/util.c | 810 |
13 files changed, 3603 insertions, 0 deletions
diff --git a/corebinutils/ls/.gitignore b/corebinutils/ls/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/ls/.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/ls/GNUmakefile b/corebinutils/ls/GNUmakefile new file mode 100644 index 0000000000..ca5d01f76a --- /dev/null +++ b/corebinutils/ls/GNUmakefile @@ -0,0 +1,45 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CCACHE_DISABLE ?= 1 +CPPFLAGS += -D_GNU_SOURCE -I$(CURDIR) +CFLAGS ?= -O2 +CFLAGS += -std=c11 -g -Wall -Wextra -Wno-unused-parameter +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/ls +OBJS := $(OBJDIR)/ls.o $(OBJDIR)/cmp.o $(OBJDIR)/print.o $(OBJDIR)/util.o + +.PHONY: all clean dirs test status + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + env CCACHE_DISABLE=$(CCACHE_DISABLE) $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/ls.o: $(CURDIR)/ls.c $(CURDIR)/ls.h $(CURDIR)/extern.h | dirs + env CCACHE_DISABLE=$(CCACHE_DISABLE) $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/ls.c" -o "$@" + +$(OBJDIR)/cmp.o: $(CURDIR)/cmp.c $(CURDIR)/ls.h $(CURDIR)/extern.h | dirs + env CCACHE_DISABLE=$(CCACHE_DISABLE) $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/cmp.c" -o "$@" + +$(OBJDIR)/print.o: $(CURDIR)/print.c $(CURDIR)/ls.h $(CURDIR)/extern.h | dirs + env CCACHE_DISABLE=$(CCACHE_DISABLE) $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/print.c" -o "$@" + +$(OBJDIR)/util.o: $(CURDIR)/util.c $(CURDIR)/ls.h $(CURDIR)/extern.h | dirs + env CCACHE_DISABLE=$(CCACHE_DISABLE) $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/util.c" -o "$@" + +test: $(TARGET) + LS_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(CURDIR)/build" "$(CURDIR)/out" diff --git a/corebinutils/ls/LICENSE b/corebinutils/ls/LICENSE new file mode 100644 index 0000000000..8a28663b2d --- /dev/null +++ b/corebinutils/ls/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 1980, 1990, 1991, 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 +Michael Fischbein. + +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. diff --git a/corebinutils/ls/LICENSES/BSD-3-Clause.txt b/corebinutils/ls/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..ea890afbc7 --- /dev/null +++ b/corebinutils/ls/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/ls/README.md b/corebinutils/ls/README.md new file mode 100644 index 0000000000..06bbf204aa --- /dev/null +++ b/corebinutils/ls/README.md @@ -0,0 +1,45 @@ +# ls + +Standalone musl-libc-based Linux port of FreeBSD `ls` 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 +``` + +## Port Strategy + +- The port aims to preserve POSIX `ls` behavior first, then FreeBSD/BSD `ls` behavior where Linux can represent it without fake compat layers. +- BSD-only interfaces were removed instead of wrapped: traversal uses direct Linux/POSIX `opendir(3)`/`readdir(3)`/`stat(2)`/`lstat(2)` logic, and birth-time handling uses Linux `statx(2)` via the raw syscall interface. +- GNU libc-only helpers are avoided in the implementation. `-v` uses an in-tree natural version comparator instead of `strverscmp(3)`. +- Long-format identity lookups use `getpwuid_r(3)` and `getgrgid_r(3)` with dynamically sized buffers so the port stays musl-clean. +- The bundled `ls.1` is the contract for this port. FreeBSD-only `-o`, `-W`, and `-Z` remain explicit hard errors on Linux instead of being emulated. + +## Linux Semantics + +Supported mappings: + +- Normal metadata comes from `stat(2)` / `lstat(2)`. +- `-H`, `-L`, `-P` control command-line or full symlink following on Linux. +- `-U` uses Linux `statx(2)` birth time when available and falls back to `mtime` when the kernel or backing filesystem does not expose creation time. +- `--color` and `-G` use fixed ANSI color sequences only; no termcap or `LSCOLORS` parser is required. +- `LS_SAMESORT` is honored like `-y`. + +Intentionally unsupported: + +- `-o`: FreeBSD file flags are not portable on Linux and are not emulated. +- `-W`: whiteout entries do not exist in Linux VFS. +- `-Z`: FreeBSD MAC label output has no portable Linux equivalent in this tool. + +Test scope: + +- The standalone shell suite fixes `LC_ALL=C` for deterministic sorting and covers usage/error paths, BSD visibility rules (`-a`, `-A`, root `-I` handling), default and explicit symlink policies (`-H`, `-L`, `-P`), recursive traversal, size/time/version sorting, `-y`/`LS_SAMESORT`, long-format output, block totals, column/stream layouts, directory grouping, color toggles, and explicit unsupported-option failures. diff --git a/corebinutils/ls/cmp.c b/corebinutils/ls/cmp.c new file mode 100644 index 0000000000..8abc6c673f --- /dev/null +++ b/corebinutils/ls/cmp.c @@ -0,0 +1,173 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1989, 1993 + * 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 + * Michael Fischbein. + * + * 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. + */ + +#include <stdlib.h> +#include <string.h> + +#include "ls.h" +#include "extern.h" + +static struct context *sort_ctx; +static bool sort_root_operands; + +static int compare_names(const struct context *ctx, const struct entry *left, + const struct entry *right) +{ + int cmp; + + (void)ctx; + cmp = strcoll(left->name, right->name); + if (cmp == 0) + cmp = strcmp(left->name, right->name); + return (cmp); +} + +static int compare_version(const struct context *ctx, const struct entry *left, + const struct entry *right) +{ + int cmp; + + (void)ctx; + cmp = natural_version_compare(left->name, right->name); + if (cmp == 0) + cmp = compare_names(ctx, left, right); + return (cmp); +} + +static int compare_dir_groups(const struct context *ctx, + const struct entry *left, const struct entry *right) +{ + if (ctx->opt.group_dirs == GROUP_NONE || left->is_dir == right->is_dir) + return (0); + if (ctx->opt.group_dirs == GROUP_DIRS_FIRST) + return (left->is_dir ? -1 : 1); + return (left->is_dir ? 1 : -1); +} + +static int compare_times(const struct context *ctx, const struct entry *left, + const struct entry *right) +{ + struct timespec lhs; + struct timespec rhs; + bool tie_reverse; + int cmp; + + cmp = compare_names(ctx, left, right); + if (!selected_time(ctx, left, &lhs) || !selected_time(ctx, right, &rhs)) + return (cmp); + if (lhs.tv_sec < rhs.tv_sec) + cmp = 1; + else if (lhs.tv_sec > rhs.tv_sec) + cmp = -1; + else if (lhs.tv_nsec < rhs.tv_nsec) + cmp = 1; + else if (lhs.tv_nsec > rhs.tv_nsec) + cmp = -1; + else { + tie_reverse = ctx->opt.reverse_sort ? !ctx->opt.same_sort_direction : + ctx->opt.same_sort_direction; + cmp = compare_names(ctx, left, right); + if (tie_reverse) + cmp = -cmp; + return (cmp); + } + if (ctx->opt.reverse_sort) + cmp = -cmp; + return (cmp); +} + +static int compare_sizes(const struct context *ctx, const struct entry *left, + const struct entry *right) +{ + if (left->st.st_size < right->st.st_size) + return (1); + if (left->st.st_size > right->st.st_size) + return (-1); + return (compare_names(ctx, left, right)); +} + +static int compare_entries(const struct context *ctx, const struct entry *left, + const struct entry *right, bool root_operands) +{ + int cmp; + + if (root_operands && !ctx->opt.list_directory_itself && + left->is_dir != right->is_dir) + return (left->is_dir ? 1 : -1); + cmp = compare_dir_groups(ctx, left, right); + if (cmp != 0) + return (cmp); + switch (ctx->opt.sort) { + case SORT_TIME: + cmp = compare_times(ctx, left, right); + break; + case SORT_SIZE: + cmp = compare_sizes(ctx, left, right); + break; + case SORT_VERSION: + cmp = compare_version(ctx, left, right); + break; + case SORT_NAME: + default: + cmp = compare_names(ctx, left, right); + break; + } + if (ctx->opt.reverse_sort && ctx->opt.sort != SORT_TIME) + cmp = -cmp; + return (cmp); +} + +static int +qsort_compare(const void *lhs, const void *rhs) +{ + const struct entry *left; + const struct entry *right; + + left = lhs; + right = rhs; + return (compare_entries(sort_ctx, left, right, sort_root_operands)); +} + +void +sort_entries(struct context *ctx, struct entry_list *list, bool root_operands) +{ + if (ctx->opt.no_sort || list->len < 2) + return; + sort_ctx = ctx; + sort_root_operands = root_operands; + qsort(list->items, list->len, sizeof(list->items[0]), qsort_compare); +} diff --git a/corebinutils/ls/extern.h b/corebinutils/ls/extern.h new file mode 100644 index 0000000000..6e70c88c67 --- /dev/null +++ b/corebinutils/ls/extern.h @@ -0,0 +1,70 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1991, 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. + */ + +#ifndef LS_EXTERN_H +#define LS_EXTERN_H + +#include "ls.h" + +void *xmalloc(size_t size); +void *xreallocarray(void *ptr, size_t nmemb, size_t size); +char *xstrdup(const char *src); +char *xasprintf(const char *fmt, ...); +char *join_path(const char *parent, const char *name); +void free_entry(struct entry *entry); +void free_entry_list(struct entry_list *list); +void append_entry(struct entry_list *list, struct entry *entry); +size_t numeric_width(uintmax_t value); +int print_name(const struct options *opt, const char *name); +size_t measure_name(const struct options *opt, const char *name); +char file_type_char(const struct entry *entry, bool slash_only); +void mode_string(const struct entry *entry, char buf[12]); +char *format_uintmax(uintmax_t value, bool thousands); +char *format_block_count(blkcnt_t blocks, long units, bool thousands); +char *format_entry_size(const struct entry *entry, bool human, bool thousands); +int ensure_owner_group(struct entry *entry, const struct options *opt); +int ensure_link_target(struct entry *entry); +bool selected_time(const struct context *ctx, const struct entry *entry, + struct timespec *ts); +char *format_entry_time(const struct context *ctx, const struct entry *entry); +const char *color_start(const struct context *ctx, const struct entry *entry); +const char *color_end(const struct context *ctx); +int natural_version_compare(const char *left, const char *right); +void sort_entries(struct context *ctx, struct entry_list *list, + bool root_operands); +void print_entries(struct context *ctx, const struct entry_list *list, + bool directory_listing); +void usage(void); + +#endif diff --git a/corebinutils/ls/ls.1 b/corebinutils/ls/ls.1 new file mode 100644 index 0000000000..21ece833dc --- /dev/null +++ b/corebinutils/ls/ls.1 @@ -0,0 +1,792 @@ +.\"- +.\" Copyright (c) 1980, 1990, 1991, 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 +.\" the Institute of Electrical and Electronics Engineers, 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. +.\" +.Dd January 16, 2025 +.Dt LS 1 +.Os +.Sh NAME +.Nm ls +.Nd list directory contents +.Sh SYNOPSIS +.Nm +.Op Fl ABCFGHILPRSTUWZabcdfghiklmnopqrstuvwxy1\&, +.Op Fl -color Ns = Ns Ar when +.Op Fl -group-directories Ns = Ns Ar order +.Op Fl -group-directories-first +.Op Fl D Ar format +.Op Ar +.Sh DESCRIPTION +For each operand that names a +.Ar file +of a type other than +directory, +.Nm +displays its name as well as any requested, +associated information. +For each operand that names a +.Ar file +of type directory, +.Nm +displays the names of files contained +within that directory, as well as any requested, associated +information. +.Pp +If no operands are given, the contents of the current +directory are displayed. +If more than one operand is given, +non-directory operands are displayed first; directory +and non-directory operands are sorted separately and in +lexicographical order. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl A +Include directory entries whose names begin with a +dot +.Pq Sq Pa \&. +except for +.Pa \&. +and +.Pa .. . +Automatically set for the super-user unless +.Fl I +is specified. +.It Fl B +Force printing of non-printable characters (as defined by +.Xr ctype 3 +and current locale settings) in file names as +.Li \e Ns Va xxx , +where +.Va xxx +is the numeric value of the character in octal. +This option is not defined in +.St -p1003.1-2008 . +.It Fl C +Force multi-column output; this is the default when output is to a terminal. +.It Fl D Ar format +When printing in the long +.Pq Fl l +format, use +.Ar format +to format the date and time output. +The argument +.Ar format +is a string used by +.Xr strftime 3 . +Depending on the choice of format string, this may result in a +different number of columns in the output. +This option overrides the +.Fl T +option. +This option is not defined in +.St -p1003.1-2008 . +.It Fl F +Display a slash +.Pq Ql / +immediately after each pathname that is a directory, +an asterisk +.Pq Ql * +after each that is executable, +an at sign +.Pq Ql @ +after each symbolic link, +an equals sign +.Pq Ql = +after each socket, +and a vertical bar +.Pq Ql \&| +after each that is a +.Tn FIFO . +.It Fl G +Enable colorized output. +This option is equivalent to defining +.Ev CLICOLOR +or +.Ev COLORTERM +in the environment and setting +.Fl -color Ns = Ns Ar auto . +(See below.) +This functionality can be compiled out by removing the definition of +.Ev COLORLS . +This option is not defined in +.St -p1003.1-2008 . +.It Fl H +Symbolic links on the command line are followed. +This option is assumed if +none of the +.Fl F , d , +or +.Fl l +options are specified. +.It Fl I +Prevent +.Fl A +from being automatically set for the super-user. +This option is not defined in +.St -p1003.1-2008 . +.It Fl L +If argument is a symbolic link, list the file or directory the link references +rather than the link itself. +This option cancels the +.Fl P +option. +.It Fl P +If argument is a symbolic link, list the link itself rather than the +object the link references. +This option cancels the +.Fl H +and +.Fl L +options. +.It Fl R +Recursively list subdirectories encountered. +.It Fl S +Sort by size (largest file first) before sorting the operands in +lexicographical order. +.It Fl T +When printing in the long +.Pq Fl l +format, display complete time information for the file, including +month, day, hour, minute, second, and year. +The +.Fl D +option gives even more control over the output format. +This option is not defined in +.St -p1003.1-2008 . +.It Fl U +Use time when file was created for sorting or printing. +On this Linux port, +.Nm +uses +.Xr statx 2 +when the kernel and backing filesystem expose creation time; otherwise it falls +back to the file's modification time. +This option is not defined in +.St -p1003.1-2008 . +.It Fl W +Reserved for FreeBSD compatibility. +FreeBSD whiteout directory entries are not available through Linux VFS, so this +port rejects +.Fl W +with an error. +This option is not defined in +.St -p1003.1-2008 . +.It Fl Z +Reserved for FreeBSD compatibility. +FreeBSD MAC labels do not have a portable Linux userland equivalent in this +port, so +.Fl Z +is rejected with an error. +This option is not defined in +.St -p1003.1-2008 . +.It Fl a +Include directory entries whose names begin with a +dot +.Pq Sq Pa \&. . +.It Fl b +As +.Fl B , +but use +.Tn C +escape codes whenever possible. +This option is not defined in +.St -p1003.1-2008 . +.It Fl c +Use time when file status was last changed for sorting or printing. +.It Fl -color Ns = Ns Ar when +Output colored escape sequences based on +.Ar when , +which may be set to either +.Cm always , +.Cm auto , +or +.Cm never . +.Pp +.Cm always +will make +.Nm +always output +.Tn ANSI +color escape sequences. +.Cm always +is the default if +.Fl -color +is specified without an argument. +.Pp +.Cm auto +will make +.Nm +output +.Tn ANSI +color escape sequences, but only if +.Dv stdout +is a tty and either the +.Fl G +flag is specified or one of the environment variables +.Ev COLORTERM +or +.Ev CLICOLOR +is set and not empty. +.Pp +.Cm never +will disable color regardless of environment variables. +.Cm never +is the default when neither +.Fl -color +nor +.Fl G +is specified. +.Pp +For compatibility with GNU coreutils, +.Nm +supports +.Cm yes +or +.Cm force +as equivalent to +.Cm always , +.Cm no +or +.Cm none +as equivalent to +.Cm never , +and +.Cm tty +or +.Cm if-tty +as equivalent to +.Cm auto . +.It Fl d +Directories are listed as plain files (not searched recursively). +.It Fl f +Output is not sorted. +This option turns on +.Fl a . +It also negates the effect of the +.Fl r , +.Fl S +and +.Fl t +options. +As allowed by +.St -p1003.1-2008 , +this option has no effect on the +.Fl d , +.Fl l , +.Fl R +and +.Fl s +options. +.It Fl g +Display the long +.Pq Fl l +format output without the file owner's name or number. +.It Fl -group-directories Ns = Ns Ar order +Within results for each operand, +group directories together and print them either +.Cm first +or +.Cm last. +.It Fl -group-directories-first +Equivalent to +.Fl -group-directories Ns = Ns Ar first . +Implemented for compatibility with GNU coreutils. +.It Fl h +When used with the +.Fl l +option, use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte +and Petabyte in order to reduce the number of digits to four or fewer +using base 2 for sizes. +This option is not defined in +.St -p1003.1-2008 . +.It Fl i +For each file, print the file's file serial number (inode number). +.It Fl k +Use 1024-byte units for block counts. +This option also nullifies any +.Fl h +options to its left. +.It Fl l +(The lowercase letter +.Dq ell . ) +List files in the long format, as described in the +.Sx The Long Format +subsection below. +.It Fl m +Stream output format; list files across the page, separated by commas. +.It Fl n +Display user and group IDs numerically rather than converting to a user +or group name in a long +.Pq Fl l +output. +.It Fl o +Reserved for FreeBSD compatibility. +FreeBSD file flags are not exposed through a portable Linux interface, so this +port rejects +.Fl o +with an error instead of emulating filesystem-specific behavior. +.It Fl p +Write a slash +.Pq Ql / +after each filename if that file is a directory. +.It Fl q +Force printing of non-graphic characters in file names as +the character +.Ql \&? ; +this is the default when output is to a terminal. +.It Fl r +Reverse the order of the sort. +.It Fl s +Display the number of blocks used in the file system by each file. +Block sizes and directory totals are handled as described in +.Sx The Long Format +subsection below, except (if the long format is not also requested) +the directory totals are not output when the output is in a +single column, even if multi-column output is requested. +.It Fl t +Sort by descending time modified (most recently modified first). +If two files have the same modification timestamp, sort their names +in ascending lexicographical order. +The +.Fl r +option reverses both of these sort orders. +.Pp +Note that these sort orders are contradictory: the time sequence is in +descending order, the lexicographical sort is in ascending order. +This behavior is mandated by +.St -p1003.2 . +This feature can cause problems listing files stored with sequential names on +FAT file systems, such as from digital cameras, where it is possible to have +more than one image with the same timestamp. +In such a case, the photos cannot be listed in the sequence in which +they were taken. +To ensure the same sort order for time and for lexicographical sorting, set the +environment variable +.Ev LS_SAMESORT +or use the +.Fl y +option. +This causes +.Nm +to reverse the lexicographical sort order when sorting files with the +same modification timestamp. +.It Fl u +Use time of last access, +instead of time of last modification +of the file for sorting +.Pq Fl t +or printing +.Pq Fl l . +.It Fl v +Sort following a natural ordering, using +.Xr strverscmp 3 +instead of +.Xr strcoll 3 +as the comparison function. +E.g., files lexicographically ordered +"bloem1", "bloem10", and "bloem9" would instead be ordered +"bloem1", "bloem9", and "bloem10", as one would perhaps expect. +.It Fl w +Force raw printing of non-printable characters. +This is the default +when output is not to a terminal. +This option is not defined in +.St -p1003.1-2001 . +.It Fl x +The same as +.Fl C , +except that the multi-column output is produced with entries sorted +across, rather than down, the columns. +.It Fl y +When the +.Fl t +option is set, sort the alphabetical output in the same order as the time output. +This has the same effect as setting +.Ev LS_SAMESORT . +See the description of the +.Fl t +option for more details. +This option is not defined in +.St -p1003.1-2001 . +.It Fl 1 +(The numeric digit +.Dq one . ) +Force output to be +one entry per line. +This is the default when +output is not to a terminal. +.It Fl , +(Comma) When the +.Fl l +or +.Fl s +option is set, print file 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. +This option is not defined in +.St -p1003.1-2001 . +.El +.Pp +The +.Fl 1 , C , x , +and +.Fl l +options all override each other; the last one specified determines +the format used. +.Pp +The +.Fl c , u , +and +.Fl U +options all override each other; the last one specified determines +the file time used. +.Pp +The +.Fl S , t +and +.Fl v +options override each other; the last one specified determines +the sort order used. +.Pp +The +.Fl B , b , w , +and +.Fl q +options all override each other; the last one specified determines +the format used for non-printable characters. +.Pp +The +.Fl H , L +and +.Fl P +options all override each other (either partially or fully); they +are applied in the order specified. +.Pp +By default, +.Nm +lists one entry per line to standard +output; the exceptions are to terminals or when the +.Fl C +or +.Fl x +options are specified. +.Pp +File information is displayed with one or more +.Ao blank Ac Ns s +separating the information associated with the +.Fl i , s , +and +.Fl l +options. +.Ss The Long Format +If the +.Fl l +option is given, the following information +is displayed for each file: +file mode, +number of links, owner name, group name, +number of bytes in the file, abbreviated +month, day-of-month file was last modified, +hour file last modified, minute file last +modified, and the pathname. +.Pp +If the modification time of the file is more than 6 months +in the past or future, and the +.Fl D +or +.Fl T +are not specified, +then the year of the last modification +is displayed in place of the hour and minute fields. +.Pp +If the owner or group names are not a known user or group name, +or the +.Fl n +option is given, +the numeric ID's are displayed. +.Pp +If the file is a character special or block special file, +the device number for the file is displayed in the size field. +If the file is a symbolic link the pathname of the +linked-to file is preceded by +.Dq Li -> . +.Pp +The listing of a directory's contents is preceded +by a labeled total number of blocks used in the file system by the files +which are listed as the directory's contents +(which may or may not include +.Pa \&. +and +.Pa .. +and other files which start with a dot, depending on other options). +If the +.Fl h +option is given, +the total size is displayed as the number of bytes. +.Pp +The default block size is 512 bytes. +The block size may be set with option +.Fl k . +Numbers of blocks in the output will have been rounded up so the +numbers of bytes is at least as many as used by the corresponding +file system blocks (which might have a different size). +.Pp +The file mode printed under the +.Fl l +option consists of the +entry type and the permissions. +The entry type character describes the type of file, as +follows: +.Pp +.Bl -tag -width 4n -offset indent -compact +.It Sy \- +Regular file. +.It Sy b +Block special file. +.It Sy c +Character special file. +.It Sy d +Directory. +.It Sy l +Symbolic link. +.It Sy p +.Tn FIFO . +.It Sy s +Socket. +.El +.Pp +The next three fields +are three characters each: +owner permissions, +group permissions, and +other permissions. +Each field has three character positions: +.Bl -enum -offset indent +.It +If +.Sy r , +the file is readable; if +.Sy \- , +it is not readable. +.It +If +.Sy w , +the file is writable; if +.Sy \- , +it is not writable. +.It +The first of the following that applies: +.Bl -tag -width 4n -offset indent +.It Sy S +If in the owner permissions, the file is not executable and +set-user-ID mode is set. +If in the group permissions, the file is not executable +and set-group-ID mode is set. +.It Sy s +If in the owner permissions, the file is executable +and set-user-ID mode is set. +If in the group permissions, the file is executable +and setgroup-ID mode is set. +.It Sy x +The file is executable or the directory is +searchable. +.It Sy \- +The file is neither readable, writable, executable, +nor set-user-ID nor set-group-ID mode, nor sticky. +(See below.) +.El +.Pp +These next two apply only to the third character in the last group +(other permissions). +.Bl -tag -width 4n -offset indent +.It Sy T +The sticky bit is set +(mode +.Li 1000 ) , +but not execute or search permission. +(See +.Xr chmod 1 +or +.Xr sticky 7 . ) +.It Sy t +The sticky bit is set (mode +.Li 1000 ) , +and is searchable or executable. +(See +.Xr chmod 1 +or +.Xr sticky 7 . ) +.El +.El +.Pp +This Linux port does not print FreeBSD file flags, MAC labels, or ACL +markers in long output. +.Sh ENVIRONMENT +The following environment variables affect the execution of +.Nm : +.Bl -tag -width ".Ev CLICOLOR_FORCE" +.It Ev CLICOLOR +Use +.Tn ANSI +color sequences to distinguish file types. +Colorization is automatically disabled unless output is directed to a terminal, +unless +.Ev CLICOLOR_FORCE +is set or +.Fl -color +is set to +.Cm always . +.It Ev CLICOLOR_FORCE +Force color sequences even when standard output is not a terminal. +.It Ev COLORTERM +Enable the same color behavior as +.Ev CLICOLOR +when set and non-empty. +.It Ev COLUMNS +If this variable contains a decimal integer, it is used as the width for +multi-column output. +.It Ev LANG +The locale to use when formatting dates and when sorting names with +.Xr strcoll 3 . +See +.Xr environ 7 +for more information. +.It Ev LS_SAMESORT +If this variable is set, the +.Fl t +option sorts the names of files with the same modification timestamp in the same +sense as the time sort. +See the description of the +.Fl t +option for more details. +.It Ev TZ +The timezone to use when displaying dates. +See +.Xr environ 7 +for more information. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +List the contents of the current working directory in long format: +.Pp +.Dl $ ls -l +.Pp +In addition to listing the contents of the current working directory in +long format, show inode numbers, and suffix each filename with a symbol +representing its file type: +.Pp +.Dl $ ls -liF +.Pp +List the files in +.Pa /var/log , +sorting the output such that the mostly recently modified entries are +printed first: +.Pp +.Dl $ ls -lt /var/log +.Sh COMPATIBILITY +The group field is now automatically included in the long listing for +files in order to be compatible with the +.St -p1003.2 +specification. +.Sh SEE ALSO +.Xr chmod 1 , +.Xr sort 1 , +.Xr xterm 1 Pq Pa ports/x11/xterm , +.Xr localeconv 3 , +.Xr statx 2 , +.Xr strcoll 3 , +.Xr strftime 3 , +.Xr strmode 3 , +.Xr strverscmp 3 , +.Xr sticky 7 , +.Xr symlink 7 +.Sh STANDARDS +With the exception of options +.Fl g +and +.Fl n , +the +.Nm +utility conforms to +.St -p1003.1-2001 +and +.St -p1003.1-2008 . +The options +.Fl B , D , G , I , T , U , W , Z , b , h , o , v , w , y +, +.Fl , +.Fl -color +and +.Fl -group-directories Ns = +(including +.Fl -group-directories-first ) +are non-standard extensions. +.Pp +On this Linux port, +.Fl o , +.Fl W , +and +.Fl Z +are accepted for command-line compatibility but fail with an error instead of +providing FreeBSD-only semantics. +.Sh HISTORY +An +.Nm +command appeared in +.At v1 . +.Pp +The +.Fl v +option was added in +.Fx 13.2 . +.Sh BUGS +To maintain backward compatibility, the relationships between the many +options are quite complex. +.Pp +The exception mentioned in the +.Fl s +option description might be a feature that was +based on the fact that single-column output +usually goes to something other than a terminal. +It is debatable whether this is a design bug. +.Pp +.St -p1003.2 +mandates opposite sort orders for files with the same timestamp when +sorting with the +.Fl t +option. diff --git a/corebinutils/ls/ls.c b/corebinutils/ls/ls.c new file mode 100644 index 0000000000..547d2bcfee --- /dev/null +++ b/corebinutils/ls/ls.c @@ -0,0 +1,776 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1989, 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 + * Michael Fischbein. + * + * 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. + */ + +#include <sys/ioctl.h> +#include <sys/syscall.h> + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <err.h> +#include <fcntl.h> +#include <limits.h> +#include <locale.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <unistd.h> + +#include "ls.h" +#include "extern.h" + +#ifndef AT_FDCWD +#define AT_FDCWD (-100) +#endif + +#ifndef AT_SYMLINK_NOFOLLOW +#define AT_SYMLINK_NOFOLLOW 0x100 +#endif + +#ifndef STATX_TYPE +struct statx_timestamp { + int64_t tv_sec; + uint32_t tv_nsec; + int32_t __reserved; +}; + +struct statx { + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t __spare0[1]; + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; + struct statx_timestamp stx_atime; + struct statx_timestamp stx_btime; + struct statx_timestamp stx_ctime; + struct statx_timestamp stx_mtime; + uint32_t stx_rdev_major; + uint32_t stx_rdev_minor; + uint32_t stx_dev_major; + uint32_t stx_dev_minor; + uint64_t stx_mnt_id; + uint32_t stx_dio_mem_align; + uint32_t stx_dio_offset_align; + uint64_t __spare3[12]; +}; + +#define STATX_TYPE 0x00000001U +#define STATX_MODE 0x00000002U +#define STATX_NLINK 0x00000004U +#define STATX_UID 0x00000008U +#define STATX_GID 0x00000010U +#define STATX_ATIME 0x00000020U +#define STATX_MTIME 0x00000040U +#define STATX_CTIME 0x00000080U +#define STATX_INO 0x00000100U +#define STATX_SIZE 0x00000200U +#define STATX_BLOCKS 0x00000400U +#define STATX_BASIC_STATS 0x000007ffU +#define STATX_BTIME 0x00000800U +#endif + +static void init_options(struct context *ctx); +static int parse_options(struct context *ctx, int argc, char **argv); +static void parse_long_option(struct context *ctx, const char *arg); +static void require_option_argument(int argc, char **argv, int *index, + const char *opt_name, const char **value); +static void finalize_options(struct context *ctx); +static size_t parse_size_t_or_default(const char *text, size_t fallback); +static bool follow_root_argument(const struct context *ctx); +static bool follow_child_entry(const struct context *ctx); +static int collect_path_entry(struct context *ctx, struct entry *entry, + const char *path, const char *name, bool root_argument); +static int stat_with_policy(const struct context *ctx, const char *path, + bool root_argument, struct entry *entry); +static int fill_birthtime(const char *path, bool follow, struct entry *entry); +static void add_dot_entries(struct context *ctx, struct entry_list *list, + const char *dir_path); +static bool collect_directory_entries(struct context *ctx, + struct entry_list *list, const char *dir_path); +static void list_root_files(struct context *ctx, struct entry_list *roots); +static void list_directory(struct context *ctx, const struct entry *dir, + const char *title, bool print_header, const struct visit_stack *stack, + int operand_count); +static bool should_recurse(const struct context *ctx, const struct entry *entry); +static bool visit_stack_contains(const struct visit_stack *stack, + const struct entry *entry); +static void print_directory_header(struct context *ctx, const char *title, + bool print_header); +static void unsupported_option(const char *opt, const char *reason); +static void warn_path_error(struct context *ctx, const char *path, int error); +static int linux_statx(const char *path, int flags, struct statx *stx); + +static void +init_options(struct context *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->now = time(NULL); + ctx->opt.layout = isatty(STDOUT_FILENO) ? LAYOUT_COLUMNS : LAYOUT_SINGLE; + ctx->opt.sort = SORT_NAME; + ctx->opt.time_field = TIME_MTIME; + ctx->opt.follow = FOLLOW_DEFAULT; + ctx->opt.group_dirs = GROUP_NONE; + ctx->opt.name_mode = isatty(STDOUT_FILENO) ? NAME_PRINTABLE : NAME_LITERAL; + ctx->opt.color_mode = COLOR_DEFAULT; + ctx->opt.block_units = 1; + ctx->opt.stdout_is_tty = isatty(STDOUT_FILENO); + ctx->opt.terminal_width = 80; +} + +static size_t +parse_size_t_or_default(const char *text, size_t fallback) +{ + size_t value; + unsigned char ch; + + if (text == NULL || *text == '\0') + return (fallback); + value = 0; + while (*text != '\0') { + ch = (unsigned char)*text++; + if (!isdigit(ch)) + return (fallback); + if (value > (SIZE_MAX - (size_t)(ch - '0')) / 10) + return (fallback); + value = value * 10 + (size_t)(ch - '0'); + } + if (value == 0) + return (fallback); + return (value); +} + +static void +require_option_argument(int argc, char **argv, int *index, const char *opt_name, + const char **value) +{ + if (++(*index) >= argc) + errx(2, "option %s requires an argument", opt_name); + *value = argv[*index]; +} + +static void +unsupported_option(const char *opt, const char *reason) +{ + errx(2, "option %s is not supported on Linux: %s", opt, reason); +} + +static void +parse_long_option(struct context *ctx, const char *arg) +{ + const char *value; + + value = strchr(arg, '='); + if (value != NULL) + value++; + if (strncmp(arg, "--color", 7) == 0 && + (arg[7] == '\0' || arg[7] == '=')) { + if (value == NULL || strcmp(value, "always") == 0 || + strcmp(value, "yes") == 0 || strcmp(value, "force") == 0) + ctx->opt.color_mode = COLOR_ALWAYS; + else if (strcmp(value, "auto") == 0 || strcmp(value, "tty") == 0 || + strcmp(value, "if-tty") == 0) + ctx->opt.color_mode = COLOR_AUTO; + else if (strcmp(value, "never") == 0 || strcmp(value, "no") == 0 || + strcmp(value, "none") == 0) + ctx->opt.color_mode = COLOR_NEVER; + else + errx(2, "unsupported --color value '%s' (must be always, auto, or never)", + value); + return; + } + if (strncmp(arg, "--group-directories", 19) == 0 && + (arg[19] == '\0' || arg[19] == '=')) { + if (value == NULL || strcmp(value, "first") == 0) + ctx->opt.group_dirs = GROUP_DIRS_FIRST; + else if (strcmp(value, "last") == 0) + ctx->opt.group_dirs = GROUP_DIRS_LAST; + else + errx(2, "unsupported --group-directories value '%s' (must be first or last)", + value); + return; + } + if (strcmp(arg, "--group-directories-first") == 0) { + ctx->opt.group_dirs = GROUP_DIRS_FIRST; + return; + } + usage(); +} + +static int +parse_options(struct context *ctx, int argc, char **argv) +{ + int i; + int j; + const char *value; + + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-' || strcmp(argv[i], "-") == 0) + break; + if (strcmp(argv[i], "--") == 0) { + i++; + break; + } + if (strncmp(argv[i], "--", 2) == 0) { + parse_long_option(ctx, argv[i]); + continue; + } + for (j = 1; argv[i][j] != '\0'; j++) { + switch (argv[i][j]) { + case '1': + ctx->opt.layout = LAYOUT_SINGLE; + break; + case 'A': + ctx->opt.show_almost_all = true; + break; + case 'B': + ctx->opt.name_mode = NAME_OCTAL; + break; + case 'C': + ctx->opt.layout = LAYOUT_COLUMNS; + ctx->opt.sort_across = false; + break; + case 'D': + if (argv[i][j + 1] != '\0') { + ctx->opt.time_format = &argv[i][j + 1]; + j = (int)strlen(argv[i]) - 1; + } else { + require_option_argument(argc, argv, &i, "-D", &value); + ctx->opt.time_format = value; + goto next_option_argument; + } + break; + case 'F': + ctx->opt.show_type = true; + ctx->opt.slash_only = false; + break; + case 'G': + ctx->opt.color_mode = COLOR_AUTO; + break; + case 'H': + ctx->opt.follow = FOLLOW_COMMAND_LINE; + break; + case 'I': + ctx->opt.disable_root_almost_all = true; + break; + case 'L': + ctx->opt.follow = FOLLOW_ALL; + break; + case 'P': + ctx->opt.follow = FOLLOW_NEVER; + break; + case 'R': + ctx->opt.recursive = true; + break; + case 'S': + ctx->opt.sort = SORT_SIZE; + break; + case 'T': + ctx->opt.show_full_time = true; + break; + case 'U': + ctx->opt.time_field = TIME_BTIME; + break; + case 'W': + unsupported_option("-W", "Linux VFS has no FreeBSD whiteout entries"); + break; + case 'Z': + unsupported_option("-Z", "FreeBSD MAC labels do not map to a portable Linux userland interface"); + break; + case 'a': + ctx->opt.show_all = true; + ctx->opt.show_almost_all = false; + break; + case 'b': + ctx->opt.name_mode = NAME_ESCAPE; + break; + case 'c': + ctx->opt.time_field = TIME_CTIME; + break; + case 'd': + ctx->opt.list_directory_itself = true; + ctx->opt.recursive = false; + break; + case 'f': + ctx->opt.no_sort = true; + ctx->opt.show_all = true; + ctx->opt.show_almost_all = false; + break; + case 'g': + ctx->opt.layout = LAYOUT_LONG; + ctx->opt.suppress_owner = true; + break; + case 'h': + ctx->opt.human_readable = true; + ctx->opt.block_units = 1; + break; + case 'i': + ctx->opt.show_inode = true; + break; + case 'k': + ctx->opt.human_readable = false; + ctx->opt.block_units = 2; + break; + case 'l': + ctx->opt.layout = LAYOUT_LONG; + break; + case 'm': + ctx->opt.layout = LAYOUT_STREAM; + break; + case 'n': + ctx->opt.numeric_ids = true; + ctx->opt.layout = LAYOUT_LONG; + break; + case 'o': + unsupported_option("-o", "FreeBSD file flags require a filesystem-specific ioctl surface on Linux; use lsattr for ext-family flags"); + break; + case 'p': + ctx->opt.show_type = true; + ctx->opt.slash_only = true; + break; + case 'q': + ctx->opt.name_mode = NAME_PRINTABLE; + break; + case 'r': + ctx->opt.reverse_sort = true; + break; + case 's': + ctx->opt.show_blocks = true; + break; + case 't': + ctx->opt.sort = SORT_TIME; + break; + case 'u': + ctx->opt.time_field = TIME_ATIME; + break; + case 'v': + ctx->opt.sort = SORT_VERSION; + break; + case 'w': + ctx->opt.name_mode = NAME_LITERAL; + break; + case 'x': + ctx->opt.layout = LAYOUT_COLUMNS; + ctx->opt.sort_across = true; + break; + case 'y': + ctx->opt.same_sort_direction = true; + break; + case ',': + ctx->opt.thousands = true; + break; + default: + usage(); + } + } +next_option_argument: + ; + } + return (i); +} + +static void +finalize_options(struct context *ctx) +{ + const char *columns; + const char *clicolor_force; + const char *clicolor; + const char *colorterm; + const char *same_sort; + struct winsize win; + bool env_color; + bool force_color; + + columns = getenv("COLUMNS"); + ctx->opt.terminal_width = parse_size_t_or_default(columns, 80); + if (columns == NULL && ctx->opt.stdout_is_tty && + ioctl(STDOUT_FILENO, TIOCGWINSZ, &win) == 0 && win.ws_col > 0) + ctx->opt.terminal_width = win.ws_col; + same_sort = getenv("LS_SAMESORT"); + if (same_sort != NULL && *same_sort != '\0') + ctx->opt.same_sort_direction = true; + if (!ctx->opt.show_all && geteuid() == 0 && !ctx->opt.disable_root_almost_all) + ctx->opt.show_almost_all = true; + clicolor = getenv("CLICOLOR"); + colorterm = getenv("COLORTERM"); + clicolor_force = getenv("CLICOLOR_FORCE"); + env_color = (clicolor != NULL) || (colorterm != NULL && *colorterm != '\0'); + force_color = clicolor_force != NULL && *clicolor_force != '\0'; + switch (ctx->opt.color_mode) { + case COLOR_NEVER: + ctx->opt.colorize = false; + break; + case COLOR_ALWAYS: + ctx->opt.colorize = true; + break; + case COLOR_AUTO: + ctx->opt.colorize = ctx->opt.stdout_is_tty || force_color; + break; + case COLOR_DEFAULT: + default: + ctx->opt.colorize = env_color && (ctx->opt.stdout_is_tty || force_color); + break; + } +} + +static bool +follow_root_argument(const struct context *ctx) +{ + switch (ctx->opt.follow) { + case FOLLOW_COMMAND_LINE: + return (true); + case FOLLOW_ALL: + return (true); + case FOLLOW_NEVER: + return (false); + case FOLLOW_DEFAULT: + default: + return (!ctx->opt.list_directory_itself && ctx->opt.layout != LAYOUT_LONG && + (!ctx->opt.show_type || ctx->opt.slash_only)); + } +} + +static bool +follow_child_entry(const struct context *ctx) +{ + return (ctx->opt.follow == FOLLOW_ALL); +} + +static int +linux_statx(const char *path, int flags, struct statx *stx) +{ +#ifdef SYS_statx + return ((int)syscall(SYS_statx, AT_FDCWD, path, flags, + STATX_BASIC_STATS | STATX_BTIME, stx)); +#else + (void)path; + (void)flags; + (void)stx; + errno = ENOSYS; + return (-1); +#endif +} + +static int +fill_birthtime(const char *path, bool follow, struct entry *entry) +{ + struct statx stx; + int flags; + + memset(&stx, 0, sizeof(stx)); + flags = follow ? 0 : AT_SYMLINK_NOFOLLOW; + if (linux_statx(path, flags, &stx) != 0) + return (-1); + if ((stx.stx_mask & STATX_BTIME) == 0) + return (-1); + entry->btime.ts.tv_sec = stx.stx_btime.tv_sec; + entry->btime.ts.tv_nsec = stx.stx_btime.tv_nsec; + entry->btime.available = true; + return (0); +} + +static int +stat_with_policy(const struct context *ctx, const char *path, bool root_argument, + struct entry *entry) +{ + bool follow; + int error; + + follow = root_argument ? follow_root_argument(ctx) : follow_child_entry(ctx); + entry->followed = follow; + if ((follow ? stat(path, &entry->st) : lstat(path, &entry->st)) != 0) { + entry->stat_errno = errno; + entry->stat_ok = false; + return (-1); + } + entry->stat_ok = true; + entry->is_dir = S_ISDIR(entry->st.st_mode); + entry->is_symlink = S_ISLNK(entry->st.st_mode); + if (ctx->opt.time_field == TIME_BTIME) { + error = fill_birthtime(path, follow, entry); + if (error != 0) + entry->btime.available = false; + } + return (0); +} + +static int +collect_path_entry(struct context *ctx, struct entry *entry, const char *path, + const char *name, bool root_argument) +{ + memset(entry, 0, sizeof(*entry)); + entry->name = xstrdup(name); + entry->path = xstrdup(path); + if (stat_with_policy(ctx, path, root_argument, entry) != 0) { + if (entry->stat_errno == 0) + entry->stat_errno = errno; + return (-1); + } + return (0); +} + +static void +warn_path_error(struct context *ctx, const char *path, int error) +{ + warnx("%s: %s", path, strerror(error)); + ctx->exit_status = 1; +} + +static void +add_dot_entries(struct context *ctx, struct entry_list *list, const char *dir_path) +{ + struct entry entry; + char *path; + + path = join_path(dir_path, "."); + if (collect_path_entry(ctx, &entry, path, ".", false) == 0) + append_entry(list, &entry); + else { + warn_path_error(ctx, path, entry.stat_errno); + free_entry(&entry); + } + free(path); + path = join_path(dir_path, ".."); + if (collect_path_entry(ctx, &entry, path, "..", false) == 0) + append_entry(list, &entry); + else { + warn_path_error(ctx, path, entry.stat_errno); + free_entry(&entry); + } + free(path); +} + +static bool +collect_directory_entries(struct context *ctx, struct entry_list *list, + const char *dir_path) +{ + DIR *dirp; + struct dirent *dent; + struct entry entry; + char *path; + + dirp = opendir(dir_path); + if (dirp == NULL) { + warn_path_error(ctx, dir_path, errno); + return (false); + } + if (ctx->opt.show_all) + add_dot_entries(ctx, list, dir_path); + while ((dent = readdir(dirp)) != NULL) { + if (strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) + continue; + if (!ctx->opt.show_all && !ctx->opt.show_almost_all && + dent->d_name[0] == '.') + continue; + path = join_path(dir_path, dent->d_name); + if (collect_path_entry(ctx, &entry, path, dent->d_name, false) == 0) + append_entry(list, &entry); + else { + warn_path_error(ctx, path, entry.stat_errno); + free_entry(&entry); + } + free(path); + } + closedir(dirp); + return (true); +} + +static void +print_directory_header(struct context *ctx, const char *title, bool print_header) +{ + if (!print_header) + return; + if (ctx->wrote_output) + putchar('\n'); + print_name(&ctx->opt, title); + puts(":"); +} + +static bool +visit_stack_contains(const struct visit_stack *stack, const struct entry *entry) +{ + while (stack != NULL) { + if (stack->dev == entry->st.st_dev && stack->ino == entry->st.st_ino) + return (true); + stack = stack->parent; + } + return (false); +} + +static bool +should_recurse(const struct context *ctx, const struct entry *entry) +{ + (void)ctx; + if (!entry->is_dir) + return (false); + if (strcmp(entry->name, ".") == 0 || strcmp(entry->name, "..") == 0) + return (false); + return (true); +} + +static void +list_directory(struct context *ctx, const struct entry *dir, const char *title, + bool print_header, const struct visit_stack *stack, int operand_count) +{ + struct entry_list list; + struct visit_stack here; + size_t i; + char *child_title; + bool child_header; + + (void)operand_count; + memset(&list, 0, sizeof(list)); + if (!collect_directory_entries(ctx, &list, dir->path)) + return; + sort_entries(ctx, &list, false); + print_directory_header(ctx, title, print_header); + print_entries(ctx, &list, true); + if (!ctx->opt.recursive) { + free_entry_list(&list); + return; + } + here.dev = dir->st.st_dev; + here.ino = dir->st.st_ino; + here.parent = stack; + for (i = 0; i < list.len; i++) { + if (!should_recurse(ctx, &list.items[i])) + continue; + if (visit_stack_contains(&here, &list.items[i])) { + warnx("%s: directory causes a cycle", list.items[i].path); + ctx->exit_status = 1; + continue; + } + child_title = join_path(title, list.items[i].name); + child_header = true; + list_directory(ctx, &list.items[i], child_title, child_header, &here, + operand_count); + free(child_title); + } + free_entry_list(&list); +} + +static void +list_root_files(struct context *ctx, struct entry_list *roots) +{ + struct entry_list files; + size_t i; + + memset(&files, 0, sizeof(files)); + for (i = 0; i < roots->len; i++) { + if (!roots->items[i].is_dir || ctx->opt.list_directory_itself) { + if (files.len == files.cap) { + files.cap = files.cap == 0 ? 8 : files.cap * 2; + files.items = xreallocarray(files.items, files.cap, + sizeof(*files.items)); + } + files.items[files.len++] = roots->items[i]; + } + } + if (files.len == 0) { + free(files.items); + return; + } + sort_entries(ctx, &files, true); + print_entries(ctx, &files, false); + free(files.items); +} + +int +main(int argc, char **argv) +{ + struct context ctx; + struct entry_list roots; + struct entry entry; + int argi; + int operand_count; + size_t i; + bool print_header; + + setlocale(LC_ALL, ""); + init_options(&ctx); + argi = parse_options(&ctx, argc, argv); + finalize_options(&ctx); + memset(&roots, 0, sizeof(roots)); + operand_count = argc - argi; + if (operand_count == 0) + operand_count = 1; + if (argi == argc) { + if (collect_path_entry(&ctx, &entry, ".", ".", true) == 0) + append_entry(&roots, &entry); + else { + warn_path_error(&ctx, ".", entry.stat_errno); + free_entry(&entry); + } + } else { + for (; argi < argc; argi++) { + if (collect_path_entry(&ctx, &entry, argv[argi], argv[argi], true) == 0) + append_entry(&roots, &entry); + else { + warn_path_error(&ctx, argv[argi], entry.stat_errno); + free_entry(&entry); + } + } + } + sort_entries(&ctx, &roots, true); + list_root_files(&ctx, &roots); + for (i = 0; i < roots.len; i++) { + if (!roots.items[i].is_dir || ctx.opt.list_directory_itself) + continue; + print_header = operand_count > 1; + list_directory(&ctx, &roots.items[i], roots.items[i].name, + print_header, NULL, operand_count); + } + free_entry_list(&roots); + return (ctx.exit_status); +} + +void +usage(void) +{ + fprintf(stderr, + "usage: ls [-ABCFGHILPRSTUabcdfghiklmnopqrstuvwxy1,] [--color=when] " + "[--group-directories=first|last] [-D format] [file ...]\n"); + exit(1); +} diff --git a/corebinutils/ls/ls.h b/corebinutils/ls/ls.h new file mode 100644 index 0000000000..9668bdce4c --- /dev/null +++ b/corebinutils/ls/ls.h @@ -0,0 +1,180 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1989, 1993 + * 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 + * Michael Fischbein. + * + * 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. + */ + +#ifndef LS_H +#define LS_H + +#include <sys/stat.h> +#include <sys/types.h> + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <time.h> + +enum layout_mode { + LAYOUT_SINGLE = 0, + LAYOUT_COLUMNS, + LAYOUT_LONG, + LAYOUT_STREAM, +}; + +enum sort_mode { + SORT_NAME = 0, + SORT_TIME, + SORT_SIZE, + SORT_VERSION, +}; + +enum time_field { + TIME_MTIME = 0, + TIME_ATIME, + TIME_BTIME, + TIME_CTIME, +}; + +enum follow_mode { + FOLLOW_DEFAULT = 0, + FOLLOW_COMMAND_LINE, + FOLLOW_ALL, + FOLLOW_NEVER, +}; + +enum group_mode { + GROUP_NONE = 0, + GROUP_DIRS_FIRST, + GROUP_DIRS_LAST, +}; + +enum name_mode { + NAME_LITERAL = 0, + NAME_PRINTABLE, + NAME_OCTAL, + NAME_ESCAPE, +}; + +enum color_mode { + COLOR_DEFAULT = 0, + COLOR_NEVER, + COLOR_AUTO, + COLOR_ALWAYS, +}; + +struct file_time { + struct timespec ts; + bool available; +}; + +struct options { + enum layout_mode layout; + enum sort_mode sort; + enum time_field time_field; + enum follow_mode follow; + enum group_mode group_dirs; + enum name_mode name_mode; + enum color_mode color_mode; + long block_units; + size_t terminal_width; + const char *time_format; + bool stdout_is_tty; + bool show_all; + bool show_almost_all; + bool disable_root_almost_all; + bool list_directory_itself; + bool recursive; + bool show_full_time; + bool human_readable; + bool show_inode; + bool show_blocks; + bool show_type; + bool slash_only; + bool numeric_ids; + bool suppress_owner; + bool reverse_sort; + bool same_sort_direction; + bool no_sort; + bool sort_across; + bool thousands; + bool colorize; +}; + +struct entry { + char *name; + char *path; + struct stat st; + struct file_time btime; + char *user; + char *group; + char *link_target; + int stat_errno; + bool stat_ok; + bool is_dir; + bool is_symlink; + bool followed; +}; + +struct entry_list { + struct entry *items; + size_t len; + size_t cap; +}; + +struct display_info { + uintmax_t total_blocks; + size_t max_name_width; + size_t inode_width; + size_t block_width; + size_t links_width; + size_t user_width; + size_t group_width; + size_t size_width; +}; + +struct context { + struct options opt; + time_t now; + int exit_status; + bool wrote_output; +}; + +struct visit_stack { + dev_t dev; + ino_t ino; + const struct visit_stack *parent; +}; + +#endif diff --git a/corebinutils/ls/print.c b/corebinutils/ls/print.c new file mode 100644 index 0000000000..057d37a3eb --- /dev/null +++ b/corebinutils/ls/print.c @@ -0,0 +1,352 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1989, 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 + * Michael Fischbein. + * + * 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. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "ls.h" +#include "extern.h" + +static void compute_display_info(const struct context *ctx, + const struct entry_list *list, struct display_info *info); +static void print_single(const struct context *ctx, + const struct entry_list *list, const struct display_info *info); +static void print_columns(const struct context *ctx, + const struct entry_list *list, const struct display_info *info, + bool across); +static void print_stream(const struct context *ctx, + const struct entry_list *list, const struct display_info *info); +static void print_long(const struct context *ctx, + const struct entry_list *list, const struct display_info *info); +static size_t print_entry_short(const struct context *ctx, + const struct entry *entry, const struct display_info *info); +static bool output_is_single_column(const struct context *ctx, + const struct entry_list *list, const struct display_info *info); + +static void +compute_display_info(const struct context *ctx, const struct entry_list *list, + struct display_info *info) +{ + size_t i; + char *tmp; + + memset(info, 0, sizeof(*info)); + for (i = 0; i < list->len; i++) { + const struct entry *entry; + size_t width; + + entry = &list->items[i]; + width = measure_name(&ctx->opt, entry->name); + if (ctx->opt.show_type && file_type_char(entry, ctx->opt.slash_only) != '\0') + width++; + if (width > info->max_name_width) + info->max_name_width = width; + if (ctx->opt.show_inode && numeric_width(entry->st.st_ino) > info->inode_width) + info->inode_width = numeric_width(entry->st.st_ino); + if (ctx->opt.show_blocks || ctx->opt.layout == LAYOUT_LONG) + info->total_blocks += (uintmax_t)entry->st.st_blocks; + if (ctx->opt.show_blocks) { + tmp = format_block_count(entry->st.st_blocks, ctx->opt.block_units, + ctx->opt.thousands); + width = strlen(tmp); + if (width > info->block_width) + info->block_width = width; + free(tmp); + } + if (ctx->opt.layout == LAYOUT_LONG) { + struct entry *mutable_entry; + + mutable_entry = (struct entry *)entry; + ensure_owner_group(mutable_entry, &ctx->opt); + if (numeric_width(entry->st.st_nlink) > info->links_width) + info->links_width = numeric_width(entry->st.st_nlink); + width = strlen(entry->user); + if (width > info->user_width) + info->user_width = width; + width = strlen(entry->group); + if (width > info->group_width) + info->group_width = width; + tmp = format_entry_size(entry, ctx->opt.human_readable, + ctx->opt.thousands); + width = strlen(tmp); + if (width > info->size_width) + info->size_width = width; + free(tmp); + } + } +} + +static size_t +print_entry_short(const struct context *ctx, const struct entry *entry, + const struct display_info *info) +{ + size_t width; + char *tmp; + char indicator; + const char *start; + const char *end; + + width = 0; + if (ctx->opt.show_inode) { + printf("%*ju ", (int)info->inode_width, (uintmax_t)entry->st.st_ino); + width += info->inode_width + 1; + } + if (ctx->opt.show_blocks) { + tmp = format_block_count(entry->st.st_blocks, ctx->opt.block_units, + ctx->opt.thousands); + printf("%*s ", (int)info->block_width, tmp); + width += info->block_width + 1; + free(tmp); + } + start = color_start(ctx, entry); + end = color_end(ctx); + if (*start != '\0') + fputs(start, stdout); + width += print_name(&ctx->opt, entry->name); + if (*start != '\0') + fputs(end, stdout); + indicator = ctx->opt.show_type ? file_type_char(entry, ctx->opt.slash_only) : '\0'; + if (indicator != '\0') { + putchar(indicator); + width++; + } + return (width); +} + +static void +print_single(const struct context *ctx, const struct entry_list *list, + const struct display_info *info) +{ + size_t i; + + for (i = 0; i < list->len; i++) { + print_entry_short(ctx, &list->items[i], info); + putchar('\n'); + } +} + +static void +print_stream(const struct context *ctx, const struct entry_list *list, + const struct display_info *info) +{ + size_t i; + size_t used; + size_t width; + + used = 0; + for (i = 0; i < list->len; i++) { + width = info->max_name_width + (ctx->opt.show_inode ? info->inode_width + 1 : 0) + + (ctx->opt.show_blocks ? info->block_width + 1 : 0); + if (used != 0 && used + width + 2 >= ctx->opt.terminal_width) { + putchar('\n'); + used = 0; + } + used += print_entry_short(ctx, &list->items[i], info); + if (i + 1 != list->len) { + fputs(", ", stdout); + used += 2; + } + } + if (used != 0) + putchar('\n'); +} + +static bool +output_is_single_column(const struct context *ctx, const struct entry_list *list, + const struct display_info *info) +{ + size_t col_width; + size_t cols; + + if (ctx->opt.layout == LAYOUT_SINGLE || list->len <= 1) + return (true); + if (ctx->opt.layout != LAYOUT_COLUMNS) + return (false); + col_width = info->max_name_width; + if (ctx->opt.show_inode) + col_width += info->inode_width + 1; + if (ctx->opt.show_blocks) + col_width += info->block_width + 1; + col_width += 2; + if (ctx->opt.terminal_width < col_width * 2) + return (true); + cols = ctx->opt.terminal_width / col_width; + return (cols <= 1); +} + +static void +print_columns(const struct context *ctx, const struct entry_list *list, + const struct display_info *info, bool across) +{ + const struct entry **table; + size_t col_width; + size_t count; + size_t cols; + size_t rows; + size_t row; + size_t col; + size_t idx; + size_t target; + size_t printed; + size_t spaces; + + count = list->len; + if (count == 0) + return; + col_width = info->max_name_width; + if (ctx->opt.show_inode) + col_width += info->inode_width + 1; + if (ctx->opt.show_blocks) + col_width += info->block_width + 1; + col_width += 2; + if (ctx->opt.terminal_width < col_width * 2) { + print_single(ctx, list, info); + return; + } + cols = ctx->opt.terminal_width / col_width; + if (cols == 0) + cols = 1; + rows = (count + cols - 1) / cols; + table = xmalloc(count * sizeof(*table)); + for (idx = 0; idx < count; idx++) + table[idx] = &list->items[idx]; + for (row = 0; row < rows; row++) { + for (col = 0; col < cols; col++) { + idx = across ? row * cols + col : col * rows + row; + if (idx >= count) + continue; + printed = print_entry_short(ctx, table[idx], info); + target = (col + 1) * col_width; + if (col + 1 == cols || (across ? row * cols + col + 1 : (col + 1) * rows + row) >= count) + continue; + if (printed < target - col * col_width) + spaces = target - col * col_width - printed; + else + spaces = 1; + while (spaces-- > 0) + putchar(' '); + } + putchar('\n'); + } + free(table); +} + +static void +print_long(const struct context *ctx, const struct entry_list *list, + const struct display_info *info) +{ + size_t i; + char mode[12]; + char *size; + char *time_str; + const char *start; + const char *end; + + for (i = 0; i < list->len; i++) { + struct entry *entry; + + entry = &list->items[i]; + mode_string(entry, mode); + size = format_entry_size(entry, ctx->opt.human_readable, + ctx->opt.thousands); + time_str = format_entry_time(ctx, entry); + printf("%s %*ju ", mode, (int)info->links_width, + (uintmax_t)entry->st.st_nlink); + if (!ctx->opt.suppress_owner) + printf("%-*s ", (int)info->user_width, entry->user); + printf("%-*s %*s %s ", (int)info->group_width, entry->group, + (int)info->size_width, size, time_str); + start = color_start(ctx, entry); + end = color_end(ctx); + if (*start != '\0') + fputs(start, stdout); + print_name(&ctx->opt, entry->name); + if (*start != '\0') + fputs(end, stdout); + if (ctx->opt.show_type) { + char indicator; + + indicator = file_type_char(entry, ctx->opt.slash_only); + if (indicator != '\0') + putchar(indicator); + } + if (entry->is_symlink && !entry->followed) { + if (ensure_link_target(entry) == 0) { + fputs(" -> ", stdout); + print_name(&ctx->opt, entry->link_target); + } + } + putchar('\n'); + free(size); + free(time_str); + } +} + +void +print_entries(struct context *ctx, const struct entry_list *list, + bool directory_listing) +{ + struct display_info info; + char *total; + + compute_display_info(ctx, list, &info); + if (directory_listing && + (ctx->opt.layout == LAYOUT_LONG || + (ctx->opt.show_blocks && !output_is_single_column(ctx, list, &info)))) { + total = format_block_count((blkcnt_t)info.total_blocks, + ctx->opt.block_units, false); + printf("total %s\n", total); + free(total); + } + switch (ctx->opt.layout) { + case LAYOUT_LONG: + print_long(ctx, list, &info); + break; + case LAYOUT_STREAM: + print_stream(ctx, list, &info); + break; + case LAYOUT_COLUMNS: + print_columns(ctx, list, &info, ctx->opt.sort_across); + break; + case LAYOUT_SINGLE: + default: + print_single(ctx, list, &info); + break; + } + ctx->wrote_output = true; +} diff --git a/corebinutils/ls/tests/test.sh b/corebinutils/ls/tests/test.sh new file mode 100755 index 0000000000..174feebe83 --- /dev/null +++ b/corebinutils/ls/tests/test.sh @@ -0,0 +1,292 @@ +#!/bin/sh + +set -eu +export LC_ALL=C +export TZ=UTC + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +LS_BIN=${LS_BIN:-"$ROOT/out/ls"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/ls-test.XXXXXX") +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_contains() { + needle=$1 + haystack=$2 + case $haystack in + *"$needle"*) ;; + *) fail "missing '$needle'" ;; + esac +} + +assert_not_contains() { + needle=$1 + haystack=$2 + case $haystack in + *"$needle"*) fail "unexpected '$needle'" ;; + *) ;; + esac +} + +assert_eq() { + expected=$1 + actual=$2 + [ "$expected" = "$actual" ] || fail "expected '$expected' got '$actual'" +} + +first_line() { + printf '%s\n' "$1" | sed -n '1p' +} + +second_line() { + printf '%s\n' "$1" | sed -n '2p' +} + +[ -x "$LS_BIN" ] || fail "missing binary: $LS_BIN" + +mkdir -p "$WORKDIR/tree/subdir" "$WORKDIR/group/a_dir" "$WORKDIR/cols" +printf '%s\n' alpha > "$WORKDIR/tree/file.txt" +printf '%s\n' beta > "$WORKDIR/tree/subdir/nested.txt" +printf '%s\n' hidden > "$WORKDIR/tree/.hidden" +: > "$WORKDIR/tree/0b0000002" +: > "$WORKDIR/tree/0b0000010" +mkfifo "$WORKDIR/tree/fifo" +chmod 755 "$WORKDIR/tree/file.txt" +ln -s subdir "$WORKDIR/tree/linkdir" +: > "$WORKDIR/group/b_file" +: > "$WORKDIR/cols/aa" +: > "$WORKDIR/cols/bb" +: > "$WORKDIR/cols/cc" +: > "$WORKDIR/cols/dd" + +usage_output=$("$LS_BIN" --definitely-invalid 2>&1 || true) +assert_contains "usage: ls " "$usage_output" + +missing_D=$("$LS_BIN" -D 2>&1 >/dev/null || true) +assert_contains "option -D requires an argument" "$missing_D" + +basic=$("$LS_BIN" -1 "$WORKDIR/tree") +assert_contains "file.txt" "$basic" +assert_contains "subdir" "$basic" +assert_not_contains ".hidden" "$basic" + +default_follow=$("$LS_BIN" -1 "$WORKDIR/tree/linkdir") +assert_contains "nested.txt" "$default_follow" + +with_a=$("$LS_BIN" -1a "$WORKDIR/tree") +assert_contains "." "$with_a" +assert_contains ".." "$with_a" +assert_contains ".hidden" "$with_a" + +with_A=$("$LS_BIN" -1A "$WORKDIR/tree") +assert_contains ".hidden" "$with_A" +assert_not_contains "$(printf '.\n..')" "$with_A" + +if [ "$(id -u)" = "0" ]; then + implicit_A=$("$LS_BIN" -1 "$WORKDIR/tree") + assert_contains ".hidden" "$implicit_A" + with_I=$("$LS_BIN" -1I "$WORKDIR/tree") + assert_not_contains ".hidden" "$with_I" +fi + +escaped=$( + cd "$WORKDIR/tree" + touch "$(printf 'bad\013name')" + "$LS_BIN" -1b +) +assert_contains "bad\\vname" "$escaped" + +octal=$( + cd "$WORKDIR/tree" + "$LS_BIN" -1B +) +assert_contains "bad\\013name" "$octal" + +printable=$( + cd "$WORKDIR/tree" + "$LS_BIN" -1q +) +assert_contains "bad?name" "$printable" + +literal=$( + cd "$WORKDIR/tree" + "$LS_BIN" -1w +) +assert_contains "bad" "$literal" + +version_sorted=$("$LS_BIN" -1v "$WORKDIR/tree/0b0000002" "$WORKDIR/tree/0b0000010") +assert_eq "$WORKDIR/tree/0b0000002" "$(first_line "$version_sorted")" +assert_eq "$WORKDIR/tree/0b0000010" "$(second_line "$version_sorted")" + +classified=$("$LS_BIN" -1F "$WORKDIR/tree") +assert_contains "subdir/" "$classified" +assert_contains "linkdir@" "$classified" +assert_contains "fifo|" "$classified" +assert_contains "file.txt*" "$classified" + +slash_only=$("$LS_BIN" -1p "$WORKDIR/tree") +assert_contains "subdir/" "$slash_only" +assert_not_contains "fifo|" "$slash_only" + +dir_self=$("$LS_BIN" -1d "$WORKDIR/tree/subdir") +assert_eq "$WORKDIR/tree/subdir" "$dir_self" + +follow_H=$("$LS_BIN" -1H "$WORKDIR/tree/linkdir") +assert_contains "nested.txt" "$follow_H" + +follow_L=$("$LS_BIN" -1L "$WORKDIR/tree/linkdir") +assert_contains "nested.txt" "$follow_L" + +physical_P=$("$LS_BIN" -1P "$WORKDIR/tree/linkdir") +assert_eq "$WORKDIR/tree/linkdir" "$physical_P" + +physical_HP=$("$LS_BIN" -1HP "$WORKDIR/tree/linkdir") +assert_eq "$WORKDIR/tree/linkdir" "$physical_HP" + +follow_PH=$("$LS_BIN" -1PH "$WORKDIR/tree/linkdir") +assert_contains "nested.txt" "$follow_PH" + +long_symlink=$("$LS_BIN" -lP "$WORKDIR/tree/linkdir") +assert_contains "-> subdir" "$long_symlink" + +classified_link=$("$LS_BIN" -1F "$WORKDIR/tree/linkdir") +assert_contains "@" "$classified_link" +assert_not_contains "nested.txt" "$classified_link" + +recursive=$("$LS_BIN" -1R "$WORKDIR/tree") +assert_contains "$WORKDIR/tree/subdir:" "$recursive" +assert_contains "nested.txt" "$recursive" + +printf 'a' > "$WORKDIR/small" +dd if=/dev/zero of="$WORKDIR/big" bs=1 count=64 >/dev/null 2>&1 +size_sorted=$("$LS_BIN" -1S "$WORKDIR/small" "$WORKDIR/big") +assert_eq "$WORKDIR/big" "$(first_line "$size_sorted")" +reverse_size=$("$LS_BIN" -1Sr "$WORKDIR/small" "$WORKDIR/big") +assert_eq "$WORKDIR/small" "$(first_line "$reverse_size")" + +touch -t 202401010101 "$WORKDIR/older" +touch -t 202402020202 "$WORKDIR/newer" +time_sorted=$("$LS_BIN" -1t "$WORKDIR/older" "$WORKDIR/newer") +assert_eq "$WORKDIR/newer" "$(first_line "$time_sorted")" +reverse_time=$("$LS_BIN" -1tr "$WORKDIR/older" "$WORKDIR/newer") +assert_eq "$WORKDIR/older" "$(first_line "$reverse_time")" + +: > "$WORKDIR/same_a" +: > "$WORKDIR/same_b" +touch -t 202403030303 "$WORKDIR/same_a" "$WORKDIR/same_b" +reverse_ties=$("$LS_BIN" -1tr "$WORKDIR/same_a" "$WORKDIR/same_b") +assert_eq "$WORKDIR/same_b" "$(first_line "$reverse_ties")" +same_sort_flag=$("$LS_BIN" -1try "$WORKDIR/same_a" "$WORKDIR/same_b") +assert_eq "$WORKDIR/same_a" "$(first_line "$same_sort_flag")" +same_sort_env=$(LS_SAMESORT=1 "$LS_BIN" -1tr "$WORKDIR/same_a" "$WORKDIR/same_b") +assert_eq "$WORKDIR/same_a" "$(first_line "$same_sort_env")" + +formatted_time=$("$LS_BIN" -lD '%s' "$WORKDIR/tree/file.txt") +assert_contains "$WORKDIR/tree/file.txt" "$formatted_time" +case $formatted_time in + *" [0-9][0-9][0-9][0-9]"*) ;; + *) : ;; + esac +full_time=$("$LS_BIN" -lT "$WORKDIR/tree/file.txt") +assert_contains ":" "$full_time" + +uid=$(id -u) +gid=$(id -g) +numeric_long=$("$LS_BIN" -lnD '%s' "$WORKDIR/tree/file.txt") +assert_contains " $uid " "$numeric_long" +assert_contains " $gid " "$numeric_long" + +group_only=$("$LS_BIN" -lgnD '%s' "$WORKDIR/tree/file.txt") +assert_contains " $gid " "$group_only" +set -- $numeric_long +numeric_fields=$# +set -- $group_only +group_fields=$# +[ "$numeric_fields" -eq 7 ] || fail "-lnD field count mismatch" +[ "$group_fields" -eq 6 ] || fail "-lgnD field count mismatch" + +block_output=$("$LS_BIN" -1s "$WORKDIR/tree/file.txt") +case $block_output in + [0-9]*" $WORKDIR/tree/file.txt") ;; + *) fail "-s output malformed" ;; + esac + +single_column_blocks=$("$LS_BIN" -1s "$WORKDIR/tree") +assert_not_contains "total " "$single_column_blocks" + +column_blocks=$(COLUMNS=80 "$LS_BIN" -Cs "$WORKDIR/tree") +assert_contains "total " "$column_blocks" + +narrow_column_blocks=$(COLUMNS=8 "$LS_BIN" -Cs "$WORKDIR/tree") +assert_not_contains "total " "$narrow_column_blocks" + +stream_output=$(COLUMNS=20 "$LS_BIN" -m "$WORKDIR/tree") +assert_contains ", " "$stream_output" + +columns_output=$(COLUMNS=8 "$LS_BIN" -C "$WORKDIR/cols") +assert_contains "aa cc" "$columns_output" +assert_contains "bb dd" "$columns_output" + +across_output=$(COLUMNS=8 "$LS_BIN" -x "$WORKDIR/cols") +assert_contains "aa bb" "$across_output" +assert_contains "cc dd" "$across_output" + +group_first=$("$LS_BIN" -1 --group-directories=first "$WORKDIR/group") +assert_eq "a_dir" "$(first_line "$group_first")" +group_last=$("$LS_BIN" -1 --group-directories=last "$WORKDIR/group") +assert_eq "b_file" "$(first_line "$group_last")" + +color_always=$("$LS_BIN" --color=always -1 "$WORKDIR/group") +assert_contains "$(printf '\033[')" "$color_always" +color_never=$("$LS_BIN" --color=never -1 "$WORKDIR/group") +assert_not_contains "$(printf '\033[')" "$color_never" + +color_follow=$("$LS_BIN" --color=always -1 "$WORKDIR/tree/linkdir") +assert_contains "nested.txt" "$color_follow" + +u_output=$("$LS_BIN" -1U "$WORKDIR/tree/file.txt") +assert_eq "$WORKDIR/tree/file.txt" "$u_output" + +missing_stderr=$( + "$LS_BIN" "$WORKDIR/does-not-exist" 2>&1 >/dev/null || true +) +assert_contains "does-not-exist" "$missing_stderr" +assert_contains "No such file" "$missing_stderr" + +root_order=$("$LS_BIN" -1 "$WORKDIR/tree/subdir" "$WORKDIR/tree/file.txt") +assert_eq "$WORKDIR/tree/file.txt" "$(first_line "$root_order")" + +fast_mode=$("$LS_BIN" -1f "$WORKDIR/tree") +assert_contains ".hidden" "$fast_mode" + +unsupported_o=$( + "$LS_BIN" -o 2>&1 >/dev/null || true +) +assert_contains "option -o is not supported on Linux" "$unsupported_o" + +unsupported_W=$( + "$LS_BIN" -W 2>&1 >/dev/null || true +) +assert_contains "option -W is not supported on Linux" "$unsupported_W" + +unsupported_Z=$( + "$LS_BIN" -Z 2>&1 >/dev/null || true +) +assert_contains "option -Z is not supported on Linux" "$unsupported_Z" + +bad_color=$( + "$LS_BIN" --color=bogus 2>&1 >/dev/null || true +) +assert_contains "unsupported --color value 'bogus'" "$bad_color" + +bad_group=$( + "$LS_BIN" --group-directories=bogus 2>&1 >/dev/null || true +) +assert_contains "unsupported --group-directories value 'bogus'" "$bad_group" + +printf '%s\n' "PASS" diff --git a/corebinutils/ls/util.c b/corebinutils/ls/util.c new file mode 100644 index 0000000000..2cd73d51a8 --- /dev/null +++ b/corebinutils/ls/util.c @@ -0,0 +1,810 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1989, 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 + * Michael Fischbein. + * + * 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. + */ + +#include <sys/sysmacros.h> + +#include <ctype.h> +#include <errno.h> +#include <err.h> +#include <grp.h> +#include <inttypes.h> +#include <limits.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <wchar.h> +#include <wctype.h> + +#include "ls.h" +#include "extern.h" + +static int print_name_literal(const char *name); +static int print_name_printable(const char *name); +static int print_name_octal(const char *name, bool c_escape); +static size_t measure_name_literal(const char *name); +static size_t measure_name_printable(const char *name); +static size_t measure_name_octal(const char *name, bool c_escape); +static char *lookup_user(uid_t uid); +static char *lookup_group(gid_t gid); +static char *humanize_u64(uint64_t value); + +void * +xmalloc(size_t size) +{ + void *ptr; + + ptr = malloc(size); + if (ptr == NULL) + err(1, "malloc"); + return (ptr); +} + +void * +xreallocarray(void *ptr, size_t nmemb, size_t size) +{ + void *new_ptr; + + if (size != 0 && nmemb > SIZE_MAX / size) + errx(1, "allocation overflow"); + new_ptr = realloc(ptr, nmemb * size); + if (new_ptr == NULL) + err(1, "realloc"); + return (new_ptr); +} + +char * +xstrdup(const char *src) +{ + char *copy; + + copy = strdup(src); + if (copy == NULL) + err(1, "strdup"); + return (copy); +} + +char * +xasprintf(const char *fmt, ...) +{ + va_list ap; + char *buf; + int len; + + va_start(ap, fmt); + len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (len < 0) + errx(1, "vsnprintf"); + buf = xmalloc((size_t)len + 1); + va_start(ap, fmt); + if (vsnprintf(buf, (size_t)len + 1, fmt, ap) != len) { + va_end(ap); + errx(1, "vsnprintf"); + } + va_end(ap); + return (buf); +} + +char * +join_path(const char *parent, const char *name) +{ + size_t parent_len, name_len; + bool need_sep; + char *path; + + parent_len = strlen(parent); + name_len = strlen(name); + need_sep = parent_len != 0 && parent[parent_len - 1] != '/'; + path = xmalloc(parent_len + (need_sep ? 1 : 0) + name_len + 1); + memcpy(path, parent, parent_len); + if (need_sep) + path[parent_len++] = '/'; + memcpy(path + parent_len, name, name_len + 1); + return (path); +} + +void +free_entry(struct entry *entry) +{ + if (entry == NULL) + return; + free(entry->name); + free(entry->path); + free(entry->user); + free(entry->group); + free(entry->link_target); + memset(entry, 0, sizeof(*entry)); +} + +void +free_entry_list(struct entry_list *list) +{ + size_t i; + + if (list == NULL) + return; + for (i = 0; i < list->len; i++) + free_entry(&list->items[i]); + free(list->items); + list->items = NULL; + list->len = 0; + list->cap = 0; +} + +void +append_entry(struct entry_list *list, struct entry *entry) +{ + if (list->len == list->cap) { + list->cap = list->cap == 0 ? 16 : list->cap * 2; + list->items = xreallocarray(list->items, list->cap, + sizeof(*list->items)); + } + list->items[list->len++] = *entry; + memset(entry, 0, sizeof(*entry)); +} + +size_t +numeric_width(uintmax_t value) +{ + size_t width; + + for (width = 1; value >= 10; width++) + value /= 10; + return (width); +} + +static int +print_name_literal(const char *name) +{ + mbstate_t state; + wchar_t wc; + size_t clen; + int width; + int total; + + memset(&state, 0, sizeof(state)); + total = 0; + while ((clen = mbrtowc(&wc, name, MB_LEN_MAX, &state)) != 0) { + if (clen == (size_t)-1) { + memset(&state, 0, sizeof(state)); + putchar((unsigned char)*name++); + total++; + continue; + } + if (clen == (size_t)-2) { + total += printf("%s", name); + break; + } + fwrite(name, 1, clen, stdout); + name += clen; + width = wcwidth(wc); + if (width > 0) + total += width; + } + return (total); +} + +static int +print_name_printable(const char *name) +{ + mbstate_t state; + wchar_t wc; + size_t clen; + int width; + int total; + + memset(&state, 0, sizeof(state)); + total = 0; + while ((clen = mbrtowc(&wc, name, MB_LEN_MAX, &state)) != 0) { + if (clen == (size_t)-1 || clen == (size_t)-2) { + putchar('?'); + total++; + if (clen == (size_t)-1) { + memset(&state, 0, sizeof(state)); + name++; + } + break; + } + if (!iswprint(wc)) { + putchar('?'); + name += clen; + total++; + continue; + } + fwrite(name, 1, clen, stdout); + name += clen; + width = wcwidth(wc); + if (width > 0) + total += width; + } + return (total); +} + +static int +print_name_octal(const char *name, bool c_escape) +{ + static const char escapes[] = "\\\\\"\"\aa\bb\ff\nn\rr\tt\vv"; + mbstate_t state; + wchar_t wc; + size_t clen; + unsigned char ch; + const char *mapped; + int printable; + int total; + int i; + + memset(&state, 0, sizeof(state)); + total = 0; + while ((clen = mbrtowc(&wc, name, MB_LEN_MAX, &state)) != 0) { + printable = clen != (size_t)-1 && clen != (size_t)-2 && + iswprint(wc) && wc != L'\\' && wc != L'\"'; + if (printable) { + fwrite(name, 1, clen, stdout); + name += clen; + i = wcwidth(wc); + if (i > 0) + total += i; + continue; + } + if (c_escape && clen != (size_t)-1 && clen != (size_t)-2 && +#if WCHAR_MIN < 0 + wc >= 0 && +#endif + wc <= (wchar_t)UCHAR_MAX && + (mapped = strchr(escapes, (char)wc)) != NULL) { + putchar('\\'); + putchar(mapped[1]); + name += clen; + total += 2; + continue; + } + if (clen == (size_t)-2) + clen = strlen(name); + else if (clen == (size_t)-1) { + clen = 1; + memset(&state, 0, sizeof(state)); + } + for (i = 0; i < (int)clen; i++) { + ch = (unsigned char)name[i]; + printf("\\%03o", ch); + total += 4; + } + name += clen; + } + return (total); +} + +int +print_name(const struct options *opt, const char *name) +{ + switch (opt->name_mode) { + case NAME_PRINTABLE: + return (print_name_printable(name)); + case NAME_OCTAL: + return (print_name_octal(name, false)); + case NAME_ESCAPE: + return (print_name_octal(name, true)); + case NAME_LITERAL: + default: + return (print_name_literal(name)); + } +} + +static size_t +measure_name_literal(const char *name) +{ + mbstate_t state; + wchar_t wc; + size_t clen; + size_t total; + int width; + + memset(&state, 0, sizeof(state)); + total = 0; + while ((clen = mbrtowc(&wc, name, MB_LEN_MAX, &state)) != 0) { + if (clen == (size_t)-1) { + memset(&state, 0, sizeof(state)); + name++; + total++; + continue; + } + if (clen == (size_t)-2) { + total += strlen(name); + break; + } + name += clen; + width = wcwidth(wc); + if (width > 0) + total += (size_t)width; + } + return (total); +} + +static size_t +measure_name_printable(const char *name) +{ + mbstate_t state; + wchar_t wc; + size_t clen; + size_t total; + int width; + + memset(&state, 0, sizeof(state)); + total = 0; + while ((clen = mbrtowc(&wc, name, MB_LEN_MAX, &state)) != 0) { + if (clen == (size_t)-1 || clen == (size_t)-2) { + total++; + if (clen == (size_t)-1) { + memset(&state, 0, sizeof(state)); + name++; + } + break; + } + if (!iswprint(wc)) { + name += clen; + total++; + continue; + } + name += clen; + width = wcwidth(wc); + if (width > 0) + total += (size_t)width; + } + return (total); +} + +static size_t +measure_name_octal(const char *name, bool c_escape) +{ + static const char escapes[] = "\\\\\"\"\aa\bb\ff\nn\rr\tt\vv"; + mbstate_t state; + wchar_t wc; + size_t clen; + size_t total; + const char *mapped; + int width; + + memset(&state, 0, sizeof(state)); + total = 0; + while ((clen = mbrtowc(&wc, name, MB_LEN_MAX, &state)) != 0) { + if (clen == (size_t)-2) { + total += 4 * strlen(name); + break; + } + if (clen == (size_t)-1) { + memset(&state, 0, sizeof(state)); + name++; + total += 4; + continue; + } + if (iswprint(wc) && wc != L'\\' && wc != L'\"') { + width = wcwidth(wc); + if (width > 0) + total += (size_t)width; + name += clen; + continue; + } + if (c_escape && +#if WCHAR_MIN < 0 + wc >= 0 && +#endif + wc <= (wchar_t)UCHAR_MAX && + (mapped = strchr(escapes, (char)wc)) != NULL) { + name += clen; + total += 2; + continue; + } + name += clen; + total += 4 * clen; + } + return (total); +} + +size_t +measure_name(const struct options *opt, const char *name) +{ + switch (opt->name_mode) { + case NAME_PRINTABLE: + return (measure_name_printable(name)); + case NAME_OCTAL: + return (measure_name_octal(name, false)); + case NAME_ESCAPE: + return (measure_name_octal(name, true)); + case NAME_LITERAL: + default: + return (measure_name_literal(name)); + } +} + +char +file_type_char(const struct entry *entry, bool slash_only) +{ + mode_t mode; + + mode = entry->st.st_mode; + if (slash_only) + return (entry->is_dir ? '/' : '\0'); + if (entry->is_dir) + return ('/'); + if (S_ISFIFO(mode)) + return ('|'); + if (S_ISLNK(mode)) + return ('@'); + if (S_ISSOCK(mode)) + return ('='); + if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) + return ('*'); + return ('\0'); +} + +void +mode_string(const struct entry *entry, char buf[12]) +{ + mode_t mode; + + mode = entry->st.st_mode; + buf[0] = S_ISDIR(mode) ? 'd' : + S_ISLNK(mode) ? 'l' : + S_ISCHR(mode) ? 'c' : + S_ISBLK(mode) ? 'b' : + S_ISFIFO(mode) ? 'p' : + S_ISSOCK(mode) ? 's' : '-'; + buf[1] = (mode & S_IRUSR) ? 'r' : '-'; + buf[2] = (mode & S_IWUSR) ? 'w' : '-'; + buf[3] = (mode & S_ISUID) ? ((mode & S_IXUSR) ? 's' : 'S') : + ((mode & S_IXUSR) ? 'x' : '-'); + buf[4] = (mode & S_IRGRP) ? 'r' : '-'; + buf[5] = (mode & S_IWGRP) ? 'w' : '-'; + buf[6] = (mode & S_ISGID) ? ((mode & S_IXGRP) ? 's' : 'S') : + ((mode & S_IXGRP) ? 'x' : '-'); + buf[7] = (mode & S_IROTH) ? 'r' : '-'; + buf[8] = (mode & S_IWOTH) ? 'w' : '-'; + buf[9] = (mode & S_ISVTX) ? ((mode & S_IXOTH) ? 't' : 'T') : + ((mode & S_IXOTH) ? 'x' : '-'); + buf[10] = ' '; + buf[11] = '\0'; +} + +char * +format_uintmax(uintmax_t value, bool thousands) +{ + char tmp[64]; + size_t len; + size_t out_len; + size_t commas; + size_t i; + char *out; + + snprintf(tmp, sizeof(tmp), "%ju", value); + if (!thousands) + return (xstrdup(tmp)); + len = strlen(tmp); + commas = len > 0 ? (len - 1) / 3 : 0; + out_len = len + commas; + out = xmalloc(out_len + 1); + out[out_len] = '\0'; + for (i = 0; i < len; i++) { + out[out_len - 1 - i - i / 3] = tmp[len - 1 - i]; + if (i / 3 != (i + 1) / 3 && i + 1 < len) + out[out_len - 1 - i - i / 3 - 1] = ','; + } + return (out); +} + +char * +format_block_count(blkcnt_t blocks, long units, bool thousands) +{ + uintmax_t scaled; + + if (blocks < 0) + blocks = 0; + if (units <= 0) + units = 1; + scaled = ((uintmax_t)blocks + (uintmax_t)units - 1) / (uintmax_t)units; + return (format_uintmax(scaled, thousands)); +} + +static char * +humanize_u64(uint64_t value) +{ + static const char suffixes[] = "BKMGTPE"; + double scaled; + unsigned idx; + + scaled = (double)value; + idx = 0; + while (scaled >= 1024.0 && idx + 1 < sizeof(suffixes) - 1) { + scaled /= 1024.0; + idx++; + } + if (idx == 0) + return (xasprintf("%" PRIu64 "B", value)); + if (scaled >= 10.0) + return (xasprintf("%.0f%c", scaled, suffixes[idx])); + return (xasprintf("%.1f%c", scaled, suffixes[idx])); +} + +char * +format_entry_size(const struct entry *entry, bool human, bool thousands) +{ + uint64_t size; + + if (S_ISCHR(entry->st.st_mode) || S_ISBLK(entry->st.st_mode)) + return (xasprintf("%u,%u", major(entry->st.st_rdev), + minor(entry->st.st_rdev))); + size = entry->st.st_size < 0 ? 0 : (uint64_t)entry->st.st_size; + if (human) + return (humanize_u64(size)); + return (format_uintmax(size, thousands)); +} + +static char * +lookup_user(uid_t uid) +{ + struct passwd pwd; + struct passwd *result; + char *name; + char *buf; + long len; + int error; + + len = sysconf(_SC_GETPW_R_SIZE_MAX); + if (len < 1024) + len = 1024; + buf = xmalloc((size_t)len); + while ((error = getpwuid_r(uid, &pwd, buf, (size_t)len, &result)) == ERANGE) { + len *= 2; + buf = xreallocarray(buf, (size_t)len, 1); + } + if (error != 0 || result == NULL) { + free(buf); + return (format_uintmax(uid, false)); + } + name = xstrdup(pwd.pw_name); + free(buf); + return (name); +} + +static char * +lookup_group(gid_t gid) +{ + struct group grp; + struct group *result; + char *name; + char *buf; + long len; + int error; + + len = sysconf(_SC_GETGR_R_SIZE_MAX); + if (len < 1024) + len = 1024; + buf = xmalloc((size_t)len); + while ((error = getgrgid_r(gid, &grp, buf, (size_t)len, &result)) == ERANGE) { + len *= 2; + buf = xreallocarray(buf, (size_t)len, 1); + } + if (error != 0 || result == NULL) { + free(buf); + return (format_uintmax(gid, false)); + } + name = xstrdup(grp.gr_name); + free(buf); + return (name); +} + +int +ensure_owner_group(struct entry *entry, const struct options *opt) +{ + if (entry->user != NULL && entry->group != NULL) + return (0); + free(entry->user); + free(entry->group); + if (opt->numeric_ids) { + entry->user = format_uintmax(entry->st.st_uid, false); + entry->group = format_uintmax(entry->st.st_gid, false); + } else { + entry->user = lookup_user(entry->st.st_uid); + entry->group = lookup_group(entry->st.st_gid); + } + return (0); +} + +int +ensure_link_target(struct entry *entry) +{ + ssize_t len; + size_t cap; + char *buf; + + if (entry->link_target != NULL || !entry->is_symlink) + return (0); + cap = 128; + buf = xmalloc(cap); + for (;;) { + len = readlink(entry->path, buf, cap - 1); + if (len < 0) { + free(buf); + return (-1); + } + if ((size_t)len < cap - 1) { + buf[len] = '\0'; + entry->link_target = buf; + return (0); + } + cap *= 2; + buf = xreallocarray(buf, cap, 1); + } +} + +bool +selected_time(const struct context *ctx, const struct entry *entry, + struct timespec *ts) +{ + switch (ctx->opt.time_field) { + case TIME_ATIME: + *ts = entry->st.st_atim; + return (true); + case TIME_CTIME: + *ts = entry->st.st_ctim; + return (true); + case TIME_BTIME: + if (entry->btime.available) + *ts = entry->btime.ts; + else + *ts = entry->st.st_mtim; + return (true); + case TIME_MTIME: + default: + *ts = entry->st.st_mtim; + return (true); + } +} + +char * +format_entry_time(const struct context *ctx, const struct entry *entry) +{ + struct timespec ts; + struct tm tm; + char buf[128]; + const char *fmt; + bool recent; + + selected_time(ctx, entry, &ts); + if (localtime_r(&ts.tv_sec, &tm) == NULL) + return (xstrdup("<bad-time>")); + if (ctx->opt.time_format != NULL) + fmt = ctx->opt.time_format; + else if (ctx->opt.show_full_time) + fmt = "%b %e %H:%M:%S %Y"; + else { + recent = ts.tv_sec + (365 / 2) * 86400 > ctx->now && + ts.tv_sec < ctx->now + (365 / 2) * 86400; + fmt = recent ? "%b %e %H:%M" : "%b %e %Y"; + } + if (strftime(buf, sizeof(buf), fmt, &tm) == 0) + return (xstrdup("<bad-format>")); + return (xstrdup(buf)); +} + +const char * +color_start(const struct context *ctx, const struct entry *entry) +{ + mode_t mode; + + if (!ctx->opt.colorize) + return (""); + mode = entry->st.st_mode; + if (S_ISDIR(mode)) + return ("\033[01;34m"); + if (S_ISLNK(mode)) + return ("\033[01;36m"); + if (S_ISFIFO(mode)) + return ("\033[33m"); + if (S_ISSOCK(mode)) + return ("\033[01;35m"); + if (S_ISBLK(mode) || S_ISCHR(mode)) + return ("\033[01;33m"); + if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) + return ("\033[01;32m"); + return (""); +} + +const char * +color_end(const struct context *ctx) +{ + return (ctx->opt.colorize ? "\033[0m" : ""); +} + +int +natural_version_compare(const char *left, const char *right) +{ + const unsigned char *a; + const unsigned char *b; + + a = (const unsigned char *)left; + b = (const unsigned char *)right; + while (*a != '\0' || *b != '\0') { + if (isdigit(*a) && isdigit(*b)) { + const unsigned char *a0; + const unsigned char *b0; + size_t len_a; + size_t len_b; + int cmp; + + a0 = a; + b0 = b; + while (*a == '0') + a++; + while (*b == '0') + b++; + len_a = 0; + while (isdigit(a[len_a])) + len_a++; + len_b = 0; + while (isdigit(b[len_b])) + len_b++; + if (len_a != len_b) + return (len_a < len_b ? -1 : 1); + cmp = memcmp(a, b, len_a); + if (cmp != 0) + return (cmp < 0 ? -1 : 1); + while (isdigit(*a0)) + a0++; + while (isdigit(*b0)) + b0++; + if ((size_t)(a0 - (const unsigned char *)left) != + (size_t)(b0 - (const unsigned char *)right)) { + if ((a0 - a) != (b0 - b)) + return ((a0 - a) > (b0 - b) ? -1 : 1); + } + a = a0; + b = b0; + continue; + } + if (*a != *b) + return (*a < *b ? -1 : 1); + if (*a != '\0') + a++; + if (*b != '\0') + b++; + } + return (0); +} |
