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

This commit is contained in:
Mario Fetka
2026-05-30 19:40:19 +00:00
parent 37039a773f
commit 434de903bd
5 changed files with 482 additions and 3 deletions

10
TODO.md
View File

@@ -483,9 +483,13 @@ Endpoint order:
`0x00000005`. The same run also confirmed that regular-file AFP Entry IDs
stay on the nwatalk/fallback path (`0x067a8d0f` for `PMDFLTS.INI`) while
directory scan/basehandle IDs continue to use the NetWare namespace mapping.
- 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.
- AFP Create File (`0x02`) and AFP 2.0 Create File (`0x0e`) are now routed
through the existing NetWare file-create path for temporary smoke files. The
Linux smoke helper creates both legacy and AFP 2.0 files under the tested
parent, verifies the returned AFP file ID with Entry ID From Path Name, and
keeps local cleanup best-effort because AFP Delete is still pending.
- Runtime status: create-file smoke is added but still needs a build-server
runtime confirmation before recording success.
- 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

@@ -67,6 +67,10 @@ 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_create_file_smoke afp_create_file_smoke.c)
target_include_directories(afp_create_file_smoke PRIVATE ${NCPFS_INCLUDE_DIR})
target_link_libraries(afp_create_file_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

@@ -222,6 +222,37 @@ Unix user while the server created the directory under its own identity. Use
that rerunning with the same explicit name may fail if a previous directory still
exists.
## AFP Create File smoke test
`afp_create_file_smoke` sends the WebSDK/nwafp.h AFP Create File requests
through libncp:
```text
NCP 0x2222/35/02 AFP Create File
NCP 0x2222/35/0e AFP 2.0 Create File
```
The helper derives the parent Entry ID with AFP Entry ID From Path Name, sends
only the new leaf name to Create File, and verifies the returned file ID by
looking the created path up again. This exercises the server-side file create
path through the existing mars_nwe create helpers rather than direct local file
creation from the test.
Example:
```sh
./tests/linux/afp_create_file_smoke -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/afpfile
./tests/linux/afp_create_file_smoke --afp20 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/afpfil2
```
The full smoke suite creates temporary files under the tested parent with fresh
short DOS-compatible leaf names by default. Local removal is best-effort, just
like the create-directory smoke, because the suite usually runs as an
unprivileged Unix user while mars_nwe creates the file as the server-side
identity. Use `--create-file-name NAME` to override the default leaf name;
rerunning with the same explicit name may fail if a previous file still exists.
## AFP Entry ID smoke test
`afp_entry_id_smoke` sends the WebSDK-documented NetWare AFP request:

View File

@@ -0,0 +1,364 @@
/*
* Linux smoke test for NetWare AFP Create File.
*
* The helper sends the WebSDK/nwafp.h AFP Create File requests through
* ncpfs/libncp and verifies that the returned AFP file ID matches a
* subsequent AFP Entry ID From Path Name lookup for the created file.
*/
#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_FILE 0x02
#define AFP20_CREATE_FILE 0x0e
#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] [--delete-existing] [--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/AFPFILE\n"
" %s --afp20 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC/AFPFILE2\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;
int delete_existing = 0;
const char *finder_type = "TEXT";
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], "--delete-existing")) {
delete_existing = 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 file name: %s\n", path);
ncp_close(conn);
return 2;
}
leaf_len = strlen(leaf);
if (leaf_len > 255) {
fprintf(stderr, "file name is too long for AFP Create File: %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] = delete_existing ? 1 : 0; /* DeleteExistingFileFlag */
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_FILE
: AFP_CREATE_FILE),
request,
fixed_len + 1 + leaf_len,
&reply);
if (expect_completion != 0xffffffffU) {
if (((unsigned int)err & 0xff) == expect_completion) {
printf("AFP Create File returned expected completion 0x%02x: "
"subfunction=0x%02x path=%s parent_entry_id=0x%08x\n",
(unsigned int)expect_completion,
afp20 ? AFP20_CREATE_FILE : AFP_CREATE_FILE,
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 File failed: subfunction=0x%02x "
"path=%s parent_entry_id=0x%08x error=0x%04x\n",
afp20 ? AFP20_CREATE_FILE : AFP_CREATE_FILE,
path, base_entry_id, (unsigned int)err);
ncp_close(conn);
return 1;
}
if (reply.fragSize < 4) {
fprintf(stderr, "AFP Create File 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 File 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 file 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 File 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 File subfunction=0x%02x path=%s parent=%s "
"leaf=%s parent_entry_id=0x%08x entry_id=0x%08x verified\n",
afp20 ? AFP20_CREATE_FILE : AFP_CREATE_FILE,
path, parent, leaf, base_entry_id, created_entry_id);
ncp_close(conn);
return 0;
}

View File

@@ -19,6 +19,7 @@ FINDER_TYPE="TEXT"
FINDER_CREATOR="MARS"
TIMESTAMP_EPOCH="1700000000"
CREATE_DIR_NAME=""
CREATE_FILE_NAME=""
READONLY_USER=""
READONLY_PASSWORD=""
READONLY_NO_PASSWORD=0
@@ -41,6 +42,7 @@ Options:
--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: generated aHHMMSS)
--create-file-name NAME Temporary file name for AFP Create File (default: generated fHHMMR)
--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
@@ -81,6 +83,8 @@ while [ $# -gt 0 ]; do
TIMESTAMP_EPOCH=$2; shift 2 ;;
--create-dir-name)
CREATE_DIR_NAME=$2; shift 2 ;;
--create-file-name)
CREATE_FILE_NAME=$2; shift 2 ;;
--readonly-user)
READONLY_USER=$2; shift 2 ;;
--readonly-password)
@@ -123,6 +127,9 @@ esac
case "$CREATE_DIR_NAME" in
""|*/*|*\\*|*:*) echo "--create-dir-name must be a single path component" >&2; exit 2 ;;
esac
case "$CREATE_FILE_NAME" in
""|*/*|*\\*|*:*) echo "--create-file-name must be a single path component" >&2; exit 2 ;;
esac
case "$LOG_WINDOW_SECONDS" in
''|*[!0-9]*) echo "--log-window-seconds must be a positive integer" >&2; exit 2 ;;
esac
@@ -134,10 +141,17 @@ fi
if [ -z "$CREATE_DIR_NAME" ]; then
CREATE_DIR_NAME=$(date +a%H%M%S)
fi
if [ -z "$CREATE_FILE_NAME" ]; then
CREATE_FILE_NAME=$(printf 'f%04d%01d' "$((10#$(date +%H%M) % 10000))" "$((RANDOM % 10))")
fi
case "$CREATE_DIR_NAME" in
???????|??????|?????|????|???|??|?) ;;
*) echo "--create-dir-name must be at most seven characters so the AFP 2.0 companion name stays DOS-compatible" >&2; exit 2 ;;
esac
case "$CREATE_FILE_NAME" in
???????|??????|?????|????|???|??|?) ;;
*) echo "--create-file-name must be at most seven characters so the AFP 2.0 companion name stays DOS-compatible" >&2; exit 2 ;;
esac
DIR_PATH=$NETWARE_PATH
case "$NETWARE_PATH" in
@@ -152,6 +166,14 @@ UNIX_DIR_PATH=$UNIX_PARENT_PATH/$CREATE_DIR_NAME
UNIX_DIR20_PATH=$UNIX_PARENT_PATH/${CREATE_DIR_NAME}2
UNIX_DIR_PATH_DOS=$UNIX_PARENT_PATH/$CREATE_DIR_NAME_DOS
UNIX_DIR20_PATH_DOS=$UNIX_PARENT_PATH/$CREATE_DIR20_NAME_DOS
CREATE_FILE_PATH="$DIR_PATH/$CREATE_FILE_NAME"
CREATE_FILE20_PATH="$DIR_PATH/${CREATE_FILE_NAME}2"
CREATE_FILE_NAME_DOS=$(printf '%s' "$CREATE_FILE_NAME" | tr '[:lower:]' '[:upper:]')
CREATE_FILE20_NAME_DOS=$(printf '%s' "${CREATE_FILE_NAME}2" | tr '[:lower:]' '[:upper:]')
UNIX_FILE_PATH=$UNIX_PARENT_PATH/$CREATE_FILE_NAME
UNIX_FILE20_PATH=$UNIX_PARENT_PATH/${CREATE_FILE_NAME}2
UNIX_FILE_PATH_DOS=$UNIX_PARENT_PATH/$CREATE_FILE_NAME_DOS
UNIX_FILE20_PATH_DOS=$UNIX_PARENT_PATH/$CREATE_FILE20_NAME_DOS
REPORT_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-afp-smoke.XXXXXX")
LOG_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-afp-log.XXXXXX")
@@ -216,6 +238,19 @@ cleanup_created_dir() {
fi
}
cleanup_created_file() {
local path=$1
local dos_path=$2
if [ -f "$path" ]; then
rm -f "$path"
elif [ -f "$dos_path" ]; then
rm -f "$dos_path"
else
rm -f "$path"
fi
}
run_optional_cmd() {
local label=$1
local printable=$2
@@ -300,6 +335,12 @@ emit "unix_dir_path=$UNIX_DIR_PATH"
emit "unix_dir20_path=$UNIX_DIR20_PATH"
emit "unix_dir_path_dos=$UNIX_DIR_PATH_DOS"
emit "unix_dir20_path_dos=$UNIX_DIR20_PATH_DOS"
emit "create_file_path=$CREATE_FILE_PATH"
emit "create_file20_path=$CREATE_FILE20_PATH"
emit "unix_file_path=$UNIX_FILE_PATH"
emit "unix_file20_path=$UNIX_FILE20_PATH"
emit "unix_file_path_dos=$UNIX_FILE_PATH_DOS"
emit "unix_file20_path_dos=$UNIX_FILE20_PATH_DOS"
if [ -n "$READONLY_USER" ]; then
emit "readonly_user=$READONLY_USER"
if [ "$READONLY_NO_PASSWORD" -eq 1 ]; then
@@ -317,6 +358,7 @@ for helper in \
afp_dos_name_smoke \
afp_scan_info_smoke \
afp_create_directory_smoke \
afp_create_file_smoke \
afp_temp_dir_handle_smoke \
afp_open_file_fork_smoke \
afp_set_file_info_smoke
@@ -422,6 +464,40 @@ run_optional_cmd \
"rmdir '$UNIX_DIR20_PATH' or '$UNIX_DIR20_PATH_DOS'" \
cleanup_created_dir "$UNIX_DIR20_PATH" "$UNIX_DIR20_PATH_DOS" || true
if [ -f "$UNIX_FILE_PATH" ] || [ -f "$UNIX_FILE_PATH_DOS" ]; then
run_optional_cmd \
"Prepare AFP Create File cleanup" \
"rm '$UNIX_FILE_PATH' or '$UNIX_FILE_PATH_DOS'" \
cleanup_created_file "$UNIX_FILE_PATH" "$UNIX_FILE_PATH_DOS" || true
fi
if [ -f "$UNIX_FILE20_PATH" ] || [ -f "$UNIX_FILE20_PATH_DOS" ]; then
run_optional_cmd \
"Prepare AFP 2.0 Create File cleanup" \
"rm '$UNIX_FILE20_PATH' or '$UNIX_FILE20_PATH_DOS'" \
cleanup_created_file "$UNIX_FILE20_PATH" "$UNIX_FILE20_PATH_DOS" || true
fi
run_cmd \
"AFP Create File" \
"./afp_create_file_smoke $COMMON_PRINT '$CREATE_FILE_PATH'" \
"$SCRIPT_DIR/afp_create_file_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_FILE_PATH"
run_cmd \
"AFP 2.0 Create File" \
"./afp_create_file_smoke --afp20 $COMMON_PRINT '$CREATE_FILE20_PATH'" \
"$SCRIPT_DIR/afp_create_file_smoke" --afp20 \
-S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" "$CREATE_FILE20_PATH"
run_optional_cmd \
"Cleanup AFP Create File" \
"rm '$UNIX_FILE_PATH' or '$UNIX_FILE_PATH_DOS'" \
cleanup_created_file "$UNIX_FILE_PATH" "$UNIX_FILE_PATH_DOS" || true
run_optional_cmd \
"Cleanup AFP 2.0 Create File" \
"rm '$UNIX_FILE20_PATH' or '$UNIX_FILE20_PATH_DOS'" \
cleanup_created_file "$UNIX_FILE20_PATH" "$UNIX_FILE20_PATH_DOS" || true
run_cmd \
"AFP Open File Fork" \
"./afp_open_file_fork_smoke $COMMON_PRINT '$NETWARE_PATH'" \