From f022b01a97ba3184c13035aef23b82c8c62ae33c Mon Sep 17 00:00:00 2001 From: Matt Jones <47545907+SoundMatt@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:21:55 -0700 Subject: [PATCH] fix: enforce LIN_MAX_DATA_LEN on Node::send()/Bus::publish(), sync version, document ldf/safety modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relay::Node::send() and Bus::publish() (VirtualBus, MockBus) never enforced the 8-byte LIN_MAX_DATA_LEN payload limit, so ErrPayloadTooLarge (spec §5.1) was dead code on the real violation path. Worse, from_message collapsed every conversion failure (bad ID, wrong protocol) into PayloadTooLarge, misusing the sentinel for unrelated errors. - LinAdapter::send now checks msg.payload.len() against LIN_MAX_DATA_LEN directly and returns ErrPayloadTooLarge, independent of the underlying Bus implementation. - from_message/publish conversion failures now route through Error::kind() instead of being force-mapped to PayloadTooLarge. - VirtualBus::publish_with_type and MockBus::publish now validate payload length and return Error::PayloadTooLarge for real violations, distinct from validate_frame's ErrInvalidFrame (spec §5.3). - Added regression tests for all three call sites. Also: - Cargo.toml version bumped from the stale 0.1.0 placeholder to 0.4.1, matching the actual release; ARCHITECTURE.md's title no longer duplicates a version number that drifts out of sync. - README module table now documents the ldf, safety, and slave modules that were already public but undocumented. Closes #4 Closes #5 Closes #6 Signed-off-by: Matt Jones <47545907+SoundMatt@users.noreply.github.com> --- ARCHITECTURE.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 3 ++ src/adapt.rs | 67 ++++++++++++++++++++++++++++++++++++------ src/mock/mod.rs | 22 +++++++++++++- src/virtual_bus/mod.rs | 25 +++++++++++++++- 7 files changed, 109 insertions(+), 14 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d697aeb..a2d2a2b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# Architecture — rust-LIN v0.2.0 +# Architecture — rust-LIN ## Overview diff --git a/Cargo.lock b/Cargo.lock index 45728ab..75c1179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,7 +400,7 @@ dependencies = [ [[package]] name = "rust-lin" -version = "0.1.0" +version = "0.4.1" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index 15a3fe1..165bdf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-lin" -version = "0.1.0" +version = "0.4.1" edition = "2021" description = "rust-LIN: Rust library for LIN bus (Local Interconnect Network) — LIN 2.x, virtual bus, LDF parser, master/slave nodes, safety E2E" license = "MPL-2.0" diff --git a/README.md b/README.md index 8357ba6..90640e3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ The `Bus` and `MasterBus` traits are stable. Implementations are swappable witho | `virtual_bus` | In-process bus — zero OS dependencies, master+slave | All | | `mock` | Mock bus for unit testing with frame injection | All | | `master` | `MasterNode` — schedule table execution, callbacks | All | +| `slave` | `SlaveNode` — registers/removes slave response registrations | All | +| `ldf` | LIN Description File (LDF) parser — nodes, frames, signals, schedule tables | All | +| `safety` | ISO 26262 ASIL-B end-to-end (E2E) data protection — `Protector`/`Receiver` | All | | `adapt` | RELAY v1.11 adapter — `adapt()`, `to_message()`, `from_message()` | All | --- diff --git a/src/adapt.rs b/src/adapt.rs index 410d5ca..21d2276 100644 --- a/src/adapt.rs +++ b/src/adapt.rs @@ -15,7 +15,7 @@ use tokio::sync::mpsc; use crate::bus::Bus; use crate::error::Error; -use crate::frame::{ChecksumType, Frame, LIN_MAX_ID}; +use crate::frame::{ChecksumType, Frame, LIN_MAX_DATA_LEN, LIN_MAX_ID}; use crate::relay::{BackPressurePolicy, Context, Message, Protocol, SubscriberOptions}; // --------------------------------------------------------------------------- @@ -118,18 +118,24 @@ impl crate::relay::Node for LinAdapter { } /// Send a relay::Message by converting to a LIN publish call. + /// + /// Enforces `LIN_MAX_DATA_LEN` (§15.3) directly on the payload so + /// `ErrPayloadTooLarge` (§5.1) is returned for real oversized payloads, + /// independent of whichever `Bus` implementation is behind this adapter. + /// Structural conversion failures (bad ID, wrong protocol) are mapped via + /// `Error::kind()` rather than being collapsed into `PayloadTooLarge`. + //fusa:req REQ-LIN-021 + //fusa:req REQ-SEC-002 async fn send(&self, _ctx: Context, msg: Message) -> Result<(), crate::relay::Error> { - let frame = from_message(&msg).map_err(|_| crate::relay::Error::PayloadTooLarge)?; + if msg.payload.len() > LIN_MAX_DATA_LEN { + return Err(crate::relay::Error::PayloadTooLarge); + } + let frame = + from_message(&msg).map_err(|e| e.kind().unwrap_or(crate::relay::Error::Closed))?; self.bus .publish(frame.id, Some(frame.data)) .await - .map_err(|e| match e { - Error::Closed => crate::relay::Error::Closed, - Error::NotConnected => crate::relay::Error::NotConnected, - Error::Timeout => crate::relay::Error::Timeout, - Error::PayloadTooLarge => crate::relay::Error::PayloadTooLarge, - _ => crate::relay::Error::Closed, - }) + .map_err(|e| e.kind().unwrap_or(crate::relay::Error::Closed)) } /// Subscribe to the bus and forward frames as relay::Messages. @@ -292,4 +298,47 @@ mod tests { assert_eq!(published.len(), 1); assert_eq!(published[0].0, 0x10); } + + //fusa:test REQ-LIN-021 + //fusa:test REQ-SEC-002 + #[tokio::test] + async fn send_rejects_oversized_payload_with_payload_too_large() { + use crate::mock::MockBus; + let mock = Arc::new(MockBus::new()); + let node = adapt(mock.clone()); + + let big_payload = vec![0u8; 200]; // 25x over LIN_MAX_DATA_LEN (8) + let msg = Message::new(Protocol::Lin, "16", big_payload); + + let result = node.send(Context::background(), msg).await; + assert_eq!(result, Err(crate::relay::Error::PayloadTooLarge)); + // The oversized payload must never have reached the bus. + assert!(mock.published_responses().await.is_empty()); + } + + //fusa:test REQ-LIN-021 + #[tokio::test] + async fn send_malformed_id_does_not_return_payload_too_large() { + use crate::mock::MockBus; + let mock = Arc::new(MockBus::new()); + let node = adapt(mock.clone()); + + let msg = Message::new(Protocol::Lin, "not_a_number", vec![0x01]); + let result = node.send(Context::background(), msg).await; + + assert!(result.is_err()); + assert_ne!(result.unwrap_err(), crate::relay::Error::PayloadTooLarge); + } + + //fusa:test REQ-LIN-021 + #[tokio::test] + async fn send_bus_payload_too_large_propagates() { + use crate::mock::MockBus; + // A payload right at the LIN_MAX_DATA_LEN boundary passes the + // adapter's own check, but MockBus::publish must still enforce the + // limit for callers that bypass the adapter's fast-path. + let mock = Arc::new(MockBus::new()); + let err = mock.publish(0x10, Some(vec![0u8; 9])).await.unwrap_err(); + assert!(matches!(err, Error::PayloadTooLarge)); + } } diff --git a/src/mock/mod.rs b/src/mock/mod.rs index 4853655..83149e8 100644 --- a/src/mock/mod.rs +++ b/src/mock/mod.rs @@ -20,7 +20,8 @@ use tokio::sync::Mutex; use crate::bus::{Bus, FrameReceiver, MasterBus, SubInner}; use crate::error::Error; use crate::frame::{ - calc_checksum, protect_id, ChecksumType, Filter, Frame, ScheduleEntry, LIN_MAX_ID, + calc_checksum, protect_id, ChecksumType, Filter, Frame, ScheduleEntry, LIN_MAX_DATA_LEN, + LIN_MAX_ID, }; use crate::relay::{Context, SubscriberOptions}; @@ -93,6 +94,12 @@ impl Bus for MockBus { if self.closed.load(Ordering::SeqCst) { return Err(Error::Closed); } + // §5.1/§15.3: reject payloads over LIN_MAX_DATA_LEN with PayloadTooLarge. + if let Some(ref d) = data { + if d.len() > LIN_MAX_DATA_LEN { + return Err(Error::PayloadTooLarge); + } + } self.published.lock().await.push((id, data.clone())); let mut responses = self.responses.lock().await; match data { @@ -249,4 +256,17 @@ mod tests { let err = bus.publish(0x10, Some(vec![0x01])).await.unwrap_err(); assert!(matches!(err, Error::Closed)); } + + #[tokio::test] + async fn publish_rejects_oversized_payload() { + let bus = MockBus::new(); + let err = bus.publish(0x10, Some(vec![0u8; 9])).await.unwrap_err(); + assert!(matches!(err, Error::PayloadTooLarge)); + } + + #[tokio::test] + async fn publish_accepts_max_data_len() { + let bus = MockBus::new(); + bus.publish(0x10, Some(vec![0u8; 8])).await.unwrap(); + } } diff --git a/src/virtual_bus/mod.rs b/src/virtual_bus/mod.rs index a838e9a..3eb0777 100644 --- a/src/virtual_bus/mod.rs +++ b/src/virtual_bus/mod.rs @@ -25,7 +25,7 @@ use crate::bus::{Bus, FrameReceiver, HealthProvider, MasterBus, MetricsProvider, use crate::error::Error; use crate::frame::{ calc_checksum, protect_id, validate_frame, ChecksumType, Filter, Frame, ScheduleEntry, - LIN_MAX_ID, + LIN_MAX_DATA_LEN, LIN_MAX_ID, }; use crate::relay::{Context, Health, Metrics, SubscriberOptions}; @@ -165,6 +165,14 @@ impl VirtualBus { id, LIN_MAX_ID ))); } + // §5.1/§15.3: reject payloads over LIN_MAX_DATA_LEN with PayloadTooLarge + // (distinct from validate_frame's structural ErrInvalidFrame, per §5.3). + if let Some(ref d) = data { + if d.len() > LIN_MAX_DATA_LEN { + self.error_count.fetch_add(1, Ordering::Relaxed); + return Err(Error::PayloadTooLarge); + } + } let mut guard = self.inner.lock().await; match data { Some(d) => { @@ -440,6 +448,21 @@ mod tests { assert!(matches!(err, Error::InvalidFrame { .. })); } + //fusa:test REQ-VIRT-004 + #[tokio::test] + async fn publish_rejects_oversized_payload() { + let bus = VirtualBus::new(); + let err = bus.publish(0x10, Some(vec![0u8; 9])).await.unwrap_err(); + assert!(matches!(err, Error::PayloadTooLarge)); + } + + //fusa:test REQ-VIRT-004 + #[tokio::test] + async fn publish_accepts_max_data_len() { + let bus = VirtualBus::new(); + bus.publish(0x10, Some(vec![0u8; 8])).await.unwrap(); + } + //fusa:test REQ-VIRT-005 #[tokio::test] async fn publish_after_close_returns_error() {