Files
mars-nwe/tests/linux/afp_scan_info_smoke.c
Mario Fetka 8e739a1ac2
All checks were successful
Source release / source-package (push) Successful in 49s
nwconn: route AFP scan file information
Add the older WebSDK/NWAFP AFP Scan File Information subfunction 0x0a to the AFP dispatcher and route it through the existing conservative directory-scan implementation used by AFP 2.0 Scan File Information 0x11.

The Micro Focus NCP documentation describes both scan variants as read-only directory/file information scans with a Mac base Entry ID, Mac last-seen ID, DesiredResponseCount, SearchBitMap, RequestBitMap, and path modifier.  The mars_nwe compatibility subset still requires a raw path-backed VOL:-style request because persistent CNID/base-ID lookup is not available yet, but accepting 0x0a lets older AFP callers probe the same read-only semantics instead of receiving Invalid Namespace for an otherwise implemented scan shape.

Teach the scan parser to accept the documented DesiredResponseCount word while keeping compatibility with the earlier compact smoke-test layout that omitted it.  For now DesiredResponseCount is logged and constrained by the smoke helper, while the server still returns one conservative file-info record plus the next-last-seen entry id per request.  This avoids pretending to implement full multi-response AFP directory scans before CNID-backed ordering, response-count batching, and AppleDouble metadata are available.

Extend afp_scan_info_smoke with --afp10/--afp20 selection and a documented --desired-count argument.  The helper now sends the WebSDK-style request layout by default, uses 0x11 unless --afp10 is requested, and keeps the existing next_last_seen output used for iterative Linux smoke tests.

Tests:

- git diff --check

- gcc -fsyntax-only tests/linux/afp_scan_info_smoke.c with local ncpfs header stubs

- cmake --build build-off --target nwconn with ENABLE_NETATALK_LIBATALK=OFF

- cmake --build build-on --target nwconn with ENABLE_NETATALK_LIBATALK=ON against Netatalk 4.4.3 headers and local link stubs

TODO:

- Implement persistent CNID/base-ID lookup so entry-id-only scans can work without a raw path.

- Replace the one-record smoke response with true DesiredResponseCount batching once stable CNID ordering and full AFP scan filtering are available.

- Fill richer AppleDouble/Finder/ProDOS/resource-fork metadata when the libatalk backend grows write-safe metadata support.
2026-05-30 10:39:02 +02:00

288 lines
9.3 KiB
C

/*
* Linux smoke test for NetWare AFP Scan File Information.
*/
#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_SCAN_FILE_INFORMATION 0x0a
#define AFP20_SCAN_FILE_INFORMATION 0x11
#define NWE_INVALID_NAMESPACE 0xbf
#define NWE_INVALID_PATH 0x9c
#define NWE_NO_FILES_FOUND 0xff
#define AFP_SCAN_REPLY_LEN 124
#define AFP_GET_ALL 0xffff
#define AFP_SEARCH_ALL 0xffff
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--afp10|--afp20] [--allow-invalid-namespace] "
"[--allow-invalid-path] [--allow-empty] [--volume N] "
"[--entry-id ID] [--last-seen ID] [--desired-count N] "
"[--search-mask MASK] [--request-mask MASK] [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\n"
" %s --afp10 -S MARS -U SUPERVISOR -P secret SYS:PUBLIC\n"
" %s --allow-empty -S MARS -U SUPERVISOR -P secret SYS:EMPTYDIR\n",
prog, 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 uint16_t be16_to_cpu(const uint8_t p[2])
{
return ((uint16_t)p[0] << 8) | p[1];
}
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 void cpu_to_be16(uint16_t v, uint8_t p[2])
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)v;
}
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 void copy_fixed_string(char *dst, size_t dstlen,
const uint8_t *src, size_t srclen)
{
size_t i;
if (!dstlen)
return;
for (i = 0; i + 1 < dstlen && i < srclen && src[i]; i++)
dst[i] = (char)src[i];
dst[i] = '\0';
}
int main(int argc, char **argv)
{
NWCONN_HANDLE conn;
NW_FRAGMENT reply;
long init_err = 0;
const char *path = NULL;
int allow_invalid_namespace = 0;
int allow_invalid_path = 0;
int allow_empty = 0;
uint32_t volume_number = 0;
uint32_t entry_id = 0;
uint32_t last_seen_id = 0;
uint32_t desired_count = 1;
uint32_t search_mask = AFP_SEARCH_ALL;
uint32_t request_mask = AFP_GET_ALL;
uint32_t afp_subfunction = AFP20_SCAN_FILE_INFORMATION;
int i;
size_t path_len;
uint8_t request[1 + 4 + 4 + 2 + 2 + 2 + 1 + 255];
uint8_t reply_buf[AFP_SCAN_REPLY_LEN];
char long_name[33];
char short_name[13];
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], "--afp10")) {
afp_subfunction = AFP_SCAN_FILE_INFORMATION;
} else if (!strcmp(argv[i], "--afp20")) {
afp_subfunction = AFP20_SCAN_FILE_INFORMATION;
} else if (!strcmp(argv[i], "--allow-invalid-namespace")) {
allow_invalid_namespace = 1;
} else if (!strcmp(argv[i], "--allow-invalid-path")) {
allow_invalid_path = 1;
} else if (!strcmp(argv[i], "--allow-empty")) {
allow_empty = 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], &entry_id)) {
fprintf(stderr, "invalid --entry-id value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--last-seen")) {
if (++i >= argc || parse_u32(argv[i], &last_seen_id)) {
fprintf(stderr, "invalid --last-seen value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--desired-count")) {
if (++i >= argc || parse_u32(argv[i], &desired_count) || desired_count > 4) {
fprintf(stderr, "invalid --desired-count value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--search-mask")) {
if (++i >= argc || parse_u32(argv[i], &search_mask) || search_mask > 0xffff) {
fprintf(stderr, "invalid --search-mask value\n");
ncp_close(conn);
return 2;
}
} else if (!strcmp(argv[i], "--request-mask")) {
if (++i >= argc || parse_u32(argv[i], &request_mask) || request_mask > 0xffff) {
fprintf(stderr, "invalid --request-mask value\n");
ncp_close(conn);
return 2;
}
} 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;
}
path_len = strlen(path);
if (path_len > 255) {
fprintf(stderr, "PATH is too long for AFP Scan File Information: %zu\n", path_len);
ncp_close(conn);
return 2;
}
memset(request, 0, sizeof(request));
request[0] = (uint8_t)volume_number;
cpu_to_be32(entry_id, request + 1);
cpu_to_be32(last_seen_id, request + 5);
cpu_to_be16((uint16_t)desired_count, request + 9);
cpu_to_be16((uint16_t)search_mask, request + 11);
cpu_to_be16((uint16_t)request_mask, request + 13);
request[15] = (uint8_t)path_len;
memcpy(request + 16, 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, (int)afp_subfunction),
request,
16 + path_len,
&reply);
if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) {
printf("AFP Scan File Information subfunction=0x%02x returned invalid namespace as expected for path=%s\n",
(unsigned int)afp_subfunction, path);
ncp_close(conn);
return 0;
}
if (err == NWE_INVALID_PATH && allow_invalid_path) {
printf("AFP Scan File Information subfunction=0x%02x returned invalid path as expected for path=%s\n",
(unsigned int)afp_subfunction, path);
ncp_close(conn);
return 0;
}
if (err == NWE_NO_FILES_FOUND && allow_empty) {
printf("AFP Scan File Information subfunction=0x%02x returned no files as expected for path=%s last_seen=0x%08x\n",
(unsigned int)afp_subfunction, path, last_seen_id);
ncp_close(conn);
return 0;
}
if (err) {
fprintf(stderr,
"AFP Scan File Information subfunction=0x%02x failed: completion=0x%02x (%u) path=%s last_seen=0x%08x\n",
(unsigned int)afp_subfunction, (unsigned int)err & 0xff,
(unsigned int)err, path, last_seen_id);
ncp_close(conn);
return 1;
}
if (reply.fragSize < AFP_SCAN_REPLY_LEN) {
fprintf(stderr, "short AFP scan reply: %zu bytes\n", reply.fragSize);
ncp_close(conn);
return 1;
}
copy_fixed_string(long_name, sizeof(long_name), reply_buf + 4 + 64, 32);
copy_fixed_string(short_name, sizeof(short_name), reply_buf + 4 + 100, 12);
printf("AFP Scan File Info subfunction=0x%02x path=%s last_seen=0x%08x desired=%u next_last_seen=0x%08x "
"entry_id=0x%08x parent_id=0x%08x attrs=0x%04x data_len=%u "
"resource_len=%u offspring=%u long_name=%s short_name=%s rights=0x%04x\n",
(unsigned int)afp_subfunction,
path,
last_seen_id,
(unsigned int)desired_count,
be32_to_cpu(reply_buf + 0),
be32_to_cpu(reply_buf + 4),
be32_to_cpu(reply_buf + 8),
be16_to_cpu(reply_buf + 12),
be32_to_cpu(reply_buf + 14),
be32_to_cpu(reply_buf + 18),
be16_to_cpu(reply_buf + 22),
long_name,
short_name,
be16_to_cpu(reply_buf + 4 + 112));
ncp_close(conn);
return 0;
}