diff --git a/.gitattributes b/.gitattributes index dae0ec2b..8cf0ac98 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ * text=auto eol=lf *.MF text eol=crlf *.jar binary +*.dylib binary \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 0f9a3fb5..3a3dff96 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ # DurianSwt releases ## [Unreleased] +### Added +- `MacDeepLink` class and dylibs for supporting deep links on modern versions of macOS. ## [5.1.0] - 2025-08-21 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a2438d63 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +This is a Gradle-based Java/Kotlin project focused on SWT (Standard Widget Toolkit) utilities. Standard gradle tasks. + +### macOS-specific Requirements +- On macOS, SWT tests require the `-XstartOnFirstThread` JVM argument (automatically configured in build.gradle). + +## Project Architecture +### Multi-Module Structure +DurianSwt is organized as a multi-module Gradle project with platform-specific implementations: + +- **durian-swt.os**: Platform detection utilities (OS, Arch, SwtPlatform) - no SWT dependencies +- **durian-swt**: Main module with core SWT utilities and builders +- **platform-specific modules**: (durian-swt.cocoa.macosx.aarch64, durian-swt.cocoa.macosx.x86_64, durian-swt.gtk.linux.x86_64, durian-swt.win32.win32.x86_64) + +## Native Components + +### macOS Deep Link Support (`natives/mac-deep-link/`) + +Contains Objective-C code for handling deep links via a custom `diffplug://` protocol on macOS without breaking SWT: + +- **DeepLinkBridge.m**: JNI bridge that intercepts URL open events from macOS +- **compile-one.sh**: Compiles the native library for a specific architecture (x86_64 or arm64) +- **clean-and-build.sh**: Builds for both architectures and deploys to appropriate module resources + +To rebuild native libraries: +```bash +cd natives/mac-deep-link +./clean-and-build.sh # Builds for both architectures +# Or compile for specific architecture: +./compile-one.sh arm64 # Apple Silicon +./compile-one.sh x86_64 # Intel +``` + +The resulting `DeepLinkBridge.dylib` files are placed in the platform-specific modules under `src/main/resources/durian-swt-natives/`. diff --git a/build.gradle b/build.gradle index daa6f8c8..7474a7e4 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,7 @@ subprojects { subProject -> } dependencies { api project(':durian-swt') + implementation "com.diffplug.durian:durian-core:$VER_DURIAN" } configurations.all { resolutionStrategy.eachDependency { DependencyResolveDetails details -> diff --git a/durian-swt.cocoa.macosx.aarch64/src/main/java/com/diffplug/common/swt/MacDeepLink.java b/durian-swt.cocoa.macosx.aarch64/src/main/java/com/diffplug/common/swt/MacDeepLink.java new file mode 100644 index 00000000..e7fc6fb6 --- /dev/null +++ b/durian-swt.cocoa.macosx.aarch64/src/main/java/com/diffplug/common/swt/MacDeepLink.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.common.swt; + +import com.diffplug.common.base.Preconditions; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.jetbrains.annotations.Nullable; + +/** + * - immediately on app startup, call `MacDeepLink.startCapturingBeforeSwt()` + * - once SWT has initialized, call `MacDeepLink.swtHasInitializedBeginReceiving(Consumer)` + * - all urls which were captured before SWT initialized will be passed immediately (on the SWT thread) + * + * That's all! Don't do anything else. + */ +public class MacDeepLink { + /** + * state transitions are: + * - `null` on startup + * - `startCapturingBeforeSwt()` transitions to an `ArrayList`, backlog urls get added to it + * - `swtHasInitializedBeginReceiving()` transitions to a `Consumer`, all new urls go there + */ + private static final AtomicReference<@Nullable Object> state = new AtomicReference<>(); + + public static void startCapturingBeforeSwt() { + String libPath = System.getProperty("durian-swt.library.path"); + if (libPath != null) { + System.load(libPath + "/durian-swt-natives/DeepLinkBridge.dylib"); + } else { + throw new IllegalArgumentException("You need to set 'durian-swt.library.path'"); + } + + var was = state.getAndSet(new ArrayList<>()); + Preconditions.checkArgument(was == null, "`startCapturingBeforeSwt() should be called first`"); + nativeBeforeSwt(); + } + + public static void swtHasInitializedBeginReceiving(Consumer handler) { + SwtMisc.assertUI(); + var was = state.getAndSet(handler); + Preconditions.checkArgument(was instanceof ArrayList, "Call `applicationStartBeforeSwt()` first."); + + var backlog = (ArrayList) was; + backlog.forEach(handler); + + nativeAfterSwt(); + } + + // Native method declarations - implemented in DeepLinkBridge.m + private static native void nativeBeforeSwt(); + + private static native void nativeAfterSwt(); + + /** + * Called from native code when a URL is received. + * This method is invoked on various threads by the native code. + * + * @param url The URL string received from the operating system + */ + public static void __internal_deliverUrl(String url) { + var was = state.get(); + if (was instanceof Consumer) { + ((Consumer) was).accept(url); + } else if (was instanceof ArrayList) { + ((ArrayList) was).add(url); + } else { + throw new IllegalStateException("Expected Consumer or ArrayList, was " + was); + } + } +} diff --git a/durian-swt.cocoa.macosx.aarch64/src/main/resources/durian-swt-natives/DeepLinkBridge.dylib b/durian-swt.cocoa.macosx.aarch64/src/main/resources/durian-swt-natives/DeepLinkBridge.dylib new file mode 100755 index 00000000..b14dd7ba Binary files /dev/null and b/durian-swt.cocoa.macosx.aarch64/src/main/resources/durian-swt-natives/DeepLinkBridge.dylib differ diff --git a/durian-swt.cocoa.macosx.x86_64/src/main/java/com/diffplug/common/swt/MacDeepLink.java b/durian-swt.cocoa.macosx.x86_64/src/main/java/com/diffplug/common/swt/MacDeepLink.java new file mode 100644 index 00000000..510b64c7 --- /dev/null +++ b/durian-swt.cocoa.macosx.x86_64/src/main/java/com/diffplug/common/swt/MacDeepLink.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.common.swt; + +import com.diffplug.common.base.Preconditions; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import org.jetbrains.annotations.Nullable; + +/** + * - immediately on app startup, call `MacDeepLink.startCapturingBeforeSwt()` + * - once SWT has initialized, call `MacDeepLink.swtHasInitializedBeginReceiving(Consumer)` + * - all urls which were captured before SWT initialized will be passed immediately (on the SWT thread) + * + * That's all! Don't do anything else. + */ +public class MacDeepLink { + /** + * state transitions are: + * - `null` on startup + * - `startCapturingBeforeSwt()` transitions to an `ArrayList`, backlog urls get added to it + * - `swtHasInitializedBeginReceiving()` transitions to a `Consumer`, all new urls go there + */ + private static final AtomicReference<@Nullable Object> state = new AtomicReference<>(); + + public static void startCapturingBeforeSwt() { + String libPath = System.getProperty("durian-swt.library.path"); + if (libPath != null) { + System.load(libPath + "/durian-swt-natives/DeepLinkBridge.dylib"); + } else { + throw new IllegalArgumentException("You need to set 'durian-swt.library.path'"); + } + + var was = state.getAndSet(new ArrayList<>()); + Preconditions.checkArgument(was == null, "`startCapturingBeforeSwt() should be called first`"); + nativeBeforeSwt(); + } + + public static void swtHasInitializedBeginReceiving(Consumer handler) { + SwtMisc.assertUI(); + var was = state.getAndSet(handler); + Preconditions.checkArgument(was instanceof ArrayList, "Call `applicationStartBeforeSwt()` first."); + + var backlog = (ArrayList) was; + backlog.forEach(handler); + + nativeAfterSwt(); + } + + // Native method declarations - implemented in DeepLinkBridge.m + private static native void nativeBeforeSwt(); + + private static native void nativeAfterSwt(); + + /** + * Called from native code when a URL is received. + * This method is invoked on various threads by the native code. + * + * @param url The URL string received from the operating system + */ + public static void __internal_deliverUrl(String url) { + var was = state.get(); + if (was instanceof Consumer) { + ((Consumer) was).accept(url); + } else if (was instanceof ArrayList) { + ((ArrayList) was).add(url); + } else { + throw new IllegalStateException("Expected Consumer or ArrayList, was " + was); + } + } +} diff --git a/durian-swt.cocoa.macosx.x86_64/src/main/resources/durian-swt-natives/DeepLinkBridge.dylib b/durian-swt.cocoa.macosx.x86_64/src/main/resources/durian-swt-natives/DeepLinkBridge.dylib new file mode 100755 index 00000000..0aad9a1d Binary files /dev/null and b/durian-swt.cocoa.macosx.x86_64/src/main/resources/durian-swt-natives/DeepLinkBridge.dylib differ diff --git a/durian-swt.gtk.linux.x86/src/main/resources/durian-swt-natives/placeholder b/durian-swt.gtk.linux.x86/src/main/resources/durian-swt-natives/placeholder new file mode 100644 index 00000000..e69de29b diff --git a/durian-swt.gtk.linux.x86_64/src/main/resources/durian-swt-natives/placeholder b/durian-swt.gtk.linux.x86_64/src/main/resources/durian-swt-natives/placeholder new file mode 100644 index 00000000..e69de29b diff --git a/durian-swt.win32.win32.x86/src/main/resources/durian-swt-natives/placeholder b/durian-swt.win32.win32.x86/src/main/resources/durian-swt-natives/placeholder new file mode 100644 index 00000000..e69de29b diff --git a/durian-swt.win32.win32.x86_64/src/main/resources/durian-swt-natives/placeholder b/durian-swt.win32.win32.x86_64/src/main/resources/durian-swt-natives/placeholder new file mode 100644 index 00000000..e69de29b diff --git a/natives/mac-deep-link/DeepLinkBridge.m b/natives/mac-deep-link/DeepLinkBridge.m new file mode 100644 index 00000000..37d39809 --- /dev/null +++ b/natives/mac-deep-link/DeepLinkBridge.m @@ -0,0 +1,260 @@ +// DeepLinkBridge.m - Handle deep links on macOS without breaking SWT +// Build as a small .dylib and load it early (see Java below). +#import +#import +#import +#import + +@class DPDelegateProxy; // Forward declaration + +static JavaVM *gJVM = NULL; +static jclass gHandlerClass = NULL; // Global ref to MacDeepLink class +static jmethodID gDeliverMID = NULL; // Method ID for deliverURL(String) +static DPDelegateProxy *gDelegateProxy = NULL; // Strong ref to prevent deallocation + +#pragma mark - Helpers + +static os_log_t getLog(void) { + static os_log_t log = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + log = os_log_create("com.diffplug.deeplink", "DeepLinkBridge"); + }); + return log; +} + +static void abortWithMessage(NSString *message) { + os_log_fault(getLog(), "FATAL: %{public}@", message); + + // Most aggressive crash - direct null pointer dereference + // This causes SIGSEGV which is very hard to catch + [[NSNotificationCenter defaultCenter] + postNotificationName:@"DiffPlug diffplug:// protocol error" + object:nil + userInfo:@{@"error": message}]; +} + +#pragma mark - JNI bootstrap + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + gJVM = vm; + os_log_info(getLog(), "JNI_OnLoad: JavaVM stored"); + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { + os_log_info(getLog(), "JNI_OnUnload: Cleaning up"); + + // Deregister Apple Event handler + [[NSAppleEventManager sharedAppleEventManager] + removeEventHandlerForEventClass:kInternetEventClass + andEventID:kAEGetURL]; + os_log_info(getLog(), "Removed Apple Event handler"); + + // Restore original delegate before releasing proxy + if (gDelegateProxy && NSApp) { + // Access the original delegate directly via ivar + Ivar ivar = class_getInstanceVariable(object_getClass(gDelegateProxy), "_realDelegate"); + id originalDelegate = object_getIvar(gDelegateProxy, ivar); + [NSApp setDelegate:originalDelegate]; + os_log_info(getLog(), "Restored original delegate"); + } + + // Clean up global reference + if (gHandlerClass) { + JNIEnv *env; + if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) == JNI_OK) { + (*env)->DeleteGlobalRef(env, gHandlerClass); + gHandlerClass = NULL; + os_log_info(getLog(), "Released global class reference"); + } + } + + // Clear cached values + gDeliverMID = NULL; + gJVM = NULL; + gDelegateProxy = NULL; // Now safe to release the proxy +} + +static JNIEnv* getEnv(BOOL *didAttach) { + *didAttach = NO; + if (!gJVM) return NULL; + JNIEnv *env = NULL; + jint rs = (*gJVM)->GetEnv(gJVM, (void **)&env, JNI_VERSION_1_6); + if (rs == JNI_EDETACHED) { + // Use daemon attachment for system threads so they don't block JVM shutdown + if ((*gJVM)->AttachCurrentThreadAsDaemon(gJVM, (void **)&env, NULL) != 0) { + return NULL; + } + *didAttach = YES; + } else if (rs != JNI_OK) { + return NULL; + } + return env; +} + +static void deliverToJava(NSString *s) { + os_log_debug(getLog(), "deliverToJava called"); + // These should never be null since we control registration timing + if (!gHandlerClass || !gDeliverMID) { + abortWithMessage(@"JNI handler not initialized - applicationStartBeforeSwt must be called first"); + } + + BOOL didAttach = NO; + JNIEnv *env = getEnv(&didAttach); + if (!env) { + abortWithMessage(@"Cannot get JNI environment - JVM may be shutting down"); + } + + const char *utf8 = s.UTF8String; + if (utf8) { + jstring jstr = (*env)->NewStringUTF(env, utf8); + if (jstr) { + os_log_debug(getLog(), "Calling Java deliverURL"); + (*env)->CallStaticVoidMethod(env, gHandlerClass, gDeliverMID, jstr); + if ((*env)->ExceptionCheck(env)) { + os_log_error(getLog(), "Java exception occurred"); + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + (*env)->DeleteLocalRef(env, jstr); + } + } + if (didAttach) (*gJVM)->DetachCurrentThread(gJVM); +} + +#pragma mark - Apple Event handler + +// Object to handle Apple Events +@interface DeepLinkAppleEventHandler : NSObject ++ (instancetype)sharedHandler; +- (void)handleGetURL:(NSAppleEventDescriptor *)event withReply:(NSAppleEventDescriptor *)reply; +@end + +@implementation DeepLinkAppleEventHandler + ++ (instancetype)sharedHandler { + static DeepLinkAppleEventHandler *handler = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + handler = [[DeepLinkAppleEventHandler alloc] init]; + }); + return handler; +} + +- (void)handleGetURL:(NSAppleEventDescriptor *)event withReply:(NSAppleEventDescriptor *)reply { + NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + os_log_debug(getLog(), "Apple Event received"); + if (urlString.length) { + deliverToJava(urlString); + } +} + +@end + +// Install Apple Event handler when Java is ready +static void installEarlyAEHandler(void) { + @autoreleasepool { + os_log_info(getLog(), "Installing Apple Event handler"); + + // Register handler for kAEGetURL events + [[NSAppleEventManager sharedAppleEventManager] + setEventHandler:[DeepLinkAppleEventHandler sharedHandler] + andSelector:@selector(handleGetURL:withReply:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; + + os_log_info(getLog(), "Apple Event handler installed"); + } +} + +#pragma mark - NSApplicationDelegate proxy + +// Dynamic proxy that intercepts application:openURLs: and forwards all other messages +@interface DPDelegateProxy : NSProxy { + id _realDelegate; +} +- (instancetype)initWithDelegate:(id)delegate; +- (id)realDelegate; // Getter to access original delegate for restoration +@end + +@implementation DPDelegateProxy + +- (instancetype)initWithDelegate:(id)delegate { + _realDelegate = delegate; + return self; +} + +- (id)realDelegate { + return _realDelegate; +} + +- (id)forwardingTargetForSelector:(SEL)sel { + // Fast-forward everything except our intercepted method + if (sel == @selector(application:openURLs:)) { + return nil; // We'll handle this ourselves + } + return _realDelegate; +} + +- (BOOL)respondsToSelector:(SEL)sel { + if (sel == @selector(application:openURLs:)) return YES; + return [_realDelegate respondsToSelector:sel]; +} + +- (void)application:(NSApplication *)app openURLs:(NSArray *)urls { + os_log_debug(getLog(), "DPDelegateProxy received %lu URL(s)", (unsigned long)urls.count); + for (NSURL *u in urls) { + if (!u) continue; + NSString *s = u.absoluteString; + os_log_debug(getLog(), "Processing URL"); + if (s.length) deliverToJava(s); + } +} +@end + +#pragma mark - JNI exports + +// Java calls this early, before SWT initialization +JNIEXPORT void JNICALL Java_com_diffplug_common_swt_MacDeepLink_nativeBeforeSwt + (JNIEnv *env, jclass clazz) { + + os_log_info(getLog(), "nativeBeforeSwt called from Java"); + + // Cache class & method (global ref so it survives) + if (!gHandlerClass) { + gHandlerClass = (*env)->NewGlobalRef(env, clazz); + os_log_debug(getLog(), "Cached Java class reference"); + } + if (!gDeliverMID) { + gDeliverMID = (*env)->GetStaticMethodID(env, gHandlerClass, "__internal_deliverUrl", "(Ljava/lang/String;)V"); + if (!gDeliverMID) { + os_log_error(getLog(), "Could not find deliverURL method"); + return; + } + os_log_debug(getLog(), "Cached deliverURL method ID"); + } + + // Now that JNI is ready, register with macOS for Apple Events + installEarlyAEHandler(); +} + +// Java calls this after SWT is initialized to install the delegate proxy +JNIEXPORT void JNICALL Java_com_diffplug_common_swt_MacDeepLink_nativeAfterSwt + (JNIEnv *env, jclass clazz) { + + os_log_info(getLog(), "nativeAfterSwt called from Java"); + + if (!NSApp) { + abortWithMessage(@"NSApp is nil! Make sure SWT Display is created first"); + } + + // Wrap the existing delegate with our proxy + id current = [NSApp delegate]; + os_log_debug(getLog(), "Current NSApp delegate: %{public}@", NSStringFromClass([current class])); + + // Store proxy in static to prevent deallocation (NSApp.delegate is weak) + gDelegateProxy = [[DPDelegateProxy alloc] initWithDelegate:current]; + [NSApp setDelegate:(id)gDelegateProxy]; + os_log_info(getLog(), "Installed delegate proxy"); +} \ No newline at end of file diff --git a/natives/mac-deep-link/clean-and-build.sh b/natives/mac-deep-link/clean-and-build.sh new file mode 100755 index 00000000..c77de5a7 --- /dev/null +++ b/natives/mac-deep-link/clean-and-build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +for arch in aarch64 x86_64; do + rm -rf ../../durian-swt.cocoa.macosx.${arch}/src/main/resources/durian-swt-natives + mkdir -p ../../durian-swt.cocoa.macosx.${arch}/src/main/resources/durian-swt-natives + if [ "$arch" = "aarch64" ]; then + ./compile-one.sh arm64 + else + ./compile-one.sh ${arch} + fi + mv DeepLinkBridge.dylib ../../durian-swt.cocoa.macosx.${arch}/src/main/resources/durian-swt-natives +done \ No newline at end of file diff --git a/natives/mac-deep-link/compile-one.sh b/natives/mac-deep-link/compile-one.sh new file mode 100755 index 00000000..15bf8e24 --- /dev/null +++ b/natives/mac-deep-link/compile-one.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# DeepLinkBridge Compilation Script +# +# Usage: ./compile-one.sh +# architecture: x86_64 (Intel) or arm64 (Apple Silicon) +# +# This script compiles native Objective-C code for handling deep links on macOS. +# +# Prerequisites: +# - Xcode Command Line Tools +# - Java Development Kit (JDK) installed +# +# Output: +# Produces DeepLinkBridge.dylib, a macOS dynamic library that can be loaded by the JVM +# to handle URL open events from the operating system. +# +# The library provides JNI functions that: +# 1. JNI_OnLoad - Stores a reference to the JVM when the library is loaded +# 2. Java_com_diffplug_deeplink_MacDeepLink_installOpenURLHandler - Installs a handler for URL open events that calls back to Java +# +# The library should be loaded early in the application lifecycle before the UI event loop starts. + +set -e # Exit on error + +# Check for required architecture argument +if [ $# -ne 1 ]; then + echo "Error: Architecture argument required" + echo "Usage: $0 " + echo " x86_64 - Compile for Intel Macs" + echo " arm64 - Compile for Apple Silicon Macs" + exit 1 +fi + +ARCH=$1 + +# Validate architecture argument +if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "arm64" ]; then + echo "Error: Invalid architecture '$ARCH'" + echo "Valid architectures: x86_64, arm64" + exit 1 +fi + +echo "Compiling for architecture: $ARCH" + +echo "Checking for Java installation..." +if ! /usr/libexec/java_home >/dev/null 2>&1; then + echo "Error: Java not found. Please install a JDK." + echo "Run: /usr/libexec/java_home to verify Java installation" + exit 1 +fi + +echo "Java found at: $(/usr/libexec/java_home)" + +# Verify the JNI headers exist +echo "Verifying JNI headers..." +if [ ! -f "$(/usr/libexec/java_home)/include/jni.h" ]; then + echo "Error: JNI headers not found at $(/usr/libexec/java_home)/include/jni.h" + echo "Ensure you have a JDK installed (not just JRE)" + exit 1 +fi +echo "JNI headers found." + +# Compile the dynamic library +echo "Compiling DeepLinkBridge.dylib for $ARCH..." +clang -arch $ARCH -dynamiclib \ + -I"$(/usr/libexec/java_home)/include" \ + -I"$(/usr/libexec/java_home)/include/darwin" \ + -framework Cocoa \ + -o DeepLinkBridge.dylib \ + DeepLinkBridge.m + +echo "Success! DeepLinkBridge.dylib has been created for $ARCH architecture." +echo "" +echo "Verifying architecture:" +file DeepLinkBridge.dylib \ No newline at end of file