Skip to content

fix(communication): tighten ProtobufMessageParser gap-gate + wire-type validation (closes #189)#197

Merged
tylerkron merged 5 commits into
mainfrom
fix/protobuf-parser-gap-and-wire-type
May 14, 2026
Merged

fix(communication): tighten ProtobufMessageParser gap-gate + wire-type validation (closes #189)#197
tylerkron merged 5 commits into
mainfrom
fix/protobuf-parser-gap-and-wire-type

Conversation

@cptkoolbeenz
Copy link
Copy Markdown
Member

Summary

Two parser-hardening bugs surfaced by Qodo on the daqifi-python-core port (PR #100); both present in the C# implementation.

  • Bug 1: gap gate fired regardless of how many body bytes had arrived, corrupting legitimate multi-KB frames split across reads. Now gated on availableBodyBytes <= 1 so the field-tag gate handles all other cases.
  • Bug 2: IsPlausibleFieldTagByte skipped wire-type validation on continuation bytes. Wire type lives in the low 3 bits of the first byte regardless of multi-byte tags, so impossible wire types (3, 4, 6, 7) are now rejected even on continuation.

Both fixes match the resolution shipped on the Python port and validated by Qodo there.

Test plan

  • 2 new regression tests added (LegitimateMultiKbFrame_NotCorruptedByGapGate Fact + GarbageWithContinuationBit_StillRejected Theory × 4 wire types)
  • All 15 ProtobufMessageParser tests pass
  • Full suite: 895/897 (2 skipped require live hardware)
  • Build: clean on net9.0 and net10.0

Closes #189

…e validation (closes #189)

Two bugs surfaced by Qodo /agentic_review on the daqifi-python-core
port (PR #100); both present in the C# implementation.

Bug 1 — gap gate corrupted multi-chunk frames. The partial-frame wait
path advanced currentIndex whenever missingPayload exceeded
MaxPartialFrameGapBytes, regardless of how many body bytes had arrived.
A legitimate multi-KB frame split across reads (e.g. an initial-status
frame on a transport returning smaller chunks) was misclassified as
garbage mid-frame, causing the parser to skip into the real body and
corrupt it. Fixed: gate the gap heuristic on availableBodyBytes <= 1.
Once 2+ body bytes are buffered, the field-tag gate above has already
structurally validated the frame start and a large declared length is
just a multi-chunk read.

Bug 2 — IsPlausibleFieldTagByte skipped wire-type validation on
multi-byte tags. The continuation-bit early return bypassed the
wire-type check, but wire type lives in the low 3 bits of the first
byte regardless of multi-byte field numbers. Impossible wire types
(3,4,6,7) MUST be rejected even on continuation bytes. Fixed: check
wire type before the continuation-bit return.

Added two regression tests: one asserting a real >1KB frame fed in
prefix+partial-body form is preserved (gap gate doesn't fire), and a
parametrized theory covering all four impossible wire types with the
continuation bit set (field-tag gate fires). 15/15 parser tests pass;
895/897 in the full suite (2 skipped require live hardware).
Fix 1 stale comment: test fixture references DeviceFwRev (actual field)
instead of HostFirmwareRev (issue text used pre-rename name).
@cptkoolbeenz cptkoolbeenz requested a review from a team as a code owner May 12, 2026 04:18
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Advisory comments

1. Test lacks size precondition ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
ProtobufMessageParser_ParseMessages_OneBodyByteOfMultiKbFrame_NotCorrupted doesn’t assert its frame
is actually > MaxPartialFrameGapBytes, so the regression could become ineffective if the fixture
message shrinks in the future. This makes the test easier to accidentally de-scope without noticing.
Code

src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs[R296-308]

+        var msg = new DaqifiOutMessage
+        {
+            MsgTimeStamp = 99,
+            DeviceFwRev = new string('x', 2000),
+        };
+        using var stream = new MemoryStream();
+        msg.WriteDelimitedTo(stream);
+        var fullFrame = stream.ToArray();
+
+        // Feed prefix + exactly 1 body byte. The first body byte of a real
+        // DaqifiOutMessage is a valid field tag (passes IsPlausibleFieldTagByte).
+        var prefixPlusOne = fullFrame.Take(GetVarintLength(fullFrame) + 1).ToArray();
+
Evidence
The 4-body-byte test includes an explicit frame-size precondition check, while the 1-body-byte test
does not, even though both intend to cover “multi-KB” frames and old behavior depended on exceeding
the gap threshold.

src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs[254-280]
src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs[282-315]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `OneBodyByteOfMultiKbFrame_NotCorrupted` regression relies on the frame being large enough to have triggered the old gap gate logic, but it doesn’t assert that precondition.

### Issue Context
A nearby test (`LegitimateMultiKbFrame_NotCorruptedByGapGate`) includes an explicit `fullFrame.Length > 1100` check; mirroring that here keeps this regression robust if message schema/serialization changes.

### Fix Focus Areas
- src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs[296-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Tighten ProtobufMessageParser gap-gate and wire-type validation

🐞 Bug fix 🧪 Tests

Grey Divider

Walkthroughs

Description
• Fix gap-gate corruption of legitimate multi-KB frames split across reads
  - Gate suspicion check on availableBodyBytes <= 1 to prevent false positives
• Fix wire-type validation bypass on multi-byte field tags with continuation bit
  - Check wire type before early-return for continuation bytes
• Add two regression tests covering both bugs
  - Multi-KB frame preservation test and parametrized impossible wire-type tests
Diagram
flowchart LR
  A["Parser receives<br/>partial frame"] --> B{"availableBodyBytes<br/><= 1?"}
  B -->|Yes| C{"missingPayload ><br/>MaxPartialFrameGapBytes?"}
  B -->|No| D["Wait for more<br/>body bytes"]
  C -->|Yes| E["Advance past<br/>garbage"]
  C -->|No| D
  F["Check wire type<br/>in low 3 bits"] --> G{"Wire type<br/>3,4,6,7?"}
  G -->|Yes| E
  G -->|No| H{"Continuation<br/>bit set?"}
  H -->|Yes| D
  H -->|No| I{"Field number<br/>!= 0?"}
  I -->|Yes| D
  I -->|No| E
Loading

Grey Divider

File Changes

1. src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs 🐞 Bug fix +33/-17

Harden field-tag and gap-gate validation logic

• Refactored gap-gate logic to only apply when availableBodyBytes <= 1, preventing false positives
 on legitimate multi-chunk frames
• Moved wire-type validation before continuation-bit check in IsPlausibleFieldTagByte to reject
 impossible wire types (3, 4, 6, 7) even on multi-byte tags
• Updated method documentation to clarify wire-type validation applies regardless of tag byte count
• Improved variable naming and added detailed comments explaining the fix rationale

src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs


2. src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs 🧪 Tests +96/-0

Add regression tests for gap-gate and wire-type fixes

• Added LegitimateMultiKbFrame_NotCorruptedByGapGate Fact test verifying multi-KB frames split
 across reads are preserved
• Added GarbageWithContinuationBit_StillRejected Theory test with four inline data cases covering
 impossible wire types (0x83, 0x84, 0x86, 0x87)
• Added GetVarintLength helper method to extract varint prefix length from test fixtures
• Tests validate parser advances past garbage instead of stalling or corrupting legitimate frames

src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Gap gate still corrupts ✓ Resolved 🐞 Bug ≡ Correctness
Description
ParseMessages can still treat a legitimate multi‑KB frame as garbage when exactly 1 body byte has
arrived because gapIsSuspicious is gated by availableBodyBytes <= 1 and ignores that the first
body byte may be plausible. This can permanently drop real frame data because
StreamMessageConsumer removes consumedBytes bytes from the front of the buffer after each parse
attempt.
Code

src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs[R125-131]

+                var gapIsSuspicious = availableBodyBytes <= 1
+                    && missingPayload > MaxPartialFrameGapBytes;
+
+                if (gapIsSuspicious || firstBodyByteIsGarbage)
                {
                    currentIndex++;
                    consumedBytes = currentIndex;
Evidence
The parser advances currentIndex (and therefore consumedBytes) when gapIsSuspicious is true;
with availableBodyBytes == 1 and a large declared length, this can happen even if the first body
byte is plausible. Because the stream consumer always removes consumedBytes from its buffer, a
single-byte mistaken advance drops real frame data permanently. Large frames are explicitly expected
(initial-status frames are described as a few KB).

src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs[14-18]
src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs[81-135]
src/Daqifi.Core/Communication/Consumers/StreamMessageConsumer.cs[154-182]
src/Daqifi.Core/Communication/Consumers/StreamMessageConsumer.cs[198-207]
src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs[268-280]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProtobufMessageParser.ParseMessages` currently considers the “gap gate” suspicious when `availableBodyBytes <= 1`. If a legitimate large frame (e.g., initial-status a few KB) arrives such that only *one* body byte is buffered (common because stream reads can return small chunks), `gapIsSuspicious` can become true and the parser will advance `currentIndex` by 1, causing the caller to drop that byte from its buffer and corrupt the frame.

### Issue Context
- The field-tag gate already validates the first body byte when any body bytes exist.
- The gap gate’s intended scope (per comments/ticket) is the *pure prefix / no body* scenario where the field-tag gate cannot run.
- `StreamMessageConsumer` trims `_messageBuffer` by `consumedBytes`, making any incorrect advance permanently destructive.

### Fix Focus Areas
- src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs[111-135]
- src/Daqifi.Core.Tests/Communication/Consumers/ProtobufMessageParserTests.cs[233-280]

### Proposed fix
1. Change the gap gate condition to apply only when **no** body bytes are available:
  - `var gapIsSuspicious = availableBodyBytes == 0 && missingPayload > MaxPartialFrameGapBytes;`
  (or `<= 0` for defensive clarity).
2. Keep `firstBodyByteIsGarbage` as the mechanism for resync when `availableBodyBytes > 0`.
3. Add/extend a regression test that feeds **prefix + 1 body byte** of a legitimate >1KB frame and asserts `consumedBytes == 0` (i.e., parser waits rather than advancing).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Harden gap-gate arithmetic

Update gap-gate arithmetic to use long and clamp values to prevent potential
integer overflow and negative results.

src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs [4-26]

-var availableBodyBytes = remainingData.Length - prefixBytes;
-var missingPayload = messageLength - availableBodyBytes;
+var availableBodyBytes = Math.Max(0, remainingData.Length - prefixBytes);
+
+var missingPayload = (long)messageLength - availableBodyBytes;
+
 var firstBodyByteIsGarbage = availableBodyBytes > 0
     && !IsPlausibleFieldTagByte(remainingData[prefixBytes]);
 
 ...
 
 var gapIsSuspicious = availableBodyBytes == 0
     && missingPayload > MaxPartialFrameGapBytes;
 
 if (gapIsSuspicious || firstBodyByteIsGarbage)
 {
     currentIndex++;
     consumedBytes = currentIndex;
     continue;
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 1

__

Why: The suggestion adds overly defensive checks for virtually impossible states (like prefixBytes exceeding array length), providing no practical improvement to the robustness of the code.

Low
  • Update

Comment thread src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs Outdated
…o availableBodyBytes == 0

Qodo flagged that the previous fix's `availableBodyBytes <= 1` still
corrupts real frames whose first body byte arrives alone (a common
chunked-read scenario). With 1 body byte buffered, the gap gate would
fire even though the field-tag check on that single byte already
provides positive evidence the prefix is a real frame start; advancing
1 byte then unrecoverably corrupts the frame because StreamMessageConsumer
trims consumedBytes from its buffer.

Tightened the gap gate to availableBodyBytes == 0 — once any body
byte is observable, the field-tag gate handles validation. Added a
regression test feeding prefix + exactly 1 body byte of a real
multi-KB frame; parser must wait, not advance.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 32cfe23

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Harden gap-gate arithmetic

Update gap-gate arithmetic to use long and clamp values to prevent potential
integer overflow and negative results.

src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs [4-26]

-var availableBodyBytes = remainingData.Length - prefixBytes;
-var missingPayload = messageLength - availableBodyBytes;
+var availableBodyBytes = Math.Max(0, remainingData.Length - prefixBytes);
+
+var missingPayload = (long)messageLength - availableBodyBytes;
+
 var firstBodyByteIsGarbage = availableBodyBytes > 0
     && !IsPlausibleFieldTagByte(remainingData[prefixBytes]);
 
 ...
 
 var gapIsSuspicious = availableBodyBytes == 0
     && missingPayload > MaxPartialFrameGapBytes;
 
 if (gapIsSuspicious || firstBodyByteIsGarbage)
 {
     currentIndex++;
     consumedBytes = currentIndex;
     continue;
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 1

__

Why: The suggestion adds overly defensive checks for virtually impossible states (like prefixBytes exceeding array length), providing no practical improvement to the robustness of the code.

Low
  • More

Add explicit fullFrame.Length > 1100 assertion in
ProtobufMessageParser_ParseMessages_OneBodyByteOfMultiKbFrame_NotCorrupted
to mirror the sibling LegitimateMultiKbFrame test. Prevents silent
regression de-scoping if the fixture message ever shrinks below the
gap-gate threshold.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 69e9a79

@qodo-code-review
Copy link
Copy Markdown

PR Code Suggestions ✨

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Harden gap-gate arithmetic

Update gap-gate arithmetic to use long and clamp values to prevent potential
integer overflow and negative results.

src/Daqifi.Core/Communication/Consumers/ProtobufMessageParser.cs [4-26]

-var availableBodyBytes = remainingData.Length - prefixBytes;
-var missingPayload = messageLength - availableBodyBytes;
+var availableBodyBytes = Math.Max(0, remainingData.Length - prefixBytes);
+
+var missingPayload = (long)messageLength - availableBodyBytes;
+
 var firstBodyByteIsGarbage = availableBodyBytes > 0
     && !IsPlausibleFieldTagByte(remainingData[prefixBytes]);
 
 ...
 
 var gapIsSuspicious = availableBodyBytes == 0
     && missingPayload > MaxPartialFrameGapBytes;
 
 if (gapIsSuspicious || firstBodyByteIsGarbage)
 {
     currentIndex++;
     consumedBytes = currentIndex;
     continue;
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 1

__

Why: The suggestion adds overly defensive checks for virtually impossible states (like prefixBytes exceeding array length), providing no practical improvement to the robustness of the code.

Low
  • More

@tylerkron
Copy link
Copy Markdown
Contributor

Hardware-in-the-loop test (DAQiFi device on /dev/cu.usbmodem101 @ 9600 baud)

Ran the daqifi-core-example-app built twice with -p:DaqifiCoreProjectPath=... — once against this branch, once against origin/main as a control — to confirm no regression on real hardware.

Results

Test This PR main
1ch @ 100 Hz, 3 s 299 samples 299 samples
16ch @ 200 Hz, 5 s 998 samples 998 samples
Median Δt (heavy) 250112 250112
Max Δt (heavy) 378111 479093

Both branches connect, complete the initial-status handshake (streaming setup wouldn't finish otherwise), and deliver matching sample counts. Timestamp deltas are clean — no gaps that would suggest a swallowed frame.

What this proves / doesn't

  • Non-regressive on real hardware — identical sample counts and median Δt vs main, all 16 analog channels deserialize correctly across 998 frames.
  • ⚠️ Bug 1 didn't fire organically on either branch. This device's initial-status frame fits under the 1 KB gap threshold, so the buggy gate on main never tripped during the runs I did. The unit test in this PR (LegitimateMultiKbFrame_NotCorruptedByGapGate) constructs a synthetic 2 KB frame to force it — that's where the regression coverage lives.
  • ⚠️ Bug 2 didn't fire either (non-deterministic — needs boot-time garbage to happen to have the continuation bit + a deprecated wire type).

daqifi-desktop compatibility

Spot-checked the desktop repo: it pins Daqifi.Core 0.21.0 and has zero direct references to ProtobufMessageParser, IMessageParser, StreamMessageConsumer, ParseMessages, or IsPlausibleFieldTagByte — it consumes the parser only transitively through DaqifiStreamingDevice. No method signatures, types, or public constants changed in this PR, so desktop picks up both fixes automatically on its next Daqifi.Core bump with no code changes.

Minor note on the <= 1 vs == 0 choice

Issue #189 suggested availableBodyBytes <= 1 for the gap gate; this PR uses == 0 (stricter) with a well-justified comment and a dedicated OneBodyByteOfMultiKbFrame_NotCorrupted test. If the Python port settled on <= 1, worth a quick note tracking the intentional divergence — otherwise no concern.

Smoke test says: ship it.

@tylerkron tylerkron merged commit e20479a into main May 14, 2026
1 check passed
@tylerkron tylerkron deleted the fix/protobuf-parser-gap-and-wire-type branch May 14, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(communication): tighten ProtobufMessageParser gap-gate + wire-type validation (Qodo findings on Python port)

2 participants