fix: keep longest manufacturer data when scan record has multiple 0xFF AD structures#1340
Open
zuozhen-ai wants to merge 2 commits into
Open
Conversation
…F AD structures On Android, parseScanResponseData walks the merged scan record and parseManufacturerData unconditionally overwrites advData.manufacturerData, so when a record carries more than one manufacturer-specific AD structure (e.g. one from ADV_IND and another from SCAN_RSP) whichever appears last wins. Peripherals that advertise a short placeholder alongside the real payload lose their data, while iOS (CoreBluetooth) exposes the full merged manufacturer data. Keep the longest structure instead. The early return is safe: the caller advances the buffer cursor itself and hands each parser an independent slice — parseLocalName already takes the same consume-nothing path when it ignores a shortened name after a name has been set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces JUnit (testImplementation only, not shipped to consumers) and covers AdvertisementData.parseScanResponseData: single-structure parsing, the sub-2-byte guard, multiple 0xFF structures in both orders, the equal-length tie-break, cursor integrity after a kept-as-is structure, and the real-world merged scan record from the PR description. Four of the seven tests fail against the previous last-wins behaviour and all pass with the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
|
Added unit tests in 160e3bc to make the fix easy to verify: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prerequisites
master, also reproduced on v3.5.1).AdvertisementData.java); the iOS path (CoreBluetooth's merged advertisement data) is untouched.npm testandnpm run lint: results are identical on unmodifiedmasterand on this branch (the change is Java-only). In my environment some jest suites fail at setup on both branches equally (a localNativeEventEmitter/Node-version issue; every test that runs passes on both), and the finaltsc --noEmitlint step needs the example app's dependencies on both — flow, eslint and documentation lint pass unchanged. CI's pinned environments should come back green.android/src/test/java/com/bleplx/adapter/AdvertisementDataTest.java(plus atestImplementation "junit:junit:4.13.2"line — test-only, nothing shipped to consumers). 7 tests cover single-structure parsing, the sub-2-byte guard, both orderings of duplicate0xFFstructures, the equal-length tie-break, cursor integrity after a kept-as-is structure, and the real-world captured record below. 4 of the 7 fail against the previous last-wins behaviour; all 7 pass with the fix. The class only depends onjava.*, so they run as plain JVM unit tests.Bug description
On Android, when a merged scan record contains more than one manufacturer-specific AD structure (
0xFF) — typically one in ADV_IND and another in SCAN_RSP —Device.manufacturerDataonly ever exposes the last structure in the byte stream. Whatever came before it is silently lost.Cause:
parseScanResponseDatawalks every AD structure of the merged record andparseManufacturerDataunconditionally overwritesadvData.manufacturerDataon each0xFFit meets:iOS is unaffected: CoreBluetooth hands over the merged
CBAdvertisementDataManufacturerDataKey, so the same peripheral yields the real payload on iOS and a placeholder on Android — a platform behavior divergence.This has been reported repeatedly over the years but never fixed at the parse layer: #483 (2019, byte-level analysis, closed by stale bot), #556 (reopen attempt), #949 (filed 2022, closed in 2023 by adding the
rawScanRecordescape hatch in #1134 rather than fixing the merge), #970, and most recently #1306 (closed 2026 with the maintainer confirming the library exposes no ADV/SCAN_RSP distinction).Real-world reproduction (captured from production hardware)
Our peripheral (an audio recorder pen) advertises a 12-byte placeholder in one packet and the real 25-byte identity (which contains the device serial number) in the other. Raw scan record captured via
rawScanRecordon Android (base64AgEGGv9kTkNQZFNnbVJQNTAxMDEwNTkwMDExODkBDf9GRkZGRkZGRkZGRkYNCUFJIE5vdGUtNEU2MwAAAAA=), decoded:0x010x060xFFdNCPdSgmRP50101059001189·0xFFFFFFFFFFFFFF0x09AI Note-4E63device.manufacturerData(before this fix):RkZGRkZGRkZGRkZG="FFFFFFFFFFFF"— the 12-byte placeholder, structure Unable to install module via npm install on Windows and Ubuntu #1 is lost.manufacturerData(our serial parsing works there unmodified).device.manufacturerData(after this fix):ZE5DUGRTZ21SUDUwMTAxMDU5MDAxMTg5AQ==— the real payload, restoring parity with iOS.Verified on 7 physical devices: with the fix, every one of them exposes the real manufacturer data through the normal
manufacturerDatafield instead of requiring therawScanRecordworkaround.Details
The fix
Keep the longest manufacturer-data structure instead of the last one:
parseScanResponseDataadvancesrawData.position()itself (line 84) and passes each parser an independentslice(), so whether the callee consumes 0 oradLengthbytes is irrelevant.parseLocalNamealready takes the same consume-nothing path when it ignores a shortened name (0x08) after a name has been set.getManufacturerData()is only consumed byScanResultToJsObjectConverter(base64 pass-through to JS).Alternatives considered
ScanRecord.getBytes()is not something to rely on either way.0xFFstructures — matches what some stacks do for same-company-ID fragments, but blindly concatenating distinct structures corrupts payloads of devices that broadcast genuinely different data per packet.Keep-longest is the smallest change that restores iOS/Android parity for the common real-world pattern (short placeholder + full payload) without any API change.
Standalone reproduction harness
AdvertisementDataonly depends onjava.*, so the fix can be verified without an emulator: