Flatten suppressable request structs into public data bags#44
Merged
Conversation
The no_std port left request types split between two visibility styles.
Per our convention (encapsulate only invariant-bearing structs, else
public data bag), flatten the suppressable single-/multi-field requests
to public fields and drop the redundant getters. The SPRMIB bit-packing
now lives only at the encode/decode boundary via a transient
SuppressablePositiveResponse, which stays pub(crate).
Also harden the sub-function enums: the reserved open-ended (u8)
variants are marked #[non_exhaustive] so callers must build them via the
range-checked TryFrom<u8>, making a SPRMIB-colliding value (>= 0x80)
unconstructable rather than caught at encode time.
Notable exceptions:
- CommunicationControlRequest stays encapsulated: node_id must be Some
iff control_type is an enhanced-address variant — a real cross-field
invariant. Its reserved CommunicationControlType variants are still
locked.
- SecurityAccessType::RequestSeed/SendKey stay directly constructible
(user-facing values); only ISOSAEReserved/SystemSupplierSpecific are
locked, so its construction-time guarantee is partial.
- TesterPresentRequest is now just { suppress_positive_response }; a
non-zero (reserved) sub-function byte still decodes but normalizes to
the zero sub-function on re-encode (it was previously preserved).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR standardizes the public API for “suppressable” UDS request types by flattening previously-encapsulated SPRMIB wrappers into public request fields, while keeping SPRMIB bit packing/unpacking at the encode/decode boundary. It also tightens construction of reserved sub-function values to prevent callers from constructing bytes that would collide with the SPRMIB bit.
Changes:
- Flattened several request structs into public “data bag” fields (
suppress_positive_response+ sub-function/value fields) and removed now-redundant getters. - Moved
SuppressablePositiveResponse<T>construction toencode/decodeonly (wire-boundary packing). - Marked reserved-range enum variants as
#[non_exhaustive]to force range-checked construction viaTryFrom<u8>.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/tester_present.rs | Flattens request into a single public suppression flag and normalizes reserved sub-function bytes on re-encode. |
| src/services/security_access.rs | Flattens request fields and shifts SPRMIB packing to encode/decode; adds #[non_exhaustive] on reserved variants. |
| src/services/routine_control.rs | Flattens request fields and shifts SPRMIB packing to encode/decode. |
| src/services/ecu_reset.rs | Flattens request fields, shifts SPRMIB packing to encode/decode, and locks reserved variants. |
| src/services/diagnostic_session_control.rs | Flattens request fields, shifts SPRMIB packing to encode/decode, and locks reserved variants. |
| src/services/control_dtc_settings.rs | Flattens request fields and shifts SPRMIB packing to encode/decode. |
| src/services/communication_control.rs | Locks reserved control-type variants via #[non_exhaustive] (keeps request encapsulated). |
| src/request.rs | Updates suppression detection to read new public fields for flattened request variants. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Finish the field-visibility convention sweep for §1A:
- RoutineControlResponse: echoed-data response, no invariant — fields
made public, getters dropped.
- RequestTransferExit{Request,Response}: macro-generated parameter_record
is opaque pass-through data — made public, getter dropped.
Left encapsulated by design (invariant- or abstraction-bearing):
- ReadDataByIdentifierRequest stores a private `Dids` representation
(native &[u16] vs borrowed wire bytes); exposing it would leak that
abstraction, so the `dids()` iterator stays the accessor.
- RequestDownloadRequest keeps private fields + fallible `new`: the
address/length format identifier must track the memory address/size
widths (cross-field invariant), like CommunicationControlRequest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mment Addresses PR review: - SecurityAccessType::RequestSeed/SendKey now carry a SecurityAccessLevel newtype (private u8 + validated `new` rejecting >= 0x80) instead of a raw u8. This was the last suppressable sub-function enum with a user-facing variant that could hold a SPRMIB-colliding value; the value is now unrepresentable rather than caught at encode time. The decode path constructs the level directly (range already guaranteed). - Reword the TesterPresent decode comment: after SPRMIB masking the low 7 bits are always a valid zero sub-function, so the parse never rejects — the previous comment implied validation that cannot fail. Audited the rest of the baseline: of the six SuppressablePositiveResponse -wrapped enums, this was the only remaining unlocked case. The other raw -int enum variants (DIDs, NRCs, DTC numbers) span the full valid integer range and steal no bit, so they carry no comparable invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JustinKovacich
approved these changes
Jul 1, 2026
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.
Summary
Follow-up to the
no_stdlanding. The ported request/response types were split between two visibility styles; this applies our field-visibility convention — encapsulate only invariant-bearing structs, otherwise a public data bag — consistently across the services, and closes a SPRMIB-collision footgun at construction time.What changed
Flattened to public data bags
Private
SuppressablePositiveResponse<T>field + getters → publicsuppress_positive_response: bool+ value field(s), getters removed:EcuResetRequest,DiagnosticSessionControlRequest,ControlDTCSettingsRequest,RoutineControlRequest,SecurityAccessRequest,TesterPresentRequest, plus the non-invariant responsesRoutineControlResponseandRequestTransferExit{Request,Response}.SPRMIB packing moved to the wire boundary
SuppressablePositiveResponse<T>stayspub(crate)and is now constructed transiently insideencode/decodeonly. The generic wrapper never appears in the public API.Reserved sub-function variants locked against SPRMIB collision
The open-ended
(u8)variants in reserved ranges (ResetType,DiagnosticSessionType,CommunicationControlType, and the reserved arms ofSecurityAccessType) are marked#[non_exhaustive], so callers must build them via the range-checkedTryFrom<u8>. A value ≥0x80that would collide with the SPRMIB bit is now unconstructable, rather than something to catch at encode time.SecurityAccessLevelnewtypeSecurityAccessType::RequestSeed/SendKeyare user-facing (kept directly constructible), so locking them wasn't an option — instead they now carry aSecurityAccessLevelnewtype (privateu8+ validatednewrejecting ≥0x80). This was the last SPRMIB-exposed enum with an unlocked user-facing variant; the colliding value is now unrepresentable. Const construction uses a const-matchunwrap sincenewis fallible.Deliberate decisions
CommunicationControlRequeststays encapsulated.node_idmust beSomeiffcontrol_typeis an enhanced-address variant — a genuine cross-field invariant — so it keeps private fields + fallible constructors. Its reservedCommunicationControlTypevariants are still locked.RequestDownloadRequeststays encapsulated. The address/length format identifier must track the memory address/size widths (cross-field invariant) + falliblenew.ReadDataByIdentifierRequeststays encapsulated. Its field is a privateDidsrepresentation (native&[u16]vs borrowed wire bytes); exposing it would leak that abstraction, so thedids()iterator remains the accessor.TesterPresentRequestis now{ suppress_positive_response }. Its sub-function enum is private, so a non-zero (reserved) sub-function byte still decodes but normalizes to the zero sub-function on re-encode (previously preserved). Per ISO 14229 the sub-function is always0x00, so this is a deliberate, minor behavior change.Review feedback addressed
0x80): fixed via theSecurityAccessLevelnewtype above. Audited the rest of the baseline — of the sixSuppressablePositiveResponse-wrapped enums this was the only remaining unlocked case; other raw-int enum variants (DIDs, NRCs, DTC numbers) span the full valid integer range and steal no bit, so carry no comparable invariant.Type::try_fromlinks: verified not an issue —cargo doc --all-featureswith-D rustdoc::broken_intra_doc_linksbuilds clean; the links resolve to the unambiguousTryFrom<u8>impl.Test plan
cargo test— 143 + 3 + doctests passcargo build --all-featuresandcargo build --no-default-features(no_std) both cleancargo clippy --all-targets --all-featurescleancargo doc --all-featuresclean with-D rustdoc::broken_intra_doc_links🤖 Generated with Claude Code