BitFoundation module to centralize shared components (#1089)

* Run local packages’ tests as well on CI

* BitFoundation module to centralize shared components
This commit is contained in:
Islam
2026-04-14 20:10:03 +01:00
committed by GitHub
parent 3e137d784c
commit 4cfcefcda6
81 changed files with 290 additions and 141 deletions
+17 -9
View File
@@ -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 }}
+6 -1
View File
@@ -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",
+20
View File
@@ -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;
+1
View File
@@ -8,6 +8,7 @@
import Tor
import SwiftUI
import BitFoundation
import UserNotifications
@main
+1
View File
@@ -81,6 +81,7 @@
///
import Foundation
import BitFoundation
// MARK: - Three-Layer Identity Model
@@ -91,6 +91,7 @@
///
import BitLogger
import BitFoundation
import Foundation
import CryptoKit
+1
View File
@@ -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,
+1
View File
@@ -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,
+1
View File
@@ -1,5 +1,6 @@
import Foundation
import CoreBluetooth
import BitFoundation
/// Represents a peer in the BitChat network with all associated metadata
struct BitchatPeer: Equatable {
+1
View File
@@ -7,6 +7,7 @@
//
import Foundation
import BitFoundation
struct ReadReceipt: Codable {
let originalMessageID: String
+1
View File
@@ -7,6 +7,7 @@
//
import BitLogger
import BitFoundation
import Foundation
final class NoiseRateLimiter {
+1
View File
@@ -9,6 +9,7 @@
import BitLogger
import Foundation
import CryptoKit
import BitFoundation
class NoiseSession {
let peerID: PeerID
+1
View File
@@ -9,6 +9,7 @@
import BitLogger
import CryptoKit
import Foundation
import BitFoundation
final class NoiseSessionManager {
private var sessions: [PeerID: NoiseSession] = [:]
+1
View File
@@ -1,4 +1,5 @@
import Foundation
import BitFoundation
// MARK: - BitChat-over-Nostr Adapter
+1 -56
View File
@@ -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..<len {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
return nil
}
data.append(byte)
index = nextIndex
}
self = data
}
}
import BitFoundation
// MARK: - Binary Encoding Utilities
+1
View File
@@ -60,6 +60,7 @@
import Foundation
import CoreBluetooth
import BitFoundation
// MARK: - Message Types
+1
View File
@@ -1,4 +1,5 @@
import BitLogger
import BitFoundation
import Foundation
import CoreBluetooth
import Combine
+1
View File
@@ -7,6 +7,7 @@
//
import Foundation
import BitFoundation
/// Result of command processing
enum CommandResult {
@@ -1,4 +1,5 @@
import BitLogger
import BitFoundation
import Foundation
import Combine
@@ -6,6 +6,7 @@
// This is free and unencumbered software released into the public domain.
//
import BitFoundation
import Foundation
import SwiftUI
+1
View File
@@ -1,4 +1,5 @@
import BitLogger
import BitFoundation
import Foundation
/// Routes messages using available transports (Mesh, Nostr, etc.)
@@ -83,6 +83,7 @@
///
import BitLogger
import BitFoundation
import Foundation
import CryptoKit
+1
View File
@@ -1,4 +1,5 @@
import BitLogger
import BitFoundation
import Foundation
import Combine
@@ -6,6 +6,7 @@
// For more information, see <https://unlicense.org>
//
import BitFoundation
import Foundation
import UserNotifications
#if os(iOS)
@@ -7,6 +7,7 @@
//
import BitLogger
import BitFoundation
import Foundation
import SwiftUI
+1
View File
@@ -1,3 +1,4 @@
import BitFoundation
import Foundation
import Combine
@@ -7,6 +7,7 @@
//
import BitLogger
import BitFoundation
import Foundation
import Combine
import SwiftUI
+1
View File
@@ -1,5 +1,6 @@
import Foundation
import BitLogger
import BitFoundation
// Gossip-based sync manager using on-demand GCS filters
final class GossipSyncManager {
+1
View File
@@ -8,6 +8,7 @@
import Foundation
import BitLogger
import BitFoundation
/// Manages outgoing sync requests and validates incoming responses.
///
-22
View File
@@ -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))
}
}
@@ -1,4 +1,5 @@
import Foundation
import BitFoundation
/// Resolves a stable display name for peers, adding a short suffix when collisions exist.
struct PeerDisplayNameResolver {
+1
View File
@@ -78,6 +78,7 @@
///
import BitLogger
import BitFoundation
import Foundation
import SwiftUI
import Combine
@@ -8,6 +8,7 @@
import Foundation
import Combine
import BitLogger
import BitFoundation
import SwiftUI
import Tor
@@ -8,6 +8,7 @@
import Foundation
import Combine
import BitLogger
import BitFoundation
import SwiftUI
extension ChatViewModel {
+1
View File
@@ -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.
+1
View File
@@ -7,6 +7,7 @@
//
import SwiftUI
import BitFoundation
struct FingerprintView: View {
@ObservedObject var viewModel: ChatViewModel
+1
View File
@@ -1,4 +1,5 @@
import SwiftUI
import BitFoundation
struct MeshPeerList: View {
@ObservedObject var viewModel: ChatViewModel
+1
View File
@@ -5,6 +5,7 @@
// Created by Islam on 30/03/2026.
//
import BitFoundation
import SwiftUI
private struct MessageDisplayItem: Identifiable {
+1
View File
@@ -8,6 +8,7 @@
import Testing
import Foundation
import CoreBluetooth
import BitFoundation
@testable import bitchat
struct BLEServiceCoreTests {
+1
View File
@@ -8,6 +8,7 @@
import Testing
import CoreBluetooth
import BitFoundation
@testable import bitchat
struct BLEServiceTests {
+1
View File
@@ -1,5 +1,6 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
@Suite("BitchatPeer Tests")
@@ -7,6 +7,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
// MARK: - Test Helpers
@@ -13,6 +13,7 @@ import UIKit
#else
import AppKit
#endif
import BitFoundation
@testable import bitchat
// MARK: - Test Helpers
@@ -8,6 +8,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct ChatViewModelRefactoringTests {
+1
View File
@@ -8,6 +8,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
// MARK: - Test Helpers
+1
View File
@@ -1,5 +1,6 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
@Suite(.serialized)
@@ -9,6 +9,7 @@
import Testing
import CryptoKit
import struct Foundation.UUID
import BitFoundation
@testable import bitchat
struct PrivateChatE2ETests {
@@ -8,6 +8,7 @@
import Testing
import struct Foundation.UUID
import BitFoundation
@testable import bitchat
struct PublicChatE2ETests {
@@ -9,6 +9,7 @@
import Testing
import Foundation
import CoreBluetooth
import BitFoundation
@testable import bitchat
struct FragmentationTests {
@@ -1,5 +1,6 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
struct GossipSyncManagerTests {
@@ -9,6 +9,7 @@
import Foundation
import CryptoKit
import Testing
import BitFoundation
@testable import bitchat
struct IntegrationTests {
@@ -8,6 +8,7 @@
import Foundation
import CryptoKit
import BitFoundation
@testable import bitchat
final class TestNetworkHelper {
@@ -9,6 +9,7 @@
import Testing
import Foundation
import SwiftUI
import BitFoundation
@testable import bitchat
struct MessageFormattingEngineTests {
+1
View File
@@ -7,6 +7,7 @@
//
import Foundation
import BitFoundation
@testable import bitchat
final class MockBLEBus {
+1
View File
@@ -8,6 +8,7 @@
import Foundation
import CoreBluetooth
import BitFoundation
@testable import bitchat
/// In-memory BLE test harness used by E2E/Integration tests.
@@ -7,6 +7,7 @@
//
import Foundation
import BitFoundation
@testable import bitchat
final class MockIdentityManager: SecureIdentityStateManagerProtocol {
+1
View File
@@ -9,6 +9,7 @@
import Foundation
import Combine
import CoreBluetooth
import BitFoundation
@testable import bitchat
/// Mock Transport implementation for testing ChatViewModel in isolation.
@@ -1,6 +1,7 @@
import CryptoKit
import Foundation
import Testing
import BitFoundation
@testable import bitchat
+1 -1
View File
@@ -9,7 +9,7 @@
import CryptoKit
import Foundation
import Testing
import BitFoundation
@testable import bitchat
// MARK: - Test Vector Support
@@ -1,4 +1,5 @@
import XCTest
import BitFoundation
@testable import bitchat
final class NoiseRateLimiterTests: XCTestCase {
+1
View File
@@ -8,6 +8,7 @@
import Testing
import CryptoKit
import Foundation
import BitFoundation
@testable import bitchat
struct NostrProtocolTests {
@@ -8,6 +8,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct BinaryProtocolTests {
+1
View File
@@ -2,6 +2,7 @@ import Testing
import Foundation
import Combine
import CoreBluetooth
import BitFoundation
@testable import bitchat
private final class DefaultDelegateProbe: BitchatDelegate {
+1
View File
@@ -1,5 +1,6 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
@Suite("ReadReceipt Tests")
@@ -1,4 +1,5 @@
import XCTest
import BitFoundation
@testable import bitchat
@MainActor
@@ -7,6 +7,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct MessageRouterTests {
@@ -1,5 +1,6 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
@Suite("NoiseEncryptionService Tests")
@@ -8,6 +8,7 @@
import Foundation
import Testing
import BitFoundation
@testable import bitchat
@Suite("NostrTransport Tests")
@@ -1,5 +1,6 @@
import XCTest
import UserNotifications
import BitFoundation
@testable import bitchat
final class NotificationServiceTests: XCTestCase {
@@ -7,6 +7,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct PrivateChatManagerTests {
@@ -1,5 +1,6 @@
import Foundation
import XCTest
import BitFoundation
@testable import bitchat
final class SecureIdentityStateManagerTests: XCTestCase {
@@ -7,6 +7,7 @@
import Testing
import Foundation
import BitFoundation
@testable import bitchat
struct UnifiedPeerServiceTests {
@@ -1,4 +1,5 @@
import XCTest
import BitFoundation
@testable import bitchat
final class RequestSyncManagerTests: XCTestCase {
@@ -8,6 +8,7 @@
import Foundation
import CryptoKit
import BitFoundation
@testable import bitchat
final class TestHelpers {
+1
View File
@@ -8,6 +8,7 @@ import UIKit
#else
import AppKit
#endif
import BitFoundation
@testable import bitchat
@MainActor
+27
View File
@@ -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"],
)
]
)
@@ -0,0 +1,57 @@
//
// Data+Hex.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
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..<len {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
return nil
}
data.append(byte)
index = nextIndex
}
self = data
}
}
@@ -0,0 +1,33 @@
//
// Data+SHA256.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
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)
}
}
@@ -1,15 +1,27 @@
//
// PeerID.swift
// bitchat
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
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<String>`
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
}
}
@@ -1,12 +1,12 @@
//
// String+Ext.swift
// bitchat
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
extension StringProtocol {
public extension StringProtocol {
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
@@ -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)
}