mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Open web and mail links on the Linux desktop from LaunchServices
LaunchServices now hands http, https and mailto URLs to the host desktop itself instead of NSWorkspace writing them to a spool that nothing in the normal workflow drains. It loads the host libsystemd through the ELF loader and calls xdg-desktop-portal OpenURI on the session bus, falling back to xdg-open in a transient systemd user unit, so the browser starts outside Darling's namespaces. LSGetApplicationForURL reports the host opener for these schemes because Electron checks for a handler before opening a link. NSWorkspace goes through LSOpenCFURLRef. Tested with a host-side D-Bus regression (79 checks) and in a fresh guest prefix through open(1) and NSWorkspace, both reaching the portal with the exact URL. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a7cd0c4ef9
commit
8f62017627
7 files changed
+1226
-81
No files matched your search
+2
-2
@@ -45,10 +45,10 @@ The current machine uses a Ryzen 5 5500, RX 6600 and Linux 7.2.9. The source sna
|
||||
- **Video scaling:** generated ELF GL resolvers lost floating-point arguments on the first call. Chromium's first linear sampler request failed, leaving nearest sampling. Preserving registers fixes the native first-call sampler/color probe; the old wrapper fails both checks. An 800×450 checkerboard scaled to 640×360 changes to bilinear sampling.
|
||||
- **Startup presentation:** drawing another differently sized surface in the same GL context stopped the active async presenter. Those surfaces now fall back to readback without stopping it. The old build fails after three visible frames; the corrected native probe retains the presenter through 357 fallback frames and presents all 360 frames at about 60 FPS. User confirmation of the splash remains pending.
|
||||
- **UI clicks:** window dragging now examines the frontmost view by geometry, including Electron's control-region overlays that deliberately return nil from hit testing. Previously a click could reach the underlying draggable web view and start a drag instead of activating a control.
|
||||
- **External-link crashes:** six LaunchServices paths dereferenced nullable error-output pointers. They now check the pointers and populate supplied outputs. AppKit also implements the missing URL-opening configuration/completion methods. Native nullable-output checks passed. The paired installer covers Electron's absolute LaunchServices subframework path as well as the flat override.
|
||||
- **External-link crashes:** six LaunchServices paths dereferenced nullable error-output pointers. They now check the pointers and populate supplied outputs. AppKit also implements the missing URL-opening configuration/completion methods, and LaunchServices hands `http`, `https` and `mailto` URLs to the host desktop (portal, then a transient systemd unit running `xdg-open`). Native nullable-output checks and the host-side D-Bus hand-off regression passed. In a fresh guest prefix, `open https://…` and `NSWorkspace -openURL:configuration:completionHandler:` both reached the portal with the exact URL and opened the host browser; a link clicked in the Riot client is not yet confirmed. The paired installer covers Electron's absolute LaunchServices subframework path as well as the flat override.
|
||||
- **Installation:** the default prefix gets a writable `/private/tmp`; the first-time launcher skips the inaccessible native intro and avoids an installed-client `--app-root`. The fetcher accepts missing informational `Info.plist` metadata. A separate HTTP 409 LaunchAgent handoff failure remains unresolved in the manual launch path.
|
||||
|
||||
The historical update hook handles HTTP 409, interrupted `pending_verify` and active downloads in 28 mocked scenarios. Historical external-link forwarding was exercised before the workflow changed. Both inspector-based helpers are now disabled in normal launches. Native URL methods can queue requests, but no normal host spool watcher consumes them; host-browser forwarding is not established by the crash fix. Legacy Electron visibility patches and automated practice/profiling tools are excluded from recommended setup/testing.
|
||||
The historical update hook handles HTTP 409, interrupted `pending_verify` and active downloads in 28 mocked scenarios. Historical external-link forwarding was exercised before the workflow changed. Both inspector-based helpers are now disabled in normal launches. Native URL methods no longer queue requests in a spool; LaunchServices hands them to the host desktop. Legacy Electron visibility patches and automated practice/profiling tools are excluded from recommended setup/testing.
|
||||
|
||||
## FPS observation and server optimization
|
||||
|
||||
|
||||
@@ -64,6 +64,21 @@ Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct creden
|
||||
|
||||
The same source passed AddressSanitizer/UndefinedBehaviorSanitizer and leak detection. Allocation counts depend on this fixture/compiler/library; preserve the no-wasted-slot property rather than interpreting them as game performance. See [setup](SETUP.md#updating-an-existing-runtime) for server build/install/activation.
|
||||
|
||||
## Host URL hand-off regression
|
||||
|
||||
LaunchServices' D-Bus hand-off code (`LSHostOpenBus.c`) builds natively against the host's libsystemd. The check starts a private `dbus-daemon` with no service directories and a fake portal/systemd service, so nothing real is activated and no browser opens:
|
||||
|
||||
```sh
|
||||
cc -std=gnu11 -O2 -Wall -Wextra \
|
||||
-I build/darling-src/src/frameworks/CoreServices/src/LaunchServices \
|
||||
tools/riot-host-open-url-check.c \
|
||||
build/darling-src/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c \
|
||||
$(pkg-config --cflags --libs libsystemd) -o build/riot-host-open-url-check
|
||||
build/riot-host-open-url-check
|
||||
```
|
||||
|
||||
Expected: `HOST_OPEN_URL PASS checks=79 failures=0`. It covers: the portal receives the exact URL; with no portal, or a portal error, `StartTransientUnit` runs `[xdg-open, url]`; a portal timeout is not retried; rejected schemes, control characters and overlong URLs never connect to the bus; and a missing bus or libsystemd returns an error. It also covers the `/run/user/<uid>/bus` fallback when `DBUS_SESSION_BUS_ADDRESS` is unset, against a temporary directory. It needs `dbus-daemon` and the libsystemd headers.
|
||||
|
||||
## Host reply path regression
|
||||
|
||||
The direct-reply checks run entirely on independent host sockets and exercise the real `src/message.cpp` (`MessageQueue::pushAndSend`, `sendMany`, `empty`):
|
||||
|
||||
@@ -60,7 +60,7 @@ There is also a separate known compatibility gap: native LaunchAgent watch regis
|
||||
|
||||
LaunchServices dereferenced a nullable error-output pointer in six paths. The patch checks the pointer itself and populates supplied outputs; the standalone regression covers missing handlers and null outputs. Electron opens LaunchServices through an absolute CoreServices subframework path, so the paired installer also replaces that binary in the stopped guest prefix. Updating only `RuntimeFrameworks/LaunchServices.framework` can leave the crash unfixed.
|
||||
|
||||
Historical host-browser forwarding depended on an inspector hook and is disabled in normal launches. The native null-pointer fix prevents this demonstrated crash; it does not guarantee that every external link opens on the host or explain every unrelated button crash.
|
||||
LaunchServices now opens `http`, `https` and `mailto` links on the Linux desktop itself. It loads the host's `libsystemd.so.0` and asks `xdg-desktop-portal` (`org.freedesktop.portal.OpenURI`) on the user session bus to open the link, so your default browser starts outside Darling and survives `darling shutdown`. Without a portal, it starts `xdg-open` in a transient systemd user unit instead. It also reports a handler for these schemes, since Electron checks for one before opening a link. Other schemes, control characters and URLs over 4096 bytes are rejected. If a link still does nothing, check the client's stderr for a `LaunchServices:` line and that `busctl --user status org.freedesktop.portal.Desktop` works on the host. Both LaunchServices and AppKit must be rebuilt; the old `/private/tmp/rclol-open` spool and `tools/riot-open-url-bridge.py` are obsolete. The hand-off passed the host-side D-Bus regression and opened the host browser from a guest through both `open(1)` and `NSWorkspace`; a link clicked in the Riot client is not yet confirmed. The null-pointer fix does not explain every unrelated button crash.
|
||||
|
||||
## Blocky video or flickering startup logo
|
||||
|
||||
|
||||
+29
-62
@@ -20,7 +20,7 @@ index 393a1eb3..6a47ac89 100644
|
||||
xkbfile
|
||||
)
|
||||
diff --git a/AppKit/MacWorkspace.m b/AppKit/MacWorkspace.m
|
||||
index ba52cd3b..423c0716 100644
|
||||
index ba52cd3b..d566b946 100644
|
||||
--- a/AppKit/MacWorkspace.m
|
||||
+++ b/AppKit/MacWorkspace.m
|
||||
@@ -13,6 +13,32 @@
|
||||
@@ -56,17 +56,16 @@ index ba52cd3b..423c0716 100644
|
||||
- (NSImage *) iconForFile: (NSString *) path {
|
||||
// TODO: call GetIconRefFromFileInfo()
|
||||
NSUnimplementedMethod();
|
||||
@@ -94,10 +120,10 @@
|
||||
return NO;
|
||||
@@ -95,9 +121,9 @@
|
||||
}
|
||||
|
||||
+extern BOOL RCLOLOpenURLViaHost(NSURL *url);
|
||||
+
|
||||
- (BOOL) openURL: (NSURL *) url {
|
||||
- // TODO: Call LSOpenFromURLSpec()
|
||||
- NSUnimplementedMethod();
|
||||
- return NO;
|
||||
+ return RCLOLOpenURLViaHost(url);
|
||||
+ if (url == nil)
|
||||
+ return NO;
|
||||
+ return LSOpenCFURLRef((CFURLRef) url, NULL) == noErr;
|
||||
}
|
||||
|
||||
- (BOOL) selectFile: (NSString *) path
|
||||
@@ -7749,29 +7748,20 @@ new file mode 100644
|
||||
+
|
||||
+@end
|
||||
diff --git a/AppKit/NSWorkspace.m b/AppKit/NSWorkspace.m
|
||||
index ad5e4f9d..304cd8b5 100644
|
||||
index ad5e4f9d..dbd997aa 100644
|
||||
--- a/AppKit/NSWorkspace.m
|
||||
+++ b/AppKit/NSWorkspace.m
|
||||
@@ -20,9 +20,90 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
@@ -20,9 +20,58 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
#import <AppKit/NSRaise.h>
|
||||
#import <AppKit/NSWorkspace.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
+#import <CoreFoundation/CoreFoundation.h>
|
||||
+#include <dispatch/dispatch.h>
|
||||
+#include <stdio.h>
|
||||
+#include <unistd.h>
|
||||
|
||||
NSString *const NSWorkspaceApplicationKey = @"NSWorkspaceApplicationKey";
|
||||
|
||||
+/* darling-lol: opening a URL from the guest.
|
||||
+ *
|
||||
+ * LaunchServices cannot start a Linux browser from inside the prefix, and Electron's
|
||||
+ * shell.openExternal() uses -openURL:configuration:completionHandler: (macOS 10.15) with an
|
||||
+ * NSWorkspaceOpenConfiguration, neither of which existed: the unrecognized selector crashed the
|
||||
+ * client ("Can't sign in?", links, ...). Hand the URL to the host through a spool directory in
|
||||
+ * the shared /private/tmp. The legacy tools/riot-open-url-bridge.py can drain it, but is excluded
|
||||
+ * from normal launches because it also installs a client inspector hook.
|
||||
+ * Only http, https and mailto URLs are queued. */
|
||||
+/* Electron's shell.openExternal() uses -openURL:configuration:completionHandler: (macOS 10.15)
|
||||
+ * with a configured NSWorkspaceOpenConfiguration, so the configuration needs real properties.
|
||||
+ * The URL itself is opened by -openURL: (LaunchServices in MacWorkspace). */
|
||||
+@interface NSWorkspaceOpenConfiguration () {
|
||||
+ BOOL _activates, _hides, _hidesOthers, _addsToRecentItems, _createsNewApplicationInstance;
|
||||
+ BOOL _allowsRunningApplicationSubstitution, _forPrinting, _promptsUserIfNeeded, _requiresUniversalLinks;
|
||||
@@ -7816,59 +7806,36 @@ index ad5e4f9d..304cd8b5 100644
|
||||
+ [super dealloc];
|
||||
+}
|
||||
+@end
|
||||
+
|
||||
+BOOL RCLOLOpenURLViaHost(NSURL *url) {
|
||||
+ NSString *scheme = [[url scheme] lowercaseString];
|
||||
+ if (![scheme isEqualToString: @"http"] && ![scheme isEqualToString: @"https"] &&
|
||||
+ ![scheme isEqualToString: @"mailto"])
|
||||
+ return NO;
|
||||
+ NSString *absolute = [url absoluteString];
|
||||
+ if ([absolute length] == 0 || [absolute length] > 4096)
|
||||
+ return NO;
|
||||
+ NSString *directory = @"/private/tmp/rclol-open";
|
||||
+ [[NSFileManager defaultManager] createDirectoryAtPath: directory
|
||||
+ withIntermediateDirectories: YES
|
||||
+ attributes: nil
|
||||
+ error: NULL];
|
||||
+ static unsigned sequence = 0;
|
||||
+ NSString *path = [NSString stringWithFormat: @"%@/%d-%u-%llu.url", directory, (int) getpid(),
|
||||
+ ++sequence, (unsigned long long) (CFAbsoluteTimeGetCurrent() * 1000)];
|
||||
+ NSString *temporary = [path stringByAppendingString: @".part"];
|
||||
+ if (![absolute writeToFile: temporary atomically: NO encoding: NSUTF8StringEncoding error: NULL])
|
||||
+ return NO;
|
||||
+ return rename([temporary fileSystemRepresentation], [path fileSystemRepresentation]) == 0;
|
||||
+}
|
||||
+
|
||||
+
|
||||
NSString *const NSWorkspaceWillPowerOffNotification =
|
||||
@"NSWorkspaceWillPowerOffNotification";
|
||||
|
||||
@@ -157,8 +238,22 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
|
||||
@@ -161,6 +210,24 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (BOOL) openURL: (NSURL *) url {
|
||||
- NSInvalidAbstractInvocation();
|
||||
- return 0;
|
||||
+ return RCLOLOpenURLViaHost(url);
|
||||
+}
|
||||
+
|
||||
+- (void) openURL: (NSURL *) url
|
||||
+ configuration: (NSWorkspaceOpenConfiguration *) configuration
|
||||
+ completionHandler: (void (^)(id application, NSError *error)) completionHandler
|
||||
+{
|
||||
+ BOOL opened = [self openURL: url];
|
||||
+ if (completionHandler != NULL) {
|
||||
+ NSError *error = nil;
|
||||
+ if (!opened)
|
||||
+ error = [NSError errorWithDomain: NSCocoaErrorDomain code: 256 userInfo: nil];
|
||||
+ dispatch_async(dispatch_get_main_queue(), ^{
|
||||
+ completionHandler(nil, error);
|
||||
+ });
|
||||
+ }
|
||||
}
|
||||
|
||||
+ // Opening may wait on another process; keep it off the caller's thread.
|
||||
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
+ BOOL opened = [self openURL: url];
|
||||
+ if (completionHandler != NULL) {
|
||||
+ NSError *error = nil;
|
||||
+ if (!opened)
|
||||
+ error = [NSError errorWithDomain: NSCocoaErrorDomain code: 256 userInfo: nil];
|
||||
+ dispatch_async(dispatch_get_main_queue(), ^{
|
||||
+ completionHandler(nil, error);
|
||||
+ });
|
||||
+ }
|
||||
+ });
|
||||
+}
|
||||
+
|
||||
- (BOOL) selectFile: (NSString *) path
|
||||
@@ -317,16 +412,3 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
|
||||
inFileViewerRootedAtPath: (NSString *) rootedAtPath
|
||||
{
|
||||
@@ -317,16 +384,3 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
+734
-15
@@ -2630,15 +2630,16 @@ index cf9933a84..6651d50dc 100644
|
||||
forKey: [NSNumber numberWithInt: wd]];
|
||||
}
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m
|
||||
index dd78f6df4..fc25c7970 100644
|
||||
index dd78f6df4..8fa95a174 100644
|
||||
--- a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m
|
||||
@@ -19,8 +19,100 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
@@ -19,8 +19,104 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#include <LaunchServices/LaunchServices.h>
|
||||
+#include <LaunchServices/LaunchServicesPriv.h>
|
||||
#include <CarbonCore/MacErrors.h>
|
||||
+#include "LSHostOpen.h"
|
||||
#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+#include <strings.h>
|
||||
@@ -2670,6 +2671,9 @@ index dd78f6df4..fc25c7970 100644
|
||||
+ if (url == NULL)
|
||||
+ return paramErr;
|
||||
+
|
||||
+ if ((params == NULL || params->application == NULL) && _LSIsHostOpenURL(url))
|
||||
+ return _LSOpenURLOnHost(url);
|
||||
+
|
||||
+ // open(1) builds URLs via +URLWithString from a raw path, which may yield
|
||||
+ // a scheme-less URL object. CFURLCopyFileSystemPath works either way.
|
||||
+ pathStr = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
|
||||
@@ -2734,7 +2738,7 @@ index dd78f6df4..fc25c7970 100644
|
||||
|
||||
OSStatus
|
||||
LSOpenURLsWithRole(
|
||||
@@ -31,8 +123,27 @@ LSOpenURLsWithRole(
|
||||
@@ -31,8 +127,27 @@ LSOpenURLsWithRole(
|
||||
ProcessSerialNumber * outPSNs,
|
||||
CFIndex inMaxPSNCount)
|
||||
{
|
||||
@@ -2765,11 +2769,14 @@ index dd78f6df4..fc25c7970 100644
|
||||
|
||||
OSStatus
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c
|
||||
index 854fa1e7f..5a39038d3 100644
|
||||
index 854fa1e7f..8a76923c4 100644
|
||||
--- a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c
|
||||
@@ -21,7 +21,11 @@
|
||||
@@ -19,9 +19,14 @@
|
||||
|
||||
#include <LaunchServices/LaunchServices.h>
|
||||
#include <LaunchServices/LaunchServicesPriv.h>
|
||||
+#include "LSHostOpen.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
-#include <launch_priv.h>
|
||||
@@ -2781,7 +2788,20 @@ index 854fa1e7f..5a39038d3 100644
|
||||
|
||||
static void get_main_executable_info(CFURLRef appURL, CFStringRef *exec, CFStringRef *bundleName);
|
||||
|
||||
@@ -108,8 +112,14 @@ OSStatus _LSLaunchApplication(CFURLRef appPath)
|
||||
@@ -77,6 +82,12 @@ OSStatus LSOpenFromURLSpec(const LSLaunchURLSpec *inLaunchSpec, CFURLRef *outLau
|
||||
if (extension != NULL)
|
||||
CFRelease(extension);
|
||||
}
|
||||
+ else if (_LSIsHostOpenURL(url))
|
||||
+ {
|
||||
+ temp = _LSOpenURLOnHost(url);
|
||||
+ if (temp)
|
||||
+ ret = temp;
|
||||
+ }
|
||||
else
|
||||
{
|
||||
printf("We can't handle this scheme yet\n");
|
||||
@@ -108,8 +119,14 @@ OSStatus _LSLaunchApplication(CFURLRef appPath)
|
||||
CFRelease(url_plus_exec);
|
||||
url_plus_exec = CFURLCreateCopyAppendingPathComponent(kCFAllocatorDefault, pre, executable, FALSE);
|
||||
CFRelease(pre);
|
||||
@@ -2798,7 +2818,7 @@ index 854fa1e7f..5a39038d3 100644
|
||||
|
||||
/* Try to use CFBundleName first */
|
||||
if (bundleName == NULL)
|
||||
@@ -122,15 +132,19 @@ OSStatus _LSLaunchApplication(CFURLRef appPath)
|
||||
@@ -122,15 +139,19 @@ OSStatus _LSLaunchApplication(CFURLRef appPath)
|
||||
CFStringGetFileSystemRepresentation(executable, exec, execSize);
|
||||
argv[0] = exec;
|
||||
argv[1] = NULL;
|
||||
@@ -2822,7 +2842,7 @@ index 854fa1e7f..5a39038d3 100644
|
||||
CFRelease(executable);
|
||||
|
||||
}
|
||||
@@ -184,3 +198,25 @@ err_pre_info:
|
||||
@@ -184,3 +205,25 @@ err_pre_info:
|
||||
CFRelease(rs);
|
||||
CFRelease(info_plist_url);
|
||||
}
|
||||
@@ -8025,10 +8045,18 @@ index 7079e613e..397ec6de0 100644
|
||||
"\treturn _elfcalls->dlsym_fatal(lib_handle, \"" << sym << "\");\n"
|
||||
"}\n\n";
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m b/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m
|
||||
index 4cb2a4941..f08d05a2b 100644
|
||||
index 4cb2a4941..29eef9956 100644
|
||||
--- a/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSInfo.m
|
||||
@@ -106,7 +106,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi
|
||||
@@ -20,6 +20,7 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#include <LaunchServices/LaunchServices.h>
|
||||
#include <CarbonCore/MacErrors.h>
|
||||
+#include "LSHostOpen.h"
|
||||
#include <stdio.h>
|
||||
#include <sqlite3.h>
|
||||
#import <Foundation/NSString.h>
|
||||
@@ -106,7 +107,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi
|
||||
{
|
||||
if (!inBundleIdentifier)
|
||||
{
|
||||
@@ -8037,7 +8065,7 @@ index 4cb2a4941..f08d05a2b 100644
|
||||
*outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, paramErr, NULL);
|
||||
return NULL;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi
|
||||
@@ -116,7 +117,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi
|
||||
|
||||
if (dq == nil)
|
||||
{
|
||||
@@ -8046,7 +8074,7 @@ index 4cb2a4941..f08d05a2b 100644
|
||||
*outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, fnfErr, NULL);
|
||||
return NULL;
|
||||
}
|
||||
@@ -138,7 +138,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi
|
||||
@@ -138,7 +139,7 @@ CFArrayRef LSCopyApplicationURLsForBundleIdentifier(CFStringRef inBundleIdentifi
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -8055,7 +8083,7 @@ index 4cb2a4941..f08d05a2b 100644
|
||||
*outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, kLSApplicationNotFoundErr, NULL);
|
||||
retval = NULL;
|
||||
}
|
||||
@@ -247,7 +247,7 @@ CFURLRef LSCopyDefaultApplicationURLForContentType(CFStringRef inContentType, LS
|
||||
@@ -247,7 +248,7 @@ CFURLRef LSCopyDefaultApplicationURLForContentType(CFStringRef inContentType, LS
|
||||
|
||||
if (dq == nil)
|
||||
{
|
||||
@@ -8064,7 +8092,7 @@ index 4cb2a4941..f08d05a2b 100644
|
||||
*outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, fnfErr, NULL);
|
||||
return NULL;
|
||||
}
|
||||
@@ -275,7 +275,7 @@ CFURLRef LSCopyDefaultApplicationURLForContentType(CFStringRef inContentType, LS
|
||||
@@ -275,7 +276,7 @@ CFURLRef LSCopyDefaultApplicationURLForContentType(CFStringRef inContentType, LS
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -8073,7 +8101,7 @@ index 4cb2a4941..f08d05a2b 100644
|
||||
*outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, kLSApplicationNotFoundErr, NULL);
|
||||
retval = NULL;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ CFURLRef LSCopyDefaultApplicationURLForURL(CFURLRef inURL, LSRolesMask inRoleMas
|
||||
@@ -292,7 +293,7 @@ CFURLRef LSCopyDefaultApplicationURLForURL(CFURLRef inURL, LSRolesMask inRoleMas
|
||||
|
||||
if (status != noErr)
|
||||
{
|
||||
@@ -8082,3 +8110,694 @@ index 4cb2a4941..f08d05a2b 100644
|
||||
*outError = CFErrorCreate(NULL, kCFErrorDomainOSStatus, status, NULL);
|
||||
return NULL;
|
||||
}
|
||||
@@ -306,6 +307,9 @@ CFURLRef LSCopyDefaultApplicationURLForURL(CFURLRef inURL, LSRolesMask inRoleMas
|
||||
|
||||
OSStatus LSGetApplicationForURL(CFURLRef inURL, LSRolesMask inRoleMask, FSRef *outAppRef, CFURLRef *outAppURL)
|
||||
{
|
||||
+ if (_LSGetHostHandlerForURL(inURL, outAppRef, outAppURL) == noErr)
|
||||
+ return noErr;
|
||||
+
|
||||
NSString* extension = [(NSURL*) inURL pathExtension];
|
||||
return LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, (CFStringRef) extension, inRoleMask, outAppRef, outAppURL);
|
||||
}
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt b/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt
|
||||
index bb7f35068..957cc9d3e 100644
|
||||
--- a/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/CMakeLists.txt
|
||||
@@ -1,5 +1,8 @@
|
||||
set(SUBFRAMEWORK_VERSION "A")
|
||||
|
||||
+# LSHostOpen.c loads the host's libsystemd through the ELF loader.
|
||||
+include_directories(${CMAKE_SOURCE_DIR}/src/startup/mldr/elfcalls)
|
||||
+
|
||||
generate_sdk_subframework(LaunchServices
|
||||
BASE_PATH ${coreservices_sdk_path}
|
||||
VERSION ${SUBFRAMEWORK_VERSION}
|
||||
@@ -20,6 +23,8 @@ add_framework(LaunchServices
|
||||
LSApplicationWorkspace.m
|
||||
LSRunning.m
|
||||
constants.c
|
||||
+ LSHostOpen.c
|
||||
+ LSHostOpenBus.c
|
||||
LSInfo.m
|
||||
LSOpen.m
|
||||
UTType.m
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h
|
||||
@@ -0,0 +1,43 @@
|
||||
+/*
|
||||
+This file is part of Darling.
|
||||
+
|
||||
+Copyright (C) 2026 Darling developers
|
||||
+
|
||||
+Darling is free software: you can redistribute it and/or modify
|
||||
+it under the terms of the GNU General Public License as published by
|
||||
+the Free Software Foundation, either version 3 of the License, or
|
||||
+(at your option) any later version.
|
||||
+
|
||||
+Darling is distributed in the hope that it will be useful,
|
||||
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+GNU General Public License for more details.
|
||||
+
|
||||
+You should have received a copy of the GNU General Public License
|
||||
+along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
+*/
|
||||
+
|
||||
+#ifndef LS_HOST_OPEN_H_
|
||||
+#define LS_HOST_OPEN_H_
|
||||
+
|
||||
+#include <CoreFoundation/CoreFoundation.h>
|
||||
+#include <LaunchServices/LaunchServices.h>
|
||||
+
|
||||
+#ifdef __cplusplus
|
||||
+extern "C" {
|
||||
+#endif
|
||||
+
|
||||
+/* URLs with these schemes (http, https, mailto) are opened by the host desktop. */
|
||||
+Boolean _LSIsHostOpenURL(CFURLRef url);
|
||||
+
|
||||
+/* Hands the URL to the host desktop. */
|
||||
+OSStatus _LSOpenURLOnHost(CFURLRef url);
|
||||
+
|
||||
+/* Reports the host opener as the default application for a host-opened URL. */
|
||||
+OSStatus _LSGetHostHandlerForURL(CFURLRef url, FSRef* outAppRef, CFURLRef* outAppURL);
|
||||
+
|
||||
+#ifdef __cplusplus
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
+#endif
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c
|
||||
@@ -0,0 +1,207 @@
|
||||
+/*
|
||||
+This file is part of Darling.
|
||||
+
|
||||
+Copyright (C) 2026 Darling developers
|
||||
+
|
||||
+Darling is free software: you can redistribute it and/or modify
|
||||
+it under the terms of the GNU General Public License as published by
|
||||
+the Free Software Foundation, either version 3 of the License, or
|
||||
+(at your option) any later version.
|
||||
+
|
||||
+Darling is distributed in the hope that it will be useful,
|
||||
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+GNU General Public License for more details.
|
||||
+
|
||||
+You should have received a copy of the GNU General Public License
|
||||
+along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
+*/
|
||||
+
|
||||
+/*
|
||||
+ * Web and mail URLs have no handler inside the prefix, so they are handed to
|
||||
+ * the Linux desktop. sd-bus is used from the host's libsystemd, loaded through
|
||||
+ * the ELF loader; the guest shares the host's user and network, so the
|
||||
+ * session bus is reachable directly.
|
||||
+ */
|
||||
+
|
||||
+#include "LSHostOpen.h"
|
||||
+#include "LSHostOpenBus.h"
|
||||
+#include <CarbonCore/MacErrors.h>
|
||||
+#include <elfcalls.h>
|
||||
+#include <pthread.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+
|
||||
+extern struct elf_calls* _elfcalls;
|
||||
+
|
||||
+/* Where the host's root file system appears inside the prefix. */
|
||||
+#define HOST_ROOT "/Volumes/SystemRoot"
|
||||
+
|
||||
+static struct ls_host_api host_api;
|
||||
+static Boolean host_api_ready;
|
||||
+static pthread_once_t host_api_once = PTHREAD_ONCE_INIT;
|
||||
+
|
||||
+static void load_host_api(void)
|
||||
+{
|
||||
+ void *systemd, *libc;
|
||||
+ struct ls_host_api api;
|
||||
+
|
||||
+ if (_elfcalls == NULL)
|
||||
+ return;
|
||||
+
|
||||
+ systemd = _elfcalls->dlopen("libsystemd.so.0");
|
||||
+ if (systemd == NULL)
|
||||
+ return;
|
||||
+ libc = _elfcalls->dlopen("libc.so.6");
|
||||
+ if (libc == NULL)
|
||||
+ {
|
||||
+ _elfcalls->dlclose(systemd);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ memset(&api, 0, sizeof(api));
|
||||
+#define BIND(lib, field) \
|
||||
+ if ((*(void**) &api.field = _elfcalls->dlsym(lib, #field)) == NULL) \
|
||||
+ goto fail;
|
||||
+ BIND(systemd, sd_bus_new);
|
||||
+ BIND(systemd, sd_bus_set_address);
|
||||
+ BIND(systemd, sd_bus_set_bus_client);
|
||||
+ BIND(systemd, sd_bus_start);
|
||||
+ BIND(systemd, sd_bus_flush_close_unref);
|
||||
+ BIND(systemd, sd_bus_message_new_method_call);
|
||||
+ BIND(systemd, sd_bus_message_append);
|
||||
+ BIND(systemd, sd_bus_message_append_strv);
|
||||
+ BIND(systemd, sd_bus_message_open_container);
|
||||
+ BIND(systemd, sd_bus_message_close_container);
|
||||
+ BIND(systemd, sd_bus_call);
|
||||
+ BIND(systemd, sd_bus_message_unref);
|
||||
+ BIND(systemd, sd_bus_error_free);
|
||||
+ /* Host IDs and paths, not the prefix's view of them. */
|
||||
+ BIND(libc, getuid);
|
||||
+ BIND(libc, access);
|
||||
+#undef BIND
|
||||
+ api.getenv = getenv;
|
||||
+ api.runtime_dir_root = "/run/user";
|
||||
+
|
||||
+ host_api = api;
|
||||
+ host_api_ready = TRUE;
|
||||
+ return;
|
||||
+
|
||||
+fail:
|
||||
+ _elfcalls->dlclose(libc);
|
||||
+ _elfcalls->dlclose(systemd);
|
||||
+}
|
||||
+
|
||||
+static const struct ls_host_api* get_host_api(void)
|
||||
+{
|
||||
+ pthread_once(&host_api_once, load_host_api);
|
||||
+ return host_api_ready ? &host_api : NULL;
|
||||
+}
|
||||
+
|
||||
+/* Returns the absolute URL as a malloc'd UTF-8 string. */
|
||||
+static char* copy_url_string(CFURLRef url)
|
||||
+{
|
||||
+ CFURLRef absolute;
|
||||
+ CFStringRef string;
|
||||
+ CFIndex size;
|
||||
+ char* buffer = NULL;
|
||||
+
|
||||
+ if (url == NULL)
|
||||
+ return NULL;
|
||||
+ absolute = CFURLCopyAbsoluteURL(url);
|
||||
+ if (absolute == NULL)
|
||||
+ return NULL;
|
||||
+
|
||||
+ string = CFURLGetString(absolute);
|
||||
+ size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8) + 1;
|
||||
+ if (size <= LS_HOST_OPEN_MAX_URL_LENGTH * 4 + 1)
|
||||
+ {
|
||||
+ buffer = malloc(size);
|
||||
+ if (buffer != NULL && !CFStringGetCString(string, buffer, size, kCFStringEncodingUTF8))
|
||||
+ {
|
||||
+ free(buffer);
|
||||
+ buffer = NULL;
|
||||
+ }
|
||||
+ }
|
||||
+ CFRelease(absolute);
|
||||
+ return buffer;
|
||||
+}
|
||||
+
|
||||
+Boolean _LSIsHostOpenURL(CFURLRef url)
|
||||
+{
|
||||
+ CFStringRef scheme;
|
||||
+ Boolean result = FALSE;
|
||||
+
|
||||
+ if (url == NULL)
|
||||
+ return FALSE;
|
||||
+ scheme = CFURLCopyScheme(url);
|
||||
+ if (scheme == NULL)
|
||||
+ return FALSE;
|
||||
+
|
||||
+ result = CFStringCompare(scheme, CFSTR("http"), kCFCompareCaseInsensitive) == kCFCompareEqualTo
|
||||
+ || CFStringCompare(scheme, CFSTR("https"), kCFCompareCaseInsensitive) == kCFCompareEqualTo
|
||||
+ || CFStringCompare(scheme, CFSTR("mailto"), kCFCompareCaseInsensitive) == kCFCompareEqualTo;
|
||||
+ CFRelease(scheme);
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+OSStatus _LSOpenURLOnHost(CFURLRef url)
|
||||
+{
|
||||
+ const struct ls_host_api* api;
|
||||
+ char* string = copy_url_string(url);
|
||||
+ int r;
|
||||
+
|
||||
+ if (string == NULL || _LSHostOpenValidateURL(string) < 0)
|
||||
+ {
|
||||
+ free(string);
|
||||
+ return paramErr;
|
||||
+ }
|
||||
+
|
||||
+ api = get_host_api();
|
||||
+ if (api == NULL)
|
||||
+ {
|
||||
+ fprintf(stderr, "LaunchServices: cannot open URL: host libsystemd is unavailable\n");
|
||||
+ free(string);
|
||||
+ return kLSApplicationNotFoundErr;
|
||||
+ }
|
||||
+
|
||||
+ r = _LSHostOpenURL(api, string, NULL);
|
||||
+ free(string);
|
||||
+ if (r < 0)
|
||||
+ {
|
||||
+ fprintf(stderr, "LaunchServices: host desktop did not accept URL (error %d)\n", -r);
|
||||
+ return kLSServerCommunicationErr;
|
||||
+ }
|
||||
+ return noErr;
|
||||
+}
|
||||
+
|
||||
+OSStatus _LSGetHostHandlerForURL(CFURLRef url, FSRef* outAppRef, CFURLRef* outAppURL)
|
||||
+{
|
||||
+ const struct ls_host_api* api;
|
||||
+ const char* opener;
|
||||
+ char path[256];
|
||||
+
|
||||
+ if (!_LSIsHostOpenURL(url))
|
||||
+ return kLSApplicationNotFoundErr;
|
||||
+ api = get_host_api();
|
||||
+ if (api == NULL)
|
||||
+ return kLSApplicationNotFoundErr;
|
||||
+
|
||||
+ opener = _LSHostOpenFindOpener(api);
|
||||
+ snprintf(path, sizeof(path), HOST_ROOT "%s", opener ? opener : "/usr/bin/xdg-open");
|
||||
+
|
||||
+ if (outAppRef != NULL)
|
||||
+ {
|
||||
+ OSStatus status = FSPathMakeRef((const UInt8*) path, outAppRef, NULL);
|
||||
+ if (status != noErr)
|
||||
+ return status;
|
||||
+ }
|
||||
+ if (outAppURL != NULL)
|
||||
+ {
|
||||
+ *outAppURL = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8*) path, strlen(path), FALSE);
|
||||
+ if (*outAppURL == NULL)
|
||||
+ return kLSApplicationNotFoundErr;
|
||||
+ }
|
||||
+ return noErr;
|
||||
+}
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h
|
||||
@@ -0,0 +1,111 @@
|
||||
+/*
|
||||
+This file is part of Darling.
|
||||
+
|
||||
+Copyright (C) 2026 Darling developers
|
||||
+
|
||||
+Darling is free software: you can redistribute it and/or modify
|
||||
+it under the terms of the GNU General Public License as published by
|
||||
+the Free Software Foundation, either version 3 of the License, or
|
||||
+(at your option) any later version.
|
||||
+
|
||||
+Darling is distributed in the hope that it will be useful,
|
||||
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+GNU General Public License for more details.
|
||||
+
|
||||
+You should have received a copy of the GNU General Public License
|
||||
+along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
+*/
|
||||
+
|
||||
+/*
|
||||
+ * Hands a URL to the host desktop over the user's D-Bus session bus.
|
||||
+ *
|
||||
+ * This part has no Darling or CoreFoundation dependencies: every host
|
||||
+ * function comes from the table below, so it can also be built natively on
|
||||
+ * Linux against libsystemd for testing. Errors are negative Linux errno
|
||||
+ * values as returned by sd-bus.
|
||||
+ */
|
||||
+
|
||||
+#ifndef LS_HOST_OPEN_BUS_H_
|
||||
+#define LS_HOST_OPEN_BUS_H_
|
||||
+
|
||||
+#include <stdint.h>
|
||||
+
|
||||
+#ifdef __cplusplus
|
||||
+extern "C" {
|
||||
+#endif
|
||||
+
|
||||
+#define LS_HOST_OPEN_MAX_URL_LENGTH 4096
|
||||
+
|
||||
+/* Linux errno values; the guest's errno numbering differs. */
|
||||
+#define LS_HOST_LINUX_ENOENT 2
|
||||
+#define LS_HOST_LINUX_EINVAL 22
|
||||
+#define LS_HOST_LINUX_ENOSYS 38
|
||||
+#define LS_HOST_LINUX_ETIMEDOUT 110
|
||||
+
|
||||
+/* Layout of sd_bus_error. */
|
||||
+struct ls_host_bus_error
|
||||
+{
|
||||
+ const char* name;
|
||||
+ const char* message;
|
||||
+ int _need_free;
|
||||
+};
|
||||
+
|
||||
+struct ls_host_api
|
||||
+{
|
||||
+ /* libsystemd (sd-bus); bus and message objects are opaque */
|
||||
+ int (*sd_bus_new)(void** ret);
|
||||
+ int (*sd_bus_set_address)(void* bus, const char* address);
|
||||
+ int (*sd_bus_set_bus_client)(void* bus, int b);
|
||||
+ int (*sd_bus_start)(void* bus);
|
||||
+ void* (*sd_bus_flush_close_unref)(void* bus);
|
||||
+ int (*sd_bus_message_new_method_call)(void* bus, void** m, const char* destination,
|
||||
+ const char* path, const char* interface, const char* member);
|
||||
+ int (*sd_bus_message_append)(void* m, const char* types, ...);
|
||||
+ int (*sd_bus_message_append_strv)(void* m, char** l);
|
||||
+ int (*sd_bus_message_open_container)(void* m, char type, const char* contents);
|
||||
+ int (*sd_bus_message_close_container)(void* m);
|
||||
+ int (*sd_bus_call)(void* bus, void* m, uint64_t usec, struct ls_host_bus_error* ret_error, void** reply);
|
||||
+ void* (*sd_bus_message_unref)(void* m);
|
||||
+ void (*sd_bus_error_free)(struct ls_host_bus_error* e);
|
||||
+
|
||||
+ /* Host process environment */
|
||||
+ char* (*getenv)(const char* name);
|
||||
+ unsigned int (*getuid)(void);
|
||||
+ int (*access)(const char* path, int mode);
|
||||
+
|
||||
+ /* Parent of the per-user runtime directories, normally "/run/user" */
|
||||
+ const char* runtime_dir_root;
|
||||
+ /* Per-call timeout; 0 selects the default */
|
||||
+ uint64_t timeout_usec;
|
||||
+};
|
||||
+
|
||||
+enum
|
||||
+{
|
||||
+ LS_HOST_OPEN_VIA_PORTAL = 1,
|
||||
+ LS_HOST_OPEN_VIA_SYSTEMD = 2,
|
||||
+};
|
||||
+
|
||||
+/* 1 if the URL has a scheme that is handed to the host (http, https, mailto). */
|
||||
+int _LSHostOpenSchemeSupported(const char* url);
|
||||
+
|
||||
+/* 0 if the URL may be handed to the host, otherwise -EINVAL. */
|
||||
+int _LSHostOpenValidateURL(const char* url);
|
||||
+
|
||||
+/* Host path of xdg-open, or NULL if it is not in the usual locations. */
|
||||
+const char* _LSHostOpenFindOpener(const struct ls_host_api* api);
|
||||
+
|
||||
+/*
|
||||
+ * Opens the URL through org.freedesktop.portal.OpenURI, or, if that fails,
|
||||
+ * runs xdg-open in a transient systemd user service. Either way the handler
|
||||
+ * is started by the host session, never as a child of the caller.
|
||||
+ * Returns 0 and stores the method used in *method (may be NULL), or a
|
||||
+ * negative errno.
|
||||
+ */
|
||||
+int _LSHostOpenURL(const struct ls_host_api* api, const char* url, int* method);
|
||||
+
|
||||
+#ifdef __cplusplus
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
+#endif
|
||||
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c
|
||||
@@ -0,0 +1,278 @@
|
||||
+/*
|
||||
+This file is part of Darling.
|
||||
+
|
||||
+Copyright (C) 2026 Darling developers
|
||||
+
|
||||
+Darling is free software: you can redistribute it and/or modify
|
||||
+it under the terms of the GNU General Public License as published by
|
||||
+the Free Software Foundation, either version 3 of the License, or
|
||||
+(at your option) any later version.
|
||||
+
|
||||
+Darling is distributed in the hope that it will be useful,
|
||||
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+GNU General Public License for more details.
|
||||
+
|
||||
+You should have received a copy of the GNU General Public License
|
||||
+along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
+*/
|
||||
+
|
||||
+#include "LSHostOpenBus.h"
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+#include <strings.h>
|
||||
+#include <time.h>
|
||||
+#include <unistd.h>
|
||||
+
|
||||
+#define DEFAULT_TIMEOUT_USEC (10ULL * 1000 * 1000)
|
||||
+#define HOST_X_OK 1
|
||||
+
|
||||
+static const char* const supported_schemes[] = { "http:", "https:", "mailto:" };
|
||||
+
|
||||
+/* Passed to the transient unit so xdg-open finds the user's display. */
|
||||
+static const char* const forwarded_environment[] = { "DISPLAY", "WAYLAND_DISPLAY", "XDG_CURRENT_DESKTOP" };
|
||||
+
|
||||
+int _LSHostOpenSchemeSupported(const char* url)
|
||||
+{
|
||||
+ if (url == NULL)
|
||||
+ return 0;
|
||||
+ for (size_t i = 0; i < sizeof(supported_schemes) / sizeof(supported_schemes[0]); i++)
|
||||
+ {
|
||||
+ size_t len = strlen(supported_schemes[i]);
|
||||
+ if (strncasecmp(url, supported_schemes[i], len) == 0)
|
||||
+ return 1;
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+int _LSHostOpenValidateURL(const char* url)
|
||||
+{
|
||||
+ size_t len;
|
||||
+
|
||||
+ if (!_LSHostOpenSchemeSupported(url))
|
||||
+ return -LS_HOST_LINUX_EINVAL;
|
||||
+
|
||||
+ len = strlen(url);
|
||||
+ if (len > LS_HOST_OPEN_MAX_URL_LENGTH)
|
||||
+ return -LS_HOST_LINUX_EINVAL;
|
||||
+
|
||||
+ for (size_t i = 0; i < len; i++)
|
||||
+ {
|
||||
+ unsigned char c = (unsigned char) url[i];
|
||||
+ if (c < 0x20 || c == 0x7f)
|
||||
+ return -LS_HOST_LINUX_EINVAL;
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+const char* _LSHostOpenFindOpener(const struct ls_host_api* api)
|
||||
+{
|
||||
+ static const char* const candidates[] = {
|
||||
+ "/usr/local/bin/xdg-open",
|
||||
+ "/usr/bin/xdg-open",
|
||||
+ "/bin/xdg-open",
|
||||
+ };
|
||||
+
|
||||
+ if (api == NULL || api->access == NULL)
|
||||
+ return NULL;
|
||||
+ for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++)
|
||||
+ {
|
||||
+ if (api->access(candidates[i], HOST_X_OK) == 0)
|
||||
+ return candidates[i];
|
||||
+ }
|
||||
+ return NULL;
|
||||
+}
|
||||
+
|
||||
+static int api_complete(const struct ls_host_api* api)
|
||||
+{
|
||||
+ return api != NULL && api->sd_bus_new && api->sd_bus_set_address && api->sd_bus_set_bus_client
|
||||
+ && api->sd_bus_start && api->sd_bus_flush_close_unref && api->sd_bus_message_new_method_call
|
||||
+ && api->sd_bus_message_append && api->sd_bus_message_append_strv
|
||||
+ && api->sd_bus_message_open_container && api->sd_bus_message_close_container
|
||||
+ && api->sd_bus_call && api->sd_bus_message_unref && api->sd_bus_error_free
|
||||
+ && api->getenv && api->getuid;
|
||||
+}
|
||||
+
|
||||
+static int open_session_bus(const struct ls_host_api* api, void** ret)
|
||||
+{
|
||||
+ char address[512];
|
||||
+ const char* env = api->getenv("DBUS_SESSION_BUS_ADDRESS");
|
||||
+ void* bus = NULL;
|
||||
+ int r;
|
||||
+
|
||||
+ if (env != NULL && env[0] != '\0')
|
||||
+ {
|
||||
+ if (strlen(env) >= sizeof(address))
|
||||
+ return -LS_HOST_LINUX_EINVAL;
|
||||
+ strcpy(address, env);
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ const char* root = api->runtime_dir_root ? api->runtime_dir_root : "/run/user";
|
||||
+ if (snprintf(address, sizeof(address), "unix:path=%s/%u/bus", root, api->getuid())
|
||||
+ >= (int) sizeof(address))
|
||||
+ return -LS_HOST_LINUX_EINVAL;
|
||||
+ }
|
||||
+
|
||||
+ r = api->sd_bus_new(&bus);
|
||||
+ if (r < 0)
|
||||
+ return r;
|
||||
+ r = api->sd_bus_set_address(bus, address);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_set_bus_client(bus, 1);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_start(bus);
|
||||
+ if (r < 0)
|
||||
+ {
|
||||
+ api->sd_bus_flush_close_unref(bus);
|
||||
+ return r;
|
||||
+ }
|
||||
+ *ret = bus;
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+static int call(const struct ls_host_api* api, void* bus, void* m)
|
||||
+{
|
||||
+ struct ls_host_bus_error error = { NULL, NULL, 0 };
|
||||
+ void* reply = NULL;
|
||||
+ uint64_t timeout = api->timeout_usec ? api->timeout_usec : DEFAULT_TIMEOUT_USEC;
|
||||
+ int r = api->sd_bus_call(bus, m, timeout, &error, &reply);
|
||||
+
|
||||
+ if (reply != NULL)
|
||||
+ api->sd_bus_message_unref(reply);
|
||||
+ api->sd_bus_error_free(&error);
|
||||
+ return r < 0 ? r : 0;
|
||||
+}
|
||||
+
|
||||
+static int open_via_portal(const struct ls_host_api* api, void* bus, const char* url)
|
||||
+{
|
||||
+ void* m = NULL;
|
||||
+ int r = api->sd_bus_message_new_method_call(bus, &m, "org.freedesktop.portal.Desktop",
|
||||
+ "/org/freedesktop/portal/desktop", "org.freedesktop.portal.OpenURI", "OpenURI");
|
||||
+ if (r < 0)
|
||||
+ return r;
|
||||
+
|
||||
+ r = api->sd_bus_message_append(m, "ss", "", url);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_append(m, "a{sv}", 0);
|
||||
+ if (r >= 0)
|
||||
+ r = call(api, bus, m);
|
||||
+ api->sd_bus_message_unref(m);
|
||||
+ return r;
|
||||
+}
|
||||
+
|
||||
+static int append_environment(const struct ls_host_api* api, void* m)
|
||||
+{
|
||||
+ char storage[3][1024];
|
||||
+ char* entries[4];
|
||||
+ size_t count = 0;
|
||||
+ int r;
|
||||
+
|
||||
+ for (size_t i = 0; i < sizeof(forwarded_environment) / sizeof(forwarded_environment[0]); i++)
|
||||
+ {
|
||||
+ const char* value = api->getenv(forwarded_environment[i]);
|
||||
+ int n;
|
||||
+
|
||||
+ if (value == NULL || value[0] == '\0')
|
||||
+ continue;
|
||||
+ n = snprintf(storage[count], sizeof(storage[count]), "%s=%s", forwarded_environment[i], value);
|
||||
+ if (n < 0 || n >= (int) sizeof(storage[count]))
|
||||
+ continue;
|
||||
+ entries[count] = storage[count];
|
||||
+ count++;
|
||||
+ }
|
||||
+ entries[count] = NULL;
|
||||
+ if (count == 0)
|
||||
+ return 0;
|
||||
+
|
||||
+ r = api->sd_bus_message_open_container(m, 'r', "sv");
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_append(m, "s", "Environment");
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_open_container(m, 'v', "as");
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_append_strv(m, entries);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_close_container(m);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_close_container(m);
|
||||
+ return r;
|
||||
+}
|
||||
+
|
||||
+static int open_via_systemd(const struct ls_host_api* api, void* bus, const char* url)
|
||||
+{
|
||||
+ static unsigned int counter;
|
||||
+ struct timespec now;
|
||||
+ char unit[128];
|
||||
+ const char* opener = _LSHostOpenFindOpener(api);
|
||||
+ void* m = NULL;
|
||||
+ int r;
|
||||
+
|
||||
+ /* Bare names are resolved by systemd >= 248. */
|
||||
+ if (opener == NULL)
|
||||
+ opener = "xdg-open";
|
||||
+
|
||||
+ clock_gettime(CLOCK_REALTIME, &now);
|
||||
+ snprintf(unit, sizeof(unit), "app-darling-openurl-%d-%llx-%u.service", (int) getpid(),
|
||||
+ (unsigned long long) now.tv_sec * 1000000000ULL + (unsigned long long) now.tv_nsec,
|
||||
+ __atomic_add_fetch(&counter, 1, __ATOMIC_RELAXED));
|
||||
+
|
||||
+ r = api->sd_bus_message_new_method_call(bus, &m, "org.freedesktop.systemd1",
|
||||
+ "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "StartTransientUnit");
|
||||
+ if (r < 0)
|
||||
+ return r;
|
||||
+
|
||||
+ r = api->sd_bus_message_append(m, "ss", unit, "fail");
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_open_container(m, 'a', "(sv)");
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_append(m, "(sv)(sv)(sv)(sv)",
|
||||
+ "Description", "s", "Open URL from Darling",
|
||||
+ /* Keep the handler running after xdg-open exits. */
|
||||
+ "KillMode", "s", "process",
|
||||
+ "CollectMode", "s", "inactive-or-failed",
|
||||
+ "ExecStart", "a(sasb)", 1, opener, 2, "xdg-open", url, 0);
|
||||
+ if (r >= 0)
|
||||
+ r = append_environment(api, m);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_close_container(m);
|
||||
+ if (r >= 0)
|
||||
+ r = api->sd_bus_message_append(m, "a(sa(sv))", 0);
|
||||
+ if (r >= 0)
|
||||
+ r = call(api, bus, m);
|
||||
+ api->sd_bus_message_unref(m);
|
||||
+ return r;
|
||||
+}
|
||||
+
|
||||
+int _LSHostOpenURL(const struct ls_host_api* api, const char* url, int* method)
|
||||
+{
|
||||
+ void* bus = NULL;
|
||||
+ int r;
|
||||
+
|
||||
+ r = _LSHostOpenValidateURL(url);
|
||||
+ if (r < 0)
|
||||
+ return r;
|
||||
+ if (!api_complete(api))
|
||||
+ return -LS_HOST_LINUX_ENOSYS;
|
||||
+
|
||||
+ r = open_session_bus(api, &bus);
|
||||
+ if (r < 0)
|
||||
+ return r;
|
||||
+
|
||||
+ r = open_via_portal(api, bus, url);
|
||||
+ if (r >= 0)
|
||||
+ {
|
||||
+ if (method)
|
||||
+ *method = LS_HOST_OPEN_VIA_PORTAL;
|
||||
+ }
|
||||
+ /* A portal that timed out may still open the URL; don't open it twice. */
|
||||
+ else if (r != -LS_HOST_LINUX_ETIMEDOUT)
|
||||
+ {
|
||||
+ r = open_via_systemd(api, bus, url);
|
||||
+ if (r >= 0 && method)
|
||||
+ *method = LS_HOST_OPEN_VIA_SYSTEMD;
|
||||
+ }
|
||||
+
|
||||
+ api->sd_bus_flush_close_unref(bus);
|
||||
+ return r < 0 ? r : 0;
|
||||
+}
|
||||
@@ -0,0 +1,440 @@
|
||||
/* Host-side regression for LaunchServices' URL hand-off (LSHostOpenBus.c).
|
||||
*
|
||||
* Runs a private dbus-daemon with no service directories (nothing can be
|
||||
* activated, so no real portal or browser is reachable), plus a fake
|
||||
* org.freedesktop.portal.Desktop / org.freedesktop.systemd1 forked from this
|
||||
* process, and drives the same hand-off code LaunchServices uses, linked
|
||||
* natively against libsystemd. The real session bus is never used.
|
||||
*
|
||||
* cc -std=gnu11 -O2 -Wall -Wextra \
|
||||
* -I build/darling-src/src/frameworks/CoreServices/src/LaunchServices \
|
||||
* tools/riot-host-open-url-check.c \
|
||||
* build/darling-src/src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c \
|
||||
* $(pkg-config --cflags --libs libsystemd) -o build/riot-host-open-url-check
|
||||
* build/riot-host-open-url-check
|
||||
*/
|
||||
#include "LSHostOpenBus.h"
|
||||
#include <systemd/sd-bus.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
enum { OWN_PORTAL = 1, OWN_SYSTEMD = 2, PORTAL_FAIL = 4, PORTAL_HANG = 8 };
|
||||
|
||||
static char tmpdir[64], records[128], bus_address[256];
|
||||
static int failures, checks, bus_new_calls, fake_mode;
|
||||
static FILE *record_file;
|
||||
static pid_t daemon_pid, main_pid;
|
||||
|
||||
/* Stops the private bus if the test exits early. */
|
||||
static void stop_daemon(void) {
|
||||
if (daemon_pid > 0 && getpid() == main_pid) {
|
||||
kill(daemon_pid, SIGTERM);
|
||||
waitpid(daemon_pid, NULL, 0);
|
||||
daemon_pid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK(cond, ...) do { checks++; if (!(cond)) { failures++; \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); } } while (0)
|
||||
|
||||
/* ---- fake desktop services (child process) ---- */
|
||||
|
||||
static void record(const char *line) {
|
||||
fputs(line, record_file);
|
||||
fputc('\n', record_file);
|
||||
fflush(record_file);
|
||||
}
|
||||
|
||||
static int portal_handler(sd_bus_message *m, void *userdata, sd_bus_error *error) {
|
||||
(void) userdata; (void) error;
|
||||
if (!sd_bus_message_is_method_call(m, "org.freedesktop.portal.OpenURI", "OpenURI"))
|
||||
return 0;
|
||||
const char *parent, *uri;
|
||||
int options = 0;
|
||||
if (sd_bus_message_read(m, "ss", &parent, &uri) < 0 ||
|
||||
sd_bus_message_enter_container(m, 'a', "{sv}") < 0)
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad OpenURI arguments");
|
||||
while (sd_bus_message_enter_container(m, 'e', "sv") > 0) {
|
||||
sd_bus_message_skip(m, "sv");
|
||||
sd_bus_message_exit_container(m);
|
||||
options++;
|
||||
}
|
||||
sd_bus_message_exit_container(m);
|
||||
char line[8192];
|
||||
snprintf(line, sizeof(line), "OpenURI\t%s\t%s\t%d", parent, uri, options);
|
||||
record(line);
|
||||
if (fake_mode & PORTAL_HANG) {
|
||||
sd_bus_message_ref(m); /* never answered */
|
||||
return 1;
|
||||
}
|
||||
if (fake_mode & PORTAL_FAIL)
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "fake portal failure");
|
||||
return sd_bus_reply_method_return(m, "o", "/org/freedesktop/portal/desktop/request/1_1/t");
|
||||
}
|
||||
|
||||
static void join(char *out, size_t size, char **list) {
|
||||
out[0] = '\0';
|
||||
for (size_t i = 0; list && list[i]; i++) {
|
||||
if (i) strncat(out, "\x1e", size - strlen(out) - 1);
|
||||
strncat(out, list[i], size - strlen(out) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static int systemd_handler(sd_bus_message *m, void *userdata, sd_bus_error *error) {
|
||||
(void) userdata; (void) error;
|
||||
if (!sd_bus_message_is_method_call(m, "org.freedesktop.systemd1.Manager", "StartTransientUnit"))
|
||||
return 0;
|
||||
const char *name, *mode, *key, *value;
|
||||
char kill_mode[64] = "", collect_mode[64] = "", path[512] = "", argv[8192] = "", env[2048] = "";
|
||||
int execs = 0, ignore;
|
||||
if (sd_bus_message_read(m, "ss", &name, &mode) < 0 ||
|
||||
sd_bus_message_enter_container(m, 'a', "(sv)") < 0)
|
||||
return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "bad unit arguments");
|
||||
while (sd_bus_message_enter_container(m, 'r', "sv") > 0) {
|
||||
sd_bus_message_read(m, "s", &key);
|
||||
if (!strcmp(key, "ExecStart")) {
|
||||
sd_bus_message_enter_container(m, 'v', "a(sasb)");
|
||||
sd_bus_message_enter_container(m, 'a', "(sasb)");
|
||||
while (sd_bus_message_enter_container(m, 'r', "sasb") > 0) {
|
||||
const char *p; char **list = NULL;
|
||||
sd_bus_message_read(m, "s", &p);
|
||||
sd_bus_message_read_strv(m, &list);
|
||||
sd_bus_message_read(m, "b", &ignore);
|
||||
sd_bus_message_exit_container(m);
|
||||
snprintf(path, sizeof(path), "%s", p);
|
||||
join(argv, sizeof(argv), list);
|
||||
for (size_t i = 0; list && list[i]; i++) free(list[i]);
|
||||
free(list);
|
||||
execs++;
|
||||
}
|
||||
sd_bus_message_exit_container(m);
|
||||
sd_bus_message_exit_container(m);
|
||||
} else if (!strcmp(key, "Environment")) {
|
||||
char **list = NULL;
|
||||
sd_bus_message_enter_container(m, 'v', "as");
|
||||
sd_bus_message_read_strv(m, &list);
|
||||
sd_bus_message_exit_container(m);
|
||||
join(env, sizeof(env), list);
|
||||
for (size_t i = 0; list && list[i]; i++) free(list[i]);
|
||||
free(list);
|
||||
} else if (!strcmp(key, "KillMode") || !strcmp(key, "CollectMode")) {
|
||||
sd_bus_message_enter_container(m, 'v', "s");
|
||||
sd_bus_message_read(m, "s", &value);
|
||||
sd_bus_message_exit_container(m);
|
||||
snprintf(!strcmp(key, "KillMode") ? kill_mode : collect_mode, 64, "%s", value);
|
||||
} else {
|
||||
sd_bus_message_skip(m, "v");
|
||||
}
|
||||
sd_bus_message_exit_container(m);
|
||||
}
|
||||
sd_bus_message_exit_container(m);
|
||||
sd_bus_message_skip(m, "a(sa(sv))");
|
||||
char line[16384];
|
||||
snprintf(line, sizeof(line), "StartTransientUnit\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s",
|
||||
name, mode, path, argv, execs, kill_mode, collect_mode, env);
|
||||
record(line);
|
||||
return sd_bus_reply_method_return(m, "o", "/org/freedesktop/systemd1/job/1");
|
||||
}
|
||||
|
||||
static pid_t start_fake(int mode) {
|
||||
int ready[2];
|
||||
if (pipe(ready)) { perror("pipe"); exit(2); }
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { perror("fork"); exit(2); }
|
||||
if (pid > 0) {
|
||||
char c = 0;
|
||||
close(ready[1]);
|
||||
if (read(ready[0], &c, 1) != 1 || c != 'R') { fprintf(stderr, "fake service failed\n"); exit(2); }
|
||||
close(ready[0]);
|
||||
return pid;
|
||||
}
|
||||
close(ready[0]);
|
||||
fake_mode = mode;
|
||||
record_file = fopen(records, "a");
|
||||
sd_bus *bus = NULL;
|
||||
if (!record_file || sd_bus_new(&bus) < 0 || sd_bus_set_address(bus, bus_address) < 0 ||
|
||||
sd_bus_set_bus_client(bus, 1) < 0 || sd_bus_start(bus) < 0)
|
||||
_exit(3);
|
||||
sd_bus_add_object(bus, NULL, "/org/freedesktop/portal/desktop", portal_handler, NULL);
|
||||
sd_bus_add_object(bus, NULL, "/org/freedesktop/systemd1", systemd_handler, NULL);
|
||||
if ((mode & OWN_PORTAL) && sd_bus_request_name(bus, "org.freedesktop.portal.Desktop", 0) < 0)
|
||||
_exit(4);
|
||||
if ((mode & OWN_SYSTEMD) && sd_bus_request_name(bus, "org.freedesktop.systemd1", 0) < 0)
|
||||
_exit(5);
|
||||
if (write(ready[1], "R", 1) != 1)
|
||||
_exit(6);
|
||||
for (;;) {
|
||||
int r = sd_bus_process(bus, NULL);
|
||||
if (r < 0) _exit(7);
|
||||
if (r == 0 && sd_bus_wait(bus, UINT64_MAX) < 0) _exit(8);
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_fake(pid_t pid) {
|
||||
kill(pid, SIGTERM);
|
||||
waitpid(pid, NULL, 0);
|
||||
}
|
||||
|
||||
/* ---- client side ---- */
|
||||
|
||||
static int counting_bus_new(void **ret) {
|
||||
bus_new_calls++;
|
||||
return sd_bus_new((sd_bus **) ret);
|
||||
}
|
||||
|
||||
static struct ls_host_api native_api(void) {
|
||||
struct ls_host_api api;
|
||||
memset(&api, 0, sizeof(api));
|
||||
#define SET(f) api.f = (__typeof__(api.f)) f
|
||||
api.sd_bus_new = counting_bus_new;
|
||||
SET(sd_bus_set_address); SET(sd_bus_set_bus_client); SET(sd_bus_start);
|
||||
SET(sd_bus_flush_close_unref); SET(sd_bus_message_new_method_call); SET(sd_bus_message_append);
|
||||
SET(sd_bus_message_append_strv); SET(sd_bus_message_open_container);
|
||||
SET(sd_bus_message_close_container); SET(sd_bus_call); SET(sd_bus_message_unref);
|
||||
SET(sd_bus_error_free); SET(getenv); SET(access);
|
||||
#undef SET
|
||||
api.getuid = (unsigned int (*)(void)) getuid;
|
||||
api.runtime_dir_root = tmpdir; /* never /run/user in this test */
|
||||
return api;
|
||||
}
|
||||
|
||||
static char *read_records(void) {
|
||||
static char buffer[65536];
|
||||
FILE *f = fopen(records, "r");
|
||||
size_t n = f ? fread(buffer, 1, sizeof(buffer) - 1, f) : 0;
|
||||
if (f) fclose(f);
|
||||
buffer[n] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static void clear_records(void) {
|
||||
FILE *f = fopen(records, "w");
|
||||
if (f) fclose(f);
|
||||
}
|
||||
|
||||
/* Splits one record line into tab-separated fields (empty fields kept). */
|
||||
static int fields(char *line, char **out, int max) {
|
||||
int n = 0;
|
||||
char *p = line;
|
||||
while (n < max) {
|
||||
out[n++] = p;
|
||||
p = strchr(p, '\t');
|
||||
if (!p) break;
|
||||
*p++ = '\0';
|
||||
}
|
||||
char *nl = strchr(out[n - 1], '\n');
|
||||
if (nl) *nl = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
static int count_lines(const char *s) {
|
||||
int n = 0;
|
||||
for (; *s; s++) n += *s == '\n';
|
||||
return n;
|
||||
}
|
||||
|
||||
static void check_portal(const char *url) {
|
||||
struct ls_host_api api = native_api();
|
||||
clear_records();
|
||||
int method = 0, r = _LSHostOpenURL(&api, url, &method);
|
||||
CHECK(r == 0 && method == LS_HOST_OPEN_VIA_PORTAL, "portal open of %s: r=%d method=%d", url, r, method);
|
||||
char *rec = read_records(), *f[8];
|
||||
CHECK(count_lines(rec) == 1, "portal: expected exactly one call, got:\n%s", rec);
|
||||
int n = fields(rec, f, 8);
|
||||
CHECK(n == 4 && !strcmp(f[0], "OpenURI") && !strcmp(f[1], "") && !strcmp(f[2], url) && !strcmp(f[3], "0"),
|
||||
"portal call for %s recorded as '%s' '%s' '%s'", url, f[0], n > 2 ? f[2] : "", n > 3 ? f[3] : "");
|
||||
}
|
||||
|
||||
static void check_systemd(const char *url, const char *what) {
|
||||
struct ls_host_api api = native_api();
|
||||
clear_records();
|
||||
int method = 0, r = _LSHostOpenURL(&api, url, &method);
|
||||
CHECK(r == 0 && method == LS_HOST_OPEN_VIA_SYSTEMD, "%s: r=%d method=%d", what, r, method);
|
||||
char *rec = read_records();
|
||||
char *unit = strstr(rec, "StartTransientUnit\t"), *f[10];
|
||||
CHECK(unit != NULL, "%s: no StartTransientUnit call, records:\n%s", what, rec);
|
||||
if (!unit) return;
|
||||
int n = fields(unit, f, 10);
|
||||
char expected_argv[8192];
|
||||
snprintf(expected_argv, sizeof(expected_argv), "xdg-open\x1e%s", url);
|
||||
size_t plen = strlen(f[3]);
|
||||
CHECK(n == 9, "%s: malformed record", what);
|
||||
CHECK(!strncmp(f[1], "app-darling-openurl-", 20) && strstr(f[1], ".service"), "%s: unit name %s", what, f[1]);
|
||||
CHECK(!strcmp(f[2], "fail"), "%s: mode %s", what, f[2]);
|
||||
CHECK(!strcmp(f[3], "xdg-open") || (f[3][0] == '/' && plen > 9 && !strcmp(f[3] + plen - 9, "/xdg-open")),
|
||||
"%s: ExecStart path %s", what, f[3]);
|
||||
CHECK(!strcmp(f[4], expected_argv), "%s: argv is not [xdg-open, url]: %s", what, f[4]);
|
||||
CHECK(!strcmp(f[5], "1"), "%s: %s ExecStart entries", what, f[5]);
|
||||
CHECK(!strcmp(f[6], "process") && !strcmp(f[7], "inactive-or-failed"), "%s: KillMode=%s CollectMode=%s",
|
||||
what, f[6], f[7]);
|
||||
CHECK(n == 9 && !strcmp(f[8], "DISPLAY=:97\x1eWAYLAND_DISPLAY=wayland-test"), "%s: Environment=%s",
|
||||
what, n == 9 ? f[8] : "");
|
||||
}
|
||||
|
||||
static void check_rejected(const char *url, const char *what) {
|
||||
struct ls_host_api api = native_api();
|
||||
clear_records();
|
||||
int before = bus_new_calls, r = _LSHostOpenURL(&api, url, NULL);
|
||||
CHECK(r == -EINVAL, "%s: expected -EINVAL, got %d", what, r);
|
||||
CHECK(bus_new_calls == before, "%s: a bus connection was opened", what);
|
||||
CHECK(read_records()[0] == '\0', "%s: reached the bus", what);
|
||||
}
|
||||
|
||||
static pid_t start_daemon(void) {
|
||||
char config[128], conf[1024];
|
||||
snprintf(config, sizeof(config), "%s/bus.conf", tmpdir);
|
||||
snprintf(conf, sizeof(conf),
|
||||
"<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN\"\n"
|
||||
" \"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">\n"
|
||||
"<busconfig><type>session</type><listen>unix:path=%s/%u/bus</listen>"
|
||||
"<auth>EXTERNAL</auth><policy context=\"default\"><allow send_destination=\"*\"/>"
|
||||
"<allow receive_sender=\"*\"/><allow own=\"*\"/></policy></busconfig>\n", tmpdir, getuid());
|
||||
FILE *f = fopen(config, "w");
|
||||
if (!f || fputs(conf, f) < 0 || fclose(f)) { perror(config); exit(2); }
|
||||
|
||||
int out[2];
|
||||
if (pipe(out)) { perror("pipe"); exit(2); }
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
char arg_config[160], arg_fd[32];
|
||||
close(out[0]);
|
||||
snprintf(arg_config, sizeof(arg_config), "--config-file=%s", config);
|
||||
snprintf(arg_fd, sizeof(arg_fd), "--print-address=%d", out[1]);
|
||||
execlp("dbus-daemon", "dbus-daemon", arg_config, "--nofork", "--nosyslog", arg_fd, (char *) NULL);
|
||||
_exit(127);
|
||||
}
|
||||
close(out[1]);
|
||||
ssize_t n = read(out[0], bus_address, sizeof(bus_address) - 1);
|
||||
close(out[0]);
|
||||
if (n <= 0) { fprintf(stderr, "dbus-daemon did not start\n"); exit(2); }
|
||||
bus_address[n] = '\0';
|
||||
bus_address[strcspn(bus_address, "\n")] = '\0';
|
||||
return pid;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(tmpdir, sizeof(tmpdir), "/tmp/riot-host-open-url-XXXXXX");
|
||||
if (!mkdtemp(tmpdir)) { perror("mkdtemp"); return 2; }
|
||||
char userdir[96];
|
||||
snprintf(userdir, sizeof(userdir), "%s/%u", tmpdir, getuid());
|
||||
mkdir(userdir, 0700);
|
||||
snprintf(records, sizeof(records), "%s/records", tmpdir);
|
||||
clear_records();
|
||||
|
||||
main_pid = getpid();
|
||||
atexit(stop_daemon);
|
||||
daemon_pid = start_daemon();
|
||||
setenv("DBUS_SESSION_BUS_ADDRESS", bus_address, 1);
|
||||
setenv("DISPLAY", ":97", 1);
|
||||
setenv("WAYLAND_DISPLAY", "wayland-test", 1);
|
||||
unsetenv("XDG_CURRENT_DESKTOP");
|
||||
|
||||
/* Portal available: OpenURI gets the exact URL, systemd is not used. */
|
||||
pid_t fake = start_fake(OWN_PORTAL | OWN_SYSTEMD);
|
||||
check_portal("https://example.invalid/path?q=1&x=%20y#frag");
|
||||
check_portal("HTTP://Example.invalid/");
|
||||
check_portal("mailto:[email protected]?subject=Hi%20there");
|
||||
|
||||
/* Rejected URLs never reach the bus. */
|
||||
char long_url[LS_HOST_OPEN_MAX_URL_LENGTH + 64];
|
||||
memset(long_url, 'a', sizeof(long_url) - 1);
|
||||
memcpy(long_url, "https://", 8);
|
||||
long_url[sizeof(long_url) - 1] = '\0';
|
||||
check_rejected("file:///etc/passwd", "file scheme");
|
||||
check_rejected("javascript:alert(1)", "javascript scheme");
|
||||
check_rejected("x-darling-test://open", "custom scheme");
|
||||
check_rejected("httpx://example.invalid/", "scheme prefix");
|
||||
check_rejected("http//example.invalid/", "missing colon");
|
||||
check_rejected("", "empty URL");
|
||||
check_rejected("https://example.invalid/\nX-Injected: 1", "newline");
|
||||
check_rejected("https://example.invalid/\t", "tab");
|
||||
check_rejected("https://example.invalid/\x7f", "DEL");
|
||||
check_rejected(long_url, "overlong URL");
|
||||
{
|
||||
struct ls_host_api api = native_api();
|
||||
int before = bus_new_calls;
|
||||
CHECK(_LSHostOpenURL(&api, NULL, NULL) == -EINVAL && bus_new_calls == before, "NULL URL");
|
||||
}
|
||||
stop_fake(fake);
|
||||
|
||||
/* Portal name absent: transient systemd unit running [xdg-open, url]. */
|
||||
fake = start_fake(OWN_SYSTEMD);
|
||||
check_systemd("https://example.invalid/fallback?a=1&b=2", "fallback, portal absent");
|
||||
check_systemd("mailto:[email protected]", "fallback mailto");
|
||||
stop_fake(fake);
|
||||
|
||||
/* Portal returns an error: fall back as well. */
|
||||
fake = start_fake(OWN_PORTAL | OWN_SYSTEMD | PORTAL_FAIL);
|
||||
check_systemd("https://example.invalid/portal-error", "fallback, portal error");
|
||||
stop_fake(fake);
|
||||
|
||||
/* Portal hangs: report the timeout, do not open the URL a second time. */
|
||||
fake = start_fake(OWN_PORTAL | OWN_SYSTEMD | PORTAL_HANG);
|
||||
{
|
||||
struct ls_host_api api = native_api();
|
||||
api.timeout_usec = 300000;
|
||||
clear_records();
|
||||
int r = _LSHostOpenURL(&api, "https://example.invalid/slow", NULL);
|
||||
char *rec = read_records();
|
||||
CHECK(r == -ETIMEDOUT, "portal timeout: r=%d", r);
|
||||
CHECK(strstr(rec, "OpenURI\t") && !strstr(rec, "StartTransientUnit"), "portal timeout fell back:\n%s", rec);
|
||||
}
|
||||
stop_fake(fake);
|
||||
|
||||
/* Neither service present: an error, no crash. */
|
||||
fake = start_fake(0);
|
||||
{
|
||||
struct ls_host_api api = native_api();
|
||||
int r = _LSHostOpenURL(&api, "https://example.invalid/nobody", NULL);
|
||||
CHECK(r < 0, "no services: r=%d", r);
|
||||
}
|
||||
stop_fake(fake);
|
||||
|
||||
/* DBUS_SESSION_BUS_ADDRESS unset: <runtime root>/<uid>/bus is used. */
|
||||
fake = start_fake(OWN_PORTAL);
|
||||
unsetenv("DBUS_SESSION_BUS_ADDRESS");
|
||||
check_portal("https://example.invalid/runtime-dir");
|
||||
stop_fake(fake);
|
||||
|
||||
/* Missing or unusable bus: an error, no crash. */
|
||||
{
|
||||
struct ls_host_api api = native_api();
|
||||
char missing[160];
|
||||
snprintf(missing, sizeof(missing), "unix:path=%s/missing/bus", tmpdir);
|
||||
setenv("DBUS_SESSION_BUS_ADDRESS", missing, 1);
|
||||
int r1 = _LSHostOpenURL(&api, "https://example.invalid/", NULL);
|
||||
setenv("DBUS_SESSION_BUS_ADDRESS", "not-an-address", 1);
|
||||
int r2 = _LSHostOpenURL(&api, "https://example.invalid/", NULL);
|
||||
unsetenv("DBUS_SESSION_BUS_ADDRESS");
|
||||
api.runtime_dir_root = missing;
|
||||
int r3 = _LSHostOpenURL(&api, "https://example.invalid/", NULL);
|
||||
CHECK(r1 < 0 && r2 < 0 && r3 < 0, "missing bus: r=%d,%d,%d", r1, r2, r3);
|
||||
}
|
||||
|
||||
/* libsystemd unavailable or incomplete: an error, no crash. */
|
||||
{
|
||||
struct ls_host_api api = native_api();
|
||||
CHECK(_LSHostOpenURL(NULL, "https://example.invalid/", NULL) == -ENOSYS, "NULL API");
|
||||
api.sd_bus_call = NULL;
|
||||
CHECK(_LSHostOpenURL(&api, "https://example.invalid/", NULL) == -ENOSYS, "incomplete API");
|
||||
}
|
||||
|
||||
stop_daemon();
|
||||
unlink(records);
|
||||
char path[160];
|
||||
snprintf(path, sizeof(path), "%s/bus.conf", tmpdir); unlink(path);
|
||||
snprintf(path, sizeof(path), "%s/bus", userdir); unlink(path);
|
||||
rmdir(userdir);
|
||||
rmdir(tmpdir);
|
||||
|
||||
printf("HOST_OPEN_URL %s checks=%d failures=%d\n", failures ? "FAIL" : "PASS", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -8,13 +8,17 @@
|
||||
"revision": "60ba801decee7a00782f74f6be4c8ffb013f79ff",
|
||||
"repository": "https://github.com/darlinghq/darling.git",
|
||||
"patch": "patches/darling.patch",
|
||||
"modified_files": 86,
|
||||
"modified_files": 87,
|
||||
"new_files": [
|
||||
"src/CoreAudio/CoreAudio/AudioConverterShim.c",
|
||||
"src/CoreAudio/AudioTrace.h",
|
||||
"src/CoreAudio/CoreAudioComponent/MatrixMixerAU.cpp",
|
||||
"src/frameworks/Carbon/HIToolbox/src/SecureInput.c",
|
||||
"src/frameworks/CoreMedia/include/CoreMedia/CMFormatDescription.h",
|
||||
"src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.c",
|
||||
"src/frameworks/CoreServices/src/LaunchServices/LSHostOpen.h",
|
||||
"src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.c",
|
||||
"src/frameworks/CoreServices/src/LaunchServices/LSHostOpenBus.h",
|
||||
"src/frameworks/CryptoTokenKit/src/TKTokenWatcher.m",
|
||||
"src/frameworks/OpenDirectory/src/ODLocalDirectory.h",
|
||||
"src/frameworks/OpenDirectory/src/ODLocalDirectory.m",
|
||||
|
||||
Reference in new issue
Block a user