| 1 | //go:build darwin && cgo |
| 2 | |
| 3 | #import <Foundation/Foundation.h> |
| 4 | |
| 5 | #include <stdlib.h> |
| 6 | #include <string.h> |
| 7 | |
| 8 | static void reasonix_set_error(char **output, NSError *error, NSString *fallback) { |
| 9 | if (output == NULL) { |
| 10 | return; |
| 11 | } |
| 12 | NSString *message = error.localizedDescription ?: fallback; |
| 13 | *output = strdup(message.UTF8String ?: "unknown macOS alias error"); |
| 14 | } |
| 15 | |
| 16 | static void reasonix_set_exception(char **output, NSException *exception, NSString *fallback) { |
| 17 | if (output == NULL) { |
| 18 | return; |
| 19 | } |
| 20 | NSString *message = exception.reason ?: exception.name ?: fallback; |
| 21 | *output = strdup(message.UTF8String ?: "unknown macOS alias exception"); |
| 22 | } |
| 23 | |
| 24 | static BOOL reasonix_is_alias(NSURL *url, NSError **error) { |
| 25 | NSNumber *value = nil; |
| 26 | if (![url getResourceValue:&value forKey:NSURLIsAliasFileKey error:error]) { |
| 27 | return NO; |
| 28 | } |
| 29 | return value.boolValue; |
| 30 | } |
| 31 | |
| 32 | static int reasonix_create_alias(NSURL *targetURL, NSURL *aliasURL, char **error_message) { |
| 33 | NSError *error = nil; |
| 34 | NSData *bookmark = [targetURL bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile |
| 35 | includingResourceValuesForKeys:nil |
| 36 | relativeToURL:nil |
| 37 | error:&error]; |
| 38 | if (bookmark == nil) { |
| 39 | reasonix_set_error(error_message, error, @"create Finder alias bookmark"); |
| 40 | return -1; |
| 41 | } |
| 42 | if (![NSURL writeBookmarkData:bookmark toURL:aliasURL options:0 error:&error]) { |
| 43 | reasonix_set_error(error_message, error, @"write Finder alias bookmark"); |
| 44 | return -1; |
| 45 | } |
| 46 | return 0; |
| 47 | } |
| 48 | |
| 49 | // NSBundle requires a file URL and raises NSInvalidArgumentException for some |
| 50 | // Finder aliases that resolve to other schemes. Objective-C exceptions cannot |
| 51 | // cross the cgo boundary safely, so keep this classifier fail-closed and convert |
| 52 | // any unexpected Foundation exception into a normal native error. |
| 53 | static int reasonix_is_reasonix_bundle_url(NSURL *url, char **error_message) { |
| 54 | if (url == nil || !url.isFileURL) { |
| 55 | return 0; |
| 56 | } |
| 57 | @try { |
| 58 | NSBundle *bundle = [NSBundle bundleWithURL:url]; |
| 59 | return [bundle.bundleIdentifier isEqualToString:@"com.wails.reasonix-desktop"] ? 1 : 0; |
| 60 | } @catch (NSException *exception) { |
| 61 | reasonix_set_exception(error_message, exception, @"inspect Finder alias target"); |
| 62 | return -1; |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | // Kept as a narrow C entry point so Go regression tests exercise the exact |
| 67 | // native URL classifier used by startup alias repair. |
| 68 | int reasonix_is_reasonix_bundle_url_string(const char *url_string, |
| 69 | char **error_message) { |
| 70 | @autoreleasepool { |
| 71 | @try { |
| 72 | if (url_string == NULL) { |
| 73 | return 0; |
| 74 | } |
| 75 | NSString *value = [NSString stringWithUTF8String:url_string]; |
| 76 | if (value == nil) { |
| 77 | return 0; |
| 78 | } |
| 79 | return reasonix_is_reasonix_bundle_url([NSURL URLWithString:value], error_message); |
| 80 | } @catch (NSException *exception) { |
| 81 | reasonix_set_exception(error_message, exception, @"parse Finder alias target"); |
| 82 | return -1; |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | static int reasonix_write_alias_impl(const char *target_path, const char *alias_path, |
| 88 | char **error_message) { |
| 89 | NSURL *targetURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:target_path]]; |
| 90 | NSURL *aliasURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:alias_path]]; |
| 91 | return reasonix_create_alias(targetURL, aliasURL, error_message); |
| 92 | } |
| 93 | |
| 94 | int reasonix_write_alias(const char *target_path, const char *alias_path, |
| 95 | char **error_message) { |
| 96 | @autoreleasepool { |
| 97 | @try { |
| 98 | return reasonix_write_alias_impl(target_path, alias_path, error_message); |
| 99 | } @catch (NSException *exception) { |
| 100 | reasonix_set_exception(error_message, exception, @"write Finder alias"); |
| 101 | return -1; |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | static int reasonix_resolve_alias_impl(const char *alias_path, char **target_path, |
| 107 | char **error_message) { |
| 108 | NSError *error = nil; |
| 109 | NSURL *aliasURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:alias_path]]; |
| 110 | NSURL *resolved = [NSURL URLByResolvingAliasFileAtURL:aliasURL |
| 111 | options:NSURLBookmarkResolutionWithoutUI | |
| 112 | NSURLBookmarkResolutionWithoutMounting |
| 113 | error:&error]; |
| 114 | if (resolved == nil || !resolved.isFileURL) { |
| 115 | reasonix_set_error(error_message, error, @"resolve Finder alias to file URL"); |
| 116 | return -1; |
| 117 | } |
| 118 | if (target_path != NULL) { |
| 119 | *target_path = strdup(resolved.path.UTF8String); |
| 120 | } |
| 121 | return 0; |
| 122 | } |
| 123 | |
| 124 | int reasonix_resolve_alias(const char *alias_path, char **target_path, |
| 125 | char **error_message) { |
| 126 | @autoreleasepool { |
| 127 | @try { |
| 128 | return reasonix_resolve_alias_impl(alias_path, target_path, error_message); |
| 129 | } @catch (NSException *exception) { |
| 130 | reasonix_set_exception(error_message, exception, @"resolve Finder alias"); |
| 131 | return -1; |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | static int reasonix_repair_alias_impl(const char *alias_path, const char *current_app_path, |
| 137 | int allow_broken, char **error_message) { |
| 138 | NSError *error = nil; |
| 139 | NSURL *aliasURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:alias_path]]; |
| 140 | if (!reasonix_is_alias(aliasURL, &error)) { |
| 141 | // Missing metadata means this is an ordinary desktop file, not an |
| 142 | // owned integration failure. Never replace it merely by name. |
| 143 | return 0; |
| 144 | } |
| 145 | |
| 146 | NSURL *currentURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:current_app_path]]; |
| 147 | NSURL *resolved = [NSURL URLByResolvingAliasFileAtURL:aliasURL |
| 148 | options:NSURLBookmarkResolutionWithoutUI | |
| 149 | NSURLBookmarkResolutionWithoutMounting |
| 150 | error:&error]; |
| 151 | BOOL owned = NO; |
| 152 | if (resolved != nil) { |
| 153 | if (!resolved.isFileURL) { |
| 154 | // URL-backed and network aliases are unrelated desktop |
| 155 | // integrations. Skipping them also keeps NSBundle from |
| 156 | // raising for a non-file URL during application startup. |
| 157 | return 0; |
| 158 | } |
| 159 | NSString *resolvedPath = resolved.URLByResolvingSymlinksInPath.standardizedURL.path; |
| 160 | NSString *currentPath = currentURL.URLByResolvingSymlinksInPath.standardizedURL.path; |
| 161 | if ([resolvedPath isEqualToString:currentPath]) { |
| 162 | // The alias is already healthy. Avoid rewriting it on every |
| 163 | // launch so Finder metadata and any user customization remain. |
| 164 | return 0; |
| 165 | } |
| 166 | int ownership = reasonix_is_reasonix_bundle_url(resolved, error_message); |
| 167 | if (ownership < 0) { |
| 168 | return -1; |
| 169 | } |
| 170 | owned = ownership == 1; |
| 171 | } else if (allow_broken != 0) { |
| 172 | // A broken alias has no resolvable target to prove ownership. Only |
| 173 | // the exact historical installer names are admitted by Go. |
| 174 | owned = YES; |
| 175 | } |
| 176 | if (!owned) { |
| 177 | return 0; |
| 178 | } |
| 179 | |
| 180 | NSURL *directory = [aliasURL URLByDeletingLastPathComponent]; |
| 181 | NSString *temporaryName = [NSString stringWithFormat:@".reasonix-alias-%@", NSUUID.UUID.UUIDString]; |
| 182 | NSURL *temporaryURL = [directory URLByAppendingPathComponent:temporaryName]; |
| 183 | if (reasonix_create_alias(currentURL, temporaryURL, error_message) != 0) { |
| 184 | return -1; |
| 185 | } |
| 186 | |
| 187 | NSURL *resultingURL = nil; |
| 188 | if (![[NSFileManager defaultManager] replaceItemAtURL:aliasURL |
| 189 | withItemAtURL:temporaryURL |
| 190 | backupItemName:nil |
| 191 | options:NSFileManagerItemReplacementUsingNewMetadataOnly |
| 192 | resultingItemURL:&resultingURL |
| 193 | error:&error]) { |
| 194 | [[NSFileManager defaultManager] removeItemAtURL:temporaryURL error:nil]; |
| 195 | reasonix_set_error(error_message, error, @"replace Finder alias"); |
| 196 | return -1; |
| 197 | } |
| 198 | return 1; |
| 199 | } |
| 200 | |
| 201 | int reasonix_repair_alias(const char *alias_path, const char *current_app_path, |
| 202 | int allow_broken, char **error_message) { |
| 203 | @autoreleasepool { |
| 204 | @try { |
| 205 | return reasonix_repair_alias_impl(alias_path, current_app_path, allow_broken, error_message); |
| 206 | } @catch (NSException *exception) { |
| 207 | reasonix_set_exception(error_message, exception, @"repair Finder alias"); |
| 208 | return -1; |
| 209 | } |
| 210 | } |
| 211 | } |
| 212 |