Created
September 24, 2026 19:25
-
-
Save erichelgeson/c22abe1f39da0998f51ccbf7c6b938c6 to your computer and use it in GitHub Desktop.
rsrcfork_check.c
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // rsrcfork_check — probe whether a volume supports classic Mac resource forks. | |
| // | |
| // Build: clang -O2 -Wall -o rsrcfork_check rsrcfork_check.c | |
| // Usage: ./rsrcfork_check [directory-on-volume-to-test] (default: $TMPDIR) | |
| // | |
| // Exercises every public path to the resource fork: | |
| // - volume capability bits (getattrlist ATTR_VOL_CAPABILITIES) | |
| // - file/..namedfork/rsrc (POSIX path to the fork) | |
| // - com.apple.ResourceFork (xattr API, incl. offset writes for large forks) | |
| // - ATTR_FILE_RSRCLENGTH (fork size as reported by the filesystem) | |
| // - copyfile(COPYFILE_ALL) (fork preservation on copy) | |
| // - truncation / deletion of the fork | |
| #include <copyfile.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/attr.h> | |
| #include <sys/mount.h> | |
| #include <sys/stat.h> | |
| #include <sys/utsname.h> | |
| #include <sys/xattr.h> | |
| #include <unistd.h> | |
| static int passes, fails; | |
| static void result(int ok, const char *name, const char *detail) { | |
| printf(" [%s] %-48s %s\n", ok ? "PASS" : "FAIL", name, detail ? detail : ""); | |
| ok ? passes++ : fails++; | |
| } | |
| static void fill(unsigned char *buf, size_t n, unsigned seed) { | |
| for (size_t i = 0; i < n; i++) buf[i] = (unsigned char)((i * 31 + seed) & 0xff); | |
| } | |
| static int write_all(int fd, const void *buf, size_t n) { | |
| const unsigned char *p = buf; | |
| while (n) { | |
| ssize_t w = write(fd, p, n); | |
| if (w < 0) return -1; | |
| p += w; n -= (size_t)w; | |
| } | |
| return 0; | |
| } | |
| static ssize_t read_all(const char *path, unsigned char *buf, size_t cap) { | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) return -1; | |
| size_t total = 0; | |
| ssize_t r; | |
| while (total < cap && (r = read(fd, buf + total, cap - total)) > 0) total += (size_t)r; | |
| close(fd); | |
| return (ssize_t)total; | |
| } | |
| static int make_file(const char *path) { | |
| int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0644); | |
| if (fd < 0) return -1; | |
| int rc = write_all(fd, "data fork\n", 10); | |
| close(fd); | |
| return rc; | |
| } | |
| static long long rsrc_length(const char *path) { | |
| struct attrlist al = { .bitmapcount = ATTR_BIT_MAP_COUNT, .fileattr = ATTR_FILE_RSRCLENGTH }; | |
| struct { uint32_t len; off_t rsrclen; } __attribute__((packed)) buf; | |
| if (getattrlist(path, &al, &buf, sizeof buf, 0) != 0) return -1; | |
| return (long long)buf.rsrclen; | |
| } | |
| static void volume_info(const char *dir) { | |
| struct statfs sfs; | |
| if (statfs(dir, &sfs) == 0) | |
| printf("Volume: %s (fs type: %s, device: %s)\n", sfs.f_mntonname, sfs.f_fstypename, sfs.f_mntfromname); | |
| struct attrlist al = { .bitmapcount = ATTR_BIT_MAP_COUNT, .volattr = ATTR_VOL_INFO | ATTR_VOL_CAPABILITIES }; | |
| struct { uint32_t len; vol_capabilities_attr_t caps; } __attribute__((packed)) buf; | |
| if (getattrlist(sfs.f_mntonname, &al, &buf, sizeof buf, 0) != 0) { | |
| printf(" (could not read volume capabilities: %s)\n", strerror(errno)); | |
| return; | |
| } | |
| uint32_t v = buf.caps.valid[VOL_CAPABILITIES_INTERFACES]; | |
| uint32_t c = buf.caps.capabilities[VOL_CAPABILITIES_INTERFACES]; | |
| #define CAP(bit) (!(v & (bit)) ? "unknown" : (c & (bit)) ? "yes" : "no") | |
| printf(" Capability EXTENDED_ATTR : %s\n", CAP(VOL_CAP_INT_EXTENDED_ATTR)); | |
| printf(" Capability NAMEDSTREAMS : %s (filesystem advertises named-fork support)\n", | |
| CAP(VOL_CAP_INT_NAMEDSTREAMS)); | |
| #undef CAP | |
| } | |
| int main(int argc, char **argv) { | |
| const char *dir = argc > 1 ? argv[1] : (getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp"); | |
| struct utsname u; uname(&u); | |
| printf("rsrcfork_check — %s %s\n", u.sysname, u.release); | |
| volume_info(dir); | |
| printf("\n"); | |
| char file[1024], fork[1100], copy[1100], copyfork[1200]; | |
| snprintf(file, sizeof file, "%s/rsrcfork_check.%d", dir, getpid()); | |
| snprintf(fork, sizeof fork, "%s/..namedfork/rsrc", file); | |
| snprintf(copy, sizeof copy, "%s.copy", file); | |
| snprintf(copyfork, sizeof copyfork, "%s/..namedfork/rsrc", copy); | |
| if (make_file(file) != 0) { perror("create test file"); return 2; } | |
| enum { SMALL = 4096, LARGE = 4 * 1024 * 1024 }; | |
| unsigned char *want = malloc(LARGE), *got = malloc(LARGE + 1); | |
| char msg[256]; | |
| ssize_t n; | |
| // 1. Write via ..namedfork/rsrc, read back via same path. | |
| fill(want, SMALL, 1); | |
| int fd = open(fork, O_WRONLY | O_CREAT, 0644); | |
| int ok = fd >= 0 && write_all(fd, want, SMALL) == 0; | |
| if (fd >= 0) close(fd); | |
| snprintf(msg, sizeof msg, "%s", ok ? "" : strerror(errno)); | |
| result(ok, "write fork via ..namedfork/rsrc", msg); | |
| n = read_all(fork, got, LARGE); | |
| ok = n == SMALL && memcmp(want, got, SMALL) == 0; | |
| snprintf(msg, sizeof msg, "read %zd bytes", n); | |
| result(ok, "read fork via ..namedfork/rsrc", msg); | |
| // 2. Same bytes visible through the xattr API. | |
| n = getxattr(file, XATTR_RESOURCEFORK_NAME, got, LARGE, 0, 0); | |
| ok = n == SMALL && memcmp(want, got, SMALL) == 0; | |
| snprintf(msg, sizeof msg, "%zd bytes%s%s", n, n < 0 ? ": " : "", n < 0 ? strerror(errno) : ""); | |
| result(ok, "read same fork via getxattr(" XATTR_RESOURCEFORK_NAME ")", msg); | |
| // 3. Filesystem-reported fork length. | |
| long long len = rsrc_length(file); | |
| snprintf(msg, sizeof msg, "ATTR_FILE_RSRCLENGTH = %lld", len); | |
| result(len == SMALL, "getattrlist reports fork length", msg); | |
| // 4. Data fork untouched. | |
| struct stat st; stat(file, &st); | |
| snprintf(msg, sizeof msg, "st_size = %lld", (long long)st.st_size); | |
| result(st.st_size == 10, "data fork independent of resource fork", msg); | |
| // 5. Large fork via setxattr with offsets (how Carbon/copyfile write big forks), | |
| // read back via ..namedfork/rsrc. | |
| fill(want, LARGE, 7); | |
| ok = 1; | |
| for (size_t off = 0; off < LARGE && ok; off += 256 * 1024) | |
| ok = setxattr(file, XATTR_RESOURCEFORK_NAME, want + off, 256 * 1024, (uint32_t)off, 0) == 0; | |
| snprintf(msg, sizeof msg, "%s", ok ? "4 MiB in 256 KiB chunks" : strerror(errno)); | |
| result(ok, "write 4 MiB fork via setxattr(position)", msg); | |
| n = read_all(fork, got, LARGE + 1); | |
| ok = n == LARGE && memcmp(want, got, LARGE) == 0; | |
| snprintf(msg, sizeof msg, "read %zd bytes", n); | |
| result(ok, "read 4 MiB fork via ..namedfork/rsrc", msg); | |
| // 6. Listed in xattr names. | |
| char names[4096]; | |
| n = listxattr(file, names, sizeof names, 0); | |
| ok = 0; | |
| for (ssize_t i = 0; i < n; i += strlen(names + i) + 1) | |
| if (strcmp(names + i, XATTR_RESOURCEFORK_NAME) == 0) ok = 1; | |
| result(ok, "listxattr shows " XATTR_RESOURCEFORK_NAME, ""); | |
| // 7. copyfile preserves the fork. | |
| unlink(copy); | |
| ok = copyfile(file, copy, NULL, COPYFILE_ALL) == 0; | |
| snprintf(msg, sizeof msg, "%s", ok ? "" : strerror(errno)); | |
| result(ok, "copyfile(COPYFILE_ALL)", msg); | |
| n = read_all(copyfork, got, LARGE + 1); | |
| ok = n == LARGE && memcmp(want, got, LARGE) == 0; | |
| snprintf(msg, sizeof msg, "copy fork = %zd bytes", n); | |
| result(ok, "copy retains identical resource fork", msg); | |
| // 8. Truncate fork via ..namedfork/rsrc. | |
| fd = open(fork, O_WRONLY | O_TRUNC); | |
| if (fd >= 0) close(fd); | |
| len = rsrc_length(file); | |
| snprintf(msg, sizeof msg, "length after O_TRUNC = %lld", len); | |
| result(fd >= 0 && len == 0, "truncate fork via ..namedfork/rsrc", msg); | |
| // 9. Remove fork via removexattr. | |
| fd = open(fork, O_WRONLY); if (fd >= 0) { write_all(fd, "x", 1); close(fd); } | |
| ok = removexattr(file, XATTR_RESOURCEFORK_NAME, 0) == 0; | |
| ok = ok && getxattr(file, XATTR_RESOURCEFORK_NAME, NULL, 0, 0, 0) < 0 && errno == ENOATTR; | |
| result(ok, "removexattr deletes fork", ""); | |
| unlink(file); | |
| unlink(copy); | |
| free(want); free(got); | |
| printf("\n%d passed, %d failed — resource forks are %s on this volume.\n", | |
| passes, fails, fails == 0 ? "SUPPORTED" : passes == 0 ? "NOT SUPPORTED" : "PARTIALLY SUPPORTED"); | |
| return fails ? 1 : 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment