From 442053eae26f27b20cfb6b00fa9e268b717e364c Mon Sep 17 00:00:00 2001 From: "T. R. Bernstein" Date: Mon, 14 Sep 2026 00:13:19 +0200 Subject: [PATCH] Move the event mask into a platform-neutral product `InotifyEventMask` took its bits from the C header, so nothing that imported it could build outside Linux. The new `InotifyMask` product spells out the kernel constants instead; a Linux test compares each of them with the header. `Inotify` re-exports the module, so existing code is unaffected. --- Package.swift | 8 ++- README.md | 2 + Sources/Inotify/Exports.swift | 3 ++ Sources/Inotify/InotifyEventMask.swift | 47 ----------------- Sources/InotifyMask/InotifyEventMask.swift | 51 +++++++++++++++++++ .../EventMaskTests.swift | 31 +++++++++++ 6 files changed, 94 insertions(+), 48 deletions(-) create mode 100644 Sources/Inotify/Exports.swift delete mode 100644 Sources/Inotify/InotifyEventMask.swift create mode 100644 Sources/InotifyMask/InotifyEventMask.swift create mode 100644 Tests/InotifyIntegrationTests/EventMaskTests.swift diff --git a/Package.swift b/Package.swift index e747daf..97f94cc 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,11 @@ let package = Package( .library( name: "Inotify", targets: ["Inotify"] - ) + ), + .library( + name: "InotifyMask", + targets: ["InotifyMask"] + ), ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.1"), @@ -20,10 +24,12 @@ let package = Package( ], targets: [ .systemLibrary(name: "CInotify"), + .target(name: "InotifyMask"), .target( name: "Inotify", dependencies: [ "CInotify", + "InotifyMask", .product(name: "Logging", package: "swift-log"), .product(name: "_NIOFileSystem", package: "swift-nio"), .product(name: "SystemPackage", package: "swift-system") diff --git a/README.md b/README.md index 442a817..49cede5 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,8 @@ Use `isExcluded(_:)` to check whether a name is currently on the exclusion list. `InotifyEventMask` is an `OptionSet` that mirrors the native inotify flags. You can combine them freely. +The mask lives in the separate `InotifyMask` product, which has no Linux dependency. Depend on it alone where code only stores or compares masks and must build or be tested on other platforms; `Inotify` re-exports it. + | Mask | Description | |------|-------------| | `.access` | File was read | diff --git a/Sources/Inotify/Exports.swift b/Sources/Inotify/Exports.swift new file mode 100644 index 0000000..baa03af --- /dev/null +++ b/Sources/Inotify/Exports.swift @@ -0,0 +1,3 @@ +// The mask lives in its own module so that it is usable off Linux; users +// of `Inotify` keep seeing it as before. +@_exported import InotifyMask diff --git a/Sources/Inotify/InotifyEventMask.swift b/Sources/Inotify/InotifyEventMask.swift deleted file mode 100644 index 5c15818..0000000 --- a/Sources/Inotify/InotifyEventMask.swift +++ /dev/null @@ -1,47 +0,0 @@ -import CInotify - -public struct InotifyEventMask: OptionSet, Sendable, Hashable { - public let rawValue: CUnsignedInt - - public init(rawValue: UInt32) { - self.rawValue = rawValue - } - - // MARK: - Watchable Events - - public static let access = InotifyEventMask(rawValue: CUnsignedInt(IN_ACCESS)) - public static let attrib = InotifyEventMask(rawValue: CUnsignedInt(IN_ATTRIB)) - public static let closeWrite = InotifyEventMask(rawValue: CUnsignedInt(IN_CLOSE_WRITE)) - public static let closeNoWrite = InotifyEventMask(rawValue: CUnsignedInt(IN_CLOSE_NOWRITE)) - public static let create = InotifyEventMask(rawValue: CUnsignedInt(IN_CREATE)) - public static let delete = InotifyEventMask(rawValue: CUnsignedInt(IN_DELETE)) - public static let deleteSelf = InotifyEventMask(rawValue: CUnsignedInt(IN_DELETE_SELF)) - public static let modify = InotifyEventMask(rawValue: CUnsignedInt(IN_MODIFY)) - public static let moveSelf = InotifyEventMask(rawValue: CUnsignedInt(IN_MOVE_SELF)) - public static let movedFrom = InotifyEventMask(rawValue: CUnsignedInt(IN_MOVED_FROM)) - public static let movedTo = InotifyEventMask(rawValue: CUnsignedInt(IN_MOVED_TO)) - public static let open = InotifyEventMask(rawValue: CUnsignedInt(IN_OPEN)) - - // MARK: - Combinations - - public static let move: InotifyEventMask = [.movedFrom, .movedTo] - public static let close: InotifyEventMask = [.closeWrite, .closeNoWrite] - public static let allEvents: InotifyEventMask = [ - .access, .attrib, .closeWrite, .closeNoWrite, - .create, .delete, .deleteSelf, .modify, - .moveSelf, .movedFrom, .movedTo, .open - ] - - // MARK: - Watch Flags - - public static let dontFollow = InotifyEventMask(rawValue: CUnsignedInt(IN_DONT_FOLLOW)) - public static let onlyDir = InotifyEventMask(rawValue: CUnsignedInt(IN_ONLYDIR)) - public static let oneShot = InotifyEventMask(rawValue: CUnsignedInt(IN_ONESHOT)) - - // MARK: - Kernel-Only Flags - - public static let isDir = InotifyEventMask(rawValue: CUnsignedInt(IN_ISDIR)) - public static let ignored = InotifyEventMask(rawValue: CUnsignedInt(IN_IGNORED)) - public static let queueOverflow = InotifyEventMask(rawValue: CUnsignedInt(IN_Q_OVERFLOW)) - public static let unmount = InotifyEventMask(rawValue: CUnsignedInt(IN_UNMOUNT)) -} diff --git a/Sources/InotifyMask/InotifyEventMask.swift b/Sources/InotifyMask/InotifyEventMask.swift new file mode 100644 index 0000000..5dad20b --- /dev/null +++ b/Sources/InotifyMask/InotifyEventMask.swift @@ -0,0 +1,51 @@ +/// The events and flags of an inotify watch or event, as bits. +/// +/// The values are the constants of the Linux `` header, +/// which are part of the kernel's stable interface. Spelling them out here +/// keeps this module free of the C header, so it builds on every platform +/// and lets code that only stores or compares masks be tested off Linux. +public struct InotifyEventMask: OptionSet, Sendable, Hashable { + public let rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + // MARK: - Watchable Events + + public static let access = InotifyEventMask(rawValue: 0x0000_0001) + public static let modify = InotifyEventMask(rawValue: 0x0000_0002) + public static let attrib = InotifyEventMask(rawValue: 0x0000_0004) + public static let closeWrite = InotifyEventMask(rawValue: 0x0000_0008) + public static let closeNoWrite = InotifyEventMask(rawValue: 0x0000_0010) + public static let open = InotifyEventMask(rawValue: 0x0000_0020) + public static let movedFrom = InotifyEventMask(rawValue: 0x0000_0040) + public static let movedTo = InotifyEventMask(rawValue: 0x0000_0080) + public static let create = InotifyEventMask(rawValue: 0x0000_0100) + public static let delete = InotifyEventMask(rawValue: 0x0000_0200) + public static let deleteSelf = InotifyEventMask(rawValue: 0x0000_0400) + public static let moveSelf = InotifyEventMask(rawValue: 0x0000_0800) + + // MARK: - Combinations + + public static let move: InotifyEventMask = [.movedFrom, .movedTo] + public static let close: InotifyEventMask = [.closeWrite, .closeNoWrite] + public static let allEvents: InotifyEventMask = [ + .access, .attrib, .closeWrite, .closeNoWrite, + .create, .delete, .deleteSelf, .modify, + .moveSelf, .movedFrom, .movedTo, .open, + ] + + // MARK: - Watch Flags + + public static let onlyDir = InotifyEventMask(rawValue: 0x0100_0000) + public static let dontFollow = InotifyEventMask(rawValue: 0x0200_0000) + public static let oneShot = InotifyEventMask(rawValue: 0x8000_0000) + + // MARK: - Kernel-Only Flags + + public static let unmount = InotifyEventMask(rawValue: 0x0000_2000) + public static let queueOverflow = InotifyEventMask(rawValue: 0x0000_4000) + public static let ignored = InotifyEventMask(rawValue: 0x0000_8000) + public static let isDir = InotifyEventMask(rawValue: 0x4000_0000) +} diff --git a/Tests/InotifyIntegrationTests/EventMaskTests.swift b/Tests/InotifyIntegrationTests/EventMaskTests.swift new file mode 100644 index 0000000..07ff4b8 --- /dev/null +++ b/Tests/InotifyIntegrationTests/EventMaskTests.swift @@ -0,0 +1,31 @@ +import CInotify +import Testing +@testable import Inotify + +@Suite("Event Mask") +struct EventMaskTests { + @Test(arguments: [ + (InotifyEventMask.access, UInt32(IN_ACCESS)), + (.attrib, UInt32(IN_ATTRIB)), + (.closeWrite, UInt32(IN_CLOSE_WRITE)), + (.closeNoWrite, UInt32(IN_CLOSE_NOWRITE)), + (.create, UInt32(IN_CREATE)), + (.delete, UInt32(IN_DELETE)), + (.deleteSelf, UInt32(IN_DELETE_SELF)), + (.modify, UInt32(IN_MODIFY)), + (.moveSelf, UInt32(IN_MOVE_SELF)), + (.movedFrom, UInt32(IN_MOVED_FROM)), + (.movedTo, UInt32(IN_MOVED_TO)), + (.open, UInt32(IN_OPEN)), + (.dontFollow, UInt32(IN_DONT_FOLLOW)), + (.onlyDir, UInt32(IN_ONLYDIR)), + (.oneShot, UInt32(IN_ONESHOT)), + (.isDir, UInt32(IN_ISDIR)), + (.ignored, UInt32(IN_IGNORED)), + (.queueOverflow, UInt32(IN_Q_OVERFLOW)), + (.unmount, UInt32(IN_UNMOUNT)), + ] as [(InotifyEventMask, UInt32)]) + func matchesTheKernelConstant(mask: InotifyEventMask, constant: UInt32) { + #expect(mask.rawValue == constant) + } +}