tests: add AFP create directory smoke
All checks were successful
Source release / source-package (push) Successful in 46s

This commit is contained in:
Mario Fetka
2026-05-30 18:52:04 +00:00
parent d1a7780532
commit 0a171048e8
5 changed files with 453 additions and 6 deletions

11
TODO.md
View File

@@ -468,13 +468,14 @@ AFP Set File Information metadata-rights convergence:
Endpoint order:
- First finish the non-destructive convergence work above.
- Then implement AFP Create File (`0x02`) and AFP 2.0 Create File (`0x15`) by
- AFP Create Directory (`0x01`) and AFP 2.0 Create Directory (`0x0d`) are now
routed through the existing NetWare directory-create helper for temporary
smoke paths. The Linux smoke suite creates both a legacy and AFP 2.0
directory under the tested parent, verifies the returned namespace-derived
Entry ID, and removes the directories through the local Unix path.
- Next implement AFP Create File (`0x02`) and AFP 2.0 Create File (`0x0e`) by
routing through the existing NetWare create/open path and testing only
explicit temporary smoke files with cleanup.
- Then implement AFP Create Directory (`0x01`) and AFP 2.0 Create Directory
(`0x14`) through the existing NetWare directory-create helper, again only on
temporary smoke paths.
- Then implement AFP Rename (`0x07`) through the existing NetWare rename/move
path, preserving FinderInfo/xattrs and checking Entry ID behavior.
- Keep AFP Delete (`0x03`) and Get Macintosh Info On Deleted File (`0x13`) for

View File

@@ -62,6 +62,11 @@ add_executable(afp_open_file_fork_smoke afp_open_file_fork_smoke.c)
target_include_directories(afp_open_file_fork_smoke PRIVATE ${NCPFS_INCLUDE_DIR})
target_link_libraries(afp_open_file_fork_smoke ${NCPFS_LIBRARY})
add_executable(afp_create_directory_smoke afp_create_directory_smoke.c)
target_include_directories(afp_create_directory_smoke PRIVATE ${NCPFS_INCLUDE_DIR})
target_link_libraries(afp_create_directory_smoke ${NCPFS_LIBRARY})
add_executable(afp_temp_dir_handle_smoke afp_temp_dir_handle_smoke.c)
target_include_directories(afp_temp_dir_handle_smoke PRIVATE ${NCPFS_INCLUDE_DIR})
target_link_libraries(afp_temp_dir_handle_smoke ${NCPFS_LIBRARY})

View File

@@ -28,6 +28,7 @@ cmake -DMARS_NWE_BUILD_LINUX_TESTS=ON ...
cmake --build . --target afp_entry_id_smoke
cmake --build . --target afp_file_info_smoke
cmake --build . --target afp_scan_info_smoke
cmake --build . --target afp_create_directory_smoke
cmake --build . --target afp_temp_dir_handle_smoke
cmake --build . --target afp_set_file_info_smoke
```
@@ -60,7 +61,8 @@ Example from the build `tests/linux` directory:
```
The report includes AFP Entry ID, Entry ID From NetWare Handle, Get File
Information, Scan File Information, Alloc Temporary Directory Handle, Open File
Information, Scan File Information, Alloc Temporary Directory Handle, AFP Create
Directory for both legacy (`0x01`) and AFP 2.0 (`0x0d`), Open File
Fork, FinderInfo Set File Information for both AFP 2.0 (`0x10`) and the legacy AFP
Set File Information (`0x09`), AFP 2.0 Hidden/System/Archive Set/Clear File
Information, legacy AFP `0x09` Hidden Set/Clear coverage, and the Linux xattr
@@ -188,6 +190,34 @@ user.org.mars-nwe.afp.attributes=0x01000000
user.org.mars-nwe.afp.entry-id=0x010000001ad06d3e
```
## AFP Create Directory smoke test
`afp_create_directory_smoke` sends the WebSDK/nwafp.h AFP Create Directory
requests through libncp:
```text
NCP 0x2222/35/01 AFP Create Directory
NCP 0x2222/35/0d AFP 2.0 Create Directory
```
The helper derives the parent Entry ID with the existing AFP Entry ID From Path
Name endpoint, sends only the new leaf name to Create Directory, and verifies
the returned directory ID by looking the created path up again. This exercises
the server-side path through the existing mars_nwe namespace/basehandle mapping
and `nw_mk_rd_dir()` rather than an AFP-only directory resolver.
Example:
```sh
./tests/linux/afp_create_directory_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/afpdirts
./tests/linux/afp_create_directory_smoke --afp20 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/afpdirts2
```
The full smoke suite creates temporary directories under the tested parent and
removes them through the local Unix path after each positive probe. Use
`--create-dir-name NAME` to override the default temporary leaf name.
## AFP Entry ID smoke test
`afp_entry_id_smoke` sends the WebSDK-documented NetWare AFP request:

View File

@@ -0,0 +1,361 @@
/*
* Linux smoke test for NetWare AFP Create Directory.
*
* The helper sends the WebSDK/nwafp.h AFP Create Directory requests through
* ncpfs/libncp and verifies that the returned AFP directory ID matches a
* subsequent AFP Entry ID From Path Name lookup for the created directory.
*/
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncp/nwcalls.h>
#include <ncp/ncplib.h>
#include <ncp/kernel/ncp.h>
#ifndef NCPC_SUBFUNCTION
#define NCPC_SUBFUNCTION 0x10000
#endif
#ifndef NCPC_SFN
#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION)
#endif
#define AFP_CREATE_DIRECTORY 0x01
#define AFP20_CREATE_DIRECTORY 0x0d
#define AFP_GET_ENTRY_ID_FROM_PATH 0x0c
#define NWE_INVALID_NAMESPACE 0xbf
#define NWE_INVALID_PATH 0x9c
#define NWE_FILE_EXISTS 0xff
#ifndef NWE_INVALID_NCP_PACKET_LENGTH
#define NWE_INVALID_NCP_PACKET_LENGTH 0x7e
#endif
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--afp20] [--expect-completion CODE] [--volume N] "
"[--entry-id ID] [--type FOURCC] [--creator FOURCC] "
"[ncpfs options] PATH\n"
"\n"
"ncpfs options are parsed by ncp_initialize(), for example:\n"
" -S SERVER -U USER -P PASSWORD -n\n"
"\n"
"Examples:\n"
" %s -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/AFPDIRTS\n"
" %s --afp20 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/AFPDIR20\n",
prog, 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 cpu_to_be32(uint32_t v, uint8_t p[4])
{
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;
}
static uint32_t be32_to_cpu(const uint8_t p[4])
{
return ((uint32_t)p[0] << 24) |
((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) |
p[3];
}
static int split_parent_leaf(const char *path, char *parent, size_t parent_size,
char *leaf, size_t leaf_size)
{
const char *slash = strrchr(path, '/');
const char *backslash = strrchr(path, '\\');
const char *sep = slash;
size_t parent_len;
size_t leaf_len;
if (backslash && (!sep || backslash > sep))
sep = backslash;
if (!sep) {
const char *colon = strchr(path, ':');
if (!colon || colon[1] == '\0')
return -1;
parent_len = (size_t)(colon - path + 1);
leaf_len = strlen(colon + 1);
if (parent_len >= parent_size || leaf_len >= leaf_size || !leaf_len)
return -1;
memcpy(parent, path, parent_len);
parent[parent_len] = '\0';
memcpy(leaf, colon + 1, leaf_len + 1);
return 0;
}
parent_len = (size_t)(sep - path);
leaf_len = strlen(sep + 1);
if (!parent_len || !leaf_len || parent_len >= parent_size ||
leaf_len >= leaf_size)
return -1;
memcpy(parent, path, parent_len);
parent[parent_len] = '\0';
memcpy(leaf, sep + 1, leaf_len + 1);
return 0;
}
static NWCCODE afp_get_entry_id_from_path(NWCONN_HANDLE conn,
const char *path,
uint32_t *entry_id)
{
size_t path_len = strlen(path);
uint8_t request[1 + 1 + 255];
uint8_t reply_buf[4];
NW_FRAGMENT reply;
NWCCODE err;
if (path_len > 255)
return NWE_INVALID_PATH;
request[0] = 0; /* directory handle 0, raw VOL:path */
request[1] = (uint8_t)path_len;
memcpy(request + 2, path, path_len);
memset(reply_buf, 0, sizeof(reply_buf));
reply.fragAddr.rw = reply_buf;
reply.fragSize = sizeof(reply_buf);
err = NWRequestSimple(conn,
NCPC_SFN(0x23, AFP_GET_ENTRY_ID_FROM_PATH),
request,
2 + path_len,
&reply);
if (err)
return err;
if (reply.fragSize < 4)
return NWE_INVALID_NCP_PACKET_LENGTH;
*entry_id = be32_to_cpu(reply_buf);
return 0;
}
int main(int argc, char **argv)
{
NWCONN_HANDLE conn;
NW_FRAGMENT reply;
long init_err = 0;
const char *path = NULL;
uint32_t volume_number = 0;
uint32_t base_entry_id = 0;
uint32_t expect_completion = 0xffffffffU;
int afp20 = 0;
const char *finder_type = "fold";
const char *finder_creator = "MARS";
int i;
char parent[256];
char leaf[256];
size_t leaf_len;
uint8_t request[1 + 4 + 1 + 32 + 6 + 1 + 255];
size_t fixed_len;
uint8_t reply_buf[4];
uint32_t created_entry_id;
uint32_t lookup_entry_id;
NWCCODE err;
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], "--afp20")) {
afp20 = 1;
} else if (!strcmp(argv[i], "--volume")) {
if (++i >= argc || parse_u32(argv[i], &volume_number) ||
volume_number > 255) {
fprintf(stderr, "invalid --volume value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--entry-id")) {
if (++i >= argc || parse_u32(argv[i], &base_entry_id)) {
fprintf(stderr, "invalid --entry-id value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--expect-completion")) {
if (++i >= argc || parse_u32(argv[i], &expect_completion) ||
expect_completion > 255) {
fprintf(stderr, "invalid --expect-completion value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--type")) {
if (++i >= argc || strlen(argv[i]) != 4) {
fprintf(stderr, "--type must be exactly four characters\n");
ncp_close(conn);
return 2;
}
finder_type = argv[i];
} else if (!strcmp(argv[i], "--creator")) {
if (++i >= argc || strlen(argv[i]) != 4) {
fprintf(stderr, "--creator must be exactly four characters\n");
ncp_close(conn);
return 2;
}
finder_creator = argv[i];
} else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
usage(argv[0]);
ncp_close(conn);
return 0;
} else if (!path) {
path = argv[i];
} else {
fprintf(stderr, "unexpected argument: %s\n", argv[i]);
usage(argv[0]);
ncp_close(conn);
return 2;
}
}
if (!path) {
fprintf(stderr, "missing PATH\n");
usage(argv[0]);
ncp_close(conn);
return 2;
}
if (split_parent_leaf(path, parent, sizeof(parent), leaf, sizeof(leaf))) {
fprintf(stderr, "PATH must include a parent and new directory name: %s\n", path);
ncp_close(conn);
return 2;
}
leaf_len = strlen(leaf);
if (leaf_len > 255) {
fprintf(stderr, "directory name is too long for AFP Create Directory: %zu\n",
leaf_len);
ncp_close(conn);
return 2;
}
if (base_entry_id == 0) {
err = afp_get_entry_id_from_path(conn, parent, &base_entry_id);
if (err) {
fprintf(stderr, "AFP parent Entry ID lookup failed for %s: 0x%04x\n",
parent, (unsigned int)err);
ncp_close(conn);
return 1;
}
}
memset(request, 0, sizeof(request));
request[0] = (uint8_t)volume_number;
cpu_to_be32(base_entry_id, request + 1);
request[5] = 0; /* reserved */
memcpy(request + 6, finder_type, 4);
memcpy(request + 10, finder_creator, 4);
if (afp20) {
fixed_len = 44; /* volume..ProDOSInfo */
request[44] = (uint8_t)leaf_len;
memcpy(request + 45, leaf, leaf_len);
} else {
fixed_len = 38; /* volume..FinderInfo */
request[38] = (uint8_t)leaf_len;
memcpy(request + 39, leaf, leaf_len);
}
memset(reply_buf, 0, sizeof(reply_buf));
reply.fragAddr.rw = reply_buf;
reply.fragSize = sizeof(reply_buf);
err = NWRequestSimple(conn,
NCPC_SFN(0x23, afp20 ? AFP20_CREATE_DIRECTORY
: AFP_CREATE_DIRECTORY),
request,
fixed_len + 1 + leaf_len,
&reply);
if (expect_completion != 0xffffffffU) {
if (((unsigned int)err & 0xff) == expect_completion) {
printf("AFP Create Directory returned expected completion 0x%02x: "
"subfunction=0x%02x path=%s parent_entry_id=0x%08x\n",
(unsigned int)expect_completion,
afp20 ? AFP20_CREATE_DIRECTORY : AFP_CREATE_DIRECTORY,
path, base_entry_id);
ncp_close(conn);
return 0;
}
fprintf(stderr, "expected completion 0x%02x but got 0x%04x\n",
(unsigned int)expect_completion, (unsigned int)err);
ncp_close(conn);
return 1;
}
if (err) {
fprintf(stderr, "AFP Create Directory failed: subfunction=0x%02x "
"path=%s parent_entry_id=0x%08x error=0x%04x\n",
afp20 ? AFP20_CREATE_DIRECTORY : AFP_CREATE_DIRECTORY,
path, base_entry_id, (unsigned int)err);
ncp_close(conn);
return 1;
}
if (reply.fragSize < 4) {
fprintf(stderr, "AFP Create Directory reply too short: %u\n",
(unsigned int)reply.fragSize);
ncp_close(conn);
return 1;
}
created_entry_id = be32_to_cpu(reply_buf);
if (!created_entry_id) {
fprintf(stderr, "AFP Create Directory returned entry_id 0\n");
ncp_close(conn);
return 1;
}
err = afp_get_entry_id_from_path(conn, path, &lookup_entry_id);
if (err) {
fprintf(stderr, "AFP Entry ID lookup for created directory failed: "
"path=%s error=0x%04x\n", path, (unsigned int)err);
ncp_close(conn);
return 1;
}
if (created_entry_id != lookup_entry_id) {
fprintf(stderr, "AFP Create Directory entry mismatch: created=0x%08x "
"lookup=0x%08x path=%s\n",
created_entry_id, lookup_entry_id, path);
ncp_close(conn);
return 1;
}
printf("AFP Create Directory subfunction=0x%02x path=%s parent=%s "
"leaf=%s parent_entry_id=0x%08x entry_id=0x%08x verified\n",
afp20 ? AFP20_CREATE_DIRECTORY : AFP_CREATE_DIRECTORY,
path, parent, leaf, base_entry_id, created_entry_id);
ncp_close(conn);
return 0;
}

View File

@@ -18,6 +18,7 @@ OUT_FILE=""
FINDER_TYPE="TEXT"
FINDER_CREATOR="MARS"
TIMESTAMP_EPOCH="1700000000"
CREATE_DIR_NAME="afpdirts"
READONLY_USER=""
READONLY_PASSWORD=""
READONLY_NO_PASSWORD=0
@@ -38,6 +39,7 @@ Options:
--type FOURCC FinderInfo type written by Set File Info (default: $FINDER_TYPE)
--creator FOURCC FinderInfo creator written by Set File Info (default: $FINDER_CREATOR)
--mtime-epoch SECONDS AFP modify/backup timestamp to write (default: $TIMESTAMP_EPOCH)
--create-dir-name NAME Temporary directory name for AFP Create Directory (default: $CREATE_DIR_NAME)
--readonly-user USER Optional user expected to lack Modify rights
--readonly-password PASS Password for --readonly-user
--readonly-no-password Use ncpfs -n for --readonly-user
@@ -75,6 +77,8 @@ while [ $# -gt 0 ]; do
FINDER_CREATOR=$2; shift 2 ;;
--mtime-epoch)
TIMESTAMP_EPOCH=$2; shift 2 ;;
--create-dir-name)
CREATE_DIR_NAME=$2; shift 2 ;;
--readonly-user)
READONLY_USER=$2; shift 2 ;;
--readonly-password)
@@ -112,11 +116,18 @@ case "$FINDER_CREATOR" in
???? ) ;;
*) echo "--creator must be exactly four characters" >&2; exit 2 ;;
esac
case "$CREATE_DIR_NAME" in
""|*/*|*\\*|*:*) echo "--create-dir-name must be a single path component" >&2; exit 2 ;;
esac
DIR_PATH=$NETWARE_PATH
case "$NETWARE_PATH" in
*/*) DIR_PATH=${NETWARE_PATH%/*} ;;
esac
CREATE_DIR_PATH="$DIR_PATH/$CREATE_DIR_NAME"
CREATE_DIR20_PATH="$DIR_PATH/${CREATE_DIR_NAME}2"
UNIX_DIR_PATH=$(dirname -- "$UNIX_PATH")/$CREATE_DIR_NAME
UNIX_DIR20_PATH=$(dirname -- "$UNIX_PATH")/${CREATE_DIR_NAME}2
REPORT_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-afp-smoke.XXXXXX")
LOG_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-afp-log.XXXXXX")
@@ -222,6 +233,10 @@ emit "log=$LOG_FILE"
emit "finder_type=$FINDER_TYPE"
emit "finder_creator=$FINDER_CREATOR"
emit "mtime_epoch=$TIMESTAMP_EPOCH"
emit "create_dir_path=$CREATE_DIR_PATH"
emit "create_dir20_path=$CREATE_DIR20_PATH"
emit "unix_dir_path=$UNIX_DIR_PATH"
emit "unix_dir20_path=$UNIX_DIR20_PATH"
if [ -n "$READONLY_USER" ]; then
emit "readonly_user=$READONLY_USER"
if [ "$READONLY_NO_PASSWORD" -eq 1 ]; then
@@ -238,6 +253,7 @@ for helper in \
afp_file_info_smoke \
afp_dos_name_smoke \
afp_scan_info_smoke \
afp_create_directory_smoke \
afp_temp_dir_handle_smoke \
afp_open_file_fork_smoke \
afp_set_file_info_smoke
@@ -309,6 +325,40 @@ run_cmd \
"./afp_temp_dir_handle_smoke $COMMON_PRINT '$DIR_PATH'" \
"$SCRIPT_DIR/afp_temp_dir_handle_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$DIR_PATH"
if [ -d "$UNIX_DIR_PATH" ]; then
run_optional_cmd \
"Prepare AFP Create Directory cleanup" \
"rmdir '$UNIX_DIR_PATH'" \
rmdir "$UNIX_DIR_PATH" || true
fi
if [ -d "$UNIX_DIR20_PATH" ]; then
run_optional_cmd \
"Prepare AFP 2.0 Create Directory cleanup" \
"rmdir '$UNIX_DIR20_PATH'" \
rmdir "$UNIX_DIR20_PATH" || true
fi
run_cmd \
"AFP Create Directory" \
"./afp_create_directory_smoke $COMMON_PRINT '$CREATE_DIR_PATH'" \
"$SCRIPT_DIR/afp_create_directory_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_DIR_PATH"
run_cmd \
"AFP 2.0 Create Directory" \
"./afp_create_directory_smoke --afp20 $COMMON_PRINT '$CREATE_DIR20_PATH'" \
"$SCRIPT_DIR/afp_create_directory_smoke" --afp20 \
-S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_DIR20_PATH"
run_cmd \
"Cleanup AFP Create Directory" \
"rmdir '$UNIX_DIR_PATH'" \
rmdir "$UNIX_DIR_PATH"
run_cmd \
"Cleanup AFP 2.0 Create Directory" \
"rmdir '$UNIX_DIR20_PATH'" \
rmdir "$UNIX_DIR20_PATH"
run_cmd \
"AFP Open File Fork" \
"./afp_open_file_fork_smoke $COMMON_PRINT '$NETWARE_PATH'" \