From 374b7fb8e474cc2a6bc89116bb60f792714cb5b5 Mon Sep 17 00:00:00 2001 From: a Date: Fri, 19 Jun 2026 19:03:25 +0000 Subject: [PATCH] nwnss: add Netatalk AFP userspace xattr boundary --- include/nwnss/internal/macNSpaceUserspace.h | 57 +++++ src/nwnss/CMakeLists.txt | 1 + src/nwnss/comn/namespace/macNSpaceUserspace.c | 233 ++++++++++++++++++ tests/nwnss/CMakeLists.txt | 1 + tests/nwnss/afp/CMakeLists.txt | 4 + tests/nwnss/afp/test_nwnss_afp.c | 118 +++++++++ 6 files changed, 414 insertions(+) create mode 100644 include/nwnss/internal/macNSpaceUserspace.h create mode 100644 src/nwnss/comn/namespace/macNSpaceUserspace.c create mode 100644 tests/nwnss/afp/CMakeLists.txt create mode 100644 tests/nwnss/afp/test_nwnss_afp.c diff --git a/include/nwnss/internal/macNSpaceUserspace.h b/include/nwnss/internal/macNSpaceUserspace.h new file mode 100644 index 0000000..de91ec7 --- /dev/null +++ b/include/nwnss/internal/macNSpaceUserspace.h @@ -0,0 +1,57 @@ +/**************************************************************************** + | + | Userspace boundary for NSS Macintosh namespace metadata. + | + | The NSS MAC namespace stores Finder metadata as RVD_MAC_META_DATA and + | resource forks as the data stream named MAC_RF. This boundary maps those + | semantics to Netatalk's adouble:ea host representation for OtherFS userspace + | providers, per object, without extending netware.metadata. + +-------------------------------------------------------------------------*/ +#ifndef _MACNSPACE_USERSPACE_H_ +#define _MACNSPACE_USERSPACE_H_ + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NSS_MAC_NETATALK_METADATA_XATTR "org.netatalk.Metadata" +#define NSS_MAC_NETATALK_RESOURCE_XATTR "org.netatalk.ResourceFork" +#define NSS_MAC_RESOURCE_FORK_STREAM_NAME "MAC_RF" + +#define NSS_MAC_NETATALK_AD_MAGIC 0x00051607u +#define NSS_MAC_NETATALK_AD_VERSION_EA 0x00020002u +#define NSS_MAC_NETATALK_METADATA_SIZE 402u +#define NSS_MAC_NETATALK_FINDER_INFO_SIZE 32u + +const char *NssMacUserspaceMetadataXattrName(void); +const char *NssMacUserspaceResourceForkXattrName(void); +const char *NssMacUserspaceResourceForkStreamName(void); + +void NssMacUserspaceInitDefaultMacInfo(zMacInfo_s *macInfo, int isDirectory); +int NssMacUserspaceBuildNetatalkMetadata(const zMacInfo_s *macInfo, + unsigned char *buffer, + size_t bufferSize); +int NssMacUserspaceParseNetatalkMetadata(const unsigned char *buffer, + size_t bufferSize, + zMacInfo_s *macInfo); + +int NssMacUserspaceWriteNetatalkMetadata(const char *path, + const zMacInfo_s *macInfo); +int NssMacUserspaceReadNetatalkMetadata(const char *path, + zMacInfo_s *macInfo); +int NssMacUserspaceWriteResourceFork(const char *path, + const void *data, + size_t dataSize); +ssize_t NssMacUserspaceReadResourceFork(const char *path, + void *data, + size_t dataSize); + +#ifdef __cplusplus +} +#endif + +#endif /* _MACNSPACE_USERSPACE_H_ */ diff --git a/src/nwnss/CMakeLists.txt b/src/nwnss/CMakeLists.txt index bdb8ae9..d46b5d1 100644 --- a/src/nwnss/CMakeLists.txt +++ b/src/nwnss/CMakeLists.txt @@ -244,6 +244,7 @@ add_library(nwnss SHARED comn/namespace/dataStreamNSpace.c comn/namespace/extAttrNSpace.c comn/namespace/macNSpace.c + comn/namespace/macNSpaceUserspace.c nss/msg/msg.c nssStartupNameGlobals.c library/misc/lbVolume.c diff --git a/src/nwnss/comn/namespace/macNSpaceUserspace.c b/src/nwnss/comn/namespace/macNSpaceUserspace.c new file mode 100644 index 0000000..bbcb78e --- /dev/null +++ b/src/nwnss/comn/namespace/macNSpaceUserspace.c @@ -0,0 +1,233 @@ +/**************************************************************************** + | + | Userspace boundary for NSS Macintosh namespace metadata. + | + | Original NSS MAC namespace semantics remain in macNSpace.c: + | - Finder/Mac metadata is RVD_MAC_META_DATA (zMacInfo_s/PackedMacInfo_s) + | - Resource forks are the MAC_RF data stream + | + | OtherFS userspace providers do not have an NSS media variable-data store. + | For that case, map the same per-object semantics to Netatalk's adouble:ea + | host representation: + | - org.netatalk.Metadata + | - org.netatalk.ResourceFork + | + | Do not extend netware.metadata and do not create a separate netware AFP + | xattr. This representation is a provider boundary, not NSS media format. + +-------------------------------------------------------------------------*/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define AD_ENTRY_LEN 12u +#define AD_HEADER_LEN 26u +#define ADEID_NUM_EA 8u +#define ADEID_FINDERI 9u +#define ADEID_COMMENT 4u +#define ADEID_FILEDATESI 8u +#define ADEID_AFPFILEI 14u +#define AD_DEV 0x80444556u +#define AD_INO 0x80494E4Fu +#define AD_SYN 0x8053594Eu +#define AD_ID 0x8053567Eu + +#define ADEDLEN_FINDERI 32u +#define ADEDLEN_COMMENT 200u +#define ADEDLEN_FILEDATESI 16u +#define ADEDLEN_AFPFILEI 4u +#define ADEDLEN_PRIVDEV 8u +#define ADEDLEN_PRIVINO 8u +#define ADEDLEN_PRIVSYN 8u +#define ADEDLEN_PRIVID 4u + +#define ADEDOFF_FINDERI_EA (AD_HEADER_LEN + (ADEID_NUM_EA * AD_ENTRY_LEN)) +#define ADEDOFF_COMMENT_EA (ADEDOFF_FINDERI_EA + ADEDLEN_FINDERI) +#define ADEDOFF_FILEDATESI_EA (ADEDOFF_COMMENT_EA + ADEDLEN_COMMENT) +#define ADEDOFF_AFPFILEI_EA (ADEDOFF_FILEDATESI_EA + ADEDLEN_FILEDATESI) +#define ADEDOFF_PRIVDEV_EA (ADEDOFF_AFPFILEI_EA + ADEDLEN_AFPFILEI) +#define ADEDOFF_PRIVINO_EA (ADEDOFF_PRIVDEV_EA + ADEDLEN_PRIVDEV) +#define ADEDOFF_PRIVSYN_EA (ADEDOFF_PRIVINO_EA + ADEDLEN_PRIVINO) +#define ADEDOFF_PRIVID_EA (ADEDOFF_PRIVSYN_EA + ADEDLEN_PRIVSYN) + +static void put_be16(unsigned char *buf, uint16_t value) +{ + uint16_t disk = htons(value); + memcpy(buf, &disk, sizeof(disk)); +} + +static void put_be32(unsigned char *buf, uint32_t value) +{ + uint32_t disk = htonl(value); + memcpy(buf, &disk, sizeof(disk)); +} + +static uint32_t get_be32(const unsigned char *buf) +{ + uint32_t disk; + memcpy(&disk, buf, sizeof(disk)); + return ntohl(disk); +} + +static void put_entry(unsigned char *buf, uint32_t eid, uint32_t offset, + uint32_t length) +{ + put_be32(buf, eid); + put_be32(buf + 4, offset); + put_be32(buf + 8, length); +} + +const char *NssMacUserspaceMetadataXattrName(void) +{ + return NSS_MAC_NETATALK_METADATA_XATTR; +} + +const char *NssMacUserspaceResourceForkXattrName(void) +{ + return NSS_MAC_NETATALK_RESOURCE_XATTR; +} + +const char *NssMacUserspaceResourceForkStreamName(void) +{ + return NSS_MAC_RESOURCE_FORK_STREAM_NAME; +} + +void NssMacUserspaceInitDefaultMacInfo(zMacInfo_s *macInfo, int isDirectory) +{ + if (!macInfo) + return; + + memset(macInfo, 0, sizeof(*macInfo)); + if (!isDirectory) { + macInfo->finderInfo[10] = 0xff; + macInfo->finderInfo[11] = 0xff; + macInfo->finderInfo[12] = 0xff; + macInfo->finderInfo[13] = 0xff; + } +} + +int NssMacUserspaceBuildNetatalkMetadata(const zMacInfo_s *macInfo, + unsigned char *buffer, + size_t bufferSize) +{ + unsigned char *entry; + + if (!macInfo || !buffer) + return -EINVAL; + if (bufferSize < NSS_MAC_NETATALK_METADATA_SIZE) + return -ENOSPC; + + memset(buffer, 0, bufferSize); + put_be32(buffer, NSS_MAC_NETATALK_AD_MAGIC); + put_be32(buffer + 4, NSS_MAC_NETATALK_AD_VERSION_EA); + put_be16(buffer + 24, ADEID_NUM_EA); + + entry = buffer + AD_HEADER_LEN; + put_entry(entry, ADEID_FINDERI, ADEDOFF_FINDERI_EA, ADEDLEN_FINDERI); + put_entry(entry + AD_ENTRY_LEN, ADEID_COMMENT, ADEDOFF_COMMENT_EA, + ADEDLEN_COMMENT); + put_entry(entry + (2 * AD_ENTRY_LEN), ADEID_FILEDATESI, + ADEDOFF_FILEDATESI_EA, ADEDLEN_FILEDATESI); + put_entry(entry + (3 * AD_ENTRY_LEN), ADEID_AFPFILEI, + ADEDOFF_AFPFILEI_EA, ADEDLEN_AFPFILEI); + put_entry(entry + (4 * AD_ENTRY_LEN), AD_DEV, ADEDOFF_PRIVDEV_EA, + ADEDLEN_PRIVDEV); + put_entry(entry + (5 * AD_ENTRY_LEN), AD_INO, ADEDOFF_PRIVINO_EA, + ADEDLEN_PRIVINO); + put_entry(entry + (6 * AD_ENTRY_LEN), AD_SYN, ADEDOFF_PRIVSYN_EA, + ADEDLEN_PRIVSYN); + put_entry(entry + (7 * AD_ENTRY_LEN), AD_ID, ADEDOFF_PRIVID_EA, + ADEDLEN_PRIVID); + + memcpy(buffer + ADEDOFF_FINDERI_EA, macInfo->finderInfo, + ADEDLEN_FINDERI); + /* + * Netatalk's adouble:ea metadata has no ProDOS field. zMacInfo_s + * proDOSInfo and dirRightsMask remain NSS MAC namespace data and must not + * be forced into an invented host xattr layout. + */ + return 0; +} + +int NssMacUserspaceParseNetatalkMetadata(const unsigned char *buffer, + size_t bufferSize, + zMacInfo_s *macInfo) +{ + if (!buffer || !macInfo) + return -EINVAL; + if (bufferSize < NSS_MAC_NETATALK_METADATA_SIZE) + return -EINVAL; + if (get_be32(buffer) != NSS_MAC_NETATALK_AD_MAGIC || + get_be32(buffer + 4) != NSS_MAC_NETATALK_AD_VERSION_EA) + return -EINVAL; + + memset(macInfo, 0, sizeof(*macInfo)); + memcpy(macInfo->finderInfo, buffer + ADEDOFF_FINDERI_EA, + ADEDLEN_FINDERI); + return 0; +} + +int NssMacUserspaceWriteNetatalkMetadata(const char *path, + const zMacInfo_s *macInfo) +{ + unsigned char buffer[NSS_MAC_NETATALK_METADATA_SIZE]; + int rc; + + if (!path) + return -EINVAL; + + rc = NssMacUserspaceBuildNetatalkMetadata(macInfo, buffer, + sizeof(buffer)); + if (rc) + return rc; + + if (setxattr(path, NSS_MAC_NETATALK_METADATA_XATTR, buffer, + sizeof(buffer), 0) != 0) + return -errno; + return 0; +} + +int NssMacUserspaceReadNetatalkMetadata(const char *path, + zMacInfo_s *macInfo) +{ + unsigned char buffer[NSS_MAC_NETATALK_METADATA_SIZE]; + ssize_t got; + + if (!path) + return -EINVAL; + + got = getxattr(path, NSS_MAC_NETATALK_METADATA_XATTR, buffer, + sizeof(buffer)); + if (got < 0) + return -errno; + return NssMacUserspaceParseNetatalkMetadata(buffer, (size_t)got, + macInfo); +} + +int NssMacUserspaceWriteResourceFork(const char *path, + const void *data, + size_t dataSize) +{ + if (!path || (!data && dataSize)) + return -EINVAL; + if (setxattr(path, NSS_MAC_NETATALK_RESOURCE_XATTR, data, dataSize, 0) != 0) + return -errno; + return 0; +} + +ssize_t NssMacUserspaceReadResourceFork(const char *path, + void *data, + size_t dataSize) +{ + if (!path) + return -EINVAL; + return getxattr(path, NSS_MAC_NETATALK_RESOURCE_XATTR, data, dataSize); +} diff --git a/tests/nwnss/CMakeLists.txt b/tests/nwnss/CMakeLists.txt index 74e4aa7..21f8b0f 100644 --- a/tests/nwnss/CMakeLists.txt +++ b/tests/nwnss/CMakeLists.txt @@ -2,6 +2,7 @@ add_subdirectory(aes) add_subdirectory(alarm) +add_subdirectory(afp) add_subdirectory(asyncio) add_subdirectory(bit) add_subdirectory(bitmap) diff --git a/tests/nwnss/afp/CMakeLists.txt b/tests/nwnss/afp/CMakeLists.txt new file mode 100644 index 0000000..a5dacca --- /dev/null +++ b/tests/nwnss/afp/CMakeLists.txt @@ -0,0 +1,4 @@ +add_executable(test_nwnss_afp test_nwnss_afp.c) +target_link_libraries(test_nwnss_afp PRIVATE mars_nwe::nwnss) +add_test(NAME nwnss.afp COMMAND test_nwnss_afp) +set_tests_properties(nwnss.afp PROPERTIES SKIP_RETURN_CODE 77) diff --git a/tests/nwnss/afp/test_nwnss_afp.c b/tests/nwnss/afp/test_nwnss_afp.c new file mode 100644 index 0000000..76aa1d5 --- /dev/null +++ b/tests/nwnss/afp/test_nwnss_afp.c @@ -0,0 +1,118 @@ +#include + +#include +#include +#include +#include +#include +#include + +#define SKIP_CODE 77 + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "check failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + return 1; \ + } \ + } while (0) + +static int is_xattr_unsupported(int err) +{ + return err == ENOTSUP || err == EOPNOTSUPP || err == ENOSYS || + err == EPERM || err == EACCES; +} + +static int make_temp_file(char *path, size_t pathSize) +{ + const char *tmpdir = getenv("TMPDIR"); + int fd; + + if (!tmpdir || tmpdir[0] == '\0') + tmpdir = "."; + if (snprintf(path, pathSize, "%s/nwnss-afp-XXXXXX", tmpdir) < 0 || + strlen(path) >= pathSize) + return -1; + fd = mkstemp(path); + if (fd < 0) + return -1; + close(fd); + return 0; +} + +int main(void) +{ + unsigned char metadata[NSS_MAC_NETATALK_METADATA_SIZE]; + unsigned char hostMetadata[NSS_MAC_NETATALK_METADATA_SIZE]; + zMacInfo_s macInfo; + zMacInfo_s roundtrip; + char tempPath[4096]; + const char resource[] = "resource fork payload"; + char resourceOut[sizeof(resource)]; + ssize_t got; + int rc; + + CHECK(strcmp(NssMacUserspaceMetadataXattrName(), + "org.netatalk.Metadata") == 0); + CHECK(strcmp(NssMacUserspaceResourceForkXattrName(), + "org.netatalk.ResourceFork") == 0); + CHECK(strcmp(NssMacUserspaceResourceForkStreamName(), "MAC_RF") == 0); + + NssMacUserspaceInitDefaultMacInfo(&macInfo, 0); + CHECK(macInfo.finderInfo[10] == 0xff); + CHECK(macInfo.finderInfo[11] == 0xff); + CHECK(macInfo.finderInfo[12] == 0xff); + CHECK(macInfo.finderInfo[13] == 0xff); + NssMacUserspaceInitDefaultMacInfo(&macInfo, 1); + CHECK(macInfo.finderInfo[10] == 0x00); + CHECK(macInfo.finderInfo[11] == 0x00); + CHECK(macInfo.finderInfo[12] == 0x00); + CHECK(macInfo.finderInfo[13] == 0x00); + + memset(&macInfo, 0, sizeof(macInfo)); + memcpy(macInfo.finderInfo, "NSSAFP-FINDER-INFO-ROUNDTRIP!", 30); + CHECK(NssMacUserspaceBuildNetatalkMetadata(&macInfo, metadata, + sizeof(metadata)) == 0); + CHECK(NssMacUserspaceParseNetatalkMetadata(metadata, sizeof(metadata), + &roundtrip) == 0); + CHECK(memcmp(roundtrip.finderInfo, macInfo.finderInfo, + sizeof(macInfo.finderInfo)) == 0); + + CHECK(make_temp_file(tempPath, sizeof(tempPath)) == 0); + rc = NssMacUserspaceWriteNetatalkMetadata(tempPath, &macInfo); + if (rc == -ENOTSUP || rc == -EOPNOTSUPP || rc == -ENOSYS || + rc == -EPERM || rc == -EACCES) { + fprintf(stderr, + "SKIP/WARN: host filesystem does not allow Netatalk AFP xattrs on test file\n"); + unlink(tempPath); + return SKIP_CODE; + } + CHECK(rc == 0); + + got = getxattr(tempPath, "org.netatalk.Metadata", hostMetadata, + sizeof(hostMetadata)); + if (got < 0 && is_xattr_unsupported(errno)) { + fprintf(stderr, + "SKIP/WARN: host filesystem does not allow Netatalk AFP xattrs on test file\n"); + unlink(tempPath); + return SKIP_CODE; + } + CHECK(got == (ssize_t)sizeof(hostMetadata)); + CHECK(memcmp(hostMetadata, metadata, sizeof(metadata)) == 0); + + memset(&roundtrip, 0, sizeof(roundtrip)); + CHECK(NssMacUserspaceReadNetatalkMetadata(tempPath, &roundtrip) == 0); + CHECK(memcmp(roundtrip.finderInfo, macInfo.finderInfo, + sizeof(macInfo.finderInfo)) == 0); + + CHECK(NssMacUserspaceWriteResourceFork(tempPath, resource, + sizeof(resource)) == 0); + memset(resourceOut, 0, sizeof(resourceOut)); + got = NssMacUserspaceReadResourceFork(tempPath, resourceOut, + sizeof(resourceOut)); + CHECK(got == (ssize_t)sizeof(resource)); + CHECK(memcmp(resourceOut, resource, sizeof(resource)) == 0); + + unlink(tempPath); + return 0; +}