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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
echo "Updated pyproject.toml to version $NEW_VERSION"

# Generate changelog using git-changelog
git-changelog --in-place --output CHANGELOG.md --provider github --bump "$NEW_VERSION"
git-changelog --output CHANGELOG.md --provider github --bump "$NEW_VERSION"
echo "Updated CHANGELOG.md with version $NEW_VERSION using git-changelog"

# Commit and push to bump branch
Expand Down
13,181 changes: 4 additions & 13,177 deletions CHANGELOG.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion docs/src/content/docs/reference/test-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Test files: `tests/{c,cpp,py,ts,js,csharp,rust}/test_negative.*`

See `tests/NEGATIVE_TESTS.md` for full scenario descriptions.

The 15 scenarios in the table below are registered in every language's `test_negative.*` file. Individual languages carry additional language-specific scenarios: C/C++/TS/JS/C# (20 each) add bulk `pkg_id`/`msg_id` corruption, cross-package rejection, network `pkg_id` corruption, and stream-recovery tests; Python (30) adds those plus diagnostic-counter and status-machine tests; Rust (16) adds stream-recovery only.
The 20 scenarios in the table below are registered in every language's `test_negative.*` file. Individual languages carry additional language-specific scenarios: C/C++/TS/JS/C# (24 each) add bulk `pkg_id`/`msg_id` corruption, cross-package rejection, and network `pkg_id` corruption; Python (34) adds those plus diagnostic-counter and status-machine tests; Rust (20) currently matches the uniform set.

| Error Scenario (test name) | C | C++ | Python | TS | JS | C# | Rust |
|--------|--------|--------|--------|--------|--------|--------|--------|
Expand All @@ -238,11 +238,16 @@ The 15 scenarios in the table below are registered in every language's `test_neg
| Invalid message ID rejection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Invalid start bytes detection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Minimal profile: Truncated frame | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Multiple frames: CRC error then valid frame | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Multiple frames: Corrupted middle frame | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Network profile: SysId/CompId corruption | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Partial frame across buffer boundary | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Split-buffer: CRC error status preserved | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Stream mode: recovers after garbage prefix | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Streaming: Corrupted CRC detection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Streaming: Garbage data handling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| TryNext drain: CRC/resync + valid | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| TryNext partial pending contract | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Truncated frame detection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Zero-length buffer handling | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

Expand Down
53 changes: 46 additions & 7 deletions src/struct_frame/boilerplate/c/frame_profiles.h
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,9 @@ static inline frame_msg_info_t buffer_reader_next(buffer_reader_t* reader)
/* Advance past complete frames — valid or CRC-failed */
reader->offset += result.frame_size;
} else if (result.status == FRAME_MSG_STATUS_WAITING_FOR_START) {
/* Head byte is not a valid start — scan forward to next start byte */
/* Head byte is not a valid start — scan forward to next start byte,
* reporting a SyncRecovery event so try_next() keeps advancing. */
size_t old_offset = reader->offset;
if (reader->config->header.num_start_bytes >= 1) {
/* Compute the expected start byte for this profile */
uint8_t sb1 = reader->config->header.start_byte1;
Expand All @@ -535,6 +537,8 @@ static inline frame_msg_info_t buffer_reader_next(buffer_reader_t* reader)
} else {
reader->offset = reader->size;
}
result.status = FRAME_MSG_STATUS_SYNC_RECOVERY;
result.frame_size = reader->offset - old_offset;
}
/* COLLECTING: leave offset unchanged — frame is incomplete (normal end-of-buffer) */

Expand All @@ -550,6 +554,19 @@ static inline bool buffer_reader_has_more(const buffer_reader_t* reader) {
return reader->offset < reader->size;
}

/* Try to parse the next frame, returning true while forward progress is made
* (valid frame, CRC-failed frame, or resync bytes skipped). Returns false only
* when the buffer is drained or only a trailing partial frame remains.
* Canonical drain loop:
* frame_msg_info_t f;
* while (buffer_reader_try_next(&reader, &f)) {
* if (f.valid) handle(f); // else CrcFailure or SyncRecovery
* } */
static inline bool buffer_reader_try_next(buffer_reader_t* reader, frame_msg_info_t* out) {
*out = buffer_reader_next(reader);
return out->valid || out->frame_size > 0;
}

/*===========================================================================
* BufferWriter - Encode multiple frames with automatic offset tracking
*===========================================================================*/
Expand Down Expand Up @@ -793,20 +810,28 @@ static inline frame_msg_info_t accumulating_reader_next(accumulating_reader_t* r
}

if (result.status == FRAME_MSG_STATUS_WAITING_FOR_START) {
/* Head byte is not a start byte — scan forward */
/* Head byte is not a start byte — scan forward, reporting a SyncRecovery event
* so accumulating_reader_try_next() keeps draining past garbage bytes. */
size_t old_current_offset = reader->current_offset;
if (reader->config->header.num_start_bytes >= 1) {
size_t scan_start = reader->current_offset + 1;
if (scan_start < reader->current_size) {
const void* p = memchr(reader->current_buffer + scan_start,
(int)sb1,
reader->current_size - scan_start);
if (p) {
reader->current_offset = (size_t)((const uint8_t*)p - reader->current_buffer);
return result; /* caller calls next() again from new position */
}
reader->current_offset = p
? (size_t)((const uint8_t*)p - reader->current_buffer)
: reader->current_size;
} else {
reader->current_offset = reader->current_size;
}
} else {
reader->current_offset = reader->current_size;
}
reader->current_offset = reader->current_size;
result.status = FRAME_MSG_STATUS_SYNC_RECOVERY;
result.frame_size = reader->current_offset - old_current_offset;
reader->diagnostics.cnt_sync_recoveries++;
reader->diagnostics.cnt_failed_bytes += (uint32_t)result.frame_size;
return result;
}

Expand Down Expand Up @@ -1149,6 +1174,20 @@ static inline size_t accumulating_reader_partial_size(const accumulating_reader_
return reader->internal_data_len;
}

/* Try to parse the next frame, returning true while forward progress is made
* (valid frame, CRC-failed frame, or resync bytes skipped). Returns false only
* when the current buffer is exhausted or only a trailing partial is pending.
* Canonical drain loop:
* frame_msg_info_t f;
* while (accumulating_reader_try_next(&reader, &f)) {
* if (f.valid) handle(f); // else CrcFailure or SyncRecovery
* }
* if (accumulating_reader_has_partial(&reader)) { // feed more data } */
static inline bool accumulating_reader_try_next(accumulating_reader_t* reader, frame_msg_info_t* out) {
*out = accumulating_reader_next(reader);
return out->valid || out->frame_size > 0;
}

static inline accumulating_reader_state_t accumulating_reader_state(const accumulating_reader_t* reader) {
return reader->state;
}
Expand Down
65 changes: 54 additions & 11 deletions src/struct_frame/boilerplate/cpp/frame_profiles.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,9 @@ class BufferReader {
// Advance past complete frames (valid or CRC-failed)
offset_ += result.frame_size;
} else if (result.status == FrameMsgStatus::WaitingForStart) {
// Head byte is not a frame start — scan forward to next start byte
// Head byte is not a frame start — scan forward to next start byte,
// reporting SyncRecovery so try_next() keeps advancing.
size_t old_offset = offset_;
if constexpr (Config::num_start_bytes >= 1) {
size_t scan_start = offset_ + 1;
if (scan_start < size_) {
Expand All @@ -519,6 +521,8 @@ class BufferReader {
} else {
offset_ = size_;
}
result.status = FrameMsgStatus::SyncRecovery;
result.frame_size = offset_ - old_offset;
}
// Collecting: leave offset unchanged — frame is incomplete (normal end-of-buffer)

Expand All @@ -545,6 +549,22 @@ class BufferReader {
*/
bool has_more() const { return offset_ < size_; }

/**
* Try to parse the next frame. Returns true while forward progress is made
* (valid frame, CRC-failed frame, or skipped resync bytes). Returns false
* when the buffer is drained or only a trailing partial frame remains.
*
* Canonical drain loop:
* FrameMsgInfo f;
* while (reader.try_next(f)) {
* if (f.valid) handle(f); // else CrcFailure or SyncRecovery
* }
*/
bool try_next(FrameMsgInfo& out) {
out = next();
return out.valid || out.frame_size > 0;
}

private:
FrameMsgInfo parse_frame(const uint8_t* buffer, size_t size) const {
if constexpr (Config::has_length || Config::has_crc) {
Expand Down Expand Up @@ -844,24 +864,30 @@ class AccumulatingReader {
}

if (result.status == FrameMsgStatus::WaitingForStart) {
// Head byte is not a start byte — scan forward for the next one
// Head byte is not a start byte — scan forward for the next one,
// reporting SyncRecovery so try_next() keeps draining.
size_t old_offset = current_offset_;
if constexpr (Config::num_start_bytes >= 1) {
size_t scan_start = current_offset_ + 1;
if (scan_start < current_size_) {
const void* p = std::memchr(current_buffer_ + scan_start,
static_cast<int>(Config::computed_start_byte1()),
current_size_ - scan_start);
if (p) {
current_offset_ = static_cast<size_t>(
static_cast<const uint8_t*>(p) - current_buffer_);
// Return empty — caller should call next() again to parse from new position
return with_diagnostics(FrameMsgInfo());
}
current_offset_ = p
? static_cast<size_t>(static_cast<const uint8_t*>(p) - current_buffer_)
: current_size_;
} else {
current_offset_ = current_size_;
}
} else {
current_offset_ = current_size_;
}
// No start byte found in remainder — consume everything
current_offset_ = current_size_;
return with_diagnostics(FrameMsgInfo());
FrameMsgInfo r;
r.status = FrameMsgStatus::SyncRecovery;
r.frame_size = current_offset_ - old_offset;
diagnostics_.cnt_sync_recoveries++;
diagnostics_.cnt_failed_bytes += static_cast<uint32_t>(r.frame_size);
return with_diagnostics(r);
}

// Collecting — save remaining bytes to internal buffer for next add_data() call
Expand Down Expand Up @@ -938,6 +964,23 @@ class AccumulatingReader {
*/
size_t partial_size() const { return internal_data_len_; }

/**
* Try to parse the next frame. Returns true while forward progress is made
* (valid frame, CRC-failed frame, or skipped resync bytes). Returns false
* when the current buffer is exhausted or only a trailing partial is pending.
*
* Canonical drain loop:
* FrameMsgInfo f;
* while (reader.try_next(f)) {
* if (f.valid) handle(f); // else CrcFailure or SyncRecovery
* }
* if (reader.has_partial()) { // incomplete frame; feed more data }
*/
bool try_next(FrameMsgInfo& out) {
out = next();
return out.valid || out.frame_size > 0;
}

/**
* Get current parser state (for debugging).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,6 @@ public void ResetDiagnostics()
/// </summary>
public State CurrentState => _state;

/// <summary>
/// Check if there's a partial message waiting for more data.
/// </summary>
public bool HasPartial => _internalDataLen > 0;

/// <summary>
/// Get the size of the partial message data.
/// </summary>
public int PartialSize => _internalDataLen;

// =========================================================================
// Buffer Mode API
// =========================================================================
Expand Down Expand Up @@ -150,7 +140,7 @@ public void AddData(byte[] buffer)
public bool TryNext(out FrameMsgInfo result)
{
result = Next();
return result.Valid || result.FrameData.Length > 0;
return result.Valid || result.FrameSize > 0;
}

/// <summary>
Expand All @@ -176,24 +166,22 @@ public FrameMsgInfo Next()

if (result.Status == FrameMsgStatus.WaitingForStart)
{
// Garbage at the start of the internal buffer. Scan forward for the
// next start-byte candidate and discard the skipped bytes.
int skip = FindStartByteOffset(_internalBuffer, 1, _internalDataLen - 1);
// Garbage at the start of the internal buffer. Byte 0 is not a valid
// start, so scan forward for the next start-byte candidate and discard
// everything before it — at least one byte, guaranteeing forward progress.
int searchLen = _internalDataLen - 1;
int found = (searchLen > 0 && _config.NumStartBytes > 0)
? Array.IndexOf(_internalBuffer, _config.ComputedStartByte1, 1, searchLen)
: -1;
int discard = found > 0 ? found : _internalDataLen;
_diagnostics.CntSyncRecoveries++;
_diagnostics.CntFailedBytes += skip;
if (skip >= _internalDataLen)
{
_internalDataLen = 0;
_bytesAppendedToInternal = 0;
}
else
{
int keep = _internalDataLen - skip;
Array.Copy(_internalBuffer, skip, _internalBuffer, 0, keep);
_internalDataLen = keep;
_bytesAppendedToInternal = Math.Max(0, _bytesAppendedToInternal - skip);
}
return StatusResult(FrameMsgStatus.SyncRecovery);
_diagnostics.CntFailedBytes += discard;
int keep = _internalDataLen - discard;
if (keep > 0)
Array.Copy(_internalBuffer, discard, _internalBuffer, 0, keep);
_internalDataLen = keep;
_bytesAppendedToInternal = Math.Max(0, _bytesAppendedToInternal - discard);
return StatusResult(FrameMsgStatus.SyncRecovery, discard);
}

if (result.FrameSize > 0)
Expand Down Expand Up @@ -229,15 +217,18 @@ public FrameMsgInfo Next()
if (parseResult.Status == FrameMsgStatus.WaitingForStart)
{
// Garbage at current offset. Scan forward for the next start-byte candidate.
int oldOffset = _currentOffset;
int searchFrom = _currentOffset + 1;
int searchLen = _currentSize - searchFrom;
int skip = searchLen > 0
? FindStartByteOffset(_currentBuffer, searchFrom, searchLen)
: searchLen;
int delta = skip < searchLen ? skip : searchLen;
_currentOffset = searchFrom + delta;
int advanced = _currentOffset - oldOffset; // >= 1
_diagnostics.CntSyncRecoveries++;
_diagnostics.CntFailedBytes += 1 + (skip < searchLen ? skip : searchLen);
_currentOffset = searchFrom + (skip < searchLen ? skip : searchLen);
return StatusResult(FrameMsgStatus.SyncRecovery);
_diagnostics.CntFailedBytes += advanced;
return StatusResult(FrameMsgStatus.SyncRecovery, advanced);
}

if (parseResult.FrameSize > 0)
Expand Down Expand Up @@ -281,6 +272,17 @@ public bool HasMore
}
}

/// <summary>
/// True if an incomplete (partial) frame is buffered awaiting more data via AddData().
/// Distinguishes "drained" (HasPartial == false) from "waiting for more data".
/// </summary>
public bool HasPartial => _internalDataLen > 0;

/// <summary>
/// Number of bytes held for a partial frame awaiting completion (0 if none).
/// </summary>
public int PartialSize => _internalDataLen;

// =========================================================================
// Stream Mode API
// =========================================================================
Expand Down Expand Up @@ -611,10 +613,11 @@ private int FindStartByteOffset(byte[] buf, int start, int length)
return found >= 0 ? found - start : length;
}

private FrameMsgInfo StatusResult(FrameMsgStatus status)
private FrameMsgInfo StatusResult(FrameMsgStatus status, int frameSize = 0)
{
var r = FrameMsgInfo.Invalid;
r.Status = status;
r.FrameSize = frameSize;
return AttachDiagnostics(r);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public FrameMsgInfo Next()
{
// Start byte mismatch at current offset: scan forward for the next candidate.
// Skip the bad byte and look for start byte 1 in the remaining data.
int oldOffset = _offset;
int searchFrom = _offset + 1;
int searchLen = _size - searchFrom;
if (searchLen > 0 && _config.NumStartBytes > 0)
Expand All @@ -74,12 +75,28 @@ public FrameMsgInfo Next()
{
_offset = _size; // nothing useful left
}
// Report the resync as a SyncRecovery event carrying the bytes skipped, so a
// TryNext() drain loop keeps advancing instead of stopping on stray bytes.
result.Status = FrameMsgStatus.SyncRecovery;
result.FrameSize = _offset - oldOffset; // >= 1
}
// Status == Collecting: genuinely incomplete data — do not advance; let caller stop.

return result;
}

/// <summary>
/// Try to parse the next frame. Returns true while there is forward progress
/// (a valid frame, a CRC-failed frame, or skipped resync bytes); false when the
/// buffer is drained or only a trailing partial frame remains. Pairs with a
/// <c>while (reader.TryNext(out var r)) { ... }</c> drain loop.
/// </summary>
public bool TryNext(out FrameMsgInfo result)
{
result = Next();
return result.Valid || result.FrameSize > 0;
}

/// <summary>
/// Reset the reader to the beginning of the buffer.
/// </summary>
Expand Down
Loading
Loading