From 4cfcefcda69c76a51c5e71107a2c9efb174b5be4 Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:10:03 +0100 Subject: [PATCH] BitFoundation module to centralize shared components (#1089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Run local packages’ tests as well on CI * BitFoundation module to centralize shared components --- .github/workflows/swift-tests.yml | 26 +++-- Package.swift | 7 +- bitchat.xcodeproj/project.pbxproj | 20 ++++ bitchat/BitchatApp.swift | 1 + bitchat/Identity/IdentityModels.swift | 1 + .../Identity/SecureIdentityStateManager.swift | 1 + bitchat/Models/BitchatMessage.swift | 1 + bitchat/Models/BitchatPacket.swift | 1 + bitchat/Models/BitchatPeer.swift | 1 + bitchat/Models/ReadReceipt.swift | 1 + bitchat/Noise/NoiseRateLimiter.swift | 1 + bitchat/Noise/NoiseSession.swift | 1 + bitchat/Noise/NoiseSessionManager.swift | 1 + bitchat/Nostr/NostrEmbeddedBitChat.swift | 1 + bitchat/Protocols/BinaryEncodingUtils.swift | 57 +---------- bitchat/Protocols/BitchatProtocol.swift | 1 + bitchat/Services/BLE/BLEService.swift | 1 + bitchat/Services/CommandProcessor.swift | 1 + .../FavoritesPersistenceService.swift | 1 + .../Services/MessageFormattingEngine.swift | 1 + bitchat/Services/MessageRouter.swift | 1 + bitchat/Services/NoiseEncryptionService.swift | 1 + bitchat/Services/NostrTransport.swift | 1 + bitchat/Services/NotificationService.swift | 1 + bitchat/Services/PrivateChatManager.swift | 1 + bitchat/Services/Transport.swift | 1 + bitchat/Services/UnifiedPeerService.swift | 1 + bitchat/Sync/GossipSyncManager.swift | 1 + bitchat/Sync/RequestSyncManager.swift | 1 + bitchat/Utils/Data+SHA256.swift | 22 ----- bitchat/Utils/PeerDisplayNameResolver.swift | 1 + bitchat/ViewModels/ChatViewModel.swift | 1 + .../Extensions/ChatViewModel+Nostr.swift | 1 + .../ChatViewModel+PrivateChat.swift | 1 + bitchat/Views/ContentView.swift | 1 + bitchat/Views/FingerprintView.swift | 1 + bitchat/Views/MeshPeerList.swift | 1 + bitchat/Views/MessageListView.swift | 1 + bitchatTests/BLEServiceCoreTests.swift | 1 + bitchatTests/BLEServiceTests.swift | 1 + bitchatTests/BitchatPeerTests.swift | 1 + .../ChatViewModelDeliveryStatusTests.swift | 1 + .../ChatViewModelExtensionsTests.swift | 1 + .../ChatViewModelRefactoringTests.swift | 1 + bitchatTests/ChatViewModelTests.swift | 1 + bitchatTests/CommandProcessorTests.swift | 1 + .../EndToEnd/PrivateChatE2ETests.swift | 1 + .../EndToEnd/PublicChatE2ETests.swift | 1 + .../Fragmentation/FragmentationTests.swift | 1 + bitchatTests/GossipSyncManagerTests.swift | 1 + .../Integration/IntegrationTests.swift | 1 + .../Integration/TestNetworkHelper.swift | 1 + .../MessageFormattingEngineTests.swift | 1 + bitchatTests/Mocks/MockBLEBus.swift | 1 + bitchatTests/Mocks/MockBLEService.swift | 1 + bitchatTests/Mocks/MockIdentityManager.swift | 1 + bitchatTests/Mocks/MockTransport.swift | 1 + bitchatTests/Noise/NoiseCoverageTests.swift | 1 + bitchatTests/Noise/NoiseProtocolTests.swift | 2 +- .../Noise/NoiseRateLimiterTests.swift | 1 + bitchatTests/NostrProtocolTests.swift | 1 + .../Protocol/BinaryProtocolTests.swift | 1 + bitchatTests/ProtocolContractTests.swift | 1 + bitchatTests/ReadReceiptTests.swift | 1 + .../FavoritesPersistenceServiceTests.swift | 1 + .../Services/MessageRouterTests.swift | 1 + .../NoiseEncryptionServiceTests.swift | 1 + .../Services/NostrTransportTests.swift | 1 + .../Services/NotificationServiceTests.swift | 1 + .../Services/PrivateChatManagerTests.swift | 1 + .../SecureIdentityStateManagerTests.swift | 1 + .../Services/UnifiedPeerServiceTests.swift | 1 + .../Sync/RequestSyncManagerTests.swift | 1 + bitchatTests/TestUtilities/TestHelpers.swift | 1 + bitchatTests/ViewSmokeTests.swift | 1 + localPackages/BitFoundation/Package.swift | 27 ++++++ .../Sources/BitFoundation/Data+Hex.swift | 57 +++++++++++ .../Sources/BitFoundation/Data+SHA256.swift | 33 +++++++ .../Sources/BitFoundation}/PeerID.swift | 97 ++++++++++--------- .../Sources/BitFoundation}/String+Ext.swift | 4 +- .../BitFoundationTests}/PeerIDTests.swift | 10 +- 81 files changed, 290 insertions(+), 141 deletions(-) delete mode 100644 bitchat/Utils/Data+SHA256.swift create mode 100644 localPackages/BitFoundation/Package.swift create mode 100644 localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift create mode 100644 localPackages/BitFoundation/Sources/BitFoundation/Data+SHA256.swift rename {bitchat/Models => localPackages/BitFoundation/Sources/BitFoundation}/PeerID.swift (82%) rename {bitchat/Utils => localPackages/BitFoundation/Sources/BitFoundation}/String+Ext.swift (89%) rename {bitchatTests/Utils => localPackages/BitFoundation/Tests/BitFoundationTests}/PeerIDTests.swift (97%) diff --git a/.github/workflows/swift-tests.yml b/.github/workflows/swift-tests.yml index 00cfc100..a56b9030 100644 --- a/.github/workflows/swift-tests.yml +++ b/.github/workflows/swift-tests.yml @@ -8,9 +8,20 @@ on: jobs: test: - name: Run Swift Tests + name: Run Swift Tests (${{ matrix.name }}) runs-on: macos-latest + strategy: + fail-fast: false # Don't cancel other matrix jobs when one fails + matrix: + include: + - name: app + path: . + - name: BitLogger + path: localPackages/BitLogger + - name: BitFoundation + path: localPackages/BitFoundation + steps: - name: Checkout code uses: actions/checkout@v5 @@ -21,14 +32,11 @@ jobs: - name: Cache build artifacts uses: actions/cache@v4 with: - path: .build - key: ${{ runner.os }}-build-${{ hashFiles('**/*.swift', '**/Package.resolved') }} + path: ${{ matrix.path }}/.build + key: ${{ runner.os }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/*.swift', matrix.path), format('{0}/**/Package.resolved', matrix.path)) }} restore-keys: | - ${{ runner.os }}-build-${{ hashFiles('**/Package.resolved') }} - ${{ runner.os }}-build- - - - name: Build the package - run: swift build + ${{ runner.os }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/Package.resolved', matrix.path)) }} + ${{ runner.os }}-${{ matrix.name }}- - name: Run Tests - run: swift test --parallel + run: swift test --parallel --quiet --package-path ${{ matrix.path }} diff --git a/Package.swift b/Package.swift index c76990f3..d7bc2cb6 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies:[ .package(path: "localPackages/Arti"), + .package(path: "localPackages/BitFoundation"), .package(path: "localPackages/BitLogger"), .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1") ], @@ -25,6 +26,7 @@ let package = Package( name: "bitchat", dependencies: [ .product(name: "P256K", package: "swift-secp256k1"), + .product(name: "BitFoundation", package: "BitFoundation"), .product(name: "BitLogger", package: "BitLogger"), .product(name: "Tor", package: "Arti") ], @@ -44,7 +46,10 @@ let package = Package( ), .testTarget( name: "bitchatTests", - dependencies: ["bitchat"], + dependencies: [ + "bitchat", + .product(name: "BitFoundation", package: "BitFoundation") + ], path: "bitchatTests", exclude: [ "Info.plist", diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index fea9b691..a648a09f 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; }; 885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; }; + A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9472F80953E001CF9B9 /* BitFoundation */; }; + A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9492F809550001CF9B9 /* BitFoundation */; }; A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E56F2E77036A0032EA8A /* BitLogger */; }; A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E5712E7703760032EA8A /* BitLogger */; }; A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA7E2E7706720032EA8A /* Tor */; }; @@ -156,6 +158,7 @@ A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */, 3EE336D150427F736F32B56C /* P256K in Frameworks */, A6E3EA812E7706A80032EA8A /* Tor in Frameworks */, + A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */, ); }; B5A5CC493FFB3D8966548140 /* Frameworks */ = { @@ -164,6 +167,7 @@ A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */, 885BBED78092484A5B069461 /* P256K in Frameworks */, A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */, + A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */, ); }; /* End PBXFrameworksBuildPhase section */ @@ -223,6 +227,7 @@ B1D9136AA0083366353BFA2F /* P256K */, A6E3E5712E7703760032EA8A /* BitLogger */, A6E3EA802E7706A80032EA8A /* Tor */, + A6BCF9492F809550001CF9B9 /* BitFoundation */, ); productName = bitchat_macOS; productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */; @@ -303,6 +308,7 @@ 4EB6BA1B8464F1EA38F4E286 /* P256K */, A6E3E56F2E77036A0032EA8A /* BitLogger */, A6E3EA7E2E7706720032EA8A /* Tor */, + A6BCF9472F80953E001CF9B9 /* BitFoundation */, ); productName = bitchat_iOS; productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */; @@ -344,6 +350,7 @@ B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */, A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */, A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */, + A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */, ); preferredProjectObjectVersion = 90; projectDirPath = ""; @@ -909,6 +916,10 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ + A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = localPackages/BitFoundation; + }; A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */ = { isa = XCLocalSwiftPackageReference; relativePath = localPackages/BitLogger; @@ -936,6 +947,15 @@ package = B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; productName = P256K; }; + A6BCF9472F80953E001CF9B9 /* BitFoundation */ = { + isa = XCSwiftPackageProductDependency; + productName = BitFoundation; + }; + A6BCF9492F809550001CF9B9 /* BitFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */; + productName = BitFoundation; + }; A6E3E56F2E77036A0032EA8A /* BitLogger */ = { isa = XCSwiftPackageProductDependency; productName = BitLogger; diff --git a/bitchat/BitchatApp.swift b/bitchat/BitchatApp.swift index 4b1a8448..128ea1b7 100644 --- a/bitchat/BitchatApp.swift +++ b/bitchat/BitchatApp.swift @@ -8,6 +8,7 @@ import Tor import SwiftUI +import BitFoundation import UserNotifications @main diff --git a/bitchat/Identity/IdentityModels.swift b/bitchat/Identity/IdentityModels.swift index 11bf2f7e..29f036c6 100644 --- a/bitchat/Identity/IdentityModels.swift +++ b/bitchat/Identity/IdentityModels.swift @@ -81,6 +81,7 @@ /// import Foundation +import BitFoundation // MARK: - Three-Layer Identity Model diff --git a/bitchat/Identity/SecureIdentityStateManager.swift b/bitchat/Identity/SecureIdentityStateManager.swift index 4fe323e5..c96aeb0e 100644 --- a/bitchat/Identity/SecureIdentityStateManager.swift +++ b/bitchat/Identity/SecureIdentityStateManager.swift @@ -91,6 +91,7 @@ /// import BitLogger +import BitFoundation import Foundation import CryptoKit diff --git a/bitchat/Models/BitchatMessage.swift b/bitchat/Models/BitchatMessage.swift index 11b204fc..002858cc 100644 --- a/bitchat/Models/BitchatMessage.swift +++ b/bitchat/Models/BitchatMessage.swift @@ -7,6 +7,7 @@ // import Foundation +import BitFoundation /// Represents a user-visible message in the BitChat system. /// Handles both broadcast messages and private encrypted messages, diff --git a/bitchat/Models/BitchatPacket.swift b/bitchat/Models/BitchatPacket.swift index a647d376..004f5414 100644 --- a/bitchat/Models/BitchatPacket.swift +++ b/bitchat/Models/BitchatPacket.swift @@ -7,6 +7,7 @@ // import Foundation +import BitFoundation /// The core packet structure for all BitChat protocol messages. /// Encapsulates all data needed for routing through the mesh network, diff --git a/bitchat/Models/BitchatPeer.swift b/bitchat/Models/BitchatPeer.swift index 468d962c..6b69d019 100644 --- a/bitchat/Models/BitchatPeer.swift +++ b/bitchat/Models/BitchatPeer.swift @@ -1,5 +1,6 @@ import Foundation import CoreBluetooth +import BitFoundation /// Represents a peer in the BitChat network with all associated metadata struct BitchatPeer: Equatable { diff --git a/bitchat/Models/ReadReceipt.swift b/bitchat/Models/ReadReceipt.swift index 402527b7..a3a04e7e 100644 --- a/bitchat/Models/ReadReceipt.swift +++ b/bitchat/Models/ReadReceipt.swift @@ -7,6 +7,7 @@ // import Foundation +import BitFoundation struct ReadReceipt: Codable { let originalMessageID: String diff --git a/bitchat/Noise/NoiseRateLimiter.swift b/bitchat/Noise/NoiseRateLimiter.swift index cfdd0543..7581244f 100644 --- a/bitchat/Noise/NoiseRateLimiter.swift +++ b/bitchat/Noise/NoiseRateLimiter.swift @@ -7,6 +7,7 @@ // import BitLogger +import BitFoundation import Foundation final class NoiseRateLimiter { diff --git a/bitchat/Noise/NoiseSession.swift b/bitchat/Noise/NoiseSession.swift index 0b96a124..1df2f0ba 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/bitchat/Noise/NoiseSession.swift @@ -9,6 +9,7 @@ import BitLogger import Foundation import CryptoKit +import BitFoundation class NoiseSession { let peerID: PeerID diff --git a/bitchat/Noise/NoiseSessionManager.swift b/bitchat/Noise/NoiseSessionManager.swift index 970e05d8..9029b228 100644 --- a/bitchat/Noise/NoiseSessionManager.swift +++ b/bitchat/Noise/NoiseSessionManager.swift @@ -9,6 +9,7 @@ import BitLogger import CryptoKit import Foundation +import BitFoundation final class NoiseSessionManager { private var sessions: [PeerID: NoiseSession] = [:] diff --git a/bitchat/Nostr/NostrEmbeddedBitChat.swift b/bitchat/Nostr/NostrEmbeddedBitChat.swift index a6f6e0e6..5ee5644c 100644 --- a/bitchat/Nostr/NostrEmbeddedBitChat.swift +++ b/bitchat/Nostr/NostrEmbeddedBitChat.swift @@ -1,4 +1,5 @@ import Foundation +import BitFoundation // MARK: - BitChat-over-Nostr Adapter diff --git a/bitchat/Protocols/BinaryEncodingUtils.swift b/bitchat/Protocols/BinaryEncodingUtils.swift index 476efcc9..8157d6a9 100644 --- a/bitchat/Protocols/BinaryEncodingUtils.swift +++ b/bitchat/Protocols/BinaryEncodingUtils.swift @@ -6,62 +6,7 @@ // import Foundation -import CryptoKit - -// MARK: - Hex Encoding/Decoding - -extension Data { - func hexEncodedString() -> String { - if self.isEmpty { - return "" - } - return self.map { String(format: "%02x", $0) }.joined() - } - - func sha256Hex() -> String { - let digest = SHA256.hash(data: self) - return digest.map { String(format: "%02x", $0) }.joined() - } - - /// Initialize Data from a hex string. - /// - Parameter hexString: A hex string, optionally prefixed with "0x" or "0X". - /// Whitespace is trimmed. Must have even length after prefix removal. - /// - Returns: nil if the string has odd length or contains invalid hex characters. - init?(hexString: String) { - var hex = hexString.trimmed - - // Remove optional 0x prefix - if hex.hasPrefix("0x") || hex.hasPrefix("0X") { - hex = String(hex.dropFirst(2)) - } - - // Reject odd-length strings - guard hex.count % 2 == 0 else { - return nil - } - - // Reject empty strings - guard !hex.isEmpty else { - self = Data() - return - } - - let len = hex.count / 2 - var data = Data(capacity: len) - var index = hex.startIndex - - for _ in 0.. // +import BitFoundation import Foundation import UserNotifications #if os(iOS) diff --git a/bitchat/Services/PrivateChatManager.swift b/bitchat/Services/PrivateChatManager.swift index e7d924db..3e920e12 100644 --- a/bitchat/Services/PrivateChatManager.swift +++ b/bitchat/Services/PrivateChatManager.swift @@ -7,6 +7,7 @@ // import BitLogger +import BitFoundation import Foundation import SwiftUI diff --git a/bitchat/Services/Transport.swift b/bitchat/Services/Transport.swift index 323cfb3a..8f7fd03d 100644 --- a/bitchat/Services/Transport.swift +++ b/bitchat/Services/Transport.swift @@ -1,3 +1,4 @@ +import BitFoundation import Foundation import Combine diff --git a/bitchat/Services/UnifiedPeerService.swift b/bitchat/Services/UnifiedPeerService.swift index 9ca8619e..3d180c68 100644 --- a/bitchat/Services/UnifiedPeerService.swift +++ b/bitchat/Services/UnifiedPeerService.swift @@ -7,6 +7,7 @@ // import BitLogger +import BitFoundation import Foundation import Combine import SwiftUI diff --git a/bitchat/Sync/GossipSyncManager.swift b/bitchat/Sync/GossipSyncManager.swift index b9785b5b..1b65f9f9 100644 --- a/bitchat/Sync/GossipSyncManager.swift +++ b/bitchat/Sync/GossipSyncManager.swift @@ -1,5 +1,6 @@ import Foundation import BitLogger +import BitFoundation // Gossip-based sync manager using on-demand GCS filters final class GossipSyncManager { diff --git a/bitchat/Sync/RequestSyncManager.swift b/bitchat/Sync/RequestSyncManager.swift index 46e6a3da..4c48644c 100644 --- a/bitchat/Sync/RequestSyncManager.swift +++ b/bitchat/Sync/RequestSyncManager.swift @@ -8,6 +8,7 @@ import Foundation import BitLogger +import BitFoundation /// Manages outgoing sync requests and validates incoming responses. /// diff --git a/bitchat/Utils/Data+SHA256.swift b/bitchat/Utils/Data+SHA256.swift deleted file mode 100644 index cd1ae629..00000000 --- a/bitchat/Utils/Data+SHA256.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Data+SHA256.swift -// bitchat -// -// Created by Islam on 26/09/2025. -// - -import struct Foundation.Data -import struct CryptoKit.SHA256 - -extension Data { - /// Returns the hex representation of SHA256 hash - func sha256Fingerprint() -> String { - // Implementation matches existing fingerprint generation in NoiseEncryptionService - sha256Hash().hexEncodedString() - } - - /// Returns the SHA256 hash wrapped in Data - func sha256Hash() -> Data { - Data(SHA256.hash(data: self)) - } -} diff --git a/bitchat/Utils/PeerDisplayNameResolver.swift b/bitchat/Utils/PeerDisplayNameResolver.swift index 95121df7..77a89ec5 100644 --- a/bitchat/Utils/PeerDisplayNameResolver.swift +++ b/bitchat/Utils/PeerDisplayNameResolver.swift @@ -1,4 +1,5 @@ import Foundation +import BitFoundation /// Resolves a stable display name for peers, adding a short suffix when collisions exist. struct PeerDisplayNameResolver { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index c0e4d368..a6f5b23c 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -78,6 +78,7 @@ /// import BitLogger +import BitFoundation import Foundation import SwiftUI import Combine diff --git a/bitchat/ViewModels/Extensions/ChatViewModel+Nostr.swift b/bitchat/ViewModels/Extensions/ChatViewModel+Nostr.swift index c9c50206..b36aa49c 100644 --- a/bitchat/ViewModels/Extensions/ChatViewModel+Nostr.swift +++ b/bitchat/ViewModels/Extensions/ChatViewModel+Nostr.swift @@ -8,6 +8,7 @@ import Foundation import Combine import BitLogger +import BitFoundation import SwiftUI import Tor diff --git a/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift b/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift index f7f69cd7..146c3959 100644 --- a/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift +++ b/bitchat/ViewModels/Extensions/ChatViewModel+PrivateChat.swift @@ -8,6 +8,7 @@ import Foundation import Combine import BitLogger +import BitFoundation import SwiftUI extension ChatViewModel { diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 2c23b7b3..88b2f19b 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -15,6 +15,7 @@ import AppKit #endif import UniformTypeIdentifiers import BitLogger +import BitFoundation /// On macOS 14+, disables the default system focus ring on TextFields. /// On earlier macOS versions and on iOS this is a no-op. diff --git a/bitchat/Views/FingerprintView.swift b/bitchat/Views/FingerprintView.swift index fc099a16..9c764e9e 100644 --- a/bitchat/Views/FingerprintView.swift +++ b/bitchat/Views/FingerprintView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import BitFoundation struct FingerprintView: View { @ObservedObject var viewModel: ChatViewModel diff --git a/bitchat/Views/MeshPeerList.swift b/bitchat/Views/MeshPeerList.swift index 188e4ebd..4c785f5e 100644 --- a/bitchat/Views/MeshPeerList.swift +++ b/bitchat/Views/MeshPeerList.swift @@ -1,4 +1,5 @@ import SwiftUI +import BitFoundation struct MeshPeerList: View { @ObservedObject var viewModel: ChatViewModel diff --git a/bitchat/Views/MessageListView.swift b/bitchat/Views/MessageListView.swift index 6dea685c..d289efc6 100644 --- a/bitchat/Views/MessageListView.swift +++ b/bitchat/Views/MessageListView.swift @@ -5,6 +5,7 @@ // Created by Islam on 30/03/2026. // +import BitFoundation import SwiftUI private struct MessageDisplayItem: Identifiable { diff --git a/bitchatTests/BLEServiceCoreTests.swift b/bitchatTests/BLEServiceCoreTests.swift index e43f35cf..8849f089 100644 --- a/bitchatTests/BLEServiceCoreTests.swift +++ b/bitchatTests/BLEServiceCoreTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation import CoreBluetooth +import BitFoundation @testable import bitchat struct BLEServiceCoreTests { diff --git a/bitchatTests/BLEServiceTests.swift b/bitchatTests/BLEServiceTests.swift index 51e833c6..2a32e838 100644 --- a/bitchatTests/BLEServiceTests.swift +++ b/bitchatTests/BLEServiceTests.swift @@ -8,6 +8,7 @@ import Testing import CoreBluetooth +import BitFoundation @testable import bitchat struct BLEServiceTests { diff --git a/bitchatTests/BitchatPeerTests.swift b/bitchatTests/BitchatPeerTests.swift index 546a7f75..eba4e4c7 100644 --- a/bitchatTests/BitchatPeerTests.swift +++ b/bitchatTests/BitchatPeerTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing +import BitFoundation @testable import bitchat @Suite("BitchatPeer Tests") diff --git a/bitchatTests/ChatViewModelDeliveryStatusTests.swift b/bitchatTests/ChatViewModelDeliveryStatusTests.swift index 30bb17c5..057a97c3 100644 --- a/bitchatTests/ChatViewModelDeliveryStatusTests.swift +++ b/bitchatTests/ChatViewModelDeliveryStatusTests.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat // MARK: - Test Helpers diff --git a/bitchatTests/ChatViewModelExtensionsTests.swift b/bitchatTests/ChatViewModelExtensionsTests.swift index 1f35736e..13934872 100644 --- a/bitchatTests/ChatViewModelExtensionsTests.swift +++ b/bitchatTests/ChatViewModelExtensionsTests.swift @@ -13,6 +13,7 @@ import UIKit #else import AppKit #endif +import BitFoundation @testable import bitchat // MARK: - Test Helpers diff --git a/bitchatTests/ChatViewModelRefactoringTests.swift b/bitchatTests/ChatViewModelRefactoringTests.swift index 22284e5b..fcb3f13f 100644 --- a/bitchatTests/ChatViewModelRefactoringTests.swift +++ b/bitchatTests/ChatViewModelRefactoringTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat struct ChatViewModelRefactoringTests { diff --git a/bitchatTests/ChatViewModelTests.swift b/bitchatTests/ChatViewModelTests.swift index e57234a9..1be3b578 100644 --- a/bitchatTests/ChatViewModelTests.swift +++ b/bitchatTests/ChatViewModelTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat // MARK: - Test Helpers diff --git a/bitchatTests/CommandProcessorTests.swift b/bitchatTests/CommandProcessorTests.swift index f88d1ec2..12e57d27 100644 --- a/bitchatTests/CommandProcessorTests.swift +++ b/bitchatTests/CommandProcessorTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing +import BitFoundation @testable import bitchat @Suite(.serialized) diff --git a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift index ca4ff1f7..6f5c7891 100644 --- a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift @@ -9,6 +9,7 @@ import Testing import CryptoKit import struct Foundation.UUID +import BitFoundation @testable import bitchat struct PrivateChatE2ETests { diff --git a/bitchatTests/EndToEnd/PublicChatE2ETests.swift b/bitchatTests/EndToEnd/PublicChatE2ETests.swift index a8c65af1..d380c4c5 100644 --- a/bitchatTests/EndToEnd/PublicChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PublicChatE2ETests.swift @@ -8,6 +8,7 @@ import Testing import struct Foundation.UUID +import BitFoundation @testable import bitchat struct PublicChatE2ETests { diff --git a/bitchatTests/Fragmentation/FragmentationTests.swift b/bitchatTests/Fragmentation/FragmentationTests.swift index 4e724206..c645b1f9 100644 --- a/bitchatTests/Fragmentation/FragmentationTests.swift +++ b/bitchatTests/Fragmentation/FragmentationTests.swift @@ -9,6 +9,7 @@ import Testing import Foundation import CoreBluetooth +import BitFoundation @testable import bitchat struct FragmentationTests { diff --git a/bitchatTests/GossipSyncManagerTests.swift b/bitchatTests/GossipSyncManagerTests.swift index 7f1cc7d3..9d45da98 100644 --- a/bitchatTests/GossipSyncManagerTests.swift +++ b/bitchatTests/GossipSyncManagerTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing +import BitFoundation @testable import bitchat struct GossipSyncManagerTests { diff --git a/bitchatTests/Integration/IntegrationTests.swift b/bitchatTests/Integration/IntegrationTests.swift index 94ba92e9..1907cf33 100644 --- a/bitchatTests/Integration/IntegrationTests.swift +++ b/bitchatTests/Integration/IntegrationTests.swift @@ -9,6 +9,7 @@ import Foundation import CryptoKit import Testing +import BitFoundation @testable import bitchat struct IntegrationTests { diff --git a/bitchatTests/Integration/TestNetworkHelper.swift b/bitchatTests/Integration/TestNetworkHelper.swift index eb3eeb62..3b633e54 100644 --- a/bitchatTests/Integration/TestNetworkHelper.swift +++ b/bitchatTests/Integration/TestNetworkHelper.swift @@ -8,6 +8,7 @@ import Foundation import CryptoKit +import BitFoundation @testable import bitchat final class TestNetworkHelper { diff --git a/bitchatTests/MessageFormattingEngineTests.swift b/bitchatTests/MessageFormattingEngineTests.swift index 2c1f8e1e..602ad38d 100644 --- a/bitchatTests/MessageFormattingEngineTests.swift +++ b/bitchatTests/MessageFormattingEngineTests.swift @@ -9,6 +9,7 @@ import Testing import Foundation import SwiftUI +import BitFoundation @testable import bitchat struct MessageFormattingEngineTests { diff --git a/bitchatTests/Mocks/MockBLEBus.swift b/bitchatTests/Mocks/MockBLEBus.swift index 8199ec1a..c4b5cb24 100644 --- a/bitchatTests/Mocks/MockBLEBus.swift +++ b/bitchatTests/Mocks/MockBLEBus.swift @@ -7,6 +7,7 @@ // import Foundation +import BitFoundation @testable import bitchat final class MockBLEBus { diff --git a/bitchatTests/Mocks/MockBLEService.swift b/bitchatTests/Mocks/MockBLEService.swift index 8398e8cc..352efa90 100644 --- a/bitchatTests/Mocks/MockBLEService.swift +++ b/bitchatTests/Mocks/MockBLEService.swift @@ -8,6 +8,7 @@ import Foundation import CoreBluetooth +import BitFoundation @testable import bitchat /// In-memory BLE test harness used by E2E/Integration tests. diff --git a/bitchatTests/Mocks/MockIdentityManager.swift b/bitchatTests/Mocks/MockIdentityManager.swift index 7d6cb8df..633c388b 100644 --- a/bitchatTests/Mocks/MockIdentityManager.swift +++ b/bitchatTests/Mocks/MockIdentityManager.swift @@ -7,6 +7,7 @@ // import Foundation +import BitFoundation @testable import bitchat final class MockIdentityManager: SecureIdentityStateManagerProtocol { diff --git a/bitchatTests/Mocks/MockTransport.swift b/bitchatTests/Mocks/MockTransport.swift index 609467b3..2f077ed2 100644 --- a/bitchatTests/Mocks/MockTransport.swift +++ b/bitchatTests/Mocks/MockTransport.swift @@ -9,6 +9,7 @@ import Foundation import Combine import CoreBluetooth +import BitFoundation @testable import bitchat /// Mock Transport implementation for testing ChatViewModel in isolation. diff --git a/bitchatTests/Noise/NoiseCoverageTests.swift b/bitchatTests/Noise/NoiseCoverageTests.swift index b16b5011..601b7061 100644 --- a/bitchatTests/Noise/NoiseCoverageTests.swift +++ b/bitchatTests/Noise/NoiseCoverageTests.swift @@ -1,6 +1,7 @@ import CryptoKit import Foundation import Testing +import BitFoundation @testable import bitchat diff --git a/bitchatTests/Noise/NoiseProtocolTests.swift b/bitchatTests/Noise/NoiseProtocolTests.swift index 1986445f..6ba8dbfa 100644 --- a/bitchatTests/Noise/NoiseProtocolTests.swift +++ b/bitchatTests/Noise/NoiseProtocolTests.swift @@ -9,7 +9,7 @@ import CryptoKit import Foundation import Testing - +import BitFoundation @testable import bitchat // MARK: - Test Vector Support diff --git a/bitchatTests/Noise/NoiseRateLimiterTests.swift b/bitchatTests/Noise/NoiseRateLimiterTests.swift index 0e7f1a6a..623fe22e 100644 --- a/bitchatTests/Noise/NoiseRateLimiterTests.swift +++ b/bitchatTests/Noise/NoiseRateLimiterTests.swift @@ -1,4 +1,5 @@ import XCTest +import BitFoundation @testable import bitchat final class NoiseRateLimiterTests: XCTestCase { diff --git a/bitchatTests/NostrProtocolTests.swift b/bitchatTests/NostrProtocolTests.swift index 91f9217c..4ea8020f 100644 --- a/bitchatTests/NostrProtocolTests.swift +++ b/bitchatTests/NostrProtocolTests.swift @@ -8,6 +8,7 @@ import Testing import CryptoKit import Foundation +import BitFoundation @testable import bitchat struct NostrProtocolTests { diff --git a/bitchatTests/Protocol/BinaryProtocolTests.swift b/bitchatTests/Protocol/BinaryProtocolTests.swift index 2758bcd2..61a5c906 100644 --- a/bitchatTests/Protocol/BinaryProtocolTests.swift +++ b/bitchatTests/Protocol/BinaryProtocolTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat struct BinaryProtocolTests { diff --git a/bitchatTests/ProtocolContractTests.swift b/bitchatTests/ProtocolContractTests.swift index 4d38b196..df8a4b46 100644 --- a/bitchatTests/ProtocolContractTests.swift +++ b/bitchatTests/ProtocolContractTests.swift @@ -2,6 +2,7 @@ import Testing import Foundation import Combine import CoreBluetooth +import BitFoundation @testable import bitchat private final class DefaultDelegateProbe: BitchatDelegate { diff --git a/bitchatTests/ReadReceiptTests.swift b/bitchatTests/ReadReceiptTests.swift index e6ec19bc..56ab9b2c 100644 --- a/bitchatTests/ReadReceiptTests.swift +++ b/bitchatTests/ReadReceiptTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing +import BitFoundation @testable import bitchat @Suite("ReadReceipt Tests") diff --git a/bitchatTests/Services/FavoritesPersistenceServiceTests.swift b/bitchatTests/Services/FavoritesPersistenceServiceTests.swift index 8338d9bd..729fda15 100644 --- a/bitchatTests/Services/FavoritesPersistenceServiceTests.swift +++ b/bitchatTests/Services/FavoritesPersistenceServiceTests.swift @@ -1,4 +1,5 @@ import XCTest +import BitFoundation @testable import bitchat @MainActor diff --git a/bitchatTests/Services/MessageRouterTests.swift b/bitchatTests/Services/MessageRouterTests.swift index 837157ef..89289320 100644 --- a/bitchatTests/Services/MessageRouterTests.swift +++ b/bitchatTests/Services/MessageRouterTests.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat struct MessageRouterTests { diff --git a/bitchatTests/Services/NoiseEncryptionServiceTests.swift b/bitchatTests/Services/NoiseEncryptionServiceTests.swift index 29f2d9d0..6642f97d 100644 --- a/bitchatTests/Services/NoiseEncryptionServiceTests.swift +++ b/bitchatTests/Services/NoiseEncryptionServiceTests.swift @@ -1,5 +1,6 @@ import Foundation import Testing +import BitFoundation @testable import bitchat @Suite("NoiseEncryptionService Tests") diff --git a/bitchatTests/Services/NostrTransportTests.swift b/bitchatTests/Services/NostrTransportTests.swift index e88be16c..cb10d7fc 100644 --- a/bitchatTests/Services/NostrTransportTests.swift +++ b/bitchatTests/Services/NostrTransportTests.swift @@ -8,6 +8,7 @@ import Foundation import Testing +import BitFoundation @testable import bitchat @Suite("NostrTransport Tests") diff --git a/bitchatTests/Services/NotificationServiceTests.swift b/bitchatTests/Services/NotificationServiceTests.swift index 7a60715c..9ffad1f4 100644 --- a/bitchatTests/Services/NotificationServiceTests.swift +++ b/bitchatTests/Services/NotificationServiceTests.swift @@ -1,5 +1,6 @@ import XCTest import UserNotifications +import BitFoundation @testable import bitchat final class NotificationServiceTests: XCTestCase { diff --git a/bitchatTests/Services/PrivateChatManagerTests.swift b/bitchatTests/Services/PrivateChatManagerTests.swift index 4a7b8c1d..a75dac5f 100644 --- a/bitchatTests/Services/PrivateChatManagerTests.swift +++ b/bitchatTests/Services/PrivateChatManagerTests.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat struct PrivateChatManagerTests { diff --git a/bitchatTests/Services/SecureIdentityStateManagerTests.swift b/bitchatTests/Services/SecureIdentityStateManagerTests.swift index 62507186..6f40558f 100644 --- a/bitchatTests/Services/SecureIdentityStateManagerTests.swift +++ b/bitchatTests/Services/SecureIdentityStateManagerTests.swift @@ -1,5 +1,6 @@ import Foundation import XCTest +import BitFoundation @testable import bitchat final class SecureIdentityStateManagerTests: XCTestCase { diff --git a/bitchatTests/Services/UnifiedPeerServiceTests.swift b/bitchatTests/Services/UnifiedPeerServiceTests.swift index 192fa1b4..c910af9b 100644 --- a/bitchatTests/Services/UnifiedPeerServiceTests.swift +++ b/bitchatTests/Services/UnifiedPeerServiceTests.swift @@ -7,6 +7,7 @@ import Testing import Foundation +import BitFoundation @testable import bitchat struct UnifiedPeerServiceTests { diff --git a/bitchatTests/Sync/RequestSyncManagerTests.swift b/bitchatTests/Sync/RequestSyncManagerTests.swift index 572cccf7..aada4bfc 100644 --- a/bitchatTests/Sync/RequestSyncManagerTests.swift +++ b/bitchatTests/Sync/RequestSyncManagerTests.swift @@ -1,4 +1,5 @@ import XCTest +import BitFoundation @testable import bitchat final class RequestSyncManagerTests: XCTestCase { diff --git a/bitchatTests/TestUtilities/TestHelpers.swift b/bitchatTests/TestUtilities/TestHelpers.swift index 57200415..c76f57af 100644 --- a/bitchatTests/TestUtilities/TestHelpers.swift +++ b/bitchatTests/TestUtilities/TestHelpers.swift @@ -8,6 +8,7 @@ import Foundation import CryptoKit +import BitFoundation @testable import bitchat final class TestHelpers { diff --git a/bitchatTests/ViewSmokeTests.swift b/bitchatTests/ViewSmokeTests.swift index 301d8dab..cdf9a8df 100644 --- a/bitchatTests/ViewSmokeTests.swift +++ b/bitchatTests/ViewSmokeTests.swift @@ -8,6 +8,7 @@ import UIKit #else import AppKit #endif +import BitFoundation @testable import bitchat @MainActor diff --git a/localPackages/BitFoundation/Package.swift b/localPackages/BitFoundation/Package.swift new file mode 100644 index 00000000..927ce283 --- /dev/null +++ b/localPackages/BitFoundation/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "BitFoundation", + platforms: [ + .iOS(.v16), + .macOS(.v13) + ], + products: [ + .library( + name: "BitFoundation", + targets: ["BitFoundation"] + ) + ], + targets: [ + .target( + name: "BitFoundation", + path: "Sources" + ), + .testTarget( + name: "BitFoundationTests", + dependencies: ["BitFoundation"], + ) + ] +) diff --git a/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift b/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift new file mode 100644 index 00000000..e847b2e2 --- /dev/null +++ b/localPackages/BitFoundation/Sources/BitFoundation/Data+Hex.swift @@ -0,0 +1,57 @@ +// +// Data+Hex.swift +// BitFoundation +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import struct Foundation.Data + +public extension Data { + func hexEncodedString() -> String { + if self.isEmpty { + return "" + } + return self.map { String(format: "%02x", $0) }.joined() + } + + /// Initialize Data from a hex string. + /// - Parameter hexString: A hex string, optionally prefixed with "0x" or "0X". + /// Whitespace is trimmed. Must have even length after prefix removal. + /// - Returns: nil if the string has odd length or contains invalid hex characters. + init?(hexString: String) { + var hex = hexString.trimmed + + // Remove optional 0x prefix + if hex.hasPrefix("0x") || hex.hasPrefix("0X") { + hex = String(hex.dropFirst(2)) + } + + // Reject odd-length strings + guard hex.count % 2 == 0 else { + return nil + } + + // Reject empty strings + guard !hex.isEmpty else { + self = Data() + return + } + + let len = hex.count / 2 + var data = Data(capacity: len) + var index = hex.startIndex + + for _ in 0.. +// + +import struct Foundation.Data +private import struct CryptoKit.SHA256 + +public extension Data { + /// Returns the hex representation of SHA256 hash + func sha256Fingerprint() -> String { + // Implementation matches existing fingerprint generation in NoiseEncryptionService + sha256Hash().hexEncodedString() + } + + /// Returns the SHA256 hash wrapped in Data + func sha256Hash() -> Data { + Data(sha256Digest) + } + + func sha256Hex() -> String { + sha256Digest.map { String(format: "%02x", $0) }.joined() + } +} + +private extension Data { + var sha256Digest: SHA256.Digest { + SHA256.hash(data: self) + } +} diff --git a/bitchat/Models/PeerID.swift b/localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift similarity index 82% rename from bitchat/Models/PeerID.swift rename to localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift index 837f56aa..0b2721c2 100644 --- a/bitchat/Models/PeerID.swift +++ b/localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift @@ -1,15 +1,27 @@ // // PeerID.swift -// bitchat +// BitFoundation // // This is free and unencumbered software released into the public domain. // For more information, see // -import Foundation +import struct Foundation.Data +import struct Foundation.CharacterSet -struct PeerID: Equatable, Hashable { - enum Prefix: String, CaseIterable { +public struct PeerID: Equatable, Hashable, Sendable { + enum Constants { + /// 16 + static let nostrConvKeyPrefixLength = 16 + /// 8 + static let nostrShortKeyDisplayLength = 8 + /// 64 + fileprivate static let maxIDLength = 64 + /// 16 + fileprivate static let hexIDLength = 16 // 8 bytes = 16 hex chars + } + + public enum Prefix: String, CaseIterable, Sendable { /// When no prefix is provided case empty = "" /// `"mesh:"` @@ -23,15 +35,15 @@ struct PeerID: Equatable, Hashable { /// `"nostr:"` (+ 8 characters hex) case geoChat = "nostr:" } - - let prefix: Prefix - + + public let prefix: Prefix + /// Returns the actual value without any prefix - let bare: String - + public let bare: String + /// Returns the full `id` value by combining `(prefix + bare)` - var id: String { prefix.rawValue + bare } - + public var id: String { prefix.rawValue + bare } + // Private so the callers have to go through a convenience init private init(prefix: Prefix, bare: any StringProtocol) { self.prefix = prefix @@ -41,17 +53,17 @@ struct PeerID: Equatable, Hashable { // MARK: - Convenience Inits -extension PeerID { +public extension PeerID { /// Convenience init to create GeoDM PeerID by appending `"nostr_"` to the first 16 characters of `pubKey` init(nostr_ pubKey: String) { - self.init(prefix: .geoDM, bare: pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength)) + self.init(prefix: .geoDM, bare: pubKey.prefix(Constants.nostrConvKeyPrefixLength)) } - + /// Convenience init to create GeoChat PeerID by appending `"nostr:"` to the first 8 characters of `pubKey` init(nostr pubKey: String) { - self.init(prefix: .geoChat, bare: pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength)) + self.init(prefix: .geoChat, bare: pubKey.prefix(Constants.nostrShortKeyDisplayLength)) } - + /// Convenience init to create PeerID from String/Substring by splitting it into prefix and bare parts init(str: any StringProtocol) { if let prefix = Prefix.allCases.first(where: { $0 != .empty && str.hasPrefix($0.rawValue) }) { @@ -60,23 +72,23 @@ extension PeerID { self.init(prefix: .empty, bare: str) } } - + /// Convenience init to handle `Optional` init?(str: (any StringProtocol)?) { guard let str else { return nil } self.init(str: str) } - + /// Convenience init to create PeerID by converting Data to String init?(data: Data) { self.init(str: String(data: data, encoding: .utf8)) } - + /// Convenience init to "hide" hex-encoding implementation detail init(hexData: Data) { self.init(str: hexData.hexEncodedString()) } - + /// Convenience init to "hide" hex-encoding implementation detail init?(hexData: Data?) { guard let hexData else { return nil } @@ -86,12 +98,12 @@ extension PeerID { // MARK: - Noise Public Key Helpers -extension PeerID { +public extension PeerID { /// Derive the stable 16-hex peer ID from a Noise static public key init(publicKey: Data) { self.init(str: publicKey.sha256Fingerprint().prefix(16)) } - + /// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed func toShort() -> PeerID { if let noiseKey { @@ -104,11 +116,11 @@ extension PeerID { // MARK: - Codable extension PeerID: Codable { - init(from decoder: any Decoder) throws { + public init(from decoder: any Decoder) throws { self.init(str: try decoder.singleValueContainer().decode(String.self)) } - - func encode(to encoder: any Encoder) throws { + + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } @@ -116,27 +128,27 @@ extension PeerID: Codable { // MARK: - Helpers -extension PeerID { +public extension PeerID { var isEmpty: Bool { id.isEmpty } - + /// Returns true if `id` starts with "`nostr:`" var isGeoChat: Bool { prefix == .geoChat } - + /// Returns true if `id` starts with "`nostr_`" var isGeoDM: Bool { prefix == .geoDM } - + func toPercentEncoded() -> String { id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id } } -extension PeerID { +public extension PeerID { var routingData: Data? { if let direct = Data(hexString: id), direct.count == 8 { return direct } if let bareData = Data(hexString: bare), bareData.count == 8 { return bareData } @@ -152,50 +164,45 @@ extension PeerID { // MARK: - Validation -extension PeerID { - private enum Constants { - static let maxIDLength = 64 - static let hexIDLength = 16 // 8 bytes = 16 hex chars - } - +public extension PeerID { /// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64) var isValid: Bool { if prefix != .empty { return PeerID(str: bare).isValid } - + // Accept short routing IDs (exact 16-hex) or Full Noise key hex (exact 64-hex) if isShort || isNoiseKeyHex { return true } - + // If length equals short or full but isn't valid hex, reject if id.count == Constants.hexIDLength || id.count == Constants.maxIDLength { return false } - + // Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64) let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_")) return !id.isEmpty && id.count < Constants.maxIDLength && id.rangeOfCharacter(from: validCharset.inverted) == nil } - + /// Returns true if the `bare` id is all hex var isHex: Bool { bare.allSatisfy { $0.isHexDigit } } - + /// Short routing IDs (exact 16-hex) var isShort: Bool { bare.count == Constants.hexIDLength && isHex } - + /// Full Noise key hex (exact 64-hex) var isNoiseKeyHex: Bool { noiseKey != nil } - + /// Full Noise key (exact 64-hex) as Data var noiseKey: Data? { guard bare.count == Constants.maxIDLength else { return nil } @@ -206,7 +213,7 @@ extension PeerID { // MARK: - Comparable extension PeerID: Comparable { - static func < (lhs: PeerID, rhs: PeerID) -> Bool { + public static func < (lhs: PeerID, rhs: PeerID) -> Bool { lhs.id < rhs.id } } @@ -215,7 +222,7 @@ extension PeerID: Comparable { extension PeerID: CustomStringConvertible { /// So it returns the actual `id` like before even inside another String - var description: String { + public var description: String { id } } diff --git a/bitchat/Utils/String+Ext.swift b/localPackages/BitFoundation/Sources/BitFoundation/String+Ext.swift similarity index 89% rename from bitchat/Utils/String+Ext.swift rename to localPackages/BitFoundation/Sources/BitFoundation/String+Ext.swift index 49e12077..30f68f3d 100644 --- a/bitchat/Utils/String+Ext.swift +++ b/localPackages/BitFoundation/Sources/BitFoundation/String+Ext.swift @@ -1,12 +1,12 @@ // // String+Ext.swift -// bitchat +// BitFoundation // // This is free and unencumbered software released into the public domain. // For more information, see // -extension StringProtocol { +public extension StringProtocol { var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } diff --git a/bitchatTests/Utils/PeerIDTests.swift b/localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift similarity index 97% rename from bitchatTests/Utils/PeerIDTests.swift rename to localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift index e979ab27..def8e89a 100644 --- a/bitchatTests/Utils/PeerIDTests.swift +++ b/localPackages/BitFoundation/Tests/BitFoundationTests/PeerIDTests.swift @@ -8,7 +8,7 @@ import Testing import Foundation -@testable import bitchat +@testable import BitFoundation struct PeerIDTests { private let hex16 = "0011223344556677" @@ -168,8 +168,8 @@ struct PeerIDTests { @Test func nostrUnderscore_pubKey() { let pubKey = hex64 let peerID = PeerID(nostr_: pubKey) - #expect(peerID.id == "nostr_\(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))") - #expect(peerID.bare == String(pubKey.prefix(TransportConfig.nostrConvKeyPrefixLength))) + #expect(peerID.id == "nostr_\(pubKey.prefix(PeerID.Constants.nostrConvKeyPrefixLength))") + #expect(peerID.bare == String(pubKey.prefix(PeerID.Constants.nostrConvKeyPrefixLength))) #expect(peerID.prefix == .geoDM) } @@ -178,8 +178,8 @@ struct PeerIDTests { @Test func nostr_pubKey() { let pubKey = hex64 let peerID = PeerID(nostr: pubKey) - #expect(peerID.id == "nostr:\(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))") - #expect(peerID.bare == String(pubKey.prefix(TransportConfig.nostrShortKeyDisplayLength))) + #expect(peerID.id == "nostr:\(pubKey.prefix(PeerID.Constants.nostrShortKeyDisplayLength))") + #expect(peerID.bare == String(pubKey.prefix(PeerID.Constants.nostrShortKeyDisplayLength))) #expect(peerID.prefix == .geoChat) }