Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ var package = Package(
name: "BluetoothHCI",
type: libraryType,
targets: ["BluetoothHCI"]
),
.library(
name: "BluetoothSDP",
type: libraryType,
targets: ["BluetoothSDP"]
)
],
targets: [
Expand Down Expand Up @@ -83,6 +88,12 @@ var package = Package(
"BluetoothGAP"
]
),
.target(
name: "BluetoothSDP",
dependencies: [
"Bluetooth"
]
),
.testTarget(
name: "BluetoothTests",
dependencies: [
Expand All @@ -102,6 +113,10 @@ var package = Package(
.target(
name: "BluetoothHCI",
condition: .when(platforms: [.macOS, .linux, .macCatalyst, .windows, .android])
),
.target(
name: "BluetoothSDP",
condition: .when(platforms: [.macOS, .linux, .macCatalyst, .windows, .android])
)
]
)
Expand All @@ -126,7 +141,7 @@ if embeddedFoundation {
name: "FoundationEmbedded",
package: "swift-embedded-foundation"
)
for name in ["Bluetooth", "BluetoothGAP", "BluetoothGATT", "BluetoothHCI"] {
for name in ["Bluetooth", "BluetoothGAP", "BluetoothGATT", "BluetoothHCI", "BluetoothSDP"] {
guard let index = package.targets.firstIndex(where: { $0.name == name }) else {
fatalError("Missing target \(name)")
}
Expand Down
100 changes: 100 additions & 0 deletions Sources/BluetoothSDP/SDPAttributeID.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// SDPAttributeID.swift
// Bluetooth
//
// The universal attribute IDs every SDP service record shares (SDP
// Protocol, Bluetooth Core Spec Vol 3, Part B, §5.1), plus the
// per-record language-base offset scheme the string attributes
// (name/description/provider) are defined relative to.
//

/// An SDP service record attribute ID.
public struct SDPAttributeID: RawRepresentable, Hashable, Sendable, Codable {

public let rawValue: UInt16

public init(rawValue: UInt16) {
self.rawValue = rawValue
}
}

public extension SDPAttributeID {

/// A 32-bit number that uniquely identifies each service record.
static var serviceRecordHandle: SDPAttributeID { SDPAttributeID(rawValue: 0x0000) }

/// A list of service class UUIDs, most specific first.
static var serviceClassIDList: SDPAttributeID { SDPAttributeID(rawValue: 0x0001) }

static var serviceRecordState: SDPAttributeID { SDPAttributeID(rawValue: 0x0002) }

/// A UUID identifying this particular service instance.
static var serviceID: SDPAttributeID { SDPAttributeID(rawValue: 0x0003) }

/// One entry per protocol in the protocol stack used to gain
/// access to the service, outermost protocol first — the entry
/// after the outermost typically carries the L2CAP PSM or RFCOMM
/// channel number the service listens on.
static var protocolDescriptorList: SDPAttributeID { SDPAttributeID(rawValue: 0x0004) }

static var browseGroupList: SDPAttributeID { SDPAttributeID(rawValue: 0x0005) }

/// A sequence of (language, encoding, attribute ID base) triples;
/// the base offset into which the string attributes below are
/// added to get the actual attribute ID used for a given language.
static var languageBaseAttributeIDList: SDPAttributeID { SDPAttributeID(rawValue: 0x0006) }

static var serviceInfoTimeToLive: SDPAttributeID { SDPAttributeID(rawValue: 0x0007) }

static var serviceAvailability: SDPAttributeID { SDPAttributeID(rawValue: 0x0008) }

/// One entry per Bluetooth profile the service conforms to: a
/// (profile UUID, version) pair.
static var bluetoothProfileDescriptorList: SDPAttributeID { SDPAttributeID(rawValue: 0x0009) }

static var documentationURL: SDPAttributeID { SDPAttributeID(rawValue: 0x000A) }

static var clientExecutableURL: SDPAttributeID { SDPAttributeID(rawValue: 0x000B) }

static var iconURL: SDPAttributeID { SDPAttributeID(rawValue: 0x000C) }

static var additionalProtocolDescriptorLists: SDPAttributeID { SDPAttributeID(rawValue: 0x000D) }
}

public extension SDPAttributeID {

/// The offsets added to a language base (from
/// ``languageBaseAttributeIDList``) to get the actual attribute ID
/// of a language-specific string attribute.
enum PrimaryLanguageOffset: UInt16, Sendable {
case serviceName = 0x0000
case serviceDescription = 0x0001
case providerName = 0x0002
}

/// The default primary-language base offset (0x0100), used when a
/// record doesn't have its own ``languageBaseAttributeIDList``.
static var primaryLanguageBase: UInt16 { 0x0100 }

/// The attribute ID for `offset` under `base` (the primary
/// language's base, from ``languageBaseAttributeIDList``, or
/// ``primaryLanguageBase`` for the default language).
static func string(_ offset: PrimaryLanguageOffset, base: UInt16 = primaryLanguageBase) -> SDPAttributeID {
SDPAttributeID(rawValue: base + offset.rawValue)
}
}

extension SDPAttributeID: ExpressibleByIntegerLiteral {

public init(integerLiteral value: UInt16) {
self.init(rawValue: value)
}
}

extension SDPAttributeID: CustomStringConvertible {

public var description: String {
let hex = String(rawValue, radix: 16, uppercase: true)
return "0x" + String(repeating: "0", count: max(0, 4 - hex.count)) + hex
}
}
60 changes: 60 additions & 0 deletions Sources/BluetoothSDP/SDPBigEndian.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// SDPBigEndian.swift
// Bluetooth
//
// SDP PDUs are big-endian (network byte order), unlike most Bluetooth
// wire formats in this package, which are little-endian. Rather than
// reuse the little-endian-oriented `DataConvertible` conformances
// elsewhere in this module (`UInt16.init(bytes:)` and friends encode
// raw native/little-endian memory), every multi-byte integer in this
// module is read and written explicitly, most-significant-byte-first,
// through these two small generic helpers.
//
// UInt128 gets its own non-generic overloads below rather than using
// these: this package's `UInt128` only conforms to `FixedWidthInteger`
// under `@available(macOS 15, ...)` (on Apple platforms before the
// native stdlib type, it's a hand-rolled struct), so a generic
// `FixedWidthInteger`-constrained call fails to compile for it on
// earlier deployment targets. The concrete overloads instead go
// through `ByteValue`'s tuple accessor, which has no such gate.
//

import Bluetooth

internal extension FixedWidthInteger {

/// Parses `self` from `bytes`, most-significant byte first.
init<Bytes: Sequence>(bigEndianBytes bytes: Bytes) where Bytes.Element == UInt8 {
self = bytes.reduce(into: 0 as Self) { result, byte in
result = (result << 8) | Self(truncatingIfNeeded: byte)
}
}

/// The big-endian (network byte order) bytes of `self`, most
/// significant byte first.
var bigEndianBytes: [UInt8] {
let byteCount = Self.bitWidth / 8
return (0 ..< byteCount).reversed().map { shift in
UInt8(truncatingIfNeeded: self >> (shift * 8))
}
}
}

internal extension UInt128 {

/// Parses `self` from 16 big-endian (network byte order) bytes.
init<Bytes: Collection>(bigEndianBytes bytes: Bytes) where Bytes.Element == UInt8 {
let b = Array(bytes)
self = UInt128(bytes: (
b[15], b[14], b[13], b[12], b[11], b[10], b[9], b[8],
b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0]
))
}

/// The big-endian (network byte order) bytes of `self`, most
/// significant byte first.
var bigEndianBytes: [UInt8] {
let b = bytes
return [b.15, b.14, b.13, b.12, b.11, b.10, b.9, b.8, b.7, b.6, b.5, b.4, b.3, b.2, b.1, b.0]
}
}
Loading
Loading