diff --git a/Handle/BNProject.cs b/Handle/BNProject.cs index 83e90ac..b4560f0 100644 --- a/Handle/BNProject.cs +++ b/Handle/BNProject.cs @@ -11,6 +11,11 @@ namespace BinaryNinja /// public sealed class Project : AbstractSafeHandle { + private readonly object notificationLock = new object(); + + private readonly List notifications = + new List(); + /// /// Initializes a new Project wrapper around an existing native handle. /// @@ -127,6 +132,19 @@ protected override bool ReleaseHandle() { if (!this.IsInvalid) { + lock (this.notificationLock) + { + foreach (ProjectNotification notification in this.notifications) + { + NativeMethods.BNUnregisterProjectNotification( + this.handle, + notification.CallbacksPointer + ); + } + + this.notifications.Clear(); + } + // Free the native project handle and mark it invalid to prevent double-free. NativeMethods.BNFreeProject(this.handle); this.SetHandleAsInvalid(); @@ -135,6 +153,51 @@ protected override bool ReleaseHandle() return true; } + /// Registers a notification receiver for this project. + public void RegisterNotification(ProjectNotification notification) + { + if (null == notification) + { + throw new ArgumentNullException(nameof(notification)); + } + + lock (this.notificationLock) + { + if (this.notifications.Contains(notification)) + { + throw new InvalidOperationException("The notification is already registered."); + } + + NativeMethods.BNRegisterProjectNotification( + this.handle, + notification.CallbacksPointer + ); + this.notifications.Add(notification); + } + } + + /// Unregisters a notification receiver from this project. + public void UnregisterNotification(ProjectNotification notification) + { + if (null == notification) + { + throw new ArgumentNullException(nameof(notification)); + } + + lock (this.notificationLock) + { + if (!this.notifications.Remove(notification)) + { + return; + } + + NativeMethods.BNUnregisterProjectNotification( + this.handle, + notification.CallbacksPointer + ); + } + } + /// /// Gets all plugin commands that are valid to run in the context of this project. /// diff --git a/ProjectNotification.cs b/ProjectNotification.cs new file mode 100644 index 0000000..3ed8859 --- /dev/null +++ b/ProjectNotification.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace BinaryNinja +{ + /// Receives lifecycle and content change events from a project. + public class ProjectNotification + { + private enum ItemKind + { + FileCreated, + FileUpdated, + FileDeleted, + FolderCreated, + FolderUpdated, + FolderDeleted + } + + private readonly List callbackRoots = new List(); + + private GCHandle callbacksHandle; + + public ProjectNotification() + { + BNProjectNotification callbacks = new BNProjectNotification(); + callbacks.context = IntPtr.Zero; + callbacks.beforeOpenProject = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeProject(this.InvokeBeforeOpenProject) + ); + callbacks.afterOpenProject = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterProject(this.InvokeAfterOpenProject) + ); + callbacks.beforeCloseProject = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeProject(this.InvokeBeforeCloseProject) + ); + callbacks.afterCloseProject = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterProject(this.InvokeAfterCloseProject) + ); + callbacks.beforeProjectMetadataWritten = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeMetadata(this.InvokeBeforeMetadataWritten) + ); + callbacks.afterProjectMetadataWritten = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterMetadata(this.InvokeAfterMetadataWritten) + ); + callbacks.beforeProjectFileCreated = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeItem(this.InvokeBeforeFileCreated) + ); + callbacks.afterProjectFileCreated = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterItem(this.InvokeAfterFileCreated) + ); + callbacks.beforeProjectFileUpdated = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeItem(this.InvokeBeforeFileUpdated) + ); + callbacks.afterProjectFileUpdated = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterItem(this.InvokeAfterFileUpdated) + ); + callbacks.beforeProjectFileDeleted = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeItem(this.InvokeBeforeFileDeleted) + ); + callbacks.afterProjectFileDeleted = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterItem(this.InvokeAfterFileDeleted) + ); + callbacks.beforeProjectFolderCreated = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeItem(this.InvokeBeforeFolderCreated) + ); + callbacks.afterProjectFolderCreated = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterItem(this.InvokeAfterFolderCreated) + ); + callbacks.beforeProjectFolderUpdated = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeItem(this.InvokeBeforeFolderUpdated) + ); + callbacks.afterProjectFolderUpdated = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterItem(this.InvokeAfterFolderUpdated) + ); + callbacks.beforeProjectFolderDeleted = this.RootCallback( + new NativeDelegates.BNProjectNotificationBeforeItem(this.InvokeBeforeFolderDeleted) + ); + callbacks.afterProjectFolderDeleted = this.RootCallback( + new NativeDelegates.BNProjectNotificationAfterItem(this.InvokeAfterFolderDeleted) + ); + this.callbacksHandle = GCHandle.Alloc(callbacks, GCHandleType.Pinned); + } + + ~ProjectNotification() + { + if (this.callbacksHandle.IsAllocated) + { + this.callbacksHandle.Free(); + } + } + + internal IntPtr CallbacksPointer + { + get { return this.callbacksHandle.AddrOfPinnedObject(); } + } + + public virtual bool OnBeforeOpenProject(Project project) { return true; } + public virtual void OnAfterOpenProject(Project project) { } + public virtual bool OnBeforeCloseProject(Project project) { return true; } + public virtual void OnAfterCloseProject(Project project) { } + public virtual bool OnBeforeProjectMetadataWritten(Project project, string key, Metadata value) { return true; } + public virtual void OnAfterProjectMetadataWritten(Project project, string key, Metadata value) { } + public virtual bool OnBeforeProjectFileCreated(Project project, ProjectFile file) { return true; } + public virtual void OnAfterProjectFileCreated(Project project, ProjectFile file) { } + public virtual bool OnBeforeProjectFileUpdated(Project project, ProjectFile file) { return true; } + public virtual void OnAfterProjectFileUpdated(Project project, ProjectFile file) { } + public virtual bool OnBeforeProjectFileDeleted(Project project, ProjectFile file) { return true; } + public virtual void OnAfterProjectFileDeleted(Project project, ProjectFile file) { } + public virtual bool OnBeforeProjectFolderCreated(Project project, ProjectFolder folder) { return true; } + public virtual void OnAfterProjectFolderCreated(Project project, ProjectFolder folder) { } + public virtual bool OnBeforeProjectFolderUpdated(Project project, ProjectFolder folder) { return true; } + public virtual void OnAfterProjectFolderUpdated(Project project, ProjectFolder folder) { } + public virtual bool OnBeforeProjectFolderDeleted(Project project, ProjectFolder folder) { return true; } + public virtual void OnAfterProjectFolderDeleted(Project project, ProjectFolder folder) { } + + private IntPtr RootCallback(TDelegate callback) + where TDelegate : Delegate + { + this.callbackRoots.Add(callback); + return Marshal.GetFunctionPointerForDelegate(callback); + } + + private bool InvokeBeforeOpenProject(IntPtr context, IntPtr project) + { + return this.InvokeBeforeProject(project, true); + } + + private bool InvokeBeforeCloseProject(IntPtr context, IntPtr project) + { + return this.InvokeBeforeProject(project, false); + } + + private bool InvokeBeforeProject(IntPtr projectHandle, bool open) + { + try + { + using (Project project = Project.MustNewFromHandle(projectHandle)) + { + return open ? this.OnBeforeOpenProject(project) : this.OnBeforeCloseProject(project); + } + } + catch (Exception exception) + { + Core.LogError("Unhandled exception in project notification: {0}", exception); + return false; + } + } + + private void InvokeAfterOpenProject(IntPtr context, IntPtr project) + { + this.InvokeAfterProject(project, true); + } + + private void InvokeAfterCloseProject(IntPtr context, IntPtr project) + { + this.InvokeAfterProject(project, false); + } + + private void InvokeAfterProject(IntPtr projectHandle, bool open) + { + try + { + using (Project project = Project.MustNewFromHandle(projectHandle)) + { + if (open) { this.OnAfterOpenProject(project); } + else { this.OnAfterCloseProject(project); } + } + } + catch (Exception exception) + { + Core.LogError("Unhandled exception in project notification: {0}", exception); + } + } + + private bool InvokeBeforeMetadataWritten(IntPtr context, IntPtr projectHandle, IntPtr key, IntPtr value) + { + string managedKey = UnsafeUtils.TakeUtf8String(key); + try + { + using (Project project = Project.MustNewFromHandle(projectHandle)) + using (Metadata metadata = Metadata.MustNewFromHandle(value)) + { + return this.OnBeforeProjectMetadataWritten(project, managedKey, metadata); + } + } + catch (Exception exception) + { + Core.LogError("Unhandled exception in project metadata notification: {0}", exception); + return false; + } + } + + private void InvokeAfterMetadataWritten(IntPtr context, IntPtr projectHandle, IntPtr key, IntPtr value) + { + string managedKey = UnsafeUtils.TakeUtf8String(key); + try + { + using (Project project = Project.MustNewFromHandle(projectHandle)) + using (Metadata metadata = Metadata.MustNewFromHandle(value)) + { + this.OnAfterProjectMetadataWritten(project, managedKey, metadata); + } + } + catch (Exception exception) + { + Core.LogError("Unhandled exception in project metadata notification: {0}", exception); + } + } + + private bool InvokeBeforeFileCreated(IntPtr c, IntPtr p, IntPtr i) { return this.InvokeBeforeItem(p, i, ItemKind.FileCreated); } + private bool InvokeBeforeFileUpdated(IntPtr c, IntPtr p, IntPtr i) { return this.InvokeBeforeItem(p, i, ItemKind.FileUpdated); } + private bool InvokeBeforeFileDeleted(IntPtr c, IntPtr p, IntPtr i) { return this.InvokeBeforeItem(p, i, ItemKind.FileDeleted); } + private bool InvokeBeforeFolderCreated(IntPtr c, IntPtr p, IntPtr i) { return this.InvokeBeforeItem(p, i, ItemKind.FolderCreated); } + private bool InvokeBeforeFolderUpdated(IntPtr c, IntPtr p, IntPtr i) { return this.InvokeBeforeItem(p, i, ItemKind.FolderUpdated); } + private bool InvokeBeforeFolderDeleted(IntPtr c, IntPtr p, IntPtr i) { return this.InvokeBeforeItem(p, i, ItemKind.FolderDeleted); } + private void InvokeAfterFileCreated(IntPtr c, IntPtr p, IntPtr i) { this.InvokeAfterItem(p, i, ItemKind.FileCreated); } + private void InvokeAfterFileUpdated(IntPtr c, IntPtr p, IntPtr i) { this.InvokeAfterItem(p, i, ItemKind.FileUpdated); } + private void InvokeAfterFileDeleted(IntPtr c, IntPtr p, IntPtr i) { this.InvokeAfterItem(p, i, ItemKind.FileDeleted); } + private void InvokeAfterFolderCreated(IntPtr c, IntPtr p, IntPtr i) { this.InvokeAfterItem(p, i, ItemKind.FolderCreated); } + private void InvokeAfterFolderUpdated(IntPtr c, IntPtr p, IntPtr i) { this.InvokeAfterItem(p, i, ItemKind.FolderUpdated); } + private void InvokeAfterFolderDeleted(IntPtr c, IntPtr p, IntPtr i) { this.InvokeAfterItem(p, i, ItemKind.FolderDeleted); } + + private bool InvokeBeforeItem(IntPtr projectHandle, IntPtr itemHandle, ItemKind kind) + { + try + { + using (Project project = Project.MustNewFromHandle(projectHandle)) + { + if (ItemKind.FileCreated == kind || ItemKind.FileUpdated == kind || ItemKind.FileDeleted == kind) + { + using (ProjectFile file = ProjectFile.MustNewFromHandle(itemHandle)) + { + if (ItemKind.FileCreated == kind) { return this.OnBeforeProjectFileCreated(project, file); } + if (ItemKind.FileUpdated == kind) { return this.OnBeforeProjectFileUpdated(project, file); } + return this.OnBeforeProjectFileDeleted(project, file); + } + } + + using (ProjectFolder folder = ProjectFolder.MustNewFromHandle(itemHandle)) + { + if (ItemKind.FolderCreated == kind) { return this.OnBeforeProjectFolderCreated(project, folder); } + if (ItemKind.FolderUpdated == kind) { return this.OnBeforeProjectFolderUpdated(project, folder); } + return this.OnBeforeProjectFolderDeleted(project, folder); + } + } + } + catch (Exception exception) + { + Core.LogError("Unhandled exception in project item notification: {0}", exception); + return false; + } + } + + private void InvokeAfterItem(IntPtr projectHandle, IntPtr itemHandle, ItemKind kind) + { + try + { + using (Project project = Project.MustNewFromHandle(projectHandle)) + { + if (ItemKind.FileCreated == kind || ItemKind.FileUpdated == kind || ItemKind.FileDeleted == kind) + { + using (ProjectFile file = ProjectFile.MustNewFromHandle(itemHandle)) + { + if (ItemKind.FileCreated == kind) { this.OnAfterProjectFileCreated(project, file); } + else if (ItemKind.FileUpdated == kind) { this.OnAfterProjectFileUpdated(project, file); } + else { this.OnAfterProjectFileDeleted(project, file); } + return; + } + } + + using (ProjectFolder folder = ProjectFolder.MustNewFromHandle(itemHandle)) + { + if (ItemKind.FolderCreated == kind) { this.OnAfterProjectFolderCreated(project, folder); } + else if (ItemKind.FolderUpdated == kind) { this.OnAfterProjectFolderUpdated(project, folder); } + else { this.OnAfterProjectFolderDeleted(project, folder); } + } + } + } + catch (Exception exception) + { + Core.LogError("Unhandled exception in project item notification: {0}", exception); + } + } + } +} diff --git a/Struct/BNProjectNotification.cs b/Struct/BNProjectNotification.cs index c825cd1..e6ef7ab 100644 --- a/Struct/BNProjectNotification.cs +++ b/Struct/BNProjectNotification.cs @@ -1,114 +1,71 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; namespace BinaryNinja { - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct BNProjectNotification + internal static partial class NativeDelegates { - /// - /// void* context - /// - public IntPtr context; - - /// - /// void** beforeOpenProject - /// - public IntPtr beforeOpenProject; - - /// - /// void** afterOpenProject - /// - public IntPtr afterOpenProject; - - /// - /// void** beforeCloseProject - /// - public IntPtr beforeCloseProject; - - /// - /// void** afterCloseProject - /// - public IntPtr afterCloseProject; - - /// - /// void** beforeProjectMetadataWritten - /// - public IntPtr beforeProjectMetadataWritten; - - /// - /// void** afterProjectMetadataWritten - /// - public IntPtr afterProjectMetadataWritten; - - /// - /// void** beforeProjectFileCreated - /// - public IntPtr beforeProjectFileCreated; - - /// - /// void** afterProjectFileCreated - /// - public IntPtr afterProjectFileCreated; - - /// - /// void** beforeProjectFileUpdated - /// - public IntPtr beforeProjectFileUpdated; - - /// - /// void** afterProjectFileUpdated - /// - public IntPtr afterProjectFileUpdated; - - /// - /// void** beforeProjectFileDeleted - /// - public IntPtr beforeProjectFileDeleted; - - /// - /// void** afterProjectFileDeleted - /// - public IntPtr afterProjectFileDeleted; - - /// - /// void** beforeProjectFolderCreated - /// - public IntPtr beforeProjectFolderCreated; - - /// - /// void** afterProjectFolderCreated - /// - public IntPtr afterProjectFolderCreated; - - /// - /// void** beforeProjectFolderUpdated - /// - public IntPtr beforeProjectFolderUpdated; - - /// - /// void** afterProjectFolderUpdated - /// - public IntPtr afterProjectFolderUpdated; - - /// - /// void** beforeProjectFolderDeleted - /// - public IntPtr beforeProjectFolderDeleted; - - /// - /// void** afterProjectFolderDeleted - /// - public IntPtr afterProjectFolderDeleted; + [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + internal delegate bool BNProjectNotificationBeforeProject(IntPtr context, IntPtr project); + + [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.Cdecl)] + internal delegate void BNProjectNotificationAfterProject(IntPtr context, IntPtr project); + + [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + internal delegate bool BNProjectNotificationBeforeMetadata( + IntPtr context, + IntPtr project, + IntPtr key, + IntPtr value + ); + + [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.Cdecl)] + internal delegate void BNProjectNotificationAfterMetadata( + IntPtr context, + IntPtr project, + IntPtr key, + IntPtr value + ); + + [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.I1)] + internal delegate bool BNProjectNotificationBeforeItem( + IntPtr context, + IntPtr project, + IntPtr item + ); + + [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.Cdecl)] + internal delegate void BNProjectNotificationAfterItem( + IntPtr context, + IntPtr project, + IntPtr item + ); } - public class ProjectNotification - { - public ProjectNotification() - { - - } - } -} \ No newline at end of file + [StructLayout(LayoutKind.Sequential)] + internal struct BNProjectNotification + { + internal IntPtr context; + internal IntPtr beforeOpenProject; + internal IntPtr afterOpenProject; + internal IntPtr beforeCloseProject; + internal IntPtr afterCloseProject; + internal IntPtr beforeProjectMetadataWritten; + internal IntPtr afterProjectMetadataWritten; + internal IntPtr beforeProjectFileCreated; + internal IntPtr afterProjectFileCreated; + internal IntPtr beforeProjectFileUpdated; + internal IntPtr afterProjectFileUpdated; + internal IntPtr beforeProjectFileDeleted; + internal IntPtr afterProjectFileDeleted; + internal IntPtr beforeProjectFolderCreated; + internal IntPtr afterProjectFolderCreated; + internal IntPtr beforeProjectFolderUpdated; + internal IntPtr afterProjectFolderUpdated; + internal IntPtr beforeProjectFolderDeleted; + internal IntPtr afterProjectFolderDeleted; + } +}