tests: add ncpfs directory quota smoke
All checks were successful
Source release / source-package (push) Successful in 1m7s

This commit is contained in:
Mario Fetka
2026-06-11 15:36:09 +00:00
parent 3091044de4
commit db9c541960
8 changed files with 429 additions and 52 deletions

2
AI.md
View File

@@ -1,4 +1,6 @@
Patch 0353 status: added live NCPFS directory-quota smoke for 3.x endpoints. `nwfs_ncpfs_dirquota` now drives decimal `22/36`/wire `0x24` and decimal `22/35`/wire `0x23` directly through libncp `NCPC_SFN`, with readback/expect modes. `nwfs_ncpfs_dirquota_smoke.sh` sets a limit, reads it back over NCP, verifies `netware.metadata`, clears it, and verifies that `22/35` reports no entries.
Patch 0351 status: started closing the MARS-NWE 3.x directory-quota block before namespace work. Added libnwfs `dirquota.c/h`, CTest `nwfs_dirquota_test`, active NCP `22/35`/wire `0x23` get and `22/36`/wire `0x24` set backed by `netware.metadata.nwm_quota_limit`, and fixed `22/40`/wire `0x28` Sequence parsing to Lo-Hi. Code comments name both decimal NCP numbers and wire hex bytes. Remaining directory-quota work is enforcement/adjustment on file growth/create/delete/rename and later `87/39` behind the 4.x line.
# AI working notes for mars-nwe

View File

@@ -3410,3 +3410,15 @@ The first NetWare-3.x directory-quota pass is active as of patch 0351. Document
`src/nwfs/quota/dirquota.c` is the libnwfs home for portable directory-quota math and NSS/OES-compatible `netware.metadata.nwm_quota_limit` conversion. The live NCP parser stays in `src/nwconn.c`. `22/35` returns no entries for unrestricted directories and one current-directory entry for a finite restriction. `22/36` stores or clears the directory restriction in `netware.metadata`. `22/40` now reads its sequence field as documented Lo-Hi and continues to use the existing MARS DOS scan reply shape until AFP/MAC_RF resource fork data exists.
This closes the first 3.x directory-quota endpoint gap without pulling in the full NSS `dirQuotas.c` runtime/cache. Deeper NSS-style parent-min restriction selection, used-space adjustment hooks on create/delete/rename/write, and namespace-aware `87/39` remain follow-up work.
### NCPFS directory-quota smoke
The directory-quota implementation now has two test layers. The CTest helper
`nwfs_dirquota_test` validates the libnwfs arithmetic and metadata conversion.
The live smoke `nwfs_ncpfs_dirquota_smoke.sh` validates the NetWare 3.x NCP
path: it uses libncp `NCPC_SFN(22, 36)` to send decimal `22/36` (wire/code
`0x24`) and `NCPC_SFN(22, 35)` for decimal `22/35` (wire/code `0x23`). The
smoke intentionally verifies both the NCP readback and the host-side
`netware.metadata` result, so parser/wire mistakes and xattr-storage mistakes
are caught independently.

13
TODO.md
View File

@@ -2507,3 +2507,16 @@ Do not merge these back together; a later BSD backend should use its own
- Keep Linuxquota, NWQUOTA, and future BSD quota backends in separate files with
separate public prefixes: `nwfs_lnxquota_*`, `nwfs_nwquota_*`, and future
`nwfs_bsdquota_*`.
### Directory quota live smoke
- Added/keep `tests/nwfs/nwfs_ncpfs_dirquota_smoke.sh` for the 3.x directory
quota endpoints. The helper must name the documented decimal calls and the
code/wire hex values together:
- decimal `22/36`, wire/code `0x24`: set directory disk-space restriction
- decimal `22/35`, wire/code `0x23`: get directory disk-space restriction
- The smoke should stay a live NCPFS test, not just a host xattr test: set over
NCP, read over NCP, then verify `netware.metadata` on the host side.
- `22/40` remains the next directory-quota smoke target once the scan reply
is verified against the documented structure.

View File

@@ -99,3 +99,14 @@ The first active implementation stores finite directory restrictions in
`netware.metadata.nwm_quota_limit`. Screenshots from FILER, SYSCON, NWADMIN or
reference NetWare servers that demonstrate directory quota behaviour belong
under this `doc/quota/` tree.
## Directory quota smoke
`tests/nwfs/nwfs_ncpfs_dirquota_smoke.sh` is the live smoke for the NetWare
3.x directory disk-space restriction calls. It sets a limit with documented
decimal `22/36` (wire/code `0x24`), reads it back with documented decimal
`22/35` (wire/code `0x23`), verifies `netware.metadata`, clears the limit, and
checks that `22/35` returns no entries again. Future screenshots from FILER,
SYSCON, or NetWare reference systems for this behavior belong under
`doc/quota/screenshots/`.

View File

@@ -85,3 +85,23 @@ Tests must therefore not validate salvage payloads by opening
`SYS:.recycle/...` or `SYS:.salvage/...` through normal NCP file calls. Use the
salvage scan/recover/purge endpoints for repository state and verify payload
content by reading the restored live file through NCP.
## NCPFS directory-quota smoke
`tests/nwfs/nwfs_ncpfs_dirquota_smoke.sh` is the live smoke for the
MARS-NWE 3.x directory quota endpoints. It uses the `nwfs_ncpfs_dirquota`
helper to call the documented decimal NCPs `22/36` and `22/35` directly
through libncp (`NCPC_SFN(22, 36)` and `NCPC_SFN(22, 35)`; wire/code bytes
`0x24` and `0x23`). The smoke sets a finite directory limit, reads it back,
checks `netware.metadata` with `nwfs_xattr_dump`, clears the limit, and
verifies that `22/35` reports no entries again.
Example:
```sh
./nwfs_ncpfs_dirquota_smoke.sh MARS SUPERVISOR secret \
/var/mars_nwe/SYS /mnt/nw-sys NWFSTEST SYS
```
Use `NWFS_NCPFS_DIR_QUOTA_4K=VALUE` to choose the tested limit.

View File

@@ -63,12 +63,15 @@ configure_file(nwfs_ncpfs_novell_quota_reference.sh.in nwfs_ncpfs_novell_quota_r
configure_file(nwfs_ncpfs_userquota_fill_smoke.sh.in nwfs_ncpfs_userquota_fill_smoke.sh @ONLY)
configure_file(nwfs_ncpfs_userquota_dual_smoke.sh.in nwfs_ncpfs_userquota_dual_smoke.sh @ONLY)
configure_file(nwfs_ncpfs_dirquota_smoke.sh.in nwfs_ncpfs_dirquota_smoke.sh @ONLY)
set(NWFS_NCPFS_DIRQUOTA_SMOKE "${CMAKE_CURRENT_BINARY_DIR}/nwfs_ncpfs_dirquota_smoke.sh")
foreach(NWFS_NCPFS_SMOKE_SCRIPT
nwfs_ncpfs_metadata_smoke.sh
nwfs_ncpfs_novell_quota_reference.sh
nwfs_ncpfs_userquota_fill_smoke.sh
nwfs_ncpfs_userquota_dual_smoke.sh)
nwfs_ncpfs_userquota_dual_smoke.sh
nwfs_ncpfs_dirquota_smoke.sh)
file(CHMOD "${CMAKE_CURRENT_BINARY_DIR}/${NWFS_NCPFS_SMOKE_SCRIPT}"
PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE

View File

@@ -1,14 +1,14 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Manual NCPFS smoke helper for NetWare directory space limits.
* Manual NCPFS smoke helper for NetWare 3.x directory disk-space limits.
*
* This is intentionally small and is based on the ncpfs contrib/testing
* dirlimit.c example by Petr Vandrovec. It mutates a directory through
* libncp/NCPFS so the host-side test can verify the mars-nwe
* netware.metadata directory-quota xattr afterwards.
* The NCP documentation names these as decimal 22/35 and 22/36. libncp's
* NCPC_SFN(22, 35/36) builds the same group/subfunction request that mars-nwe
* dispatches as wire/code bytes 0x23 and 0x24 in src/nwconn.c.
*/
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -17,14 +17,108 @@
#include <ncp/nwcalls.h>
#include <ncp/nwnet.h>
#ifndef NCPC_SFN
#define NCPC_SUBFUNCTION 0x10000U
#define NCPC_SFN(fn, subfn) (NCPC_SUBFUNCTION | (((fn) & 0xffU) << 8) | ((subfn) & 0xffU))
#endif
struct dirquota_entry {
uint8_t level;
uint32_t max4k;
uint32_t current4k;
};
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [-l LIMIT_4K] [-p VOLUME:PATH] NCPFS_MOUNTED_PATH\n"
"Usage: %s [--set-limit-4k LIMIT] [--get] [--expect-limit-4k LIMIT|--expect-empty] [-p VOLUME:PATH] NCPFS_MOUNTED_PATH\n"
" %s -l LIMIT NCPFS_MOUNTED_PATH\n"
"\n"
"Sets the NetWare DOS-info MaximumSpace field through libncp.\n"
"LIMIT_4K is in 4 KiB NetWare blocks, matching ncpfs dirlimit.c.\n",
prog);
"Exercises NetWare directory disk-space NCPs through libncp:\n"
" decimal 22/35 / wire 0x23: Get Directory Disk Space Restriction\n"
" decimal 22/36 / wire 0x24: Set Directory Disk Space Restriction\n"
"LIMIT is in 4 KiB NetWare blocks. LIMIT 0 clears the restriction.\n",
prog, prog);
}
static int parse_u32(const char *text, uint32_t *value)
{
char *end = NULL;
unsigned long v;
errno = 0;
v = strtoul(text, &end, 0);
if (errno || !end || *end || v > 0xffffffffUL)
return -1;
*value = (uint32_t)v;
return 0;
}
static void put_u32_le(uint8_t *p, uint32_t value)
{
p[0] = (uint8_t)(value & 0xffU);
p[1] = (uint8_t)((value >> 8) & 0xffU);
p[2] = (uint8_t)((value >> 16) & 0xffU);
p[3] = (uint8_t)((value >> 24) & 0xffU);
}
static uint32_t get_u32_le(const uint8_t *p)
{
return ((uint32_t)p[0]) |
((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
static NWCCODE ncp22_36_set_dirquota(NWCONN_HANDLE conn,
NWDIR_HANDLE dirhandle,
uint32_t limit4k)
{
uint8_t rq[5];
rq[0] = (uint8_t)dirhandle;
put_u32_le(rq + 1, limit4k);
return NWRequestSimple(conn, NCPC_SFN(22, 36), rq, sizeof(rq), NULL);
}
static NWCCODE ncp22_35_get_dirquota(NWCONN_HANDLE conn,
NWDIR_HANDLE dirhandle,
struct dirquota_entry *entries,
size_t entry_cap,
size_t *entry_count)
{
uint8_t rq[1];
uint8_t rpbuf[1024];
NW_FRAGMENT rp;
NWCCODE err;
size_t count;
size_t i;
rq[0] = (uint8_t)dirhandle;
memset(rpbuf, 0, sizeof(rpbuf));
rp.fragAddr.rw = rpbuf;
rp.fragSize = sizeof(rpbuf);
err = NWRequestSimple(conn, NCPC_SFN(22, 35), rq, sizeof(rq), &rp);
if (err)
return err;
if (rp.fragSize < 1)
return 0xff;
count = rpbuf[0];
if (count > entry_cap)
count = entry_cap;
if (rp.fragSize < 1 + count * 9)
return 0xff;
for (i = 0; i < count; i++) {
const uint8_t *p = rpbuf + 1 + i * 9;
entries[i].level = p[0];
entries[i].max4k = get_u32_le(p + 1);
entries[i].current4k = get_u32_le(p + 5);
}
*entry_count = count;
return 0;
}
int main(int argc, char **argv)
@@ -38,46 +132,67 @@ int main(int argc, char **argv)
NWDIR_HANDLE dirhandle = 0;
NWVOL_NUM volnum = 0;
const char *override_path = NULL;
const char *mounted_path;
unsigned long limit = 0;
int set_limit = 0;
int opt;
const char *mounted_path = NULL;
uint32_t set_limit = 0;
uint32_t expect_limit = 0;
int do_set = 0;
int do_get = 0;
int do_expect_limit = 0;
int do_expect_empty = 0;
int i;
int len;
struct dirquota_entry entries[16];
size_t entry_count = 0;
if (NWCallsInit(NULL, NULL)) {
fprintf(stderr, "NWCallsInit failed\n");
return 2;
}
while ((opt = getopt(argc, argv, "h?p:l:")) != -1) {
switch (opt) {
case 'l':
errno = 0;
limit = strtoul(optarg, NULL, 0);
if (errno || limit > 0xffffffffUL) {
fprintf(stderr, "invalid limit: %s\n", optarg);
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
usage(argv[0]);
return 0;
} else if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--set-limit-4k")) {
if (++i >= argc || parse_u32(argv[i], &set_limit)) {
fprintf(stderr, "invalid limit\n");
return 2;
}
set_limit = 1;
break;
case 'p':
override_path = optarg;
break;
case 'h':
case '?':
do_set = 1;
} else if (!strcmp(argv[i], "--get")) {
do_get = 1;
} else if (!strcmp(argv[i], "--expect-limit-4k")) {
if (++i >= argc || parse_u32(argv[i], &expect_limit)) {
fprintf(stderr, "invalid expected limit\n");
return 2;
}
do_get = 1;
do_expect_limit = 1;
} else if (!strcmp(argv[i], "--expect-empty")) {
do_get = 1;
do_expect_empty = 1;
} else if (!strcmp(argv[i], "-p") || !strcmp(argv[i], "--path")) {
if (++i >= argc) {
usage(argv[0]);
return 2;
}
override_path = argv[i];
} else if (argv[i][0] == '-') {
fprintf(stderr, "unknown argument: %s\n", argv[i]);
usage(argv[0]);
return opt == 'h' ? 0 : 2;
default:
return 2;
} else if (!mounted_path) {
mounted_path = argv[i];
} else {
usage(argv[0]);
return 2;
}
}
if (!set_limit || optind + 1 != argc) {
if (!mounted_path || (!do_set && !do_get) || (do_expect_limit && do_expect_empty)) {
usage(argv[0]);
return 2;
}
mounted_path = argv[optind];
if (NWCallsInit(NULL, NULL)) {
fprintf(stderr, "NWCallsInit failed\n");
return 2;
}
err = NWParsePath(mounted_path, NULL, &conn, volume, volpath);
if (err) {
@@ -107,7 +222,7 @@ int main(int argc, char **argv)
err = ncp_ns_alloc_short_dir_handle(conn, NW_NS_DOS, NCP_DIRSTYLE_NOHANDLE,
0, 0, nwpath, len,
NCP_ALLOC_TEMPORARY, &dirhandle, &volnum);
NCP_ALLOC_PERMANENT, &dirhandle, &volnum);
if (err) {
fprintf(stderr, "cannot obtain directory handle for %s: %s\n",
mounted_path, strnwerror(err));
@@ -115,24 +230,63 @@ int main(int argc, char **argv)
return 1;
}
{
struct ncp_dos_info info;
memset(&info, 0, sizeof(info));
info.MaximumSpace = limit;
err = ncp_ns_modify_entry_dos_info(conn, NW_NS_DOS, SA_ALL,
NCP_DIRSTYLE_HANDLE,
volnum, dirhandle, NULL, 0,
DM_MAXIMUM_SPACE, &info);
if (do_set) {
err = ncp22_36_set_dirquota(conn, dirhandle, set_limit);
if (err) {
fprintf(stderr, "NCP 22/36 set directory quota failed on %s (vol=%u handle=0x%02x): %s\n",
mounted_path, (unsigned)volnum, (unsigned)dirhandle, strnwerror(err));
ncp_dealloc_dir_handle(conn, dirhandle);
ncp_close(conn);
return 1;
}
printf("dirquota set path=%s vol=%u handle=0x%02x limit4k=%u\n",
mounted_path, (unsigned)volnum, (unsigned)dirhandle,
(unsigned)set_limit);
}
if (err) {
fprintf(stderr, "cannot set directory quota on %s (vol=%u handle=0x%02x): %s\n",
mounted_path, (unsigned)volnum, (unsigned)dirhandle, strnwerror(err));
ncp_close(conn);
return 1;
if (do_get) {
err = ncp22_35_get_dirquota(conn, dirhandle, entries,
sizeof(entries) / sizeof(entries[0]),
&entry_count);
if (err) {
fprintf(stderr, "NCP 22/35 get directory quota failed on %s (vol=%u handle=0x%02x): %s\n",
mounted_path, (unsigned)volnum, (unsigned)dirhandle, strnwerror(err));
ncp_dealloc_dir_handle(conn, dirhandle);
ncp_close(conn);
return 1;
}
printf("dirquota get path=%s vol=%u handle=0x%02x entries=%zu\n",
mounted_path, (unsigned)volnum, (unsigned)dirhandle, entry_count);
for (i = 0; i < (int)entry_count; i++) {
uint32_t used = entries[i].max4k >= entries[i].current4k ?
entries[i].max4k - entries[i].current4k : 0;
printf("dirquota entry%u level=%u max4k=%u current4k=%u used4k=%u\n",
(unsigned)i, (unsigned)entries[i].level,
(unsigned)entries[i].max4k,
(unsigned)entries[i].current4k,
(unsigned)used);
}
if (do_expect_empty && entry_count != 0) {
fprintf(stderr, "expected no directory quota entries, got %zu\n", entry_count);
ncp_dealloc_dir_handle(conn, dirhandle);
ncp_close(conn);
return 1;
}
if (do_expect_limit) {
if (entry_count < 1 || entries[0].max4k != expect_limit) {
fprintf(stderr, "expected first directory quota max4k=%u, got %s%u\n",
(unsigned)expect_limit,
entry_count < 1 ? "no entry, fallback=" : "",
entry_count < 1 ? 0U : (unsigned)entries[0].max4k);
ncp_dealloc_dir_handle(conn, dirhandle);
ncp_close(conn);
return 1;
}
}
}
printf("directory quota set path=%s limit4k=%lu\n", mounted_path, limit);
ncp_dealloc_dir_handle(conn, dirhandle);
ncp_close(conn);
return 0;
}

View File

@@ -0,0 +1,162 @@
#!/bin/sh
# Manual NCPFS directory-quota smoke for mars-nwe 3.x quota endpoints.
#
# Exercises decimal NCP 22/36 (wire/code 0x24) to set a directory disk-space
# restriction and decimal NCP 22/35 (wire/code 0x23) to read it back. Host-side
# xattr verification checks that the mutation reached netware.metadata.
set -eu
if [ "$#" -lt 4 ]; then
echo "usage: $0 SERVER ADMIN PASSWORD HOST_SYSROOT [MOUNTPOINT] [TESTDIR] [VOLUME]" >&2
echo "example: $0 MARS SUPERVISOR secret /var/mars_nwe/SYS /mnt/nw-sys NWFSTEST SYS" >&2
echo "Set NWFS_NCPFS_DIR_QUOTA_4K to choose the tested limit; default: 12." >&2
exit 2
fi
SERVER=$1
ADMIN_USER=$2
ADMIN_PASSWORD=$3
SYSROOT=$4
MOUNTPOINT=${5:-}
TESTDIR=${6:-${NWFS_NCPFS_TESTDIR:-NWFSTEST}}
VOLUME=${7:-${NWFS_NCPFS_VOLUME:-SYS}}
DIR_QUOTA_4K=${NWFS_NCPFS_DIR_QUOTA_4K:-12}
DIRQUOTA_HELPER="@NWFS_NCPFS_DIRQUOTA_HELPER@"
DUMP="@CMAKE_CURRENT_BINARY_DIR@/nwfs_xattr_dump"
case "$TESTDIR" in
/*|*..*|"") echo "TESTDIR must be relative and must not contain '..': $TESTDIR" >&2; exit 2 ;;
esac
case "$VOLUME" in
*:*|*/*|*..*|"") echo "VOLUME must be a bare NetWare volume name: $VOLUME" >&2; exit 2 ;;
esac
case "$DIR_QUOTA_4K" in
''|*[!0-9]*) echo "NWFS_NCPFS_DIR_QUOTA_4K must be an integer 4K block count" >&2; exit 2 ;;
esac
if [ -z "$DIRQUOTA_HELPER" ] || [ ! -x "$DIRQUOTA_HELPER" ]; then
echo "nwfs_ncpfs_dirquota helper not built" >&2
exit 1
fi
if [ ! -x "$DUMP" ]; then
echo "nwfs_xattr_dump not built: $DUMP" >&2
exit 1
fi
if ! command -v ncpmount >/dev/null 2>&1; then
echo "ncpmount missing" >&2
exit 1
fi
if [ -z "$MOUNTPOINT" ]; then
MOUNTPOINT=$(mktemp -d "${TMPDIR:-/tmp}/mars-ncpfs-dirquota.XXXXXX")
CLEAN_MOUNT=1
else
mkdir -p "$MOUNTPOINT"
CLEAN_MOUNT=0
fi
MOUNTED_BY_SCRIPT=0
OUT=$(mktemp "${TMPDIR:-/tmp}/nwfs_dirquota_out.XXXXXX")
DUMP_OUT=$(mktemp "${TMPDIR:-/tmp}/nwfs_dirquota_dump.XXXXXX")
ncpfs_mount_is_active() {
mp=$1
awk -v mp="$mp" '$2 == mp && $3 == "ncpfs" { found = 1 } END { exit found ? 0 : 1 }' /proc/self/mounts
}
ncpfs_umount_path() {
mp=$1
if ! ncpfs_mount_is_active "$mp"; then
return 0
fi
if command -v ncpumount >/dev/null 2>&1; then
ncpumount "$mp" || umount "$mp"
else
umount "$mp"
fi
}
cleanup() {
if [ "$MOUNTED_BY_SCRIPT" = 1 ]; then
ncpfs_umount_path "$MOUNTPOINT" || true
fi
if [ "$CLEAN_MOUNT" = 1 ]; then
rmdir "$MOUNTPOINT" 2>/dev/null || true
fi
rm -f "$OUT" "$DUMP_OUT"
}
trap cleanup EXIT HUP INT TERM
purge_testdir_recycle() {
for path in "$SYSROOT"/.recycle/*/"$TESTDIR" "$SYSROOT"/.salvage/*/"$TESTDIR"; do
[ -e "$path" ] || continue
echo "purging old recycle/salvage test remnant $path" >&2
rm -rf -- "$path"
done
}
mount_admin() {
err=$(mktemp "${TMPDIR:-/tmp}/nwfs_dirquota_mount.XXXXXX")
ncpfs_umount_path "$MOUNTPOINT"
if ! ncpmount -S "$SERVER" -U "$ADMIN_USER" -P "$ADMIN_PASSWORD" -V "$VOLUME" "$MOUNTPOINT" 2>"$err"; then
echo "ncpmount failed for //$SERVER/$VOLUME at $MOUNTPOINT" >&2
cat "$err" >&2 || true
rm -f "$err"
exit 1
fi
rm -f "$err"
MOUNTED_BY_SCRIPT=1
echo "mounted //$SERVER/$VOLUME at $MOUNTPOINT as $ADMIN_USER" >&2
}
verify_dump_limit() {
expected=$1
"$DUMP" "$SYSROOT/$TESTDIR" | tee "$DUMP_OUT"
if [ "$expected" = 0 ]; then
if grep -q 'dirQuotaLimit=.*active' "$DUMP_OUT"; then
echo "expected directory quota to be cleared on $SYSROOT/$TESTDIR" >&2
exit 1
fi
else
if ! grep -q "dirQuotaLimit=$expected active" "$DUMP_OUT"; then
echo "directory quota metadata did not contain dirQuotaLimit=$expected active on $SYSROOT/$TESTDIR" >&2
exit 1
fi
fi
}
purge_testdir_recycle
mount_admin
rm -rf -- "$MOUNTPOINT/$TESTDIR"
mkdir -p -- "$MOUNTPOINT/$TESTDIR"
sync
printf '# initial directory quota read should be unrestricted\n'
"$DIRQUOTA_HELPER" --expect-empty "$MOUNTPOINT/$TESTDIR" | tee "$OUT"
printf '# setting directory quota using NCP 22/36 decimal / wire 0x24\n'
"$DIRQUOTA_HELPER" --set-limit-4k "$DIR_QUOTA_4K" "$MOUNTPOINT/$TESTDIR" | tee "$OUT"
printf '# reading directory quota using NCP 22/35 decimal / wire 0x23\n'
"$DIRQUOTA_HELPER" --expect-limit-4k "$DIR_QUOTA_4K" "$MOUNTPOINT/$TESTDIR" | tee "$OUT"
if ! grep -q "max4k=$DIR_QUOTA_4K" "$OUT"; then
echo "NCP 22/35 readback did not report max4k=$DIR_QUOTA_4K" >&2
exit 1
fi
printf '# verifying host netware.metadata directory quota\n'
verify_dump_limit "$DIR_QUOTA_4K"
printf '# clearing directory quota using NCP 22/36 decimal / wire 0x24\n'
"$DIRQUOTA_HELPER" --set-limit-4k 0 "$MOUNTPOINT/$TESTDIR" | tee "$OUT"
printf '# reading cleared directory quota using NCP 22/35 decimal / wire 0x23\n'
"$DIRQUOTA_HELPER" --expect-empty "$MOUNTPOINT/$TESTDIR" | tee "$OUT"
printf '# verifying host netware.metadata directory quota clear\n'
verify_dump_limit 0
rm -rf -- "$MOUNTPOINT/$TESTDIR"
sync
echo "directory quota smoke completed for $VOLUME:$TESTDIR limit=${DIR_QUOTA_4K}x4K"