prefs 0.1.3

Type-safe macOS preferences library
#include <CoreFoundation/CoreFoundation.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#include "prefs.h"

static CFTypeRef PrefsCopyPreference(const char *domain, const char *key);
static bool PrefsSetPreference(const char *domain, const char *key, CFTypeRef value);
static bool PrefsParseBoolString(const char *value, bool *out_value);
static bool PrefsGetBoolValue(CFTypeRef value, bool default_value);
static int64_t PrefsGetInt64Value(CFTypeRef value, int64_t default_value);
static double PrefsGetDoubleValue(CFTypeRef value, double default_value);
static char *PrefsCopyUTF8String(CFStringRef value);
static CFStringRef PrefsCreateCFString(const char *value);

char *prefs_copy_string(const char *domain, const char *key) {
    CFTypeRef value = PrefsCopyPreference(domain, key);
    char *result = NULL;
    if (value != NULL && CFGetTypeID(value) == CFStringGetTypeID()) {
        result = PrefsCopyUTF8String((CFStringRef)value);
    }
    if (value != NULL) {
        CFRelease(value);
    }
    return result;
}

PrefsBoolResult prefs_get_bool_optional(const char *domain, const char *key) {
    CFTypeRef value = PrefsCopyPreference(domain, key);
    PrefsBoolResult result = { .found = value != NULL, .value = false };
    if (value != NULL) {
        result.value = PrefsGetBoolValue(value, false);
        CFRelease(value);
    }
    return result;
}

PrefsInt64Result prefs_get_i64_optional(const char *domain, const char *key) {
    CFTypeRef value = PrefsCopyPreference(domain, key);
    PrefsInt64Result result = { .found = value != NULL, .value = 0 };
    if (value != NULL) {
        result.value = PrefsGetInt64Value(value, 0);
        CFRelease(value);
    }
    return result;
}

PrefsDoubleResult prefs_get_f64_optional(const char *domain, const char *key) {
    CFTypeRef value = PrefsCopyPreference(domain, key);
    PrefsDoubleResult result = { .found = value != NULL, .value = 0.0 };
    if (value != NULL) {
        result.value = PrefsGetDoubleValue(value, 0.0);
        CFRelease(value);
    }
    return result;
}

PrefsStringArray prefs_copy_string_array(const char *domain, const char *key) {
    PrefsStringArray result = { .found = false, .values = NULL, .len = 0 };
    CFTypeRef raw_value = PrefsCopyPreference(domain, key);
    if (raw_value == NULL) {
        return result;
    }
    result.found = true;

    CFTypeID type = CFGetTypeID(raw_value);
    if (type != CFArrayGetTypeID()) {
        CFRelease(raw_value);
        return result;
    }

    CFIndex count = CFArrayGetCount((CFArrayRef)raw_value);
    if (count <= 0) {
        CFRelease(raw_value);
        return result;
    }

    result.len = (uintptr_t)count;
    result.values = calloc((size_t)count, sizeof(char *));
    if (result.values == NULL) {
        result.len = 0;
        CFRelease(raw_value);
        return result;
    }

    for (CFIndex index = 0; index < count; index += 1) {
        CFTypeRef entry = CFArrayGetValueAtIndex((CFArrayRef)raw_value, index);
        if (entry == NULL || CFGetTypeID(entry) != CFStringGetTypeID()) {
            continue;
        }

        result.values[index] = PrefsCopyUTF8String((CFStringRef)entry);
    }

    CFRelease(raw_value);
    return result;
}

bool prefs_set_bool(const char *domain, const char *key, bool value) {
    return PrefsSetPreference(domain, key, value ? kCFBooleanTrue : kCFBooleanFalse);
}

bool prefs_set_i64(const char *domain, const char *key, int64_t value) {
    CFNumberRef num_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &value);
    if (num_ref == NULL) {
        return false;
    }

    bool result = PrefsSetPreference(domain, key, num_ref);
    CFRelease(num_ref);
    return result;
}

bool prefs_set_f64(const char *domain, const char *key, double value) {
    CFNumberRef num_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &value);
    if (num_ref == NULL) {
        return false;
    }

    bool result = PrefsSetPreference(domain, key, num_ref);
    CFRelease(num_ref);
    return result;
}

bool prefs_set_string(const char *domain, const char *key, const char *value) {
    CFStringRef value_ref = PrefsCreateCFString(value);
    if (value_ref == NULL) {
        return false;
    }

    bool result = PrefsSetPreference(domain, key, value_ref);
    CFRelease(value_ref);
    return result;
}

bool prefs_set_string_array(const char *domain, const char *key, const char **values, uintptr_t len) {
    if (len == 0) {
        return PrefsSetPreference(domain, key, NULL);
    }

    CFMutableArrayRef array = CFArrayCreateMutable(kCFAllocatorDefault, (CFIndex)len, &kCFTypeArrayCallBacks);
    if (array == NULL) {
        return false;
    }

    for (uintptr_t i = 0; i < len; i++) {
        CFStringRef str_ref = PrefsCreateCFString(values[i]);
        if (str_ref != NULL) {
            CFArrayAppendValue(array, str_ref);
            CFRelease(str_ref);
        }
    }

    bool result = PrefsSetPreference(domain, key, array);
    CFRelease(array);
    return result;
}

bool prefs_remove(const char *domain, const char *key) {
    return PrefsSetPreference(domain, key, NULL);
}

bool prefs_contains(const char *domain, const char *key) {
    CFTypeRef value = PrefsCopyPreference(domain, key);
    bool result = value != NULL;
    if (value != NULL) {
        CFRelease(value);
    }
    return result;
}

void prefs_free_string(char *value) {
    free(value);
}

void prefs_free_string_array(PrefsStringArray *array) {
    if (array == NULL || array->values == NULL) {
        return;
    }

    for (uintptr_t index = 0; index < array->len; index += 1) {
        free(array->values[index]);
    }
    free(array->values);
    memset(array, 0, sizeof(*array));
}

static CFTypeRef PrefsCopyPreference(const char *domain, const char *key) {
    if (domain == NULL || key == NULL) {
        return NULL;
    }

    CFStringRef domain_ref = CFStringCreateWithCString(kCFAllocatorDefault, domain, kCFStringEncodingUTF8);
    CFStringRef key_ref = CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8);
    if (domain_ref == NULL || key_ref == NULL) {
        if (domain_ref != NULL) {
            CFRelease(domain_ref);
        }
        if (key_ref != NULL) {
            CFRelease(key_ref);
        }
        return NULL;
    }

    CFPreferencesAppSynchronize(domain_ref);
    CFTypeRef value = CFPreferencesCopyAppValue(key_ref, domain_ref);
    CFRelease(key_ref);
    CFRelease(domain_ref);
    return value;
}

static bool PrefsSetPreference(const char *domain, const char *key, CFTypeRef value) {
    CFStringRef domain_ref = PrefsCreateCFString(domain);
    CFStringRef key_ref = PrefsCreateCFString(key);
    if (domain_ref == NULL || key_ref == NULL) {
        if (domain_ref != NULL) CFRelease(domain_ref);
        if (key_ref != NULL) CFRelease(key_ref);
        return false;
    }

    CFPreferencesSetAppValue(key_ref, value, domain_ref);
    bool result = CFPreferencesAppSynchronize(domain_ref);
    CFRelease(key_ref);
    CFRelease(domain_ref);
    return result;
}

static bool PrefsParseBoolString(const char *value, bool *out_value) {
    char *end = NULL;
    long long numeric = strtoll(value, &end, 10);
    if (end != value && *end == '\0') {
        *out_value = numeric != 0;
        return true;
    }

    if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0) {
        *out_value = true;
        return true;
    }
    if (strcasecmp(value, "false") == 0 || strcasecmp(value, "no") == 0) {
        *out_value = false;
        return true;
    }

    return false;
}

static bool PrefsGetBoolValue(CFTypeRef value, bool default_value) {
    if (value == NULL) {
        return default_value;
    }

    CFTypeID type = CFGetTypeID(value);
    if (type == CFBooleanGetTypeID()) {
        return CFBooleanGetValue((CFBooleanRef)value);
    }

    if (type == CFNumberGetTypeID()) {
        int64_t numeric = 0;
        if (CFNumberGetValue((CFNumberRef)value, kCFNumberSInt64Type, &numeric)) {
            return numeric != 0;
        }
        return default_value;
    }

    if (type == CFStringGetTypeID()) {
        char *buffer = PrefsCopyUTF8String((CFStringRef)value);
        if (buffer == NULL) {
            return default_value;
        }

        bool parsed = false;
        bool result = default_value;
        if (PrefsParseBoolString(buffer, &result)) {
            parsed = true;
        }
        free(buffer);
        return parsed ? result : default_value;
    }

    return default_value;
}

static int64_t PrefsGetInt64Value(CFTypeRef value, int64_t default_value) {
    if (value == NULL) {
        return default_value;
    }

    CFTypeID type = CFGetTypeID(value);
    if (type == CFNumberGetTypeID()) {
        int64_t numeric = 0;
        if (CFNumberGetValue((CFNumberRef)value, kCFNumberSInt64Type, &numeric)) {
            return numeric;
        }
        return default_value;
    }

    if (type == CFStringGetTypeID()) {
        char *buffer = PrefsCopyUTF8String((CFStringRef)value);
        if (buffer == NULL) {
            return default_value;
        }

        char *end = NULL;
        long long numeric = strtoll(buffer, &end, 10);
        const bool parsed = end != buffer && *end == '\0';
        free(buffer);
        if (parsed) {
            return (int64_t)numeric;
        }
    }

    return default_value;
}

static double PrefsGetDoubleValue(CFTypeRef value, double default_value) {
    if (value == NULL) {
        return default_value;
    }

    CFTypeID type = CFGetTypeID(value);
    if (type == CFNumberGetTypeID()) {
        double numeric = 0.0;
        if (CFNumberGetValue((CFNumberRef)value, kCFNumberDoubleType, &numeric)) {
            return numeric;
        }
        return default_value;
    }

    if (type == CFStringGetTypeID()) {
        char *buffer = PrefsCopyUTF8String((CFStringRef)value);
        if (buffer == NULL) {
            return default_value;
        }

        char *end = NULL;
        double numeric = strtod(buffer, &end);
        const bool parsed = end != buffer && *end == '\0';
        free(buffer);
        if (parsed) {
            return numeric;
        }
    }

    return default_value;
}

static char *PrefsCopyUTF8String(CFStringRef value) {
    if (value == NULL) {
        return NULL;
    }

    CFIndex length = CFStringGetLength(value);
    CFIndex capacity = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
    char *buffer = calloc((size_t)capacity, sizeof(char));
    if (buffer == NULL) {
        return NULL;
    }

    if (!CFStringGetCString(value, buffer, capacity, kCFStringEncodingUTF8)) {
        free(buffer);
        return NULL;
    }

    return buffer;
}

static CFStringRef PrefsCreateCFString(const char *value) {
    if (value == NULL) {
        return NULL;
    }
    return CFStringCreateWithCString(kCFAllocatorDefault, value, kCFStringEncodingUTF8);
}