Skip to content

Commit

Permalink
Fix filtering by only company ID
Browse files Browse the repository at this point in the history
  • Loading branch information
twyatt committed Feb 6, 2025
1 parent 213aed5 commit bfc026e
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 5 deletions.
14 changes: 10 additions & 4 deletions kable-core/src/commonMain/kotlin/Filter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public sealed class Filter {
*/
public val id: Int,

public val data: ByteArray,
/** Must be non-`null` if [dataMask] is non-`null`. */
public val data: ByteArray? = null,

/**
* For any bit in the mask, set it to 1 if advertisement manufacturer data needs to match the corresponding bit
Expand All @@ -112,16 +113,20 @@ public sealed class Filter {
public val dataMask: ByteArray? = null,
) : Filter() {

public constructor(id: ByteArray, data: ByteArray, dataMask: ByteArray? = null) : this(id.toShort(), data, dataMask)
public constructor(id: ByteArray, data: ByteArray? = null, dataMask: ByteArray? = null) : this(id.toShort(), data, dataMask)

init {
require(id >= 0) { "Company identifier cannot be negative, was $id" }
require(id <= 65535) { "Company identifier cannot be more than 16-bits (65535), was $id" }
if (dataMask != null) requireDataAndMaskHaveSameLength(data, dataMask)
if (data != null && data.isEmpty()) throw IllegalArgumentException("If data is present (non-null), it must be non-empty")
if (dataMask != null) {
requireNotNull(data) { "Data is null but must be non-null when dataMask is non-null" }
requireDataAndMaskHaveSameLength(data, dataMask)
}
}

override fun toString(): String =
"ManufacturerData(id=$id, data=${data.toHexString()}, dataMask=${dataMask?.toHexString()})"
"ManufacturerData(id=$id, data=${data?.toHexString()}, dataMask=${dataMask?.toHexString()})"
}
}

Expand All @@ -144,6 +149,7 @@ internal fun Filter.Name.matches(name: String?): Boolean {
}

internal fun Filter.ManufacturerData.matches(data: ByteArray?): Boolean {
if (this.data == null) return true
if (data == null) return false
if (dataMask == null) return this.data.contentEquals(data)
val lastMaskIndex = dataMask.indexOfLast { it != 0.toByte() }
Expand Down
114 changes: 114 additions & 0 deletions kable-core/src/commonTest/kotlin/FilterPredicateTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.juul.kable
import com.juul.kable.Filter.Name.Exact
import com.juul.kable.Filter.Name.Prefix
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.uuid.Uuid
Expand Down Expand Up @@ -244,6 +245,119 @@ class FilterPredicateTests {
),
)
}

@Test
fun manufacturerDataFilter_nullDataWithNullDataMask_isAllowed() {
ManufacturerDataFilter(
id = 1,
data = null,
dataMask = null,
)
}

@Test
fun manufacturerDataFilter_nullDataWithEmptyDataMask_throwsIllegalArgumentException() {
assertFailsWith<IllegalArgumentException> {
ManufacturerDataFilter(
id = 1,
data = null,
dataMask = byteArrayOf(),
)
}
}

@Test
fun manufacturerDataFilter_nullDataWithNonNullDataMask_throwsIllegalArgumentException() {
assertFailsWith<IllegalArgumentException> {
ManufacturerDataFilter(
id = 1,
data = null,
dataMask = byteArrayOf(0xFF.toByte(), 0xFF.toByte()),
)
}
}

@Test
fun manufacturerDataFilter_emptyData_throwsIllegalArgumentException() {
assertFailsWith<IllegalArgumentException> {
ManufacturerDataFilter(
id = 1,
data = byteArrayOf(),
)
}
}

@Test
fun matches_manufacturerDataFilterWithNullDataVsEmptyData_isTrue() {
val predicate = ManufacturerDataFilter(
id = 1,
data = null,
dataMask = null,
).toPredicate()

assertTrue(
predicate.matches(
manufacturerData = ManufacturerData(
code = 1,
data = byteArrayOf(),
),
),
)
}

@Test
fun matches_manufacturerDataFilterWithNullDataVsData_isTrue() {
val predicate = ManufacturerDataFilter(
id = 1,
data = null,
dataMask = null,
).toPredicate()

assertTrue(
predicate.matches(
manufacturerData = ManufacturerData(
code = 1,
data = byteArrayOf(0xFF.toByte(), 0xFF.toByte()),
),
),
)
}

@Test
fun matches_manufacturerDataFilterWithoutDataMaskVsData_isTrue() {
val predicate = ManufacturerDataFilter(
id = 1,
data = byteArrayOf(0xFF.toByte(), 0xFF.toByte()),
dataMask = null,
).toPredicate()

assertTrue(
predicate.matches(
manufacturerData = ManufacturerData(
code = 1,
data = byteArrayOf(0xFF.toByte(), 0xFF.toByte()),
),
),
)
}

@Test
fun matches_manufacturerDataFilterWithoutDataMaskVsDifferentData_isFalse() {
val predicate = ManufacturerDataFilter(
id = 1,
data = byteArrayOf(0xF0.toByte(), 0x0D.toByte()),
dataMask = null,
).toPredicate()

assertFalse(
predicate.matches(
manufacturerData = ManufacturerData(
code = 1,
data = byteArrayOf(0x12, 0x34),
),
),
)
}
}

private fun Filter.toPredicate() = FilterPredicate(listOf(this))
4 changes: 3 additions & 1 deletion kable-core/src/jsMain/kotlin/BluetoothLEScanOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ private fun FilterPredicate.toBluetoothLEScanFilterInit(): BluetoothLEScanFilter
private fun toBluetoothManufacturerDataFilterInit(filter: Filter.ManufacturerData) =
jso<BluetoothManufacturerDataFilterInit> {
companyIdentifier = filter.id
dataPrefix = filter.data
if (filter.data != null) {
dataPrefix = filter.data
}
if (filter.dataMask != null) {
mask = filter.dataMask
}
Expand Down

0 comments on commit bfc026e

Please sign in to comment.