tauri-plugin-spotsearch 0.2.0

A Tauri plugin that indexes content into iOS Spotlight.
Documentation
#import <CoreSpotlight/CoreSpotlight.h>
#import <Foundation/Foundation.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <dispatch/dispatch.h>
#import "macos_spotlight.h"

static char* copy_string(NSString *s) {
    if (!s) return NULL;
    const char *utf8 = [s UTF8String];
    size_t len = strlen(utf8) + 1;
    char *buf = (char *)malloc(len);
    if (buf) memcpy(buf, utf8, len);
    return buf;
}

static char* run_core_spotlight(void (^block)(void (^resolve)(NSError *))) {
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    __block NSError *blockError = nil;

    block(^(NSError *error) {
        blockError = error;
        dispatch_semaphore_signal(sem);
    });

    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    if (blockError) {
        return copy_string(blockError.localizedDescription);
    }
    return NULL;
}

int32_t spotsearch_index_items(const char *json_items, char **out_error) {
    @autoreleasepool {
        NSData *data = [NSData dataWithBytes:json_items length:strlen(json_items)];
        NSError *parseError = nil;
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
        if (parseError || ![dict isKindOfClass:[NSDictionary class]]) {
            if (out_error) *out_error = copy_string(parseError.localizedDescription ?: @"Failed to parse JSON");
            return 1;
        }

        NSArray *items = dict[@"items"];
        if (![items isKindOfClass:[NSArray class]]) {
            if (out_error) *out_error = copy_string(@"Missing 'items' array");
            return 1;
        }

        NSMutableArray<CSSearchableItem *> *searchableItems = [NSMutableArray arrayWithCapacity:items.count];

        for (NSDictionary *item in items) {
            if (![item isKindOfClass:[NSDictionary class]]) continue;

            NSString *uniqueId = item[@"id"];
            NSString *domain = item[@"domain"];
            if (![uniqueId isKindOfClass:[NSString class]] || ![domain isKindOfClass:[NSString class]]) continue;

            CSSearchableItemAttributeSet *attrs = [[CSSearchableItemAttributeSet alloc] initWithContentType:UTTypeText];
            if ([item[@"title"] isKindOfClass:[NSString class]]) attrs.title = item[@"title"];
            if ([item[@"snippet"] isKindOfClass:[NSString class]]) attrs.contentDescription = item[@"snippet"];
            if ([item[@"keywords"] isKindOfClass:[NSArray class]]) attrs.keywords = item[@"keywords"];
            if ([item[@"url"] isKindOfClass:[NSString class]]) attrs.contentURL = [NSURL URLWithString:item[@"url"]];
            if ([item[@"thumbnailBase64"] isKindOfClass:[NSString class]]) {
                NSData *thumbData = [[NSData alloc] initWithBase64EncodedString:item[@"thumbnailBase64"] options:0];
                if (thumbData) attrs.thumbnailData = thumbData;
            }

            CSSearchableItem *si = [[CSSearchableItem alloc] initWithUniqueIdentifier:uniqueId
                                                                    domainIdentifier:domain
                                                                        attributeSet:attrs];
            [searchableItems addObject:si];
        }

        char *err = run_core_spotlight(^(void (^resolve)(NSError *)) {
            [[CSSearchableIndex defaultSearchableIndex] indexSearchableItems:searchableItems completionHandler:resolve];
        });

        if (err) {
            if (out_error) *out_error = err;
            else free(err);
            return 1;
        }
        return 0;
    }
}

int32_t spotsearch_delete_items(const char *json_ids, char **out_error) {
    @autoreleasepool {
        NSData *data = [NSData dataWithBytes:json_ids length:strlen(json_ids)];
        NSError *parseError = nil;
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
        if (parseError || ![dict isKindOfClass:[NSDictionary class]]) {
            if (out_error) *out_error = copy_string(parseError.localizedDescription ?: @"Failed to parse JSON");
            return 1;
        }

        NSArray *ids = dict[@"ids"];
        if (![ids isKindOfClass:[NSArray class]]) {
            if (out_error) *out_error = copy_string(@"Missing 'ids' array");
            return 1;
        }

        char *err = run_core_spotlight(^(void (^resolve)(NSError *)) {
            [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithIdentifiers:ids completionHandler:resolve];
        });

        if (err) {
            if (out_error) *out_error = err;
            else free(err);
            return 1;
        }
        return 0;
    }
}

int32_t spotsearch_delete_domain(const char *domain, char **out_error) {
    @autoreleasepool {
        NSString *domainStr = [NSString stringWithUTF8String:domain];
        if (!domainStr) {
            if (out_error) *out_error = copy_string(@"Invalid domain string");
            return 1;
        }

        char *err = run_core_spotlight(^(void (^resolve)(NSError *)) {
            [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithDomainIdentifiers:@[domainStr] completionHandler:resolve];
        });

        if (err) {
            if (out_error) *out_error = err;
            else free(err);
            return 1;
        }
        return 0;
    }
}

void spotsearch_free_error(char *error) {
    if (error) free(error);
}