diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:25:38 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:25:38 +0300 |
| commit | ad40f7111a3ec201820c1abc8fab25b07c9ef429 (patch) | |
| tree | c6b0222e5bd480aad5a8891493009babf620b8fc /corebinutils/getfacl | |
| parent | 5c7294b9667f0a38005272add8c78483b8658092 (diff) | |
| parent | 550119fbc3fe5fb6677c00484944354aa98f4b37 (diff) | |
| download | Project-Tick-ad40f7111a3ec201820c1abc8fab25b07c9ef429.tar.gz Project-Tick-ad40f7111a3ec201820c1abc8fab25b07c9ef429.zip | |
Add 'corebinutils/getfacl/' from commit '550119fbc3fe5fb6677c00484944354aa98f4b37'
git-subtree-dir: corebinutils/getfacl
git-subtree-mainline: 5c7294b9667f0a38005272add8c78483b8658092
git-subtree-split: 550119fbc3fe5fb6677c00484944354aa98f4b37
Diffstat (limited to 'corebinutils/getfacl')
| -rw-r--r-- | corebinutils/getfacl/.gitignore | 26 | ||||
| -rw-r--r-- | corebinutils/getfacl/GNUmakefile | 35 | ||||
| -rw-r--r-- | corebinutils/getfacl/LICENSE | 26 | ||||
| -rw-r--r-- | corebinutils/getfacl/LICENSES/BSD-2-Clause.txt | 9 | ||||
| -rw-r--r-- | corebinutils/getfacl/README.md | 41 | ||||
| -rw-r--r-- | corebinutils/getfacl/getfacl.1 | 108 | ||||
| -rw-r--r-- | corebinutils/getfacl/getfacl.c | 834 | ||||
| -rw-r--r-- | corebinutils/getfacl/tests/test.sh | 229 |
8 files changed, 1308 insertions, 0 deletions
diff --git a/corebinutils/getfacl/.gitignore b/corebinutils/getfacl/.gitignore new file mode 100644 index 0000000000..33ad7b7869 --- /dev/null +++ b/corebinutils/getfacl/.gitignore @@ -0,0 +1,26 @@ +*.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/ +.tmp-tests/ diff --git a/corebinutils/getfacl/GNUmakefile b/corebinutils/getfacl/GNUmakefile new file mode 100644 index 0000000000..05c11e2557 --- /dev/null +++ b/corebinutils/getfacl/GNUmakefile @@ -0,0 +1,35 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Wpedantic +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/getfacl +OBJS := $(OBJDIR)/getfacl.o + +.PHONY: all clean dirs test status + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/getfacl.o: $(CURDIR)/getfacl.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/getfacl.c" -o "$@" + +test: $(TARGET) + GETFACL_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(CURDIR)/build" "$(CURDIR)/out" "$(CURDIR)/.tmp-tests" diff --git a/corebinutils/getfacl/LICENSE b/corebinutils/getfacl/LICENSE new file mode 100644 index 0000000000..ccaeff6027 --- /dev/null +++ b/corebinutils/getfacl/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 1999, 2001, 2002 Robert N M Watson +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. + +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/getfacl/LICENSES/BSD-2-Clause.txt b/corebinutils/getfacl/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..5f662b354c --- /dev/null +++ b/corebinutils/getfacl/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +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. + +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/getfacl/README.md b/corebinutils/getfacl/README.md new file mode 100644 index 0000000000..a494ab44c2 --- /dev/null +++ b/corebinutils/getfacl/README.md @@ -0,0 +1,41 @@ +# getfacl + +Standalone musl-libc-friendly Linux port of FreeBSD `getfacl` 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 + +- FreeBSD `sys/acl.h` and `acl_*_np()` dependencies were removed. +- Linux-native ACL reads use `stat(2)` / `lstat(2)` plus `getxattr(2)` / `lgetxattr(2)`. +- POSIX ACL data is decoded directly from `system.posix_acl_access` and `system.posix_acl_default`. +- If no access ACL xattr exists, the tool synthesizes the base ACL from file mode bits instead of depending on a compat library. + +## Supported semantics on Linux + +- Access ACL output for regular files and directories. +- Default ACL output for directories when `system.posix_acl_default` is present. +- `-d`, `-h`, `-n`, `-q`, and `-s`. +- Reading path lists from standard input when no file operands are supplied or when `-` is used. + +## Unsupported semantics on Linux + +- FreeBSD NFSv4-oriented flags `-i` and `-v` have no Linux POSIX ACL equivalent in this port and fail with an explicit error. +- `-h` against a symbolic link fails with an explicit error because Linux does not support POSIX ACLs on symlink inodes. + +## Notes + +- The parser validates ACL xattr structure and rejects malformed data instead of silently printing partial output. +- Verified with `gmake -f GNUmakefile clean test` and `gmake -f GNUmakefile clean test CC=musl-gcc`. diff --git a/corebinutils/getfacl/getfacl.1 b/corebinutils/getfacl/getfacl.1 new file mode 100644 index 0000000000..74e5fa0e65 --- /dev/null +++ b/corebinutils/getfacl/getfacl.1 @@ -0,0 +1,108 @@ +.\"- +.\" Copyright (c) 2000, 2001, 2002 Robert N. M. Watson +.\" All rights reserved. +.\" +.\" Copyright (c) 2026 +.\" Project Tick. All rights reserved. +.\" +.\" This software was developed by Robert Watson for the TrustedBSD Project. +.\" +.\" 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. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 February 28, 2026 +.Dt GETFACL 1 +.Os +.Sh NAME +.Nm getfacl +.Nd show Linux POSIX ACL information +.Sh SYNOPSIS +.Nm +.Op Fl dhnqs +.Op Ar file ... +.Sh DESCRIPTION +The +.Nm +utility writes Linux POSIX ACL information for each +.Ar file +to standard output. +This port reads ACLs directly from the Linux xattrs +.Pa system.posix_acl_access +and +.Pa system.posix_acl_default . +If an access ACL xattr is absent, the base ACL is synthesized from the file +mode bits. +.Pp +The following options are available: +.Bl -tag -width indent +.It Fl d , Fl -default +Show the default ACL instead of the access ACL. +If no default ACL xattr exists, only the header is printed unless +.Fl q +or +.Fl s +suppresses output. +.It Fl h +Do not follow symbolic links. +On Linux, POSIX ACLs are not supported on symbolic link inodes, so this port +returns an explicit error when +.Fl h +targets a symbolic link. +.It Fl n , Fl -numeric +Display numeric user and group identifiers instead of resolving names. +.It Fl q , Fl -omit-header +Suppress the commented file, owner, and group header. +.It Fl s , Fl -skip-base +Skip access ACLs that are trivial and can be represented entirely by the file +mode bits. +For default ACLs, suppress output when no default ACL xattr exists. +.It Fl i +Unsupported on Linux in this port. +This flag was specific to FreeBSD NFSv4 ACL output and exits with an error. +.It Fl v +Unsupported on Linux in this port. +This flag was specific to FreeBSD NFSv4 ACL output and exits with an error. +.El +.Pp +If no +.Ar file +operand is provided, or if an operand is +.Fl - , +.Nm +reads pathnames from standard input, one per line. +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr chmod 1 , +.Xr setfacl 1 , +.Xr getxattr 2 , +.Xr lgetxattr 2 , +.Xr stat 2 , +.Xr xattr 7 +.Sh HISTORY +Extended Attribute and Access Control List support was developed as part of +the +.Tn TrustedBSD +Project. +This Linux port replaces the original FreeBSD ACL library dependencies with a +Linux-native xattr parser. +.Sh AUTHORS +.An Robert N M Watson diff --git a/corebinutils/getfacl/getfacl.c b/corebinutils/getfacl/getfacl.c new file mode 100644 index 0000000000..8e18e49f77 --- /dev/null +++ b/corebinutils/getfacl/getfacl.c @@ -0,0 +1,834 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1999, 2001, 2002 Robert N M Watson + * All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * This software was developed by Robert Watson for the TrustedBSD Project. + * + * 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + */ +/* + * Linux-native getfacl implementation. + * + * FreeBSD's original utility depends on sys/acl.h and acl_*_np interfaces. + * This port reads Linux POSIX ACLs directly from xattrs instead: + * - system.posix_acl_access + * - system.posix_acl_default + * + * The goal is a native Linux implementation that builds cleanly with musl + * without a FreeBSD ACL compatibility layer. + */ + +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/xattr.h> + +#include <endian.h> +#include <err.h> +#include <errno.h> +#include <getopt.h> +#include <grp.h> +#include <pwd.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#ifndef le16toh +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define le16toh(x) (x) +#define le32toh(x) (x) +#else +#define le16toh(x) __builtin_bswap16(x) +#define le32toh(x) __builtin_bswap32(x) +#endif +#endif + +#ifndef ENOATTR +#define ENOATTR ENODATA +#endif + +#define ACL_XATTR_ACCESS "system.posix_acl_access" +#define ACL_XATTR_DEFAULT "system.posix_acl_default" + +#define POSIX_ACL_XATTR_VERSION 0x0002U +#define ACL_UNDEFINED_ID ((uint32_t)-1) + +#define ACL_READ 0x04U +#define ACL_WRITE 0x02U +#define ACL_EXECUTE 0x01U + +#define ACL_USER_OBJ 0x01U +#define ACL_USER 0x02U +#define ACL_GROUP_OBJ 0x04U +#define ACL_GROUP 0x08U +#define ACL_MASK 0x10U +#define ACL_OTHER 0x20U + +struct posix_acl_xattr_entry_linux { + uint16_t e_tag; + uint16_t e_perm; + uint32_t e_id; +}; + +struct posix_acl_xattr_header_linux { + uint32_t a_version; +}; + +enum acl_kind { + ACL_KIND_ACCESS, + ACL_KIND_DEFAULT, +}; + +enum xattr_result { + XATTR_RESULT_ERROR = -1, + XATTR_RESULT_ABSENT = 0, + XATTR_RESULT_PRESENT = 1, +}; + +struct options { + bool default_acl; + bool no_follow; + bool numeric_ids; + bool omit_header; + bool skip_base; +}; + +struct acl_entry_linux { + uint16_t tag; + uint16_t perm; + uint32_t id; +}; + +struct acl_blob { + struct acl_entry_linux *entries; + size_t count; +}; + +struct acl_shape { + bool has_mask; + bool has_named_user; + bool has_named_group; + uint16_t user_obj_perm; + uint16_t group_obj_perm; + uint16_t other_perm; + uint16_t mask_perm; +}; + +struct output_state { + size_t sections_emitted; +}; + +static const struct option long_options[] = { + { "default", no_argument, NULL, 'd' }, + { "numeric", no_argument, NULL, 'n' }, + { "omit-header", no_argument, NULL, 'q' }, + { "skip-base", no_argument, NULL, 's' }, + { NULL, 0, NULL, 0 }, +}; + +static void usage(void) __attribute__((noreturn)); +static int process_path(const char *path, const struct options *opts, + struct output_state *state); +static int process_stdin(const struct options *opts, struct output_state *state); +static int stat_path(const char *path, bool no_follow, struct stat *st); +static int load_acl_xattr(const char *path, enum acl_kind kind, bool no_follow, + void **buf_out, size_t *size_out); +static int parse_acl_blob(const void *buf, size_t size, struct acl_blob *acl, + const char **error_out); +static void free_acl_blob(struct acl_blob *acl); +static int compare_acl_entries(const void *lhs, const void *rhs); +static int validate_acl_blob(const struct acl_blob *acl, struct acl_shape *shape, + const char **error_out); +static bool acl_is_trivial(const struct acl_shape *shape, mode_t mode); +static int emit_access_acl(const char *path, const struct stat *st, + const struct options *opts, const struct acl_blob *acl, + const struct acl_shape *shape, bool have_acl, struct output_state *state); +static int emit_default_acl(const char *path, const struct stat *st, + const struct options *opts, const struct acl_blob *acl, bool have_acl, + struct output_state *state); +static void begin_section_if_needed(struct output_state *state); +static void print_header(const char *path, const struct stat *st, + bool numeric_ids); +static void print_acl_entries(const struct acl_blob *acl, enum acl_kind kind, + bool numeric_ids); +static void print_synthesized_access_acl(mode_t mode); +static const char *tag_name(uint16_t tag); +static void perm_string(uint16_t perm, char out[4]); +static const char *format_uid(uid_t uid, bool numeric, char *buf, size_t buflen); +static const char *format_gid(gid_t gid, bool numeric, char *buf, size_t buflen); + +static void +usage(void) +{ + + fprintf(stderr, "usage: getfacl [-dhinqsv] [file ...]\n"); + exit(1); +} + +static int +stat_path(const char *path, bool no_follow, struct stat *st) +{ + + if (no_follow) + return (lstat(path, st)); + return (stat(path, st)); +} + +static int +load_acl_xattr(const char *path, enum acl_kind kind, bool no_follow, void **buf_out, + size_t *size_out) +{ + const char *name; + ssize_t size; + void *buf; + + *buf_out = NULL; + *size_out = 0; + name = (kind == ACL_KIND_DEFAULT) ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS; + + for (;;) { + if (no_follow) + size = lgetxattr(path, name, NULL, 0); + else + size = getxattr(path, name, NULL, 0); + if (size >= 0) + break; + if (errno == ENODATA || errno == ENOATTR || errno == ENOTSUP || + errno == EOPNOTSUPP) + return (XATTR_RESULT_ABSENT); + return (XATTR_RESULT_ERROR); + } + + if (size == 0) + return (XATTR_RESULT_ABSENT); + + buf = malloc((size_t)size); + if (buf == NULL) + err(1, "malloc"); + + for (;;) { + ssize_t nread; + + if (no_follow) + nread = lgetxattr(path, name, buf, (size_t)size); + else + nread = getxattr(path, name, buf, (size_t)size); + if (nread >= 0) { + *buf_out = buf; + *size_out = (size_t)nread; + return (XATTR_RESULT_PRESENT); + } + if (errno != ERANGE) { + free(buf); + if (errno == ENODATA || errno == ENOATTR || errno == ENOTSUP || + errno == EOPNOTSUPP) + return (XATTR_RESULT_ABSENT); + return (XATTR_RESULT_ERROR); + } + free(buf); + if (no_follow) + size = lgetxattr(path, name, NULL, 0); + else + size = getxattr(path, name, NULL, 0); + if (size < 0) { + if (errno == ENODATA || errno == ENOATTR || errno == ENOTSUP || + errno == EOPNOTSUPP) + return (XATTR_RESULT_ABSENT); + return (XATTR_RESULT_ERROR); + } + buf = malloc((size_t)size); + if (buf == NULL) + err(1, "malloc"); + } +} + +static int +compare_acl_entries(const void *lhs, const void *rhs) +{ + const struct acl_entry_linux *a, *b; + int order_a, order_b; + + a = lhs; + b = rhs; + + switch (a->tag) { + case ACL_USER_OBJ: + order_a = 0; + break; + case ACL_USER: + order_a = 1; + break; + case ACL_GROUP_OBJ: + order_a = 2; + break; + case ACL_GROUP: + order_a = 3; + break; + case ACL_MASK: + order_a = 4; + break; + case ACL_OTHER: + order_a = 5; + break; + default: + order_a = 6; + break; + } + + switch (b->tag) { + case ACL_USER_OBJ: + order_b = 0; + break; + case ACL_USER: + order_b = 1; + break; + case ACL_GROUP_OBJ: + order_b = 2; + break; + case ACL_GROUP: + order_b = 3; + break; + case ACL_MASK: + order_b = 4; + break; + case ACL_OTHER: + order_b = 5; + break; + default: + order_b = 6; + break; + } + + if (order_a != order_b) + return (order_a - order_b); + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + return (0); +} + +static int +parse_acl_blob(const void *buf, size_t size, struct acl_blob *acl, + const char **error_out) +{ + const struct posix_acl_xattr_header_linux *header; + const struct posix_acl_xattr_entry_linux *src; + size_t payload_size, count, i; + + memset(acl, 0, sizeof(*acl)); + + if (size < sizeof(*header)) { + *error_out = "truncated header"; + return (-1); + } + + header = buf; + if (le32toh(header->a_version) != POSIX_ACL_XATTR_VERSION) { + *error_out = "unsupported ACL xattr version"; + return (-1); + } + + payload_size = size - sizeof(*header); + if (payload_size % sizeof(*src) != 0) { + *error_out = "truncated ACL entry"; + return (-1); + } + + count = payload_size / sizeof(*src); + if (count == 0) { + *error_out = "empty ACL"; + return (-1); + } + + acl->entries = calloc(count, sizeof(*acl->entries)); + if (acl->entries == NULL) + err(1, "calloc"); + acl->count = count; + + src = (const struct posix_acl_xattr_entry_linux *)((const char *)buf + + sizeof(*header)); + for (i = 0; i < count; i++) { + acl->entries[i].tag = le16toh(src[i].e_tag); + acl->entries[i].perm = le16toh(src[i].e_perm); + acl->entries[i].id = le32toh(src[i].e_id); + } + + qsort(acl->entries, acl->count, sizeof(*acl->entries), compare_acl_entries); + return (0); +} + +static int +validate_acl_blob(const struct acl_blob *acl, struct acl_shape *shape, + const char **error_out) +{ + size_t i; + bool have_user_obj, have_group_obj, have_other; + + memset(shape, 0, sizeof(*shape)); + have_user_obj = false; + have_group_obj = false; + have_other = false; + + for (i = 0; i < acl->count; i++) { + const struct acl_entry_linux *entry; + + entry = &acl->entries[i]; + if ((entry->perm & ~(ACL_READ | ACL_WRITE | ACL_EXECUTE)) != 0) { + *error_out = "invalid permission bits"; + return (-1); + } + + switch (entry->tag) { + case ACL_USER_OBJ: + if (have_user_obj) { + *error_out = "duplicate user:: entry"; + return (-1); + } + have_user_obj = true; + shape->user_obj_perm = entry->perm; + break; + case ACL_USER: + if (entry->id == (uint32_t)ACL_UNDEFINED_ID) { + *error_out = "named user entry without id"; + return (-1); + } + if (i > 0 && acl->entries[i - 1].tag == ACL_USER && + acl->entries[i - 1].id == entry->id) { + *error_out = "duplicate named user entry"; + return (-1); + } + shape->has_named_user = true; + break; + case ACL_GROUP_OBJ: + if (have_group_obj) { + *error_out = "duplicate group:: entry"; + return (-1); + } + have_group_obj = true; + shape->group_obj_perm = entry->perm; + break; + case ACL_GROUP: + if (entry->id == (uint32_t)ACL_UNDEFINED_ID) { + *error_out = "named group entry without id"; + return (-1); + } + if (i > 0 && acl->entries[i - 1].tag == ACL_GROUP && + acl->entries[i - 1].id == entry->id) { + *error_out = "duplicate named group entry"; + return (-1); + } + shape->has_named_group = true; + break; + case ACL_MASK: + if (shape->has_mask) { + *error_out = "duplicate mask:: entry"; + return (-1); + } + shape->has_mask = true; + shape->mask_perm = entry->perm; + break; + case ACL_OTHER: + if (have_other) { + *error_out = "duplicate other:: entry"; + return (-1); + } + have_other = true; + shape->other_perm = entry->perm; + break; + default: + *error_out = "unknown ACL tag"; + return (-1); + } + } + + if (!have_user_obj || !have_group_obj || !have_other) { + *error_out = "missing required ACL entry"; + return (-1); + } + if ((shape->has_named_user || shape->has_named_group) && !shape->has_mask) { + *error_out = "named ACL entries require mask::"; + return (-1); + } + + return (0); +} + +static bool +acl_is_trivial(const struct acl_shape *shape, mode_t mode) +{ + uint16_t user_perm, group_perm, other_perm; + + if (shape->has_named_user || shape->has_named_group) + return (false); + + user_perm = (uint16_t)((mode >> 6) & 0x7); + group_perm = (uint16_t)((mode >> 3) & 0x7); + other_perm = (uint16_t)(mode & 0x7); + + if (shape->user_obj_perm != user_perm || + shape->group_obj_perm != group_perm || + shape->other_perm != other_perm) + return (false); + if (shape->has_mask && shape->mask_perm != group_perm) + return (false); + return (true); +} + +static void +free_acl_blob(struct acl_blob *acl) +{ + + free(acl->entries); + acl->entries = NULL; + acl->count = 0; +} + +static void +begin_section_if_needed(struct output_state *state) +{ + + if (state->sections_emitted > 0) + printf("\n"); + state->sections_emitted++; +} + +static const char * +format_uid(uid_t uid, bool numeric, char *buf, size_t buflen) +{ + struct passwd *pw; + + if (!numeric) { + pw = getpwuid(uid); + if (pw != NULL) + return (pw->pw_name); + } + snprintf(buf, buflen, "%lu", (unsigned long)uid); + return (buf); +} + +static const char * +format_gid(gid_t gid, bool numeric, char *buf, size_t buflen) +{ + struct group *gr; + + if (!numeric) { + gr = getgrgid(gid); + if (gr != NULL) + return (gr->gr_name); + } + snprintf(buf, buflen, "%lu", (unsigned long)gid); + return (buf); +} + +static void +print_header(const char *path, const struct stat *st, bool numeric_ids) +{ + char owner_buf[32], group_buf[32]; + + printf("# file: %s\n", path); + printf("# owner: %s\n", format_uid(st->st_uid, numeric_ids, owner_buf, + sizeof(owner_buf))); + printf("# group: %s\n", format_gid(st->st_gid, numeric_ids, group_buf, + sizeof(group_buf))); +} + +static void +perm_string(uint16_t perm, char out[4]) +{ + + out[0] = (perm & ACL_READ) ? 'r' : '-'; + out[1] = (perm & ACL_WRITE) ? 'w' : '-'; + out[2] = (perm & ACL_EXECUTE) ? 'x' : '-'; + out[3] = '\0'; +} + +static const char * +tag_name(uint16_t tag) +{ + + switch (tag) { + case ACL_USER_OBJ: + case ACL_USER: + return ("user"); + case ACL_GROUP_OBJ: + case ACL_GROUP: + return ("group"); + case ACL_MASK: + return ("mask"); + case ACL_OTHER: + return ("other"); + default: + return ("unknown"); + } +} + +static void +print_acl_entries(const struct acl_blob *acl, enum acl_kind kind, bool numeric_ids) +{ + size_t i; + + for (i = 0; i < acl->count; i++) { + const struct acl_entry_linux *entry; + char qualifier_buf[32], perms[4]; + const char *prefix, *qualifier; + + entry = &acl->entries[i]; + prefix = (kind == ACL_KIND_DEFAULT) ? "default:" : ""; + perm_string(entry->perm, perms); + + switch (entry->tag) { + case ACL_USER_OBJ: + case ACL_GROUP_OBJ: + case ACL_MASK: + case ACL_OTHER: + printf("%s%s::%s\n", prefix, tag_name(entry->tag), perms); + break; + case ACL_USER: + qualifier = format_uid((uid_t)entry->id, numeric_ids, + qualifier_buf, sizeof(qualifier_buf)); + printf("%suser:%s:%s\n", prefix, qualifier, perms); + break; + case ACL_GROUP: + qualifier = format_gid((gid_t)entry->id, numeric_ids, + qualifier_buf, sizeof(qualifier_buf)); + printf("%sgroup:%s:%s\n", prefix, qualifier, perms); + break; + default: + break; + } + } +} + +static void +print_synthesized_access_acl(mode_t mode) +{ + char perms[4]; + + perm_string((uint16_t)((mode >> 6) & 0x7), perms); + printf("user::%s\n", perms); + + perm_string((uint16_t)((mode >> 3) & 0x7), perms); + printf("group::%s\n", perms); + + perm_string((uint16_t)(mode & 0x7), perms); + printf("other::%s\n", perms); +} + +static int +emit_access_acl(const char *path, const struct stat *st, const struct options *opts, + const struct acl_blob *acl, const struct acl_shape *shape, bool have_acl, + struct output_state *state) +{ + bool trivial; + + trivial = !have_acl || acl_is_trivial(shape, st->st_mode); + if (opts->skip_base && trivial) + return (0); + + begin_section_if_needed(state); + if (!opts->omit_header) + print_header(path, st, opts->numeric_ids); + if (have_acl) + print_acl_entries(acl, ACL_KIND_ACCESS, opts->numeric_ids); + else + print_synthesized_access_acl(st->st_mode); + return (0); +} + +static int +emit_default_acl(const char *path, const struct stat *st, const struct options *opts, + const struct acl_blob *acl, bool have_acl, struct output_state *state) +{ + bool have_output; + + if (!have_acl && opts->skip_base) + return (0); + + have_output = !opts->omit_header || acl->count > 0; + if (!have_output) + return (0); + + begin_section_if_needed(state); + if (!opts->omit_header) + print_header(path, st, opts->numeric_ids); + if (acl->count > 0) + print_acl_entries(acl, ACL_KIND_DEFAULT, opts->numeric_ids); + return (0); +} + +static int +process_path(const char *path, const struct options *opts, struct output_state *state) +{ + struct stat st; + struct acl_blob acl; + struct acl_shape shape; + const char *parse_error; + void *raw_acl; + size_t raw_size; + int xattr_state; + + memset(&acl, 0, sizeof(acl)); + raw_acl = NULL; + raw_size = 0; + + if (stat_path(path, opts->no_follow, &st) == -1) { + warn("%s", path); + return (1); + } + + if (opts->no_follow && S_ISLNK(st.st_mode)) { + warnx("%s: symbolic link ACLs are not supported on Linux", path); + return (1); + } + + xattr_state = load_acl_xattr(path, + opts->default_acl ? ACL_KIND_DEFAULT : ACL_KIND_ACCESS, + opts->no_follow, &raw_acl, &raw_size); + if (xattr_state == XATTR_RESULT_ERROR) { + warn("%s", path); + return (1); + } + + if (xattr_state == XATTR_RESULT_PRESENT) { + if (parse_acl_blob(raw_acl, raw_size, &acl, &parse_error) != 0) { + warnx("%s: invalid POSIX ACL xattr: %s", path, parse_error); + free(raw_acl); + return (1); + } + if (validate_acl_blob(&acl, &shape, &parse_error) != 0) { + warnx("%s: invalid POSIX ACL xattr: %s", path, parse_error); + free(raw_acl); + free_acl_blob(&acl); + return (1); + } + } + free(raw_acl); + + if (opts->default_acl) + xattr_state = emit_default_acl(path, &st, opts, &acl, + xattr_state == XATTR_RESULT_PRESENT, state); + else + xattr_state = emit_access_acl(path, &st, opts, &acl, &shape, + xattr_state == XATTR_RESULT_PRESENT, state); + + free_acl_blob(&acl); + return (xattr_state); +} + +static int +process_stdin(const struct options *opts, struct output_state *state) +{ + char *line; + size_t cap; + ssize_t len; + int status; + + line = NULL; + cap = 0; + status = 0; + + while ((len = getline(&line, &cap, stdin)) != -1) { + if (len > 0 && line[len - 1] == '\n') + line[len - 1] = '\0'; + if (line[0] == '\0') { + warnx("stdin: empty pathname"); + status = 1; + continue; + } + if (process_path(line, opts, state) != 0) + status = 1; + } + + if (ferror(stdin)) { + warn("stdin"); + status = 1; + } + + free(line); + return (status); +} + +int +main(int argc, char *argv[]) +{ + struct options opts; + struct output_state state; + int ch, i, status; + + memset(&opts, 0, sizeof(opts)); + memset(&state, 0, sizeof(state)); + opterr = 0; + + while ((ch = getopt_long(argc, argv, "+dhinqsv", long_options, NULL)) != -1) { + switch (ch) { + case 'd': + opts.default_acl = true; + break; + case 'h': + opts.no_follow = true; + break; + case 'i': + errx(1, "option -i is not supported on Linux"); + case 'n': + opts.numeric_ids = true; + break; + case 'q': + opts.omit_header = true; + break; + case 's': + opts.skip_base = true; + break; + case 'v': + errx(1, "option -v is not supported on Linux"); + case '?': + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + if (argc == 0) + return (process_stdin(&opts, &state)); + + status = 0; + for (i = 0; i < argc; i++) { + if (strcmp(argv[i], "-") == 0) { + if (process_stdin(&opts, &state) != 0) + status = 1; + continue; + } + if (process_path(argv[i], &opts, &state) != 0) + status = 1; + } + + return (status); +} diff --git a/corebinutils/getfacl/tests/test.sh b/corebinutils/getfacl/tests/test.sh new file mode 100644 index 0000000000..ae22edbb81 --- /dev/null +++ b/corebinutils/getfacl/tests/test.sh @@ -0,0 +1,229 @@ +#!/bin/sh +set -eu + +: "${GETFACL_BIN:?GETFACL_BIN is required}" + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +TMPBASE="$ROOT/.tmp-tests" +mkdir -p "$TMPBASE" +WORKDIR=$(mktemp -d "$TMPBASE/getfacl-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_match() { + pattern=$1 + text=$2 + message=$3 + printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$message" +} + +assert_not_match() { + pattern=$1 + text=$2 + message=$3 + if printf '%s\n' "$text" | grep -Eq "$pattern"; then + fail "$message" + fi +} + +assert_empty() { + text=$1 + message=$2 + [ -z "$text" ] || fail "$message" +} + +assert_eq() { + expected=$1 + actual=$2 + message=$3 + [ "$expected" = "$actual" ] || { + printf '%s\n' "FAIL: $message" >&2 + printf '%s\n' "--- expected ---" >&2 + printf '%s\n' "$expected" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s\n' "$actual" >&2 + exit 1 + } +} + +run_cmd() { + : >"$STDOUT_FILE" + : >"$STDERR_FILE" + set +e + "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE" + CMD_STATUS=$? + set -e + CMD_STDOUT=$(cat "$STDOUT_FILE") + CMD_STDERR=$(cat "$STDERR_FILE") +} + +run_stdin_cmd() { + input=$1 + shift + : >"$STDOUT_FILE" + : >"$STDERR_FILE" + set +e + printf '%s' "$input" | "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE" + CMD_STATUS=$? + set -e + CMD_STDOUT=$(cat "$STDOUT_FILE") + CMD_STDERR=$(cat "$STDERR_FILE") +} + +acl_body() { + printf '%s\n' "$1" | sed '/^#/d;/^$/d' +} + +normalize_default_body() { + printf '%s\n' "$1" | sed 's/^default://' +} + +[ -x "$GETFACL_BIN" ] || fail "missing binary: $GETFACL_BIN" + +run_cmd "$GETFACL_BIN" -z +[ "$CMD_STATUS" -eq 1 ] || fail "invalid option should exit 1" +assert_match '^usage: getfacl ' "$CMD_STDERR" "usage output missing for invalid option" + +touch "$WORKDIR/file" +chmod 640 "$WORKDIR/file" + +run_cmd "$GETFACL_BIN" "$WORKDIR/file" +[ "$CMD_STATUS" -eq 0 ] || fail "base ACL read failed" +assert_match "^# file: $WORKDIR/file$" "$CMD_STDOUT" "missing file header" +assert_match '^# owner: ' "$CMD_STDOUT" "missing owner header" +assert_match '^# group: ' "$CMD_STDOUT" "missing group header" +assert_match '^user::rw-$' "$CMD_STDOUT" "missing user base entry" +assert_match '^group::r--$' "$CMD_STDOUT" "missing group base entry" +assert_match '^other::---$' "$CMD_STDOUT" "missing other base entry" +base_output=$CMD_STDOUT + +run_cmd "$GETFACL_BIN" -h "$WORKDIR/file" +[ "$CMD_STATUS" -eq 0 ] || fail "-h should work on regular files" +assert_eq "$(acl_body "$base_output")" "$(acl_body "$CMD_STDOUT")" "-h should not change regular-file ACL body" + +run_cmd "$GETFACL_BIN" -q "$WORKDIR/file" +[ "$CMD_STATUS" -eq 0 ] || fail "quiet ACL read failed" +assert_not_match '^# file:' "$CMD_STDOUT" "header was not omitted" +assert_match '^user::rw-$' "$CMD_STDOUT" "quiet output lost ACL data" + +run_cmd "$GETFACL_BIN" -s -q "$WORKDIR/file" +[ "$CMD_STATUS" -eq 0 ] || fail "skip-base should not fail on trivial ACL" +assert_empty "$CMD_STDOUT" "skip-base should suppress trivial ACLs" +assert_empty "$CMD_STDERR" "skip-base should not warn for trivial ACL" + +run_cmd "$GETFACL_BIN" -n "$WORKDIR/file" +[ "$CMD_STATUS" -eq 0 ] || fail "numeric ACL read failed" +assert_match "^# owner: $(id -u)$" "$CMD_STDOUT" "numeric owner header missing" +assert_match "^# group: $(id -g)$" "$CMD_STDOUT" "numeric group header missing" + +run_stdin_cmd "$(printf '%s\n%s\n' "$WORKDIR/file" "$WORKDIR/file")" "$GETFACL_BIN" -q +[ "$CMD_STATUS" -eq 0 ] || fail "stdin path processing failed" +count=$(printf '%s\n' "$CMD_STDOUT" | grep -c '^user::rw-$') +[ "$count" -eq 2 ] || fail "stdin path processing did not emit two ACL blocks" +assert_match '^$' "$CMD_STDOUT" "multiple stdin paths should be separated by a blank line" + +run_stdin_cmd "$(printf '%s\n\n%s\n' "$WORKDIR/file" "$WORKDIR/file")" "$GETFACL_BIN" -q +[ "$CMD_STATUS" -eq 1 ] || fail "empty stdin pathname should fail" +assert_match '^getfacl: stdin: empty pathname$' "$CMD_STDERR" "empty stdin pathname error missing" +count=$(printf '%s\n' "$CMD_STDOUT" | grep -c '^user::rw-$') +[ "$count" -eq 2 ] || fail "stdin processing should continue after empty pathname" + +run_cmd "$GETFACL_BIN" "$WORKDIR/missing" +[ "$CMD_STATUS" -eq 1 ] || fail "missing file should exit 1" +assert_match "^getfacl: $WORKDIR/missing: " "$CMD_STDERR" "missing file error not reported" + +run_cmd "$GETFACL_BIN" "$WORKDIR/file" "$WORKDIR/missing" +[ "$CMD_STATUS" -eq 1 ] || fail "mixed success/failure operands should exit 1" +assert_match '^user::rw-$' "$CMD_STDOUT" "successful operand output missing in mixed run" +assert_match "^getfacl: $WORKDIR/missing: " "$CMD_STDERR" "mixed operand error missing" + +run_cmd "$GETFACL_BIN" -i "$WORKDIR/file" +[ "$CMD_STATUS" -eq 1 ] || fail "unsupported -i should exit 1" +assert_eq "getfacl: option -i is not supported on Linux" "$CMD_STDERR" "unsupported -i check missing" + +run_cmd "$GETFACL_BIN" -v "$WORKDIR/file" +[ "$CMD_STATUS" -eq 1 ] || fail "unsupported -v should exit 1" +assert_eq "getfacl: option -v is not supported on Linux" "$CMD_STDERR" "unsupported -v check missing" + +ln -s "$WORKDIR/file" "$WORKDIR/link" +run_cmd "$GETFACL_BIN" -h "$WORKDIR/link" +[ "$CMD_STATUS" -eq 1 ] || fail "symlink -h should fail on Linux" +assert_eq "getfacl: $WORKDIR/link: symbolic link ACLs are not supported on Linux" "$CMD_STDERR" "symlink -h error missing" + +mkdir "$WORKDIR/plain-dir" +run_cmd "$GETFACL_BIN" -d "$WORKDIR/plain-dir" +[ "$CMD_STATUS" -eq 0 ] || fail "default ACL query without default xattr should succeed" +assert_match "^# file: $WORKDIR/plain-dir$" "$CMD_STDOUT" "default ACL header missing" +assert_not_match '^default:' "$CMD_STDOUT" "unexpected default ACL entries on plain dir" + +run_cmd "$GETFACL_BIN" -d -q "$WORKDIR/plain-dir" +[ "$CMD_STATUS" -eq 0 ] || fail "quiet default ACL query without xattr should succeed" +assert_empty "$CMD_STDOUT" "quiet default ACL output without xattr should be empty" + +run_cmd "$GETFACL_BIN" -d -s -q "$WORKDIR/plain-dir" +[ "$CMD_STATUS" -eq 0 ] || fail "skip-base default ACL query without xattr should succeed" +assert_empty "$CMD_STDOUT" "skip-base should suppress missing default ACL output" + +acl_supported=0 +if command -v setfacl >/dev/null 2>&1; then + if setfacl -m m::r-- "$WORKDIR/file" 2>/dev/null; then + acl_supported=1 + fi +fi + +if [ "$acl_supported" -eq 1 ]; then + run_cmd "$GETFACL_BIN" "$WORKDIR/file" + [ "$CMD_STATUS" -eq 0 ] || fail "extended ACL read failed" + assert_match '^mask::r--$' "$CMD_STDOUT" "extended ACL mask missing" + + setfacl -m "u:$(id -u):rw-" "$WORKDIR/file" + setfacl -m "g:$(id -g):r--" "$WORKDIR/file" + + run_cmd "$GETFACL_BIN" -n "$WORKDIR/file" + [ "$CMD_STATUS" -eq 0 ] || fail "numeric named ACL read failed" + assert_match "^user:$(id -u):rw-$" "$CMD_STDOUT" "named user ACL missing" + assert_match "^group:$(id -g):r--$" "$CMD_STDOUT" "named group ACL missing" + assert_match '^mask::rw-$' "$CMD_STDOUT" "mask was not updated after named ACL" + + run_cmd "$GETFACL_BIN" "$WORKDIR/file" + [ "$CMD_STATUS" -eq 0 ] || fail "named ACL read failed" + assert_match '^user:[^:][^:]*:rw-$' "$CMD_STDOUT" "named user should resolve to a name" + assert_match '^group:[^:][^:]*:r--$' "$CMD_STDOUT" "named group should resolve to a name" + + mkdir "$WORKDIR/dir" + setfacl -d -m u::rwx,g::r-x,o::---,m::r-x,u:$(id -u):rwx "$WORKDIR/dir" + + run_cmd "$GETFACL_BIN" -d "$WORKDIR/dir" + [ "$CMD_STATUS" -eq 0 ] || fail "default ACL read failed" + assert_match '^default:user::rwx$' "$CMD_STDOUT" "default user entry missing" + assert_match "^default:user:[^:][^:]*:rwx$" "$CMD_STDOUT" "default named user entry missing" + assert_match '^default:group::r-x$' "$CMD_STDOUT" "default group entry missing" + assert_match '^default:mask::r-x$' "$CMD_STDOUT" "default mask entry missing" + assert_match '^default:other::---$' "$CMD_STDOUT" "default other entry missing" + + run_cmd "$GETFACL_BIN" -n -d "$WORKDIR/dir" + [ "$CMD_STATUS" -eq 0 ] || fail "numeric default ACL read failed" + assert_match "^default:user:$(id -u):rwx$" "$CMD_STDOUT" "numeric default named user entry missing" + + if command -v getfacl >/dev/null 2>&1; then + run_cmd "$GETFACL_BIN" -n -q "$WORKDIR/file" + ours_access=$(acl_body "$CMD_STDOUT") + system_access=$(getfacl -E -p -n "$WORKDIR/file" 2>/dev/null | sed '/^#/d;/^$/d') + assert_eq "$system_access" "$ours_access" "access ACL body diverges from system getfacl" + + run_cmd "$GETFACL_BIN" -n -q -d "$WORKDIR/dir" + ours_default=$(normalize_default_body "$(acl_body "$CMD_STDOUT")") + system_default=$(getfacl -E -p -n -d "$WORKDIR/dir" 2>/dev/null | sed '/^#/d;/^$/d') + assert_eq "$system_default" "$ours_default" "default ACL body diverges from system getfacl" + fi +else + printf '%s\n' "SKIP: extended ACL checks (setfacl unavailable or filesystem lacks ACL support)" >&2 +fi + +printf '%s\n' "PASS" |
