diff --git a/src/nwconn.c b/src/nwconn.c index 5072399..4de9542 100644 --- a/src/nwconn.c +++ b/src/nwconn.c @@ -419,6 +419,7 @@ static const char *afp_call_name(int ufunc) switch (ufunc) { case 0x03: return("AFP Delete"); case 0x04: return("AFP Get Entry ID From Name"); + case 0x05: return("AFP Get File Information"); case 0x06: return("AFP Get Entry ID From NetWare Handle"); case 0x07: return("AFP Rename"); case 0x08: return("AFP Open File Fork"); @@ -532,6 +533,194 @@ static int afp_get_entry_id_from_path_name(uint8 *afp_req, int afp_len, return(4); } + +static void afp_copy_fixed_name(uint8 *dst, int dst_len, + const uint8 *src, int src_len) +{ + int i; + + memset(dst, 0, dst_len); + if (!src || src_len < 1) return; + if (src_len > dst_len) src_len = dst_len; + for (i = 0; i < src_len; i++) { + uint8 c = src[i]; + if (!c) break; + dst[i] = c; + } +} + +static void afp_leaf_name_from_path(uint8 *dst, int dst_len, + const uint8 *path, int path_len) +{ + int start = 0; + int end = path_len; + int i; + + if (!path || path_len < 1) { + afp_copy_fixed_name(dst, dst_len, (const uint8 *)"", 0); + return; + } + + while (end > 0 && (path[end - 1] == '/' || path[end - 1] == '\\')) + end--; + + for (i = end - 1; i >= 0; i--) { + if (path[i] == ':' || path[i] == '/' || path[i] == '\\') { + start = i + 1; + break; + } + } + + if (start >= end) { + int colon = -1; + for (i = 0; i < path_len; i++) { + if (path[i] == ':') { + colon = i; + break; + } + } + if (colon > 0) { + start = 0; + end = colon; + } + } + + afp_copy_fixed_name(dst, dst_len, path + start, end - start); +} + +static uint16 afp_basic_attributes(const struct stat *stb) +{ + if (S_ISDIR(stb->st_mode)) + return(0x0400); /* AFP_SA_SUBDIR */ + return(0x0000); /* AFP_SA_NORMAL */ +} + +static uint16 afp_basic_access_privileges(const struct stat *stb) +{ + if (S_ISDIR(stb->st_mode)) + return(0x4000 | 0x2000 | 0x8000); /* search, parental, modify attrs */ + return(0x0100 | 0x0200 | 0x0400 | 0x1000 | 0x8000); /* rw open delete modify */ +} + +static uint16 afp_count_offspring(const char *unixname, const struct stat *stb) +{ + DIR *dir; + struct dirent *de; + uint32 count = 0; + + if (!S_ISDIR(stb->st_mode)) return(0); + + dir = opendir(unixname); + if (!dir) return(0); + while ((de = readdir(dir)) != NULL) { + if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) + continue; + if (count < 0xffff) count++; + } + closedir(dir); + return((uint16)count); +} + +static int afp_get_file_information(uint8 *afp_req, int afp_len, + uint8 *response) +{ + uint8 volume_number; + uint32 request_entry_id; + uint16 request_mask; + int path_len; + int volume; + char unixname[PATH_MAX]; + struct stat stbuff; + uint32 entry_id = 0; + uint32 parent_id = 0; + uint32 resource_size = 0; + uint8 finder_info[NWATALK_FINDER_INFO_LEN]; + uint8 empty_path = 0; + int result; + + if (afp_len < 9) { + XDPRINTF((2,0, "AFP Get File Information rejected: short request len=%d", + afp_len)); + return(-0x7e); /* NCP Boundary Check Failed */ + } + + volume_number = afp_req[1]; + request_entry_id = GET_BE32(afp_req + 2); + request_mask = GET_BE16(afp_req + 6); + path_len = (int)afp_req[8]; + if (path_len < 0 || afp_len < 9 + path_len) { + XDPRINTF((2,0, "AFP Get File Information rejected: boundary check len=%d path_len=%d", + afp_len, path_len)); + return(-0x7e); + } + + if (!nwatalk_backend_available()) { + XDPRINTF((3,0, "AFP Get File Information rejected: libatalk backend unavailable")); + return(-0xbf); /* invalid namespace */ + } + + if (!path_len) { + XDPRINTF((2,0, "AFP Get File Information rejected: entry-id-only lookup unsupported vol=%d entry=0x%08x mask=0x%04x", + (int)volume_number, request_entry_id, request_mask)); + return(-0x9c); /* Invalid Path until persistent entry-id lookup exists */ + } + + volume = conn_get_kpl_unxname(unixname, sizeof(unixname), 0, + path_len ? afp_req + 9 : &empty_path, + path_len); + if (volume < 0) { + XDPRINTF((2,0, "AFP Get File Information path resolve failed: vol=%d entry=0x%08x path='%s' result=-0x%x", + (int)volume_number, request_entry_id, + visable_data(afp_req + 9, path_len), -volume)); + return(volume); + } + + if (stat(unixname, &stbuff)) { + XDPRINTF((2,0, "AFP Get File Information stat failed: vol=%d entry=0x%08x path='%s' unix='%s' errno=%d", + (int)volume_number, request_entry_id, + visable_data(afp_req + 9, path_len), unixname, errno)); + return(-0x9c); /* Invalid Path */ + } + + result = nwatalk_get_entry_id(unixname, &entry_id); + if (result < 0 || !entry_id) + entry_id = afp_fallback_entry_id(volume, &stbuff); + + memset(response, 0, 120); + U32_TO_BE32(entry_id, response + 0); + U32_TO_BE32(parent_id, response + 4); + U16_TO_BE16(afp_basic_attributes(&stbuff), response + 8); + U32_TO_BE32(S_ISDIR(stbuff.st_mode) ? 0 : (uint32)stbuff.st_size, response + 10); + + if (!S_ISDIR(stbuff.st_mode)) + (void)nwatalk_get_resource_fork_size(unixname, &resource_size); + U32_TO_BE32(resource_size, response + 14); + U16_TO_BE16(afp_count_offspring(unixname, &stbuff), response + 18); + + un_date_2_nw(stbuff.st_ctime, response + 20, 1); + un_date_2_nw(stbuff.st_atime, response + 22, 1); + un_date_2_nw(stbuff.st_mtime, response + 24, 1); + un_time_2_nw(stbuff.st_mtime, response + 26, 1); + U16_TO_BE16(0, response + 28); /* Backup Date */ + U16_TO_BE16(0, response + 30); /* Backup Time */ + + memset(finder_info, 0, sizeof(finder_info)); + (void)nwatalk_get_finder_info(unixname, finder_info, sizeof(finder_info)); + memcpy(response + 32, finder_info, NWATALK_FINDER_INFO_LEN); + + afp_leaf_name_from_path(response + 64, 32, afp_req + 9, path_len); + U32_TO_BE32(get_file_owner(&stbuff), response + 96); + afp_leaf_name_from_path(response + 100, 12, afp_req + 9, path_len); + U16_TO_BE16(afp_basic_access_privileges(&stbuff), response + 112); + /* ProDOS info at offset 114 stays zero until a real Mac namespace maps it. */ + + XDPRINTF((3,0, "AFP Get File Information: vol=%d entry=0x%08x mask=0x%04x path='%s' reply_entry=0x%08x%s", + (int)volume_number, request_entry_id, request_mask, + visable_data(afp_req + 9, path_len), entry_id, + (result < 0) ? " fallback" : "")); + return(120); +} + static int handle_ncp_serv(void) { int function = (int)ncprequest->function; @@ -2472,7 +2661,8 @@ static int handle_ncp_serv(void) * * WebSDK / headers identify the old NCP 0x2222/35 AFP * subfunctions used by nwafp.h, including AFP Delete - * (0x03), Get Entry ID From Name (0x04), Get Entry ID From + * (0x03), Get Entry ID From Name (0x04), Get File + * Information (0x05), Get Entry ID From * NetWare Handle (0x06), Rename (0x07), Open File Fork * (0x08), Alloc Temporary Dir Handle (0x0b), Get Entry ID * From Path Name (0x0c), the AFP 2.0 create calls @@ -2480,7 +2670,9 @@ static int handle_ncp_serv(void) * Scan File Information (0x11). * * Implement the path-name entry-id probe first because the - * SDK helpers use it to test AFP support. It still requires + * SDK helpers use it to test AFP support, then expose the + * read-only AFP Get File Information query for the same + * SYS:-style path inputs. Both still require * the optional libatalk backend to be present; without a Mac * namespace backend, keep returning invalid namespace. */ @@ -2489,6 +2681,11 @@ static int handle_ncp_serv(void) afp_len, responsedata); if (result > -1) data_len = result; else completition = (uint8)-result; + } else if (ufunc == 0x05) { + int result = afp_get_file_information(afp_req, + afp_len, responsedata); + if (result > -1) data_len = result; + else completition = (uint8)-result; } else { XDPRINTF((3,0, "AFP call rejected: ufunc=0x%02x (%s), Mac namespace unavailable, libatalk backend=%s", ufunc, afp_call_name(ufunc), diff --git a/tests/linux/CMakeLists.txt b/tests/linux/CMakeLists.txt index 01b1a29..9c09f41 100644 --- a/tests/linux/CMakeLists.txt +++ b/tests/linux/CMakeLists.txt @@ -21,3 +21,7 @@ endif() add_executable(afp_entry_id_smoke afp_entry_id_smoke.c) target_include_directories(afp_entry_id_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) target_link_libraries(afp_entry_id_smoke ${NCPFS_LIBRARY}) + +add_executable(afp_file_info_smoke afp_file_info_smoke.c) +target_include_directories(afp_file_info_smoke PRIVATE ${NCPFS_INCLUDE_DIR}) +target_link_libraries(afp_file_info_smoke ${NCPFS_LIBRARY}) diff --git a/tests/linux/afp_file_info_smoke.c b/tests/linux/afp_file_info_smoke.c new file mode 100644 index 0000000..410d43a --- /dev/null +++ b/tests/linux/afp_file_info_smoke.c @@ -0,0 +1,237 @@ +/* + * Linux smoke test for NetWare AFP Get File Information. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef NCPC_SUBFUNCTION +#define NCPC_SUBFUNCTION 0x10000 +#endif +#ifndef NCPC_SFN +#define NCPC_SFN(FN, SFN) ((FN) | ((SFN) << 8) | NCPC_SUBFUNCTION) +#endif + +#define AFP_GET_FILE_INFORMATION 0x05 +#define NWE_INVALID_NAMESPACE 0xbf +#define NWE_INVALID_PATH 0x9c +#define AFP_REPLY_LEN 120 +#define AFP_GET_ALL 0xffff + +static void usage(const char *prog) +{ + fprintf(stderr, + "Usage: %s [--allow-invalid-namespace] [--allow-invalid-path] " + "[--volume N] [--entry-id ID] [--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 --allow-invalid-namespace -S MARS SYS:PUBLIC\n" + " %s --allow-invalid-path -S MARS SYS:NO_SUCH_PATH\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; + uint32_t volume_number = 0; + uint32_t entry_id = 0; + uint32_t request_mask = AFP_GET_ALL; + int i; + size_t path_len; + uint8_t request[1 + 1 + 4 + 2 + 1 + 255]; + uint8_t reply_buf[AFP_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], "--allow-invalid-namespace")) { + allow_invalid_namespace = 1; + } else if (!strcmp(argv[i], "--allow-invalid-path")) { + allow_invalid_path = 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], "--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 Get File Information: %zu\n", path_len); + ncp_close(conn); + return 2; + } + + memset(request, 0, sizeof(request)); + request[0] = AFP_GET_FILE_INFORMATION; + request[1] = (uint8_t)volume_number; + cpu_to_be32(entry_id, request + 2); + cpu_to_be16((uint16_t)request_mask, request + 6); + request[8] = (uint8_t)path_len; + memcpy(request + 9, 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_FILE_INFORMATION), + request, + 9 + path_len, + &reply); + if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) { + printf("AFP Get File Information returned invalid namespace as expected for path=%s\n", path); + ncp_close(conn); + return 0; + } + if (err == NWE_INVALID_PATH && allow_invalid_path) { + printf("AFP Get File Information returned invalid path as expected for path=%s\n", path); + ncp_close(conn); + return 0; + } + if (err) { + fprintf(stderr, + "AFP Get File Information failed: completion=0x%02x (%u) path=%s\n", + (unsigned int)err & 0xff, (unsigned int)err, path); + ncp_close(conn); + return 1; + } + + if (reply.fragSize < AFP_REPLY_LEN) { + fprintf(stderr, "short AFP file-info reply: %zu bytes\n", reply.fragSize); + ncp_close(conn); + return 1; + } + + copy_fixed_string(long_name, sizeof(long_name), reply_buf + 64, 32); + copy_fixed_string(short_name, sizeof(short_name), reply_buf + 100, 12); + + printf("AFP File Info path=%s 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", + path, + be32_to_cpu(reply_buf + 0), + be32_to_cpu(reply_buf + 4), + be16_to_cpu(reply_buf + 8), + be32_to_cpu(reply_buf + 10), + be32_to_cpu(reply_buf + 14), + be16_to_cpu(reply_buf + 18), + long_name, + short_name, + be16_to_cpu(reply_buf + 112)); + + ncp_close(conn); + return 0; +}