#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libproc.h>
#include <limits.h>
#include <mach/mach.h>
#include <paths.h>
#include <pwd.h>
#include <regex.h>
#include <spawn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <CoreFoundation/CoreFoundation.h>
#include <NSSystemDirectories.h>
#include "shortcuts.h"
#include "versions.h"
#define DATAVERSIONLEN 16
#define DEFAULTPREFER32BIT 0
#define ENV_DEBUG ENVPREFIX "DEBUG"
#define ENV_PREFER32BIT ENVPREFIX UPROJECT "_PREFER_32_BIT"
#define ENV_VERSION ENVPREFIX UPROJECT "_VERSION"
#define ENVPREFIX "VERSIONER_"
#define EXPECT_FALSE(x) __builtin_expect((x), 0)
#define EXPECT_TRUE(x) __builtin_expect((x), 1)
#define PLISTPATHLEN (sizeof(plistpath) - 1)
#define PREFER3CPULEN (sizeof(prefer32cpu) / sizeof(cpu_type_t))
#define USRBINLEN (sizeof(usrbin) - 1)
struct data {
int prefer32bit;
char version[DATAVERSIONLEN];
};
extern char **environ;
static int debug = 0;
static const char plistpath[] = "/Preferences/com.apple.versioner." PROJECT ".plist";
static cpu_type_t prefer32cpu[] = {
CPU_TYPE_I386,
CPU_TYPE_POWERPC,
CPU_TYPE_X86_64,
CPU_TYPE_POWERPC64
};
static const char usrbin[] = "/usr/bin/";
static inline int boolean_check(const char *str);
static char * cfstrdup(CFStringRef str);
static int cmpshortcuts(const void * restrict a, const void * restrict b);
static int cmpstrarray(const void * restrict a, const void * restrict b);
static void read_plist(const char * restrict path, struct data * restrict dp);
static inline int searchshortcuts(const char * restrict name, const char * restrict version, char * restrict path);
static void versionargs(int argc, char **argv, const char *version);
static inline int version_check(const char *str);
static inline int
boolean_check(const char *str)
{
if (strcasecmp(str, "yes") == 0 || strcasecmp(str, "true") == 0) return 1;
if (strcasecmp(str, "no") == 0 || strcasecmp(str, "false") == 0) return 0;
if (strspn(str, "0123456789") == strlen(str)) return (strtol(str, NULL, 10) != 0L);
return -1;
}
static char *
cfstrdup(CFStringRef str) {
char *result;
CFIndex length = CFStringGetLength(str);
CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8);
result = malloc(size + 1);
if (EXPECT_TRUE(result != NULL)) {
length = CFStringGetBytes(str, CFRangeMake(0, length), kCFStringEncodingUTF8, '?', 0, (UInt8*)result, size, NULL);
result[length] = 0;
}
return result;
}
static int
cmpshortcuts(const void * restrict a, const void * restrict b)
{
return strcmp((const char *)a, ((struct shortcut *)b)->name);
}
static int
cmpstrarray(const void * restrict a, const void * restrict b)
{
return strcmp((const char *)a, *(const char **)b);
}
static void
read_plist(const char * restrict path, struct data * restrict dp)
{
do {
int fd = open(path, O_RDONLY, (mode_t)0);
if (fd < 0) {
if (EXPECT_FALSE(debug)) warn("read_plist: %s: open", path);
break;
}
do {
struct stat sb;
if (EXPECT_FALSE(fstat(fd, &sb) < 0)) {
if (EXPECT_FALSE(debug)) warn("read_plist: %s: stat", path);
break;
}
off_t size = sb.st_size;
void *buffer = mmap(NULL, size, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, (off_t)0);
if (EXPECT_FALSE(buffer == MAP_FAILED)) {
if (EXPECT_FALSE(debug)) warn("read_plist: %s: mmap", path);
break;
}
do {
CFDataRef data = CFDataCreateWithBytesNoCopy(NULL, buffer, size, kCFAllocatorNull);
if (EXPECT_FALSE(data == NULL)) {
if (EXPECT_FALSE(debug)) warnx("read_plist: %s: CFDataCreateWithBytesNoCopy failed", path);
break;
}
do {
CFStringRef str = NULL;
CFPropertyListRef plist = CFPropertyListCreateFromXMLData(NULL, data, kCFPropertyListMutableContainers, &str);
if (EXPECT_FALSE(plist == NULL)) {
if (EXPECT_FALSE(debug)) {
char *s = cfstrdup(str);
char *sp = s;
if (EXPECT_FALSE(sp == NULL)) sp = "(cfstrdup also failed)";
warnx("read_plist: %s: CFPropertyListCreateFromXMLData failed: %s", path, sp);
if (EXPECT_TRUE(s != NULL)) free(s);
}
break;
}
do {
if (EXPECT_FALSE(CFGetTypeID(plist) != CFDictionaryGetTypeID())) {
if (EXPECT_FALSE(debug)) warnx("read_plist: %s: plist not a dictionary", path);
break;
}
if (dp->prefer32bit < 0) {
CFBooleanRef prefer32bit = CFDictionaryGetValue(plist, CFSTR("Prefer-32-Bit"));
if (prefer32bit
&& EXPECT_TRUE(CFGetTypeID(prefer32bit) == CFBooleanGetTypeID())) {
dp->prefer32bit = CFBooleanGetValue(prefer32bit);
} else if (EXPECT_FALSE(debug)) warnx("read_plist: %s: Prefer-32-Bit not a boolean", path);
}
if (*dp->version == 0) {
char *vers;
CFStringRef version;
version = CFDictionaryGetValue(plist, CFSTR("Version"));
if (version
&& EXPECT_TRUE(CFGetTypeID(version) == CFStringGetTypeID())
&& EXPECT_TRUE((vers = cfstrdup(version)) != NULL)) {
if (EXPECT_TRUE(version_check(vers))) {
strcpy(dp->version, vers);
} else if (EXPECT_FALSE(debug)) warnx("read_plist: %s: %s: Version unknown", path, vers);
free(vers);
} else if (EXPECT_FALSE(debug)) warnx("read_plist: %s: Version not a string", path);
}
} while(0);
CFRelease(plist);
} while(0);
CFRelease(data);
} while(0);
munmap(buffer, size);
} while(0);
close(fd);
} while(0);
}
static inline int
searchshortcuts(const char * restrict name, const char * restrict version, char * restrict path)
{
struct shortcut *sp;
if ((sp = (struct shortcut *)bsearch(name, shortcuts, NSHORTCUTS, sizeof(struct shortcut), cmpshortcuts)) != NULL) {
strcpy(path, sp->before);
strcat(path, version);
strcat(path, sp->after);
return 1;
}
return 0;
}
static void
versionargs(int argc, char **argv, const char *version)
{
char buf[PATH_MAX];
char *punct;
size_t len;
size_t vlen = strlen(version);
for(argc--, argv++; argc > 0; argc--, argv++) {
if (strncmp(*argv, usrbin, USRBINLEN) != 0) continue;
if ((len = strlen(*argv)) + vlen >= sizeof(buf)) continue;
strcpy(buf, *argv);
strcat(buf, version);
if (access(buf, F_OK) == 0) {
found:
if (EXPECT_FALSE(debug)) warnx("%s -> %s", *argv, buf);
if (EXPECT_FALSE((*argv = strdup(buf)) == NULL)) errx(1, "versionarg: Out of memory");
continue;
}
if (EXPECT_TRUE(len + vlen + 1 < sizeof(buf))) {
buf[len] = '-';
strcpy(buf + len + 1, version);
if (access(buf, F_OK) == 0) goto found;
}
buf[len] = 0;
if ((punct = strchr(buf, '-')) != NULL || (punct = strrchr(buf, '.')) != NULL) {
memmove(punct + vlen, punct, strlen(punct) + 1);
memcpy(punct, version, vlen);
if (access(buf, F_OK) == 0) goto found;
}
}
}
static inline int
version_check(const char *str)
{
return (bsearch(str, versions, NVERSIONS, sizeof(const char *), cmpstrarray) != NULL);
}
int
main(int argc, char **argv)
{
char path[PATH_MAX], path0[PATH_MAX];
NSSearchPathEnumerationState state;
struct data data = {-1, ""};
int i, ret, appendvers = 1;
char *env, *name;
size_t vlen;
posix_spawnattr_t attr;
pid_t pid;
if (EXPECT_FALSE(getenv(ENV_DEBUG) != NULL)) debug = 1;
if ((env = getenv(ENV_VERSION)) != NULL) {
if (EXPECT_TRUE(version_check(env))) strcpy(data.version, env);
else warnx("%s environment variable error (ignored)", ENV_VERSION);
}
if ((env = getenv(ENV_PREFER32BIT)) != NULL) {
if (EXPECT_TRUE((i = boolean_check(env)) >= 0)) data.prefer32bit = i;
else warnx("%s environment variable error (ignored)", ENV_PREFER32BIT);
}
if (data.prefer32bit < 0 || *data.version == 0) {
state = NSStartSearchPathEnumeration(NSLibraryDirectory, NSAllDomainsMask & (~NSSystemDomainMask));
while ((state = NSGetNextSearchPathEnumeration(state, path)) != 0) {
size_t len = strlen(path);
if (*path == '~') {
struct passwd *pw = getpwuid(getuid());
size_t dlen;
if (EXPECT_FALSE(pw == NULL)) errx(1, "no user %d", (int)getuid());
dlen = strlen(pw->pw_dir);
if (EXPECT_FALSE(dlen + len - 1 >= sizeof(path))) errx(1, "%s: too long", pw->pw_dir);
memmove(path + dlen, path + 1, len); memcpy(path, pw->pw_dir, dlen);
len += dlen - 1;
}
if (EXPECT_FALSE(len + PLISTPATHLEN >= sizeof(path))) errx(1, "%s: Can't append \"%s\"\n", path, plistpath);
strcat(path, plistpath);
read_plist(path, &data);
if (data.prefer32bit >= 0 && *data.version) break;
}
if (data.prefer32bit < 0) data.prefer32bit = DEFAULTPREFER32BIT;
if (*data.version == 0) strcpy(data.version, DEFAULTVERSION);
}
if (EXPECT_FALSE(debug)) warnx("prefer32bit=%d version=%s", data.prefer32bit, data.version);
vlen = strlen(data.version);
if ((name = strrchr(*argv, '/')) != NULL) {
strlcpy(path0, *argv, sizeof(path0));
} else if (proc_pidpath(getpid(), path0, sizeof(path0)) != 0) {
char *p;
size_t plen;
if ((p = strrchr(path0, '/')) != NULL) p++;
else p = path0;
plen = strlen(p);
if (EXPECT_FALSE(strlen(path0) - plen + strlen(*argv) >= sizeof(path0))) errx(1, "Executable path too long");
strcpy(p, *argv);
} else {
char *cur, *p;
size_t plen;
size_t alen = strlen(*argv);
if (EXPECT_FALSE(debug)) warn("proc_pidpath");
if (EXPECT_FALSE((env = getenv("PATH")) == NULL)) env = _PATH_DEFPATH;
cur = alloca(strlen(env) + 1);
if (EXPECT_FALSE(cur == NULL)) errx(1, "alloca: out of memory");
strcpy(cur, env);
for (;;) {
if (EXPECT_FALSE((p = strsep(&cur, ":")) == NULL)) {
memcpy(path0, *argv, alen + 1);
break;
}
if (*p == 0) {
p = ".";
plen = 1;
} else plen = strlen(p);
if (EXPECT_FALSE(plen + alen + 1 >= sizeof(path0))) errx(1, "Executable path too long");
memcpy(path0, p, plen);
path0[plen] = '/';
memcpy(path0 + plen + 1, *argv, alen + 1);
if (EXPECT_TRUE(access(path0, X_OK) == 0)) break;
}
}
if (EXPECT_FALSE(realpath(path0, path) == NULL))
errx(1, "realpath couldn't resolve \"%s\"", path0);
if (strncmp(path, usrbin, USRBINLEN) == 0
&& searchshortcuts(path + USRBINLEN, data.version, path)) appendvers = 0;
if (appendvers) {
if (EXPECT_FALSE(strlen(path) + vlen >= sizeof(path))) errx(1, "%s: Can't append \"%s\"", path, data.version);
strcat(path, data.version);
}
if (EXPECT_FALSE(debug)) warnx("argv=%s path=%s", *argv, path);
versionargs(argc, argv, data.version);
if (EXPECT_FALSE((ret = posix_spawnattr_init(&attr)) != 0)) errc(1, ret, "posix_spawnattr_init");
if (EXPECT_FALSE((ret = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETEXEC)) != 0)) errc(1, ret, "posix_spawnattr_setflags");
if (data.prefer32bit) {
size_t copied;
if (EXPECT_FALSE((ret = posix_spawnattr_setbinpref_np(&attr, PREFER3CPULEN, prefer32cpu, &copied)) != 0)) errc(1, ret, "posix_spawnattr_setbinpref_np");
if (EXPECT_FALSE(copied != PREFER3CPULEN)) errx(1, "posix_spawnattr_setbinpref_np only copied %d of %d", (int)copied, PREFER3CPULEN);
}
ret = posix_spawn(&pid, path, NULL, &attr, argv, environ);
errc(1, ret, "posix_spawn: %s", path);
return 1;
}