diff --git a/src/connect.c b/src/connect.c index d0171d1..cc3be59 100644 --- a/src/connect.c +++ b/src/connect.c @@ -58,11 +58,13 @@ static int act_umode_file=0; #include "nwattrib.h" #include "nwarchive.h" #include "trustee.h" +#include "nwxattr.h" #include "nwfile.h" #include "nwconn.h" #include "namspace.h" #include "namedos.h" #include "connect.h" +#include /* connect.h may already be include-guarded through another header before * NW_VOL is visible, so keep this local forward declaration before the @@ -1466,6 +1468,40 @@ static int set_ncp22_25_file_info(char *unixname, } +static int set_ncp22_25_dir_quota_metadata(char *unixname, uint32 max_space) +{ + zNW_metadata_s metadata; + ssize_t len; + size_t size; + SQUAD quota; + + if (unixname == NULL) + return(-0x8c); + + memset(&metadata, 0, sizeof(metadata)); + len = mars_nwe_getxattr(unixname, zNW_METADATA, &metadata, sizeof(metadata)); + if (len < 0) { + nwfs_metadata_init(&metadata); + } else if (nwfs_metadata_validate(&metadata, (size_t)len) != NWFS_OK) { + XDPRINTF((2,0, "invalid netware.metadata while setting dir quota: %s", unixname)); + return(-0x8c); + } + + if (max_space == MAX_U32 || max_space == 0x40000000UL) + quota = zDIR_NO_QUOTA; + else + quota = (SQUAD)max_space; + + if (nwfs_metadata_set_quota_limit(&metadata, quota) != NWFS_OK) + return(-0x8c); + + size = nwfs_metadata_size_for_trustees(metadata.nwm_trustee_num); + if (size == 0 || mars_nwe_setxattr(unixname, zNW_METADATA, &metadata, size, 0)) + return(-0x8c); + + return(0); +} + static int set_ncp22_25_maximum_space(int volume, char *unixname, struct stat *stb, NW_SET_DIR_INFO *f, @@ -1473,16 +1509,15 @@ static int set_ncp22_25_maximum_space(int volume, char *unixname, int is_dir) { uint32 max_space; - uint32 quota; int result; if (!(change_mask & DM_MAXIMUM_SPACE)) return(0); - /* NetWare exposes maximum space on directory entries. mars_nwe already - * has a Linux user-quota backend for volume restrictions; use that as the - * closest available mapping. Files have no max_space field in the DOS - * layout, so ignore the bit if a client sends it for a file. + /* NetWare/NSS exposes maximum space on directory entries. Keep it in the + * OES-compatible netware.metadata nwm_quota_limit field; user/volume + * restrictions are a separate netware.userquota data model and are handled + * by the volume-restriction NCPs. */ if (!is_dir) { XDPRINTF((5,0, @@ -1495,26 +1530,10 @@ static int set_ncp22_25_maximum_space(int volume, char *unixname, return(-0x8c); max_space = GET_BE32(f->u.d.max_space); - - /* Treat the common unlimited sentinels as "remove restriction". */ - if (max_space == MAX_U32 || max_space == 0x40000000UL) - quota = 0; - else - quota = max_space; - - result = nw_set_vol_restrictions((uint8)volume, act_uid, quota); - if (result) { - /* If quota support is not compiled/enabled, do not break old clients. */ - XDPRINTF((5,0, - "NCP22/25 maximum space quota ignored: vol=%d uid=%d max=0x%08lx rc=%d", - volume, act_uid, (unsigned long)max_space, result)); - if (result == -0xfb) - return(0); - } else { - XDPRINTF((5,0, - "NCP22/25 maximum space quota set: vol=%d uid=%d max=0x%08lx quota=0x%08lx", - volume, act_uid, (unsigned long)max_space, (unsigned long)quota)); - } + result = set_ncp22_25_dir_quota_metadata(unixname, max_space); + XDPRINTF((5,0, + "NCP22/25 directory quota metadata set: vol=%d path=%s max=0x%08lx rc=%d", + volume, unixname ? unixname : "", (unsigned long)max_space, result)); return(result); } diff --git a/tests/nwfs/CMakeLists.txt b/tests/nwfs/CMakeLists.txt index 0dfa22e..1f99bb2 100644 --- a/tests/nwfs/CMakeLists.txt +++ b/tests/nwfs/CMakeLists.txt @@ -8,6 +8,29 @@ target_link_libraries(nwfs_xattr_dump PRIVATE mars_nwe::nwfs ${XATTR_LIBRARIES}) add_executable(nwfs_xattr_edit nwfs_xattr_edit.c) target_link_libraries(nwfs_xattr_edit PRIVATE mars_nwe::nwfs ${XATTR_LIBRARIES}) +find_path(NCPFS_INCLUDE_DIR + NAMES ncp/nwcalls.h +) +find_library(NCPFS_LIBRARY + NAMES ncp +) + +set(NWFS_NCPFS_DIRQUOTA_HELPER "") +set(NWFS_NCPFS_USERQUOTA_HELPER "") +if(NCPFS_INCLUDE_DIR AND NCPFS_LIBRARY) + add_executable(nwfs_ncpfs_dirquota nwfs_ncpfs_dirquota.c) + target_include_directories(nwfs_ncpfs_dirquota PRIVATE ${NCPFS_INCLUDE_DIR}) + target_link_libraries(nwfs_ncpfs_dirquota PRIVATE ${NCPFS_LIBRARY}) + set(NWFS_NCPFS_DIRQUOTA_HELPER "${CMAKE_CURRENT_BINARY_DIR}/nwfs_ncpfs_dirquota") + + add_executable(nwfs_ncpfs_userquota nwfs_ncpfs_userquota.c) + target_include_directories(nwfs_ncpfs_userquota PRIVATE ${NCPFS_INCLUDE_DIR}) + target_link_libraries(nwfs_ncpfs_userquota PRIVATE ${NCPFS_LIBRARY}) + set(NWFS_NCPFS_USERQUOTA_HELPER "${CMAKE_CURRENT_BINARY_DIR}/nwfs_ncpfs_userquota") +else() + message(STATUS "ncpfs/libncp not found; manual nwfs NCPFS quota smoke helpers disabled") +endif() + configure_file(nwfs_metadata_xattr_file_test.sh.in nwfs_metadata_xattr_file_test.sh @ONLY) add_test(NAME nwfs_metadata_xattr_file_test COMMAND sh ${CMAKE_CURRENT_BINARY_DIR}/nwfs_metadata_xattr_file_test.sh) diff --git a/tests/nwfs/nwfs_ncpfs_dirquota.c b/tests/nwfs/nwfs_ncpfs_dirquota.c new file mode 100644 index 0000000..433bbd4 --- /dev/null +++ b/tests/nwfs/nwfs_ncpfs_dirquota.c @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Manual NCPFS smoke helper for NetWare directory 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. + */ + +#include +#include +#include +#include +#include + +#include +#include + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [-l LIMIT_4K] [-p VOLUME:PATH] 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); +} + +int main(int argc, char **argv) +{ + NWDSCCODE err; + NWCONN_HANDLE conn = NULL; + char volume[1000]; + char volpath[1000]; + char remote[2000]; + unsigned char nwpath[1000]; + const char *override_path = NULL; + const char *mounted_path; + unsigned long limit = 0; + int set_limit = 0; + int opt; + int len; + + 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); + return 2; + } + set_limit = 1; + break; + case 'p': + override_path = optarg; + break; + case 'h': + case '?': + usage(argv[0]); + return opt == 'h' ? 0 : 2; + default: + usage(argv[0]); + return 2; + } + } + + if (!set_limit || optind + 1 != argc) { + usage(argv[0]); + return 2; + } + mounted_path = argv[optind]; + + err = NWParsePath(mounted_path, NULL, &conn, volume, volpath); + if (err) { + fprintf(stderr, "NWParsePath failed for %s: %s\n", mounted_path, strnwerror(err)); + return 1; + } + if (!conn) { + fprintf(stderr, "path is not on an NCPFS mount: %s\n", mounted_path); + return 1; + } + + if (override_path) { + len = ncp_path_to_NW_format(override_path, nwpath, sizeof(nwpath)); + } else { + if ((size_t)snprintf(remote, sizeof(remote), "%s:%s", volume, volpath) >= sizeof(remote)) { + fprintf(stderr, "remote path too long\n"); + ncp_close(conn); + return 2; + } + len = ncp_path_to_NW_format(remote, nwpath, sizeof(nwpath)); + } + if (len < 0) { + fprintf(stderr, "cannot convert remote path: %s\n", strerror(-len)); + ncp_close(conn); + 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_NOHANDLE, + 0, 0, nwpath, len, + DM_MAXIMUM_SPACE, &info); + } + + if (err) { + fprintf(stderr, "cannot set directory quota on %s: %s\n", mounted_path, strnwerror(err)); + ncp_close(conn); + return 1; + } + + printf("directory quota set path=%s limit4k=%lu\n", mounted_path, limit); + ncp_close(conn); + return 0; +} diff --git a/tests/nwfs/nwfs_ncpfs_metadata_smoke.sh.in b/tests/nwfs/nwfs_ncpfs_metadata_smoke.sh.in index db5e96f..2aec4f3 100755 --- a/tests/nwfs/nwfs_ncpfs_metadata_smoke.sh.in +++ b/tests/nwfs/nwfs_ncpfs_metadata_smoke.sh.in @@ -9,6 +9,8 @@ if [ "$#" -lt 4 ]; then echo "example: $0 localhost supervisor secret /var/local/mars_nwe/SYS /mnt/nw-sys SYSTEM/NWFSTEST SYS" >&2 echo "TESTDIR defaults to NWFSTEST. If the volume root is not creatable through NCP, pass an existing writable directory below SYS." >&2 echo "Set NWFS_NCPFS_TRUSTEE_OBJECT and NWFS_NCPFS_TRUSTEE_TYPE to override the default GUEST user trustee mutation." >&2 + echo "Set NWFS_NCPFS_DIR_QUOTA_4K to override the default directory quota limit; set it empty to skip." >&2 + echo "Set NWFS_NCPFS_USER_QUOTA_4K to enable the user-volume restriction smoke. The server must run with NWFS_QUOTA_BACKEND=METADATAONLY for netware.userquota." >&2 exit 2 fi @@ -23,6 +25,12 @@ TRUSTEE_OBJECT=${NWFS_NCPFS_TRUSTEE_OBJECT:-GUEST} TRUSTEE_TYPE=${NWFS_NCPFS_TRUSTEE_TYPE:-1} TRUSTEE_RIGHTS=${NWFS_NCPFS_TRUSTEE_RIGHTS:-RF} DUMP="@CMAKE_CURRENT_BINARY_DIR@/nwfs_xattr_dump" +DIRQUOTA_HELPER="@NWFS_NCPFS_DIRQUOTA_HELPER@" +USERQUOTA_HELPER="@NWFS_NCPFS_USERQUOTA_HELPER@" +DIR_QUOTA_4K=${NWFS_NCPFS_DIR_QUOTA_4K-321} +USER_QUOTA_4K=${NWFS_NCPFS_USER_QUOTA_4K-} +USER_QUOTA_OBJECT=${NWFS_NCPFS_USER_QUOTA_OBJECT:-$TRUSTEE_OBJECT} +USER_QUOTA_TYPE=${NWFS_NCPFS_USER_QUOTA_TYPE:-$TRUSTEE_TYPE} MKDIR_ERR=$(mktemp "${TMPDIR:-/tmp}/nwfs_ncpfs_mkdir.XXXXXX") WRITE_ERR=$(mktemp "${TMPDIR:-/tmp}/nwfs_ncpfs_write.XXXXXX") GRANT_ERR=$(mktemp "${TMPDIR:-/tmp}/nwfs_ncpfs_grant.XXXXXX") @@ -129,3 +137,41 @@ if command -v nwrevoke >/dev/null 2>&1; then else echo "nwrevoke not found; skipping trustee-remove mutation" >&2 fi + +if [ -n "$DIR_QUOTA_4K" ]; then + if [ -n "$DIRQUOTA_HELPER" ] && [ -x "$DIRQUOTA_HELPER" ]; then + echo "setting directory quota ${DIR_QUOTA_4K}x4K on $NCP_TEST_PATH" >&2 + "$DIRQUOTA_HELPER" -l "$DIR_QUOTA_4K" "$NCP_TEST_PATH" + sync + printf '\n# after directory quota set\n' + "$DUMP" "$HOST_TEST_PATH" + else + echo "nwfs_ncpfs_dirquota helper not built; skipping directory quota mutation" >&2 + fi +else + echo "NWFS_NCPFS_DIR_QUOTA_4K is empty; skipping directory quota mutation" >&2 +fi + +if [ -n "$USER_QUOTA_4K" ]; then + if [ -n "$USERQUOTA_HELPER" ] && [ -x "$USERQUOTA_HELPER" ]; then + echo "setting user quota ${USER_QUOTA_4K}x4K for $USER_QUOTA_OBJECT type $USER_QUOTA_TYPE on $VOLUME" >&2 + "$USERQUOTA_HELPER" -S "$SERVER" -U "$USER" -P "$PASSWORD" \ + --volume "$VOLUME" --object "$USER_QUOTA_OBJECT" --type "$USER_QUOTA_TYPE" \ + --limit-4k "$USER_QUOTA_4K" + sync + printf '\n# after user quota set\n' + "$DUMP" "$SYSROOT" + + echo "removing user quota for $USER_QUOTA_OBJECT type $USER_QUOTA_TYPE on $VOLUME" >&2 + "$USERQUOTA_HELPER" -S "$SERVER" -U "$USER" -P "$PASSWORD" \ + --volume "$VOLUME" --object "$USER_QUOTA_OBJECT" --type "$USER_QUOTA_TYPE" \ + --remove + sync + printf '\n# after user quota remove\n' + "$DUMP" "$SYSROOT" + else + echo "nwfs_ncpfs_userquota helper not built; skipping user quota mutation" >&2 + fi +else + echo "NWFS_NCPFS_USER_QUOTA_4K not set; skipping user quota mutation" >&2 +fi diff --git a/tests/nwfs/nwfs_ncpfs_userquota.c b/tests/nwfs/nwfs_ncpfs_userquota.c new file mode 100644 index 0000000..517ccf4 --- /dev/null +++ b/tests/nwfs/nwfs_ncpfs_userquota.c @@ -0,0 +1,164 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Manual NCP/libncp smoke helper for NetWare volume/user restrictions. + * + * It uses the same public ncpfs/NWCalls APIs as ncpfs contrib/testing/pp/volres.c + * so tests mutate the server through NCP and verify netware.userquota on the + * host SYS root afterwards. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [ncpfs options] --volume VOL --object NAME --type TYPE (--limit-4k LIMIT | --remove)\n" + "\n" + "ncpfs options are parsed by ncp_initialize(), for example:\n" + " -S MARS -U SUPERVISOR -P secret\n", + 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; +} + +int main(int argc, char **argv) +{ + NWCONN_HANDLE conn; + long init_err = 0; + const char *volume = NULL; + const char *object = NULL; + uint32_t object_type = 1; + uint32_t limit = 0; + int have_limit = 0; + int remove_limit = 0; + int i; + NWCCODE err; + NWVOL_NUM volnum; + nuint32 object_id; + nuint32 restriction = 0; + nuint32 in_use = 0; + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { + usage(argv[0]); + return 0; + } + } + + if (NWCallsInit(NULL, NULL)) { + fprintf(stderr, "NWCallsInit failed\n"); + return 2; + } + + conn = ncp_initialize(&argc, argv, 1, &init_err); + if (!conn) { + fprintf(stderr, "ncp_initialize/login failed: %ld\n", init_err); + usage(argv[0]); + return 2; + } + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--volume")) { + if (++i >= argc) { + usage(argv[0]); + ncp_close(conn); + return 2; + } + volume = argv[i]; + } else if (!strcmp(argv[i], "--object")) { + if (++i >= argc) { + usage(argv[0]); + ncp_close(conn); + return 2; + } + object = argv[i]; + } else if (!strcmp(argv[i], "--type")) { + if (++i >= argc || parse_u32(argv[i], &object_type)) { + fprintf(stderr, "invalid --type value\n"); + ncp_close(conn); + return 2; + } + } else if (!strcmp(argv[i], "--limit-4k")) { + if (++i >= argc || parse_u32(argv[i], &limit)) { + fprintf(stderr, "invalid --limit-4k value\n"); + ncp_close(conn); + return 2; + } + have_limit = 1; + } else if (!strcmp(argv[i], "--remove")) { + remove_limit = 1; + } else { + fprintf(stderr, "unknown argument: %s\n", argv[i]); + usage(argv[0]); + ncp_close(conn); + return 2; + } + } + + if (!volume || !object || (have_limit == remove_limit)) { + usage(argv[0]); + ncp_close(conn); + return 2; + } + + err = NWGetVolumeNumber(conn, volume, &volnum); + if (err) { + fprintf(stderr, "NWGetVolumeNumber(%s) failed: %s\n", volume, strnwerror(err)); + ncp_close(conn); + return 1; + } + + err = NWGetObjectID(conn, object, (NWObjectType)object_type, &object_id); + if (err) { + fprintf(stderr, "NWGetObjectID(%s,type=%u) failed: %s\n", + object, (unsigned)object_type, strnwerror(err)); + ncp_close(conn); + return 1; + } + + if (remove_limit) + err = NWRemoveObjectDiskRestrictions(conn, volnum, object_id); + else + err = NWSetObjectVolSpaceLimit(conn, volnum, object_id, limit); + + if (err) { + fprintf(stderr, "%s object volume restriction failed: %s\n", + remove_limit ? "remove" : "set", strnwerror(err)); + ncp_close(conn); + return 1; + } + + err = NWGetObjDiskRestrictions(conn, volnum, object_id, &restriction, &in_use); + if (err) { + fprintf(stderr, "NWGetObjDiskRestrictions failed after mutation: %s\n", strnwerror(err)); + ncp_close(conn); + return 1; + } + + printf("userquota %s object=%s type=%u id=0x%08x volume=%s volnum=%u restriction4k=%u inuse4k=%u\n", + remove_limit ? "removed" : "set", + object, (unsigned)object_type, (unsigned)object_id, + volume, (unsigned)volnum, (unsigned)restriction, (unsigned)in_use); + + ncp_close(conn); + return 0; +}