diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d47c930..a60a34e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,21 @@ jobs: files: lcov.info fail_ci_if_error: true + no_std: + name: Build no_std (bare-metal) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: thumbv6m-none-eabi + - uses: Swatinem/rust-cache@v2 + - name: Build no_std + no_alloc + run: cargo build --no-default-features --target thumbv6m-none-eabi + - name: Build no_std + alloc + run: cargo build --no-default-features --features alloc --target thumbv6m-none-eabi + lint: name: Run Clippy runs-on: ubuntu-latest @@ -59,6 +74,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: rustup component add clippy - run: cargo clippy --all-features + - run: cargo clippy --no-default-features + - run: cargo clippy --no-default-features --features alloc format: name: Run Cargo Format diff --git a/Cargo.lock b/Cargo.lock index 546a136..67b2757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.20" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,33 +19,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", @@ -68,11 +68,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-embedded-io" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed6bb9472871706c9b1f648ca527031e33d647a95706d6ab5659f22ca28d419" +dependencies = [ + "byteorder", + "embedded-io", +] + [[package]] name = "clap" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -80,9 +90,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -92,9 +102,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -104,15 +114,21 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "embedded-io" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" [[package]] name = "equivalent" @@ -122,9 +138,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -134,75 +150,58 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", "serde", + "serde_core", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "once_cell" -version = "1.21.3" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - [[package]] name = "serde" version = "1.0.228" @@ -245,14 +244,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -263,9 +263,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -274,74 +274,43 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - [[package]] name = "uds_protocol" version = "0.1.0" dependencies = [ "bitmask-enum", - "byteorder", + "byteorder-embedded-io", "clap", + "embedded-io", "serde", "serde_bytes", "thiserror", - "tracing", "utoipa", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" @@ -374,80 +343,21 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 9628e1a..1b2682c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,15 +19,18 @@ authors = [ ] [features] +default = ["std"] +std = ["alloc", "byteorder-embedded-io/std", "embedded-io/std", "thiserror/std"] +alloc = ["embedded-io/alloc"] serde = ["dep:serde", "dep:serde_bytes"] utoipa = ["dep:utoipa"] clap = ["dep:clap"] [dependencies] bitmask-enum = "2" -byteorder = "1" -thiserror = "2" -tracing = "0.1" +byteorder-embedded-io = { version = "0.1", default-features = false, features = ["embedded-io"] } +embedded-io = { version = "0.7", default-features = false } +thiserror = { version = "2", default-features = false } # Optional dependencies serde = { version = "1", optional = true, features = ["derive"] } serde_bytes = { version = "0.11", optional = true } diff --git a/README.md b/README.md index 5e8ee2f..411b53a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # Unified Diagnostics Services (UDS) Protocol -This crate aims to offer an ergonomic implementation of the UDS protocol for tooling and test workloads in Rust. -Embedded support is an explicit, non-goal of this library. -It suppports both serialization and deserialization of UDS both protocol messages as well as custom data types. +This crate offers an ergonomic, `no_std`-friendly implementation of the UDS (ISO 14229) protocol codec in Rust. +It targets embedded ECU diagnostics and desktop tooling alike: encoding and decoding UDS protocol messages — and custom data types — with no required allocator and no async runtime. It is not in a complete state yet with the 0.1.0 release, please check back soon! [![Crates.io](https://img.shields.io/crates/v/uds_protocol.svg?style=for-the-badge)](https://crates.io/crates/uds_protocol) @@ -14,30 +13,80 @@ It is not in a complete state yet with the 0.1.0 release, please check back soon This library provides serialization and deserialization of UDS messages. It is based on the ISO 14229-1:2020 standard. -| Service Name | Request SID | Response SID | Support | -|-----------------------------------|-------------|--------------|---------| -| DiagnosticSessionControl | 0x10 | 0x50 | ✓ | -| ECUReset | 0x11 | 0x51 | ✓ | -| ClearDiagnosticInformation | 0x14 | 0x54 | ✓ | -| ReadDTCInformation | 0x19 | 0x59 | Partial | -| ReadDataByIdentifier | 0x22 | 0x62 | ✓ | -| ReadMemoryByAddress | 0x23 | 0x63 | | -| ReadScalingDataByIdentifier | 0x24 | 0x64 | | -| SecurityAccess | 0x27 | 0x67 | ✓ | -| CommunicationControl | 0x28 | 0x68 | ✓ | -| Authentication | 0x29 | 0x69 | | -| ReadDataByPeriodicIdentifier | 0x2A | 0x6A | | -| WriteDataByIdentifier | 0x2E | 0x6E | ✓ | -| InputOutputControlByIdentifier | 0x2F | 0x6F | | -| RoutineControl | 0x31 | 0x71 | ✓ | -| RequestDownload | 0x34 | 0x74 | ✓ | -| RequestUpload | 0x35 | 0x75 | | -| TransferData | 0x36 | 0x76 | ✓ | -| RequestTransferExit | 0x37 | 0x77 | ✓ | -| RequestFileTransfer | 0x38 | 0x78 | ✓ | -| WriteMemoryByAddress | 0x3D | 0x7D | | -| TesterPresent | 0x3E | 0x7E | ✓ | -| SecuredDataTransmission | 0x84 | 0xC4 | | -| ControlDTCSetting | 0x85 | 0xC5 | ✓ | -| ResponseOnEvent | 0x86 | 0xC6 | | -| LinkControl | 0x87 | 0xC7 | | +| Service Name | Request SID | Response SID | Support | +|---------------------------------------|-------------|--------------|---------| +| `DiagnosticSessionControl` | 0x10 | 0x50 | ✓ | +| `ECUReset` | 0x11 | 0x51 | ✓ | +| `ClearDiagnosticInformation` | 0x14 | 0x54 | ✓ | +| `ReadDTCInformation` | 0x19 | 0x59 | Partial | +| `ReadDataByIdentifier` | 0x22 | 0x62 | ✓ | +| `ReadMemoryByAddress` | 0x23 | 0x63 | | +| `ReadScalingDataByIdentifier` | 0x24 | 0x64 | | +| `SecurityAccess` | 0x27 | 0x67 | ✓ | +| `CommunicationControl` | 0x28 | 0x68 | ✓ | +| `Authentication` | 0x29 | 0x69 | | +| `ReadDataByPeriodicIdentifier` | 0x2A | 0x6A | | +| `WriteDataByIdentifier` | 0x2E | 0x6E | ✓ | +| `InputOutputControlByIdentifier` | 0x2F | 0x6F | | +| `RoutineControl` | 0x31 | 0x71 | ✓ | +| `RequestDownload` | 0x34 | 0x74 | ✓ | +| `RequestUpload` | 0x35 | 0x75 | | +| `TransferData` | 0x36 | 0x76 | ✓ | +| `RequestTransferExit` | 0x37 | 0x77 | ✓ | +| `RequestFileTransfer` | 0x38 | 0x78 | ✓ | +| `WriteMemoryByAddress` | 0x3D | 0x7D | | +| `TesterPresent` | 0x3E | 0x7E | ✓ | +| `SecuredDataTransmission` | 0x84 | 0xC4 | | +| `ControlDTCSetting` | 0x85 | 0xC5 | ✓ | +| `ResponseOnEvent` | 0x86 | 0xC6 | | +| `LinkControl` | 0x87 | 0xC7 | | + +## Integration + +`uds_protocol` is a synchronous, allocation-free codec. It owns no sockets, buffers, or +async runtime. To use it over any transport (`DoIP`, `UDSonIP`, ISO-TP, …): + +- **Decode** an inbound frame from the `&[u8]` you received. +- **Encode** an outbound frame into any `embedded_io::Write` (or a caller-owned buffer + sized with `encoded_size()`). + +Drive the I/O loop from your own sync or async layer — the crate never blocks or awaits. + +### Encode (build a request) + +```rust +use uds_protocol::{Encode, TesterPresentRequest}; + +let req = TesterPresentRequest::new(false); +let mut buf = [0u8; 8]; +let mut writer = buf.as_mut_slice(); +let written = Encode::encode(&req, &mut writer).unwrap(); +// `buf[..written]` is the wire frame, ready to hand to your transport. +``` + +### Decode (parse a response) + +```rust +use uds_protocol::{Decode, Response}; + +// `frame` is the &[u8] your transport handed you. +let frame = [0x7E, 0x00]; +let (response, _rest) = Response::decode(&frame).unwrap(); +``` + +The decoded value **borrows** from `frame`: it points into that buffer (like a `struct` +overlaid on a `char buf[]`) and is valid only while `frame` lives. Copy out any fields +you need to keep before the buffer is reused. + +## Service coverage + +These services decode into typed [`Request`]/[`Response`] variants: `DiagnosticSessionControl`, +`EcuReset`, `SecurityAccess`, `CommunicationControl`, `TesterPresent`, `ControlDTCSettings`, +`ReadDataByIdentifier`, `WriteDataByIdentifier`, `ClearDiagnosticInfo`, `ReadDTCInfo`, +`RoutineControl`, `RequestDownload`, `TransferData`, `RequestTransferExit`, `RequestFileTransfer`, +and `NegativeResponse`. + +All other services enumerated in [`UdsServiceType`] (e.g. `Authentication`, `ReadMemoryByAddress`, +`RequestUpload`, `ResponseOnEvent`) are not individually modeled. Frames for them decode into +[`Request::Other`] / [`Response::Other`], carrying the service type and raw payload bytes for +pass-through. diff --git a/docs/superpowers/plans/2026-06-01-no-std-api-alignment.md b/docs/superpowers/plans/2026-06-01-no-std-api-alignment.md new file mode 100644 index 0000000..2323cc6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-no-std-api-alignment.md @@ -0,0 +1,1166 @@ +# no_std API Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Align the `uds_protocol` public API with its revised scope — a pure, synchronous, `no_std`/`no_alloc` UDS codec — before publishing, so breaking changes happen once. + +**Architecture:** Remove the orphaned `DiagnosticDefinition` abstraction and all generic identifier machinery in favor of concrete types and raw payload byte slices. Make TX builders mirror the raw-bytes RX side, apply the Tx/Rx naming convention strictly, unify the unknown-service escape hatch into symmetric `Other` variants, and clean up the codec traits. Simplicity for C developers new to Rust is a first-class acceptance criterion. + +**Tech Stack:** Rust 2024, `no_std`, `embedded-io` (sync `Write`), `byteorder-embedded-io`, `thiserror` (no_std). No async, no allocation in the baseline. + +**Spec:** `docs/superpowers/specs/2026-06-01-no-std-api-alignment-design.md` + +**Execute tasks in order.** Each task ends green (builds + tests pass) and is committed independently. + +--- + +## File Structure + +Files created: +- `src/test_util.rs` — `#[cfg(test)]` helper asserting `encode` length equals `encoded_size()`. + +Files deleted: +- `src/protocol_definitions.rs` — `ProtocolIdentifier` / `ProtocolPayloadTx` / `ProtocolRoutinePayloadTx` are removed. + +Files modified (primary responsibility): +- `src/traits.rs` — remove `Identifier`/`RoutineIdentifier`/`impl_identifier!`, blanket impls, `DiagnosticDefinition`, and `Encode::is_positive_response_suppressed`; expand `Decode` docs. +- `src/lib.rs` — register `test_util`; drop removed re-exports + `UdsSpec`; add service-coverage docs. +- `src/common/diagnostic_identifier.rs` — direct `Encode`/`Decode` for `UDSIdentifier`/`UDSRoutineIdentifier`. +- `src/services/read_data_by_identifier.rs` — `ReadDataByIdentifierRequestTx<'d>` over `&'d [u16]`. +- `src/services/write_data_by_identifier.rs` — `WriteDataByIdentifierRequestTx<'d>` + `WriteDataByIdentifierResponse { identifier: u16 }`. +- `src/services/routine_control.rs` — `RoutineControlRequestTx<'d>` / `RoutineControlResponseTx<'d>`. +- `src/services/control_dtc_settings.rs` — inherent `suppress_positive_response()`. +- `src/request.rs` / `src/response.rs` — `Other { service, data }`; remove `UdsResponse`; inherent suppression. +- `src/error.rs` — remove `ServiceNotImplemented`. +- `README.md` — integration + borrow-model docs. + +--- + +## Task 1: Add the `encode`/`encoded_size` agreement test helper + +Created first so every later task can assert the invariant directly. + +**Files:** +- Create: `src/test_util.rs` +- Modify: `src/lib.rs` + +- [ ] **Step 1: Create the helper module** + +Create `src/test_util.rs`: + +```rust +//! Test-only helpers shared across the crate. + +use crate::Encode; + +/// Assert that an [`Encode`] value writes exactly `encoded_size()` bytes. +/// +/// Guards against the two methods drifting, which would corrupt callers that pre-size +/// a buffer from `encoded_size()`. +pub(crate) fn assert_encode_size_agrees(value: &T) { + let mut buf = [0u8; 512]; + let mut writer = buf.as_mut_slice(); + let written = value.encode(&mut writer).unwrap(); + assert_eq!( + written, + value.encoded_size(), + "encode wrote {written} bytes but encoded_size() reported {}", + value.encoded_size() + ); +} +``` + +- [ ] **Step 2: Register the module in `src/lib.rs`** + +After the `mod error;` line (near the other `mod` declarations), add: + +```rust +#[cfg(test)] +mod test_util; +``` + +- [ ] **Step 3: Build and test** + +Run: `cargo build && cargo test` +Expected: PASS — the helper is `#[cfg(test)]` and unused so far (a dead-code warning is acceptable until Task 2 uses it; if `#![warn]` escalates it, this resolves once Task 2 lands). + +- [ ] **Step 4: Commit** + +```bash +git add src/test_util.rs src/lib.rs +git commit -m "$(printf 'add encode/encoded_size agreement test helper\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 2: De-genericize `ReadDataByIdentifierRequestTx` to `&[u16]` + +**Files:** +- Modify: `src/services/read_data_by_identifier.rs` + +- [ ] **Step 1: Replace the file with a concrete `&[u16]` version** + +Replace the entire contents of `src/services/read_data_by_identifier.rs` with: + +```rust +//! `ReadDataByIdentifier` (0x22) service implementation +use crate::{Encode, Error, NegativeResponseCode}; + +const READ_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::ResponseTooLong, + NegativeResponseCode::ConditionsNotCorrect, + NegativeResponseCode::RequestOutOfRange, + NegativeResponseCode::SecurityAccessDenied, +]; + +/// Zero-alloc TX request to read data by identifier. Borrows the DID list from the caller. +/// +/// A Data Identifier is a 16-bit value, so the list is held as `&[u16]`; each DID is +/// written big-endian on the wire. +/// +/// See ISO-14229-1:2020, Table 11.2.1 for format information +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ReadDataByIdentifierRequestTx<'d> { + /// The list of Data Identifiers to read. + pub dids: &'d [u16], +} + +impl<'d> ReadDataByIdentifierRequestTx<'d> { + /// Create a new request from a slice of data identifiers. + #[must_use] + pub const fn new(dids: &'d [u16]) -> Self { + Self { dids } + } + + /// Get the allowed Nack codes for this request + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &READ_DID_NEGATIVE_RESPONSE_CODES + } +} + +impl Encode for ReadDataByIdentifierRequestTx<'_> { + fn encoded_size(&self) -> usize { + self.dids.len() * 2 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + for did in self.dids { + writer.write_all(&did.to_be_bytes()).map_err(Error::io)?; + } + Ok(self.encoded_size()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn encode_read_did_request_tx() { + let ids = [0xF180u16, 0xF186u16]; + let req = ReadDataByIdentifierRequestTx::new(&ids); + let mut buf = [0u8; 16]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 4); // 2 DIDs * 2 bytes each + assert_eq!(&buf[..4], &[0xF1, 0x80, 0xF1, 0x86]); + assert_encode_size_agrees(&req); + } +} +``` + +- [ ] **Step 2: Build and test** + +Run: `cargo build && cargo test read_data_by_identifier` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add src/services/read_data_by_identifier.rs +git commit -m "$(printf 'de-genericize ReadDataByIdentifierRequestTx to &[u16]\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 3: De-genericize `WriteDataByIdentifier` request + response + +**Files:** +- Modify: `src/services/write_data_by_identifier.rs` + +- [ ] **Step 1: Replace the file with concrete raw-bytes request + `u16` response** + +Replace the entire contents of `src/services/write_data_by_identifier.rs` with: + +```rust +//! `WriteDataByIdentifier` (0x2E) service implementation +use crate::{Encode, Error, NegativeResponseCode}; + +const WRITE_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ + NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, + NegativeResponseCode::ConditionsNotCorrect, + NegativeResponseCode::RequestOutOfRange, + NegativeResponseCode::SecurityAccessDenied, + NegativeResponseCode::GeneralProgrammingFailure, +]; + +/// Zero-alloc TX request to write data by identifier. Borrows the raw payload from the caller. +/// +/// The payload is the DID (2 bytes, big-endian) followed by the data record, exactly as +/// it appears on the wire after the service byte. +/// +/// See ISO-14229-1:2020, Section 11.7.2.1 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WriteDataByIdentifierRequestTx<'d> { + /// The raw payload bytes: DID followed by the data record. + pub payload: &'d [u8], +} + +impl<'d> WriteDataByIdentifierRequestTx<'d> { + /// Create a new write-by-identifier request from raw payload bytes. + #[must_use] + pub const fn new(payload: &'d [u8]) -> Self { + Self { payload } + } + + /// Get the allowed [`NegativeResponseCode`] variants for this request. + #[must_use] + pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { + &WRITE_DID_NEGATIVE_RESPONSE_CODES + } +} + +impl Encode for WriteDataByIdentifierRequestTx<'_> { + fn encoded_size(&self) -> usize { + self.payload.len() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(self.payload).map_err(Error::io)?; + Ok(self.payload.len()) + } +} + +/// Positive response to `WriteDataByIdentifier`: echoes the DID that was written. +/// +/// See ISO-14229-1:2020, Section 11.7.3.1 +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct WriteDataByIdentifierResponse { + /// The DID that was written to. + pub identifier: u16, +} + +impl WriteDataByIdentifierResponse { + /// Create a new response echoing the identifier that was written. + #[must_use] + pub const fn new(identifier: u16) -> Self { + Self { identifier } + } +} + +impl Encode for WriteDataByIdentifierResponse { + fn encoded_size(&self) -> usize { + 2 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&self.identifier.to_be_bytes()) + .map_err(Error::io)?; + Ok(2) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn test_write_response_encode() { + let response = WriteDataByIdentifierResponse::new(0xBEEF); + let mut buf = [0u8; 4]; + let written = Encode::encode(&response, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 2); + assert_eq!(buf[0], 0xBE); + assert_eq!(buf[1], 0xEF); + assert_encode_size_agrees(&response); + } + + #[test] + fn test_write_request_encode() { + // DID 0xF186 + one data byte 0x01 + let payload = [0xF1, 0x86, 0x01]; + let request = WriteDataByIdentifierRequestTx::new(&payload); + let mut buf = [0u8; 8]; + let written = Encode::encode(&request, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 3); + assert_eq!(&buf[..3], &[0xF1, 0x86, 0x01]); + assert_encode_size_agrees(&request); + } +} +``` + +- [ ] **Step 2: Build and test** + +Run: `cargo build && cargo test write_data_by_identifier` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add src/services/write_data_by_identifier.rs +git commit -m "$(printf 'de-genericize WriteDataByIdentifier to raw bytes + u16 response\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 4: De-genericize `RoutineControl` and rename to `...Tx` + +**Files:** +- Modify: `src/services/routine_control.rs` +- Modify: `src/services/mod.rs` (only if it names the old types explicitly) + +- [ ] **Step 1: Replace the file with concrete raw-bytes `...Tx` types** + +Replace the entire contents of `src/services/routine_control.rs` with: + +```rust +//! Routine Control (0x31) Service is used to perform functions on the ECU that may not be covered by other services. +//! +//! It can also be used to check the ECU's health, erase memory, or other custom manufacturer/supplier routines. +//! However, some routines may have side effects or require certain preconditions to be met. +use crate::{Encode, Error, RoutineControlSubFunction}; + +/// Used by a client to execute a defined sequence of events and obtain any relevant results. +/// +/// The payload is the routine identifier (2 bytes, big-endian) followed by any optional +/// routine input parameters, exactly as it appears on the wire after the sub-function byte. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct RoutineControlRequestTx<'d> { + /// The routine control operation (start, stop, or request results). + pub sub_function: RoutineControlSubFunction, + /// The raw payload bytes: routine identifier followed by optional parameters. + pub raw_payload: &'d [u8], +} + +impl<'d> RoutineControlRequestTx<'d> { + /// Create a new `RoutineControlRequestTx`. + #[must_use] + pub const fn new(sub_function: RoutineControlSubFunction, raw_payload: &'d [u8]) -> Self { + Self { + sub_function, + raw_payload, + } + } +} + +impl Encode for RoutineControlRequestTx<'_> { + fn encoded_size(&self) -> usize { + 1 + self.raw_payload.len() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.sub_function)]) + .map_err(Error::io)?; + writer.write_all(self.raw_payload).map_err(Error::io)?; + Ok(self.encoded_size()) + } +} + +/// `RoutineControlResponseTx` is a variable-length response that can contain routine status. +/// +/// The status record is the routine identifier echo plus any routine-info / status bytes, +/// held as raw bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct RoutineControlResponseTx<'d> { + /// The sub-function echoed from the routine control request. + pub routine_control_type: RoutineControlSubFunction, + /// Raw routine status record bytes (routine identifier + routine info + status). + pub raw_status_record: &'d [u8], +} + +impl<'d> RoutineControlResponseTx<'d> { + /// Create a new `RoutineControlResponseTx`. + #[must_use] + pub const fn new( + routine_control_type: RoutineControlSubFunction, + raw_status_record: &'d [u8], + ) -> Self { + Self { + routine_control_type, + raw_status_record, + } + } +} + +impl Encode for RoutineControlResponseTx<'_> { + fn encoded_size(&self) -> usize { + 1 + self.raw_status_record.len() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.routine_control_type)]) + .map_err(Error::io)?; + writer + .write_all(self.raw_status_record) + .map_err(Error::io)?; + Ok(self.encoded_size()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn encode_routine_control_request_tx() { + // RID 0xFF00 (EraseMemory) + 1 parameter byte + let payload = [0xFF, 0x00, 0xAA]; + let req = RoutineControlRequestTx::new(RoutineControlSubFunction::StartRoutine, &payload); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x01, 0xFF, 0x00, 0xAA]); + assert_encode_size_agrees(&req); + } + + #[test] + fn encode_routine_control_response_tx() { + let record = [0xFF, 0x00, 0x10]; + let resp = + RoutineControlResponseTx::new(RoutineControlSubFunction::StartRoutine, &record); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x01, 0xFF, 0x00, 0x10]); + assert_encode_size_agrees(&resp); + } +} +``` + +- [ ] **Step 2: Update `services/mod.rs` if it names the old types** + +Run: `grep -n "RoutineControl" src/services/mod.rs` +If it re-exports explicit names (e.g. `pub use routine_control::{RoutineControlRequest, RoutineControlResponse};`), rename them to `RoutineControlRequestTx, RoutineControlResponseTx`. If it uses `pub use routine_control::*;`, no change is needed. + +- [ ] **Step 3: Build and test** + +Run: `cargo build && cargo test routine_control` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add src/services/routine_control.rs src/services/mod.rs +git commit -m "$(printf 'de-genericize RoutineControl to raw-bytes RoutineControl*Tx types\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 5: Remove `DiagnosticDefinition` and `UdsSpec` + +**Files:** +- Modify: `src/traits.rs` +- Modify: `src/lib.rs` + +- [ ] **Step 1: Delete the `DiagnosticDefinition` trait from `src/traits.rs`** + +Remove this entire block (currently around lines 143–154): + +```rust +/// Trait for diagnostic definitions that specifies the identifier and payload +/// types used when constructing and parsing UDS requests and responses. +pub trait DiagnosticDefinition<'a> { + /// UDS Data Identifier type. + type DID: Identifier + Clone + core::fmt::Debug + PartialEq + 'static; + /// Payload type for read/write data by identifier etc. + type DiagnosticPayload: Encode + Clone + core::fmt::Debug + PartialEq + 'a; + /// UDS Routine Identifier type. + type RID: RoutineIdentifier + Clone + core::fmt::Debug + PartialEq + 'static; + /// Payload type for routine control requests/responses. + type RoutinePayload: Encode + Clone + core::fmt::Debug + PartialEq + 'a; +} +``` + +- [ ] **Step 2: Delete `UdsSpec` and its impl from `src/lib.rs`** + +Remove the `UdsSpec` struct (around lines 38–45) and its `impl<'a> DiagnosticDefinition<'a> for UdsSpec { ... }` block (around lines 47–52). + +- [ ] **Step 3: Update the `traits` re-export in `src/lib.rs`** + +Change: + +```rust +pub use traits::{Decode, DecodeIter, DiagnosticDefinition, Encode, Identifier, RoutineIdentifier}; +``` + +to: + +```rust +pub use traits::{Decode, DecodeIter, Encode, Identifier, RoutineIdentifier}; +``` + +(`Identifier`/`RoutineIdentifier` are removed in Task 7; leaving them here keeps this commit compiling.) + +- [ ] **Step 4: Build and test** + +Run: `cargo build && cargo test` +Expected: PASS — nothing consumed `DiagnosticDefinition` or `UdsSpec`. + +- [ ] **Step 5: Commit** + +```bash +git add src/traits.rs src/lib.rs +git commit -m "$(printf 'remove orphaned DiagnosticDefinition trait and UdsSpec\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 6: Delete `protocol_definitions.rs` + +**Files:** +- Delete: `src/protocol_definitions.rs` +- Modify: `src/lib.rs` + +- [ ] **Step 1: Confirm no remaining references outside the module itself** + +Run: `grep -rn "ProtocolIdentifier\|ProtocolPayloadTx\|ProtocolRoutinePayloadTx\|protocol_definitions" src/ | grep -v "src/protocol_definitions.rs"` +Expected: only the two lines in `src/lib.rs` (the `mod` + `pub use`). Tasks 3–4 already removed the `ProtocolPayloadTx` test usages. If any other reference appears, stop and fix it first. + +- [ ] **Step 2: Delete the module file** + +Run: `git rm src/protocol_definitions.rs` + +- [ ] **Step 3: Remove the module declaration and re-export from `src/lib.rs`** + +Delete these two lines: + +```rust +mod protocol_definitions; +pub use protocol_definitions::{ProtocolIdentifier, ProtocolPayloadTx, ProtocolRoutinePayloadTx}; +``` + +- [ ] **Step 4: Build and test** + +Run: `cargo build && cargo test` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add -A src/protocol_definitions.rs src/lib.rs +git commit -m "$(printf 'delete protocol_definitions module (ProtocolIdentifier/PayloadTx)\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 7: Remove identifier machinery; add direct codec to UDS identifiers + +This task is atomic: the blanket `impl` and a direct `impl` for `UDSIdentifier` cannot coexist (coherence conflict), so the traits and direct impls must change together. + +**Files:** +- Modify: `src/traits.rs` +- Modify: `src/common/diagnostic_identifier.rs` +- Modify: `src/lib.rs` + +- [ ] **Step 1: Remove identifier traits, macro, and blanket impls from `src/traits.rs`** + +Delete the `Identifier` trait, the `impl_identifier!` macro, the `RoutineIdentifier` trait, and all three blanket impls (`Encode`, `Decode`, `DecodeIter` for `T: Identifier`) — currently around lines 70–141. Also delete the `traits.rs` test module's `MyIdentifier` enum and its two identifier tests (around lines 156–222), since they depend on `impl_identifier!`. Keep the `Encode`, `Decode`, `DecodeIter` trait definitions. + +- [ ] **Step 2: Add direct `Encode`/`Decode` impls for the UDS identifier enums** + +In `src/common/diagnostic_identifier.rs`, change the import line: + +```rust +use crate::{Error, impl_identifier, traits::RoutineIdentifier}; +``` + +to: + +```rust +use crate::{Decode, Encode, Error}; +``` + +Delete the two `impl_identifier!(UDSIdentifier);` / `impl_identifier!(UDSRoutineIdentifier);` lines and the `impl RoutineIdentifier for UDSRoutineIdentifier {}` line. + +Add, at the end of the file: + +```rust +impl Encode for UDSIdentifier { + fn encoded_size(&self) -> usize { + 2 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&u16::from(*self).to_be_bytes()) + .map_err(Error::io)?; + Ok(2) + } +} + +impl<'a> Decode<'a> for UDSIdentifier { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + let raw = u16::from_be_bytes([buf[0], buf[1]]); + Ok((Self::try_from(raw)?, &buf[2..])) + } +} + +impl Encode for UDSRoutineIdentifier { + fn encoded_size(&self) -> usize { + 2 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&u16::from(*self).to_be_bytes()) + .map_err(Error::io)?; + Ok(2) + } +} + +impl<'a> Decode<'a> for UDSRoutineIdentifier { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + let raw = u16::from_be_bytes([buf[0], buf[1]]); + Ok((Self::from(raw), &buf[2..])) + } +} + +#[cfg(test)] +mod codec_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn uds_identifier_roundtrip() { + let id = UDSIdentifier::ActiveDiagnosticSession; + let mut buf = [0u8; 2]; + Encode::encode(&id, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xF1, 0x86]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, id); + assert!(rest.is_empty()); + assert_encode_size_agrees(&id); + } + + #[test] + fn uds_routine_identifier_roundtrip() { + let id = UDSRoutineIdentifier::EraseMemory; + let mut buf = [0u8; 2]; + Encode::encode(&id, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xFF, 0x00]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, id); + assert!(rest.is_empty()); + assert_encode_size_agrees(&id); + } +} +``` + +- [ ] **Step 3: Update the `traits` re-export in `src/lib.rs`** + +Change: + +```rust +pub use traits::{Decode, DecodeIter, Encode, Identifier, RoutineIdentifier}; +``` + +to: + +```rust +pub use traits::{Decode, DecodeIter, Encode}; +``` + +- [ ] **Step 4: Build and test** + +Run: `cargo build && cargo test diagnostic_identifier` +Expected: PASS. Then `cargo build` (full crate) to confirm nothing else referenced the removed traits. + +- [ ] **Step 5: Commit** + +```bash +git add src/traits.rs src/common/diagnostic_identifier.rs src/lib.rs +git commit -m "$(printf 'remove Identifier machinery; add direct codec to UDS identifiers\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 8: Move `is_positive_response_suppressed` off the `Encode` trait + +**Files:** +- Modify: `src/traits.rs` +- Modify: `src/services/ecu_reset.rs`, `control_dtc_settings.rs`, `tester_present.rs`, `security_access.rs`, `diagnostic_session_control.rs` +- Modify: `src/request.rs` + +- [ ] **Step 1: Remove the method from the `Encode` trait** + +In `src/traits.rs`, delete from the `Encode` trait: + +```rust + /// Whether the positive response for this message is suppressed (SPRMIB). + fn is_positive_response_suppressed(&self) -> bool { + false + } +``` + +- [ ] **Step 2: Remove the trait-method overrides from each service `Encode` impl** + +In each of `ecu_reset.rs`, `control_dtc_settings.rs`, `tester_present.rs`, `security_access.rs`, `diagnostic_session_control.rs`, delete the `fn is_positive_response_suppressed(&self) -> bool { ... }` block from inside its `impl Encode for ...` block. Confirm with: `grep -rn "fn is_positive_response_suppressed" src/services/` — expect no matches afterward. + +- [ ] **Step 3: Add an inherent `suppress_positive_response()` to `ControlDTCSettingsRequest`** + +`ControlDTCSettingsRequest` exposes a public `suppress_response` field but no getter matching the other services. In `src/services/control_dtc_settings.rs`, add to its inherent `impl ControlDTCSettingsRequest` block: + +```rust + /// Whether the server should suppress the positive response (SPRMIB). + #[must_use] + pub const fn suppress_positive_response(&self) -> bool { + self.suppress_response + } +``` + +(ecu_reset, tester_present, security_access, diagnostic_session_control, and communication_control already have inherent `suppress_positive_response()` getters.) + +- [ ] **Step 4: Replace the `Request` trait override with an inherent method** + +In `src/request.rs`, remove the `fn is_positive_response_suppressed(&self) -> bool { ... }` block from the `impl Encode for Request<'_>` block, and add this inherent method to the `impl Request<'_>` block (the one that also defines `service`): + +```rust + /// Whether the positive response for this request is suppressed (SPRMIB). + #[must_use] + pub fn is_positive_response_suppressed(&self) -> bool { + match self { + Self::CommunicationControl(req) => req.suppress_positive_response(), + Self::ControlDTCSettings(req) => req.suppress_positive_response(), + Self::DiagnosticSessionControl(req) => req.suppress_positive_response(), + Self::EcuReset(req) => req.suppress_positive_response(), + Self::SecurityAccess(req) => req.suppress_positive_response(), + Self::TesterPresent(req) => req.suppress_positive_response(), + _ => false, + } + } +``` + +The existing `suppression_forwards_to_inner_request` test calls `.is_positive_response_suppressed()` and now resolves to this inherent method, so it is unchanged. + +- [ ] **Step 5: Build and test** + +Run: `cargo build && cargo test` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/traits.rs src/request.rs src/services/ +git commit -m "$(printf 'move is_positive_response_suppressed off the Encode trait\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 9: Add symmetric `Other` escape hatch; remove `UdsResponse` and `ServiceNotImplemented` + +**Files:** +- Modify: `src/request.rs` +- Modify: `src/response.rs` +- Modify: `src/error.rs` +- Modify: `src/lib.rs` + +- [ ] **Step 1: Write failing tests for the `Other` variants** + +In `src/request.rs` `mod tests`, add: + +```rust + #[test] + fn unmodeled_service_decodes_to_other() { + // 0x23 = ReadMemoryByAddress, enumerated but not modeled. + let frame = [0x23, 0xAA, 0xBB]; + let (req, rest) = Request::decode(&frame).unwrap(); + assert!(rest.is_empty()); + match req { + Request::Other { service, data } => { + assert_eq!(service, UdsServiceType::ReadMemoryByAddress); + assert_eq!(data, &[0xAA, 0xBB]); + } + other => panic!("expected Other, got {other:?}"), + } + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &frame); + } +``` + +In `src/response.rs`, add a test module: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unmodeled_response_decodes_to_other() { + // 0x63 = ReadMemoryByAddress positive response, not modeled. + let frame = [0x63, 0x01, 0x02]; + let (resp, rest) = Response::decode(&frame).unwrap(); + assert!(rest.is_empty()); + match resp { + Response::Other { service, data } => { + assert_eq!(service, UdsServiceType::ReadMemoryByAddress); + assert_eq!(data, &[0x01, 0x02]); + } + other => panic!("expected Other, got {other:?}"), + } + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &frame); + } +} +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `cargo test unmodeled` +Expected: FAIL — no `Other` variant exists. + +- [ ] **Step 3: Add `Other` to `Request<'a>`** + +In `src/request.rs`, add to the `Request<'a>` enum: + +```rust + /// A known-but-unmodeled (or unrecognized) service. Carries the service type and + /// the raw payload bytes following the service identifier, for pass-through. + Other { + /// The service this frame addresses. + service: UdsServiceType, + /// Raw payload bytes after the service byte. + data: &'a [u8], + }, +``` + +In `Request::decode`, replace `_ => return Err(Error::ServiceNotImplemented(service)),` with: + +```rust + _ => Self::Other { + service, + data: payload, + }, +``` + +In `Request::encoded_size`, add to the payload match: `Self::Other { data, .. } => data.len(),` + +In `Request::encode`, add to the payload match: + +```rust + Self::Other { data, .. } => { + writer.write_all(data).map_err(Error::io)?; + data.len() + } +``` + +In `Request::service`, add: `Self::Other { service, .. } => *service,` + +- [ ] **Step 4: Add `Other` to `Response<'a>`; remove `UdsResponse`** + +In `src/response.rs`, add to the `Response<'a>` enum: + +```rust + /// A known-but-unmodeled (or unrecognized) service response. Carries the service + /// type and the raw payload bytes following the service identifier. + Other { + /// The service this response addresses. + service: UdsServiceType, + /// Raw payload bytes after the service byte. + data: &'a [u8], + }, +``` + +In `Response::decode`, replace `_ => return Err(Error::ServiceNotImplemented(service)),` with: + +```rust + _ => Self::Other { + service, + data: payload, + }, +``` + +In `Response::response_sid`, add: `Self::Other { service, .. } => service.response_to_byte(),` + +In `Response::encoded_size`, add: `Self::Other { data, .. } => data.len(),` + +In `Response::encode`, add to the payload match: + +```rust + Self::Other { data, .. } => { + writer.write_all(data).map_err(Error::io)?; + data.len() + } +``` + +Delete the entire `UdsResponse<'a>` struct and its `impl<'a> Decode<'a> for UdsResponse<'a>` (currently lines ~206–228). + +- [ ] **Step 5: Remove the `UdsResponse` re-export from `src/lib.rs`** + +Change `pub use response::{Response, UdsResponse};` to `pub use response::Response;` + +- [ ] **Step 6: Remove `ServiceNotImplemented` from `src/error.rs`** + +Delete: + +```rust + /// The service type is not yet implemented in this crate. + #[error("UDS service not implemented: {0:?}")] + ServiceNotImplemented(crate::UdsServiceType), +``` + +- [ ] **Step 7: Run tests** + +Run: `cargo test` +Expected: PASS, including the new `unmodeled` tests. Confirm no stragglers: `grep -rn "UdsResponse\|ServiceNotImplemented" src/` — expect no matches. + +- [ ] **Step 8: Commit** + +```bash +git add src/request.rs src/response.rs src/error.rs src/lib.rs +git commit -m "$(printf 'add symmetric Other escape hatch; drop UdsResponse + ServiceNotImplemented\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 10: Document the `Decode` remainder / borrow contract + +**Files:** +- Modify: `src/traits.rs` + +- [ ] **Step 1: Expand the `Decode` trait doc comment** + +Replace the existing `Decode` trait doc comment (the `/// RX-side trait: zero-copy decode ...` block above `pub trait Decode<'a>`) with: + +```rust +/// RX-side trait: zero-copy decode from a byte slice. +/// +/// Implementations borrow directly from the input buffer where possible. The decoded +/// value points into `buf` and is valid only as long as `buf` lives — for C developers +/// new to Rust, think of it like a `struct` overlaid on a `char buf[]`. Copy out any +/// fields you need to retain beyond the buffer's lifetime. +/// +/// [`decode`](Self::decode) returns the value together with the unconsumed remainder of +/// the buffer, so leaf and sequence decoders can be composed. Frame-level decoders +/// (`Request`, `Response`) consume the whole buffer and return an empty remainder; use +/// [`decode_exact`](Self::decode_exact) when a buffer must contain exactly one value. +``` + +- [ ] **Step 2: Build docs** + +Run: `cargo build && cargo doc --no-deps` +Expected: PASS, no rustdoc warnings. + +- [ ] **Step 3: Commit** + +```bash +git add src/traits.rs +git commit -m "$(printf 'document the Decode remainder / borrow contract\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 11: README integration + borrow-model docs + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Append an Integration section to `README.md`** + +Add at the end of `README.md`: + +````markdown +## Integration + +`uds_protocol` is a synchronous, allocation-free codec. It owns no sockets, buffers, or +async runtime. To use it over any transport (DoIP, UDSonIP, ISO-TP, …): + +- **Decode** an inbound frame from the `&[u8]` you received. +- **Encode** an outbound frame into any `embedded_io::Write` (or a caller-owned buffer + sized with `encoded_size()`). + +Drive the I/O loop from your own sync or async layer — the crate never blocks or awaits. + +### Encode (build a request) + +```rust +use uds_protocol::{Encode, TesterPresentRequest}; + +let req = TesterPresentRequest::new(false); +let mut buf = [0u8; 8]; +let mut writer = buf.as_mut_slice(); +let written = Encode::encode(&req, &mut writer).unwrap(); +// `buf[..written]` is the wire frame, ready to hand to your transport. +``` + +### Decode (parse a response) + +```rust +use uds_protocol::{Decode, Response}; + +// `frame` is the &[u8] your transport handed you. +let frame = [0x7E, 0x00]; +let (response, _rest) = Response::decode(&frame).unwrap(); +``` + +The decoded value **borrows** from `frame`: it points into that buffer (like a `struct` +overlaid on a `char buf[]`) and is valid only while `frame` lives. Copy out any fields +you need to keep before the buffer is reused. +```` + +- [ ] **Step 2: Verify the doctests compile (README is included as crate docs)** + +Run: `cargo test --doc` +Expected: PASS — `src/lib.rs` includes `README.md` via `#![doc = include_str!(...)]`, so the snippets run as doctests. If a referenced symbol (e.g. `TesterPresentRequest`) is not exported at the crate root, adjust the snippet's import path to match the actual re-export. + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "$(printf 'document runtime-agnostic integration model and borrow semantics\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 12: Apply the agreement helper to the remaining service tests + +**Files:** +- Modify: `src/services/ecu_reset.rs`, `tester_present.rs`, `communication_control.rs`, `diagnostic_session_control.rs`, `clear_dtc_information.rs`, `security_access.rs`, `request_download.rs`, `transfer_data.rs`, `request_file_transfer.rs`, `control_dtc_settings.rs`, `negative_response.rs` + +- [ ] **Step 1: Add an agreement assertion to each service's existing encode test** + +For each module above: add `use crate::test_util::assert_encode_size_agrees;` to the test module's imports, and append `assert_encode_size_agrees(&);` to the existing test that builds and encodes a value (reuse the request/response value already constructed there). Example for `ecu_reset.rs`: + +```rust + use crate::test_util::assert_encode_size_agrees; + // ...inside the existing encode test, after `request` is built: + assert_encode_size_agrees(&request); +``` + +If a module has no encode test that builds a value, add a minimal one using that file's `new` constructor and an appropriately sized stack buffer. + +- [ ] **Step 2: Test** + +Run: `cargo test` +Expected: PASS. A failure here is a real `encode`/`encoded_size` mismatch — fix the offending `encoded_size` to match the bytes `encode` writes, then re-run. + +- [ ] **Step 3: Commit** + +```bash +git add src/services/ +git commit -m "$(printf 'assert encode/encoded_size agreement across all services\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 13: Document the service-coverage boundary + +**Files:** +- Modify: `src/lib.rs` + +- [ ] **Step 1: Add a crate-root doc block listing modeled vs pass-through services** + +In `src/lib.rs`, add this doc comment immediately above the `pub const SUCCESS: u8 = 0x80;` declaration: + +```rust +/// ## Service coverage +/// +/// These services decode into typed [`Request`]/[`Response`] variants: +/// `DiagnosticSessionControl`, `EcuReset`, `SecurityAccess`, `CommunicationControl`, +/// `TesterPresent`, `ControlDTCSettings`, `ReadDataByIdentifier`, `WriteDataByIdentifier`, +/// `ClearDiagnosticInfo`, `ReadDTCInfo`, `RoutineControl`, `RequestDownload`, +/// `TransferData`, `RequestTransferExit`, `RequestFileTransfer`, and `NegativeResponse`. +/// +/// All other services enumerated in [`UdsServiceType`] (e.g. `Authentication`, +/// `ReadMemoryByAddress`, `RequestUpload`, `ResponseOnEvent`) are not individually +/// modeled. Frames for them decode into [`Request::Other`] / [`Response::Other`], +/// carrying the service type and raw payload bytes for pass-through. +``` + +- [ ] **Step 2: Build docs** + +Run: `cargo doc --no-deps` +Expected: PASS, no warnings, no broken intra-doc links. + +- [ ] **Step 3: Commit** + +```bash +git add src/lib.rs +git commit -m "$(printf 'document the modeled vs pass-through service coverage boundary\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 14: Full verification across the feature matrix + +**Files:** none (verification only) + +- [ ] **Step 1: Host builds across feature combos** + +```bash +cargo build +cargo build --no-default-features --features alloc +cargo build --no-default-features +``` +Expected: all succeed. + +- [ ] **Step 2: Tests** + +Run: `cargo test` +Expected: all pass (pre-refactor baseline was 87; expect that plus the new `Other`, identifier-codec, and size-agreement tests). + +- [ ] **Step 3: Clippy across host combos (crate sets `#![warn(clippy::pedantic, missing_docs)]`)** + +```bash +cargo clippy --all-targets +cargo clippy --no-default-features --features alloc --all-targets +cargo clippy --no-default-features --all-targets +``` +Expected: zero warnings. + +- [ ] **Step 4: Bare-metal target** + +```bash +cargo build --no-default-features --target thumbv6m-none-eabi +cargo build --no-default-features --features alloc --target thumbv6m-none-eabi +``` +Expected: success. (If missing: `rustup target add thumbv6m-none-eabi`.) + +- [ ] **Step 5: Confirm no orphaned references remain** + +Run: `grep -rn "DiagnosticDefinition\|UdsSpec\|ProtocolIdentifier\|ProtocolPayloadTx\|impl_identifier\|ServiceNotImplemented\|UdsResponse\|: Identifier\|: RoutineIdentifier" src/` +Expected: no matches. + +- [ ] **Step 6: Commit any verification fixes** + +If steps 1–5 required changes (clippy fixes, doc tweaks), commit them; otherwise nothing to commit: + +```bash +git add -A +git commit -m "$(printf 'verification fixes across the no_std feature matrix\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Self-Review Notes + +**Spec coverage:** Decision 1 → Task 5; Decision 2 → Tasks 6–7; Decision 3 → Tasks 2–4; Decision 4 (naming) → Task 4 (RoutineControl rename) + Task 3 (WriteDataByIdentifierRequestTx), already-conforming types unchanged; Decision 5 → Task 9; Decision 6 → Task 8; Decision 7 → Tasks 1 + 12; Decision 8 → Task 10; Decision 9 → Task 11; Decision 10 → Task 13. Open item (`ServiceNotImplemented` fate) resolved in Task 9: removed, fully subsumed by `Other`. + +**Type consistency:** `assert_encode_size_agrees` (Task 1) is used identically everywhere. New public names — `ReadDataByIdentifierRequestTx<'d>` (`&[u16]`), `WriteDataByIdentifierRequestTx<'d>` / `WriteDataByIdentifierResponse { identifier: u16 }`, `RoutineControlRequestTx<'d>` / `RoutineControlResponseTx<'d>`, `Request::Other { service, data }` / `Response::Other { service, data }` — are referenced consistently across tasks. + +**Verification:** Task 14 enforces the same matrix the existing CI uses (three host feature combos + `thumbv6m-none-eabi` + clippy), so the refactor lands against the project's real definition of green. diff --git a/docs/superpowers/plans/2026-06-03-api-consistency-phase2.md b/docs/superpowers/plans/2026-06-03-api-consistency-phase2.md new file mode 100644 index 0000000..38104ce --- /dev/null +++ b/docs/superpowers/plans/2026-06-03-api-consistency-phase2.md @@ -0,0 +1,862 @@ +# API Consistency — Phase 2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Finish the cohesive no_std breaking change: drop the misleading `…Tx`/`…Rx` suffixes from bidirectional descriptor types, wrap the remaining raw enum variants in their descriptors (RDBI excepted), and remove the duplicated variable-length integer codec and the duplicate primitive macros. + +**Architecture:** Seven commits. (1) Rename 13 bidirectional types (drop suffix). (2) Extract a `read_be_uint`/`write_be_uint` helper pair and use it at the 4 hand-rolled sites. (3) Merge the two identical primitive macros. (4) Wrap `WriteDataByIdentifier` (req + resp) in the enums. (5) Wrap `RoutineControl` (req + resp) using `SuppressablePositiveResponse`. (6) Add `Decode` to 5 DTC parameter types + a 25-variant `ReadDTCInfoRequest::Decode`, and wrap `Request::ReadDTCInfo`. (7) Full-matrix verification. + +**Tech Stack:** Rust, no_std + no_alloc, `embedded_io::Write` for encode, borrowed `&[u8]` for decode. Spec: `docs/superpowers/specs/2026-06-03-api-consistency-phase2-design.md`. + +--- + +## Conventions for every task + +- Local per-task verification: `cargo test --all-features` (fast host run). +- Commit message format: + ``` + + + Co-Authored-By: Claude Opus 4.8 + ``` +- Platform is macOS: in-place `sed` requires `sed -i ''`. + +--- + +## Task 1: Rename bidirectional descriptor types (drop `…Tx`/`…Rx`) + +Pure mechanical rename across the crate. The compiler + a final grep are the safety net. `ReadDataByIdentifierRequestTx` is **NOT** renamed (it is the one genuinely TX-only type). + +**Files:** all of `src/` (and a README check). + +- [ ] **Step 1: Apply the renames** + +Each old name is a full unique identifier, so a global text replace is safe. Run these from the repo root (the trailing `''` after `-i` is required on macOS): + +```bash +cd /Users/zacharyheylmun/dev/rust/uds_protocol +for pair in \ + "SecurityAccessRequestTx:SecurityAccessRequest" \ + "SecurityAccessResponseTx:SecurityAccessResponse" \ + "TransferDataRequestTx:TransferDataRequest" \ + "TransferDataResponseTx:TransferDataResponse" \ + "RequestFileTransferRequestTx:RequestFileTransferRequest" \ + "RequestFileTransferResponseTx:RequestFileTransferResponse" \ + "RequestDownloadResponseTx:RequestDownloadResponse" \ + "RoutineControlRequestTx:RoutineControlRequest" \ + "RoutineControlResponseTx:RoutineControlResponse" \ + "WriteDataByIdentifierRequestTx:WriteDataByIdentifierRequest" \ + "ReadDTCInfoResponseRx:ReadDTCInfoResponse" \ + "NamePayloadTx:NamePayload" \ + "SentDataPayloadTx:SentDataPayload" ; do + old="${pair%%:*}"; new="${pair##*:}" + grep -rl "$old" src README.md | xargs sed -i '' "s/${old}/${new}/g" +done +``` + +Note: the order matters only for substrings; none of these old names is a substring of another old name, so order is irrelevant here. `ReadDataByIdentifierRequestTx` contains none of the above as a substring, so it is untouched. + +- [ ] **Step 2: Verify no old names remain (except the intentional one)** + +```bash +grep -rn "RequestTx\|ResponseTx\|ResponseRx\|PayloadTx" src README.md +``` +Expected: only `ReadDataByIdentifierRequestTx` matches. If any other old name appears, the rename missed a spot — re-run the relevant replacement. + +- [ ] **Step 3: Build + test + fmt** + +Run: +```bash +cargo build --all-features +cargo test --all-features +cargo fmt -- --check +``` +Expected: PASS. (`cargo fmt` may reflow the now-shorter names in `use` lists; run `cargo fmt` if `--check` complains.) + +- [ ] **Step 4: Commit** + +```bash +git add -A +git commit -m "$(printf 'rename bidirectional descriptor types: drop misleading Tx/Rx suffixes\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 2: Deduplicate the variable-length big-endian integer codec + +Add one concrete helper pair to `src/common/util.rs` (kept `pub(crate)`) and call it from the four hand-rolled sites. + +**Files:** +- Modify: `src/common/util.rs` +- Modify: `src/services/request_file_transfer.rs` (`SizePayload`, `FileSizePayload`, `DirSizePayload`) +- Modify: `src/services/request_download.rs` (`RequestDownloadRequest`) + +- [ ] **Step 1: Write failing tests for the helpers** + +In `src/common/util.rs`, inside the existing `#[cfg(test)] mod tests`, add: + +```rust + #[test] + fn be_uint_roundtrip() { + use crate::common::util::{read_be_uint, write_be_uint}; + let mut buf = [0u8; 16]; + let mut w = buf.as_mut_slice(); + let written = write_be_uint(0x00AB_CDEFu128, 3, &mut w).unwrap(); + assert_eq!(written, 3); + assert_eq!(&buf[..3], &[0xAB, 0xCD, 0xEF]); + let v = read_be_uint(&buf[..3], 3).unwrap(); + assert_eq!(v, 0x00AB_CDEF); + } + + #[test] + fn be_uint_zero_width() { + use crate::common::util::{read_be_uint, write_be_uint}; + let mut buf = [0u8; 4]; + let mut w = buf.as_mut_slice(); + assert_eq!(write_be_uint(0, 0, &mut w).unwrap(), 0); + assert_eq!(read_be_uint(&[], 0).unwrap(), 0); + } + + #[test] + fn read_be_uint_rejects_short_and_overwide() { + use crate::common::util::read_be_uint; + assert!(read_be_uint(&[0x01], 2).is_err()); + assert!(read_be_uint(&[0u8; 17], 17).is_err()); + } +``` + +- [ ] **Step 2: Run to verify they fail** + +Run: `cargo test --all-features be_uint` +Expected: FAIL (helpers don't exist). + +- [ ] **Step 3: Implement the helpers** + +At the top of `src/common/util.rs`, add the import and helpers (place above the existing `param_length_*` functions): + +```rust +use crate::Error; + +/// Maximum width of a big-endian unsigned integer this codec handles. +const BE_UINT_MAX_BYTES: usize = 16; + +/// Read the first `n` big-endian bytes of `src` as a left-padded `u128`. +/// +/// # Errors +/// Returns [`Error::InsufficientData`] if `src` is shorter than `n`, or +/// [`Error::IncorrectMessageLengthOrInvalidFormat`] if `n > 16`. +pub(crate) fn read_be_uint(src: &[u8], n: usize) -> Result { + if n > BE_UINT_MAX_BYTES { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + if src.len() < n { + return Err(Error::InsufficientData(n)); + } + let mut bytes = [0u8; BE_UINT_MAX_BYTES]; + bytes[BE_UINT_MAX_BYTES - n..].copy_from_slice(&src[..n]); + Ok(u128::from_be_bytes(bytes)) +} + +/// Write the low `n` big-endian bytes of `value` to `writer`, returning `n`. +/// +/// # Errors +/// Returns [`Error::IncorrectMessageLengthOrInvalidFormat`] if `n > 16`, or +/// [`Error::IoError`] if the writer fails. +pub(crate) fn write_be_uint( + value: u128, + n: usize, + writer: &mut impl embedded_io::Write, +) -> Result { + if n > BE_UINT_MAX_BYTES { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + let bytes = value.to_be_bytes(); + writer + .write_all(&bytes[BE_UINT_MAX_BYTES - n..]) + .map_err(Error::io)?; + Ok(n) +} +``` + +- [ ] **Step 4: Route the helpers through `common/mod.rs`** + +In `src/common/mod.rs`, the `mod util;` line is followed by a `pub use util::{param_length_*}`. Add the new helpers to the crate-internal surface by changing that line's neighbours to also re-export them `pub(crate)`: + +```rust +mod util; +pub use util::{param_length_u16, param_length_u32, param_length_u64, param_length_u128}; +pub(crate) use util::{read_be_uint, write_be_uint}; +``` + +- [ ] **Step 5: Use the helpers in `request_file_transfer.rs`** + +Add `use crate::common::{read_be_uint, write_be_uint};` to the imports. Then: + +In `impl Encode for SizePayload`, replace the two `uncompressed`/`compressed` `to_be_bytes()` + `write_all(&…[U128_MAX_BYTES - n..])` blocks with: +```rust + write_be_uint(self.file_size_uncompressed, n, writer)?; + write_be_uint(self.file_size_compressed, n, writer)?; +``` +(keep the `file_size_parameter_length` byte write before them, and the `n > U128_MAX_BYTES` guard.) + +In `impl Decode for SizePayload`, replace the `u_bytes`/`c_bytes` blocks with: +```rust + let file_size_uncompressed = read_be_uint(&buf[1..], n)?; + let file_size_compressed = read_be_uint(&buf[1 + n..], n)?; +``` +and use those in the returned `Self`. + +In `impl Encode for FileSizePayload`, replace the two write blocks with the same two `write_be_uint(... n ...)` calls. In `impl Decode for FileSizePayload`, replace the `u_bytes`/`c_bytes` blocks with: +```rust + let file_size_uncompressed = read_be_uint(&buf[2..], n)?; + let file_size_compressed = read_be_uint(&buf[2 + n..], n)?; +``` + +In `impl Encode for DirSizePayload`, replace the single `bytes` write block with `write_be_uint(self.dir_info_length, n, writer)?;`. In `impl Decode for DirSizePayload`, replace the `bytes` block with `let dir_info_length = read_be_uint(&buf[2..], n)?;`. + +Keep every existing length/`total` bounds check and `n > U128_MAX_BYTES` guard exactly as-is; only the padding+convert lines change. `U128_MAX_BYTES` may now be unused — if the compiler warns, remove the `const U128_MAX_BYTES` definition. + +- [ ] **Step 6: Use the helpers in `request_download.rs`** + +Add `use crate::common::{read_be_uint, write_be_uint};` to the imports. + +In `impl Encode for RequestDownloadRequest::encode`, replace the `addr_bytes`/`size_bytes` blocks with: +```rust + write_be_uint(u128::from(self.memory_address), addr_len, writer)?; + write_be_uint(u128::from(self.memory_size), size_len, writer)?; +``` +where `addr_len` / `size_len` are the existing `memory_address_length` / `memory_size_length` reads. + +In `impl Decode`, replace the `addr_bytes`/`size_bytes` blocks with: +```rust + let memory_address = read_be_uint(&buf[2..], addr_len)? as u64; + let memory_size = read_be_uint(&buf[2 + addr_len..], size_len)? as u32; +``` +Keep the `total` bounds check. Add `#[allow(clippy::cast_possible_truncation)]` on the `decode` fn if clippy flags the `as u64`/`as u32` (the widths are bounded by the format nibble, ≤8 and ≤4). + +- [ ] **Step 7: Verify and commit** + +Run: `cargo test --all-features && cargo clippy --all-features && cargo fmt` +Expected: PASS, clippy clean. The existing payload round-trip tests must still pass. +```bash +git add -A +git commit -m "$(printf 'dedup variable-length big-endian integer codec into util helpers\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 3: Merge the duplicate primitive macros + +**Files:** `src/common/primitive_generics.rs` + +- [ ] **Step 1: Replace the two macros with one** + +Replace the entire contents of `src/common/primitive_generics.rs` (both `unsigned_primitive_encode_decode!` and `signed_primitive_encode_decode!` definitions and their two invocations) with a single macro and one invocation: + +```rust +use crate::{Decode, Encode, Error}; + +/// Implement [`Encode`] and [`Decode`] for integer primitives (no_std-compatible). +macro_rules! primitive_encode_decode { + ( $($primitive:ty), * ) => { + $( + impl Encode for $primitive { + fn encoded_size(&self) -> usize { + core::mem::size_of::<$primitive>() + } + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&self.to_be_bytes()).map_err(Error::io)?; + Ok(self.encoded_size()) + } + } + impl<'a> Decode<'a> for $primitive { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + const SIZE: usize = core::mem::size_of::<$primitive>(); + if buf.len() < SIZE { + return Err(Error::InsufficientData(SIZE)); + } + let (head, tail) = buf.split_at(SIZE); + let value = <$primitive>::from_be_bytes(head.try_into().unwrap()); + Ok((value, tail)) + } + } + )* + }; +} + +primitive_encode_decode!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); +``` + +- [ ] **Step 2: Verify and commit** + +Run: `cargo test --all-features && cargo clippy --all-features && cargo fmt -- --check` +Expected: PASS (same 10 impls as before, generated by one macro). +```bash +git add src/common/primitive_generics.rs +git commit -m "$(printf 'merge identical signed/unsigned primitive codec macros\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 4: Wrap `WriteDataByIdentifier` in the enums + +**Files:** +- Modify: `src/services/write_data_by_identifier.rs` (add `Decode` for `WriteDataByIdentifierRequest`) +- Modify: `src/request.rs`, `src/response.rs` + +- [ ] **Step 1: Write a failing round-trip test** + +In `src/request.rs` `mod tests` (create the test fn alongside existing ones), add: +```rust + #[test] + fn write_data_by_identifier_request_roundtrips() { + // SID 0x2E, DID 0xF190, one data byte 0x01 + let wire = [0x2E, 0xF1, 0x90, 0x01]; + let (req, rest) = Request::decode(&wire).unwrap(); + assert!(rest.is_empty()); + assert!(matches!(req, Request::WriteDataByIdentifier(_))); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `cargo test --all-features write_data_by_identifier_request_roundtrips` +Expected: FAIL — `Request::WriteDataByIdentifier` still holds `&[u8]` (the `matches!` and/or type will mismatch once wired; before wiring it won't compile against the new variant). + +- [ ] **Step 3: Add `Decode` for `WriteDataByIdentifierRequest`** + +In `src/services/write_data_by_identifier.rs`, ensure `Decode` is imported (it is, from Phase 1). After the `impl Encode for WriteDataByIdentifierRequest` block add: +```rust +impl<'a> Decode<'a> for WriteDataByIdentifierRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + Ok((Self { payload: buf }, &[])) + } +} +``` + +- [ ] **Step 4: Rewire `Request::WriteDataByIdentifier`** + +In `src/request.rs`: +- Add `WriteDataByIdentifierRequest` to the `services::{…}` import. +- Change the variant from `WriteDataByIdentifier(&'a [u8])` to `WriteDataByIdentifier(WriteDataByIdentifierRequest<'a>)` and update its doc comment. +- In `decode`, change the arm to: + ```rust + UdsServiceType::WriteDataByIdentifier => Self::WriteDataByIdentifier( + ::decode_exact(payload)?, + ), + ``` +- In `encoded_size`, remove `WriteDataByIdentifier` from the grouped `ReadDataByIdentifier | WriteDataByIdentifier | ReadDTCInfo => bytes.len()` arm (leaving `ReadDataByIdentifier | ReadDTCInfo`) and add `Self::WriteDataByIdentifier(req) => req.encoded_size(),`. +- In `encode`, likewise split it out: remove from the grouped raw arm and add `Self::WriteDataByIdentifier(req) => req.encode(writer)?,`. +- `service` arm is unchanged (`Self::WriteDataByIdentifier(_) => …`, already ignores payload). + +- [ ] **Step 5: Rewire `Response::WriteDataByIdentifier`** + +In `src/response.rs`: +- Add `WriteDataByIdentifierResponse` to the crate imports. +- Change the variant from `WriteDataByIdentifier(&'a [u8])` to `WriteDataByIdentifier(WriteDataByIdentifierResponse)` and update its doc comment (note: `WriteDataByIdentifierResponse` is fixed-size, no lifetime). +- In `decode`, change the arm to: + ```rust + UdsServiceType::WriteDataByIdentifier => Self::WriteDataByIdentifier( + ::decode_exact(payload)?, + ), + ``` +- In `encoded_size`, remove `WriteDataByIdentifier` from the `ReadDataByIdentifier | WriteDataByIdentifier => bytes.len()` arm and add `Self::WriteDataByIdentifier(resp) => resp.encoded_size(),`. +- In `encode`, split it out the same way: `Self::WriteDataByIdentifier(resp) => resp.encode(writer)?,`. +- `response_sid` arm unchanged. + +- [ ] **Step 6: Run tests, add response round-trip, verify** + +Add to `src/response.rs` `mod tests`: +```rust + #[test] + fn write_data_by_identifier_response_roundtrips() { + // SID 0x6E, echoed DID 0xF190 + let wire = [0x6E, 0xF1, 0x90]; + let (resp, rest) = Response::decode(&wire).unwrap(); + assert!(rest.is_empty()); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } +``` +Run: `cargo test --all-features && cargo clippy --all-features && cargo fmt` +Expected: PASS. + +- [ ] **Step 7: Commit** +```bash +git add -A +git commit -m "$(printf 'wrap WriteDataByIdentifier request/response in their descriptor types\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 5: Wrap `RoutineControl` in the enums (with SPRMIB fidelity) + +**Files:** +- Modify: `src/services/routine_control.rs` (use `SuppressablePositiveResponse`; add `Decode` for both) +- Modify: `src/request.rs`, `src/response.rs` + +- [ ] **Step 1: Write failing round-trip tests (incl. suppress bit)** + +In `src/request.rs` `mod tests` add: +```rust + #[test] + fn routine_control_request_roundtrips_with_suppress_bit() { + // SID 0x31, sub 0x81 (StartRoutine + SPRMIB), RID 0xFF00, param 0xAA + let wire = [0x31, 0x81, 0xFF, 0x00, 0xAA]; + let (req, rest) = Request::decode(&wire).unwrap(); + assert!(rest.is_empty()); + assert!(req.is_positive_response_suppressed()); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } +``` +In `src/response.rs` `mod tests` add: +```rust + #[test] + fn routine_control_response_roundtrips() { + // SID 0x71, sub 0x01, RID 0xFF00, status 0x10 + let wire = [0x71, 0x01, 0xFF, 0x00, 0x10]; + let (resp, rest) = Response::decode(&wire).unwrap(); + assert!(rest.is_empty()); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } +``` + +- [ ] **Step 2: Run to verify they fail** + +Run: `cargo test --all-features routine_control_request_roundtrips_with_suppress_bit routine_control_response_roundtrips` +Expected: FAIL (variants still hold inline fields; no `Decode`). + +- [ ] **Step 3: Rework `RoutineControlRequest` to use `SuppressablePositiveResponse`** + +In `src/services/routine_control.rs`, change the imports to: +```rust +use crate::common::SuppressablePositiveResponse; +use crate::{Decode, Encode, Error, RoutineControlSubFunction}; +``` +Replace the `RoutineControlRequest` struct + its `impl` + `impl Encode` with: +```rust +/// Used by a client to execute a defined sequence of events and obtain any relevant results. +/// +/// The payload is the routine identifier (2 bytes, big-endian) followed by any optional +/// routine input parameters, exactly as it appears on the wire after the sub-function byte. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct RoutineControlRequest<'d> { + sub_function: SuppressablePositiveResponse, + raw_payload: &'d [u8], +} + +impl<'d> RoutineControlRequest<'d> { + /// Create a new `RoutineControlRequest`. + #[must_use] + pub const fn new( + suppress_positive_response: bool, + sub_function: RoutineControlSubFunction, + raw_payload: &'d [u8], + ) -> Self { + Self { + sub_function: SuppressablePositiveResponse::new(suppress_positive_response, sub_function), + raw_payload, + } + } + + /// Whether the server should suppress the positive response (SPRMIB). + #[must_use] + pub fn suppress_positive_response(&self) -> bool { + self.sub_function.suppress_positive_response() + } + + /// The routine control operation (start, stop, or request results). + #[must_use] + pub fn sub_function(&self) -> RoutineControlSubFunction { + self.sub_function.value() + } + + /// The raw payload bytes: routine identifier followed by optional parameters. + #[must_use] + pub const fn raw_payload(&self) -> &[u8] { + self.raw_payload + } +} + +impl Encode for RoutineControlRequest<'_> { + fn encoded_size(&self) -> usize { + 1 + self.raw_payload.len() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.sub_function)]) + .map_err(Error::io)?; + writer.write_all(self.raw_payload).map_err(Error::io)?; + Ok(self.encoded_size()) + } +} + +impl<'a> Decode<'a> for RoutineControlRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let sub_function = SuppressablePositiveResponse::try_from(buf[0])?; + Ok(( + Self { + sub_function, + raw_payload: &buf[1..], + }, + &[], + )) + } +} +``` + +- [ ] **Step 4: Add `Decode` for `RoutineControlResponse`** + +In the same file, leave the `RoutineControlResponse` struct and its `impl Encode` as renamed in Task 1 (public fields `routine_control_type: RoutineControlSubFunction`, `raw_status_record: &[u8]`), and add after its `impl Encode`: +```rust +impl<'a> Decode<'a> for RoutineControlResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let routine_control_type = RoutineControlSubFunction::try_from(buf[0])?; + Ok(( + Self { + routine_control_type, + raw_status_record: &buf[1..], + }, + &[], + )) + } +} +``` +Update the existing in-file `mod test` if it constructs `RoutineControlRequest` with the old public-field/`new(sub_function, payload)` signature — switch to `RoutineControlRequest::new(false, RoutineControlSubFunction::StartRoutine, &payload)`. + +- [ ] **Step 5: Rewire the enums** + +In `src/request.rs`: +- Add `RoutineControlRequest` to the `services::{…}` import. +- Change the variant from the `RoutineControl { sub_function: u8, raw_payload: &'a [u8] }` struct form to `RoutineControl(RoutineControlRequest<'a>)`; update its doc comment. +- `decode` arm becomes: + ```rust + UdsServiceType::RoutineControl => Self::RoutineControl( + ::decode_exact(payload)?, + ), + ``` + (delete the old manual `if payload.is_empty()` + field construction.) +- `encoded_size` arm: `Self::RoutineControl(req) => req.encoded_size(),` (remove the old `{ raw_payload, .. } => 1 + raw_payload.len()`). +- `encode` arm: `Self::RoutineControl(req) => req.encode(writer)?,` (remove the old field-writing block). +- `service` arm: `Self::RoutineControl(_) => UdsServiceType::RoutineControl,`. +- `is_positive_response_suppressed`: add `Self::RoutineControl(req) => req.suppress_positive_response(),` before the `_ => false` arm. + +In `src/response.rs`: +- Add `RoutineControlResponse` to the crate imports. +- Change the variant from `RoutineControl { routine_control_type: u8, raw_status_record: &'a [u8] }` to `RoutineControl(RoutineControlResponse<'a>)`; update its doc comment. +- `decode` arm becomes: + ```rust + UdsServiceType::RoutineControl => Self::RoutineControl( + ::decode_exact(payload)?, + ), + ``` +- `encoded_size` arm: `Self::RoutineControl(resp) => resp.encoded_size(),`. +- `encode` arm: `Self::RoutineControl(resp) => resp.encode(writer)?,`. +- `response_sid` arm: `Self::RoutineControl(_) => UdsServiceType::RoutineControl.response_to_byte(),`. + +- [ ] **Step 6: Run tests, verify, commit** + +Run: `cargo test --all-features && cargo clippy --all-features && cargo fmt` +Expected: PASS, including the new suppress-bit round-trip. +```bash +git add -A +git commit -m "$(printf 'wrap RoutineControl in descriptors; round-trip SPRMIB via SuppressablePositiveResponse\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 6: `Decode` for the DTC params + `ReadDTCInfoRequest`; wrap `Request::ReadDTCInfo` + +**Files:** +- Modify: `src/common/dtc_snapshot.rs`, `src/common/dtc_ext_data.rs`, `src/common/dtc_status.rs` (add `Decode` to the 5 param types) +- Modify: `src/services/read_dtc_information.rs` (add `ReadDTCInfoRequest::Decode`) +- Modify: `src/request.rs` + +- [ ] **Step 1: Write a failing round-trip test (encode-as-oracle)** + +In `src/services/read_dtc_information.rs`, in the `read_dtc_info_request_encode_tests` module, add (`decode_exact` returns just the value, so assert equality directly): +```rust + #[test] + fn read_dtc_info_request_roundtrips() { + use crate::Decode; + let cases = [ + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportSupportedDTC), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTC_ByStatusMask( + DTCStatusMask::from(0xFF), + )), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportWWHOBDDTC_ByMaskRecord( + FunctionalGroupIdentifier::EmissionsSystemGroup, + DTCStatusMask::from(0x08), + DTCSeverityMask::CheckImmediately, + )), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ISOSAEReserved(0x57)), + ]; + for req in cases { + let mut buf = [0u8; 16]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + let decoded = ::decode_exact(&buf[..written]).unwrap(); + assert_eq!(decoded, req); + } + } +``` +`ReadDTCInfoRequest` derives `PartialEq` already (verify; it derives `Clone, Copy, Debug, PartialEq`). + +- [ ] **Step 2: Run to verify it fails** + +Run: `cargo test --all-features read_dtc_info_request_roundtrips` +Expected: FAIL (no `Decode` for `ReadDTCInfoRequest`, and param types lack `Decode`). + +- [ ] **Step 3: Add `Decode` to the 5 DTC parameter types** + +These mirror their Phase 1 `Encode` (read exactly 1 byte). Each round-trips its `Encode` (verified: `new`/`from` followed by `value()`/`bits()`/`.0` is the identity). + +In `src/common/dtc_snapshot.rs`, add `Decode` to the import (`use crate::{Decode, Encode, Error};`) and after the `impl Encode for DTCSnapshotRecordNumber`: +```rust +impl<'a> Decode<'a> for DTCSnapshotRecordNumber { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::new(buf[0]), &buf[1..])) + } +} +``` + +In `src/common/dtc_ext_data.rs`, add `Decode` to the import and after `impl Encode for DTCExtDataRecordNumber`: +```rust +impl<'a> Decode<'a> for DTCExtDataRecordNumber { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::new(buf[0]), &buf[1..])) + } +} +``` + +In `src/common/dtc_status.rs` (already imports `Decode`), add after each type's `Encode` impl: +```rust +impl<'a> Decode<'a> for DTCStoredDataRecordNumber { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::from(buf[0]), &buf[1..])) + } +} + +impl<'a> Decode<'a> for DTCSeverityMask { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::from(buf[0]), &buf[1..])) + } +} + +impl<'a> Decode<'a> for FunctionalGroupIdentifier { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::from(buf[0]), &buf[1..])) + } +} +``` +(`DTCStoredDataRecordNumber` uses the lenient `From`, not `new` — decode must not reject reserved record numbers. `DTCSeverityMask` and `FunctionalGroupIdentifier` both have `From`.) + +- [ ] **Step 4: Implement `ReadDTCInfoRequest::Decode` (25-variant inverse)** + +In `src/services/read_dtc_information.rs`, after the `impl Encode for ReadDTCInfoRequest` block (relocated near the type in Phase 1), add: +```rust +impl<'a> Decode<'a> for ReadDTCInfoRequest { + #[allow(clippy::too_many_lines)] + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + use ReadDTCInfoSubFunction as S; + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let sub = buf[0]; + let rest = &buf[1..]; + let (dtc_subfunction, rest) = match sub { + 0x01 => { + let (m, r) = DTCStatusMask::decode(rest)?; + (S::ReportNumberOfDTC_ByStatusMask(m), r) + } + 0x02 => { + let (m, r) = DTCStatusMask::decode(rest)?; + (S::ReportDTC_ByStatusMask(m), r) + } + 0x03 => (S::ReportDTCSnapshotIdentification, rest), + 0x04 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCSnapshotRecordNumber::decode(r)?; + (S::ReportDTCSnapshotRecord_ByDTCNumber(rec, n), r) + } + 0x05 => { + let (n, r) = DTCStoredDataRecordNumber::decode(rest)?; + (S::ReportDTCStoredData_ByRecordNumber(n), r) + } + 0x06 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCExtDataRecordNumber::decode(r)?; + (S::ReportDTCExtDataRecord_ByDTCNumber(rec, n), r) + } + 0x07 => { + let (s, r) = DTCSeverityMask::decode(rest)?; + let (m, r) = DTCStatusMask::decode(r)?; + (S::ReportNumberOfDTC_BySeverityMaskRecord(s, m), r) + } + 0x08 => { + let (s, r) = DTCSeverityMask::decode(rest)?; + let (m, r) = DTCStatusMask::decode(r)?; + (S::ReportDTC_BySeverityMaskRecord(s, m), r) + } + 0x09 => { + let (rec, r) = DTCRecord::decode(rest)?; + (S::ReportSeverityInfoOfDTC(rec), r) + } + 0x0A => (S::ReportSupportedDTC, rest), + 0x0B => (S::ReportFirstTestFailedDTC, rest), + 0x0C => (S::ReportFirstConfirmedDTC, rest), + 0x0D => (S::ReportMostRecentTestFailedDTC, rest), + 0x0E => (S::ReportMostRecentConfirmedDTC, rest), + 0x14 => (S::ReportDTCFaultDetectionCounter, rest), + 0x15 => (S::ReportDTCWithPermanentStatus, rest), + 0x16 => { + let (n, r) = DTCExtDataRecordNumber::decode(rest)?; + (S::ReportDTCExtDataRecord_ByRecordNumber(n), r) + } + 0x17 => { + let (m, r) = DTCStatusMask::decode(rest)?; + (S::ReportUserDefMemoryDTC_ByStatusMask(m), r) + } + 0x18 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCSnapshotRecordNumber::decode(r)?; + let (mem, r) = u8::decode(r)?; + ( + S::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(rec, n, mem), + r, + ) + } + 0x19 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCExtDataRecordNumber::decode(r)?; + let (mem, r) = u8::decode(r)?; + ( + S::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(rec, n, mem), + r, + ) + } + 0x1A => { + let (n, r) = DTCExtDataRecordNumber::decode(rest)?; + (S::ReportSupportedDTCExtDataRecord(n), r) + } + 0x42 => { + let (g, r) = FunctionalGroupIdentifier::decode(rest)?; + let (m, r) = DTCStatusMask::decode(r)?; + let (s, r) = DTCSeverityMask::decode(r)?; + (S::ReportWWHOBDDTC_ByMaskRecord(g, m, s), r) + } + 0x55 => { + let (g, r) = FunctionalGroupIdentifier::decode(rest)?; + (S::ReportWWHOBDDTC_WithPermanentStatus(g), r) + } + 0x56 => { + let (g, r) = FunctionalGroupIdentifier::decode(rest)?; + let (rg, r) = u8::decode(r)?; + ( + S::ReportDTCInformation_ByDTCReadinessGroupIdentifier(g, rg), + r, + ) + } + other => (S::ISOSAEReserved(other), rest), + }; + Ok((ReadDTCInfoRequest::new(dtc_subfunction), rest)) + } +} +``` + +- [ ] **Step 5: Wrap `Request::ReadDTCInfo`** + +In `src/request.rs`: +- Add `ReadDTCInfoRequest` to the `services::{…}` import. +- Change the variant from `ReadDTCInfo(&'a [u8])` to `ReadDTCInfo(ReadDTCInfoRequest)` (no lifetime — it is owned); update its doc comment. +- `decode` arm: + ```rust + UdsServiceType::ReadDTCInfo => { + Self::ReadDTCInfo(::decode_exact(payload)?) + } + ``` +- `encoded_size`: remove `ReadDTCInfo` from the remaining grouped raw arm (now just `Self::ReadDataByIdentifier(bytes) => bytes.len()`) and add `Self::ReadDTCInfo(req) => req.encoded_size(),`. +- `encode`: split out similarly: `Self::ReadDTCInfo(req) => req.encode(writer)?,`. +- `service` arm unchanged. + +After this, the only raw-bytes request variants remaining are `ReadDataByIdentifier(&[u8])` and `Other { data }` — as designed. + +- [ ] **Step 6: Run tests, verify, commit** + +Run: `cargo test --all-features && cargo clippy --all-features && cargo fmt` +Expected: PASS, including `read_dtc_info_request_roundtrips`. +```bash +git add -A +git commit -m "$(printf 'add Decode for DTC params and ReadDTCInfoRequest; wrap Request::ReadDTCInfo\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 7: Full-matrix verification + +No code changes; if any command fails, fix the offending task and re-run. + +- [ ] **Step 1: host build + test** +```bash +cargo build --all-features --release +cargo test --all-features +``` +- [ ] **Step 2: no_std / no_alloc bare-metal** +```bash +cargo build --no-default-features --target thumbv6m-none-eabi +cargo build --no-default-features --features alloc --target thumbv6m-none-eabi +``` +- [ ] **Step 3: clippy combos** +```bash +cargo clippy --all-features +cargo clippy --no-default-features +cargo clippy --no-default-features --features alloc +``` +- [ ] **Step 4: fmt + doc + leftover-name sweep** +```bash +cargo fmt -- --check +cargo doc --release --all-features --no-deps +grep -rn "RequestTx\|ResponseTx\|ResponseRx\|PayloadTx" src README.md +``` +Expected: all green; the grep shows only `ReadDataByIdentifierRequestTx`. +- [ ] **Step 5: (No commit)** — verification only. Phase 2 complete. + +--- + +## Self-review notes + +- **Spec coverage:** Decision 1 (naming) → Task 1; Decision 2 (wrapping) → Tasks 4–6 (WDBI, RoutineControl, ReadDTCInfo; RDBI deliberately left raw); Decision 3 (varint dedup) → Task 2; Decision 4 (macro merge) → Task 3; testing/matrix → Task 7. +- **SPRMIB fidelity** locked by `routine_control_request_roundtrips_with_suppress_bit` (Task 5). +- **`ReadDTCInfoRequest::Decode`** correctness guarded by encode-as-oracle round-trip (Task 6), reusing the Phase 1 `Encode`. +- **Type consistency:** all `Decode<'a>`/`Encode` signatures match `src/traits.rs`; param-type `Decode`s use the lenient `From`/`new` that round-trips each `Encode`; `read_be_uint`/`write_be_uint` are `pub(crate)` per the spec. +- **RDBI** remains the single documented raw exception (request `&[u8]`, `ReadDataByIdentifierRequestTx` keeps its suffix). diff --git a/docs/superpowers/plans/2026-06-03-api-exposure-and-consistency-phase1.md b/docs/superpowers/plans/2026-06-03-api-exposure-and-consistency-phase1.md new file mode 100644 index 0000000..a46af45 --- /dev/null +++ b/docs/superpowers/plans/2026-06-03-api-exposure-and-consistency-phase1.md @@ -0,0 +1,696 @@ +# API Exposure & Consistency — Phase 1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliberately expose every intended-public type from the crate root and complete the two codec-incomplete descriptors (`ReadDTCInfoRequest` gains `Encode`; `WriteDataByIdentifierResponse` gains `Decode`), so the `no_std` breaking change ships as one cohesive chunk. + +**Architecture:** Five independent commits. (1) Replace the `pub use {common,services}::*` globs in `lib.rs` with explicit named re-exports. (2) Add 1-byte `Encode` impls to the five DTC parameter types `ReadDTCInfoSubFunction` needs, fixing a latent `todo!()` panic in `FunctionalGroupIdentifier::value()`. (3) Give `ReadDTCInfoSubFunction` (and a delegating `ReadDTCInfoRequest`) a faithful per-variant `Encode`. (4) Give `WriteDataByIdentifierResponse` a 2-byte `Decode`. (5) Add crate-root integration tests proving the completed types round-trip through the public API. No `Request`/`Response` enum shapes change — that is Phase 2. + +**Tech Stack:** Rust, `no_std` + `no_alloc` (`alloc`/`std` additive), `embedded_io::Write` for encoding, borrowed `&[u8]` for decoding. Test helper `assert_encode_size_agrees` in `src/test_util.rs`. + +**Spec:** `docs/superpowers/specs/2026-06-03-api-exposure-and-consistency-design.md` + +--- + +## Conventions for every task + +- Local per-task verification: `cargo test --all-features` (fast host run). +- Commit message format (matches repo history): + ``` + + + Co-Authored-By: Claude Opus 4.8 + ``` +- Do **not** touch `Request`/`Response` enum variant shapes anywhere in this plan. + +--- + +## Task 1: De-glob the crate-root re-exports + +Replace the two wildcard re-exports in `src/lib.rs` with explicit named lists so the public surface is intentional and individually documented. The compiler is the safety net: every internal module imports common/service types via `crate::…`, so a missing name fails the build. + +**Files:** +- Modify: `src/lib.rs:18` (`pub use common::*;`) and `src/lib.rs:30` (`pub use services::*;`) + +- [ ] **Step 1: Replace `pub use common::*;`** + +In `src/lib.rs`, replace the single line `pub use common::*;` with: + +```rust +pub use common::{ + CLEAR_ALL_DTCS, CommunicationControlType, CommunicationType, DTCExtDataRecordNumber, + DTCFormatIdentifier, DTCRecord, DTCSeverityMask, DTCSeverityRecord, DTCSnapshotRecordNumber, + DTCStatusMask, DTCStoredDataRecordNumber, DiagnosticSessionType, FunctionalGroupIdentifier, + NegativeResponseCode, ResetType, SecurityAccessType, UDSIdentifier, UDSRoutineIdentifier, + param_length_u128, param_length_u16, param_length_u32, param_length_u64, +}; +``` + +- [ ] **Step 2: Replace `pub use services::*;`** + +In `src/lib.rs`, replace the single line `pub use services::*;` with: + +```rust +pub use services::{ + ClearDiagnosticInfoRequest, CommunicationControlRequest, CommunicationControlResponse, + ControlDTCSettingsRequest, ControlDTCSettingsResponse, DiagnosticSessionControlRequest, + DiagnosticSessionControlResponse, DirSizePayload, DtcAndStatusIter, DtcFaultDetectionIter, + DtcSeverityAndStatusIter, EcuResetRequest, EcuResetResponse, FileOperationMode, + FileSizePayload, NamePayloadTx, NegativeResponse, PositionPayload, + ReadDTCInfoRequest, ReadDTCInfoResponseRx, ReadDTCInfoSubFunction, + ReadDataByIdentifierRequestTx, RequestDownloadRequest, RequestDownloadResponseTx, + RequestFileTransferRequestTx, RequestFileTransferResponseTx, RoutineControlRequestTx, + RoutineControlResponseTx, SecurityAccessRequestTx, SecurityAccessResponseTx, SentDataPayloadTx, + SizePayload, TesterPresentRequest, TesterPresentResponse, TransferDataRequestTx, + TransferDataResponseTx, WriteDataByIdentifierRequestTx, WriteDataByIdentifierResponse, +}; +``` + +- [ ] **Step 3: Build to verify no public name was dropped** + +Run: `cargo build --all-features` +Expected: PASS. A missing re-export would fail here (internal modules resolve these via `crate::…`). If a name is reported unresolved or unused, add/remove it from the lists above to match. + +- [ ] **Step 4: Confirm nothing newly hidden / formatting** + +Run: `cargo test --all-features && cargo fmt -- --check` +Expected: PASS (tests reference these names through the crate root). If `cargo fmt` reports diffs, run `cargo fmt` and re-check. + +- [ ] **Step 5: Commit** + +```bash +git add src/lib.rs +git commit -m "$(printf 'make crate-root re-exports explicit (drop glob re-exports)\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 2: `Encode` for the five DTC parameter types + +`ReadDTCInfoSubFunction` (Task 3) encodes its parameters by delegating to each parameter type's own `Encode`. `DTCStatusMask` and `DTCRecord` already implement `Encode`; these five do not yet. Each is a 1-byte value. This task also fixes a latent `todo!()` panic in `FunctionalGroupIdentifier::value()`. + +**Files:** +- Modify: `src/common/dtc_snapshot.rs` (add `use` + `Encode` for `DTCSnapshotRecordNumber`) +- Modify: `src/common/dtc_ext_data.rs` (add `use` + `Encode` for `DTCExtDataRecordNumber`) +- Modify: `src/common/dtc_status.rs` (`Encode` for `DTCStoredDataRecordNumber`, `DTCSeverityMask`, `FunctionalGroupIdentifier`; fix `FunctionalGroupIdentifier::value()`) + +- [ ] **Step 1: Write failing tests for all five `Encode` impls** + +In `src/common/dtc_snapshot.rs`, inside `mod snapshot`, add: + +```rust + #[test] + fn encode_snapshot_record_number() { + use crate::test_util::assert_encode_size_agrees; + let n = DTCSnapshotRecordNumber::new(0x02); + let mut buf = [0u8; 4]; + let written = crate::Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x02); + assert_encode_size_agrees(&n); + } +``` + +In `src/common/dtc_ext_data.rs`, inside `mod tests`, add: + +```rust + #[test] + fn encode_ext_data_record_number() { + use crate::test_util::assert_encode_size_agrees; + let n = DTCExtDataRecordNumber::new(0x90); + let mut buf = [0u8; 4]; + let written = crate::Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x90); + assert_encode_size_agrees(&n); + } +``` + +In `src/common/dtc_status.rs`, add a test module at the end of the file (if one does not exist) or append these tests to the existing `#[cfg(test)] mod` block: + +```rust +#[cfg(test)] +mod encode_param_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn encode_stored_data_record_number() { + let n = DTCStoredDataRecordNumber::new(0x05).unwrap(); + let mut buf = [0u8; 4]; + let written = Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x05); + assert_encode_size_agrees(&n); + } + + #[test] + fn encode_severity_mask() { + let m = DTCSeverityMask::CheckImmediately; + let mut buf = [0u8; 4]; + let written = Encode::encode(&m, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0b1000_0000); + assert_encode_size_agrees(&m); + } + + #[test] + fn encode_functional_group_identifier_named() { + let g = FunctionalGroupIdentifier::EmissionsSystemGroup; + let mut buf = [0u8; 4]; + let written = Encode::encode(&g, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x33); + assert_encode_size_agrees(&g); + } + + #[test] + fn functional_group_identifier_value_does_not_panic_on_reserved() { + // Regression: value() previously called todo!() for carried-byte variants. + let g = FunctionalGroupIdentifier::from(0x10); // -> ISOSAEReserved(0x10) + assert_eq!(g.value(), 0x10); + let g2 = FunctionalGroupIdentifier::from(0xD5); // -> LegislativeSystemGroup(0xD5) + assert_eq!(g2.value(), 0xD5); + } +} +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: `cargo test --all-features encode_snapshot_record_number encode_ext_data_record_number encode_stored_data_record_number encode_severity_mask encode_functional_group_identifier_named functional_group_identifier_value_does_not_panic_on_reserved` +Expected: FAIL — `Encode` not implemented for these types; the reserved-value test panics with `todo!`. + +- [ ] **Step 3: Fix `FunctionalGroupIdentifier::value()`** + +In `src/common/dtc_status.rs`, replace the body of `FunctionalGroupIdentifier::value()` (currently the `match` with two `todo!()` arms) with: + +```rust + /// Return the raw `u8` value of this functional group identifier. + #[must_use] + pub fn value(&self) -> u8 { + match self { + FunctionalGroupIdentifier::EmissionsSystemGroup => 0x33, + FunctionalGroupIdentifier::SafetySystemGroup => 0xD0, + FunctionalGroupIdentifier::VODBSystem => 0xFE, + FunctionalGroupIdentifier::LegislativeSystemGroup(value) + | FunctionalGroupIdentifier::ISOSAEReserved(value) => *value, + } + } +``` + +- [ ] **Step 4: Add the `Encode` impls** + +In `src/common/dtc_snapshot.rs`, add at the top of the file (the file currently has no imports): + +```rust +use crate::{Encode, Error}; +``` + +and after the `impl PartialEq for DTCSnapshotRecordNumber` block add: + +```rust +impl Encode for DTCSnapshotRecordNumber { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.value()]).map_err(Error::io)?; + Ok(1) + } +} +``` + +In `src/common/dtc_ext_data.rs`, add at the top of the file: + +```rust +use crate::{Encode, Error}; +``` + +and after the `impl PartialEq for DTCExtDataRecordNumber` block add: + +```rust +impl Encode for DTCExtDataRecordNumber { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.value()]).map_err(Error::io)?; + Ok(1) + } +} +``` + +In `src/common/dtc_status.rs` (which already imports `Encode`/`Error` for the existing `DTCStatusMask`/`DTCRecord` impls), add these three impls (place each near its type definition): + +```rust +impl Encode for DTCStoredDataRecordNumber { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.0]).map_err(Error::io)?; + Ok(1) + } +} + +impl Encode for DTCSeverityMask { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.bits()]).map_err(Error::io)?; + Ok(1) + } +} + +impl Encode for FunctionalGroupIdentifier { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.value()]).map_err(Error::io)?; + Ok(1) + } +} +``` + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `cargo test --all-features encode_snapshot_record_number encode_ext_data_record_number encode_stored_data_record_number encode_severity_mask encode_functional_group_identifier_named functional_group_identifier_value_does_not_panic_on_reserved` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/common/dtc_snapshot.rs src/common/dtc_ext_data.rs src/common/dtc_status.rs +git commit -m "$(printf 'add Encode to DTC parameter types; fix FunctionalGroupIdentifier::value panic\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 3: `Encode` for `ReadDTCInfoSubFunction` and `ReadDTCInfoRequest` + +Give the 25-variant `ReadDTCInfoSubFunction` a faithful `Encode` (sub-function byte + each variant's typed parameters, delegating to the parameter `Encode` impls from Task 2), and have `ReadDTCInfoRequest` delegate to it. The two match arms (in `encode` and `encoded_size`) use identical variant grouping and reference `encoded_size()` rather than hard-coded widths; `assert_encode_size_agrees` guards drift. + +**Files:** +- Modify: `src/services/read_dtc_information.rs` (already imports `Decode, Encode, Error`) + +- [ ] **Step 1: Write failing tests** + +In `src/services/read_dtc_information.rs`, add (or extend) a `#[cfg(test)]` module with: + +```rust +#[cfg(test)] +mod read_dtc_info_request_encode_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn encode_no_param_subfunction() { + // 0x0A ReportSupportedDTC, no parameters. + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportSupportedDTC); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x0A]); + assert_encode_size_agrees(&req); + } + + #[test] + fn encode_single_param_subfunction() { + // 0x02 ReportDTC_ByStatusMask(mask). DTCStatusMask is 1 byte. + let mask = DTCStatusMask::from(0xFF); + let req = + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTC_ByStatusMask(mask)); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x02, 0xFF]); + assert_encode_size_agrees(&req); + } + + #[test] + fn encode_multi_param_subfunction() { + // 0x42 ReportWWHOBDDTC_ByMaskRecord(group, status, severity). + let req = ReadDTCInfoRequest::new( + ReadDTCInfoSubFunction::ReportWWHOBDDTC_ByMaskRecord( + FunctionalGroupIdentifier::EmissionsSystemGroup, + DTCStatusMask::from(0x08), + DTCSeverityMask::CheckImmediately, + ), + ); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x42, 0x33, 0x08, 0b1000_0000]); + assert_encode_size_agrees(&req); + } + + #[test] + fn encode_reserved_subfunction() { + // ISOSAEReserved carries the sub-function byte itself, no params. + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ISOSAEReserved(0x57)); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x57]); + assert_encode_size_agrees(&req); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cargo test --all-features read_dtc_info_request_encode_tests` +Expected: FAIL — `Encode` not implemented for `ReadDTCInfoRequest`. + +- [ ] **Step 3: Implement `Encode` for `ReadDTCInfoSubFunction`** + +In `src/services/read_dtc_information.rs`, after the `impl ReadDTCInfoSubFunction { … value() … }` block, add: + +```rust +impl Encode for ReadDTCInfoSubFunction { + fn encoded_size(&self) -> usize { + use ReadDTCInfoSubFunction as S; + 1 + match self { + S::ReportNumberOfDTC_ByStatusMask(m) + | S::ReportDTC_ByStatusMask(m) + | S::ReportUserDefMemoryDTC_ByStatusMask(m) => m.encoded_size(), + S::ReportDTCSnapshotRecord_ByDTCNumber(r, n) => r.encoded_size() + n.encoded_size(), + S::ReportDTCStoredData_ByRecordNumber(n) => n.encoded_size(), + S::ReportDTCExtDataRecord_ByDTCNumber(r, n) => r.encoded_size() + n.encoded_size(), + S::ReportNumberOfDTC_BySeverityMaskRecord(s, m) + | S::ReportDTC_BySeverityMaskRecord(s, m) => s.encoded_size() + m.encoded_size(), + S::ReportSeverityInfoOfDTC(r) => r.encoded_size(), + S::ReportDTCExtDataRecord_ByRecordNumber(n) + | S::ReportSupportedDTCExtDataRecord(n) => n.encoded_size(), + S::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(r, n, mem) => { + r.encoded_size() + n.encoded_size() + mem.encoded_size() + } + S::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(r, n, mem) => { + r.encoded_size() + n.encoded_size() + mem.encoded_size() + } + S::ReportWWHOBDDTC_ByMaskRecord(g, m, s) => { + g.encoded_size() + m.encoded_size() + s.encoded_size() + } + S::ReportWWHOBDDTC_WithPermanentStatus(g) => g.encoded_size(), + S::ReportDTCInformation_ByDTCReadinessGroupIdentifier(g, rg) => { + g.encoded_size() + rg.encoded_size() + } + S::ReportDTCSnapshotIdentification + | S::ReportSupportedDTC + | S::ReportFirstTestFailedDTC + | S::ReportFirstConfirmedDTC + | S::ReportMostRecentTestFailedDTC + | S::ReportMostRecentConfirmedDTC + | S::ReportDTCFaultDetectionCounter + | S::ReportDTCWithPermanentStatus + | S::ISOSAEReserved(_) => 0, + } + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + use ReadDTCInfoSubFunction as S; + writer.write_all(&[self.value()]).map_err(Error::io)?; + match self { + S::ReportNumberOfDTC_ByStatusMask(m) + | S::ReportDTC_ByStatusMask(m) + | S::ReportUserDefMemoryDTC_ByStatusMask(m) => { + m.encode(writer)?; + } + S::ReportDTCSnapshotRecord_ByDTCNumber(r, n) => { + r.encode(writer)?; + n.encode(writer)?; + } + S::ReportDTCStoredData_ByRecordNumber(n) => { + n.encode(writer)?; + } + S::ReportDTCExtDataRecord_ByDTCNumber(r, n) => { + r.encode(writer)?; + n.encode(writer)?; + } + S::ReportNumberOfDTC_BySeverityMaskRecord(s, m) + | S::ReportDTC_BySeverityMaskRecord(s, m) => { + s.encode(writer)?; + m.encode(writer)?; + } + S::ReportSeverityInfoOfDTC(r) => { + r.encode(writer)?; + } + S::ReportDTCExtDataRecord_ByRecordNumber(n) + | S::ReportSupportedDTCExtDataRecord(n) => { + n.encode(writer)?; + } + S::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(r, n, mem) => { + r.encode(writer)?; + n.encode(writer)?; + mem.encode(writer)?; + } + S::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(r, n, mem) => { + r.encode(writer)?; + n.encode(writer)?; + mem.encode(writer)?; + } + S::ReportWWHOBDDTC_ByMaskRecord(g, m, s) => { + g.encode(writer)?; + m.encode(writer)?; + s.encode(writer)?; + } + S::ReportWWHOBDDTC_WithPermanentStatus(g) => { + g.encode(writer)?; + } + S::ReportDTCInformation_ByDTCReadinessGroupIdentifier(g, rg) => { + g.encode(writer)?; + rg.encode(writer)?; + } + S::ReportDTCSnapshotIdentification + | S::ReportSupportedDTC + | S::ReportFirstTestFailedDTC + | S::ReportFirstConfirmedDTC + | S::ReportMostRecentTestFailedDTC + | S::ReportMostRecentConfirmedDTC + | S::ReportDTCFaultDetectionCounter + | S::ReportDTCWithPermanentStatus + | S::ISOSAEReserved(_) => {} + } + Ok(self.encoded_size()) + } +} +``` + +(Note: `mem` is `MemorySelection` and `rg` is `DTCReadinessGroupIdentifier`; both are `type … = u8;` aliases, and `u8` already implements `Encode` with a 1-byte width.) + +- [ ] **Step 4: Implement `Encode` for `ReadDTCInfoRequest` (delegates)** + +In `src/services/read_dtc_information.rs`, after the `impl ReadDTCInfoRequest { … new() … }` block, add: + +```rust +impl Encode for ReadDTCInfoRequest { + fn encoded_size(&self) -> usize { + self.dtc_subfunction.encoded_size() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + self.dtc_subfunction.encode(writer) + } +} +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `cargo test --all-features read_dtc_info_request_encode_tests` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/services/read_dtc_information.rs +git commit -m "$(printf 'implement Encode for ReadDTCInfoSubFunction and ReadDTCInfoRequest\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 4: `Decode` for `WriteDataByIdentifierResponse` + +The fixed 2-byte echo response currently has `Encode` but no `Decode`, so it cannot round-trip. Add a 2-byte big-endian `Decode`. + +**Files:** +- Modify: `src/services/write_data_by_identifier.rs:2` (imports) and add a `Decode` impl + +- [ ] **Step 1: Write a failing round-trip test** + +In `src/services/write_data_by_identifier.rs`, inside `mod test`, add: + +```rust + #[test] + fn write_response_roundtrip() { + let response = WriteDataByIdentifierResponse::new(0xF186); + let mut buf = [0u8; 4]; + let written = Encode::encode(&response, &mut buf.as_mut_slice()).unwrap(); + let (decoded, rest) = + ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, response); + assert!(rest.is_empty()); + } + + #[test] + fn write_response_decode_rejects_short_buffer() { + let err = ::decode(&[0x01]); + assert!(err.is_err()); + } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cargo test --all-features write_response_roundtrip write_response_decode_rejects_short_buffer` +Expected: FAIL — `Decode` not implemented for `WriteDataByIdentifierResponse`. + +- [ ] **Step 3: Add `Decode` to imports and implement it** + +In `src/services/write_data_by_identifier.rs`, change the import line: + +```rust +use crate::{Encode, Error, NegativeResponseCode}; +``` + +to: + +```rust +use crate::{Decode, Encode, Error, NegativeResponseCode}; +``` + +Then add, after the `impl Encode for WriteDataByIdentifierResponse` block: + +```rust +impl<'a> Decode<'a> for WriteDataByIdentifierResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let identifier = u16::from_be_bytes([buf[0], buf[1]]); + Ok((Self { identifier }, &buf[2..])) + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cargo test --all-features write_response_roundtrip write_response_decode_rejects_short_buffer` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/services/write_data_by_identifier.rs +git commit -m "$(printf 'add Decode for WriteDataByIdentifierResponse (2-byte round-trip)\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 5: Crate-root integration tests for the completed types + +Prove the two newly-completed types are reachable through the explicit crate-root re-exports (validating Task 1) and behave correctly end-to-end (validating Tasks 3–4). Tests import only via `crate::…` (the public surface), not via internal module paths. + +**Files:** +- Modify: `src/lib.rs` (`#[cfg(test)] mod no_std_api_tests`) + +- [ ] **Step 1: Write the integration tests** + +In `src/lib.rs`, inside `mod no_std_api_tests`, add: + +```rust + #[test] + fn read_dtc_info_request_encodes_through_public_api() { + // Public-surface construction: types reached via crate root, not common::/services::. + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTC_ByStatusMask( + DTCStatusMask::from(0xFF), + )); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x02, 0xFF]); + assert_eq!(written, req.encoded_size()); + } + + #[test] + fn write_data_by_identifier_response_roundtrips_through_public_api() { + let resp = WriteDataByIdentifierResponse::new(0xBEEF); + let mut buf = [0u8; 4]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + let (decoded, rest) = + ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, resp); + assert!(rest.is_empty()); + } +``` + +- [ ] **Step 2: Run tests to verify they pass** + +Run: `cargo test --all-features read_dtc_info_request_encodes_through_public_api write_data_by_identifier_response_roundtrips_through_public_api` +Expected: PASS. (If a name does not resolve, Task 1's re-export list is missing it — add it.) + +- [ ] **Step 3: Commit** + +```bash +git add src/lib.rs +git commit -m "$(printf 'add crate-root integration tests for completed descriptor types\n\nCo-Authored-By: Claude Opus 4.8 ')" +``` + +--- + +## Task 6: Full-matrix verification + +Confirm the whole CI matrix is green before considering Phase 1 done. No code changes; if any command fails, fix the offending task and re-run. + +- [ ] **Step 1: Ensure the bare-metal target is installed** + +Run: `rustup target add thumbv6m-none-eabi` +Expected: installed (or "up to date"). + +- [ ] **Step 2: Build + test (host, all features)** + +Run: +```bash +cargo build --all-features --release +cargo test --all-features +``` +Expected: PASS. + +- [ ] **Step 3: no_std / no_alloc builds** + +Run: +```bash +cargo build --no-default-features --target thumbv6m-none-eabi +cargo build --no-default-features --features alloc --target thumbv6m-none-eabi +``` +Expected: PASS. + +- [ ] **Step 4: Clippy on all host feature combos** + +Run: +```bash +cargo clippy --all-features +cargo clippy --no-default-features +cargo clippy --no-default-features --features alloc +``` +Expected: no warnings. (The crate sets `#![warn(clippy::pedantic, missing_docs)]`; resolve any new lints — all added items are documented and `#[must_use]` where applicable.) + +- [ ] **Step 5: Formatting + docs** + +Run: +```bash +cargo fmt -- --check +cargo doc --release --all-features --no-deps +``` +Expected: PASS (no diffs, no doc warnings). + +- [ ] **Step 6: (No commit)** — verification only. Phase 1 complete. + +--- + +## Self-review notes + +- **Spec coverage:** Phase-1 commit 1 → Task 1; commit 2 → Task 2; commit 3 → Task 3; commit 4 → Task 4; commit 5 (repurposed) → Task 5; matrix testing → Task 6. Phase 2 items are intentionally out of scope. +- **No enum restructuring:** confirmed — no task edits `Request`/`Response` variant shapes. +- **Type consistency:** `Encode`/`Decode` signatures match `src/traits.rs`; `assert_encode_size_agrees` matches `src/test_util.rs`; parameter accessor methods (`value()`, `bits()`, field `.0`) match the definitions in `src/common/`. +- **Latent bug:** `FunctionalGroupIdentifier::value()` `todo!()` panic is fixed in Task 2 (required for its `Encode`). diff --git a/docs/superpowers/specs/2026-06-01-no-std-api-alignment-design.md b/docs/superpowers/specs/2026-06-01-no-std-api-alignment-design.md new file mode 100644 index 0000000..5cabaa6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-no-std-api-alignment-design.md @@ -0,0 +1,242 @@ +# UDS Protocol — no_std API Alignment Design + +**Date:** 2026-06-01 +**Branch:** `feature/no_std` +**Status:** Approved scope, pending implementation plan + +## Purpose + +Before landing the `no_std` rearchitecture, align the public API with the crate's +revised scope so that breaking changes happen once, not in a series of post-publish +follow-ups. The scope changed from "desktop diagnostic tooling" to **a pure, +synchronous, runtime-agnostic UDS codec usable on `no_std` + `no_alloc` targets.** + +## Confirmed scope (decisions) + +- **Pure codec.** The crate encodes/decodes UDS messages only. Transport (DoIP, + UDSonIP, sync or async) lives in downstream crates that depend on this one. +- **Fully synchronous.** No async anywhere. `Encode` writes into any + `embedded_io::Write`; `Decode` borrows from a `&[u8]`. Callers — sync or async — + own the I/O loop. The crate never owns a runtime, a socket, or a buffer it + allocates. +- **`no_std` + `no_alloc` baseline.** `alloc` and `std` are strictly additive + features. This is already true structurally and must stay true. + +## Guiding principle — simplicity for C developers new to Rust + +The growing user base is competent C developers who are new to Rust. **Simplicity is +a first-class acceptance criterion, not a nice-to-have:** prefer concrete types over +generics, obvious over clever, fewer types over more. This directly motivates +Decisions 1–3 (generics and trait bounds are the steepest part of Rust's learning +curve) and Decision 5 (fewer types). The one Rust concept that cannot be designed +away on a `no_alloc` target is **borrowing** — decoded values point into the caller's +buffer — so the documentation must teach that model explicitly in C-familiar terms +(see Decision 9). + +## What is NOT changing + +- The `Encode` (stream) / `Decode` (borrow-from-slice) split — this is the correct + shape for `no_alloc` and is kept. +- The concrete fixed-size service types (`EcuResetRequest`, `TesterPresentRequest`, + the `*Response` fixed types, `NegativeResponse`, etc.). +- The `UDSIdentifier` / `UDSRoutineIdentifier` enums (kept; see Decision 2). +- The `ReadDTCInfoResponseRx` "typed enum holding raw bytes + lazy iterators" + pattern — this is the model the rest of the RX surface should resemble. +- The existing `no_std` / `alloc` / bare-metal CI matrix. + +--- + +## Decisions + +### Decision 1 — Remove the orphaned generic-typing abstraction + +`DiagnosticDefinition` is exported with a worked example (`UdsSpec`) but **nothing +consumes it** — the only references are its own definition and the `UdsSpec` impl. +The decode path already produces raw `&[u8]` for the identifier-driven services. The +original highly-genericized design was found to be harder to understand than simply +carrying payloads, and the abstraction did not survive the `no_std` pass. + +**Action:** Delete `DiagnosticDefinition` and `UdsSpec`. Commit fully to the +payload-carrying model: dispatch enums are non-generic and hand back raw payload +bytes; callers interpret those bytes themselves. + +### Decision 2 — Strip all identifier machinery + +With `DiagnosticDefinition` gone, the remaining generics exist only to support +pluggable, user-defined identifier types — which the payload-carrying model no longer +needs. + +**Remove:** +- The `Identifier` and `RoutineIdentifier` traits. +- The `impl_identifier!` macro. +- The blanket `Encode` / `Decode` / `DecodeIter` impls for `T: Identifier` + (`src/traits.rs`). +- `ProtocolIdentifier`, `ProtocolPayloadTx`, `ProtocolRoutinePayloadTx` + (`src/protocol_definitions.rs` — the module is deleted). + +**Keep:** +- `UDSIdentifier` and `UDSRoutineIdentifier` enums, with their existing + `TryFrom` / `From for u16` conversions. +- Because these enums currently obtain `Encode`/`Decode` via the blanket impl, give + each a **direct, concrete** `Encode` + `Decode` impl (2-byte big-endian) so users + can still serialize/parse a known identifier when they want to. This is the only + identifier-level codec that survives. + +**Net effect:** the crate exposes raw payload bytes plus the canonical +`UDSIdentifier` / `UDSRoutineIdentifier` enums. No generic identifier plumbing. + +### Decision 3 — De-genericize the service builders to match RX + +The TX builders for the identifier services are the last place generics survive. +De-genericize them so TX mirrors the raw-bytes RX side. + +| Service | Current (TX) | New (TX) carries | +|---|---|---| +| ReadDataByIdentifier | `ReadDataByIdentifierRequestTx<'d, DID>` | `&'d [u16]` DID list (BE-encoded on write) | +| WriteDataByIdentifier | `WriteDataByIdentifierRequest` | `&'d [u8]` raw (DID + data) | +| WriteDataByIdentifier (resp) | `WriteDataByIdentifierResponse` | `u16` echoed identifier (fixed, 2 bytes) | +| RoutineControl (req) | `RoutineControlRequest` | `sub_function` + `&'d [u8]` raw (RID + data) | +| RoutineControl (resp) | `RoutineControlResponse` | `routine_control_type` + `&'d [u8]` raw status record | + +Rationale for `&[u16]` on the Read-DID request: it is a list of identifiers, and a +`u16` slice avoids an endianness footgun while staying alloc-free and generic-free. A +DID is conceptually a 16-bit number, so `&[u16]` reads more clearly to a C developer +than a byte-pair-encoded `&[u8]` would. All other payloads are opaque bytes and carry +`&[u8]`, exactly matching what the RX enum variants already hold. **Resolved:** keep +`&[u16]` for Read-DID (was previously an open risk). + +### Decision 4 — Apply the Tx/Rx naming convention strictly + +**Rule:** fixed-size bidirectional types take **no suffix**; zero-copy borrowed TX +types take **`...Tx`**; zero-copy borrowed RX types take **`...Rx`**. + +Renames required to make the existing (correct) intent consistent: + +| Current name | New name | Reason | +|---|---|---| +| `WriteDataByIdentifierRequest` | `WriteDataByIdentifierRequestTx` | now carries borrowed bytes | +| `RoutineControlRequest` | `RoutineControlRequestTx` | now carries borrowed bytes | +| `RoutineControlResponse` | `RoutineControlResponseTx` | now carries borrowed bytes | + +Types already conforming and unchanged: `ReadDataByIdentifierRequestTx`, +`TransferDataRequestTx`, `SecurityAccessRequestTx`, `RequestFileTransferRequestTx`, +`RequestDownloadResponseTx`, `SecurityAccessResponseTx`, `TransferDataResponseTx`, +`RequestFileTransferResponseTx`, `ReadDTCInfoResponseRx`, and all fixed-size +no-suffix types. `WriteDataByIdentifierResponse` stays unsuffixed (fixed `u16`). + +### Decision 5 — Unify the raw passthrough / unknown-service escape hatch + +Today the escape hatch is asymmetric: `Response` has both a raw `UdsResponse<'a>` +view and raw-slice variants, while unmodeled services on the `Request` side hard-error +with `ServiceNotImplemented`. + +**Action:** Add symmetric `Other { service: UdsServiceType, data: &'a [u8] }` variants +to **both** `Request<'a>` and `Response<'a>`. A frame for a known-but-unmodeled service +decodes into `Other` rather than erroring, so downstream transport code can pass it +through. Remove the standalone `UdsResponse<'a>` type — `Response::Other` subsumes its +"don't parse, just hand me service + bytes" role. `ServiceNotImplemented` is retained +in `Error` only for genuinely unrecognized service bytes (if any remain) and may be +removed if `Other` covers all cases — to be settled in the implementation plan. + +### Decision 6 — Move `is_positive_response_suppressed` off `Encode` + +This is UDS protocol semantics (SPRMIB) bolted onto a serialization trait; it is +meaningless for most `Encode` impls. Remove it from the `Encode` trait. Expose it as +an **inherent method** on the request types that actually carry a suppress bit +(those already wrapping `SuppressablePositiveResponse`) and on `Request<'a>`, which +forwards to its inner variant. `Encode` becomes a clean, general codec trait. + +### Decision 7 — Enforce the `encode` / `encoded_size` invariant + +Many `encode` impls return `self.encoded_size()` after writing, with no guard against +the two diverging. A caller pre-sizing a buffer from `encoded_size()` would silently +overflow/underfill if they drift. + +**Action:** Document the invariant on `Encode::encoded_size` ("must equal the byte +count `encode` writes"). Add a small generic test helper — +`assert_encode_size_agrees(value: &T)` — that encodes into a counting +writer and asserts the returned length equals `encoded_size()`, and apply it across +every service's unit tests. + +### Decision 8 — Document the `Decode` remainder contract + +`Decode::decode` returns `(value, remaining)` for composition, but every top-level +frame decoder consumes the whole buffer and returns `&[]`. Document on the trait that +frame-level decoders are whole-buffer (use `decode_exact` semantics) and the remainder +is meaningful only for leaf/sequence decoding. No code change beyond doc comments. + +### Decision 9 — Document the integration model (scope alignment) + +The README is a stub. Add an **Integration** section stating the contract explicitly: + +> This crate is a synchronous, allocation-free codec. To use it over any transport +> (DoIP, UDSonIP, ISO-TP, …), decode inbound frames from the received `&[u8]` and +> encode outbound frames into any `embedded_io::Write` or a caller-owned buffer sized +> via `encoded_size()`. The crate owns no sockets, buffers, or async runtime; drive +> I/O from your own sync or async layer. + +Include one short encode + one short decode snippet. This prevents downstream authors +from re-introducing async coupling at the codec layer. + +Because the audience is C developers new to Rust, the decode snippet must make the +**borrow** explicit: the decoded value points into the receive buffer you passed in +(like a `struct` overlaid on a `char buf[]`), and is valid only while that buffer +lives — copy out any fields you need to keep. This is the single Rust-specific concept +the docs must land clearly. + +### Decision 10 — State the service-coverage boundary + +`UdsServiceType` enumerates ~10 services the dispatch enums do not model +(`Authentication`, `ReadMemoryByAddress`, `RequestUpload`, `ResponseOnEvent`, etc.). +With Decision 5, frames for these decode into `Other` rather than erroring. Document, +in the crate root, the explicit list of fully-modeled services vs. those reached only +through `Other`, so coverage is a stated decision rather than an accident. + +--- + +## Components touched + +- `src/traits.rs` — remove `Identifier`/`RoutineIdentifier`/`impl_identifier!` and + the three blanket impls; remove `is_positive_response_suppressed` from `Encode`; + expand `Decode` doc comments; remove `DiagnosticDefinition`. +- `src/protocol_definitions.rs` — **deleted** (module + `pub use` removed from `lib.rs`). +- `src/lib.rs` — drop `UdsSpec`, `DiagnosticDefinition`, and `protocol_definitions` + re-exports; update exports for renamed types. +- `src/common/diagnostic_identifier.rs` — replace `impl_identifier!` usage with direct + `Encode`/`Decode` impls for `UDSIdentifier` / `UDSRoutineIdentifier`. +- `src/services/read_data_by_identifier.rs` — `ReadDataByIdentifierRequestTx<'d>` over + `&'d [u16]`. +- `src/services/write_data_by_identifier.rs` — `WriteDataByIdentifierRequestTx<'d>` + over `&'d [u8]`; `WriteDataByIdentifierResponse { identifier: u16 }`. +- `src/services/routine_control.rs` — `RoutineControlRequestTx<'d>` / + `RoutineControlResponseTx<'d>` over `&'d [u8]`. +- `src/request.rs` / `src/response.rs` — add `Other { service, data }`; remove + `UdsResponse`; move suppression to inherent method; update construction for renamed + builders. +- `README.md` — add Integration section + snippets. +- Service test modules — apply `assert_encode_size_agrees`. + +## Testing + +- Preserve all existing round-trip tests; update them for renamed types and the new + builder signatures. +- Add the `assert_encode_size_agrees` harness and apply per service. +- Add decode tests that exercise `Request::Other` / `Response::Other` for a + known-but-unmodeled service byte. +- Verify the full matrix still builds and passes: default (`std`), + `--no-default-features --features alloc`, `--no-default-features`, and + `thumbv6m-none-eabi`. Clippy clean on all host combos. + +## Out of scope (explicitly deferred) + +- Implementing additional UDS services (`Authentication`, `ReadMemoryByAddress`, + `RequestUpload`, etc.). They remain reachable via `Other`. +- Any transport, session, or async layer — belongs in downstream crates. +- An `embedded-io-adapters` std-bridge convenience (can be a later additive feature). + +## Risks + +- **Broad rename churn.** Mitigated by no external consumers (no `examples/`, + `tests/`, or dependent crates in-tree) and a green CI matrix to catch breakage. +- **`Other` vs `ServiceNotImplemented` overlap.** The implementation plan must settle + whether `ServiceNotImplemented` is fully removed or retained for truly unknown bytes. diff --git a/docs/superpowers/specs/2026-06-03-api-consistency-phase2-design.md b/docs/superpowers/specs/2026-06-03-api-consistency-phase2-design.md new file mode 100644 index 0000000..b08fee6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-api-consistency-phase2-design.md @@ -0,0 +1,201 @@ +# UDS Protocol — API Consistency Phase 2 Design + +**Date:** 2026-06-03 +**Branch:** `feature/no_std` +**Status:** Design — pending user review, then implementation plan +**Follows:** `2026-06-03-api-exposure-and-consistency-design.md` (Phase 1, landed). This resolves +the Phase 2 open questions recorded there. + +## Purpose + +Phase 1 made the public surface deliberate and completed the codec-incomplete descriptors. +Phase 2 finishes the cohesive breaking change by removing the remaining inconsistencies a +reviewer sees immediately: + +1. The `…Tx`/`…Rx` suffixes mislead — most "Tx" descriptors are actually bidirectional + (they implement both `Encode` and `Decode` and appear in both the `Request` and + `Response` enums). +2. The dispatch enums model the same class of service two different ways — some variants wrap + a named descriptor type, others hold a bare `&[u8]` or inline fields. +3. The variable-length big-endian integer codec is hand-rolled in ~5 places. +4. Two byte-identical primitive macros exist where one would do. + +All of this lands on `feature/no_std` so the entire breaking change ships together. + +## Guiding principle (unchanged) + +Concrete types, no generics, no user-supplied types. Lightweight typed descriptors that +faithfully model the ISO-14229 message elements on TX; borrowed slices / lazy iterators for +variable-length RX sequences. Simplicity for C developers new to Rust is a first-class goal — +which is why the naming rule below removes the `Tx`/`Rx` jargon everywhere it is not +load-bearing. + +--- + +## Decision 1 — Suffix marks asymmetry, not direction + +**Rule:** a `…Tx` or `…Rx` suffix is used **only** when a service's TX and RX representations +are genuinely different types. A type that is bidirectional (implements both `Encode` and +`Decode`, and is used on both the request-building and response-parsing paths) takes **no +suffix** and reads as plain `FooRequest` / `FooResponse`. The `<'a>` lifetime already signals +that a type borrows from the wire buffer. + +**Renames (all verified bidirectional — they implement both `Encode` and `Decode`):** + +| Current | New | +|---|---| +| `SecurityAccessRequestTx` | `SecurityAccessRequest` | +| `SecurityAccessResponseTx` | `SecurityAccessResponse` | +| `TransferDataRequestTx` | `TransferDataRequest` | +| `TransferDataResponseTx` | `TransferDataResponse` | +| `RequestFileTransferRequestTx` | `RequestFileTransferRequest` | +| `RequestFileTransferResponseTx` | `RequestFileTransferResponse` | +| `RequestDownloadResponseTx` | `RequestDownloadResponse` | +| `RoutineControlRequestTx` | `RoutineControlRequest` | +| `RoutineControlResponseTx` | `RoutineControlResponse` | +| `WriteDataByIdentifierRequestTx` | `WriteDataByIdentifierRequest` | +| `ReadDTCInfoResponseRx` | `ReadDTCInfoResponse` | +| `NamePayloadTx` | `NamePayload` | +| `SentDataPayloadTx` | `SentDataPayload` | + +**Keeps its suffix — the single genuine asymmetry:** `ReadDataByIdentifierRequestTx` +(`&[u16]` DID list on TX; the wire cannot be reinterpreted as `&[u16]` zero-copy, so RX is +raw `&[u8]`). + +Already correctly unsuffixed and unchanged: `RequestDownloadRequest`, +`WriteDataByIdentifierResponse`, `ReadDTCInfoRequest`, `PositionPayload`, `SizePayload`, +`FileSizePayload`, `DirSizePayload`, `FileOperationMode`, and all fixed-size service types. + +All renames must update the explicit crate-root re-export lists in `src/lib.rs` (from Phase 1) +and every doc-comment reference. + +## Decision 2 — Wrap descriptors in the enums where feasible + +Each modeled `Request`/`Response` variant wraps a single named descriptor type wherever a +zero-copy decode allows it, replacing bare `&[u8]` and inline-field variants. This eliminates +the orphaned descriptor types (the enums now construct them) and makes the variants +round-trippable. + +| Variant | Before | After | Decode work | +|---|---|---|---| +| `Request::WriteDataByIdentifier` | `(&[u8])` | `(WriteDataByIdentifierRequest)` | add trivial `Decode` (borrow whole payload) | +| `Request::ReadDTCInfo` | `(&[u8])` | `(ReadDTCInfoRequest)` | add **25-variant `Decode`** (inverse of Phase 1 `Encode`) | +| `Request::RoutineControl` | `{ sub_function: u8, raw_payload: &[u8] }` | `(RoutineControlRequest)` | add `Decode` via `SuppressablePositiveResponse` + borrowed payload (see below) | +| `Response::WriteDataByIdentifier` | `(&[u8])` | `(WriteDataByIdentifierResponse)` | `Decode` already added in Phase 1 | +| `Response::RoutineControl` | `{ routine_control_type: u8, raw_status_record: &[u8] }` | `(RoutineControlResponse)` | add `Decode` | + +**RDBI is the documented exception.** `Request::ReadDataByIdentifier(&[u8])` and +`Response::ReadDataByIdentifier(&[u8])` stay raw, because the DID list cannot be produced as +`&[u16]` zero-copy. `ReadDataByIdentifierRequestTx` remains a TX-only build/encode helper. + +The `Request`/`Response` `Encode`/`encoded_size`/`service`/`response_sid` match arms and +`is_positive_response_suppressed` are updated for the new variant shapes. + +### Sub-function byte fidelity (resolved) + +The wire sub-function byte for RoutineControl carries the SPRMIB suppress bit (`0x80`) in its +high bit and the `routineControlType` in the low 7 bits. RoutineControl is modeled exactly +like every other sub-function service (`EcuResetRequest` is the template), using the existing +internal `SuppressablePositiveResponse` wrapper: + +```rust +pub struct RoutineControlRequest<'d> { + sub_function: SuppressablePositiveResponse, + raw_payload: &'d [u8], +} +``` + +`RoutineControlSubFunction` already satisfies the wrapper's bounds (`TryFrom` ++ `From<…> for u8`). On decode, `SuppressablePositiveResponse::try_from(payload[0])?` splits the +byte into `(suppress_flag, RoutineControlSubFunction::try_from(byte & 0x7F))`; on encode, +`u8::from(self.sub_function)` re-applies the bit — so the suppress bit round-trips losslessly. +Following the `EcuResetRequest` pattern, the `SuppressablePositiveResponse` field is private +(the type stays `pub(crate)`); the public surface is +`new(suppress_positive_response: bool, sub_function: RoutineControlSubFunction, raw_payload: &'d [u8])` +plus `suppress_positive_response()`, `sub_function()`, and `raw_payload()` getters. + +This also lets `Request::is_positive_response_suppressed()` forward to RoutineControl (today it +falls through to `false`). + +`RoutineControlResponse.routine_control_type` is a plain `RoutineControlSubFunction` (responses +never carry the SPRMIB bit — a suppressed request produces no positive response), mirroring how +`EcuResetResponse` holds a plain `ResetType`. + +**One intentional behavior change:** the typed form rejects reserved `routineControlType` values +(low 7 bits outside 0x01–0x03) on decode with `IncorrectMessageLengthOrInvalidFormat`, where the +old raw-`u8` variant accepted any byte. ISO defines only 0x01–0x03, and every sibling +sub-function service already rejects unknown values, so this makes RoutineControl consistent. +A round-trip test with the suppress bit set (e.g. `0x81`) locks the fidelity in. + +## Decision 3 — Deduplicate the variable-length big-endian integer codec + +The pattern +`let mut b = [0u8; N]; b[N-n..].copy_from_slice(&src[..n]); T::from_be_bytes(b)` (and its +`to_be_bytes()[N-n..]` encode twin) is duplicated across `SizePayload`, `FileSizePayload`, +`DirSizePayload` (`request_file_transfer.rs`) and `RequestDownloadRequest` +(`request_download.rs`). + +**Action:** add one concrete, non-generic helper pair to `src/common/util.rs`: + +- `read_be_uint(src: &[u8], n: usize) -> Result` — left-pads `n` big-endian + bytes into a `u128`; returns `Error::InsufficientData(n)` when `src.len() < n`, and rejects + `n > 16`. +- `write_be_uint(value: u128, n: usize, writer: &mut impl embedded_io::Write) -> Result` + — writes the low `n` big-endian bytes of `value`. + +`u128` is the widest case, so the `u64`/`u32` call sites (`RequestDownloadRequest`) cast down +from the returned `u128` (`as u64` / `as u32`). No generics — one concrete pair serves every +site. Whether these helpers are part of the public API or `pub(crate)` is settled in the plan +(default: `pub(crate)`, since they are an internal codec detail). + +## Decision 4 — Merge the duplicate primitive macros + +`unsigned_primitive_encode_decode!` and `signed_primitive_encode_decode!` in +`src/common/primitive_generics.rs` have byte-identical bodies (both use `to_be_bytes` / +`from_be_bytes`). Merge into a single `primitive_encode_decode!` macro invoked once with all +ten integer types (`u8, u16, u32, u64, u128, i8, i16, i32, i64, i128`). + +--- + +## Components touched + +- `src/lib.rs` — update explicit re-export lists for renamed types. +- `src/services/security_access.rs`, `transfer_data.rs`, `request_file_transfer.rs`, + `request_download.rs`, `routine_control.rs`, `write_data_by_identifier.rs`, + `read_dtc_information.rs` — type renames; add `Decode` impls where Decision 2 requires; + call the new `read_be_uint`/`write_be_uint` helpers. +- `src/request.rs` / `src/response.rs` — rewire the four+ variants to wrap descriptors; update + all match arms; rename referenced types. +- `src/common/util.rs` — add `read_be_uint` / `write_be_uint`. +- `src/common/primitive_generics.rs` — merge the two macros. + +## Testing + +- Round-trip (`Request`/`Response` `decode` → `encode` → bytes identical) for every + newly-wrapped variant: `WriteDataByIdentifier` (req & resp), `ReadDTCInfo` (req), + `RoutineControl` (req & resp). +- A `RoutineControl` round-trip with the SPRMIB suppress bit set, asserting the byte survives. +- `assert_encode_size_agrees` on every new/changed `Encode` impl. +- Unit tests for `read_be_uint`/`write_be_uint` (including `n=0`, max width, and + `InsufficientData`), and confirmation the refactored payload decoders still pass their + existing round-trip tests. +- Full CI matrix green: default (`std`), `--no-default-features --features alloc`, + `--no-default-features`, `thumbv6m-none-eabi`; clippy clean on all host combos; `fmt` clean. + +## Out of scope + +- Implementing additional UDS services (still reached via `Other`). +- Any transport, session, or async layer. +- Merging `feature/no_std` to `main` — happens after Phase 2 lands, as the full cohesive + breaking change. + +## Risks + +- **Broad rename churn.** 13 renamed types ripple across services, the enums, re-exports, + tests, and docs. Mitigated by no external in-tree consumers and a green CI matrix; a final + grep for the old names confirms none remain. +- **`ReadDTCInfoRequest::Decode` correctness across 25 variants.** Mitigated by per-variant + round-trip tests reusing the Phase 1 `Encode` as the oracle (encode a known value, decode it + back, assert equality). +- **Routine-control sub-function byte regression.** Explicitly guarded by the SPRMIB round-trip + test (Decision 2). diff --git a/docs/superpowers/specs/2026-06-03-api-exposure-and-consistency-design.md b/docs/superpowers/specs/2026-06-03-api-exposure-and-consistency-design.md new file mode 100644 index 0000000..760370e --- /dev/null +++ b/docs/superpowers/specs/2026-06-03-api-exposure-and-consistency-design.md @@ -0,0 +1,124 @@ +# UDS Protocol — API Exposure & Consistency Design + +**Date:** 2026-06-03 +**Branch:** `feature/no_std` +**Status:** Design — pending user review, then implementation plan +**Follows:** `2026-06-01-no-std-api-alignment-design.md` (this completes and tightens that work; it does **not** change tack) + +## Purpose + +The `no_std` rearchitecture is substantially landed but left two consistency gaps a +reviewer spots immediately: (1) several intended-public types are reachable only +through glob re-exports and some are codec-incomplete, and (2) the `Request` / +`Response` enums model the same class of service inconsistently (some wrap a typed +descriptor, others hand back raw bytes or decomposed fields). We want to ship this +major breaking change as **one cohesive chunk**, so we close these before publishing. + +This is split into two phases. **Phase 1** (this doc, ready to implement) deliberately +exposes and completes the descriptor types without restructuring the dispatch enums. +**Phase 2** (framed here, to be planned separately) resolves the naming and +typed-vs-raw symmetry questions across the enums. + +## Guiding principle (unchanged, clarified) + +Carried from the prior design: **concrete types, no generics, no user-supplied types.** +Clarified here: "lightweight descriptor" means a concrete type that **faithfully models +the ISO-14229 message elements** so a caller can describe a request precisely — it does +**not** mean "minimal" or "raw." Where ISO enumerates the structure (e.g. the ~25 +`ReadDTCInformation` sub-functions), the descriptor models it fully. Where the payload +is opaque or caller-defined (routine parameters, write data records, security seeds), +it is carried as a borrowed `&[u8]`. + +- **TX side:** lightweight typed descriptors that implement `Encode`. +- **RX side:** borrowed slices / lazy iterators for variable-length response sequences + (the `ReadDTCInfoResponseRx` model); fixed/small spec structures stay typed. + +## Confirmed decisions for Phase 1 + +- **Deliberate exposure.** Every intended-public type is re-exported **by name** from + the crate root. Glob re-exports (`pub use services::*;`, `pub use common::*;`) are + replaced with explicit named lists in `lib.rs`. Rationale: an intentional public + surface, individually documented types, and globs are how the current orphans hid. +- **Complete the codec-incomplete descriptors.** `ReadDTCInfoRequest` gains a real + `Encode`; `WriteDataByIdentifierResponse` gains `Decode`. The DTC parameter types + that `ReadDTCInfoRequest` needs gain `Encode`. +- **No enum restructuring in Phase 1.** `Request` / `Response` variant shapes are left + exactly as they are; the typed-vs-raw symmetry decision is Phase 2. +- **No speculative code (YAGNI).** Parameter types gain `Encode` only (not `Decode`); + `Decode` is added later only if Phase 2 decides to type the request RX path. + +## Phase 1 — commit series + +Each commit builds and tests green on its own across the CI matrix. + +1. **Explicit crate-root re-exports.** Replace `pub use services::*;` and + `pub use common::*;` in `src/lib.rs` with explicit named re-exports. No type changes; + pure surface tightening. Verify nothing public is dropped or newly hidden. +2. **`Encode` for the 1-byte DTC parameter types** currently lacking it: + `DTCSeverityMask`, `FunctionalGroupIdentifier`, `DTCSnapshotRecordNumber`, + `DTCExtDataRecordNumber`, `DTCStoredDataRecordNumber`. (`DTCStatusMask` and + `DTCRecord` already implement `Encode`.) Each is a small, faithful 1-byte/3-byte + spec encoder with `assert_encode_size_agrees` coverage. +3. **`Encode` + `encoded_size` for `ReadDTCInfoRequest`** over all 25 + `ReadDTCInfoSubFunction` variants: write the sub-function byte, then encode each + variant's typed parameters (now that they all implement `Encode`). Add encode tests + with `assert_encode_size_agrees` for representative variants (no-param, + single-param, multi-param, `ISOSAEReserved`). +4. **`Decode` for `WriteDataByIdentifierResponse`** (2-byte big-endian `u16`), with a + round-trip test. +5. **Usage / round-trip tests for the TX builders** so none is dead: + `ReadDataByIdentifierRequestTx`, `WriteDataByIdentifierRequestTx`, + `RoutineControlRequestTx`, `RoutineControlResponseTx`. Confirms each is constructible + and encodes to the expected wire bytes. + +### Phase 1 testing + +- All new `Encode`/`Decode` impls covered by `assert_encode_size_agrees` and explicit + wire-byte assertions. +- Full matrix builds and passes: default (`std`), + `--no-default-features --features alloc`, `--no-default-features`, and + `thumbv6m-none-eabi`. Clippy clean on all host combos. +- A doc/grep check that the explicit re-export list covers everything the glob did. + +## Phase 2 — open questions (to be planned after Phase 1) + +Phase 2 is **not** specified here — it is the set of decisions to work through next. +Captured now so they are not lost. Notably, the RX side is already +borrowed-slice/bidirectional, so this is mostly naming and symmetry, not a heavy +re-parse-to-raw migration. + +1. **`...ResponseTx` naming (Decision 4 follow-up).** `SecurityAccessResponseTx`, + `TransferDataResponseTx`, `RequestDownloadResponseTx`, and + `RequestFileTransferResponseTx` are decoded on the RX path (wrapped in `Response`) + yet carry the TX-only `...Tx` suffix. They are really *bidirectional borrowed* types, + a case Decision 4 never named. Ruling needed: rename to `...Rx`, keep `...Tx`, or + define a bidirectional/no-suffix convention. +2. **Typed-vs-raw RX symmetry (the core fork).** The enums are inconsistent: + `SecurityAccess` / `TransferData` / `RequestFileTransfer` / `RequestDownload` wrap a + typed descriptor, while `ReadDataByIdentifier` / `WriteDataByIdentifier` / + `ReadDTCInfo` / `RoutineControl` hand back raw `&[u8]` or decomposed + `{ sub_function: u8, raw_payload }`. Decide whether decode should produce the typed + descriptors (symmetric, round-trippable) or whether the raw holdouts should be + normalized further toward raw. This is the inconsistency the code review flagged. +3. **Dedup the variable-length integer codec.** The + `[0u8; N]` + `copy_from_slice` + `from_be_bytes` pattern (and its `to_be_bytes` + encode twin) is duplicated across `SizePayload`, `FileSizePayload`, `DirSizePayload` + (`request_file_transfer.rs`) and `RequestDownloadRequest` (`request_download.rs`). + Extract a shared `read_var_be_uint` / `write_var_be_uint` helper. +4. **Merge the duplicated primitive macros.** `unsigned_primitive_encode_decode!` and + `signed_primitive_encode_decode!` in `primitive_generics.rs` have byte-for-byte + identical bodies; collapse to one macro. + +## Out of scope + +- Implementing additional UDS services (still reached via `Other`). +- Any transport, session, or async layer. +- The Phase 2 decisions themselves — only their framing is recorded here. + +## Risks + +- **Public-surface drift when de-globbing.** Mitigated by a grep/doc diff confirming the + explicit list matches the glob's prior exports before committing. +- **`ReadDTCInfoRequest::encode` correctness across 25 variants.** Mitigated by + per-variant `assert_encode_size_agrees` and wire-byte tests, and by reusing the + parameter types' own `Encode` impls rather than re-deriving byte layouts. diff --git a/src/common/diagnostic_identifier.rs b/src/common/diagnostic_identifier.rs index 06f136c..b8bab81 100644 --- a/src/common/diagnostic_identifier.rs +++ b/src/common/diagnostic_identifier.rs @@ -1,5 +1,5 @@ //! DIDs are used to identify the data that is requested or sent in a diagnostic service. -use crate::{Error, impl_identifier, traits::RoutineIdentifier}; +use crate::{Decode, Encode, Error}; /// C.1 DID - Diagnostic Data Identifier specified in ISO 14229-1 /// @@ -94,7 +94,6 @@ pub enum UDSIdentifier { /// Reserved for ISO 15765-5 (`0xFF01`). ReservedForISO15765_5 = 0xFF01, } -impl_identifier!(UDSIdentifier); impl TryFrom for UDSIdentifier { type Error = Error; @@ -190,15 +189,15 @@ impl From for u16 { } } -impl std::fmt::Display for UDSIdentifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for UDSIdentifier { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let value: u16 = (*self).into(); write!(f, "{value:#06X?}") } } -impl std::fmt::Debug for UDSIdentifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for UDSIdentifier { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let value: u16 = (*self).into(); write!(f, "{value:#06X}") } @@ -257,7 +256,6 @@ pub enum UDSRoutineIdentifier { /// 0xFF01 CheckProgrammingDependencies = 0xFF01, } -impl_identifier!(UDSRoutineIdentifier); /// We know all values for the Routine Identifier, so we can implement `From` for `UDSRoutineIdentifier` impl From for UDSRoutineIdentifier { @@ -295,4 +293,78 @@ impl From for u16 { } } -impl RoutineIdentifier for UDSRoutineIdentifier {} +impl Encode for UDSIdentifier { + fn encoded_size(&self) -> usize { + 2 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&u16::from(*self).to_be_bytes()) + .map_err(Error::io)?; + Ok(2) + } +} + +impl<'a> Decode<'a> for UDSIdentifier { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + let raw = u16::from_be_bytes([buf[0], buf[1]]); + Ok((Self::try_from(raw)?, &buf[2..])) + } +} + +impl Encode for UDSRoutineIdentifier { + fn encoded_size(&self) -> usize { + 2 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&u16::from(*self).to_be_bytes()) + .map_err(Error::io)?; + Ok(2) + } +} + +impl<'a> Decode<'a> for UDSRoutineIdentifier { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + let raw = u16::from_be_bytes([buf[0], buf[1]]); + Ok((Self::from(raw), &buf[2..])) + } +} + +#[cfg(test)] +mod codec_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn uds_identifier_roundtrip() { + let id = UDSIdentifier::ActiveDiagnosticSession; + let mut buf = [0u8; 2]; + Encode::encode(&id, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xF1, 0x86]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, id); + assert!(rest.is_empty()); + assert_encode_size_agrees(&id); + } + + #[test] + fn uds_routine_identifier_roundtrip() { + let id = UDSRoutineIdentifier::EraseMemory; + let mut buf = [0u8; 2]; + Encode::encode(&id, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xFF, 0x00]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, id); + assert!(rest.is_empty()); + assert_encode_size_agrees(&id); + } +} diff --git a/src/common/dtc_ext_data.rs b/src/common/dtc_ext_data.rs index eba7055..56d0d2a 100644 --- a/src/common/dtc_ext_data.rs +++ b/src/common/dtc_ext_data.rs @@ -1,19 +1,15 @@ -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::{Decode, Encode, Error}; -use crate::{ - DTCRecord, DTCStatusMask, Error, IterableWireFormat, SingleValueWireFormat, WireFormat, -}; - -/// The `DTCExtDataRecordNumber` is used in the request message to get a stored [`DTCExtDataRecord`] +/// The `DTCExtDataRecordNumber` is used in the request message to get a stored `DTCExtDataRecord` /// Its used to specify the type of `DTCExtDataRecord` to be reported. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DTCExtDataRecordNumber { - /// ISO/SAE reserved record numbers (`0x00`, `0xF0–0xFD`). + /// ISO/SAE reserved record numbers (`0x00`, `0xF0-0xFD`). ISOSAEReserved(u8), - /// Vehicle manufactured specific stored [`DTCExtDataRecord`]s + /// Vehicle manufactured specific stored `DTCExtDataRecord`s /// /// 0x01-0x8F VehicleManufacturer(u8), @@ -71,132 +67,23 @@ impl PartialEq for DTCExtDataRecordNumber { } } -impl WireFormat for DTCExtDataRecordNumber { - fn required_size(&self) -> usize { +impl Encode for DTCExtDataRecordNumber { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.value())?; - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for DTCExtDataRecordNumber { - fn decode(reader: &mut T) -> Result { - Ok(Self::new(reader.read_u8()?)) - } -} - -impl IterableWireFormat for DTCExtDataRecordNumber { - fn decode_next(reader: &mut T) -> Result, Error> { - match reader.read_u8() { - Ok(v) => Ok(Some(Self::new(v))), - Err(_) => Ok(None), - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -/// A single DTC extended-data record containing user-defined payload items. -pub struct DTCExtDataRecord { - /// The decoded payload entries for this record. - pub data: Vec, -} - -impl WireFormat for DTCExtDataRecord { - fn required_size(&self) -> usize { - // n bytes of data per UserPayload - self.data - .iter() - .map(WireFormat::required_size) - .sum::() - } - - fn encode(&self, writer: &mut T) -> Result { - for d in &self.data { - d.encode(writer)?; - } - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.value()]).map_err(Error::io)?; + Ok(1) } } -impl SingleValueWireFormat for DTCExtDataRecord { - fn decode(reader: &mut T) -> Result { - let mut data = Vec::new(); - for payload in UserPayload::decode_iter(reader) { - match payload { - Ok(p) => data.push(p), - Err(_) => break, - } +impl<'a> Decode<'a> for DTCExtDataRecordNumber { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); } - Ok(Self { data }) - } -} - -impl IterableWireFormat for DTCExtDataRecord { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut data = Vec::new(); - for payload in UserPayload::decode_iter(reader) { - match payload { - Err(_) => return Ok(None), - Ok(payload) => data.push(payload), - } - } - Ok(Some(Self { data })) - } -} - -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -/// A DTC extended-data record list: a DTC + status mask followed by one or more [`DTCExtDataRecord`]s. -pub struct DTCExtDataRecordList { - /// The DTC this extended data belongs to. - pub mask_record: DTCRecord, - /// The DTC status mask at the time of reporting. - pub status_mask: DTCStatusMask, - /// The extended-data records associated with this DTC. - pub record_data: Vec>, -} - -impl WireFormat for DTCExtDataRecordList { - fn required_size(&self) -> usize { - self.mask_record.required_size() - + self.status_mask.required_size() - + self - .record_data - .iter() - .map(WireFormat::required_size) - .sum::() - } - - fn encode(&self, writer: &mut T) -> Result { - self.mask_record.encode(writer)?; - self.status_mask.encode(writer)?; - for record in &self.record_data { - record.encode(writer)?; - } - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for DTCExtDataRecordList { - fn decode(reader: &mut T) -> Result { - let mask_record = DTCRecord::decode(reader)?; - let status_mask = DTCStatusMask::decode(reader)?; - let mut record_data = Vec::new(); - // Read the record number, and then the payload - if let Some(record) = DTCExtDataRecord::decode_next(reader)? { - record_data.push(record); - } - Ok(Self { - mask_record, - status_mask, - record_data, - }) + Ok((Self::new(buf[0]), &buf[1..])) } } @@ -211,4 +98,15 @@ mod tests { assert_eq!(record_number, DTCExtDataRecordNumber::ISOSAEReserved(0x00)); assert_eq!(record_number.value(), 0x00); } + + #[test] + fn encode_ext_data_record_number() { + use crate::test_util::assert_encode_size_agrees; + let n = DTCExtDataRecordNumber::new(0x90); + let mut buf = [0u8; 4]; + let written = crate::Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x90); + assert_encode_size_agrees(&n); + } } diff --git a/src/common/dtc_snapshot.rs b/src/common/dtc_snapshot.rs index 3efc1ee..c68eccd 100644 --- a/src/common/dtc_snapshot.rs +++ b/src/common/dtc_snapshot.rs @@ -1,159 +1,8 @@ //! Diagnostic Trouble Code (DTC) Snapshot Data //! Snapshot data represents a collection of sensor values captured when a DTC is triggered. //! Represents the state of the server at the time the DTC was triggered. -use byteorder::{ReadBytesExt, WriteBytesExt}; -use crate::{ - DTCRecord, DTCStatusMask, Error, IterableWireFormat, SingleValueWireFormat, WireFormat, -}; - -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -/// A DTC snapshot record list: a DTC + status mask followed by one or more numbered snapshot records. -pub struct DTCSnapshotRecordList { - /// The DTC this snapshot data belongs to. - pub dtc_record: DTCRecord, - /// The DTC status mask at the time of reporting. - pub status_mask: DTCStatusMask, - /// The snapshot records, each paired with its record number. - pub snapshot_data: Vec<(DTCSnapshotRecordNumber, DTCSnapshotRecord)>, -} - -impl WireFormat for DTCSnapshotRecordList { - fn required_size(&self) -> usize { - self.dtc_record.required_size() - + self.status_mask.required_size() - + self - .snapshot_data - .iter() - .fold(0, |acc, (record_number, record)| { - acc + record_number.required_size() + record.required_size() - }) - } - - fn encode(&self, writer: &mut T) -> Result { - self.dtc_record.encode(writer)?; - self.status_mask.encode(writer)?; - for (record_number, record) in &self.snapshot_data { - record_number.encode(writer)?; - record.encode(writer)?; - } - - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for DTCSnapshotRecordList { - fn decode(reader: &mut T) -> Result { - let dtc_record = DTCRecord::decode(reader)?; - let status_mask = DTCStatusMask::decode(reader)?; - - // Loop until we can't read any more records - let mut snapshot_data = Vec::new(); - loop { - let record_number = match DTCSnapshotRecordNumber::decode_next(reader) { - Ok(Some(record_number)) => record_number, - Ok(None) => break, - Err(e) => return Err(e), - }; - - let record = DTCSnapshotRecord::decode(reader)?; - - snapshot_data.push((record_number, record)); - } - - Ok(Self { - dtc_record, - status_mask, - snapshot_data, - }) - } -} - -/// Contains a snapshot of data values from the time of the system malfunction occurrence. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct DTCSnapshotRecord { - /// The data identifier (DID) for the data values taken at the time of the system malfunction occurrence - /// These can be vehicle manufacturer specific - /// See C.1 for broad categories of data identifiers - /// The data values taken at the time of the system malfunction occurrence - /// The data values are dependent on the data identifier, and are specified by the vehicle manufacturer/supplier - pub data: Vec, -} - -impl DTCSnapshotRecord { - /// Create a new snapshot record from a list of payload items. - #[must_use] - pub fn new(data: Vec) -> Self { - Self { data } - } - - /// The number of DIDs in the snapshot record - /// If the number of DIDs exceeds 0xFF, the value 0x00 shall be used - #[allow(clippy::cast_possible_truncation)] - #[must_use] - pub fn number_of_dids(&self) -> u8 { - if self.data.len() > 0xFF { - 0 - } else { - self.data.len() as u8 - } - } -} - -impl WireFormat for DTCSnapshotRecord { - fn required_size(&self) -> usize { - 1 + self - .data - .iter() - .map(WireFormat::required_size) - .sum::() - } - - // TODO: Must write the DIDs as well... - fn encode(&self, writer: &mut T) -> Result { - // write 0x00 if the number of DIDs exceed 0xFF - writer.write_u8(self.number_of_dids())?; - - let mut payload_written = 0; - for payload in &self.data { - // Assumes this writes the DID as well, I think that's safe? - payload_written += payload.encode(writer)?; - } - Ok(1 + payload_written) - } -} - -impl SingleValueWireFormat for DTCSnapshotRecord { - #[allow(clippy::cast_possible_truncation)] - fn decode(reader: &mut T) -> Result { - let number_of_dids = reader.read_u8()?; - // Make sure we read the correct number of DIDs, 0 means unlimited (or at least more than 0xFF) - let mut data = Vec::new(); - for payload in UserPayload::decode_iter(reader) { - match payload { - Ok(did) => { - data.push(did); - // Do not attempt to read more than the number of DIDs the server said it would send - if number_of_dids != 0 && data.len() == number_of_dids as usize { - break; - } - } - Err(e) => { - return Err(e); - } - } - } - if number_of_dids != 0x00 && number_of_dids != data.len() as u8 { - return Err(Error::IncorrectMessageLengthOrInvalidFormat); - } - - Ok(Self { data }) - } -} +use crate::{Decode, Encode, Error}; /// Identifies which DTC snapshot record is being requested or reported. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -196,214 +45,48 @@ impl PartialEq for DTCSnapshotRecordNumber { } } -impl WireFormat for DTCSnapshotRecordNumber { - fn required_size(&self) -> usize { +impl Encode for DTCSnapshotRecordNumber { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.value())?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.value()]).map_err(Error::io)?; Ok(1) } } -impl SingleValueWireFormat for DTCSnapshotRecordNumber { - fn decode(reader: &mut T) -> Result { - Ok(Self::new(reader.read_u8()?)) - } -} - -impl IterableWireFormat for DTCSnapshotRecordNumber { - fn decode_next(reader: &mut T) -> Result, Error> { - let Ok(record_number) = reader.read_u8() else { - return Ok(None); - }; - Ok(Some(Self::new(record_number))) +impl<'a> Decode<'a> for DTCSnapshotRecordNumber { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::new(buf[0]), &buf[1..])) } } #[cfg(test)] mod snapshot { - - pub enum ProtocolPayload { - Did4711([u8; 5]), - Did8711([u8; 5]), - Did8712(u8, u8, u16), - } - // Testing out a macro to make simplifying the enum to DID value "nicer" - macro_rules! value_map { - ($(($e:ident, $v:literal)),* $(,)?) => { - pub fn value(&self) -> u16 { - match self { - $(ProtocolPayload::$e(..) => $v,)* - } - } - } - } - impl ProtocolPayload { - #[rustfmt::skip] - value_map![ - (Did4711, 0x4711), - (Did8711, 0x8711), - (Did8712, 0x8712), - ]; - } - - impl WireFormat for ProtocolPayload { - #[allow(clippy::match_same_arms)] - fn required_size(&self) -> usize { - 2 + match self { - ProtocolPayload::Did4711(_) => 5, - ProtocolPayload::Did8711(_) => 5, - ProtocolPayload::Did8712(..) => 4, - } - } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_u16::(self.value())?; - let mut written = 2; - - match self { - ProtocolPayload::Did8711(data) | ProtocolPayload::Did4711(data) => { - writer.write_all(data)?; - written += data.len(); - } - // bogus data - ProtocolPayload::Did8712(..) => { - writer.write_u32::(78)?; - written += 4; - } - } - Ok(written) - } - } - - impl IterableWireFormat for ProtocolPayload { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 => return Ok(None), - 1 => return Err(Error::IncorrectMessageLengthOrInvalidFormat), - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - // read the identifier - let identifier = u16::from_be_bytes(identifier_data); - match identifier { - 0x4711 => { - let mut did_4711 = [0u8; 5]; - match reader.read(&mut did_4711)? { - 0 => return Ok(None), - 1 => return Err(Error::IncorrectMessageLengthOrInvalidFormat), - 5 => (), - _ => unreachable!("Impossible to read more than 5 bytes into 5 byte array"), - } - Ok(Some(Self::Did4711(did_4711))) - } - 0x8711 => { - let mut did_8711 = [0u8; 5]; - match reader.read(&mut did_8711)? { - 0 => return Ok(None), - 1 => return Err(Error::IncorrectMessageLengthOrInvalidFormat), - 5 => (), - _ => unreachable!("Impossible to read more than 5 bytes into 5 byte array"), - } - Ok(Some(Self::Did8711(did_8711))) - } - _ => Err(Error::IncorrectMessageLengthOrInvalidFormat), - } - } - } - use super::*; #[test] - fn snapshot_record() { + fn snapshot_record_number() { let record = DTCSnapshotRecordNumber::new(0x01); - let mut writer = Vec::new(); - let written_number = record.encode(&mut writer).unwrap(); - assert_eq!(record.required_size(), 1); - assert_eq!(written_number, 1); - } - - #[test] - fn test_value() { - let did = ProtocolPayload::Did8712(1, 2, 3); - assert_eq!(did.value(), 0x8712); + assert_eq!(record.value(), 0x01); + assert_eq!(record, DTCSnapshotRecordNumber::Number(0x01)); - match did { - ProtocolPayload::Did8712(a, b, c) => { - assert_eq!(a, 1); - assert_eq!(b, 2); - assert_eq!(c, 3); - } - _ => panic!("Expected Did8712"), - } + let all = DTCSnapshotRecordNumber::new(0xFF); + assert_eq!(all, DTCSnapshotRecordNumber::All); } #[test] - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::match_wildcard_for_single_variants)] - fn snapshot_list() { - #[rustfmt::skip] - let bytes:[u8; 29] = [ - // DTC Number + Status - 0x12, 0x34, 0x56, 0x24, - // DTC Snapshot Record Number - 0x01, - // Number of DIDs to read - 0x02, - // DID (fake) - 0x47, 0x11, - // Snapshot data - 0xA6, 0x66, 0x07, 0x50, 0x20, - 0x87, 0x11, - 0x00, 0x00, 0x00, 0x00, 0x09, - // New DTC Snapshot record number - 0x02, - // Number of DIDs to read (0 indicates an unlimited number) - 0x01, - 0x47, 0x11, - 0xA6, 0x66, 0x07, 0x50, 0x20, - ]; - - let resp = DTCSnapshotRecordList::decode(&mut bytes.as_slice()).unwrap(); - - assert_eq!(resp.dtc_record, DTCRecord::from(0x0012_3456)); - let mut number: u8 = 1; - - resp.snapshot_data - .iter() - .for_each(|(record_number, record)| { - // Check the record numbers match for the ones we're expecting - assert_eq!(*record_number, number); - number += 1; - // Just check the helper function - assert_eq!(record.number_of_dids(), record.data.len() as u8); - // check the data of the payload - for payload in &record.data { - match payload { - ProtocolPayload::Did4711(data) => { - assert_eq!(data, &[0xA6, 0x66, 0x07, 0x50, 0x20]); - } - ProtocolPayload::Did8711(data) => { - assert_eq!(data, &[0x00, 0x00, 0x00, 0x00, 0x09]); - } - _ => panic!("Unexpected payload in bagging area"), - } - let mut writer = Vec::new(); - let written = payload.encode(&mut writer).unwrap(); - assert_eq!(written, payload.required_size()); - } - }); - let mut writer = Vec::new(); - let written = resp.encode(&mut writer).unwrap(); - assert_eq!(written, resp.required_size()); - assert_eq!( - written, - bytes.len(), - "Written bytes: \n{writer:?}\n{bytes:?}" - ); - assert_eq!(writer, bytes); + fn encode_snapshot_record_number() { + use crate::test_util::assert_encode_size_agrees; + let n = DTCSnapshotRecordNumber::new(0x02); + let mut buf = [0u8; 4]; + let written = crate::Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x02); + assert_encode_size_agrees(&n); } } diff --git a/src/common/dtc_status.rs b/src/common/dtc_status.rs index 9f41da5..4f96ff7 100644 --- a/src/common/dtc_status.rs +++ b/src/common/dtc_status.rs @@ -1,7 +1,6 @@ use bitmask_enum::bitmask; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use crate::{Error, IterableWireFormat, SingleValueWireFormat, WireFormat}; +use crate::{Decode, DecodeIter, Encode, Error}; /// Bit-packed DTC status information used by the `ReadDTCInformation` service /// @@ -107,21 +106,22 @@ pub enum DTCStatusMask { WarningIndicatorRequested, } -impl WireFormat for DTCStatusMask { - fn required_size(&self) -> usize { +impl Encode for DTCStatusMask { + fn encoded_size(&self) -> usize { 1 } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.bits())?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.bits()]).map_err(Error::io)?; Ok(1) } } -impl SingleValueWireFormat for DTCStatusMask { - fn decode(reader: &mut T) -> Result { - let status_byte = reader.read_u8()?; - Ok(Self::from(status_byte)) +impl<'a> Decode<'a> for DTCStatusMask { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::from(buf[0]), &buf[1..])) } } @@ -230,42 +230,41 @@ impl From for u32 { } } -impl WireFormat for DTCRecord { - fn required_size(&self) -> usize { +impl Encode for DTCRecord { + fn encoded_size(&self) -> usize { 3 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_all(&[self.high_byte, self.middle_byte, self.low_byte])?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.high_byte, self.middle_byte, self.low_byte]) + .map_err(Error::io)?; Ok(3) } } -impl SingleValueWireFormat for DTCRecord { - fn decode(reader: &mut T) -> Result { - let high_byte = reader.read_u8()?; - let middle_byte = reader.read_u8()?; - let low_byte = reader.read_u8()?; - Ok(Self { - high_byte, - middle_byte, - low_byte, - }) +impl<'a> Decode<'a> for DTCRecord { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 3 { + return Err(Error::InsufficientData(3)); + } + Ok(( + Self { + high_byte: buf[0], + middle_byte: buf[1], + low_byte: buf[2], + }, + &buf[3..], + )) } } -impl IterableWireFormat for DTCRecord { - fn decode_next(reader: &mut T) -> Result, crate::Error> { - let Ok(high_byte) = reader.read_u8() else { +impl<'a> DecodeIter<'a> for DTCRecord { + fn decode_next(buf: &'a [u8]) -> Result, Error> { + if buf.is_empty() { return Ok(None); - }; - let middle_byte = reader.read_u8()?; - let low_byte = reader.read_u8()?; - Ok(Some(Self { - high_byte, - middle_byte, - low_byte, - })) + } + Decode::decode(buf).map(Some) } } @@ -306,18 +305,8 @@ impl FunctionalGroupIdentifier { FunctionalGroupIdentifier::EmissionsSystemGroup => 0x33, FunctionalGroupIdentifier::SafetySystemGroup => 0xD0, FunctionalGroupIdentifier::VODBSystem => 0xFE, - FunctionalGroupIdentifier::LegislativeSystemGroup(value) => { - todo!( - "FunctionalGroupIdentifiers::LegislativeSystemGroup is not a valid value {}", - value - ) - } - FunctionalGroupIdentifier::ISOSAEReserved(value) => { - todo!( - "FunctionalGroupIdentifiers::ISOSAEReserved is not a valid value {}", - value - ) - } + FunctionalGroupIdentifier::LegislativeSystemGroup(value) + | FunctionalGroupIdentifier::ISOSAEReserved(value) => *value, } } } @@ -340,6 +329,26 @@ impl From for u8 { } } +impl Encode for FunctionalGroupIdentifier { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.value()]).map_err(Error::io)?; + Ok(1) + } +} + +impl<'a> Decode<'a> for FunctionalGroupIdentifier { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::from(buf[0]), &buf[1..])) + } +} + /// GTR DTC Class Information /// /// Bits 7-5 of the DTCSeverityMask/DTCSeverity parameters contain severity information (optional) @@ -385,7 +394,7 @@ pub enum DTCSeverityMask { } impl DTCSeverityMask { - /// Returns `true` if at least one DTC class bit (bits 0–4) is set. + /// Returns `true` if at least one DTC class bit (bits 0-4) is set. /// Multiple class bits may be set to query multiple DTC classes at once. #[must_use] pub fn is_valid(&self) -> bool { @@ -399,6 +408,26 @@ impl DTCSeverityMask { } } +impl Encode for DTCSeverityMask { + fn encoded_size(&self) -> usize { + 1 + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.bits()]).map_err(Error::io)?; + Ok(1) + } +} + +impl<'a> Decode<'a> for DTCSeverityMask { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok((Self::from(buf[0]), &buf[1..])) + } +} + /// Indicates the number of the specific `DTCSnapshot` data record requested /// Setting to 0xFF will return all `DTCStoredDataRecords` at once #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -413,43 +442,35 @@ impl DTCStoredDataRecordNumber { /// Will return `Err(Error::ReservedForLegislativeUse()` if the record number == 0x00 or 0xF0 pub fn new(record_number: u8) -> Result { if record_number == 0 || record_number == 0xF0 { - return Err(Error::ReservedForLegislativeUse( - "DTCStoredDataRecordNumber".to_string(), - record_number, - )); + return Err(Error::ReservedForLegislativeUse(record_number)); } Ok(Self(record_number)) } } -impl WireFormat for DTCStoredDataRecordNumber { - fn required_size(&self) -> usize { +impl From for DTCStoredDataRecordNumber { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl Encode for DTCStoredDataRecordNumber { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.0)?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&[self.0]).map_err(Error::io)?; Ok(1) } } -impl SingleValueWireFormat for DTCStoredDataRecordNumber { - fn decode(reader: &mut T) -> Result { - let value = reader.read_u8()?; - if value == 0x00 { - // Reserved for Legislative purposes - return Err(Error::ReservedForLegislativeUse( - "DTCStoredDataRecordNumber".to_string(), - value, - )); +impl<'a> Decode<'a> for DTCStoredDataRecordNumber { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); } - Ok(Self(value)) - } -} - -impl From for DTCStoredDataRecordNumber { - fn from(value: u8) -> Self { - Self(value) + Ok((Self::from(buf[0]), &buf[1..])) } } @@ -468,37 +489,48 @@ pub struct DTCSeverityRecord { pub dtc_status_mask: DTCStatusMask, } -impl WireFormat for DTCSeverityRecord { - fn required_size(&self) -> usize { - 6 - } +#[cfg(test)] +mod encode_param_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.severity.bits())?; - writer.write_u8(self.functional_group_identifier.value())?; - self.dtc_record.encode(writer)?; - self.dtc_status_mask.encode(writer)?; - Ok(self.required_size()) + #[test] + fn encode_stored_data_record_number() { + let n = DTCStoredDataRecordNumber::new(0x05).unwrap(); + let mut buf = [0u8; 4]; + let written = Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x05); + assert_encode_size_agrees(&n); } -} -impl IterableWireFormat for DTCSeverityRecord { - fn decode_next(reader: &mut T) -> Result, Error> { - let Ok(sev) = reader.read_u8() else { - return Ok(None); - }; + #[test] + fn encode_severity_mask() { + let m = DTCSeverityMask::CheckImmediately; + let mut buf = [0u8; 4]; + let written = Encode::encode(&m, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0b1000_0000); + assert_encode_size_agrees(&m); + } - let severity = DTCSeverityMask::from(sev); - let functional_group_identifier = FunctionalGroupIdentifier::from(reader.read_u8()?); - let dtc_record = DTCRecord::decode(reader)?; - let dtc_status_mask = DTCStatusMask::from(reader.read_u8()?); + #[test] + fn encode_functional_group_identifier_named() { + let g = FunctionalGroupIdentifier::EmissionsSystemGroup; + let mut buf = [0u8; 4]; + let written = Encode::encode(&g, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + assert_eq!(buf[0], 0x33); + assert_encode_size_agrees(&g); + } - Ok(Some(Self { - severity, - functional_group_identifier, - dtc_record, - dtc_status_mask, - })) + #[test] + fn functional_group_identifier_value_does_not_panic_on_reserved() { + // Regression: value() previously called todo!() for carried-byte variants. + let g = FunctionalGroupIdentifier::from(0x10); // -> ISOSAEReserved(0x10) + assert_eq!(g.value(), 0x10); + let g2 = FunctionalGroupIdentifier::from(0xD5); // -> LegislativeSystemGroup(0xD5) + assert_eq!(g2.value(), 0xD5); } } @@ -531,11 +563,13 @@ mod dtc_status_tests { } #[test] - fn dtc_record() { + fn dtc_record_encode_decode() { let record = DTCRecord::new(0x01, 0x02, 0x03); - let mut writer = Vec::new(); - let written_number = record.encode(&mut writer).unwrap(); - assert_eq!(record.required_size(), 3); - assert_eq!(written_number, 3); + let mut buf = [0u8; 3]; + let written = Encode::encode(&record, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 3); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, record); + assert!(rest.is_empty()); } } diff --git a/src/common/format_identifiers.rs b/src/common/format_identifiers.rs index b71ae02..4bf3331 100644 --- a/src/common/format_identifiers.rs +++ b/src/common/format_identifiers.rs @@ -1,5 +1,4 @@ -use crate::{Error, SingleValueWireFormat, WireFormat}; -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::Error; const LOW_NIBBLE_MASK: u8 = 0b0000_1111; const HIGH_NIBBLE_MASK: u8 = 0b1111_0000; @@ -30,19 +29,6 @@ pub(crate) struct MemoryFormatIdentifier { } impl MemoryFormatIdentifier { - /// Takes in the actual memory address to be used and the size of the memory to be used - /// and computes how many bytes are needed to represent them - #[allow(clippy::cast_possible_truncation)] - pub fn from_values(memory_size: u32, memory_address: u64) -> Self { - let memory_address_length = (u64::BITS - memory_address.leading_zeros()).div_ceil(8) as u8; - let memory_size_length = (u32::BITS - memory_size.leading_zeros()).div_ceil(8) as u8; - - Self { - memory_size_length, - memory_address_length, - } - } - /// Get the total length of the `memory_size` and `memory_address` fields pub fn len(self) -> usize { self.memory_size_length as usize + self.memory_address_length as usize @@ -160,24 +146,6 @@ impl PartialEq for DataFormatIdentifier { } } -impl WireFormat for DataFormatIdentifier { - fn required_size(&self) -> usize { - 1 - } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(*self))?; - Ok(1) - } -} - -impl SingleValueWireFormat for DataFormatIdentifier { - fn decode(reader: &mut T) -> Result { - let value = reader.read_u8()?; - Ok(DataFormatIdentifier::from(value)) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/common/mod.rs b/src/common/mod.rs index d67e277..6301d8e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -38,5 +38,6 @@ pub(crate) use format_identifiers::{ mod util; pub use util::{param_length_u16, param_length_u32, param_length_u64, param_length_u128}; +pub(crate) use util::{read_be_uint, write_be_uint}; mod primitive_generics; diff --git a/src/common/primitive_generics.rs b/src/common/primitive_generics.rs index 49f16f8..3354cf6 100644 --- a/src/common/primitive_generics.rs +++ b/src/common/primitive_generics.rs @@ -1,95 +1,74 @@ -use crate::{Error, SingleValueWireFormat, WireFormat}; -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use crate::{Decode, Encode, Error}; -/// Implement [`WireFormat`] and [`SingleValueWireFormat`] for unsigned integer primitives. -#[macro_export] -macro_rules! unsigned_primitive_wire_format { +/// Implement [`Encode`] and [`Decode`] for integer primitives (no_std-compatible). +macro_rules! primitive_encode_decode { ( $($primitive:ty), * ) => { $( - impl WireFormat for $primitive { - fn required_size(&self) -> usize { - std::mem::size_of::<$primitive>() + impl Encode for $primitive { + fn encoded_size(&self) -> usize { + core::mem::size_of::<$primitive>() } - fn encode(&self, writer: &mut W) -> Result { - writer.write_uint128::(u128::from(*self), self.required_size())?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&self.to_be_bytes()).map_err(Error::io)?; + Ok(self.encoded_size()) } } - impl SingleValueWireFormat for $primitive { - fn decode(reader: &mut T) -> Result { - let value: $primitive = reader - .read_uint128::(std::mem::size_of::<$primitive>())? - .try_into() - .expect("Failed to convert value to the target primitive type"); - Ok(value) + impl<'a> Decode<'a> for $primitive { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + const SIZE: usize = core::mem::size_of::<$primitive>(); + if buf.len() < SIZE { + return Err(Error::InsufficientData(SIZE)); + } + let (head, tail) = buf.split_at(SIZE); + let value = <$primitive>::from_be_bytes(head.try_into().unwrap()); + Ok((value, tail)) } } - )* + )* }; } -unsigned_primitive_wire_format!(u8, u16, u32, u64, u128); +primitive_encode_decode!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); -/// Implement [`WireFormat`] and [`SingleValueWireFormat`] for signed integer primitives. -#[macro_export] -macro_rules! signed_primitive_wire_format { - ( $($primitive:ty), * ) => { - $( - impl WireFormat for $primitive { - fn required_size(&self) -> usize { - std::mem::size_of::<$primitive>() - } - fn encode(&self, writer: &mut W) -> Result { - writer.write_int128::(i128::from(*self), self.required_size())?; - Ok(self.required_size()) - } - } - impl SingleValueWireFormat for $primitive { - fn decode(reader: &mut T) -> Result { - let value: $primitive = reader - .read_int128::(std::mem::size_of::<$primitive>())? - .try_into() - .expect("Failed to convert value to the target primitive type"); - Ok(value) - } - } - )* - }; -} - -signed_primitive_wire_format!(i8, i16, i32, i64, i128); - -impl WireFormat for f32 { - fn required_size(&self) -> usize { +impl Encode for f32 { + fn encoded_size(&self) -> usize { 4 } - fn encode(&self, writer: &mut W) -> Result { - writer.write_f32::(*self)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&self.to_be_bytes()).map_err(Error::io)?; + Ok(4) } } -impl SingleValueWireFormat for f32 { - fn decode(reader: &mut T) -> Result { - let value: f32 = reader.read_f32::()?; - Ok(value) +impl<'a> Decode<'a> for f32 { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 4 { + return Err(Error::InsufficientData(4)); + } + let (head, tail) = buf.split_at(4); + let value = f32::from_be_bytes(head.try_into().unwrap()); + Ok((value, tail)) } } -impl WireFormat for f64 { - fn required_size(&self) -> usize { +impl Encode for f64 { + fn encoded_size(&self) -> usize { 8 } - fn encode(&self, writer: &mut W) -> Result { - writer.write_f64::(*self)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(&self.to_be_bytes()).map_err(Error::io)?; + Ok(8) } } -impl SingleValueWireFormat for f64 { - fn decode(reader: &mut T) -> Result { - let value: f64 = reader.read_f64::()?; - Ok(value) +impl<'a> Decode<'a> for f64 { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 8 { + return Err(Error::InsufficientData(8)); + } + let (head, tail) = buf.split_at(8); + let value = f64::from_be_bytes(head.try_into().unwrap()); + Ok((value, tail)) } } @@ -98,80 +77,55 @@ mod tests { use super::*; #[test] - fn test_u8() { - // Read some bytes - let data = vec![0xFF]; - let mut reader = &data[..]; - - let u8_byte = u8::decode(&mut reader).unwrap(); - assert_eq!(u8_byte, 0xFF); - assert_eq!(u8_byte.required_size(), 1); - - let mut write_buffer = vec![]; - u8_byte.encode(&mut write_buffer).unwrap(); - assert_eq!(write_buffer, data); + fn test_u8_encode_decode() { + let val: u8 = 0xFF; + let mut buf = [0u8; 1]; + Encode::encode(&val, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xFF]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, 0xFF); + assert!(rest.is_empty()); } #[test] - fn test_u16() { - // Read some bytes - let data = vec![0xFF, 0x01]; - let mut reader = &data[..]; - - let u16_byte = u16::decode(&mut reader).unwrap(); - assert_eq!(u16_byte, 0xFF01); - assert_eq!(u16_byte.required_size(), 2); - - let mut write_buffer = vec![]; - u16_byte.encode(&mut write_buffer).unwrap(); - assert_eq!(write_buffer, data); + fn test_u16_encode_decode() { + let val: u16 = 0xFF01; + let mut buf = [0u8; 2]; + Encode::encode(&val, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xFF, 0x01]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, 0xFF01); + assert!(rest.is_empty()); } #[test] - fn test_u32() { - // Read some bytes - let data = vec![0xFF, 0x20, 0x02, 0x01]; - let mut reader = &data[..]; - - let u32_byte = u32::decode(&mut reader).unwrap(); - assert_eq!(u32_byte, 0xFF20_0201); - assert_eq!(u32_byte.required_size(), 4); - - let mut write_buffer = vec![]; - u32_byte.encode(&mut write_buffer).unwrap(); - assert_eq!(write_buffer, data); + fn test_u32_encode_decode() { + let val: u32 = 0xFF20_0201; + let mut buf = [0u8; 4]; + Encode::encode(&val, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(buf, [0xFF, 0x20, 0x02, 0x01]); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, 0xFF20_0201); + assert!(rest.is_empty()); } #[test] - fn test_u64() { - // Read some bytes - let data = vec![0xFF, 0x20, 0x02, 0x01, 0xFF, 0x20, 0x02, 0x01]; - let mut reader = &data[..]; - - let u64_byte = u64::decode(&mut reader).unwrap(); - assert_eq!(u64_byte, 0xFF20_0201_FF20_0201); - assert_eq!(u64_byte.required_size(), 8); - - let mut write_buffer = vec![]; - u64_byte.encode(&mut write_buffer).unwrap(); - assert_eq!(write_buffer, data); + fn test_u64_encode_decode() { + let val: u64 = 0xFF20_0201_FF20_0201; + let mut buf = [0u8; 8]; + Encode::encode(&val, &mut buf.as_mut_slice()).unwrap(); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, val); + assert!(rest.is_empty()); } #[test] - fn test_u128() { - // Read some bytes - let data = vec![ - 0xFF, 0x20, 0x02, 0x01, 0xFF, 0x20, 0x02, 0x01, 0xFF, 0x20, 0x02, 0x01, 0xFF, 0x20, - 0x02, 0x01, - ]; - let mut reader = &data[..]; - - let u128_byte = u128::decode(&mut reader).unwrap(); - assert_eq!(u128_byte, 0xFF20_0201_FF20_0201_FF20_0201_FF20_0201); - assert_eq!(u128_byte.required_size(), 16); - - let mut write_buffer = vec![]; - u128_byte.encode(&mut write_buffer).unwrap(); - assert_eq!(write_buffer, data); + fn test_u128_encode_decode() { + let val: u128 = 0xFF20_0201_FF20_0201_FF20_0201_FF20_0201; + let mut buf = [0u8; 16]; + Encode::encode(&val, &mut buf.as_mut_slice()).unwrap(); + let (decoded, rest) = ::decode(&buf).unwrap(); + assert_eq!(decoded, val); + assert!(rest.is_empty()); } } diff --git a/src/common/util.rs b/src/common/util.rs index 76317d8..8f63f1e 100644 --- a/src/common/util.rs +++ b/src/common/util.rs @@ -1,4 +1,47 @@ //! Compute the number of bytes needed to represent a value using core +use crate::Error; + +/// Maximum width of a big-endian unsigned integer this codec handles. +const BE_UINT_MAX_BYTES: usize = 16; + +/// Read the first `n` big-endian bytes of `src` as a left-padded `u128`. +/// +/// # Errors +/// Returns [`Error::InsufficientData`] if `src` is shorter than `n`, or +/// [`Error::IncorrectMessageLengthOrInvalidFormat`] if `n > 16`. +pub(crate) fn read_be_uint(src: &[u8], n: usize) -> Result { + if n > BE_UINT_MAX_BYTES { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + if src.len() < n { + return Err(Error::InsufficientData(n)); + } + let mut bytes = [0u8; BE_UINT_MAX_BYTES]; + bytes[BE_UINT_MAX_BYTES - n..].copy_from_slice(&src[..n]); + Ok(u128::from_be_bytes(bytes)) +} + +/// Write the low `n` big-endian bytes of `value` to `writer`, returning `n`. +/// Only the low `n` bytes of `value` are written; higher bytes are discarded. +/// +/// # Errors +/// Returns [`Error::IncorrectMessageLengthOrInvalidFormat`] if `n > 16`, or +/// [`Error::IoError`] if the writer fails. +pub(crate) fn write_be_uint( + value: u128, + n: usize, + writer: &mut impl embedded_io::Write, +) -> Result { + if n > BE_UINT_MAX_BYTES { + return Err(Error::IncorrectMessageLengthOrInvalidFormat); + } + let bytes = value.to_be_bytes(); + writer + .write_all(&bytes[BE_UINT_MAX_BYTES - n..]) + .map_err(Error::io)?; + Ok(n) +} + /// Return the minimum number of bytes needed to represent a `u16` value. #[allow(clippy::cast_possible_truncation)] #[must_use] @@ -28,6 +71,34 @@ pub fn param_length_u128(value: u128) -> u16 { mod tests { use super::*; + #[test] + fn be_uint_roundtrip() { + use crate::common::util::{read_be_uint, write_be_uint}; + let mut buf = [0u8; 16]; + let mut w = buf.as_mut_slice(); + let written = write_be_uint(0x00AB_CDEFu128, 3, &mut w).unwrap(); + assert_eq!(written, 3); + assert_eq!(&buf[..3], &[0xAB, 0xCD, 0xEF]); + let v = read_be_uint(&buf[..3], 3).unwrap(); + assert_eq!(v, 0x00AB_CDEF); + } + + #[test] + fn be_uint_zero_width() { + use crate::common::util::{read_be_uint, write_be_uint}; + let mut buf = [0u8; 4]; + let mut w = buf.as_mut_slice(); + assert_eq!(write_be_uint(0, 0, &mut w).unwrap(), 0); + assert_eq!(read_be_uint(&[], 0).unwrap(), 0); + } + + #[test] + fn read_be_uint_rejects_short_and_overwide() { + use crate::common::util::read_be_uint; + assert!(read_be_uint(&[0x01], 2).is_err()); + assert!(read_be_uint(&[0u8; 17], 17).is_err()); + } + #[test] fn test_bits_needed() { assert_eq!(param_length_u32(0x1234), 2); diff --git a/src/error.rs b/src/error.rs index 9e8a1d0..46b67ef 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,8 +5,8 @@ use thiserror::Error; #[non_exhaustive] pub enum Error { /// An underlying I/O error occurred while reading or writing. - #[error(transparent)] - IoError(#[from] std::io::Error), + #[error("I/O error: {0:?}")] + IoError(embedded_io::ErrorKind), /// The byte stream contained fewer bytes than expected. #[error("Insufficient data. Expected {0} bytes.")] InsufficientData(usize), @@ -14,8 +14,8 @@ pub enum Error { #[error("Invalid Diagnostic Identifier: {0:X}")] InvalidDiagnosticIdentifier(u16), /// The u16 identifier is unrecognised and carried an unexpected payload. - #[error("Invalid Diagnostic Identifier: {0:X} with payload {1:?}")] - InvalidDiagnosticIdentifierPayload(u16, Vec), + #[error("Invalid Diagnostic Identifier with payload: {0:X}")] + InvalidDiagnosticIdentifierPayload(u16), /// The session-type byte is not a valid [`DiagnosticSessionType`](crate::DiagnosticSessionType). #[error("Invalid diagnostic session type: {0}")] InvalidDiagnosticSessionType(u8), @@ -59,9 +59,28 @@ pub enum Error { #[error("Invalid DTC Format Identifier: {0}")] InvalidDtcFormatIdentifier(u8), /// The value is reserved for legislative use and must not be used. - #[error("Reserved for legislative use: {0} ({1})")] - ReservedForLegislativeUse(String, u8), - /// The service type is not yet implemented in this crate. - #[error("UDS service not implemented: {0:?}")] - ServiceNotImplemented(crate::UdsServiceType), + #[error("Reserved for legislative use: {0}")] + ReservedForLegislativeUse(u8), +} + +impl Error { + /// Convert any `embedded_io::Error` into [`Error::IoError`]. + #[inline] + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn io(e: E) -> Self { + Self::IoError(e.kind()) + } +} + +impl From for Error { + fn from(kind: embedded_io::ErrorKind) -> Self { + Self::IoError(kind) + } +} + +#[cfg(feature = "std")] +impl From for Error { + fn from(err: std::io::Error) -> Self { + Self::IoError(err.kind().into()) + } } diff --git a/src/lib.rs b/src/lib.rs index 2589d00..4339d40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,31 +1,51 @@ #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] #![warn(clippy::pedantic, missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "alloc")] +extern crate alloc; + mod error; pub use error::Error; +#[cfg(test)] +mod test_util; + mod traits; -pub use traits::{ - DiagnosticDefinition, Identifier, IterableWireFormat, RoutineIdentifier, SingleValueWireFormat, - WireFormat, -}; +pub use traits::{Decode, DecodeIter, Encode}; mod common; -pub use common::*; - -mod protocol_definitions; -pub use protocol_definitions::{ProtocolIdentifier, ProtocolPayload, ProtocolRoutinePayload}; +pub use common::{ + CLEAR_ALL_DTCS, CommunicationControlType, CommunicationType, DTCExtDataRecordNumber, + DTCFormatIdentifier, DTCRecord, DTCSeverityMask, DTCSeverityRecord, DTCSnapshotRecordNumber, + DTCStatusMask, DTCStoredDataRecordNumber, DiagnosticSessionType, FunctionalGroupIdentifier, + NegativeResponseCode, ResetType, SecurityAccessType, UDSIdentifier, UDSRoutineIdentifier, + param_length_u16, param_length_u32, param_length_u64, param_length_u128, +}; mod request; pub use request::Request; mod response; -pub use response::{Response, UdsResponse}; +pub use response::Response; mod service; pub use service::UdsServiceType; mod services; -pub use services::*; +pub use services::{ + ClearDiagnosticInfoRequest, CommunicationControlRequest, CommunicationControlResponse, + ControlDTCSettingsRequest, ControlDTCSettingsResponse, DiagnosticSessionControlRequest, + DiagnosticSessionControlResponse, DirSizePayload, DtcAndStatusIter, DtcFaultDetectionIter, + DtcSeverityAndStatusIter, EcuResetRequest, EcuResetResponse, FileOperationMode, + FileSizePayload, NamePayload, NegativeResponse, PositionPayload, ReadDTCInfoRequest, + ReadDTCInfoResponse, ReadDTCInfoSubFunction, ReadDataByIdentifierRequestTx, + RequestDownloadRequest, RequestDownloadResponse, RequestFileTransferRequest, + RequestFileTransferResponse, RoutineControlRequest, RoutineControlResponse, + SecurityAccessRequest, SecurityAccessResponse, SentDataPayload, SizePayload, + TesterPresentRequest, TesterPresentResponse, TransferDataRequest, TransferDataResponse, + WriteDataByIdentifierRequest, WriteDataByIdentifierResponse, +}; /// UDS positive-response service-ID offset. Added to the request SID to form the response SID. pub const SUCCESS: u8 = 0x80; @@ -33,27 +53,6 @@ pub const SUCCESS: u8 = 0x80; /// Signals that the server received the request but needs additional time to process it. pub const PENDING: u8 = 0x78; -/// Basic UDS implementation of the [`DiagnosticDefinition`] trait. -/// -/// This is an example of a simple data spec that can be used with UDS requests and responses. -/// It should **not** be used directly in production code, but rather as a base for more complex data specifiers. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct UdsSpec; -impl DiagnosticDefinition for UdsSpec { - type RID = UDSRoutineIdentifier; - type DID = ProtocolIdentifier; - type RoutinePayload = ProtocolRoutinePayload; - type DiagnosticPayload = ProtocolPayload; -} - -/// Type alias for a UDS Request type that only implements the messages explicitly defined by the UDS specification. -pub type ProtocolRequest = Request; - -/// Type alias for a UDS Response type that only implements the messages explicitly defined by the UDS specification. -pub type ProtocolResponse = Response; - /// What type of routine control to perform for a [`RoutineControlRequest`]. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -86,44 +85,18 @@ impl From for u8 { } } -impl From for RoutineControlSubFunction { - fn from(value: u8) -> Self { +impl TryFrom for RoutineControlSubFunction { + type Error = Error; + fn try_from(value: u8) -> Result { match value { - 0x01 => RoutineControlSubFunction::StartRoutine, - 0x02 => RoutineControlSubFunction::StopRoutine, - 0x03 => RoutineControlSubFunction::RequestRoutineResults, - _ => panic!("Invalid routine control subfunction: {value}"), + 0x01 => Ok(RoutineControlSubFunction::StartRoutine), + 0x02 => Ok(RoutineControlSubFunction::StopRoutine), + 0x03 => Ok(RoutineControlSubFunction::RequestRoutineResults), + _ => Err(Error::IncorrectMessageLengthOrInvalidFormat), } } } -impl WireFormat for Vec { - fn required_size(&self) -> usize { - self.len() - } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_all(self)?; - Ok(self.len()) - } -} - -impl SingleValueWireFormat for Vec { - fn decode(reader: &mut T) -> Result { - let mut data = Vec::new(); - reader.read_to_end(&mut data)?; - Ok(data) - } -} - -impl IterableWireFormat for Vec { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut data = Vec::new(); - reader.read_to_end(&mut data)?; - Ok(Some(data)) - } -} - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] @@ -148,12 +121,160 @@ impl From for u8 { } } -impl From for DtcSettings { - fn from(value: u8) -> Self { +impl TryFrom for DtcSettings { + type Error = Error; + fn try_from(value: u8) -> Result { match value { - 0x01 => Self::On, - 0x02 => Self::Off, - _ => panic!("Invalid DTC setting: {value}"), + 0x01 => Ok(Self::On), + 0x02 => Ok(Self::Off), + _ => Err(Error::IncorrectMessageLengthOrInvalidFormat), } } } + +#[cfg(test)] +mod no_std_api_tests { + use super::*; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + + #[test] + fn encode_decode_tester_present_roundtrip() { + let req = TesterPresentRequest::new(false); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + + let (decoded, rest) = ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, req); + assert!(rest.is_empty()); + } + + #[test] + fn encode_decode_transfer_data_tx_roundtrip() { + let data = [0x01, 0x02, 0x03, 0x04]; + let req = TransferDataRequest::new(0x05, &data); + let mut buf = [0u8; 16]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 5); + + let (decoded, _) = ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded.block_sequence_counter, 0x05); + assert_eq!(decoded.data, &[0x01, 0x02, 0x03, 0x04]); + } + + #[test] + fn decode_response_tester_present() { + // TesterPresent response: SID=0x7E, sub=0x00 + let wire = [0x7E, 0x00]; + let (resp, _) = Response::decode(&wire).unwrap(); + assert!(matches!(resp, Response::TesterPresent(_))); + } + + #[test] + fn decode_response_negative() { + // NegativeResponse: SID=0x7F, service=0x10, NRC=0x12 + let wire = [0x7F, 0x10, 0x12]; + let (resp, _) = Response::decode(&wire).unwrap(); + assert!(matches!(resp, Response::NegativeResponse(_))); + } + + #[test] + fn decode_request_ecu_reset() { + // EcuReset request: SID=0x11, sub=0x01 (HardReset) + let wire = [0x11, 0x01]; + let (req, _) = Request::decode(&wire).unwrap(); + assert!(matches!(req, Request::EcuReset(_))); + assert_eq!(req.service(), UdsServiceType::EcuReset); + } + + #[cfg(feature = "alloc")] + #[test] + fn dtc_and_status_iter_roundtrip() { + // 2 DTC records: (0x01,0x02,0x03, status=0x0A), (0x04,0x05,0x06, status=0x0B) + let data = [0x01, 0x02, 0x03, 0x0A, 0x04, 0x05, 0x06, 0x0B]; + let iter = DtcAndStatusIter::new(&data); + assert_eq!(iter.len(), 2); + + let records: Vec<_> = iter.map(|r| r.unwrap()).collect(); + assert_eq!(records.len(), 2); + assert_eq!(u32::from(records[0].0), 0x010203); + assert_eq!(u32::from(records[1].0), 0x040506); + } + + #[test] + fn request_frame_roundtrip_prepends_sid() { + // EcuReset request: SID=0x11, sub=0x01 + let wire = [0x11, 0x01]; + let (req, _) = Request::decode(&wire).unwrap(); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + assert_eq!(written, req.encoded_size()); + } + + #[test] + fn response_frame_roundtrip_prepends_sid() { + // NegativeResponse: SID=0x7F, service=0x10, NRC=0x12 + let wire = [0x7F, 0x10, 0x12]; + let (resp, _) = Response::decode(&wire).unwrap(); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + assert_eq!(written, resp.encoded_size()); + } + + #[test] + fn request_file_transfer_frame_roundtrip() { + // RequestFileTransfer: SID=0x38, DeleteFile(0x02), name_len=0x0003, "abc" + let wire = [0x38, 0x02, 0x00, 0x03, b'a', b'b', b'c']; + let (req, _) = Request::decode(&wire).unwrap(); + assert_eq!(req.service(), UdsServiceType::RequestFileTransfer); + let mut buf = [0u8; 16]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } + + #[test] + fn read_dtc_info_response_frame_roundtrip() { + // ReadDTCInfo response: SID=0x59, sub=0x02, mask=0xFF, then DTC records + let wire = [0x59, 0x02, 0xFF, 0x01, 0x02, 0x03, 0x0A]; + let (resp, _) = Response::decode(&wire).unwrap(); + let mut buf = [0u8; 16]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } + + #[test] + fn read_dtc_info_request_encodes_through_public_api() { + // Public-surface construction: types reached via crate root, not common::/services::. + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTC_ByStatusMask( + DTCStatusMask::from(0xFF), + )); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + // sub=0x02 ReportDTC_ByStatusMask, mask=0xFF + assert_eq!(&buf[..written], &[0x02, 0xFF]); + assert_eq!(written, req.encoded_size()); + } + + #[test] + fn write_data_by_identifier_response_roundtrips_through_public_api() { + // Reachability check: the WDBI response codec works through the crate-root public API. + let resp = WriteDataByIdentifierResponse::new(0xBEEF); + let mut buf = [0u8; 4]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + let (decoded, remainder) = + ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, resp); + assert!(remainder.is_empty()); + } + + #[test] + fn const_construction() { + // Verify const construction works at compile time + const _REQ: TransferDataRequest<'static> = TransferDataRequest::new(1, &[0x01, 0x02, 0x03]); + const _SEC: SecurityAccessRequest<'static> = + SecurityAccessRequest::new(false, SecurityAccessType::RequestSeed(0x01), &[0xAA, 0xBB]); + } +} diff --git a/src/protocol_definitions.rs b/src/protocol_definitions.rs deleted file mode 100644 index 8c75718..0000000 --- a/src/protocol_definitions.rs +++ /dev/null @@ -1,286 +0,0 @@ -use crate::{ - Error, IterableWireFormat, SingleValueWireFormat, UDSIdentifier, UDSRoutineIdentifier, - WireFormat, impl_identifier, -}; -use std::ops::Deref; -use tracing::error; - -/// Protocol Identifier provides an implementation of Diagnostics Identifiers that only supports Diagnostic Identifiers defined by UDS -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ProtocolIdentifier { - identifier: UDSIdentifier, -} -impl_identifier!(ProtocolIdentifier); - -impl ProtocolIdentifier { - /// Wrap a [`UDSIdentifier`] in a `ProtocolIdentifier`. - #[must_use] - pub fn new(identifier: UDSIdentifier) -> Self { - ProtocolIdentifier { identifier } - } - - /// Convert an iterator of [`UDSIdentifier`]s into a `Vec`. - pub fn identifiers(list: I) -> Vec - where - I: IntoIterator, - { - list.into_iter().map(Self::new).collect() - } -} - -impl TryFrom for ProtocolIdentifier { - type Error = Error; - fn try_from(value: u16) -> Result { - Ok(Self { - identifier: UDSIdentifier::try_from(value)?, - }) - } -} - -impl From for u16 { - fn from(value: ProtocolIdentifier) -> Self { - u16::from(value.identifier) - } -} - -impl Deref for ProtocolIdentifier { - type Target = UDSIdentifier; - fn deref(&self) -> &UDSIdentifier { - &self.identifier - } -} - -/// The UDS protocol does not define the structure of any payload, but exists as a container for diagnostic implementations that use the generic UDS identifiers -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Eq, PartialEq)] -#[non_exhaustive] -pub struct ProtocolPayload { - /// The UDS data identifier this payload belongs to. - pub identifier: UDSIdentifier, - /// The raw payload bytes following the identifier. - pub payload: Vec, -} - -impl ProtocolPayload { - /// Creates a new `ProtocolPayload` with the given identifier and payload - #[must_use] - pub fn new(identifier: UDSIdentifier, payload: Vec) -> Self { - ProtocolPayload { - identifier, - payload, - } - } -} -impl WireFormat for ProtocolPayload { - fn required_size(&self) -> usize { - 2 + self.payload.len() - } - - fn encode(&self, writer: &mut T) -> Result { - self.identifier.encode(writer)?; - writer.write_all(&self.payload)?; - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for ProtocolPayload { - fn decode(reader: &mut T) -> Result { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 | 1 => { - error!( - "Only read 0 or 1 byte of identifier, need 2: read byte was: {}", - identifier_data[0] - ); - return Err(Error::IncorrectMessageLengthOrInvalidFormat); - } - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - let identifier = UDSIdentifier::try_from(u16::from_be_bytes(identifier_data))?; - let mut payload: Vec = Vec::new(); - reader.read_to_end(&mut payload)?; - Ok(ProtocolPayload { - identifier, - payload, - }) - } -} - -impl IterableWireFormat for ProtocolPayload { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 => return Ok(None), - 1 => { - error!( - "Only read 1 byte of identifier, need 2: read byte was: {}", - identifier_data[0] - ); - return Err(Error::IncorrectMessageLengthOrInvalidFormat); - } - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - let identifier = UDSIdentifier::try_from(u16::from_be_bytes(identifier_data))?; - // Reads the entire payload, but does not have the ability to determine the amount of bytes to read - // depending on the Identifier, so all data is read until EOF - // - // TODO: We could be more clever, we do know the response size of some identifiers - let mut payload: Vec = Vec::new(); - reader.read_to_end(&mut payload)?; - Ok(Some(ProtocolPayload { - identifier, - payload, - })) - } -} - -impl std::fmt::Debug for ProtocolPayload { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} => {}", - self.identifier, - self.payload - .iter() - .map(|b| format!("{b:02X}")) - .collect::>() - .join(" ") - ) - } -} - -/// Routine-specific payload for [`UdsSpec`](crate::UdsSpec). -/// -/// Used as the `RoutinePayload` associated type in [`UdsSpec`](crate::UdsSpec). -/// On the wire, the routine identifier is already encoded by the -/// [`RoutineControlRequest`](crate::RoutineControlRequest), so `encode` writes -/// only the raw payload bytes. `decode` reads the identifier first (since -/// [`RoutineControlResponse`](crate::RoutineControlResponse) includes it in the -/// status record) followed by any remaining payload bytes. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Eq, PartialEq)] -#[non_exhaustive] -pub struct ProtocolRoutinePayload { - /// The routine identifier this payload belongs to. - pub identifier: UDSRoutineIdentifier, - /// The raw payload bytes following the identifier. - pub payload: Vec, -} - -impl ProtocolRoutinePayload { - /// Creates a new `ProtocolRoutinePayload` with the given identifier and payload. - #[must_use] - pub fn new(identifier: UDSRoutineIdentifier, payload: Vec) -> Self { - Self { - identifier, - payload, - } - } -} - -impl WireFormat for ProtocolRoutinePayload { - /// Size of the raw payload only — the identifier is written by the request. - fn required_size(&self) -> usize { - self.payload.len() - } - - /// Writes only the raw payload bytes. The routine identifier is already - /// encoded by [`RoutineControlRequest::encode`](crate::RoutineControlRequest). - fn encode(&self, writer: &mut T) -> Result { - writer.write_all(&self.payload)?; - Ok(self.payload.len()) - } -} - -impl SingleValueWireFormat for ProtocolRoutinePayload { - fn decode(reader: &mut T) -> Result { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 | 1 => { - error!( - "Only read 0 or 1 byte of routine identifier, need 2: read byte was: {}", - identifier_data[0] - ); - return Err(Error::IncorrectMessageLengthOrInvalidFormat); - } - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - let identifier = UDSRoutineIdentifier::from(u16::from_be_bytes(identifier_data)); - let mut payload: Vec = Vec::new(); - reader.read_to_end(&mut payload)?; - Ok(Self { - identifier, - payload, - }) - } -} - -impl IterableWireFormat for ProtocolRoutinePayload { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 => return Ok(None), - 1 => { - error!( - "Only read 1 byte of routine identifier, need 2: read byte was: {}", - identifier_data[0] - ); - return Err(Error::IncorrectMessageLengthOrInvalidFormat); - } - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - let identifier = UDSRoutineIdentifier::from(u16::from_be_bytes(identifier_data)); - let mut payload: Vec = Vec::new(); - reader.read_to_end(&mut payload)?; - Ok(Some(Self { - identifier, - payload, - })) - } -} - -impl std::fmt::Debug for ProtocolRoutinePayload { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{:?} => {}", - self.identifier, - self.payload - .iter() - .map(|b| format!("{b:02X}")) - .collect::>() - .join(" ") - ) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_construction_and_debug_format() { - let payload = ProtocolPayload::new(UDSIdentifier::ActiveDiagnosticSession, vec![0x01]); - assert_eq!(format!("{payload:?}"), "0xF186 => 01"); - let mut buffer = Vec::new(); - assert_eq!(3, payload.encode(&mut buffer).unwrap()); - } - - #[test] - fn test_read_and_write() { - let payload = ProtocolPayload::new(UDSIdentifier::ActiveDiagnosticSession, vec![0x03]); - let mut buffer = Vec::new(); - assert_eq!(3, payload.encode(&mut buffer).unwrap()); - let deserialized_payload = ProtocolPayload::decode(&mut buffer.as_slice()).unwrap(); - assert_eq!(payload, deserialized_payload); - } -} diff --git a/src/request.rs b/src/request.rs index 3d71b82..b31dd1a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,253 +1,199 @@ //! Module for making and handling UDS Requests use crate::{ - DiagnosticDefinition, Error, NegativeResponseCode, ReadDTCInfoRequest, ResetType, - SecurityAccessType, SingleValueWireFormat, WireFormat, + Decode, Encode, Error, services::{ ClearDiagnosticInfoRequest, CommunicationControlRequest, ControlDTCSettingsRequest, - DiagnosticSessionControlRequest, EcuResetRequest, ReadDataByIdentifierRequest, - RequestDownloadRequest, RoutineControlRequest, SecurityAccessRequest, TesterPresentRequest, - TransferDataRequest, WriteDataByIdentifierRequest, + DiagnosticSessionControlRequest, EcuResetRequest, ReadDTCInfoRequest, + RequestDownloadRequest, RequestFileTransferRequest, RoutineControlRequest, + SecurityAccessRequest, TesterPresentRequest, TransferDataRequest, + WriteDataByIdentifierRequest, }, }; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{Read, Write}; -use super::{ - CommunicationControlType, CommunicationType, DTCRecord, DataFormatIdentifier, - DiagnosticSessionType, DtcSettings, ReadDTCInfoSubFunction, RoutineControlSubFunction, - service::UdsServiceType, -}; +use super::service::UdsServiceType; -/// UDS Request types -/// Each variant corresponds to a request for a different UDS service -/// The variants contain all request data for each service -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +/// Zero-copy parsed request. Borrows from the wire buffer. +/// +/// Variable-length payloads are stored as raw `&'a [u8]` slices that can be +/// further parsed on demand. +#[derive(Clone, Debug)] #[non_exhaustive] -pub enum Request { - /// Request to clear diagnostic information. See [`ClearDiagnosticInfoRequest`]. +pub enum Request<'a> { + /// Clear diagnostic information request. ClearDiagnosticInfo(ClearDiagnosticInfoRequest), - /// Request to control communication. See [`CommunicationControlRequest`]. + /// Communication control request. CommunicationControl(CommunicationControlRequest), - /// Request to enable or disable DTC setting. See [`ControlDTCSettingsRequest`]. + /// Control DTC settings request. ControlDTCSettings(ControlDTCSettingsRequest), - /// Request to change the diagnostic session. See [`DiagnosticSessionControlRequest`]. + /// Diagnostic session control request. DiagnosticSessionControl(DiagnosticSessionControlRequest), - /// Request to reset the ECU. See [`EcuResetRequest`]. + /// ECU reset request. EcuReset(EcuResetRequest), - /// Request to read data by identifier. See [`ReadDataByIdentifierRequest`]. - ReadDataByIdentifier(ReadDataByIdentifierRequest), - /// Request to read DTC information. See [`ReadDTCInfoRequest`]. + /// Read data by identifier request. Raw DID bytes. + ReadDataByIdentifier(&'a [u8]), + /// Read DTC information request. ReadDTCInfo(ReadDTCInfoRequest), - /// Request to initiate a download. See [`RequestDownloadRequest`]. + /// Request download. RequestDownload(RequestDownloadRequest), - /// Request to exit an active transfer. + /// Request file transfer. + RequestFileTransfer(RequestFileTransferRequest<'a>), + /// Request transfer exit. RequestTransferExit, - /// Request to control a routine. See [`RoutineControlRequest`]. - RoutineControl(RoutineControlRequest), - /// Request for security access. See [`SecurityAccessRequest`]. - SecurityAccess(SecurityAccessRequest), - /// Tester present keep-alive request. See [`TesterPresentRequest`]. + /// Routine control request. + RoutineControl(RoutineControlRequest<'a>), + /// Security access request. + SecurityAccess(SecurityAccessRequest<'a>), + /// Tester present request. TesterPresent(TesterPresentRequest), - /// Request to transfer data. See [`TransferDataRequest`]. - TransferData(TransferDataRequest), - /// Request to write data by identifier. See [`WriteDataByIdentifierRequest`]. - WriteDataByIdentifier(WriteDataByIdentifierRequest), -} - -impl Request { - /// Create a `ClearDiagnosticInfo` request, clears diagnostic information in one or more servers' memory - #[must_use] - pub fn clear_diagnostic_info(group_of_dtc: DTCRecord, memory_selection: u8) -> Self { - Request::ClearDiagnosticInfo(ClearDiagnosticInfoRequest::new( - group_of_dtc, - memory_selection, - )) - } - /// Create a `ClearDiagnosticInfo` request that clears all DTC information in one or more servers' memory - #[must_use] - pub fn clear_all_dtc_info(memory_selection: u8) -> Self { - Request::ClearDiagnosticInfo(ClearDiagnosticInfoRequest::clear_all(memory_selection)) - } - - /// Create a `CommunicationControlRequest` with standard address information. - /// - /// # Panics - /// - /// Panics if one of the extended address control types is passed. - #[must_use] - pub fn communication_control( - communication_enable: CommunicationControlType, - communication_type: CommunicationType, - suppress_response: bool, - ) -> Self { - Request::CommunicationControl(CommunicationControlRequest::new( - suppress_response, - communication_enable, - communication_type, - )) - } - - /// Create a `CommunicationControl` request with extended address information. - /// This is used for the `EnableRxAndDisableTxWithEnhancedAddressInfo` and - /// `EnableRxAndTxWithEnhancedAddressInfo` communication control types. - /// - /// # Panics - /// - /// Panics if one of the standard address control types is passed. - #[must_use] - pub fn communication_control_with_node_id( - communication_enable: CommunicationControlType, - communication_type: CommunicationType, - node_id: u16, - suppress_response: bool, - ) -> Self { - Request::CommunicationControl(CommunicationControlRequest::new_with_node_id( - suppress_response, - communication_enable, - communication_type, - node_id, - )) - } - - /// Create a new `ControlDTCSettings` request - #[must_use] - pub fn control_dtc_settings(setting: DtcSettings, suppress_response: bool) -> Self { - Request::ControlDTCSettings(ControlDTCSettingsRequest::new(setting, suppress_response)) - } - - /// Create a new `DiagnosticSessionControl` request - #[must_use] - pub fn diagnostic_session_control( - suppress_positive_response: bool, - session_type: DiagnosticSessionType, - ) -> Self { - Request::DiagnosticSessionControl(DiagnosticSessionControlRequest::new( - suppress_positive_response, - session_type, - )) - } - - /// Create a new `EcuReset` request - #[must_use] - pub fn ecu_reset(suppress_positive_response: bool, reset_type: ResetType) -> Self { - Request::EcuReset(EcuResetRequest::new(suppress_positive_response, reset_type)) - } - - /// Create a new `ReadDataByIdentifier` request - pub fn read_data_by_identifier(dids: I) -> Self - where - I: IntoIterator, - { - Request::ReadDataByIdentifier(ReadDataByIdentifierRequest::new(dids)) - } - - /// Create a new `ReadDTCInformation` request for the given sub-function. - #[must_use] - pub fn read_dtc_information(sub_function: ReadDTCInfoSubFunction) -> Self { - Request::ReadDTCInfo(ReadDTCInfoRequest::new(sub_function)) - } - - /// Create a new `RequestDownload` request - /// `encryption_method`: vehicle manufacturer specific (0x0 for no encryption) - /// `compression_method`: vehicle manufacturer specific (0x0 for no compression) - /// `memory_address`: the address in memory to start downloading from (Maximum 40 bits - 1024GB) - /// `memory_size`: the size of the memory to download (Max 4GB) - /// - /// # Errors - /// Will generate an error of type `Error::InvalidEncryptionCompressionMethod()`. - /// Generated when `compression_method` or `encryption_method` > 0x15 - pub fn request_download( - encryption_method: u8, - compression_method: u8, - memory_address: u64, - memory_size: u32, - ) -> Result { - let data_format_identifier = - DataFormatIdentifier::new(compression_method, encryption_method)?; - - Ok(Request::RequestDownload(RequestDownloadRequest::new( - data_format_identifier, - memory_address, - memory_size, - )?)) - } - - /// Create a `RequestTransferExit` request to end an active upload or download. - #[must_use] - pub fn request_transfer_exit() -> Self { - Self::RequestTransferExit - } - - /// Create a new `RoutineControl` request with no payload + /// Transfer data request. + TransferData(TransferDataRequest<'a>), + /// Write data by identifier request. + WriteDataByIdentifier(WriteDataByIdentifierRequest<'a>), + /// A known-but-unmodeled (or unrecognized) service. Carries the service type and + /// the raw payload bytes following the service identifier, for pass-through. /// - /// **Note**: This does not check if the server requires a payload to perform the routine - /// # Parameters: - /// * `sub_function`: The type of routine control to perform. - /// * [`RoutineControlSubFunction::StartRoutine`] - /// * [`RoutineControlSubFunction::StopRoutine`] - /// * [`RoutineControlSubFunction::RequestRoutineResults`] - /// * `routine_id`: The identifier of the routine to control - pub fn routine_control(sub_function: RoutineControlSubFunction, routine_id: D::RID) -> Self { - Request::RoutineControl(RoutineControlRequest::new(sub_function, routine_id, None)) - } + /// Re-encoding is lossless for any service byte in the ISO 14229-1 table; a byte + /// that maps to [`UdsServiceType::UnsupportedDiagnosticService`] re-encodes as `0x7F`. + Other { + /// The service this frame addresses. + service: UdsServiceType, + /// Raw payload bytes after the service byte. + data: &'a [u8], + }, +} - /// Create a new `RoutineControl` request - /// - /// **Note**: This could be cleaner as the Identifier is technically represented in the `RoutinePayload` - /// and if the `RoutinePayload` is a single value, then the `RoutineIdentifier` is not needed - /// - /// This does not check if the server requires a payload - /// - /// # Parameters: - /// * `sub_function`: The type of routine control to perform. - /// * [`RoutineControlSubFunction::StartRoutine`] - /// * [`RoutineControlSubFunction::StopRoutine`] - /// * [`RoutineControlSubFunction::RequestRoutineResults`] - /// * `routine_id`: The identifier of the routine to control. User defined routine identifiers and payloads are allowed - /// * General purpose/UDS defined: [`crate::UDSRoutineIdentifier`] - /// * `data`: Optional payload for the routine control request - pub fn routine_control_payload( - sub_function: RoutineControlSubFunction, - routine_id: D::RID, - data: Option, - ) -> Self { - Request::RoutineControl(RoutineControlRequest::new(sub_function, routine_id, data)) +impl<'a> Decode<'a> for Request<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let service = UdsServiceType::service_from_request_byte(buf[0]); + let payload = &buf[1..]; + + let request = match service { + UdsServiceType::ClearDiagnosticInfo => Self::ClearDiagnosticInfo( + ::decode_exact(payload)?, + ), + UdsServiceType::CommunicationControl => Self::CommunicationControl( + ::decode_exact(payload)?, + ), + UdsServiceType::ControlDTCSettings => Self::ControlDTCSettings( + ::decode_exact(payload)?, + ), + UdsServiceType::DiagnosticSessionControl => Self::DiagnosticSessionControl( + ::decode_exact(payload)?, + ), + UdsServiceType::EcuReset => { + Self::EcuReset(::decode_exact(payload)?) + } + UdsServiceType::ReadDataByIdentifier => Self::ReadDataByIdentifier(payload), + UdsServiceType::ReadDTCInfo => { + Self::ReadDTCInfo(::decode_exact(payload)?) + } + UdsServiceType::RequestDownload => { + Self::RequestDownload(::decode_exact(payload)?) + } + UdsServiceType::RequestFileTransfer => Self::RequestFileTransfer( + ::decode_exact(payload)?, + ), + UdsServiceType::RequestTransferExit => Self::RequestTransferExit, + UdsServiceType::RoutineControl => { + Self::RoutineControl(::decode_exact(payload)?) + } + UdsServiceType::SecurityAccess => { + Self::SecurityAccess(::decode_exact(payload)?) + } + UdsServiceType::TesterPresent => { + Self::TesterPresent(::decode_exact(payload)?) + } + UdsServiceType::TransferData => { + Self::TransferData(::decode_exact(payload)?) + } + UdsServiceType::WriteDataByIdentifier => Self::WriteDataByIdentifier( + ::decode_exact(payload)?, + ), + _ => Self::Other { + service, + data: payload, + }, + }; + Ok((request, &[])) } +} - /// Create a new `SecurityAccess` request (seed or key phase). - #[must_use] - pub fn security_access( - suppress_positive_response: bool, - access_type: SecurityAccessType, - data_record: Vec, - ) -> Self { - Request::SecurityAccess(SecurityAccessRequest::new( - suppress_positive_response, - access_type, - data_record, - )) +impl Encode for Request<'_> { + fn encoded_size(&self) -> usize { + let payload = match self { + Self::ClearDiagnosticInfo(req) => req.encoded_size(), + Self::CommunicationControl(req) => req.encoded_size(), + Self::ControlDTCSettings(req) => req.encoded_size(), + Self::DiagnosticSessionControl(req) => req.encoded_size(), + Self::EcuReset(req) => req.encoded_size(), + Self::ReadDataByIdentifier(bytes) => bytes.len(), + Self::ReadDTCInfo(req) => req.encoded_size(), + Self::WriteDataByIdentifier(req) => req.encoded_size(), + Self::RequestDownload(req) => req.encoded_size(), + Self::RequestFileTransfer(req) => req.encoded_size(), + Self::RequestTransferExit => 0, + Self::Other { data, .. } => data.len(), + Self::RoutineControl(req) => req.encoded_size(), + Self::SecurityAccess(req) => req.encoded_size(), + Self::TesterPresent(req) => req.encoded_size(), + Self::TransferData(req) => req.encoded_size(), + }; + 1 + payload } - /// Create a new `TesterPresent` keep-alive request. - #[must_use] - pub fn tester_present(suppress_positive_response: bool) -> Self { - Request::TesterPresent(TesterPresentRequest::new(suppress_positive_response)) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.service().request_service_to_byte()]) + .map_err(Error::io)?; + let payload = match self { + Self::ClearDiagnosticInfo(req) => req.encode(writer)?, + Self::CommunicationControl(req) => req.encode(writer)?, + Self::ControlDTCSettings(req) => req.encode(writer)?, + Self::DiagnosticSessionControl(req) => req.encode(writer)?, + Self::EcuReset(req) => req.encode(writer)?, + Self::ReadDataByIdentifier(bytes) => { + writer.write_all(bytes).map_err(Error::io)?; + bytes.len() + } + Self::ReadDTCInfo(req) => req.encode(writer)?, + Self::WriteDataByIdentifier(req) => req.encode(writer)?, + Self::RequestDownload(req) => req.encode(writer)?, + Self::RequestFileTransfer(req) => req.encode(writer)?, + Self::RequestTransferExit => 0, + Self::Other { data, .. } => { + writer.write_all(data).map_err(Error::io)?; + data.len() + } + Self::RoutineControl(req) => req.encode(writer)?, + Self::SecurityAccess(req) => req.encode(writer)?, + Self::TesterPresent(req) => req.encode(writer)?, + Self::TransferData(req) => req.encode(writer)?, + }; + Ok(1 + payload) } +} - /// Create a new `TransferData` request with the given block-sequence counter and payload. +impl Request<'_> { + /// Whether the positive response for this request is suppressed (SPRMIB). #[must_use] - pub fn transfer_data(sequence: u8, data: Vec) -> Self { - Request::TransferData(TransferDataRequest::new(sequence, data)) - } - - /// Create a new `WriteDataByIdentifier` request with the given payload. - pub fn write_data_by_identifier(payload: D::DiagnosticPayload) -> Self { - Request::WriteDataByIdentifier(WriteDataByIdentifierRequest::new(payload)) + pub fn is_positive_response_suppressed(&self) -> bool { + match self { + Self::CommunicationControl(req) => req.suppress_positive_response(), + Self::ControlDTCSettings(req) => req.suppress_positive_response(), + Self::DiagnosticSessionControl(req) => req.suppress_positive_response(), + Self::EcuReset(req) => req.suppress_positive_response(), + Self::RoutineControl(req) => req.suppress_positive_response(), + Self::SecurityAccess(req) => req.suppress_positive_response(), + Self::TesterPresent(req) => req.suppress_positive_response(), + _ => false, + } } /// Returns the [`UdsServiceType`] corresponding to this request variant. + #[must_use] pub fn service(&self) -> UdsServiceType { match self { Self::ClearDiagnosticInfo(_) => UdsServiceType::ClearDiagnosticInfo, @@ -258,297 +204,86 @@ impl Request { Self::ReadDataByIdentifier(_) => UdsServiceType::ReadDataByIdentifier, Self::ReadDTCInfo(_) => UdsServiceType::ReadDTCInfo, Self::RequestDownload(_) => UdsServiceType::RequestDownload, + Self::RequestFileTransfer(_) => UdsServiceType::RequestFileTransfer, Self::RequestTransferExit => UdsServiceType::RequestTransferExit, Self::RoutineControl(_) => UdsServiceType::RoutineControl, Self::SecurityAccess(_) => UdsServiceType::SecurityAccess, Self::TesterPresent(_) => UdsServiceType::TesterPresent, Self::TransferData(_) => UdsServiceType::TransferData, Self::WriteDataByIdentifier(_) => UdsServiceType::WriteDataByIdentifier, - } - } - - /// Returns the negative-response codes that are valid for this request's service. - pub fn allowed_nack_codes(&self) -> &'static [NegativeResponseCode] { - match self { - Self::ClearDiagnosticInfo(_) => ClearDiagnosticInfoRequest::allowed_nack_codes(), - Self::DiagnosticSessionControl(_) => { - DiagnosticSessionControlRequest::allowed_nack_codes() - } - Self::EcuReset(_) => EcuResetRequest::allowed_nack_codes(), - Self::SecurityAccess(_) => SecurityAccessRequest::allowed_nack_codes(), - Self::RequestDownload(_) => RequestDownloadRequest::allowed_nack_codes(), - _ => &[NegativeResponseCode::ServiceNotSupported], + Self::Other { service, .. } => *service, } } } -impl WireFormat for Request { - fn required_size(&self) -> usize { - 1 + match self { - Self::ClearDiagnosticInfo(cdi) => cdi.required_size(), - Self::CommunicationControl(cc) => cc.required_size(), - Self::ControlDTCSettings(ct) => ct.required_size(), - Self::DiagnosticSessionControl(ds) => ds.required_size(), - Self::EcuReset(er) => er.required_size(), - Self::ReadDataByIdentifier(rd) => rd.required_size(), - Self::ReadDTCInfo(rd) => rd.required_size(), - Self::RequestDownload(rd) => rd.required_size(), - Self::RequestTransferExit => 0, - Self::RoutineControl(rc) => rc.required_size(), - Self::SecurityAccess(sa) => sa.required_size(), - Self::TesterPresent(tp) => tp.required_size(), - Self::TransferData(td) => td.required_size(), - Self::WriteDataByIdentifier(wd) => wd.required_size(), - } - } - - /// Serialization function to write a [`Request`] to a [`Writer`](std::io::Write) - /// This function writes the service byte and then calls the appropriate - /// serialization function for the service represented by self. - fn encode(&self, writer: &mut W) -> Result { - // Write the service byte - writer.write_u8(self.service().request_service_to_byte())?; - // Write the payload - Ok(1 + match self { - Self::ClearDiagnosticInfo(cdi) => cdi.encode(writer), - Self::CommunicationControl(cc) => cc.encode(writer), - Self::ControlDTCSettings(ct) => ct.encode(writer), - Self::DiagnosticSessionControl(ds) => ds.encode(writer), - Self::EcuReset(er) => er.encode(writer), - Self::ReadDataByIdentifier(rd) => rd.encode(writer), - Self::ReadDTCInfo(rd) => rd.encode(writer), - Self::RequestDownload(rd) => rd.encode(writer), - Self::RequestTransferExit => Ok(0), - Self::RoutineControl(rc) => rc.encode(writer), - Self::SecurityAccess(sa) => sa.encode(writer), - Self::TesterPresent(tp) => tp.encode(writer), - Self::TransferData(td) => td.encode(writer), - Self::WriteDataByIdentifier(wd) => wd.encode(writer), - }?) - } - - fn is_positive_response_suppressed(&self) -> bool { - match self { - Self::CommunicationControl(cc) => cc.suppress_positive_response(), - Self::ControlDTCSettings(ct) => ct.is_positive_response_suppressed(), - Self::DiagnosticSessionControl(ds) => ds.suppress_positive_response(), - Self::EcuReset(er) => er.suppress_positive_response(), - Self::SecurityAccess(sa) => sa.suppress_positive_response(), - Self::TesterPresent(tp) => tp.suppress_positive_response(), - _ => false, - } - } -} - -impl SingleValueWireFormat for Request { - /// Deserialization function to read a [`Request`] from a [`Reader`](std::io::Read) - /// This function reads the service byte and then calls the appropriate - /// deserialization function for the service in question - /// - /// *Note*: - /// - /// Some services allow for custom byte arrays at the end of the request - /// It is important that only the request data is passed to this function - /// or the deserialization could read unexpected data - #[allow(clippy::too_many_lines)] - fn decode(reader: &mut R) -> Result { - let service = UdsServiceType::service_from_request_byte(reader.read_u8()?); - Ok(match service { - UdsServiceType::CommunicationControl => { - Self::CommunicationControl(CommunicationControlRequest::decode(reader)?) - } - UdsServiceType::ControlDTCSettings => { - Self::ControlDTCSettings(ControlDTCSettingsRequest::decode(reader)?) - } - UdsServiceType::DiagnosticSessionControl => { - Self::DiagnosticSessionControl(DiagnosticSessionControlRequest::decode(reader)?) - } - UdsServiceType::EcuReset => Self::EcuReset(EcuResetRequest::decode(reader)?), - UdsServiceType::ReadDataByIdentifier => { - Self::ReadDataByIdentifier(ReadDataByIdentifierRequest::decode(reader)?) - } - UdsServiceType::ReadDTCInfo => Self::ReadDTCInfo(ReadDTCInfoRequest::decode(reader)?), - UdsServiceType::RequestDownload => { - Self::RequestDownload(RequestDownloadRequest::decode(reader)?) - } - UdsServiceType::RequestTransferExit => Self::RequestTransferExit, - UdsServiceType::RoutineControl => { - Self::RoutineControl(RoutineControlRequest::decode(reader)?) - } - UdsServiceType::SecurityAccess => { - Self::SecurityAccess(SecurityAccessRequest::decode(reader)?) - } - UdsServiceType::TesterPresent => { - Self::TesterPresent(TesterPresentRequest::decode(reader)?) - } - UdsServiceType::TransferData => { - Self::TransferData(TransferDataRequest::decode(reader)?) - } - UdsServiceType::WriteDataByIdentifier => { - Self::WriteDataByIdentifier(WriteDataByIdentifierRequest::decode(reader)?) - } - UdsServiceType::Authentication => { - return Err(Error::ServiceNotImplemented(UdsServiceType::Authentication)); - } - UdsServiceType::AccessTimingParameters => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::AccessTimingParameters, - )); - } - UdsServiceType::SecuredDataTransmission => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::SecuredDataTransmission, - )); - } - UdsServiceType::ResponseOnEvent => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ResponseOnEvent, - )); - } - UdsServiceType::LinkControl => { - return Err(Error::ServiceNotImplemented(UdsServiceType::LinkControl)); - } - UdsServiceType::ReadMemoryByAddress => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ReadMemoryByAddress, - )); - } - UdsServiceType::ReadScalingDataByIdentifier => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ReadScalingDataByIdentifier, - )); - } - UdsServiceType::ReadDataByIdentifierPeriodic => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ReadDataByIdentifierPeriodic, - )); - } - UdsServiceType::DynamicallyDefinedDataIdentifier => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::DynamicallyDefinedDataIdentifier, - )); - } - UdsServiceType::WriteMemoryByAddress => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::WriteMemoryByAddress, - )); - } - UdsServiceType::ClearDiagnosticInfo => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ClearDiagnosticInfo, - )); - } - UdsServiceType::InputOutputControlByIdentifier => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::InputOutputControlByIdentifier, - )); - } - UdsServiceType::RequestUpload => { - return Err(Error::ServiceNotImplemented(UdsServiceType::RequestUpload)); - } - UdsServiceType::RequestFileTransfer => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::RequestFileTransfer, - )); - } - UdsServiceType::NegativeResponse => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::NegativeResponse, - )); - } - UdsServiceType::UnsupportedDiagnosticService => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::UnsupportedDiagnosticService, - )); - } - }) - } -} - #[cfg(test)] mod tests { use super::*; - use crate::{ - CommunicationControlType, CommunicationType, ProtocolRequest, ResetType, SecurityAccessType, - }; + use crate::{ResetType, service::UdsServiceType}; #[test] - fn test_is_positive_response_suppressed() { - let communication_control_request = ProtocolRequest::communication_control( - CommunicationControlType::EnableRxAndTx, - CommunicationType::Normal, - true, - ); - assert!(communication_control_request.is_positive_response_suppressed()); - - let control_dtc_settings_request = - ProtocolRequest::control_dtc_settings(DtcSettings::On, true); - assert!(control_dtc_settings_request.is_positive_response_suppressed()); - - let diagnostic_session_control_request = ProtocolRequest::diagnostic_session_control( - true, - DiagnosticSessionType::ProgrammingSession, - ); - assert!(diagnostic_session_control_request.is_positive_response_suppressed()); - let diagnostic_session_control_request = ProtocolRequest::diagnostic_session_control( - false, - DiagnosticSessionType::ProgrammingSession, - ); - let should_not_be_suppressed = - diagnostic_session_control_request.is_positive_response_suppressed(); - assert!(!should_not_be_suppressed); - - let ecu_reset_request = ProtocolRequest::ecu_reset(true, ResetType::HardReset); - assert!(ecu_reset_request.is_positive_response_suppressed()); - - let security_access_request = ProtocolRequest::security_access( - true, - SecurityAccessType::ISO26021_2SendKeyValues, - vec![0x01, 0x02], - ); - assert!(security_access_request.is_positive_response_suppressed()); + fn decode_rejects_trailing_bytes() { + // ECU reset is a fixed 1-byte payload; an extra trailing byte is a + // malformed frame and must be rejected rather than silently dropped. + let mut frame = [0u8; 3]; + frame[0] = UdsServiceType::EcuReset.request_service_to_byte(); + frame[1] = u8::from(ResetType::HardReset); + frame[2] = 0xAA; // trailing junk + let result = Request::decode(&frame); + assert!(matches!( + result, + Err(Error::IncorrectMessageLengthOrInvalidFormat) + )); + } - let tester_present_request = ProtocolRequest::tester_present(true); - assert!(tester_present_request.is_positive_response_suppressed()); + #[test] + fn suppression_forwards_to_inner_request() { + let suppressed = Request::EcuReset(EcuResetRequest::new(true, ResetType::HardReset)); + assert!(suppressed.is_positive_response_suppressed()); - let clear_diagnostic_info_request = - ProtocolRequest::clear_diagnostic_info(DTCRecord::new(0x01, 0x02, 0x03), 0x01); - assert!(!clear_diagnostic_info_request.is_positive_response_suppressed()); + let not_suppressed = Request::EcuReset(EcuResetRequest::new(false, ResetType::HardReset)); + assert!(!not_suppressed.is_positive_response_suppressed()); } #[test] - fn test_communication_control_sprmib_wire_bytes() { - // DisableRxAndTx with suppress=true (as used in firmware_flashing archive) - let request = ProtocolRequest::communication_control( - CommunicationControlType::DisableRxAndTx, - CommunicationType::Normal, - true, - ); - assert!(request.is_positive_response_suppressed()); + fn write_data_by_identifier_request_roundtrips() { + // SID 0x2E, DID 0xF190, one data byte 0x01 + let wire = [0x2E, 0xF1, 0x90, 0x01]; + let (req, rest) = Request::decode(&wire).unwrap(); + assert!(rest.is_empty()); + assert!(matches!(req, Request::WriteDataByIdentifier(_))); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } - let mut bytes = Vec::new(); - request.encode(&mut bytes).unwrap(); - // SID=0x28, sub-function=0x03|0x80=0x83, communication_type - assert_eq!(bytes[0], 0x28, "SID should be 0x28"); - assert_eq!( - bytes[1], 0x83, - "Sub-function should be 0x83 (DisableRxAndTx=0x03 | SPRMIB=0x80), got {:#04x}", - bytes[1] - ); + #[test] + fn routine_control_request_roundtrips_with_suppress_bit() { + // SID 0x31, sub 0x81 (StartRoutine + SPRMIB), RID 0xFF00, param 0xAA + let wire = [0x31, 0x81, 0xFF, 0x00, 0xAA]; + let (req, rest) = Request::decode(&wire).unwrap(); + assert!(rest.is_empty()); + assert!(req.is_positive_response_suppressed()); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } - // EnableRxAndTx with suppress=true - let request2 = ProtocolRequest::communication_control( - CommunicationControlType::EnableRxAndTx, - CommunicationType::Normal, - true, - ); - let mut bytes2 = Vec::new(); - request2.encode(&mut bytes2).unwrap(); - assert_eq!(bytes2[0], 0x28); - assert_eq!( - bytes2[1], 0x80, - "Sub-function should be 0x80 (EnableRxAndTx=0x00 | SPRMIB=0x80), got {:#04x}", - bytes2[1] - ); + #[test] + fn unmodeled_service_decodes_to_other() { + // 0x23 = ReadMemoryByAddress, enumerated but not modeled. + let frame = [0x23, 0xAA, 0xBB]; + let (req, rest) = Request::decode(&frame).unwrap(); + assert!(rest.is_empty()); + match req { + Request::Other { service, data } => { + assert_eq!(service, UdsServiceType::ReadMemoryByAddress); + assert_eq!(data, &[0xAA, 0xBB]); + } + other => panic!("expected Other, got {other:?}"), + } + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &frame); } } diff --git a/src/response.rs b/src/response.rs index de78a02..cc3cead 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,341 +1,253 @@ use crate::{ - CommunicationControlResponse, CommunicationControlType, ControlDTCSettingsResponse, - DiagnosticDefinition, DiagnosticSessionControlResponse, DiagnosticSessionType, DtcSettings, - EcuResetResponse, Error, NegativeResponse, NegativeResponseCode, ReadDTCInfoResponse, - ReadDataByIdentifierResponse, RequestDownloadResponse, RequestFileTransferResponse, ResetType, - RoutineControlResponse, SecurityAccessResponse, SecurityAccessType, SingleValueWireFormat, - TesterPresentResponse, TransferDataResponse, UdsServiceType, WireFormat, - WriteDataByIdentifierResponse, + CommunicationControlResponse, ControlDTCSettingsResponse, Decode, + DiagnosticSessionControlResponse, EcuResetResponse, Encode, Error, NegativeResponse, + ReadDTCInfoResponse, RequestDownloadResponse, RequestFileTransferResponse, + RoutineControlResponse, SecurityAccessResponse, TesterPresentResponse, TransferDataResponse, + UdsServiceType, WriteDataByIdentifierResponse, }; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{Read, Write}; -/// A raw UDS response consisting of the service type and its unparsed payload bytes. +/// Parsed zero-copy UDS response. Borrows from the wire buffer. +/// +/// Variable-length payloads are stored as raw `&'a [u8]` slices that can be +/// further parsed on demand. +#[derive(Clone, Debug)] #[non_exhaustive] -pub struct UdsResponse { - /// The service this response corresponds to. - pub service: UdsServiceType, - /// The raw payload bytes following the service identifier. - pub data: Vec, -} - -/// Parsed UDS response. Each variant corresponds to a different UDS service response. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -#[non_exhaustive] -pub enum Response { - /// Response to a [`ClearDiagnosticInfoRequest`](crate::ClearDiagnosticInfoRequest) +pub enum Response<'a> { + /// Positive response to `ClearDiagnosticInfo`. ClearDiagnosticInfo, - /// Response to a [`CommunicationControlRequest`](crate::CommunicationControlRequest) + /// Positive response to `CommunicationControl`. CommunicationControl(CommunicationControlResponse), - /// Response to a [`ControlDTCSettingsRequest`](crate::ControlDTCSettingsRequest) + /// Positive response to `ControlDTCSettings`. ControlDTCSettings(ControlDTCSettingsResponse), - /// Response to a [`DiagnosticSessionControlRequest`](crate::DiagnosticSessionControlRequest) + /// Positive response to `DiagnosticSessionControl`. DiagnosticSessionControl(DiagnosticSessionControlResponse), - /// Response to a [`EcuResetRequest`](crate::EcuResetRequest) + /// Positive response to `EcuReset`. EcuReset(EcuResetResponse), - /// Negative response to any request + /// Negative response to any request. NegativeResponse(NegativeResponse), - /// Response to a [`ReadDataByIdentifierRequest`](crate::ReadDataByIdentifierRequest) - ReadDataByIdentifier(ReadDataByIdentifierResponse), - /// Response to a [`ReadDTCInfoRequest`](crate::ReadDTCInfoRequest) - ReadDTCInfo(ReadDTCInfoResponse), - /// Response to a [`RequestDownloadRequest`](crate::RequestDownloadRequest) - RequestDownload(RequestDownloadResponse), - /// Response to a [`RequestFileTransferRequest`](crate::RequestFileTransferRequest) - RequestFileTransfer(RequestFileTransferResponse), - /// Response to a `RequestTransferExit` request + /// Positive response to `ReadDataByIdentifier`. Raw payload bytes. + ReadDataByIdentifier(&'a [u8]), + /// Positive response to `ReadDTCInformation` with lazy iterators. + ReadDTCInfo(ReadDTCInfoResponse<'a>), + /// Positive response to `RequestDownload`. + RequestDownload(RequestDownloadResponse<'a>), + /// Positive response to `RequestFileTransfer`. + RequestFileTransfer(RequestFileTransferResponse<'a>), + /// Positive response to `RequestTransferExit`. RequestTransferExit, - /// Response to a [`RoutineControl` request](crate::RoutineControlRequest) - RoutineControl(RoutineControlResponse), - /// Response to a [`SecurityAccessRequest`](crate::SecurityAccessRequest) - SecurityAccess(SecurityAccessResponse), - /// Response to a [`TesterPresentRequest`](crate::TesterPresentRequest) + /// Positive response to `RoutineControl`. + RoutineControl(RoutineControlResponse<'a>), + /// Positive response to `SecurityAccess`. + SecurityAccess(SecurityAccessResponse<'a>), + /// Positive response to `TesterPresent`. TesterPresent(TesterPresentResponse), - /// Response to a [`TransferDataRequest`](crate::TransferDataRequest) - TransferData(TransferDataResponse), - /// Response to a [`WriteDataByIdentifierRequest`](crate::WriteDataByIdentifierRequest) - WriteDataByIdentifier(WriteDataByIdentifierResponse), -} - -impl Response { - /// Create a `ClearDiagnosticInfo` positive response. - #[must_use] - pub fn clear_diagnostic_info() -> Self { - Response::ClearDiagnosticInfo - } - /// Create a `CommunicationControl` positive response. - #[must_use] - pub fn communication_control(control_type: CommunicationControlType) -> Self { - Response::CommunicationControl(CommunicationControlResponse::new(control_type)) - } - - /// Create a `ControlDTCSettings` positive response. - #[must_use] - pub fn control_dtc_settings(setting: DtcSettings) -> Self { - Response::ControlDTCSettings(ControlDTCSettingsResponse::new(setting)) - } - - /// Create a `DiagnosticSessionControl` positive response with timing parameters. - #[must_use] - pub fn diagnostic_session_control( - session_type: DiagnosticSessionType, - p2_max: u16, - p2_star_max: u16, - ) -> Self { - Response::DiagnosticSessionControl(DiagnosticSessionControlResponse::new( - session_type, - p2_max, - p2_star_max, - )) - } - - /// Create an `EcuReset` positive response. - #[must_use] - pub fn ecu_reset(reset_type: ResetType, power_down_time: u8) -> Self { - Response::EcuReset(EcuResetResponse::new(reset_type, power_down_time)) - } - - /// Create a negative response for the given service and response code. - #[must_use] - pub fn negative_response(request_service: UdsServiceType, nrc: NegativeResponseCode) -> Self { - Response::NegativeResponse(NegativeResponse::new(request_service, nrc)) - } - - /// Create a `ReadDataByIdentifier` positive response from an iterator of payloads. - #[must_use] - pub fn read_data_by_identifier(payload: I) -> Self - where - I: IntoIterator, - { - Response::ReadDataByIdentifier(ReadDataByIdentifierResponse::new(payload)) - } - - /// Create a `RequestDownload` positive response. - #[must_use] - pub fn request_download( - length_format_identifier: u8, - max_number_of_block_length: Vec, - ) -> Self { - Response::RequestDownload(RequestDownloadResponse::new( - length_format_identifier, - max_number_of_block_length, - )) - } - - /// Create a `RequestFileTransfer` positive response. Not yet implemented. - #[must_use] - pub fn request_file_transfer() -> Self { - todo!() - } - - /// Create a `RoutineControl` positive response. - pub fn routine_control( - routine_control_type: crate::RoutineControlSubFunction, - data: D::RoutinePayload, - ) -> Self { - Response::RoutineControl(RoutineControlResponse::new(routine_control_type, data)) - } - - /// Create a `SecurityAccess` positive response carrying the security seed. - #[must_use] - pub fn security_access(access_type: SecurityAccessType, security_seed: Vec) -> Self { - Response::SecurityAccess(SecurityAccessResponse::new(access_type, security_seed)) - } - - /// Create a `TesterPresent` positive response. - #[must_use] - pub fn tester_present() -> Self { - Response::TesterPresent(TesterPresentResponse::new()) - } - - /// Create a `TransferData` positive response. - #[must_use] - pub fn transfer_data(block_sequence_counter: u8, data: Vec) -> Self { - Response::TransferData(TransferDataResponse::new(block_sequence_counter, data)) - } - - /// Returns the [`UdsServiceType`] corresponding to this response variant. - pub fn service(&self) -> UdsServiceType { - match self { - Self::ClearDiagnosticInfo => UdsServiceType::ClearDiagnosticInfo, - Self::CommunicationControl(_) => UdsServiceType::CommunicationControl, - Self::ControlDTCSettings(_) => UdsServiceType::ControlDTCSettings, - Self::DiagnosticSessionControl(_) => UdsServiceType::DiagnosticSessionControl, - Self::EcuReset(_) => UdsServiceType::EcuReset, - Self::NegativeResponse(_) => UdsServiceType::NegativeResponse, - Self::ReadDataByIdentifier(_) => UdsServiceType::ReadDataByIdentifier, - Self::ReadDTCInfo(_) => UdsServiceType::ReadDTCInfo, - Self::RequestDownload(_) => UdsServiceType::RequestDownload, - Self::RequestFileTransfer(_) => UdsServiceType::RequestFileTransfer, - Self::RequestTransferExit => UdsServiceType::RequestTransferExit, - Self::RoutineControl(_) => UdsServiceType::RoutineControl, - Self::SecurityAccess(_) => UdsServiceType::SecurityAccess, - Self::TesterPresent(_) => UdsServiceType::TesterPresent, - Self::TransferData(_) => UdsServiceType::TransferData, - Self::WriteDataByIdentifier(_) => UdsServiceType::WriteDataByIdentifier, - } - } + /// Positive response to `TransferData`. + TransferData(TransferDataResponse<'a>), + /// Positive response to `WriteDataByIdentifier`. Contains the echoed DID. + WriteDataByIdentifier(WriteDataByIdentifierResponse), + /// A known-but-unmodeled (or unrecognized) service response. Carries the service + /// type and the raw payload bytes following the service identifier. + /// + /// Re-encoding is lossless for any service byte in the ISO 14229-1 table; a byte + /// that maps to [`UdsServiceType::UnsupportedDiagnosticService`] re-encodes as `0x7F`. + Other { + /// The service this response addresses. + service: UdsServiceType, + /// Raw payload bytes after the service byte. + data: &'a [u8], + }, } -impl WireFormat for Response { - #[allow(clippy::match_same_arms)] - fn required_size(&self) -> usize { - 1 + match self { - Self::ClearDiagnosticInfo => 0, - Self::CommunicationControl(cc) => cc.required_size(), - Self::ControlDTCSettings(dtc) => dtc.required_size(), - Self::DiagnosticSessionControl(ds) => ds.required_size(), - Self::EcuReset(reset) => reset.required_size(), - Self::NegativeResponse(nr) => nr.required_size(), - Self::ReadDataByIdentifier(rd) => rd.required_size(), - Self::ReadDTCInfo(rd) => rd.required_size(), - Self::RequestDownload(rd) => rd.required_size(), - Self::RequestFileTransfer(rft) => rft.required_size(), - Self::RequestTransferExit => 0, - Self::RoutineControl(rc) => rc.required_size(), - Self::SecurityAccess(sa) => sa.required_size(), - Self::TesterPresent(tp) => tp.required_size(), - Self::TransferData(td) => td.required_size(), - Self::WriteDataByIdentifier(wdbi) => wdbi.required_size(), +impl<'a> Decode<'a> for Response<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); } - } - - #[allow(clippy::match_same_arms)] - fn encode(&self, writer: &mut T) -> Result { - // Write the service byte - writer.write_u8(self.service().response_to_byte())?; - // Write the payload - Ok(1 + match self { - Self::ClearDiagnosticInfo => Ok(0), - Self::CommunicationControl(cc) => cc.encode(writer), - Self::ControlDTCSettings(dtc) => dtc.encode(writer), - Self::DiagnosticSessionControl(ds) => ds.encode(writer), - Self::EcuReset(reset) => reset.encode(writer), - Self::NegativeResponse(nr) => nr.encode(writer), - Self::ReadDataByIdentifier(rd) => rd.encode(writer), - Self::ReadDTCInfo(rd) => rd.encode(writer), - Self::RequestDownload(rd) => rd.encode(writer), - Self::RequestFileTransfer(rft) => rft.encode(writer), - Self::RequestTransferExit => Ok(0), - Self::RoutineControl(rc) => rc.encode(writer), - Self::SecurityAccess(sa) => sa.encode(writer), - Self::TesterPresent(tp) => tp.encode(writer), - Self::TransferData(td) => td.encode(writer), - Self::WriteDataByIdentifier(wdbi) => wdbi.encode(writer), - }?) - } -} - -impl SingleValueWireFormat for Response { - #[allow(clippy::too_many_lines)] - fn decode(reader: &mut T) -> Result { - let service = UdsServiceType::response_from_byte(reader.read_u8()?); - Ok(match service { - UdsServiceType::CommunicationControl => { - Self::CommunicationControl(CommunicationControlResponse::decode(reader)?) - } - UdsServiceType::ControlDTCSettings => { - Self::ControlDTCSettings(ControlDTCSettingsResponse::decode(reader)?) + let service = UdsServiceType::response_from_byte(buf[0]); + let payload = &buf[1..]; + + let response = match service { + UdsServiceType::ClearDiagnosticInfo => Self::ClearDiagnosticInfo, + UdsServiceType::CommunicationControl => Self::CommunicationControl( + ::decode_exact(payload)?, + ), + UdsServiceType::ControlDTCSettings => Self::ControlDTCSettings( + ::decode_exact(payload)?, + ), + UdsServiceType::DiagnosticSessionControl => Self::DiagnosticSessionControl( + ::decode_exact(payload)?, + ), + UdsServiceType::EcuReset => { + Self::EcuReset(::decode_exact(payload)?) } - UdsServiceType::DiagnosticSessionControl => { - Self::DiagnosticSessionControl(DiagnosticSessionControlResponse::decode(reader)?) + UdsServiceType::NegativeResponse => { + Self::NegativeResponse(::decode_exact(payload)?) } - UdsServiceType::EcuReset => Self::EcuReset(EcuResetResponse::decode(reader)?), - UdsServiceType::ReadDataByIdentifier => { - Self::ReadDataByIdentifier(ReadDataByIdentifierResponse::decode(reader)?) + UdsServiceType::ReadDataByIdentifier => Self::ReadDataByIdentifier(payload), + UdsServiceType::ReadDTCInfo => { + Self::ReadDTCInfo(::decode_exact(payload)?) } - UdsServiceType::ReadDTCInfo => Self::ReadDTCInfo(ReadDTCInfoResponse::decode(reader)?), UdsServiceType::RequestDownload => { - Self::RequestDownload(RequestDownloadResponse::decode(reader)?) - } - UdsServiceType::RequestFileTransfer => { - Self::RequestFileTransfer(RequestFileTransferResponse::decode(reader)?) + Self::RequestDownload(::decode_exact(payload)?) } + UdsServiceType::RequestFileTransfer => Self::RequestFileTransfer( + ::decode_exact(payload)?, + ), UdsServiceType::RequestTransferExit => Self::RequestTransferExit, UdsServiceType::RoutineControl => { - Self::RoutineControl(RoutineControlResponse::decode(reader)?) + Self::RoutineControl(::decode_exact(payload)?) } UdsServiceType::SecurityAccess => { - Self::SecurityAccess(SecurityAccessResponse::decode(reader)?) + Self::SecurityAccess(::decode_exact(payload)?) } UdsServiceType::TesterPresent => { - Self::TesterPresent(TesterPresentResponse::decode(reader)?) - } - UdsServiceType::NegativeResponse => { - Self::NegativeResponse(NegativeResponse::decode(reader)?) - } - UdsServiceType::WriteDataByIdentifier => { - Self::WriteDataByIdentifier(WriteDataByIdentifierResponse::decode(reader)?) - } - UdsServiceType::Authentication => { - return Err(Error::ServiceNotImplemented(UdsServiceType::Authentication)); - } - UdsServiceType::AccessTimingParameters => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::AccessTimingParameters, - )); - } - UdsServiceType::SecuredDataTransmission => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::SecuredDataTransmission, - )); - } - UdsServiceType::ResponseOnEvent => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ResponseOnEvent, - )); - } - UdsServiceType::LinkControl => { - return Err(Error::ServiceNotImplemented(UdsServiceType::LinkControl)); - } - UdsServiceType::ReadMemoryByAddress => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ReadMemoryByAddress, - )); - } - UdsServiceType::ReadScalingDataByIdentifier => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ReadScalingDataByIdentifier, - )); - } - UdsServiceType::ReadDataByIdentifierPeriodic => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ReadDataByIdentifierPeriodic, - )); - } - UdsServiceType::DynamicallyDefinedDataIdentifier => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::DynamicallyDefinedDataIdentifier, - )); - } - UdsServiceType::WriteMemoryByAddress => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::WriteMemoryByAddress, - )); - } - UdsServiceType::ClearDiagnosticInfo => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::ClearDiagnosticInfo, - )); - } - UdsServiceType::InputOutputControlByIdentifier => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::InputOutputControlByIdentifier, - )); - } - UdsServiceType::RequestUpload => { - return Err(Error::ServiceNotImplemented(UdsServiceType::RequestUpload)); + Self::TesterPresent(::decode_exact(payload)?) } UdsServiceType::TransferData => { - Self::TransferData(TransferDataResponse::decode(reader)?) - } - UdsServiceType::UnsupportedDiagnosticService => { - return Err(Error::ServiceNotImplemented( - UdsServiceType::UnsupportedDiagnosticService, - )); - } - }) + Self::TransferData(::decode_exact(payload)?) + } + UdsServiceType::WriteDataByIdentifier => Self::WriteDataByIdentifier( + ::decode_exact(payload)?, + ), + _ => Self::Other { + service, + data: payload, + }, + }; + Ok((response, &[])) + } +} + +impl Response<'_> { + /// Returns the response service-ID byte that frames this response on the wire. + fn response_sid(&self) -> u8 { + match self { + Self::ClearDiagnosticInfo => UdsServiceType::ClearDiagnosticInfo.response_to_byte(), + Self::CommunicationControl(_) => { + UdsServiceType::CommunicationControl.response_to_byte() + } + Self::ControlDTCSettings(_) => UdsServiceType::ControlDTCSettings.response_to_byte(), + Self::DiagnosticSessionControl(_) => { + UdsServiceType::DiagnosticSessionControl.response_to_byte() + } + Self::EcuReset(_) => UdsServiceType::EcuReset.response_to_byte(), + Self::NegativeResponse(_) => UdsServiceType::NegativeResponse.response_to_byte(), + Self::ReadDataByIdentifier(_) => { + UdsServiceType::ReadDataByIdentifier.response_to_byte() + } + Self::ReadDTCInfo(_) => UdsServiceType::ReadDTCInfo.response_to_byte(), + Self::RequestDownload(_) => UdsServiceType::RequestDownload.response_to_byte(), + Self::RequestFileTransfer(_) => UdsServiceType::RequestFileTransfer.response_to_byte(), + Self::RequestTransferExit => UdsServiceType::RequestTransferExit.response_to_byte(), + Self::RoutineControl(_) => UdsServiceType::RoutineControl.response_to_byte(), + Self::SecurityAccess(_) => UdsServiceType::SecurityAccess.response_to_byte(), + Self::TesterPresent(_) => UdsServiceType::TesterPresent.response_to_byte(), + Self::TransferData(_) => UdsServiceType::TransferData.response_to_byte(), + Self::WriteDataByIdentifier(_) => { + UdsServiceType::WriteDataByIdentifier.response_to_byte() + } + Self::Other { service, .. } => service.response_to_byte(), + } + } +} + +impl Encode for Response<'_> { + fn encoded_size(&self) -> usize { + let payload = match self { + Self::ClearDiagnosticInfo | Self::RequestTransferExit => 0, + Self::Other { data, .. } => data.len(), + Self::CommunicationControl(resp) => resp.encoded_size(), + Self::ControlDTCSettings(resp) => resp.encoded_size(), + Self::DiagnosticSessionControl(resp) => resp.encoded_size(), + Self::EcuReset(resp) => resp.encoded_size(), + Self::NegativeResponse(resp) => resp.encoded_size(), + Self::ReadDataByIdentifier(bytes) => bytes.len(), + Self::WriteDataByIdentifier(resp) => resp.encoded_size(), + Self::ReadDTCInfo(resp) => resp.encoded_size(), + Self::RequestDownload(resp) => resp.encoded_size(), + Self::RequestFileTransfer(resp) => resp.encoded_size(), + Self::RoutineControl(resp) => resp.encoded_size(), + Self::SecurityAccess(resp) => resp.encoded_size(), + Self::TesterPresent(resp) => resp.encoded_size(), + Self::TransferData(resp) => resp.encoded_size(), + }; + 1 + payload + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.response_sid()]) + .map_err(Error::io)?; + let payload = match self { + Self::ClearDiagnosticInfo | Self::RequestTransferExit => 0, + Self::CommunicationControl(resp) => resp.encode(writer)?, + Self::ControlDTCSettings(resp) => resp.encode(writer)?, + Self::DiagnosticSessionControl(resp) => resp.encode(writer)?, + Self::EcuReset(resp) => resp.encode(writer)?, + Self::NegativeResponse(resp) => resp.encode(writer)?, + Self::ReadDataByIdentifier(bytes) => { + writer.write_all(bytes).map_err(Error::io)?; + bytes.len() + } + Self::WriteDataByIdentifier(resp) => resp.encode(writer)?, + Self::ReadDTCInfo(resp) => resp.encode(writer)?, + Self::RequestDownload(resp) => resp.encode(writer)?, + Self::RequestFileTransfer(resp) => resp.encode(writer)?, + Self::RoutineControl(resp) => resp.encode(writer)?, + Self::SecurityAccess(resp) => resp.encode(writer)?, + Self::TesterPresent(resp) => resp.encode(writer)?, + Self::TransferData(resp) => resp.encode(writer)?, + Self::Other { data, .. } => { + writer.write_all(data).map_err(Error::io)?; + data.len() + } + }; + Ok(1 + payload) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn write_data_by_identifier_response_roundtrips() { + // SID 0x6E, echoed DID 0xF190 + let wire = [0x6E, 0xF1, 0x90]; + let (resp, rest) = Response::decode(&wire).unwrap(); + assert!(rest.is_empty()); + assert!(matches!(resp, Response::WriteDataByIdentifier(_))); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } + + #[test] + fn routine_control_response_roundtrips() { + // SID 0x71, sub 0x01, RID 0xFF00, status 0x10 + let wire = [0x71, 0x01, 0xFF, 0x00, 0x10]; + let (resp, rest) = Response::decode(&wire).unwrap(); + assert!(rest.is_empty()); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &wire); + } + + #[test] + fn unmodeled_response_decodes_to_other() { + // 0x63 = ReadMemoryByAddress positive response, not modeled. + let frame = [0x63, 0x01, 0x02]; + let (resp, rest) = Response::decode(&frame).unwrap(); + assert!(rest.is_empty()); + match resp { + Response::Other { service, data } => { + assert_eq!(service, UdsServiceType::ReadMemoryByAddress); + assert_eq!(data, &[0x01, 0x02]); + } + other => panic!("expected Other, got {other:?}"), + } + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &frame); } } diff --git a/src/service.rs b/src/service.rs index 6db299f..475d527 100644 --- a/src/service.rs +++ b/src/service.rs @@ -283,8 +283,8 @@ impl UdsServiceType { } } -impl std::fmt::Display for UdsServiceType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for UdsServiceType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{self:?}") } } diff --git a/src/services/clear_dtc_information.rs b/src/services/clear_dtc_information.rs index 2100886..62fb46f 100644 --- a/src/services/clear_dtc_information.rs +++ b/src/services/clear_dtc_information.rs @@ -1,6 +1,5 @@ //! `ClearDiagnosticInformation` (0x14) service implementation -use crate::{CLEAR_ALL_DTCS, DTCRecord, NegativeResponseCode, SingleValueWireFormat, WireFormat}; -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::{CLEAR_ALL_DTCS, DTCRecord, Decode, Encode, NegativeResponseCode}; /// Negative response codes const CLEAR_DIAG_INFO_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ @@ -48,29 +47,34 @@ impl ClearDiagnosticInfoRequest { } } -impl WireFormat for ClearDiagnosticInfoRequest { - fn required_size(&self) -> usize { - self.group_of_dtc.required_size() + 1 +impl Encode for ClearDiagnosticInfoRequest { + fn encoded_size(&self) -> usize { + 4 // DTCRecord (3) + memory_selection (1) } - fn encode(&self, writer: &mut T) -> Result { - let mut size = 0; - size += self.group_of_dtc.encode(writer)?; - writer.write_u8(self.memory_selection)?; - size += 1; - Ok(size) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + let size = Encode::encode(&self.group_of_dtc, writer)?; + writer + .write_all(&[self.memory_selection]) + .map_err(crate::Error::io)?; + Ok(size + 1) } } -impl SingleValueWireFormat for ClearDiagnosticInfoRequest { - fn decode(reader: &mut T) -> Result { - let group_of_dtc = DTCRecord::decode(reader)?; - let memory_selection = reader.read_u8()?; - - Ok(Self { - group_of_dtc, - memory_selection, - }) +impl<'a> Decode<'a> for ClearDiagnosticInfoRequest { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), crate::Error> { + let (group_of_dtc, buf) = ::decode(buf)?; + if buf.is_empty() { + return Err(crate::Error::InsufficientData(4)); + } + let memory_selection = buf[0]; + Ok(( + Self { + group_of_dtc, + memory_selection, + }, + &buf[1..], + )) } } @@ -78,18 +82,23 @@ impl SingleValueWireFormat for ClearDiagnosticInfoRequest { #[cfg(test)] mod request { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec; + #[cfg(feature = "alloc")] #[test] fn decode_clear_dtc_info_request() { let bytes = [0xFF, 0xFF, 0xFF, 0x00]; let compare = ClearDiagnosticInfoRequest::new(CLEAR_ALL_DTCS, 0); - let req = ClearDiagnosticInfoRequest::decode(&mut &bytes[..]).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); assert_eq!(req, compare); - let mut bytes = vec![]; - let written = req.encode(&mut bytes).unwrap(); - assert_eq!(bytes, [0xFF, 0xFF, 0xFF, 0x00]); - assert_eq!(req.required_size(), written); + let mut buf = vec![]; + let written = Encode::encode(&req, &mut buf).unwrap(); + assert_eq!(buf, [0xFF, 0xFF, 0xFF, 0x00]); + assert_eq!(req.encoded_size(), written); + assert_encode_size_agrees(&req); } #[test] diff --git a/src/services/communication_control.rs b/src/services/communication_control.rs index f8713c1..a4c33ac 100644 --- a/src/services/communication_control.rs +++ b/src/services/communication_control.rs @@ -1,9 +1,8 @@ //! `CommunicationControl` (0x28) service implementation +use crate::common::SuppressablePositiveResponse; use crate::{ - CommunicationControlType, CommunicationType, Error, NegativeResponseCode, - SingleValueWireFormat, SuppressablePositiveResponse, WireFormat, + CommunicationControlType, CommunicationType, Decode, Encode, Error, NegativeResponseCode, }; -use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; const COMMUNICATION_CONTROL_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ NegativeResponseCode::SubFunctionNotSupported, @@ -31,7 +30,12 @@ pub struct CommunicationControlRequest { } impl CommunicationControlRequest { - pub(crate) fn new( + /// Create a `CommunicationControlRequest` with standard address information. + /// + /// # Panics + /// Panics (debug) if an extended-address control type is passed. + #[must_use] + pub fn new( suppress_positive_response: bool, control_type: CommunicationControlType, communication_type: CommunicationType, @@ -47,7 +51,12 @@ impl CommunicationControlRequest { } } - pub(crate) fn new_with_node_id( + /// Create a `CommunicationControlRequest` with enhanced address information. + /// + /// # Panics + /// Panics if a non-extended control type is passed. + #[must_use] + pub fn new_with_node_id( suppress_positive_response: bool, control_type: CommunicationControlType, communication_type: CommunicationType, @@ -82,16 +91,20 @@ impl CommunicationControlRequest { &COMMUNICATION_CONTROL_NEGATIVE_RESPONSE_CODES } } -impl WireFormat for CommunicationControlRequest { - fn required_size(&self) -> usize { +impl Encode for CommunicationControlRequest { + fn encoded_size(&self) -> usize { if self.node_id.is_some() { 4 } else { 2 } } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.control_type))?; - writer.write_u8(u8::from(self.communication_type))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[ + u8::from(self.control_type), + u8::from(self.communication_type), + ]) + .map_err(Error::io)?; if let Some(id) = self.node_id { - writer.write_u16::(id)?; + writer.write_all(&id.to_be_bytes()).map_err(Error::io)?; Ok(4) } else { Ok(2) @@ -99,26 +112,37 @@ impl WireFormat for CommunicationControlRequest { } } -impl SingleValueWireFormat for CommunicationControlRequest { - fn decode(reader: &mut T) -> Result { - let enable_byte = reader.read_u8()?; - let communication_enable = SuppressablePositiveResponse::try_from(enable_byte)?; - let communication_type = CommunicationType::try_from(reader.read_u8()?)?; +impl<'a> Decode<'a> for CommunicationControlRequest { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let communication_enable = SuppressablePositiveResponse::try_from(buf[0])?; + let communication_type = CommunicationType::try_from(buf[1])?; match communication_enable.value() { CommunicationControlType::EnableRxAndDisableTxWithEnhancedAddressInfo | CommunicationControlType::EnableRxAndTxWithEnhancedAddressInfo => { - let node_id = Some(reader.read_u16::()?); - Ok(Self { + if buf.len() < 4 { + return Err(Error::InsufficientData(4)); + } + let node_id = Some(u16::from_be_bytes([buf[2], buf[3]])); + Ok(( + Self { + control_type: communication_enable, + communication_type, + node_id, + }, + &buf[4..], + )) + } + _ => Ok(( + Self { control_type: communication_enable, communication_type, - node_id, - }) - } - _ => Ok(Self { - control_type: communication_enable, - communication_type, - node_id: None, - }), + node_id: None, + }, + &buf[2..], + )), } } } @@ -134,37 +158,48 @@ pub struct CommunicationControlResponse { } impl CommunicationControlResponse { - pub(crate) fn new(control_type: CommunicationControlType) -> Self { + /// Create a new `CommunicationControlResponse`. + #[must_use] + pub fn new(control_type: CommunicationControlType) -> Self { Self { control_type } } } -impl WireFormat for CommunicationControlResponse { - fn required_size(&self) -> usize { +impl Encode for CommunicationControlResponse { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.control_type))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.control_type)]) + .map_err(Error::io)?; Ok(1) } } -impl SingleValueWireFormat for CommunicationControlResponse { - fn decode(reader: &mut T) -> Result { - let control_type = CommunicationControlType::try_from(reader.read_u8()?)?; - Ok(Self::new(control_type)) +impl<'a> Decode<'a> for CommunicationControlResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let control_type = CommunicationControlType::try_from(buf[0])?; + Ok((Self::new(control_type), &buf[1..])) } } #[cfg(test)] mod request { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn simple_request() { let bytes: [u8; 3] = [0x01, 0x02, 0x03]; - let req = CommunicationControlRequest::decode(&mut bytes.as_slice()).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); assert_eq!( req.control_type(), CommunicationControlType::EnableRxAndDisableTx @@ -173,15 +208,17 @@ mod request { assert_eq!(req.node_id, None); let mut buffer = Vec::new(); - let written = req.encode(&mut buffer).unwrap(); - assert_eq!(written, req.required_size()); - assert_eq!(buffer.len(), req.required_size()); + let written = Encode::encode(&req, &mut buffer).unwrap(); + assert_eq!(written, req.encoded_size()); + assert_eq!(buffer.len(), req.encoded_size()); + assert_encode_size_agrees(&req); } + #[cfg(feature = "alloc")] #[test] fn node_id() { let bytes: [u8; 4] = [0x05, 0x02, 0x01, 0x02]; - let req = CommunicationControlRequest::decode(&mut bytes.as_slice()).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); assert_eq!( req.control_type(), CommunicationControlType::EnableRxAndTxWithEnhancedAddressInfo @@ -190,9 +227,10 @@ mod request { assert_eq!(req.node_id, Some(258)); let mut buffer = Vec::new(); - let written = req.encode(&mut buffer).unwrap(); - assert_eq!(written, req.required_size()); - assert_eq!(buffer.len(), req.required_size()); + let written = Encode::encode(&req, &mut buffer).unwrap(); + assert_eq!(written, req.encoded_size()); + assert_eq!(buffer.len(), req.encoded_size()); + assert_encode_size_agrees(&req); } #[test] @@ -222,19 +260,24 @@ mod request { #[cfg(test)] mod response { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn simple_response() { let bytes: [u8; 1] = [0x01]; - let res = CommunicationControlResponse::decode(&mut bytes.as_slice()).unwrap(); + let (res, _) = ::decode(&bytes).unwrap(); assert_eq!( res.control_type, CommunicationControlType::EnableRxAndDisableTx ); let mut buffer = Vec::new(); - let written = res.encode(&mut buffer).unwrap(); + let written = Encode::encode(&res, &mut buffer).unwrap(); assert_eq!(written, 1); assert_eq!(buffer.len(), written); + assert_encode_size_agrees(&res); } } diff --git a/src/services/control_dtc_settings.rs b/src/services/control_dtc_settings.rs index 81a33c6..bcf17dc 100644 --- a/src/services/control_dtc_settings.rs +++ b/src/services/control_dtc_settings.rs @@ -1,6 +1,5 @@ //! `ControlDTCSetting` (0x85) service implementation -use crate::{DtcSettings, Error, SUCCESS, SingleValueWireFormat, WireFormat}; -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::{Decode, DtcSettings, Encode, Error, SUCCESS}; /// The `ControlDTCSettings` service is used to control the DTC settings of the ECU. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -15,40 +14,50 @@ pub struct ControlDTCSettingsRequest { } impl ControlDTCSettingsRequest { - pub(crate) fn new(setting: DtcSettings, suppress_response: bool) -> Self { + /// Create a new `ControlDTCSettingsRequest`. + #[must_use] + pub fn new(setting: DtcSettings, suppress_response: bool) -> Self { Self { setting, suppress_response, } } + + /// Whether the server should suppress the positive response (SPRMIB). + #[must_use] + pub const fn suppress_positive_response(&self) -> bool { + self.suppress_response + } } -impl WireFormat for ControlDTCSettingsRequest { - fn required_size(&self) -> usize { +impl Encode for ControlDTCSettingsRequest { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { let request_byte = u8::from(self.setting) | if self.suppress_response { SUCCESS } else { 0 }; - writer.write_u8(request_byte)?; + writer.write_all(&[request_byte]).map_err(Error::io)?; Ok(1) } - - fn is_positive_response_suppressed(&self) -> bool { - self.suppress_response - } } -impl SingleValueWireFormat for ControlDTCSettingsRequest { - fn decode(reader: &mut T) -> Result { - let request_byte = reader.read_u8()?; - let setting = DtcSettings::from(request_byte & !SUCCESS); +impl<'a> Decode<'a> for ControlDTCSettingsRequest { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let request_byte = buf[0]; + let setting = DtcSettings::try_from(request_byte & !SUCCESS)?; let suppress_response = request_byte & SUCCESS != 0; - Ok(Self { - setting, - suppress_response, - }) + Ok(( + Self { + setting, + suppress_response, + }, + &buf[1..], + )) } } @@ -65,64 +74,79 @@ pub struct ControlDTCSettingsResponse { } impl ControlDTCSettingsResponse { - pub(crate) fn new(setting: DtcSettings) -> Self { + /// Create a new `ControlDTCSettingsResponse`. + #[must_use] + pub fn new(setting: DtcSettings) -> Self { Self { setting } } } -impl WireFormat for ControlDTCSettingsResponse { - fn required_size(&self) -> usize { +impl Encode for ControlDTCSettingsResponse { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.setting))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.setting)]) + .map_err(Error::io)?; Ok(1) } } -impl SingleValueWireFormat for ControlDTCSettingsResponse { - fn decode(reader: &mut T) -> Result { - let setting = DtcSettings::from(reader.read_u8()?); - Ok(Self { setting }) +impl<'a> Decode<'a> for ControlDTCSettingsResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let setting = DtcSettings::try_from(buf[0])?; + Ok((Self { setting }, &buf[1..])) } } #[cfg(test)] mod request { use super::*; - use crate::DtcSettings; + use crate::{Decode, DtcSettings, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::{vec, vec::Vec}; + #[cfg(feature = "alloc")] #[test] fn simple_request() { let req = ControlDTCSettingsRequest::new(DtcSettings::On, true); let mut buffer = Vec::new(); - let written = req.encode(&mut buffer).unwrap(); + let written = Encode::encode(&req, &mut buffer).unwrap(); assert_eq!(buffer, vec![0x81]); assert_eq!(written, buffer.len()); - assert_eq!(req.required_size(), buffer.len()); + assert_eq!(req.encoded_size(), buffer.len()); - let parsed = ControlDTCSettingsRequest::decode(&mut buffer.as_slice()).unwrap(); + let (parsed, _) = ::decode(&buffer).unwrap(); assert_eq!(parsed.setting, DtcSettings::On); assert!(parsed.suppress_response); + assert_encode_size_agrees(&req); } } #[cfg(test)] mod response { use super::*; - use crate::DtcSettings; + use crate::{Decode, DtcSettings, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::{vec, vec::Vec}; + #[cfg(feature = "alloc")] #[test] fn simple_response() { let req = ControlDTCSettingsResponse::new(DtcSettings::On); let mut buffer = Vec::new(); - let written = req.encode(&mut buffer).unwrap(); + let written = Encode::encode(&req, &mut buffer).unwrap(); assert_eq!(buffer, vec![0x01]); assert_eq!(written, buffer.len()); - assert_eq!(req.required_size(), buffer.len()); + assert_eq!(req.encoded_size(), buffer.len()); - let parsed = ControlDTCSettingsResponse::decode(&mut buffer.as_slice()).unwrap(); + let (parsed, _) = ::decode(&buffer).unwrap(); assert_eq!(parsed.setting, DtcSettings::On); + assert_encode_size_agrees(&req); } } diff --git a/src/services/diagnostic_session_control.rs b/src/services/diagnostic_session_control.rs index 50c9ab5..77f5816 100644 --- a/src/services/diagnostic_session_control.rs +++ b/src/services/diagnostic_session_control.rs @@ -9,11 +9,8 @@ //! A server shall be capable of providing diagnostic functionality under normal operating conditions, //! as well as in other operation conditions defined by the vehicle manufacturer (e.g. limp home operation condition). -use crate::{ - DiagnosticSessionType, Error, NegativeResponseCode, SingleValueWireFormat, - SuppressablePositiveResponse, WireFormat, -}; -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::common::SuppressablePositiveResponse; +use crate::{Decode, DiagnosticSessionType, Encode, Error, NegativeResponseCode}; const DIAGNOSTIC_SESSION_CONTROL_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 3] = [ NegativeResponseCode::SubFunctionNotSupported, @@ -31,10 +28,8 @@ pub struct DiagnosticSessionControlRequest { impl DiagnosticSessionControlRequest { /// Create a new `DiagnosticSessionControlRequest` - pub(crate) fn new( - suppress_positive_response: bool, - session_type: DiagnosticSessionType, - ) -> Self { + #[must_use] + pub fn new(suppress_positive_response: bool, session_type: DiagnosticSessionType) -> Self { Self { session_type: SuppressablePositiveResponse::new( suppress_positive_response, @@ -61,25 +56,26 @@ impl DiagnosticSessionControlRequest { &DIAGNOSTIC_SESSION_CONTROL_NEGATIVE_RESPONSE_CODES } } -impl WireFormat for DiagnosticSessionControlRequest { - fn required_size(&self) -> usize { +impl Encode for DiagnosticSessionControlRequest { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.session_type))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.session_type)]) + .map_err(Error::io)?; Ok(1) } - - fn is_positive_response_suppressed(&self) -> bool { - self.suppress_positive_response() - } } -impl SingleValueWireFormat for DiagnosticSessionControlRequest { - fn decode(reader: &mut T) -> Result { - let session_type = SuppressablePositiveResponse::try_from(reader.read_u8()?)?; - Ok(Self { session_type }) +impl<'a> Decode<'a> for DiagnosticSessionControlRequest { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let session_type = SuppressablePositiveResponse::try_from(buf[0])?; + Ok((Self { session_type }, &buf[1..])) } } @@ -99,7 +95,8 @@ pub struct DiagnosticSessionControlResponse { impl DiagnosticSessionControlResponse { /// Create a new `DiagnosticSessionControlResponse` - pub(crate) fn new( + #[must_use] + pub fn new( session_type: DiagnosticSessionType, p2_server_max: u16, p2_star_server_max: u16, @@ -111,43 +108,56 @@ impl DiagnosticSessionControlResponse { } } } -impl WireFormat for DiagnosticSessionControlResponse { - fn required_size(&self) -> usize { +impl Encode for DiagnosticSessionControlResponse { + fn encoded_size(&self) -> usize { 5 } - fn encode(&self, buffer: &mut T) -> Result { - buffer.write_u8(u8::from(self.session_type))?; - buffer.write_u16::(self.p2_server_max)?; - buffer.write_u16::(self.p2_star_server_max)?; - + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.session_type)]) + .map_err(Error::io)?; + writer + .write_all(&self.p2_server_max.to_be_bytes()) + .map_err(Error::io)?; + writer + .write_all(&self.p2_star_server_max.to_be_bytes()) + .map_err(Error::io)?; Ok(5) } } -impl SingleValueWireFormat for DiagnosticSessionControlResponse { - fn decode(reader: &mut T) -> Result { - let session_type = DiagnosticSessionType::try_from(reader.read_u8()?)?; - let p2_server_max = reader.read_u16::()?; - let p2_star_server_max = reader.read_u16::()?; - Ok(Self { - session_type, - p2_server_max, - p2_star_server_max, - }) +impl<'a> Decode<'a> for DiagnosticSessionControlResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 5 { + return Err(Error::InsufficientData(5)); + } + let session_type = DiagnosticSessionType::try_from(buf[0])?; + let p2_server_max = u16::from_be_bytes([buf[1], buf[2]]); + let p2_star_server_max = u16::from_be_bytes([buf[3], buf[4]]); + Ok(( + Self { + session_type, + p2_server_max, + p2_star_server_max, + }, + &buf[5..], + )) } } #[cfg(test)] mod request { use super::*; - use crate::DiagnosticSessionType; + use crate::{Decode, DiagnosticSessionType, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn test_diagnostic_session_control_request() { let bytes: [u8; 1] = [0x02]; - let req: DiagnosticSessionControlRequest = - DiagnosticSessionControlRequest::decode(&mut bytes.as_slice()).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); assert!(!req.suppress_positive_response()); assert_eq!( req.session_type(), @@ -155,29 +165,33 @@ mod request { ); let mut buffer = Vec::new(); - req.encode(&mut buffer).unwrap(); + Encode::encode(&req, &mut buffer).unwrap(); assert_eq!(buffer, bytes); - assert_eq!(req.required_size(), 1); + assert_eq!(req.encoded_size(), 1); + assert_encode_size_agrees(&req); } } #[cfg(test)] mod response { use super::*; - use crate::DiagnosticSessionType; + use crate::{Decode, DiagnosticSessionType, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn test_diagnostic_session_control_response() { let bytes = [0x02, 0x11, 0x22, 0x33, 0x44]; - let resp: DiagnosticSessionControlResponse = - DiagnosticSessionControlResponse::decode(&mut bytes.as_slice()).unwrap(); + let (resp, _) = ::decode(&bytes).unwrap(); assert_eq!(resp.session_type, DiagnosticSessionType::ProgrammingSession); assert_eq!(resp.p2_server_max, 0x1122); assert_eq!(resp.p2_star_server_max, 0x3344); let mut buffer = Vec::new(); - resp.encode(&mut buffer).unwrap(); + Encode::encode(&resp, &mut buffer).unwrap(); assert_eq!(buffer, bytes); - assert_eq!(resp.required_size(), 5); + assert_eq!(resp.encoded_size(), 5); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/ecu_reset.rs b/src/services/ecu_reset.rs index 3e879b6..7cd2827 100644 --- a/src/services/ecu_reset.rs +++ b/src/services/ecu_reset.rs @@ -1,10 +1,6 @@ //! `ECUReset` (0x11) service implementation -use crate::{ - Error, NegativeResponseCode, ResetType, SingleValueWireFormat, SuppressablePositiveResponse, - WireFormat, -}; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{Read, Write}; +use crate::common::SuppressablePositiveResponse; +use crate::{Decode, Encode, Error, NegativeResponseCode, ResetType}; const ECU_RESET_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 4] = [ NegativeResponseCode::SubFunctionNotSupported, @@ -23,7 +19,8 @@ pub struct EcuResetRequest { impl EcuResetRequest { /// Create a new '`EcuResetRequest`' - pub(crate) fn new(suppress_positive_response: bool, reset_type: ResetType) -> Self { + #[must_use] + pub fn new(suppress_positive_response: bool, reset_type: ResetType) -> Self { Self { reset_type: SuppressablePositiveResponse::new(suppress_positive_response, reset_type), } @@ -48,25 +45,26 @@ impl EcuResetRequest { } } -impl WireFormat for EcuResetRequest { - fn required_size(&self) -> usize { +impl Encode for EcuResetRequest { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.reset_type))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.reset_type)]) + .map_err(Error::io)?; Ok(1) } - - fn is_positive_response_suppressed(&self) -> bool { - self.suppress_positive_response() - } } -impl SingleValueWireFormat for EcuResetRequest { - fn decode(reader: &mut T) -> Result { - let reset_type = SuppressablePositiveResponse::try_from(reader.read_u8()?)?; - Ok(Self { reset_type }) +impl<'a> Decode<'a> for EcuResetRequest { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let reset_type = SuppressablePositiveResponse::try_from(buf[0])?; + Ok((Self { reset_type }, &buf[1..])) } } @@ -84,7 +82,8 @@ pub struct EcuResetResponse { impl EcuResetResponse { /// Create a new '`EcuResetResponse`' - pub(crate) fn new(reset_type: ResetType, power_down_time: u8) -> Self { + #[must_use] + pub fn new(reset_type: ResetType, power_down_time: u8) -> Self { Self { reset_type, power_down_time, @@ -92,63 +91,80 @@ impl EcuResetResponse { } } -impl WireFormat for EcuResetResponse { - fn required_size(&self) -> usize { +impl Encode for EcuResetResponse { + fn encoded_size(&self) -> usize { 2 } - fn encode(&self, buffer: &mut T) -> Result { - buffer.write_u8(u8::from(self.reset_type))?; - buffer.write_u8(self.power_down_time)?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.reset_type), self.power_down_time]) + .map_err(Error::io)?; Ok(2) } } -impl SingleValueWireFormat for EcuResetResponse { - fn decode(reader: &mut T) -> Result { - let reset_type = ResetType::try_from(reader.read_u8()?)?; - // powerDownTime is conditional per ISO 14229-1 — only present when - // the server needs to report how long until power-down. - let power_down_time = reader.read_u8().unwrap_or(0); - Ok(Self { - reset_type, - power_down_time, - }) +impl<'a> Decode<'a> for EcuResetResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let reset_type = ResetType::try_from(buf[0])?; + // powerDownTime is conditional per ISO 14229-1 + let power_down_time = buf.get(1).copied().unwrap_or(0); + let consumed = core::cmp::min(buf.len(), 2); + Ok(( + Self { + reset_type, + power_down_time, + }, + &buf[consumed..], + )) } } #[cfg(test)] mod request { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn ecu_reset_request() { let bytes: [u8; 2] = [0x81, 0x00]; let req = EcuResetRequest::new(true, ResetType::HardReset); let mut buffer = Vec::new(); - let written = req.encode(&mut buffer).unwrap(); - let result = EcuResetRequest::decode(&mut bytes.as_slice()).unwrap(); + let written = Encode::encode(&req, &mut buffer).unwrap(); + let (result, _) = ::decode(&bytes).unwrap(); assert_eq!(result, req); assert_eq!(written, 1); - assert_eq!(written, req.required_size()); + assert_eq!(written, req.encoded_size()); + assert_encode_size_agrees(&req); } } #[cfg(test)] mod response { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn ecu_reset_response() { let bytes: [u8; 2] = [0x01, 0x20]; let resp = EcuResetResponse::new(ResetType::HardReset, 0x20); let mut buffer = Vec::new(); - let written = resp.encode(&mut buffer).unwrap(); - let result = EcuResetResponse::decode(&mut bytes.as_slice()).unwrap(); + let written = Encode::encode(&resp, &mut buffer).unwrap(); + let (result, _) = ::decode(&bytes).unwrap(); assert_eq!(result, resp); assert_eq!(written, 2); - assert_eq!(written, resp.required_size()); + assert_eq!(written, resp.encoded_size()); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/mod.rs b/src/services/mod.rs index 4fa5330..1ff3667 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -19,17 +19,21 @@ mod negative_response; pub use negative_response::NegativeResponse; mod read_data_by_identifier; -pub use read_data_by_identifier::{ReadDataByIdentifierRequest, ReadDataByIdentifierResponse}; +pub use read_data_by_identifier::ReadDataByIdentifierRequestTx; mod read_dtc_information; -pub use read_dtc_information::{ReadDTCInfoRequest, ReadDTCInfoResponse, ReadDTCInfoSubFunction}; +pub use read_dtc_information::{ + DtcAndStatusIter, DtcFaultDetectionIter, DtcSeverityAndStatusIter, ReadDTCInfoRequest, + ReadDTCInfoResponse, ReadDTCInfoSubFunction, +}; mod request_download; pub use request_download::{RequestDownloadRequest, RequestDownloadResponse}; mod request_file_transfer; pub use request_file_transfer::{ - FileOperationMode, RequestFileTransferRequest, RequestFileTransferResponse, + DirSizePayload, FileOperationMode, FileSizePayload, NamePayload, PositionPayload, + RequestFileTransferRequest, RequestFileTransferResponse, SentDataPayload, SizePayload, }; mod routine_control; diff --git a/src/services/negative_response.rs b/src/services/negative_response.rs index 06d4a90..6afbdbc 100644 --- a/src/services/negative_response.rs +++ b/src/services/negative_response.rs @@ -1,6 +1,5 @@ //! `NegativeResponse` (0x7F) service implementation -use crate::{Error, NegativeResponseCode, SingleValueWireFormat, UdsServiceType, WireFormat}; -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::{Decode, Encode, Error, NegativeResponseCode, UdsServiceType}; /// A negative response from the server indicating a request could not be fulfilled #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -16,7 +15,8 @@ pub struct NegativeResponse { impl NegativeResponse { /// Create a new `NegativeResponse` - pub(crate) fn new(request_service: UdsServiceType, nrc: NegativeResponseCode) -> Self { + #[must_use] + pub fn new(request_service: UdsServiceType, nrc: NegativeResponseCode) -> Self { Self { request_service, nrc, @@ -24,25 +24,50 @@ impl NegativeResponse { } } -impl WireFormat for NegativeResponse { - fn required_size(&self) -> usize { +impl Encode for NegativeResponse { + fn encoded_size(&self) -> usize { 2 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.request_service.request_service_to_byte())?; - writer.write_u8(u8::from(self.nrc))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[ + self.request_service.request_service_to_byte(), + u8::from(self.nrc), + ]) + .map_err(Error::io)?; Ok(2) } } -impl SingleValueWireFormat for NegativeResponse { - fn decode(reader: &mut T) -> Result { - let request_service = UdsServiceType::service_from_request_byte(reader.read_u8()?); - let nrc = NegativeResponseCode::from(reader.read_u8()?); - Ok(Self { - request_service, - nrc, - }) +impl<'a> Decode<'a> for NegativeResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let request_service = UdsServiceType::service_from_request_byte(buf[0]); + let nrc = NegativeResponseCode::from(buf[1]); + Ok(( + Self { + request_service, + nrc, + }, + &buf[2..], + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn negative_response_encode_size_agrees() { + let value = NegativeResponse::new( + UdsServiceType::DiagnosticSessionControl, + NegativeResponseCode::ServiceNotSupported, + ); + assert_encode_size_agrees(&value); } } diff --git a/src/services/read_data_by_identifier.rs b/src/services/read_data_by_identifier.rs index bd3fc99..a8de609 100644 --- a/src/services/read_data_by_identifier.rs +++ b/src/services/read_data_by_identifier.rs @@ -1,7 +1,5 @@ //! `ReadDataByIdentifier` (0x22) service implementation -use crate::{ - Error, Identifier, IterableWireFormat, NegativeResponseCode, SingleValueWireFormat, WireFormat, -}; +use crate::{Encode, Error, NegativeResponseCode}; const READ_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, @@ -11,23 +9,22 @@ const READ_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ NegativeResponseCode::SecurityAccessDenied, ]; +/// Zero-alloc TX request to read data by identifier. Borrows the DID list from the caller. +/// +/// A Data Identifier is a 16-bit value, so the list is held as `&[u16]`; each DID is +/// written big-endian on the wire. +/// /// See ISO-14229-1:2020, Table 11.2.1 for format information -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub struct ReadDataByIdentifierRequest { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ReadDataByIdentifierRequestTx<'d> { /// The list of Data Identifiers to read. - pub dids: Vec, + pub dids: &'d [u16], } -impl ReadDataByIdentifierRequest { - /// Create a new request from a sequence of data identifiers - pub(crate) fn new(dids: I) -> Self - where - I: IntoIterator, - { - let dids = dids.into_iter().collect(); +impl<'d> ReadDataByIdentifierRequestTx<'d> { + /// Create a new request from a slice of data identifiers. + #[must_use] + pub const fn new(dids: &'d [u16]) -> Self { Self { dids } } @@ -38,464 +35,32 @@ impl ReadDataByIdentifierRequest { } } -impl WireFormat for ReadDataByIdentifierRequest { - fn required_size(&self) -> usize { +impl Encode for ReadDataByIdentifierRequestTx<'_> { + fn encoded_size(&self) -> usize { self.dids.len() * 2 } - fn encode(&self, writer: &mut W) -> Result { - let mut count = 0; - for did in &self.dids { - did.encode(writer)?; - count += 2; - } - Ok(count) - } -} - -impl SingleValueWireFormat - for ReadDataByIdentifierRequest -{ - fn decode(reader: &mut R) -> Result { - let dids = DataIdentifier::parse_from_list(reader)?; - if dids.is_empty() { - Err(Error::NoDataAvailable) - } else { - Ok(ReadDataByIdentifierRequest::new(dids)) - } - } -} - -/// See ISO-14229-1:2020, Table 11.2.3 for format information -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Eq, PartialEq)] -#[non_exhaustive] -pub struct ReadDataByIdentifierResponse { - /// The decoded payload entries returned by the server. - pub data: Vec, -} - -impl ReadDataByIdentifierResponse { - pub(crate) fn new(data: I) -> Self - where - I: IntoIterator, - { - let data = data.into_iter().collect(); - Self { data } - } -} - -impl WireFormat for ReadDataByIdentifierResponse { - fn required_size(&self) -> usize { - self.data.iter().map(WireFormat::required_size).sum() - } - - fn encode(&self, writer: &mut W) -> Result { - let mut total_written = 0; - for payload in &self.data { - total_written += payload.encode(writer)?; - } - Ok(total_written) - } -} - -impl SingleValueWireFormat - for ReadDataByIdentifierResponse -{ - fn decode(reader: &mut R) -> Result { - let mut data = Vec::new(); - for payload in UserPayload::decode_iter(reader) { - match payload { - Ok(p) => { - data.push(p); - } - Err(e) => { - return Err(e); - } - } - } - if data.is_empty() { - Err(Error::NoDataAvailable) - } else { - Ok(ReadDataByIdentifierResponse::new(data)) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + for did in self.dids { + writer.write_all(&did.to_be_bytes()).map_err(Error::io)?; } - } -} - -impl std::fmt::Debug for ReadDataByIdentifierResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "ReadDataByIdentifierResponse\n{:?}", self.data) + Ok(self.encoded_size()) } } #[cfg(test)] mod test { use super::*; - use crate::{ProtocolIdentifier, UDSIdentifier}; - - mod request { - use super::*; - - struct ReadRequestTestData { - pub test_name: String, - pub dids_bytes: Vec, - } - - impl ReadRequestTestData { - // Creates a Test Read Request from a list of data identifiers - fn from_ids(test_name: &str, dids: &[ProtocolIdentifier]) -> Self { - let dids_bytes = to_bytes(dids); - Self { - test_name: test_name.to_string(), - dids_bytes, - } - } - - // Create a Test Read Request from a list of bytes. - // Note: These bytes may not properly translate to a list of data identifiers - fn from_bytes(test_name: &str, dids_bytes: Vec) -> Self { - Self { - test_name: test_name.to_string(), - dids_bytes, - } - } - } - - // Holds a list of dids that will be transformed into a byte sequence - struct WriteRequestTestData { - pub test_name: String, - pub dids: Vec, - } - - impl WriteRequestTestData { - fn from_ids(test_name: &str, dids: &[ProtocolIdentifier]) -> Self { - Self { - test_name: test_name.to_string(), - dids: dids.to_vec(), - } - } - } - - fn to_bytes(ids: &[ProtocolIdentifier]) -> Vec { - ids.iter() - .flat_map(|id: &ProtocolIdentifier| { - let mut buffer = Vec::new(); - id.encode(&mut buffer).unwrap(); - buffer - }) - .collect() - } - - fn get_test_ids() -> Vec { - vec![ - ProtocolIdentifier::new(UDSIdentifier::BootSoftwareIdentification), - ProtocolIdentifier::new(UDSIdentifier::ApplicationSoftwareIdentification), - ProtocolIdentifier::new(UDSIdentifier::ApplicationDataIdentification), - ProtocolIdentifier::new(UDSIdentifier::BootSoftwareFingerprint), - ProtocolIdentifier::new(UDSIdentifier::ApplicationSoftwareFingerprint), - ProtocolIdentifier::new(UDSIdentifier::ApplicationDataFingerprint), - ProtocolIdentifier::new(UDSIdentifier::ActiveDiagnosticSession), - ProtocolIdentifier::new(UDSIdentifier::VehicleManufacturerSparePartNumber), - ProtocolIdentifier::new(UDSIdentifier::VehicleManufacturerECUSoftwareNumber), - ProtocolIdentifier::new(UDSIdentifier::VehicleManufacturerECUSoftwareVersionNumber), - ] - } - - #[test] - fn read_did_request_bytes() { - let test_ids = get_test_ids(); - - let test_data_sets: Vec = vec![ - ReadRequestTestData::from_bytes("No ids", vec![]), - ReadRequestTestData::from_bytes("Invalid byte length", vec![0x00]), - ReadRequestTestData::from_bytes("Invalid id", vec![0x00, 0x01]), - ReadRequestTestData::from_ids("1 id", &test_ids[0..1]), - ReadRequestTestData::from_ids("2 ids", &test_ids[0..2]), - ReadRequestTestData::from_ids("3 ids", &test_ids[0..3]), - ReadRequestTestData::from_ids("4 ids", &test_ids[0..4]), - ReadRequestTestData::from_ids("All ids", &test_ids), - ReadRequestTestData::from_ids( - "Repeated ids", - &test_ids - .clone() - .iter() - .cycle() - .take(100) - .copied() - .collect::>(), - ), - ]; - - for test_data in &test_data_sets { - let read_result = ReadDataByIdentifierRequest::::decode( - &mut test_data.dids_bytes.as_slice(), - ); - - match read_result { - Ok(response) => { - let mut translated_bytes = Vec::new(); - response.encode(&mut translated_bytes).unwrap(); - assert_eq!( - translated_bytes, *test_data.dids_bytes, - "Ok: Failed: {}", - test_data.test_name - ); - } - Err(e) => { - if test_data.dids_bytes.is_empty() { - assert!( - matches!(e, Error::NoDataAvailable), - "NoDataAvailable: Failed {}", - test_data.test_name - ); - } else { - assert!( - matches!(e, Error::IncorrectMessageLengthOrInvalidFormat), - "IncorrectMessageLengthOrInvalidFormat: Failed {}", - test_data.test_name - ); - } - } - } - } - } - - #[test] - fn write_did_request_bytes() { - let test_ids = get_test_ids(); - - let test_data_sets: Vec = vec![ - WriteRequestTestData::from_ids("No ids", &Vec::new()), - WriteRequestTestData::from_ids("1 id", &test_ids[0..1]), - WriteRequestTestData::from_ids("2 ids", &test_ids[0..2]), - WriteRequestTestData::from_ids("3 ids", &test_ids[0..3]), - WriteRequestTestData::from_ids("4 ids", &test_ids[0..4]), - WriteRequestTestData::from_ids("All ids", &test_ids), - WriteRequestTestData::from_ids( - "Repeated ids", - &test_ids - .clone() - .iter() - .cycle() - .take(100) - .copied() - .collect::>(), - ), - ]; - - for test_data in &test_data_sets { - let request = ReadDataByIdentifierRequest::new(test_data.dids.clone()); - let mut buffer = Vec::new(); - let write_result = request.encode(&mut buffer); - - match write_result { - Ok(bytes_read) => { - // 1 did is 2 bytes - let expected_byte_count = request.dids.len() * 2; - assert_eq!(bytes_read, expected_byte_count); - } - Err(e) => { - assert!( - matches!(e, Error::InsufficientData(_)), - "InsufficientData: Failed {}", - test_data.test_name - ); - } - } - } - } - } - - mod response { - use super::*; - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - pub struct BazData { - pub data: [u8; 16], - pub data2: u64, - pub data3: u16, - } - - // The UDSIdentifiers are vender defined and don't have interesting payloads, so we define our own types for - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Debug, Eq, PartialEq)] - pub enum TestPayload { - #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] - MeaningOfLife([u8; 42]), - Foo(u32), - Bar, - Baz(BazData), - UDSIdentifier(UDSIdentifier), - } - - impl TestPayload { - fn new(did: u16, reader: &mut R) -> Result { - match did { - 0xFF00 => { - let mut data = [0u8; 42]; - reader.read_exact(&mut data)?; - Ok(TestPayload::MeaningOfLife(data)) - } - 0xFF01 => { - let mut data = [0u8; 4]; - reader.read_exact(&mut data)?; - let value = u32::from_be_bytes(data); - Ok(TestPayload::Foo(value)) - } - 0xFF02 => Ok(TestPayload::Bar), - 0xFF03 => { - let data = BazData::decode(reader)?; - Ok(TestPayload::Baz(data)) - } - _ => { - let identifier = UDSIdentifier::try_from(did)?; - Ok(TestPayload::UDSIdentifier(identifier)) - } - } - } - } - - impl From for u16 { - fn from(value: TestPayload) -> Self { - match value { - TestPayload::MeaningOfLife(_) => 0xFF00, - TestPayload::Foo(_) => 0xFF01, - TestPayload::Bar => 0xFF02, - TestPayload::Baz(_) => 0xFF03, - TestPayload::UDSIdentifier(uds_id) => u16::from(uds_id), - } - } - } - - impl WireFormat for TestPayload { - #[allow(clippy::match_same_arms)] - fn required_size(&self) -> usize { - match self { - TestPayload::MeaningOfLife(_) => 42, - TestPayload::Foo(_) => 4, - TestPayload::Bar => 0, - TestPayload::Baz(_) => 26, - TestPayload::UDSIdentifier(_) => 0, - } - } - - #[allow(clippy::match_same_arms)] - fn encode(&self, writer: &mut W) -> Result { - let id_bytes = u16::from(self.clone()).to_be_bytes(); - let did_len = writer.write(&id_bytes)?; - match self { - TestPayload::MeaningOfLife(data) => { - writer.write_all(data)?; - Ok(did_len + data.len()) - } - TestPayload::Foo(value) => { - let bytes = value.to_be_bytes(); - writer.write_all(&bytes)?; - Ok(did_len + bytes.len()) - } - TestPayload::Bar => Ok(did_len), - TestPayload::Baz(data) => data.encode(writer), - TestPayload::UDSIdentifier(_) => Ok(did_len), - } - } - } - - impl IterableWireFormat for TestPayload { - fn decode_next(reader: &mut R) -> Result, Error> { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 => return Ok(None), - 1 => return Err(Error::IncorrectMessageLengthOrInvalidFormat), - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - let did = u16::from_be_bytes(identifier_data); - Ok(Some(TestPayload::new(did, reader)?)) - } - } - - impl WireFormat for BazData { - fn required_size(&self) -> usize { - 26 - } - - fn encode(&self, writer: &mut W) -> Result { - writer.write_all(&self.data)?; - let mut count = 16; - count += writer.write(&self.data2.to_be_bytes())?; - count += writer.write(&self.data3.to_be_bytes())?; - // 2 for the initial did bytes - Ok(2 + count) - } - } - - impl SingleValueWireFormat for BazData { - fn decode(reader: &mut R) -> Result { - let mut data = [0u8; 16]; - reader.read_exact(&mut data)?; - - let mut data2_bytes = [0u8; 8]; - reader.read_exact(&mut data2_bytes)?; - let data2 = u64::from_be_bytes(data2_bytes); - - let mut data3_bytes = [0u8; 2]; - reader.read_exact(&mut data3_bytes)?; - let data3 = u16::from_be_bytes(data3_bytes); - - Ok(BazData { data, data2, data3 }) - } - } - - fn get_test_response_data() -> Vec { - vec![ - TestPayload::MeaningOfLife([0; 42]), - TestPayload::Foo(42), - TestPayload::Bar, - TestPayload::Baz(BazData { - data: [5; 16], - data2: 1_234_567_890, - data3: 54_321, - }), - TestPayload::UDSIdentifier(UDSIdentifier::BootSoftwareIdentification), - ] - } - - #[test] - fn read_did_response_bytes() { - let test_data = get_test_response_data(); - - let response = ReadDataByIdentifierResponse::new(test_data); - let mut buffer = Vec::new(); - response.encode(&mut buffer).unwrap(); - - let read_response: ReadDataByIdentifierResponse = - ReadDataByIdentifierResponse::::decode(&mut buffer.as_slice()) - .unwrap(); - - assert_eq!(response, read_response); - } - - #[test] - fn write_did_response_bytes() { - let test_data = get_test_response_data(); - - let response = ReadDataByIdentifierResponse::new(test_data.clone()); - let mut buffer = Vec::new(); - let bytes_written = response.encode(&mut buffer).unwrap(); - - let expected_bytes: Vec = test_data - .iter() - .flat_map(|payload| { - let mut buf = Vec::new(); - payload.encode(&mut buf).unwrap(); - buf - }) - .collect(); - - assert_eq!(buffer, expected_bytes); - assert_eq!(bytes_written, expected_bytes.len()); - } + use crate::test_util::assert_encode_size_agrees; + + #[test] + fn encode_read_did_request_tx() { + let ids = [0xF180u16, 0xF186u16]; + let req = ReadDataByIdentifierRequestTx::new(&ids); + let mut buf = [0u8; 16]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 4); // 2 DIDs * 2 bytes each + assert_eq!(&buf[..4], &[0xF1, 0x80, 0xF1, 0x86]); + assert_encode_size_agrees(&req); } } diff --git a/src/services/read_dtc_information.rs b/src/services/read_dtc_information.rs index 353ec81..ecc80a8 100644 --- a/src/services/read_dtc_information.rs +++ b/src/services/read_dtc_information.rs @@ -1,11 +1,9 @@ //! `ReadDTCInformation` (0x19) request and response service implementation -use byteorder::{ReadBytesExt, WriteBytesExt}; use crate::{ - DTCExtDataRecordList, DTCExtDataRecordNumber, DTCFormatIdentifier, DTCRecord, DTCSeverityMask, - DTCSeverityRecord, DTCSnapshotRecord, DTCSnapshotRecordList, DTCSnapshotRecordNumber, - DTCStatusMask, DTCStoredDataRecordNumber, Error, FunctionalGroupIdentifier, IterableWireFormat, - SingleValueWireFormat, WireFormat, + DTCExtDataRecordNumber, DTCFormatIdentifier, DTCRecord, DTCSeverityMask, + DTCSnapshotRecordNumber, DTCStatusMask, DTCStoredDataRecordNumber, Decode, Encode, Error, + FunctionalGroupIdentifier, }; /// Used for non-emissions related servers @@ -24,203 +22,217 @@ pub struct ReadDTCInfoRequest { } impl ReadDTCInfoRequest { - pub(crate) fn new(dtc_subfunction: ReadDTCInfoSubFunction) -> Self { + /// Create a new `ReadDTCInfoRequest`. + #[must_use] + pub fn new(dtc_subfunction: ReadDTCInfoSubFunction) -> Self { Self { dtc_subfunction } } } -impl WireFormat for ReadDTCInfoRequest { - fn required_size(&self) -> usize { - self.dtc_subfunction.required_size() +impl Encode for ReadDTCInfoRequest { + fn encoded_size(&self) -> usize { + self.dtc_subfunction.encoded_size() } - fn encode(&self, writer: &mut T) -> Result { + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { self.dtc_subfunction.encode(writer) } } -impl SingleValueWireFormat for ReadDTCInfoRequest { - fn decode(reader: &mut T) -> Result { - let dtc_subfunction = ReadDTCInfoSubFunction::decode(reader)?; - - Ok(Self { dtc_subfunction }) +impl<'a> Decode<'a> for ReadDTCInfoRequest { + #[allow(clippy::too_many_lines)] + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + use ReadDTCInfoSubFunction as S; + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let sub = buf[0]; + let rest = &buf[1..]; + let (dtc_subfunction, rest) = match sub { + 0x01 => { + let (m, r) = DTCStatusMask::decode(rest)?; + (S::ReportNumberOfDTC_ByStatusMask(m), r) + } + 0x02 => { + let (m, r) = DTCStatusMask::decode(rest)?; + (S::ReportDTC_ByStatusMask(m), r) + } + 0x03 => (S::ReportDTCSnapshotIdentification, rest), + 0x04 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCSnapshotRecordNumber::decode(r)?; + (S::ReportDTCSnapshotRecord_ByDTCNumber(rec, n), r) + } + 0x05 => { + let (n, r) = DTCStoredDataRecordNumber::decode(rest)?; + (S::ReportDTCStoredData_ByRecordNumber(n), r) + } + 0x06 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCExtDataRecordNumber::decode(r)?; + (S::ReportDTCExtDataRecord_ByDTCNumber(rec, n), r) + } + 0x07 => { + let (s, r) = DTCSeverityMask::decode(rest)?; + let (m, r) = DTCStatusMask::decode(r)?; + (S::ReportNumberOfDTC_BySeverityMaskRecord(s, m), r) + } + 0x08 => { + let (s, r) = DTCSeverityMask::decode(rest)?; + let (m, r) = DTCStatusMask::decode(r)?; + (S::ReportDTC_BySeverityMaskRecord(s, m), r) + } + 0x09 => { + let (rec, r) = DTCRecord::decode(rest)?; + (S::ReportSeverityInfoOfDTC(rec), r) + } + 0x0A => (S::ReportSupportedDTC, rest), + 0x0B => (S::ReportFirstTestFailedDTC, rest), + 0x0C => (S::ReportFirstConfirmedDTC, rest), + 0x0D => (S::ReportMostRecentTestFailedDTC, rest), + 0x0E => (S::ReportMostRecentConfirmedDTC, rest), + 0x14 => (S::ReportDTCFaultDetectionCounter, rest), + 0x15 => (S::ReportDTCWithPermanentStatus, rest), + 0x16 => { + let (n, r) = DTCExtDataRecordNumber::decode(rest)?; + (S::ReportDTCExtDataRecord_ByRecordNumber(n), r) + } + 0x17 => { + let (m, r) = DTCStatusMask::decode(rest)?; + (S::ReportUserDefMemoryDTC_ByStatusMask(m), r) + } + 0x18 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCSnapshotRecordNumber::decode(r)?; + let (mem, r) = u8::decode(r)?; + ( + S::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(rec, n, mem), + r, + ) + } + 0x19 => { + let (rec, r) = DTCRecord::decode(rest)?; + let (n, r) = DTCExtDataRecordNumber::decode(r)?; + let (mem, r) = u8::decode(r)?; + ( + S::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(rec, n, mem), + r, + ) + } + 0x1A => { + let (n, r) = DTCExtDataRecordNumber::decode(rest)?; + (S::ReportSupportedDTCExtDataRecord(n), r) + } + 0x42 => { + let (g, r) = FunctionalGroupIdentifier::decode(rest)?; + let (m, r) = DTCStatusMask::decode(r)?; + let (s, r) = DTCSeverityMask::decode(r)?; + (S::ReportWWHOBDDTC_ByMaskRecord(g, m, s), r) + } + 0x55 => { + let (g, r) = FunctionalGroupIdentifier::decode(rest)?; + (S::ReportWWHOBDDTC_WithPermanentStatus(g), r) + } + 0x56 => { + let (g, r) = FunctionalGroupIdentifier::decode(rest)?; + let (rg, r) = u8::decode(r)?; + ( + S::ReportDTCInformation_ByDTCReadinessGroupIdentifier(g, rg), + r, + ) + } + other => (S::ISOSAEReserved(other), rest), + }; + Ok((ReadDTCInfoRequest::new(dtc_subfunction), rest)) } } -/// A DTC paired with its fault detection counter value -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct DTCFaultDetectionCounterRecord { - pub dtc_record: DTCRecord, - pub dtc_fault_detection_counter: DTCFaultDetectionCounter, -} - -impl WireFormat for DTCFaultDetectionCounterRecord { - fn required_size(&self) -> usize { - 4 - } +#[cfg(test)] +mod read_dtc_info_request_encode_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; - fn encode(&self, writer: &mut T) -> Result { - self.dtc_record.encode(writer)?; - writer.write_u8(self.dtc_fault_detection_counter)?; - Ok(self.required_size()) + #[test] + fn encode_no_param_subfunction() { + // 0x0A ReportSupportedDTC, no parameters. + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportSupportedDTC); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x0A]); + assert_encode_size_agrees(&req); } -} -impl IterableWireFormat for DTCFaultDetectionCounterRecord { - #[allow(clippy::match_same_arms)] - fn decode_next(reader: &mut T) -> Result, Error> { - let dtc_record = match DTCRecord::decode_next(reader) { - Ok(None) => return Ok(None), - Ok(Some(record)) => record, - Err(_) => return Ok(None), - }; - let dtc_fault_detection_counter = reader.read_u8()?; - Ok(Some(Self { - dtc_record, - dtc_fault_detection_counter, - })) + #[test] + fn encode_single_param_subfunction() { + // 0x02 ReportDTC_ByStatusMask(mask). DTCStatusMask is 1 byte. + let mask = DTCStatusMask::from(0xFF); + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTC_ByStatusMask(mask)); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x02, 0xFF]); + assert_encode_size_agrees(&req); } -} -/// Record containing user-defined memory DTC information filtered by status mask -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct UserDefMemoryDTCByStatusMaskRecord { - // This parameter shall be used to address the respective user defined DTC memory when retrieving DTCs. - pub memory_selection: MemorySelection, - /// The status mask of the DTC, representing its current state. - pub status_availability_mask: DTCStatusMask, - /// Vector of DTC Records and Status of Corresponding DTC - pub record_data: Vec<(DTCRecord, DTCStatusMask)>, -} - -/// Record containing user-defined memory DTC snapshot data for a specific DTC -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct UserDefMemoryDTCSnapshotRecordByDTCNumRecord { - // This parameter shall be used to address the respective user defined DTC memory when retrieving DTCs. - pub memory_selection: MemorySelection, - pub dtc_record: DTCRecord, - pub dtc_status_mask: DTCStatusMask, - /// Contains a snapshot of data values from the time of the system malfunction occurrence. - pub dtc_snapshot_record: Vec<(DTCSnapshotRecordNumber, DTCSnapshotRecord)>, -} -impl WireFormat - for UserDefMemoryDTCSnapshotRecordByDTCNumRecord -{ - fn required_size(&self) -> usize { - 1 + self.dtc_record.required_size() - + self.dtc_status_mask.required_size() - + self - .dtc_snapshot_record - .iter() - .fold(0, |acc, (record_number, record)| { - acc + record_number.required_size() + record.required_size() - }) + #[test] + fn encode_multi_param_subfunction() { + // 0x42 ReportWWHOBDDTC_ByMaskRecord(group, status, severity). + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportWWHOBDDTC_ByMaskRecord( + FunctionalGroupIdentifier::EmissionsSystemGroup, + DTCStatusMask::from(0x08), + DTCSeverityMask::CheckImmediately, + )); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x42, 0x33, 0x08, 0b1000_0000]); + assert_encode_size_agrees(&req); } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.memory_selection)?; - self.dtc_record.encode(writer)?; - self.dtc_status_mask.encode(writer)?; - for (record_number, record) in &self.dtc_snapshot_record { - record_number.encode(writer)?; - record.encode(writer)?; - } - Ok(self.required_size()) + #[test] + fn encode_reserved_subfunction() { + // ISOSAEReserved carries the sub-function byte itself, no params. + let req = ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ISOSAEReserved(0x57)); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x57]); + assert_encode_size_agrees(&req); } -} -impl SingleValueWireFormat - for UserDefMemoryDTCSnapshotRecordByDTCNumRecord -{ - fn decode(reader: &mut T) -> Result { - let memory_selection = reader.read_u8()?; - let dtc_record = DTCRecord::decode(reader)?; - let dtc_status_mask = DTCStatusMask::decode(reader)?; - let mut dtc_snapshot_record = Vec::new(); - - while let Ok(Some(dtc_snapshot_record_number)) = - DTCSnapshotRecordNumber::decode_next(reader) - { - let snapshot_record = DTCSnapshotRecord::decode(reader)?; - dtc_snapshot_record.push((dtc_snapshot_record_number, snapshot_record)); + #[test] + fn read_dtc_info_request_roundtrips() { + use crate::Decode; + // Encode into a scratch buffer (oracle), then decode_exact and assert round-trip fidelity. + let cases = [ + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportSupportedDTC), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTC_ByStatusMask( + DTCStatusMask::from(0xFF), + )), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportWWHOBDDTC_ByMaskRecord( + FunctionalGroupIdentifier::EmissionsSystemGroup, + DTCStatusMask::from(0x08), + DTCSeverityMask::CheckImmediately, + )), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ISOSAEReserved(0x57)), + ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTCSnapshotRecord_ByDTCNumber( + DTCRecord::new(0x12, 0x34, 0x56), + DTCSnapshotRecordNumber::new(0x01), + )), + ]; + for req in cases { + let mut buf = [0u8; 16]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + let decoded = ::decode_exact(&buf[..written]).unwrap(); + assert_eq!(decoded, req); } - - Ok(UserDefMemoryDTCSnapshotRecordByDTCNumRecord { - memory_selection, - dtc_record, - dtc_status_mask, - dtc_snapshot_record, - }) } } -/// List of WWH OBD DTCs and corresponding status and severity information matching a client defined status mask and severity mask record -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct WWHOBDDTCByMaskRecord { - /// Echo from the request. - pub functional_group_identifier: FunctionalGroupIdentifier, - /// Same representation as [`DTCStatusMask`] but with the bits 'on' representing the DTC status supported by the server - pub status_availability_mask: DTCStatusAvailabilityMask, - pub severity_availability_mask: DTCSeverityMask, - /// Specifies the format of the DTC reported by the server. - /// Only possible options: - /// `DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04` - /// `DTCFormatIdentifier::SAE_J1939_73_DTCFormat` - pub format_identifier: DTCFormatIdentifier, - pub record_data: Vec<(DTCSeverityMask, DTCRecord, DTCStatusMask)>, -} - -/// List of WWH OBD DTCs with "permanent DTC" status as described in 3.12 -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct WWHOBDDTCWithPermanentStatusRecord { - /// Echo from the request. - pub functional_group_identifier: FunctionalGroupIdentifier, - /// Same representation as [`DTCStatusMask`] but with the bits 'on' representing the DTC status supported by the server - pub status_availability_mask: DTCStatusAvailabilityMask, - /// Specifies the format of the DTC reported by the server. - /// Only possible options: - /// `DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04` - /// `DTCFormatIdentifier::SAE_J1939_73_DTCFormat` - pub format_identifier: DTCFormatIdentifier, - pub record_data: Vec<(DTCRecord, DTCStatusMask)>, -} - -/// List of OBD DTCs which matches the `DTCReadiness` Group Identifier -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct DTCByReadinessGroupIdentifierRecord { - /// Echo from the request. - pub functional_group_identifier: FunctionalGroupIdentifier, - /// Same representation as [`DTCStatusMask`] but with the bits 'on' representing the DTC status supported by the server - pub status_availability_mask: DTCStatusAvailabilityMask, - /// Specifies the format of the DTC reported by the server. - pub format_identifier: DTCFormatIdentifier, - /// DTC readiness groups - pub readiness_group_identifier: DTCReadinessGroupIdentifier, - pub record_data: Vec<(DTCRecord, DTCStatusMask)>, -} - -/// List of DTCs that support a specific extended data record number +/// A DTC paired with its fault detection counter value #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct SupportedDTCExtDataRecord { - /// Same representation as [`DTCStatusMask`] but with the bits 'on' representing the DTC status supported by the server - pub status_availability_mask: DTCStatusAvailabilityMask, - /// Request message to get a stored [`DTCExtDataRecord`] - pub ext_data_record_number: Option, - pub dtc_and_status_records: Vec<(DTCRecord, DTCStatusMask)>, +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct DTCFaultDetectionCounterRecord { + pub dtc_record: DTCRecord, + pub dtc_fault_detection_counter: DTCFaultDetectionCounter, } /// Have to reference SAE J1979-DA for the corresponding DTC readiness groups and the [`FunctionalGroupIdentifier`]s @@ -383,1592 +395,499 @@ impl ReadDTCInfoSubFunction { } } -impl WireFormat for ReadDTCInfoSubFunction { - #[allow(clippy::match_same_arms)] - fn required_size(&self) -> usize { +impl Encode for ReadDTCInfoSubFunction { + fn encoded_size(&self) -> usize { + use ReadDTCInfoSubFunction as S; 1 + match self { - Self::ReportNumberOfDTC_ByStatusMask(_) => 1, - Self::ReportDTC_ByStatusMask(_) => 1, - Self::ReportDTCSnapshotIdentification => 0, - Self::ReportDTCSnapshotRecord_ByDTCNumber(_, _) => 4, - Self::ReportDTCStoredData_ByRecordNumber(_) => 2, - Self::ReportDTCExtDataRecord_ByDTCNumber(_, _) => 4, - Self::ReportNumberOfDTC_BySeverityMaskRecord(_, _) => 2, - Self::ReportDTC_BySeverityMaskRecord(_, _) => 2, - Self::ReportSeverityInfoOfDTC(_) => 3, - Self::ReportSupportedDTC => 0, - Self::ReportFirstTestFailedDTC => 0, - Self::ReportFirstConfirmedDTC => 0, - Self::ReportMostRecentTestFailedDTC => 0, - Self::ReportMostRecentConfirmedDTC => 0, - Self::ReportDTCFaultDetectionCounter => 0, - Self::ReportDTCWithPermanentStatus => 0, - Self::ReportDTCExtDataRecord_ByRecordNumber(_) => 1, - Self::ReportUserDefMemoryDTC_ByStatusMask(_) => 1, - Self::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(_, _, _) => 5, - Self::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(_, _, _) => 5, - Self::ReportSupportedDTCExtDataRecord(_) => 1, - Self::ReportWWHOBDDTC_ByMaskRecord(_, _, _) => 3, - Self::ReportWWHOBDDTC_WithPermanentStatus(_) => 1, - Self::ReportDTCInformation_ByDTCReadinessGroupIdentifier(_, _) => 2, - - Self::ISOSAEReserved(_) => 0, + S::ReportNumberOfDTC_ByStatusMask(m) + | S::ReportDTC_ByStatusMask(m) + | S::ReportUserDefMemoryDTC_ByStatusMask(m) => m.encoded_size(), + S::ReportDTCSnapshotRecord_ByDTCNumber(r, n) => r.encoded_size() + n.encoded_size(), + S::ReportDTCStoredData_ByRecordNumber(n) => n.encoded_size(), + S::ReportDTCExtDataRecord_ByDTCNumber(r, n) => r.encoded_size() + n.encoded_size(), + S::ReportNumberOfDTC_BySeverityMaskRecord(s, m) + | S::ReportDTC_BySeverityMaskRecord(s, m) => s.encoded_size() + m.encoded_size(), + S::ReportSeverityInfoOfDTC(r) => r.encoded_size(), + S::ReportDTCExtDataRecord_ByRecordNumber(n) | S::ReportSupportedDTCExtDataRecord(n) => { + n.encoded_size() + } + S::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(r, n, mem) => { + r.encoded_size() + n.encoded_size() + mem.encoded_size() + } + S::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(r, n, mem) => { + r.encoded_size() + n.encoded_size() + mem.encoded_size() + } + S::ReportWWHOBDDTC_ByMaskRecord(g, m, s) => { + g.encoded_size() + m.encoded_size() + s.encoded_size() + } + S::ReportWWHOBDDTC_WithPermanentStatus(g) => g.encoded_size(), + S::ReportDTCInformation_ByDTCReadinessGroupIdentifier(g, rg) => { + g.encoded_size() + rg.encoded_size() + } + S::ReportDTCSnapshotIdentification + | S::ReportSupportedDTC + | S::ReportFirstTestFailedDTC + | S::ReportFirstConfirmedDTC + | S::ReportMostRecentTestFailedDTC + | S::ReportMostRecentConfirmedDTC + | S::ReportDTCFaultDetectionCounter + | S::ReportDTCWithPermanentStatus + | S::ISOSAEReserved(_) => 0, } } - #[allow(clippy::match_same_arms)] - fn encode(&self, writer: &mut T) -> Result { - // Write the subfunction value - writer.write_u8(self.value())?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + use ReadDTCInfoSubFunction as S; + writer.write_all(&[self.value()]).map_err(Error::io)?; match self { - Self::ReportNumberOfDTC_ByStatusMask(mask) => { - mask.encode(writer)?; - } - Self::ReportDTC_ByStatusMask(mask) => { - mask.encode(writer)?; - } - Self::ReportDTCSnapshotIdentification => {} - Self::ReportDTCSnapshotRecord_ByDTCNumber(mask, record_number) => { - mask.encode(writer)?; - record_number.encode(writer)?; - } - Self::ReportDTCStoredData_ByRecordNumber(record_number) => { - record_number.encode(writer)?; - } - Self::ReportDTCExtDataRecord_ByDTCNumber(mask, record_number) => { - mask.encode(writer)?; - record_number.encode(writer)?; - } - Self::ReportNumberOfDTC_BySeverityMaskRecord(severity, status) => { - writer.write_u8(severity.bits())?; - status.encode(writer)?; - } - Self::ReportDTC_BySeverityMaskRecord(severity, status) => { - writer.write_u8(severity.bits())?; - status.encode(writer)?; - } - Self::ReportSeverityInfoOfDTC(mask) => { - mask.encode(writer)?; - } - Self::ReportSupportedDTC => {} - Self::ReportFirstTestFailedDTC => {} - Self::ReportFirstConfirmedDTC => {} - Self::ReportMostRecentTestFailedDTC => {} - Self::ReportMostRecentConfirmedDTC => {} - Self::ReportDTCFaultDetectionCounter => {} - Self::ReportDTCWithPermanentStatus => {} - Self::ReportDTCExtDataRecord_ByRecordNumber(record_number) => { - record_number.encode(writer)?; - } - Self::ReportUserDefMemoryDTC_ByStatusMask(mask) => { - mask.encode(writer)?; - } - Self::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(mask, number, selection) => { - mask.encode(writer)?; - number.encode(writer)?; - writer.write_u8(*selection)?; - } - Self::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(mask, number, selection) => { - mask.encode(writer)?; - number.encode(writer)?; - writer.write_u8(*selection)?; - } - Self::ReportSupportedDTCExtDataRecord(number) => { - number.encode(writer)?; - } - Self::ReportWWHOBDDTC_ByMaskRecord(group, status, severity) => { - writer.write_u8(group.value())?; - status.encode(writer)?; - writer.write_u8(severity.bits())?; - } - Self::ReportWWHOBDDTC_WithPermanentStatus(group) => { - writer.write_u8(group.value())?; - } - Self::ReportDTCInformation_ByDTCReadinessGroupIdentifier(group, readiness) => { - writer.write_u8(group.value())?; - writer.write_u8(*readiness)?; - } - Self::ISOSAEReserved(value) => { - writer.write_u8(*value)?; - } + S::ReportNumberOfDTC_ByStatusMask(m) + | S::ReportDTC_ByStatusMask(m) + | S::ReportUserDefMemoryDTC_ByStatusMask(m) => { + m.encode(writer)?; + } + S::ReportDTCSnapshotRecord_ByDTCNumber(r, n) => { + r.encode(writer)?; + n.encode(writer)?; + } + S::ReportDTCStoredData_ByRecordNumber(n) => { + n.encode(writer)?; + } + S::ReportDTCExtDataRecord_ByDTCNumber(r, n) => { + r.encode(writer)?; + n.encode(writer)?; + } + S::ReportNumberOfDTC_BySeverityMaskRecord(s, m) + | S::ReportDTC_BySeverityMaskRecord(s, m) => { + s.encode(writer)?; + m.encode(writer)?; + } + S::ReportSeverityInfoOfDTC(r) => { + r.encode(writer)?; + } + S::ReportDTCExtDataRecord_ByRecordNumber(n) | S::ReportSupportedDTCExtDataRecord(n) => { + n.encode(writer)?; + } + S::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber(r, n, mem) => { + r.encode(writer)?; + n.encode(writer)?; + mem.encode(writer)?; + } + S::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber(r, n, mem) => { + r.encode(writer)?; + n.encode(writer)?; + mem.encode(writer)?; + } + S::ReportWWHOBDDTC_ByMaskRecord(g, m, s) => { + g.encode(writer)?; + m.encode(writer)?; + s.encode(writer)?; + } + S::ReportWWHOBDDTC_WithPermanentStatus(g) => { + g.encode(writer)?; + } + S::ReportDTCInformation_ByDTCReadinessGroupIdentifier(g, rg) => { + g.encode(writer)?; + rg.encode(writer)?; + } + S::ReportDTCSnapshotIdentification + | S::ReportSupportedDTC + | S::ReportFirstTestFailedDTC + | S::ReportFirstConfirmedDTC + | S::ReportMostRecentTestFailedDTC + | S::ReportMostRecentConfirmedDTC + | S::ReportDTCFaultDetectionCounter + | S::ReportDTCWithPermanentStatus + | S::ISOSAEReserved(_) => {} } - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for ReadDTCInfoSubFunction { - #[allow(clippy::match_same_arms)] - fn decode(reader: &mut T) -> Result { - let report_type = reader.read_u8()?; - - let subfunction = match report_type { - 0x01 | 0x02 => { - let status = DTCStatusMask::from(reader.read_u8()?); - match report_type { - 0x01 => Self::ReportNumberOfDTC_ByStatusMask(status), - 0x02 => Self::ReportDTC_ByStatusMask(status), - _ => unreachable!(), - } - } - 0x03 => Self::ReportDTCSnapshotIdentification, - 0x04 => Self::ReportDTCSnapshotRecord_ByDTCNumber( - DTCRecord::decode(reader)?, - DTCSnapshotRecordNumber::decode(reader)?, - ), - 0x05 => { - Self::ReportDTCStoredData_ByRecordNumber(DTCStoredDataRecordNumber::decode(reader)?) - } - // 0xFF for all records, 0xFE for all OBD records - 0x06 => Self::ReportDTCExtDataRecord_ByDTCNumber( - DTCRecord::decode(reader)?, - DTCExtDataRecordNumber::decode(reader)?, - ), - 0x07 => Self::ReportNumberOfDTC_BySeverityMaskRecord( - DTCSeverityMask::from(reader.read_u8()?), - DTCStatusMask::from(reader.read_u8()?), - ), - 0x08 => Self::ReportDTC_BySeverityMaskRecord( - DTCSeverityMask::from(reader.read_u8()?), - DTCStatusMask::from(reader.read_u8()?), - ), - 0x09 => Self::ReportSeverityInfoOfDTC(DTCRecord::decode(reader)?), - 0x0A => Self::ReportSupportedDTC, - 0x0B => Self::ReportFirstTestFailedDTC, - 0x0C => Self::ReportFirstConfirmedDTC, - 0x0D => Self::ReportMostRecentTestFailedDTC, - 0x0E => Self::ReportMostRecentConfirmedDTC, - 0x14 => Self::ReportDTCFaultDetectionCounter, - 0x15 => Self::ReportDTCWithPermanentStatus, - 0x16 => { - Self::ReportDTCExtDataRecord_ByRecordNumber(DTCExtDataRecordNumber::decode(reader)?) - } - 0x17 => { - Self::ReportUserDefMemoryDTC_ByStatusMask(DTCStatusMask::from(reader.read_u8()?)) - } - // 0xFF for all records - 0x18 => Self::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber( - DTCRecord::decode(reader)?, - DTCSnapshotRecordNumber::decode(reader)?, - reader.read_u8()?, - ), - 0x19 => Self::ReportUserDefMemoryDTCExtDataRecord_ByDTCNumber( - DTCRecord::decode(reader)?, - DTCExtDataRecordNumber::decode(reader)?, - reader.read_u8()?, - ), - 0x1A => Self::ReportSupportedDTCExtDataRecord(DTCExtDataRecordNumber::decode(reader)?), - 0x42 => Self::ReportWWHOBDDTC_ByMaskRecord( - FunctionalGroupIdentifier::from(reader.read_u8()?), - DTCStatusMask::from(reader.read_u8()?), - DTCSeverityMask::from(reader.read_u8()?), - ), - 0x43..=0x54 => Self::ISOSAEReserved(report_type), - 0x55 => Self::ReportWWHOBDDTC_WithPermanentStatus(FunctionalGroupIdentifier::from( - reader.read_u8()?, - )), - 0x56 => Self::ReportDTCInformation_ByDTCReadinessGroupIdentifier( - FunctionalGroupIdentifier::from(reader.read_u8()?), - reader.read_u8()?, - ), - 0x57..=0x7F => Self::ISOSAEReserved(report_type), - _ => return Err(Error::InvalidDtcSubfunctionType(report_type)), - }; - Ok(subfunction) + Ok(self.encoded_size()) } } -type NumberOfDTCs = u16; /// Same representation as [`DTCStatusMask`] but with the bits 'on' representing the DTC status supported by the server /// IE if the server doesn't support [`DTCStatusMask::WarningIndicatorRequested`] then the bit for that status will be 'off' /// and all other bits will be 'on' type DTCStatusAvailabilityMask = DTCStatusMask; -/// Subfunction ID for the response -type SubFunctionID = u8; +// --------------------------------------------------------------------------- +// no_std RX types with lazy iterators +// --------------------------------------------------------------------------- -/// Response payloads can be shared among multiple request subfunctions +/// Lazy iterator over `(DTCRecord, DTCStatusMask)` pairs from raw bytes. /// -/// For example, subfunction 0x01 and 0x07 both return the number of DTCs -/// and have the same response format -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -#[non_exhaustive] -pub enum ReadDTCInfoResponse { - /// * Parameter: [`DTCStatusMask`] (1) - /// * Parameter: `NumberOfDTCs`(2) - /// - /// For subfunctions 0x01, 0x07 - /// * 0x01: [`ReadDTCInfoSubFunction::ReportNumberOfDTC_ByStatusMask`] - /// * 0x07: [`ReadDTCInfoSubFunction::ReportNumberOfDTC_BySeverityMaskRecord`] - NumberOfDTCs(SubFunctionID, DTCStatusAvailabilityMask, NumberOfDTCs), +/// Each pair is 4 bytes: 3 for the DTC record + 1 for the status mask. +#[derive(Clone, Debug)] +pub struct DtcAndStatusIter<'a> { + remaining: &'a [u8], +} - /// A list of DTCs matching the subfunction request - /// - /// * Parameter: [`DTCStatusMask`] (1) - /// * Parameter: `Vec` (4 * n) - /// - /// Note: DTC list can be empty if there are none to report, - /// but the response will still be sent - /// - /// For subfunctions 0x02, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x15 - /// * 0x02: [`ReadDTCInfoSubFunction::ReportDTC_ByStatusMask`] - /// * 0x0A: [`ReadDTCInfoSubFunction::ReportSupportedDTC`] - /// * 0x0B: [`ReadDTCInfoSubFunction::ReportFirstTestFailedDTC`] - /// * 0x0C: [`ReadDTCInfoSubFunction::ReportFirstConfirmedDTC`] - /// * 0x0D: [`ReadDTCInfoSubFunction::ReportMostRecentTestFailedDTC`] - /// * 0x0E: [`ReadDTCInfoSubFunction::ReportMostRecentConfirmedDTC`] - /// * 0x15: [`ReadDTCInfoSubFunction::ReportDTCWithPermanentStatus`] - DTCList( - SubFunctionID, - DTCStatusAvailabilityMask, - Vec<(DTCRecord, DTCStatusMask)>, - ), +impl<'a> DtcAndStatusIter<'a> { + /// Create an iterator over `(DTCRecord, DTCStatusMask)` pairs. + #[must_use] + pub const fn new(data: &'a [u8]) -> Self { + Self { remaining: data } + } - /// Snapshot identification - aka "Freeze Frame" - /// - /// Parameter: Vec<(`DTCRecord`, `DTCSnapshotRecordNumber`> (4 * n) - /// - /// Note: `DTCSnapshot` list might be empty - /// - /// For subfunction 0x03 - /// * 0x03: [`ReadDTCInfoSubFunction::ReportDTCSnapshotIdentification`] - DTCSnapshotList(Vec<(DTCRecord, DTCSnapshotRecordNumber)>), + /// Number of complete records available. + #[must_use] + pub const fn len(&self) -> usize { + self.remaining.len() / 4 + } - /// Get the DTC status and snapshot number and information w/ corresponding Data Identifier (DID) - /// - /// DTC, Status, snapshot number, # of identifiers, DID (times # of identifiers), Snapshot info. - /// - /// If all records are requested, it can be a theoretically large amount of data. - /// - /// Parameter: `DTCRecord` (3 bytes) - Echo of the request - /// Parameter: `DTCStatusMask` (1) - status of the requested DTC - /// C2/C4: There are multiple dataIdentifier/snapshotData combinations allowed to be present in a single `DTCSnapshotRecord`. - /// This can, for example be the case for the situation where a single dataIdentifier only references an integral part of data. When - /// the dataIdentifier references a block of data then a single dataIdentifier/snapshotData combination can be used. - /// - /// Note: See example 12.3.5.6.2 in ISO 14229-1:2020 for more information - /// - /// For subfunction 0x04 - /// * 0x04: [`ReadDTCInfoSubFunction::ReportDTCSnapshotRecord_ByDTCNumber`] - DTCSnapshotRecordList(DTCSnapshotRecordList), + /// Whether there are no records. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.remaining.is_empty() + } - /// List of [`crate::DTCExtDataRecord`]s for a given DTC. - /// - /// `UserPayload` is so the data can be read according to a specific format - /// defined by the supplier/vehicle manufacturer + /// Collect all records into a `Vec`. /// - /// * Parameter: [`DTCRecord`] (3 bytes) - Echo of the request - /// * Parameter: [`DTCStatusMask`] (1) - status of the requested DTC - /// * Parameter: [`crate::DTCExtDataRecord`] (n) - /// - /// For subfunction 0x06 - /// * 0x06: [`ReadDTCInfoSubFunction::ReportDTCExtDataRecord_ByDTCNumber`] - DTCExtDataRecordList(DTCExtDataRecordList), + /// # Errors + /// Returns an error if the byte data contains a partial record. + #[cfg(feature = "alloc")] + pub fn collect_all(self) -> Result, Error> { + self.collect() + } +} - /// List of DTC Records that either match a severity and status mask for subfunction [`ReadDTCInfoSubFunction::ReportDTC_BySeverityMaskRecord`], - /// or a single record if the request type was [`ReadDTCInfoSubFunction::ReportSeverityInfoOfDTC`]. - /// - /// * Parameter: [`DTCStatusMask`] (1 byte) - /// * Parameter: `Vec` (6 bytes) - /// - /// For Subfunctions 0x08, 0x09 - /// * 0x08: [`ReadDTCInfoSubFunction::ReportDTC_BySeverityMaskRecord`] - /// * 0x09: [`ReadDTCInfoSubFunction::ReportSeverityInfoOfDTC`] - DTCSeverityRecordList( - SubFunctionID, - DTCStatusAvailabilityMask, - Vec, - ), - /// List of DTC Records along with their fault detection counters for subfunction [`ReadDTCInfoSubFunction::ReportDTCFaultDetectionCounter`]. +impl Iterator for DtcAndStatusIter<'_> { + type Item = Result<(DTCRecord, DTCStatusMask), Error>; - /// - /// * Parameter: [`DTCRecord`] - (3 bytes) - /// * Parameter: `DTCFaultDetectionCounter` - (1 byte) - /// - /// For Subfunction 0x14: - /// * 0x14: [`ReadDTCInfoSubFunction::ReportDTCFaultDetectionCounter`] - DTCFaultDetectionCounterRecordList(Vec), + fn next(&mut self) -> Option { + if self.remaining.is_empty() { + return None; + } + if self.remaining.len() < 4 { + return Some(Err(Error::IncorrectMessageLengthOrInvalidFormat)); + } + let record = DTCRecord::new(self.remaining[0], self.remaining[1], self.remaining[2]); + let status = DTCStatusMask::from(self.remaining[3]); + self.remaining = &self.remaining[4..]; + Some(Ok((record, status))) + } +} - /// List of DTCs out of User Defined DTC Memory and corresponding statuses matching client - /// defined status mask - /// - /// * Parameter: `UserDefMemoryDTCByStatusMaskRecord` (n) - /// - /// For subfunction 0x17 - /// * 0x17: [`ReadDTCInfoSubFunction::ReportUserDefMemoryDTC_ByStatusMask`] - UserDefMemoryDTCByStatusMaskList(UserDefMemoryDTCByStatusMaskRecord), +/// Lazy iterator over `DTCFaultDetectionCounterRecord` from raw bytes. +/// +/// Each record is 4 bytes: 3 for the DTC record + 1 for the fault detection counter. +#[derive(Clone, Debug)] +pub struct DtcFaultDetectionIter<'a> { + remaining: &'a [u8], +} - /// List of [`crate::DTCSnapshotRecord`]s for a given DTC. - /// - /// `UserPayload` is so the data can be read according to a specific format - /// defined by the supplier/vehicle manufacturer - /// - /// Contains a snapshot of data values from the time of the system malfunction occurrence. - /// * Parameter: `MemorySelection` (1) - user defined DTC memory when retrieving DTCs. - /// * Parameter: [`DTCRecord`] (3 bytes) - /// * Parameter: [`DTCStatusMask`] (1 bytes) - /// * Parameter: `Vec<(DTCSnapshotRecordNumber, DTCSnapshotRecord)>` (m*(1+n) bytes) - Echo of the request - /// - /// For subfunction 0x18 - /// * 0x18: [`ReadDTCInfoSubFunction::ReportUserDefMemoryDTCSnapshotRecord_ByDTCNumber`] - UserDefMemoryDTCSnapshotRecordByDTCNumberList( - UserDefMemoryDTCSnapshotRecordByDTCNumRecord, - ), +impl<'a> DtcFaultDetectionIter<'a> { + /// Create an iterator over `DTCFaultDetectionCounterRecord` values. + #[must_use] + pub const fn new(data: &'a [u8]) -> Self { + Self { remaining: data } + } - /// DTCs which supports a `DTCExtendedDataRecord` - /// - /// * Parameter: [`DTCStatusMask`] (1) - /// * Parameter: `Option` (1) - /// * Parameter: `Vec<(DTCRecord, DTCStatusMask)>` (4 * n bytes) + /// Collect all records into a `Vec`. /// - /// `Option` is only present if atleast one DTC supports the `DTCExtendedDataRecord` - /// `Vec<(DTCRecord, DTCStatusMask)>` length is non-zero only if atleast one DTC supports the `DTCExtendedDataRecord` - /// - /// For Subfunction 0x1A - /// * 0x1A: [`ReadDTCInfoSubFunction::ReportSupportedDTCExtDataRecord`] - SupportedDTCExtDataRecordList(SupportedDTCExtDataRecord), + /// # Errors + /// Returns an error if the byte data contains a partial record. + #[cfg(feature = "alloc")] + pub fn collect_all(self) -> Result, Error> { + self.collect() + } +} - /// List of WWH OBD DTCs and corresponding status and severity information - /// matching a client defined status mask and severity mask record - /// - ///Contains a struct of `WWHOBDDTCByMaskRecord` - /// * Parameter: [`FunctionalGroupIdentifier`] (1) - /// * Parameter: [`DTCStatusMask`] (1) - /// * Parameter: [`DTCSeverityMask`] (1) - /// * Parameter: [`DTCFormatIdentifier`] (1) - /// * Parameter: `Vec<(DTCSeverityMask, DTCRecord, DTCStatusMask)>` (5*n) - /// - /// Only possible options for [`DTCFormatIdentifier`] : - /// `DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04` - /// `DTCFormatIdentifier::SAE_J1939_73_DTCFormat` - /// * Returns `Error::InvalidDtcFormatIdentifier` in case of incorrect `DTCFormatIdentifier` - /// - /// For Subfunction 0x42 - /// * 0x42: [`ReadDTCInfoSubFunction::ReportWWHOBDDTC_ByMaskRecord`] - WWHOBDDTCByMaskRecordList(WWHOBDDTCByMaskRecord), +impl Iterator for DtcFaultDetectionIter<'_> { + type Item = Result; - /// List of WWH OBD DTCs with "permanent DTC" status as described in 3.12 - /// - ///Contains a struct of `WWHOBDDTCWithPermanentStatusRecord` - /// * Parameter: [`FunctionalGroupIdentifier`] (1) - /// * Parameter: [`DTCStatusMask`] (1) - /// * Parameter: [`DTCFormatIdentifier`] (1) - /// * Parameter: `Vec<(DTCRecord, DTCStatusMask)>` (4*n) - /// - /// Only possible options for [`DTCFormatIdentifier`] : - /// `DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04` - /// `DTCFormatIdentifier::SAE_J1939_73_DTCFormat` - /// * Returns `Error::InvalidDtcFormatIdentifier` in case of incorrect `DTCFormatIdentifier` - /// - /// For Subfunction 0x55 - /// * 0x55: [`ReadDTCInfoSubFunction::ReportWWHOBDDTC_WithPermanentStatus`] - WWHOBDDTCWithPermanentStatusList(WWHOBDDTCWithPermanentStatusRecord), + fn next(&mut self) -> Option { + if self.remaining.is_empty() { + return None; + } + if self.remaining.len() < 4 { + return Some(Err(Error::IncorrectMessageLengthOrInvalidFormat)); + } + let dtc_record = DTCRecord::new(self.remaining[0], self.remaining[1], self.remaining[2]); + let dtc_fault_detection_counter = self.remaining[3]; + self.remaining = &self.remaining[4..]; + Some(Ok(DTCFaultDetectionCounterRecord { + dtc_record, + dtc_fault_detection_counter, + })) + } +} - /// List of OBD DTCs which matches the `DTCReadiness` Group Identifier - /// - /// Contains a struct of `DTCByReadinessGroupIdentifierRecord` - /// * Parameter: [`FunctionalGroupIdentifier`] (1) - /// * Parameter: [`DTCStatusMask`] (1) - /// * Parameter: [`DTCFormatIdentifier`] (1) - /// * Parameter: `DTCReadinessGroupIdentifier` (1) - /// * Parameter: `Vec<(DTCRecord, DTCStatusMask)>` (5*n) +/// Lazy iterator over `(DTCSeverityMask, DTCRecord, DTCStatusMask)` triples from raw bytes. +/// +/// Each triple is 5 bytes: 1 severity + 3 DTC record + 1 status mask. +#[derive(Clone, Debug)] +pub struct DtcSeverityAndStatusIter<'a> { + remaining: &'a [u8], +} + +impl<'a> DtcSeverityAndStatusIter<'a> { + /// Create an iterator over severity/DTC/status triples. + #[must_use] + pub const fn new(data: &'a [u8]) -> Self { + Self { remaining: data } + } + + /// Collect all triples into a `Vec`. /// - /// For Subfunction 0x56 - /// * 0x56: [`ReadDTCInfoSubFunction::ReportDTCInformation_ByDTCReadinessGroupIdentifier`] - DTCByReadinessGroupIdentifierList(DTCByReadinessGroupIdentifierRecord), + /// # Errors + /// Returns an error if the byte data contains a partial record. + #[cfg(feature = "alloc")] + pub fn collect_all( + self, + ) -> Result, Error> { + self.collect() + } } -impl WireFormat for ReadDTCInfoResponse { - fn required_size(&self) -> usize { - // subfunction ID + subfunction contents - 1 + match self { - Self::NumberOfDTCs(_, _, _) => 3, - Self::DTCList(_, _, list) => 1 + list.len() * 4, - Self::DTCSnapshotList(list) => 1 + list.len() * 4, - Self::DTCSnapshotRecordList(list) => list.required_size(), - Self::DTCExtDataRecordList(list) => list.required_size(), - Self::DTCSeverityRecordList(_, _, list) => 1 + list.len() * 6, - Self::DTCFaultDetectionCounterRecordList(list) => list.len() * 4, - Self::UserDefMemoryDTCByStatusMaskList(list) => 2 + list.record_data.len() * 4, - Self::UserDefMemoryDTCSnapshotRecordByDTCNumberList(list) => list.required_size(), - Self::SupportedDTCExtDataRecordList(list) => { - if list.ext_data_record_number.is_some() { - 2 + list.dtc_and_status_records.len() * 4 - } else { - 1 - } - } - Self::WWHOBDDTCByMaskRecordList(response_struct) => { - 4 + response_struct.record_data.len() * 5 - } - Self::WWHOBDDTCWithPermanentStatusList(response_struct) => { - 3 + response_struct.record_data.len() * 4 - } - Self::DTCByReadinessGroupIdentifierList(response_struct) => { - 4 + response_struct.record_data.len() * 4 - } +impl Iterator for DtcSeverityAndStatusIter<'_> { + type Item = Result<(DTCSeverityMask, DTCRecord, DTCStatusMask), Error>; + + fn next(&mut self) -> Option { + if self.remaining.is_empty() { + return None; + } + if self.remaining.len() < 5 { + return Some(Err(Error::IncorrectMessageLengthOrInvalidFormat)); } + let severity = DTCSeverityMask::from(self.remaining[0]); + let record = DTCRecord::new(self.remaining[1], self.remaining[2], self.remaining[3]); + let status = DTCStatusMask::from(self.remaining[4]); + self.remaining = &self.remaining[5..]; + Some(Ok((severity, record, status))) } +} - #[allow(clippy::too_many_lines)] - fn encode(&self, writer: &mut T) -> Result { +/// Zero-copy parsed response for `ReadDTCInformation` (0x19). +/// +/// Stores raw bytes for record collections and provides lazy iterators +/// that parse on demand without allocation. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum ReadDTCInfoResponse<'a> { + /// Sub-functions 0x01, 0x07: count of DTCs matching a mask. + NumberOfDTCs { + /// Sub-function byte echo. + sub_function_id: u8, + /// DTC status availability mask. + status_availability_mask: DTCStatusAvailabilityMask, + /// Number of matching DTCs. + count: u16, + }, + /// Sub-functions 0x02, 0x0A-0x0E, 0x15: list of `(DTCRecord, DTCStatusMask)` pairs. + DTCList { + /// Sub-function byte echo. + sub_function_id: u8, + /// DTC status availability mask. + status_availability_mask: DTCStatusAvailabilityMask, + /// Raw record bytes — use [`DtcAndStatusIter`] to iterate. + raw_records: &'a [u8], + }, + /// Sub-function 0x14: list of DTC fault detection counter records. + DTCFaultDetectionCounterList { + /// Raw record bytes — use [`DtcFaultDetectionIter`] to iterate. + raw_records: &'a [u8], + }, + /// Sub-functions 0x08, 0x09: list of DTC severity records. + DTCSeverityList { + /// Sub-function byte echo. + sub_function_id: u8, + /// DTC status availability mask. + status_availability_mask: DTCStatusAvailabilityMask, + /// Raw record bytes (6 bytes per record) — use [`DtcSeverityAndStatusIter`] iteration. + raw_records: &'a [u8], + }, + /// Sub-function 0x42: WWH-OBD DTC by mask with severity info. + WWHOBDDTCByMaskRecord { + /// Functional group identifier echo. + functional_group_identifier: FunctionalGroupIdentifier, + /// DTC status availability mask. + status_availability_mask: DTCStatusAvailabilityMask, + /// Severity availability mask. + severity_availability_mask: DTCSeverityMask, + /// DTC format identifier. + format_identifier: DTCFormatIdentifier, + /// Raw record bytes (5 bytes per record) — use [`DtcSeverityAndStatusIter`]. + raw_records: &'a [u8], + }, +} + +impl<'a> ReadDTCInfoResponse<'a> { + /// Iterate `(DTCRecord, DTCStatusMask)` pairs for `DTCList` variants. + /// + /// Returns `None` if this is not a `DTCList` variant. + #[must_use] + pub fn dtc_and_status_iter(&self) -> Option> { match self { - Self::NumberOfDTCs(id, mask, count) => { - writer.write_u8(*id)?; - writer.write_u8(mask.bits())?; - writer.write_u16::(*count)?; - } - Self::DTCList(id, mask, list) => { - writer.write_u8(*id)?; - writer.write_u8(mask.bits())?; - for (record, status) in list { - record.encode(writer)?; - status.encode(writer)?; - } - } - Self::DTCSnapshotList(list) => { - writer.write_u8(0x03)?; - for (record, number) in list { - record.encode(writer)?; - number.encode(writer)?; - } - } - Self::DTCSnapshotRecordList(list) => { - writer.write_u8(0x04)?; - list.encode(writer)?; - } - Self::DTCExtDataRecordList(list) => { - writer.write_u8(0x06)?; - list.encode(writer)?; - } - Self::DTCFaultDetectionCounterRecordList(list) => { - writer.write_u8(0x14)?; - for fault_detection_counter in list { - fault_detection_counter.encode(writer)?; - } - } - Self::DTCSeverityRecordList(id, status, list) => { - writer.write_u8(*id)?; - status.encode(writer)?; - for dtcs in list { - dtcs.encode(writer)?; - } - } - Self::UserDefMemoryDTCByStatusMaskList(data_record_struct) => { - writer.write_u8(0x17)?; - writer.write_u8(data_record_struct.memory_selection)?; - data_record_struct.status_availability_mask.encode(writer)?; - for (data_record, status) in &data_record_struct.record_data { - data_record.encode(writer)?; - status.encode(writer)?; - } - } + Self::DTCList { raw_records, .. } => Some(DtcAndStatusIter::new(raw_records)), + _ => None, + } + } - Self::UserDefMemoryDTCSnapshotRecordByDTCNumberList(snapshot_struct) => { - writer.write_u8(0x18)?; - snapshot_struct.encode(writer)?; - } - Self::SupportedDTCExtDataRecordList(response_struct) => { - writer.write_u8(0x1A)?; - response_struct.status_availability_mask.encode(writer)?; - if let Some(record_number) = &response_struct.ext_data_record_number { - record_number.encode(writer)?; - for (record, status) in &response_struct.dtc_and_status_records { - record.encode(writer)?; - status.encode(writer)?; - } - } - } - Self::WWHOBDDTCByMaskRecordList(response_struct) => { - writer.write_u8(0x42)?; - writer.write_u8(response_struct.functional_group_identifier.value())?; - response_struct.status_availability_mask.encode(writer)?; - writer.write_u8(response_struct.severity_availability_mask.into())?; - writer.write_u8(response_struct.format_identifier.into())?; - for (dtc_severity, dtc_record, dtc_status) in &response_struct.record_data { - writer.write_u8((*dtc_severity).into())?; - dtc_record.encode(writer)?; - dtc_status.encode(writer)?; - } - } - Self::WWHOBDDTCWithPermanentStatusList(response_struct) => { - writer.write_u8(0x55)?; - writer.write_u8(response_struct.functional_group_identifier.value())?; - response_struct.status_availability_mask.encode(writer)?; - writer.write_u8(response_struct.format_identifier.into())?; - for (dtc_record, dtc_status) in &response_struct.record_data { - dtc_record.encode(writer)?; - dtc_status.encode(writer)?; - } + /// Iterate fault detection counter records for the `DTCFaultDetectionCounterList` variant. + /// + /// Returns `None` if this is not that variant. + #[must_use] + pub fn fault_detection_iter(&self) -> Option> { + match self { + Self::DTCFaultDetectionCounterList { raw_records } => { + Some(DtcFaultDetectionIter::new(raw_records)) } - Self::DTCByReadinessGroupIdentifierList(response_struct) => { - writer.write_u8(0x56)?; - writer.write_u8(response_struct.functional_group_identifier.value())?; - response_struct.status_availability_mask.encode(writer)?; - writer.write_u8(response_struct.format_identifier.into())?; - writer.write_u8(response_struct.readiness_group_identifier)?; - for (dtc_record, dtc_status) in &response_struct.record_data { - dtc_record.encode(writer)?; - dtc_status.encode(writer)?; - } + _ => None, + } + } + + /// Iterate severity/DTC/status triples for WWH-OBD variants. + /// + /// Returns `None` if this is not a severity variant. + #[must_use] + pub fn severity_and_status_iter(&self) -> Option> { + match self { + Self::WWHOBDDTCByMaskRecord { raw_records, .. } => { + Some(DtcSeverityAndStatusIter::new(raw_records)) } + _ => None, } - Ok(self.required_size()) } } -impl SingleValueWireFormat for ReadDTCInfoResponse { - #[allow(clippy::too_many_lines)] - fn decode(reader: &mut T) -> Result { - let subfunction_id = reader.read_u8()?; +impl<'a> Decode<'a> for ReadDTCInfoResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let subfunction_id = buf[0]; + let buf = &buf[1..]; match subfunction_id { 0x01 | 0x07 => { - let status = DTCStatusAvailabilityMask::from(reader.read_u8()?); - let count = reader.read_u16::()?; - Ok(Self::NumberOfDTCs(subfunction_id, status, count)) - } - 0x02 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0E | 0x15 => { - let status = DTCStatusAvailabilityMask::from(reader.read_u8()?); - let mut dtcs: Vec<(DTCRecord, DTCStatusMask)> = Vec::new(); - - // Loop until we're done with the reader and fill the DTC list - while let Ok(Some(record)) = DTCRecord::decode_next(reader) { - match reader.read_u8() { - Ok(status) => dtcs.push((record, DTCStatusMask::from(status))), - Err(_) => break, - } - } - - Ok(Self::DTCList(subfunction_id, status, dtcs)) - } - 0x03 => { - let mut dtcs: Vec<(DTCRecord, DTCSnapshotRecordNumber)> = Vec::new(); - - // Loop until we're done with the reader and fill the DTC list - while let Ok(Some(record)) = DTCRecord::decode_next(reader) { - match DTCSnapshotRecordNumber::decode_next(reader)? { - Some(number) => dtcs.push((record, number)), - None => break, - } - } - - Ok(Self::DTCSnapshotList(dtcs)) - } - 0x04 => { - let snapshot_list = DTCSnapshotRecordList::decode(reader)?; - Ok(Self::DTCSnapshotRecordList(snapshot_list)) - } - 0x06 => { - let ext_data_list = DTCExtDataRecordList::decode(reader)?; - Ok(Self::DTCExtDataRecordList(ext_data_list)) - } - 0x08 | 0x09 => { - let status = DTCStatusAvailabilityMask::from(reader.read_u8()?); - let mut dtcs = Vec::new(); - - for dtc_severity_record in DTCSeverityRecord::decode_iter(reader) { - match dtc_severity_record { - Ok(p) => { - dtcs.push(p); - } - Err(e) => { - return Err(e); - } - } - } - - Ok(Self::DTCSeverityRecordList(subfunction_id, status, dtcs)) - } - 0x14 => { - let mut dtcs = Vec::new(); - for dtc_fault_record in DTCFaultDetectionCounterRecord::decode_iter(reader) { - match dtc_fault_record { - Ok(p) => { - dtcs.push(p); - } - Err(e) => { - return Err(e); - } - } - } - Ok(Self::DTCFaultDetectionCounterRecordList(dtcs)) - } - 0x17 => { - let memory_selection = reader.read_u8()?; - let status_availability_mask = DTCStatusMask::decode(reader)?; - let mut record_data = Vec::new(); - - while let Ok(Some(record)) = DTCRecord::decode_next(reader) { - let status = DTCStatusMask::decode(reader)?; - record_data.push((record, status)); - } - - Ok(Self::UserDefMemoryDTCByStatusMaskList( - UserDefMemoryDTCByStatusMaskRecord { - memory_selection, + if buf.len() < 3 { + return Err(Error::InsufficientData(4)); + } + let status_availability_mask = DTCStatusAvailabilityMask::from(buf[0]); + let count = u16::from_be_bytes([buf[1], buf[2]]); + Ok(( + Self::NumberOfDTCs { + sub_function_id: subfunction_id, status_availability_mask, - record_data, + count, }, + &buf[3..], )) } - 0x18 => Ok(Self::UserDefMemoryDTCSnapshotRecordByDTCNumberList( - UserDefMemoryDTCSnapshotRecordByDTCNumRecord::decode(reader)?, - )), - 0x1A => { - let status_availability_mask = DTCStatusAvailabilityMask::decode(reader)?; - let mut dtc_and_status_records = Vec::new(); - let ext_data_record_number = DTCExtDataRecordNumber::decode_next(reader)?; - if ext_data_record_number.is_some() { - while let Ok(Some(dtc_record)) = DTCRecord::decode_next(reader) { - let dtc_status = DTCStatusMask::decode(reader)?; - dtc_and_status_records.push((dtc_record, dtc_status)); - } + 0x02 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0E | 0x15 => { + if buf.is_empty() { + return Err(Error::InsufficientData(2)); } - Ok(Self::SupportedDTCExtDataRecordList( - SupportedDTCExtDataRecord { + let status_availability_mask = DTCStatusAvailabilityMask::from(buf[0]); + Ok(( + Self::DTCList { + sub_function_id: subfunction_id, status_availability_mask, - ext_data_record_number, - dtc_and_status_records, + raw_records: &buf[1..], }, + &[], )) } - - 0x42 => { - let functional_group_identifier = - FunctionalGroupIdentifier::from(reader.read_u8()?); - let status_availability_mask = DTCStatusAvailabilityMask::decode(reader)?; - let severity_availability_mask = DTCSeverityMask::from(reader.read_u8()?); - let format_identifier = DTCFormatIdentifier::from(reader.read_u8()?); - if (format_identifier != DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04) - && (format_identifier != DTCFormatIdentifier::SAE_J1939_73_DTCFormat) - { - return Err(Error::InvalidDtcFormatIdentifier(u8::from( - format_identifier, - ))); - } - let mut record_data = Vec::new(); - while let Ok(dtc_severity_mask) = reader.read_u8() { - let dtc_severity_mask = DTCSeverityMask::from(dtc_severity_mask); - let dtc_record = DTCRecord::decode(reader)?; - let dtc_status = DTCStatusMask::decode(reader)?; - record_data.push((dtc_severity_mask, dtc_record, dtc_status)); - } - - Ok(Self::WWHOBDDTCByMaskRecordList(WWHOBDDTCByMaskRecord { - functional_group_identifier, - status_availability_mask, - severity_availability_mask, - format_identifier, - record_data, - })) - } - 0x55 => { - let functional_group_identifier = - FunctionalGroupIdentifier::from(reader.read_u8()?); - let status_availability_mask = DTCStatusAvailabilityMask::decode(reader)?; - let format_identifier = DTCFormatIdentifier::from(reader.read_u8()?); - if !matches!( - format_identifier, - DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04 - | DTCFormatIdentifier::SAE_J1939_73_DTCFormat - ) { - return Err(Error::InvalidDtcFormatIdentifier(u8::from( - format_identifier, - ))); - } - let mut record_data = Vec::new(); - while let Ok(Some(dtc_record)) = DTCRecord::decode_next(reader) { - let dtc_status = DTCStatusMask::decode(reader)?; - record_data.push((dtc_record, dtc_status)); + 0x14 => Ok((Self::DTCFaultDetectionCounterList { raw_records: buf }, &[])), + 0x08 | 0x09 => { + if buf.is_empty() { + return Err(Error::InsufficientData(2)); } - - Ok(Self::WWHOBDDTCWithPermanentStatusList( - WWHOBDDTCWithPermanentStatusRecord { - functional_group_identifier, + let status_availability_mask = DTCStatusAvailabilityMask::from(buf[0]); + Ok(( + Self::DTCSeverityList { + sub_function_id: subfunction_id, status_availability_mask, - format_identifier, - record_data, + raw_records: &buf[1..], }, + &[], )) } - 0x56 => { - let functional_group_identifier = - FunctionalGroupIdentifier::from(reader.read_u8()?); - let status_availability_mask = DTCStatusAvailabilityMask::decode(reader)?; - let format_identifier = DTCFormatIdentifier::from(reader.read_u8()?); - let readiness_group_identifier = - DTCReadinessGroupIdentifier::from(reader.read_u8()?); - let mut record_data = Vec::new(); - while let Ok(Some(dtc_record)) = DTCRecord::decode_next(reader) { - let dtc_status = DTCStatusMask::decode(reader)?; - record_data.push((dtc_record, dtc_status)); - } - - Ok(Self::DTCByReadinessGroupIdentifierList( - DTCByReadinessGroupIdentifierRecord { + 0x42 => { + if buf.len() < 4 { + return Err(Error::InsufficientData(5)); + } + let functional_group_identifier = FunctionalGroupIdentifier::from(buf[0]); + let status_availability_mask = DTCStatusAvailabilityMask::from(buf[1]); + let severity_availability_mask = DTCSeverityMask::from(buf[2]); + let format_identifier = DTCFormatIdentifier::from(buf[3]); + Ok(( + Self::WWHOBDDTCByMaskRecord { functional_group_identifier, status_availability_mask, + severity_availability_mask, format_identifier, - readiness_group_identifier, - record_data, + raw_records: &buf[4..], }, + &[], )) } - _ => todo!(), // _ => Err(Error::InvalidDtcSubfunctionType(subfunction_id)), + _ => Err(Error::InvalidDtcSubfunctionType(subfunction_id)), } } } -#[cfg(test)] -mod response { - - use super::*; - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, PartialEq)] - pub enum TestIdentifier { - Abracadabra = 0xBEEF, - } - - impl PartialEq for TestIdentifier { - fn eq(&self, other: &u16) -> bool { - match self { - TestIdentifier::Abracadabra => *other == 0xBEEF, - } - } - } - - impl WireFormat for TestIdentifier { - fn encode(&self, writer: &mut T) -> Result { - writer.write_u16::(*self as u16)?; - Ok(self.required_size()) - } - - fn required_size(&self) -> usize { - 2 - } - } - - impl IterableWireFormat for TestIdentifier { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut buf = [0u8; 2]; - reader.read_exact(&mut buf)?; - - let id = u16::from_be_bytes(buf); - if TestIdentifier::Abracadabra == id { - Ok(Some(TestIdentifier::Abracadabra)) - } else { - Err(Error::NoDataAvailable) - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////// - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, PartialEq)] - enum TestPayload { - Abracadabra(u8), - } - - impl WireFormat for TestPayload { - fn encode(&self, writer: &mut T) -> Result { - let id_bytes: u16 = match self { - TestPayload::Abracadabra(_) => 0xBEEF, - }; - - writer.write_all(&id_bytes.to_be_bytes())?; - - match self { - TestPayload::Abracadabra(value) => { - writer.write_u8(*value)?; - Ok(self.required_size()) - } - } - } - - fn required_size(&self) -> usize { - 3 - } - } - impl IterableWireFormat for TestPayload { - fn decode_next(reader: &mut T) -> Result, Error> { - let mut buf = [0u8; 2]; - reader.read_exact(&mut buf)?; - - let value = u16::from_be_bytes(buf); - - if value == TestIdentifier::Abracadabra as u16 { - let mut byte = [0u8; 1]; - reader.read_exact(&mut byte)?; - Ok(Some(TestPayload::Abracadabra(byte[0]))) - } else { - Err(Error::NoDataAvailable) - } - } - } - - #[test] - fn dtc_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x02, // subfunction - 0x01, // Availability mask - // First DTC record - 0x01, 0x02, 0x03, (DTCStatusMask::PendingDTC | DTCStatusMask::TestFailed).into(), - // Second DTC record - 0x17, 0x04, 0x03, DTCStatusMask::TestNotCompletedThisOperationCycle.into(), - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - assert_eq!( - response, - ReadDTCInfoResponse::DTCList( - 0x02, - DTCStatusMask::TestFailed, - vec![ - ( - DTCRecord::new(0x01, 0x02, 0x03), - DTCStatusMask::PendingDTC | DTCStatusMask::TestFailed - ), - ( - DTCRecord::new(0x17, 0x04, 0x03), - DTCStatusMask::TestNotCompletedThisOperationCycle - ) - ] - ) - ); - - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes); - assert_eq!(written, bytes.len()); - assert_eq!(written, response.required_size()); - } - - #[test] - fn severity_list_test() { - let bytes: [u8; 8] = [ - 0x08, // subfunction - 0x01, // Availability mask - DTCSeverityMask::CheckImmediately.into(), - FunctionalGroupIdentifier::EmissionsSystemGroup.into(), - 0x01, - 0x02, - 0x03, - (DTCStatusMask::PendingDTC | DTCStatusMask::TestFailed).into(), - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - assert_eq!( - response, - ReadDTCInfoResponse::DTCSeverityRecordList( - 0x08, - DTCStatusMask::TestFailed, - vec![ - (DTCSeverityRecord { - severity: DTCSeverityMask::CheckImmediately, - functional_group_identifier: - FunctionalGroupIdentifier::EmissionsSystemGroup, - dtc_record: DTCRecord::new(0x01, 0x02, 0x03), - dtc_status_mask: (DTCStatusMask::PendingDTC | DTCStatusMask::TestFailed), - }) - ] - ) - ); - - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes); - assert_eq!(written, bytes.len()); - assert_eq!(written, response.required_size()); - } - - #[test] - fn severity_empty_list_test() { - let bytes: [u8; 2] = [ - 0x08, // subfunction - 0x01, // Availability mask - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - assert_eq!( - response, - ReadDTCInfoResponse::DTCSeverityRecordList(0x08, DTCStatusMask::TestFailed, vec![]) - ); - - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes); - assert_eq!(written, bytes.len()); - assert_eq!(written, response.required_size()); - } - - #[test] - fn fault_detection_test() { - let bytes = [ - 0x14, // subfunction - 0x01, 0x02, 0x03, //DTC Record - 0x04, //DTC Status - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - assert_eq!( - response, - ReadDTCInfoResponse::DTCFaultDetectionCounterRecordList(vec![ - DTCFaultDetectionCounterRecord { - dtc_record: DTCRecord::new(0x01, 0x02, 0x03), - dtc_fault_detection_counter: 0x04 - } - ]) - ); - - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes); - assert_eq!(written, bytes.len()); - assert_eq!(written, response.required_size()); - } - #[test] - fn fault_detection_empty_test() { - let bytes = [ - 0x14, // subfunction - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - assert_eq!( - response, - ReadDTCInfoResponse::DTCFaultDetectionCounterRecordList(vec![]) - ); - - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes); - assert_eq!(written, bytes.len()); - assert_eq!(written, response.required_size()); - } - - #[test] - fn user_def_memory_dtc_by_statusmask_empty_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x17, // subfunction - 0x15, // Memory Selection - DTCStatusAvailabilityMask::TestFailed.into(), //Availability Mask - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::UserDefMemoryDTCByStatusMaskList( - UserDefMemoryDTCByStatusMaskRecord { - memory_selection: 0x15, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - record_data: vec![] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn user_def_memory_dtc_by_statusmask_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x17, // subfunction - 0x15, // Memory Selection - DTCStatusAvailabilityMask::TestFailed.into(), // Availability Mask - 0x12, 0x34, 0x56, // DTC Mask - DTCStatusMask::TestFailed.into(), // Status - 0x12, 0x34, 0x56, // DTC Mask - DTCStatusMask::TestFailed.into(), // Status - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::UserDefMemoryDTCByStatusMaskList( - UserDefMemoryDTCByStatusMaskRecord { - memory_selection: 0x15, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - record_data: vec![ - (DTCRecord::new(0x12, 0x34, 0x56), DTCStatusMask::TestFailed), - (DTCRecord::new(0x12, 0x34, 0x56), DTCStatusMask::TestFailed), - ] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - #[test] - fn user_def_memory_dtc_by_dtc_number_empty_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x18, // subfunction - 0x01, // Memory Selection - 0x12, 0x34, 0x56, // DTC Mask - DTCStatusAvailabilityMask::TestFailed.into(), // Availability Mask - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::UserDefMemoryDTCSnapshotRecordByDTCNumberList( - UserDefMemoryDTCSnapshotRecordByDTCNumRecord { - memory_selection: 0x1, - dtc_record: DTCRecord::new(0x12, 0x34, 0x56), - dtc_status_mask: DTCStatusMask::TestFailed, - dtc_snapshot_record: vec![] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn user_def_memory_dtc_by_dtc_number_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x18, // subfunction - 0x01, // Memory Selection - 0x12, 0x34, 0x56, // DTC Mask - DTCStatusAvailabilityMask::TestFailed.into(), // Availability Mask - 0x13, // DTCSnapshotRecordNumber - 0x02, // DTCSnapshotRecordNumberOfIdentifiers - 0xBE, 0xEF, // SnapshotDataIdentifier - 0x05, // SnapshotData - 0xBE, 0xEF, // SnapshotDataIdentifier - 0x05, // SnapshotData - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::UserDefMemoryDTCSnapshotRecordByDTCNumberList( - UserDefMemoryDTCSnapshotRecordByDTCNumRecord { - memory_selection: 0x1, - dtc_record: DTCRecord::new(0x12, 0x34, 0x56), - dtc_status_mask: DTCStatusMask::TestFailed, - dtc_snapshot_record: vec![( - DTCSnapshotRecordNumber::new(0x13), - DTCSnapshotRecord { - data: vec![ - TestPayload::Abracadabra(0x05), - TestPayload::Abracadabra(0x05) - ] - } - )] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn supported_dtc_ext_data_record_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x1A, // subfunction - DTCStatusAvailabilityMask::TestFailed.into(), // Availability Mask - DTCExtDataRecordNumber::AllDTCExtDataRecords.value(), // DTC Extended Data Record Number - 0x15,0x17,0x19 ,// DTCRecord - DTCStatusMask::TestFailedSinceLastClear.into(),// DTC Status - 0x15,0x17,0x19 ,// DTCRecord - DTCStatusMask::TestFailedSinceLastClear.into(),// DTC Status - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::SupportedDTCExtDataRecordList(SupportedDTCExtDataRecord { - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - ext_data_record_number: Some(DTCExtDataRecordNumber::AllDTCExtDataRecords), - dtc_and_status_records: vec![ - ( - DTCRecord::new(0x15, 0x17, 0x19), // DTCRecord - DTCStatusMask::TestFailedSinceLastClear - ), - ( - DTCRecord::new(0x15, 0x17, 0x19), // DTCRecord - DTCStatusMask::TestFailedSinceLastClear - ) - ] - }) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn supported_dtc_ext_data_record_empty_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x1A, // subfunction - DTCStatusAvailabilityMask::TestFailed.into(), // Availability Mask - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - assert_eq!( - response, - ReadDTCInfoResponse::SupportedDTCExtDataRecordList(SupportedDTCExtDataRecord { - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - ext_data_record_number: None, - dtc_and_status_records: vec![] - }) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn report_wwhobd_dtc_by_mask_record_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x42, // subfunction - FunctionalGroupIdentifier::VODBSystem.into(), - DTCStatusAvailabilityMask::TestFailed.into(), - DTCSeverityMask::DTCClass_0.into(), - DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04.into(), - DTCSeverityMask::DTCClass_0.into(), - 0x15,0x17,0x19 ,// DTCRecord - DTCStatusAvailabilityMask::TestFailed.into(), - DTCSeverityMask::DTCClass_0.into(), - 0x15,0x17,0x19 ,// DTCRecord - DTCStatusAvailabilityMask::TestFailed.into(), - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::WWHOBDDTCByMaskRecordList(WWHOBDDTCByMaskRecord { - functional_group_identifier: FunctionalGroupIdentifier::VODBSystem, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - severity_availability_mask: DTCSeverityMask::DTCClass_0, - format_identifier: DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04, - record_data: vec![ - ( - DTCSeverityMask::DTCClass_0, - DTCRecord::new(0x15, 0x17, 0x19), - DTCStatusAvailabilityMask::TestFailed - ), - ( - DTCSeverityMask::DTCClass_0, - DTCRecord::new(0x15, 0x17, 0x19), - DTCStatusAvailabilityMask::TestFailed - ) - ] - }) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn report_wwhobd_dtc_by_mask_record_empty_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x42, // subfunction - FunctionalGroupIdentifier::VODBSystem.into(), - DTCStatusAvailabilityMask::TestFailed.into(), - DTCSeverityMask::all_flags().into(), - DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04.into(), - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::WWHOBDDTCByMaskRecordList(WWHOBDDTCByMaskRecord { - functional_group_identifier: FunctionalGroupIdentifier::VODBSystem, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - severity_availability_mask: DTCSeverityMask::all_flags(), - format_identifier: DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04, - record_data: vec![] - }) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn report_wwhobd_dtc_with_permanent_status_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x55, // subfunction - FunctionalGroupIdentifier::VODBSystem.into(), - DTCStatusAvailabilityMask::TestFailed.into(), - DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04.into(), - 0x15,0x17,0x19 ,// DTCRecord - DTCStatusMask::TestFailed.into(), - 0x51,0x71,0x91 ,// DTCRecord - DTCStatusMask::TestFailed.into(), - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::WWHOBDDTCWithPermanentStatusList( - WWHOBDDTCWithPermanentStatusRecord { - functional_group_identifier: FunctionalGroupIdentifier::VODBSystem, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - format_identifier: DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04, - record_data: vec![ - (DTCRecord::new(0x15, 0x17, 0x19), DTCStatusMask::TestFailed), - (DTCRecord::new(0x51, 0x71, 0x91), DTCStatusMask::TestFailed) - ] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn report_dtc_by_readiness_group_identifier_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x56, // subfunction - FunctionalGroupIdentifier::VODBSystem.into(), - DTCStatusAvailabilityMask::TestFailed.into(), - DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04.into(), - 0x72,// Readiness Group Identifier - 0x15,0x17,0x19 ,// DTCRecord - DTCStatusAvailabilityMask::TestFailed.into(), - 0x51,0x71,0x91 ,// DTCRecord - DTCStatusAvailabilityMask::TestFailed.into(), - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::DTCByReadinessGroupIdentifierList( - DTCByReadinessGroupIdentifierRecord { - functional_group_identifier: FunctionalGroupIdentifier::VODBSystem, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - format_identifier: DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04, - readiness_group_identifier: DTCReadinessGroupIdentifier::from(0x72), - record_data: vec![ - ( - DTCRecord::new(0x15, 0x17, 0x19), - DTCStatusAvailabilityMask::TestFailed - ), - ( - DTCRecord::new(0x51, 0x71, 0x91), - DTCStatusAvailabilityMask::TestFailed - ) - ] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } - - #[test] - fn report_dtc_by_readiness_group_identifier_empty_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x56, // subfunction - FunctionalGroupIdentifier::VODBSystem.into(), - DTCStatusAvailabilityMask::TestFailed.into(), - DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04.into(), - 0x72,// Readiness Group Identifier - ]; - let mut reader = &bytes[..]; - - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - assert_eq!( - response, - ReadDTCInfoResponse::DTCByReadinessGroupIdentifierList( - DTCByReadinessGroupIdentifierRecord { - functional_group_identifier: FunctionalGroupIdentifier::VODBSystem, - status_availability_mask: DTCStatusAvailabilityMask::TestFailed, - format_identifier: DTCFormatIdentifier::SAE_J2012_DA_DTCFormat_04, - readiness_group_identifier: DTCReadinessGroupIdentifier::from(0x72), - record_data: vec![] - } - ) - ); - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } -} - -#[cfg(test)] -mod ext_data { - use super::*; - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, PartialEq)] - pub enum TestDTCExtDataRecordNumber { - // DTC records - WarmUpCycleCount = 0x04, - FaultDetectionCounter = 0x05, - } - - impl WireFormat for TestDTCExtDataRecordNumber { - fn required_size(&self) -> usize { - 1 - } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(*self as u8)?; - Ok(self.required_size()) - } - } - - impl IterableWireFormat for TestDTCExtDataRecordNumber { - fn decode_next(reader: &mut T) -> Result, Error> { - let id = reader.read_u8(); - match id { - Ok(0x04) => Ok(Some(TestDTCExtDataRecordNumber::WarmUpCycleCount)), - Ok(0x05) => Ok(Some(TestDTCExtDataRecordNumber::FaultDetectionCounter)), - Err(_) => Ok(None), - _ => Err(Error::NoDataAvailable), - } - } - } - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, PartialEq)] - enum TestDTCExtData { - WarmUpCycleCount(u16), - FaultDetectionCounter(u8), - } - - impl WireFormat for TestDTCExtData { - fn required_size(&self) -> usize { - match self { - TestDTCExtData::WarmUpCycleCount(_) => 3, - TestDTCExtData::FaultDetectionCounter(_) => 2, - } - } - - fn encode(&self, writer: &mut T) -> Result { - match self { - TestDTCExtData::WarmUpCycleCount(count) => { - writer.write_u8(TestDTCExtDataRecordNumber::WarmUpCycleCount as u8)?; - writer.write_u16::(*count)?; - } - TestDTCExtData::FaultDetectionCounter(count) => { - writer.write_u8(TestDTCExtDataRecordNumber::FaultDetectionCounter as u8)?; - writer.write_u8(*count)?; - } - } - Ok(self.required_size()) - } - } - - impl IterableWireFormat for TestDTCExtData { - fn decode_next(reader: &mut T) -> Result, Error> { - let id = TestDTCExtDataRecordNumber::decode_next(reader)?; - match id { - Some(TestDTCExtDataRecordNumber::WarmUpCycleCount) => { - let count = reader.read_u16::()?; - Ok(Some(TestDTCExtData::WarmUpCycleCount(count))) - } - Some(TestDTCExtDataRecordNumber::FaultDetectionCounter) => { - let count = reader.read_u8()?; - Ok(Some(TestDTCExtData::FaultDetectionCounter(count))) - } - None => Ok(None), +impl Encode for ReadDTCInfoResponse<'_> { + fn encoded_size(&self) -> usize { + match self { + Self::NumberOfDTCs { .. } => 4, + Self::DTCList { raw_records, .. } | Self::DTCSeverityList { raw_records, .. } => { + 2 + raw_records.len() } + Self::DTCFaultDetectionCounterList { raw_records } => 1 + raw_records.len(), + Self::WWHOBDDTCByMaskRecord { raw_records, .. } => 5 + raw_records.len(), } } - #[test] - fn ext_data_list() { - // skip formatting - #[rustfmt::skip] - let bytes = [ - 0x06, // subfunction - // First DTC record - 0x12, 0x34, 0x56, // DTC Mask - 0x24, //Status - 0x04, // "WarmUpCycleCount" - //Ext data - 0xBE, 0xEF, - 0x05, // "FaultDetectionCounter" - 0x10, - - ]; - let mut reader = &bytes[..]; - let response: ReadDTCInfoResponse = - ReadDTCInfoResponse::decode(&mut reader).unwrap(); - - // write - let mut writer = Vec::new(); - let written = response.encode(&mut writer).unwrap(); - assert_eq!(writer, bytes, "Written: \n{writer:02X?}\n{bytes:02X?}"); - assert_eq!(written, bytes.len(), "Written: \n{writer:?}\n{bytes:?}"); - assert_eq!(written, response.required_size()); - } -} - -#[cfg(test)] -mod request { - use super::*; - use crate::DTCStatusMask; - - #[test] - fn test_read_dtc_information_request() { - let bytes = [0x01, 0x01]; - let mut reader = &bytes[..]; - let mut writer = Vec::new(); - ReadDTCInfoRequest::new(ReadDTCInfoSubFunction::ReportDTCStoredData_ByRecordNumber( - DTCStoredDataRecordNumber::new(5).unwrap(), - )) - .encode(&mut writer) - .unwrap(); - let request = ReadDTCInfoRequest::decode(&mut reader).unwrap(); - assert_eq!( - request, - ReadDTCInfoRequest { - dtc_subfunction: ReadDTCInfoSubFunction::ReportNumberOfDTC_ByStatusMask( - DTCStatusMask::TestFailed - ) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + match self { + Self::NumberOfDTCs { + sub_function_id, + status_availability_mask, + count, + } => { + writer + .write_all(&[*sub_function_id, status_availability_mask.bits()]) + .map_err(Error::io)?; + writer.write_all(&count.to_be_bytes()).map_err(Error::io)?; + } + Self::DTCList { + sub_function_id, + status_availability_mask, + raw_records, + } + | Self::DTCSeverityList { + sub_function_id, + status_availability_mask, + raw_records, + } => { + writer + .write_all(&[*sub_function_id, status_availability_mask.bits()]) + .map_err(Error::io)?; + writer.write_all(raw_records).map_err(Error::io)?; + } + Self::DTCFaultDetectionCounterList { raw_records } => { + writer.write_all(&[0x14]).map_err(Error::io)?; + writer.write_all(raw_records).map_err(Error::io)?; + } + Self::WWHOBDDTCByMaskRecord { + functional_group_identifier, + status_availability_mask, + severity_availability_mask, + format_identifier, + raw_records, + } => { + writer + .write_all(&[ + 0x42, + u8::from(*functional_group_identifier), + status_availability_mask.bits(), + severity_availability_mask.bits(), + u8::from(*format_identifier), + ]) + .map_err(Error::io)?; + writer.write_all(raw_records).map_err(Error::io)?; } - ); - } - - #[test] - fn test_read_dtc_information_subfunction() { - let mut writer = Vec::new(); - let b = ReadDTCInfoSubFunction::ReportDTCWithPermanentStatus; - b.encode(&mut writer).unwrap(); - - assert_eq!(writer, vec![0x15]); - - for id in 0x01..=0x07 { - let mut writer = Vec::new(); - let func = match id { - 0x01 => ReadDTCInfoSubFunction::ReportNumberOfDTC_ByStatusMask( - DTCStatusMask::TestFailed, - ), - 0x02 => ReadDTCInfoSubFunction::ReportDTC_ByStatusMask( - DTCStatusMask::WarningIndicatorRequested, - ), - 0x03 => ReadDTCInfoSubFunction::ReportDTCSnapshotIdentification, - 0x04 => ReadDTCInfoSubFunction::ReportDTCSnapshotRecord_ByDTCNumber( - DTCRecord::new(0x01, 0x02, 0x03), - DTCSnapshotRecordNumber::new(0x04), - ), - 0x05 => ReadDTCInfoSubFunction::ReportDTCStoredData_ByRecordNumber( - DTCStoredDataRecordNumber::new(0x20).unwrap(), - ), - 0x06 => ReadDTCInfoSubFunction::ReportDTCExtDataRecord_ByDTCNumber( - DTCRecord::new(0x01, 0x02, 0x03), - DTCExtDataRecordNumber::new(0x04), - ), - 0x07 => ReadDTCInfoSubFunction::ReportNumberOfDTC_BySeverityMaskRecord( - DTCSeverityMask::DTCClass_4, - DTCStatusMask::TestFailed, - ), - _ => unreachable!("Invalid loop value"), - }; - let written = func.encode(&mut writer).unwrap(); - assert_eq!(written, func.required_size()); } + Ok(self.encoded_size()) } } diff --git a/src/services/request_download.rs b/src/services/request_download.rs index 0493316..1f0ef7c 100644 --- a/src/services/request_download.rs +++ b/src/services/request_download.rs @@ -1,10 +1,10 @@ //! `RequestDownload` (0x34) service implementation -use byteorder::{ReadBytesExt, WriteBytesExt}; -use crate::{ - DataFormatIdentifier, Error, LengthFormatIdentifier, MemoryFormatIdentifier, - NegativeResponseCode, SingleValueWireFormat, WireFormat, +use crate::common::{ + DataFormatIdentifier, LengthFormatIdentifier, MemoryFormatIdentifier, read_be_uint, + write_be_uint, }; +use crate::{Decode, Encode, Error, NegativeResponseCode}; const REQUEST_DOWNLOAD_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 6] = [ NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, @@ -41,7 +41,12 @@ pub struct RequestDownloadRequest { } impl RequestDownloadRequest { - pub(crate) fn new( + /// Create a new `RequestDownloadRequest` + /// + /// # Errors + /// Returns an error if `memory_address` exceeds 5 bytes (> `0xFF_FFFF_FFFF`). + #[allow(clippy::cast_possible_truncation)] + pub fn new( data_format_identifier: DataFormatIdentifier, memory_address: u64, memory_size: u32, @@ -49,8 +54,17 @@ impl RequestDownloadRequest { if memory_address > 0xFF_FFFF_FFFF { return Err(Error::InvalidMemoryAddress(memory_address)); } - let address_and_length_format_identifier = - MemoryFormatIdentifier::from_values(memory_size, memory_address); + // A length of 0 produces an invalid `MemoryFormatIdentifier` (the nibbles + // must be >=1 per ISO-14229), so clamp to at least one byte even when the + // address or size is 0. + let memory_address_length = + ((u64::BITS - memory_address.leading_zeros()).div_ceil(8) as u8).max(1); + let memory_size_length = + ((u32::BITS - memory_size.leading_zeros()).div_ceil(8) as u8).max(1); + let address_and_length_format_identifier = MemoryFormatIdentifier { + memory_size_length, + memory_address_length, + }; Ok(Self { data_format_identifier, address_and_length_format_identifier, @@ -59,131 +73,131 @@ impl RequestDownloadRequest { }) } - fn get_shortened_memory_address(&self) -> Vec { - self.memory_address - .to_be_bytes() - .iter() - .skip( - 8 - self - .address_and_length_format_identifier - .memory_address_length as usize, - ) - .copied() - .collect() - } - - fn get_shortened_memory_size(&self) -> Vec { - self.memory_size - .to_be_bytes() - .iter() - .skip(4 - self.address_and_length_format_identifier.memory_size_length as usize) - .copied() - .collect() - } - /// Get the allowed [`NegativeResponseCode`] variants for this request #[must_use] pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { &REQUEST_DOWNLOAD_NEGATIVE_RESPONSE_CODES } } -impl WireFormat for RequestDownloadRequest { - fn required_size(&self) -> usize { +impl Encode for RequestDownloadRequest { + fn encoded_size(&self) -> usize { 2 + self.address_and_length_format_identifier.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.data_format_identifier.into())?; - writer.write_u8(self.address_and_length_format_identifier.into())?; - - writer.write_all(self.get_shortened_memory_address().as_mut_slice())?; - writer.write_all(self.get_shortened_memory_size().as_mut_slice())?; - - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[ + self.data_format_identifier.into(), + self.address_and_length_format_identifier.into(), + ]) + .map_err(Error::io)?; + + let addr_len = self + .address_and_length_format_identifier + .memory_address_length as usize; + let size_len = self.address_and_length_format_identifier.memory_size_length as usize; + write_be_uint(u128::from(self.memory_address), addr_len, writer)?; + write_be_uint(u128::from(self.memory_size), size_len, writer)?; + + Ok(self.encoded_size()) } } -impl SingleValueWireFormat for RequestDownloadRequest { - fn decode(reader: &mut T) -> Result { - let data_format_identifier = DataFormatIdentifier::from(reader.read_u8()?); - let memory_identifier = MemoryFormatIdentifier::try_from(reader.read_u8()?)?; - - let mut memory_address: Vec = vec![0; memory_identifier.memory_address_length as usize]; - let mut memory_size: Vec = vec![0; memory_identifier.memory_size_length as usize]; - - reader.read_exact(&mut memory_address)?; - reader.read_exact(&mut memory_size)?; +impl<'a> Decode<'a> for RequestDownloadRequest { + #[allow(clippy::cast_possible_truncation)] + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let data_format_identifier = DataFormatIdentifier::from(buf[0]); + let memory_identifier = MemoryFormatIdentifier::try_from(buf[1])?; + let addr_len = memory_identifier.memory_address_length as usize; + let size_len = memory_identifier.memory_size_length as usize; + let total = 2 + addr_len + size_len; + if buf.len() < total { + return Err(Error::InsufficientData(total)); + } - Ok(Self { - data_format_identifier, - address_and_length_format_identifier: memory_identifier, - memory_address: u64::from_be_bytes({ - let mut bytes = [0; 8]; - bytes[8 - memory_address.len()..].copy_from_slice(&memory_address); - bytes - }), - memory_size: u32::from_be_bytes({ - let mut bytes = [0; 4]; - bytes[4 - memory_size.len()..].copy_from_slice(&memory_size); - bytes - }), - }) + let memory_address = read_be_uint(&buf[2..], addr_len)? as u64; + let memory_size = read_be_uint(&buf[2 + addr_len..], size_len)? as u32; + + Ok(( + Self { + data_format_identifier, + address_and_length_format_identifier: memory_identifier, + memory_address, + memory_size, + }, + &buf[total..], + )) } } +/// Zero-alloc response for request download. Borrows from the caller. +/// /// Positive response to a [`RequestDownloadRequest`] indicating the server is ready to receive data. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -#[non_exhaustive] -pub struct RequestDownloadResponse { - /// Format is similar to `address_and_length_format_identifier` field of the [`RequestDownloadRequest`] struct. - /// In it is a byte with the high nibble being the length of the `max_number_of_block_length` field. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RequestDownloadResponse<'d> { length_format_identifier: LengthFormatIdentifier, - /// Maximum number of bytes to include in each [`TransferDataRequest`](crate::TransferDataRequest). - /// Variable length, determined by `length_format_identifier`. - pub max_number_of_block_length: Vec, + /// Maximum number of bytes per [`TransferDataRequest`](crate::TransferDataRequest). + pub max_number_of_block_length: &'d [u8], } -impl RequestDownloadResponse { - pub(crate) fn new(length_format_identifier: u8, max_number_of_block_length: Vec) -> Self { +impl<'d> RequestDownloadResponse<'d> { + /// Create a new request download response from a raw format byte and block length. + #[must_use] + pub fn new(length_format_byte: u8, max_number_of_block_length: &'d [u8]) -> Self { Self { - length_format_identifier: LengthFormatIdentifier::from(length_format_identifier), + length_format_identifier: LengthFormatIdentifier::from(length_format_byte), max_number_of_block_length, } } } -impl WireFormat for RequestDownloadResponse { - fn required_size(&self) -> usize { +impl Encode for RequestDownloadResponse<'_> { + fn encoded_size(&self) -> usize { 1 + self.max_number_of_block_length.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.length_format_identifier.into())?; - writer.write_all(&self.max_number_of_block_length)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.length_format_identifier.into()]) + .map_err(Error::io)?; + writer + .write_all(self.max_number_of_block_length) + .map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat for RequestDownloadResponse { - fn decode(reader: &mut T) -> Result { - let length_format_identifier = LengthFormatIdentifier::from(reader.read_u8()?); - - let mut max_number_of_block_length: Vec = - vec![0; length_format_identifier.max_number_of_block_length as usize]; - reader.read_exact(&mut max_number_of_block_length)?; - - Ok(Self { - length_format_identifier, - max_number_of_block_length, - }) +impl<'a> Decode<'a> for RequestDownloadResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let length_format_identifier = LengthFormatIdentifier::from(buf[0]); + let len = length_format_identifier.max_number_of_block_length as usize; + let total = 1 + len; + if buf.len() < total { + return Err(Error::InsufficientData(total)); + } + Ok(( + Self { + length_format_identifier, + max_number_of_block_length: &buf[1..total], + }, + &buf[total..], + )) } } #[cfg(test)] mod tests { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec; + #[test] fn simple_request() { let bytes: [u8; 7] = [ @@ -192,7 +206,7 @@ mod tests { 0xF0, 0xFF, 0xFF, 0x67, // memory address 0x0A, ]; - let req = RequestDownloadRequest::decode(&mut bytes.as_slice()).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); assert_eq!(u8::from(req.data_format_identifier), 0); assert_eq!(u8::from(req.address_and_length_format_identifier), 0x14); @@ -208,12 +222,6 @@ mod tests { assert_eq!(req.memory_address, 0xF0FF_FF67); assert_eq!(req.memory_size, 0x0A); - - assert_eq!( - req.get_shortened_memory_address(), - vec![0xF0, 0xFF, 0xFF, 0x67] - ); - assert_eq!(req.get_shortened_memory_size(), vec![0x0A]); } #[test] @@ -223,8 +231,8 @@ mod tests { 0x11, // 1 byte for memory size, 1 byte for memory address 0x67, ]; - let req = RequestDownloadRequest::decode(&mut bytes.as_slice()); - assert!(matches!(req, Err(Error::IoError(_)))); + let result = ::decode(&bytes); + assert!(result.is_err()); } #[test] @@ -244,13 +252,44 @@ mod tests { assert_eq!(u8::from(length_format_identifier), 0xF0); } + #[test] + fn zero_address_and_size_clamp_to_one_byte() { + // A 0 address/size must still produce a valid (>=1 byte) length nibble, + // otherwise the encoded frame cannot be decoded back. + let req = RequestDownloadRequest::new(0x00.into(), 0, 0).unwrap(); + assert_eq!( + req.address_and_length_format_identifier + .memory_address_length, + 1 + ); + assert_eq!( + req.address_and_length_format_identifier.memory_size_length, + 1 + ); + + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + let (decoded, _) = ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded.memory_address, 0); + assert_eq!(decoded.memory_size, 0); + } + + #[cfg(feature = "alloc")] #[test] fn check_message_size() { let req = RequestDownloadRequest::new(0x00.into(), 0xF0_FF_FF_67, 0x0A).unwrap(); let mut vec = vec![]; - req.encode(&mut vec).unwrap(); + Encode::encode(&req, &mut vec).unwrap(); - assert_eq!(vec.len(), req.required_size()); + assert_eq!(vec.len(), req.encoded_size()); + assert_encode_size_agrees(&req); + } + + #[test] + fn response_encode_size_agrees() { + let block = [0x10u8, 0x00, 0x00]; + let resp = RequestDownloadResponse::new(0x30, &block); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/request_file_transfer.rs b/src/services/request_file_transfer.rs index f09b6ca..50cc336 100644 --- a/src/services/request_file_transfer.rs +++ b/src/services/request_file_transfer.rs @@ -1,8 +1,7 @@ //! `RequestFileTransfer` (0x38) service implementation -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::Read; -use crate::{DataFormatIdentifier, Error, SingleValueWireFormat, WireFormat}; +use crate::common::{DataFormatIdentifier, read_be_uint, write_be_uint}; +use crate::{Decode, Encode, Error}; ///////////////////////////////////////// - Request - /////////////////////////////////////////////////// /// Mode of operation for file transfer requests @@ -73,12 +72,12 @@ impl TryFrom for FileOperationMode { /// [ReadFile]: FileOperationMode::ReadFile /// [ReadDir]: FileOperationMode::ReadDir /// [ResumeFile]: FileOperationMode::ResumeFile -/// [Request]: RequestFileTransferRequest (RequestFileTransferRequest) -/// [Response]: RequestFileTransferResponse (RequestFileTransferResponse) +/// [Request]: RequestFileTransferRequest +/// [Response]: RequestFileTransferResponse #[allow(clippy::struct_field_names)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct SizePayload { /// Length in bytes for both `file_size_uncompressed` and `file_size_compressed` /// @@ -108,58 +107,9 @@ pub struct SizePayload { pub file_size_compressed: u128, } -impl WireFormat for SizePayload { - fn required_size(&self) -> usize { - 1 + (2 * self.file_size_parameter_length as usize) - } - - fn encode(&self, writer: &mut T) -> Result { - // Always write the file size as 1 byte - writer.write_u8(self.file_size_parameter_length)?; - // write the file size only as many bytes as needed - // Slice off only the number of bytes we need from the end of the file_size bytes - let uncompressed = self.file_size_uncompressed.to_be_bytes(); - let compressed = self.file_size_compressed.to_be_bytes(); - // file_size_uncompressed - let mut bytes: Vec = Vec::new(); - bytes.extend_from_slice(&uncompressed[16 - self.file_size_parameter_length as usize..]); - // file_size_compressed - bytes.extend_from_slice(&compressed[16 - self.file_size_parameter_length as usize..]); - - writer.write_all(&bytes)?; - - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for SizePayload { - fn decode(reader: &mut T) -> Result { - let file_size_parameter_length = reader.read_u8()?; - let mut file_size_uncompressed = vec![0; file_size_parameter_length as usize]; - let mut file_size_compressed = vec![0; file_size_parameter_length as usize]; - - reader.read_exact(&mut file_size_uncompressed)?; - reader.read_exact(&mut file_size_compressed)?; - - Ok(Self { - file_size_parameter_length, - file_size_uncompressed: u128::from_be_bytes({ - let mut bytes = [0; 16]; - bytes[16 - file_size_parameter_length as usize..] - .copy_from_slice(&file_size_uncompressed); - bytes - }), - file_size_compressed: u128::from_be_bytes({ - let mut bytes = [0; 16]; - bytes[16 - file_size_parameter_length as usize..] - .copy_from_slice(&file_size_compressed); - bytes - }), - }) - } -} - -/// Payload used for all [`RequestFileTransfer` requests][RequestFileTransferRequest] +/// Payload used for all [`RequestFileTransferRequest`] requests. +/// +/// Borrows `file_path_and_name` from the caller. /// /// #### ***Request*** Message /// | | [AddFile] | [DeleteFile] | [ReplaceFile] | [ReadFile] | [ReadDir] | [ResumeFile] | @@ -172,57 +122,21 @@ impl SingleValueWireFormat for SizePayload { /// [ReadFile]: FileOperationMode::ReadFile /// [ReadDir]: FileOperationMode::ReadDir /// [ResumeFile]: FileOperationMode::ResumeFile -/// [Request]: RequestFileTransferRequest (RequestFileTransferRequest) +/// [Request]: RequestFileTransferRequest #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct NamePayload { +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NamePayload<'a> { /// 0x01 - 0x06, the type of operation to be applied to the file or directory specified in `file_path_and_name` - /// - /// Duplicated as we need to read and store it somewhere - mode_of_operation: FileOperationMode, + pub mode_of_operation: FileOperationMode, /// Length in bytes of the `file_path_and_name` field - file_path_and_name_length: u16, + pub file_path_and_name_length: u16, /// The path and name of the file or directory on the server - file_path_and_name: String, -} - -impl WireFormat for NamePayload { - fn required_size(&self) -> usize { - 1 + 2 + self.file_path_and_name.len() - } - - fn encode(&self, writer: &mut T) -> Result { - // Write the mode of operation - writer.write_u8((self.mode_of_operation).into())?; - // Write the file path and name length - writer.write_u16::(self.file_path_and_name_length)?; - // Write the file path and name - writer.write_all(self.file_path_and_name.as_bytes())?; - Ok(self.required_size()) - } + pub file_path_and_name: &'a str, } -impl SingleValueWireFormat for NamePayload { - fn decode(reader: &mut T) -> Result { - let mode_of_operation = FileOperationMode::try_from(reader.read_u8()?)?; - let file_path_and_name_length = reader.read_u16::()?; - - // Read # of bytes specified by `file_path_and_name_length` - let mut file_path_and_name = String::new(); - reader - .take(u64::from(file_path_and_name_length)) - .read_to_string(&mut file_path_and_name)?; - - Ok(Self { - mode_of_operation, - file_path_and_name_length, - file_path_and_name, - }) - } -} /// A request to the server to transfer a file, either upload or download. /// /// Capabilities: @@ -238,107 +152,43 @@ impl SingleValueWireFormat for NamePayload { /// there is no need to use the `TransferData` or [`crate::UdsServiceType::RequestTransferExit`] services. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] -pub enum RequestFileTransferRequest { +pub enum RequestFileTransferRequest<'a> { /// Add a file to the server - AddFile(NamePayload, DataFormatIdentifier, SizePayload), + AddFile( + #[cfg_attr(feature = "serde", serde(borrow))] NamePayload<'a>, + DataFormatIdentifier, + SizePayload, + ), /// Delete the specified file from the server - DeleteFile(NamePayload), + DeleteFile(#[cfg_attr(feature = "serde", serde(borrow))] NamePayload<'a>), /// Replace the specified file on the server, if it does not exist, add it - ReplaceFile(NamePayload, DataFormatIdentifier, SizePayload), + ReplaceFile( + #[cfg_attr(feature = "serde", serde(borrow))] NamePayload<'a>, + DataFormatIdentifier, + SizePayload, + ), /// Read the specified file from the server (upload) - ReadFile(NamePayload, DataFormatIdentifier), + ReadFile( + #[cfg_attr(feature = "serde", serde(borrow))] NamePayload<'a>, + DataFormatIdentifier, + ), /// Read the directory from the server /// Implies that the request does not include a `fileName` - ReadDir(NamePayload), + ReadDir(#[cfg_attr(feature = "serde", serde(borrow))] NamePayload<'a>), /// Resume a file transfer at the returned `filePosition` indicator /// The file must already exist in the ECU's filesystem - ResumeFile(NamePayload, DataFormatIdentifier, SizePayload), -} - -impl WireFormat for RequestFileTransferRequest { - fn required_size(&self) -> usize { - match self { - Self::AddFile(name_payload, data_format_identifier, file_size_payload) - | Self::ReplaceFile(name_payload, data_format_identifier, file_size_payload) - | Self::ResumeFile(name_payload, data_format_identifier, file_size_payload) => { - name_payload.required_size() - + data_format_identifier.required_size() - + file_size_payload.required_size() - } - Self::ReadFile(name_payload, data_format_identifier) => { - name_payload.required_size() + data_format_identifier.required_size() - } - Self::DeleteFile(name_payload) | Self::ReadDir(name_payload) => { - name_payload.required_size() - } - } - } - - fn encode(&self, writer: &mut T) -> Result { - let mut len = 0; - Ok(match self { - Self::AddFile(name_payload, data_format_identifier, file_size_payload) - | Self::ReplaceFile(name_payload, data_format_identifier, file_size_payload) - | Self::ResumeFile(name_payload, data_format_identifier, file_size_payload) => { - len += name_payload.encode(writer)?; - len += data_format_identifier.encode(writer)?; - len += file_size_payload.encode(writer)?; - len - } - Self::ReadFile(name_payload, data_format_identifier) => { - len += name_payload.encode(writer)?; - len += data_format_identifier.encode(writer)?; - len - } - Self::DeleteFile(name_payload) | Self::ReadDir(name_payload) => { - len += name_payload.encode(writer)?; - len - } - }) - } -} - -impl SingleValueWireFormat for RequestFileTransferRequest { - fn decode(reader: &mut T) -> Result { - let name_payload = NamePayload::decode(reader)?; - - // read the filename - Ok(match name_payload.mode_of_operation { - // Complicated - FileOperationMode::AddFile => Self::AddFile( - name_payload, - DataFormatIdentifier::decode(reader)?, - SizePayload::decode(reader)?, - ), - FileOperationMode::ReplaceFile => Self::ReplaceFile( - name_payload, - DataFormatIdentifier::decode(reader)?, - SizePayload::decode(reader)?, - ), - FileOperationMode::ResumeFile => Self::ResumeFile( - name_payload, - DataFormatIdentifier::decode(reader)?, - SizePayload::decode(reader)?, - ), - FileOperationMode::ReadFile => { - Self::ReadFile(name_payload, DataFormatIdentifier::decode(reader)?) - } - FileOperationMode::ReadDir => Self::ReadDir(name_payload), - FileOperationMode::DeleteFile => Self::DeleteFile(name_payload), - FileOperationMode::ISOSAEReserved(_) => { - return Err(Error::InvalidFileOperationMode( - name_payload.mode_of_operation.into(), - )); - } - }) - } + ResumeFile( + #[cfg_attr(feature = "serde", serde(borrow))] NamePayload<'a>, + DataFormatIdentifier, + SizePayload, + ), } ///////////////////////////////////////// - Response - /////////////////////////////////////////////////// @@ -355,13 +205,13 @@ impl SingleValueWireFormat for RequestFileTransferRequest { /// [ReadFile]: FileOperationMode::ReadFile /// [ReadDir]: FileOperationMode::ReadDir /// [ResumeFile]: FileOperationMode::ResumeFile -/// [Response]: RequestFileTransferRequest (RequestFileTransferResponse) +/// [Response]: RequestFileTransferResponse #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -pub struct SentDataPayload { +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SentDataPayload<'a> { /// Not related to `RequestDownload` - length_format_identifier: u8, + pub length_format_identifier: u8, /// This parameter is used by the requestFileTransfer positive response message to inform the client how many /// data bytes (maxNumberOfBlockLength) to include in each `TransferData` request message from the client or how /// many data bytes the server will include in a `TransferData` positive response when uploading data. This length @@ -378,32 +228,7 @@ pub struct SentDataPayload { /// affect the memory address of where the subsequent transferData request data would be written. /// If the modeOfOperation parameter equals to 0x02 (`DeleteFile`) this parameter shall be not be included in the /// response message. - pub max_number_of_block_length: Vec, -} - -impl WireFormat for SentDataPayload { - fn required_size(&self) -> usize { - 1 + self.max_number_of_block_length.len() - } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.length_format_identifier)?; - writer.write_all(&self.max_number_of_block_length)?; - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for SentDataPayload { - fn decode(reader: &mut T) -> Result { - let length_format_identifier = reader.read_u8()?; - - let mut max_number_of_block_length: Vec = vec![0; length_format_identifier as usize]; - reader.read_exact(&mut max_number_of_block_length)?; - Ok(Self { - length_format_identifier, - max_number_of_block_length, - }) - } + pub max_number_of_block_length: &'a [u8], } /// Used to inform the client of the size of the file to be transferred @@ -418,11 +243,11 @@ impl SingleValueWireFormat for SentDataPayload { /// [ReadFile]: FileOperationMode::ReadFile /// [ReadDir]: FileOperationMode::ReadDir /// [ResumeFile]: FileOperationMode::ResumeFile -/// [Response]: RequestFileTransferRequest (RequestFileTransferResponse) +/// [Response]: RequestFileTransferResponse #[allow(clippy::struct_field_names)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct FileSizePayload { /// Length in bytes of both `file_size_uncompressed` and `file_size_compressed`. pub file_size_parameter_length: u16, @@ -432,58 +257,6 @@ pub struct FileSizePayload { pub file_size_compressed: u128, } -impl WireFormat for FileSizePayload { - fn required_size(&self) -> usize { - 2 + (2 * self.file_size_parameter_length as usize) - } - - fn encode(&self, writer: &mut T) -> Result { - // Always write the file size as 1 byte - - writer.write_u16::(self.file_size_parameter_length)?; - // write the file size only as many bytes as needed - // Slice off only the number of bytes we need from the end of the file_size bytes - let uncompressed = self.file_size_uncompressed.to_be_bytes(); - let compressed = self.file_size_compressed.to_be_bytes(); - // file_size_uncompressed - let mut bytes: Vec = Vec::new(); - bytes.extend_from_slice(&uncompressed[16 - self.file_size_parameter_length as usize..]); - // file_size_compressed - bytes.extend_from_slice(&compressed[16 - self.file_size_parameter_length as usize..]); - - writer.write_all(&bytes)?; - - Ok(self.required_size()) - } -} - -impl SingleValueWireFormat for FileSizePayload { - fn decode(reader: &mut T) -> Result { - let file_size_parameter_length = reader.read_u16::()?; - let mut file_size_uncompressed = vec![0; file_size_parameter_length as usize]; - let mut file_size_compressed = vec![0; file_size_parameter_length as usize]; - - reader.read_exact(&mut file_size_uncompressed)?; - reader.read_exact(&mut file_size_compressed)?; - - Ok(Self { - file_size_parameter_length, - file_size_uncompressed: u128::from_be_bytes({ - let mut bytes = [0; 16]; - bytes[16 - file_size_parameter_length as usize..] - .copy_from_slice(&file_size_uncompressed); - bytes - }), - file_size_compressed: u128::from_be_bytes({ - let mut bytes = [0; 16]; - bytes[16 - file_size_parameter_length as usize..] - .copy_from_slice(&file_size_compressed); - bytes - }), - }) - } -} - /// Used to inform the client of the size of the directory to be transferred /// /// | | [AddFile] | [DeleteFile] | [ReplaceFile] | [ReadFile] | [ReadDir] | [ResumeFile] | @@ -496,10 +269,10 @@ impl SingleValueWireFormat for FileSizePayload { /// [ReadFile]: FileOperationMode::ReadFile /// [ReadDir]: FileOperationMode::ReadDir /// [ResumeFile]: FileOperationMode::ResumeFile -/// [Response]: RequestFileTransferRequest (RequestFileTransferResponse) +/// [Response]: RequestFileTransferResponse #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct DirSizePayload { /// Length in bytes of the `dir_info_length` field. pub dir_info_parameter_length: u16, @@ -507,46 +280,6 @@ pub struct DirSizePayload { pub dir_info_length: u128, } -impl WireFormat for DirSizePayload { - fn required_size(&self) -> usize { - 2 + self.dir_info_parameter_length as usize - } - - fn encode(&self, writer: &mut T) -> Result { - let mut len = 0; - writer.write_u16::(self.dir_info_parameter_length)?; - len += 2; - // write the file size only as many bytes as needed - // Slice off only the number of bytes we need from the end of the file_size bytes - let dir_info_length = self.dir_info_length.to_be_bytes(); - let mut bytes: Vec = Vec::new(); - - bytes.extend_from_slice(&dir_info_length[16 - self.dir_info_parameter_length as usize..]); - writer.write_all(&bytes)?; - - len += bytes.len(); - - Ok(len) - } -} - -impl SingleValueWireFormat for DirSizePayload { - fn decode(reader: &mut T) -> Result { - let dir_info_parameter_length = reader.read_u16::()?; - let mut dir_info_length = vec![0; dir_info_parameter_length as usize]; - reader.read_exact(&mut dir_info_length)?; - - Ok(Self { - dir_info_parameter_length, - dir_info_length: u128::from_be_bytes({ - let mut bytes = [0; 16]; - bytes[16 - dir_info_parameter_length as usize..].copy_from_slice(&dir_info_length); - bytes - }), - }) - } -} - /// Used to inform the client of the byte position within the file at which the Tester will resume downloading after an initial download is suspended /// /// | | [AddFile] | [DeleteFile] | [ReplaceFile] | [ReadFile] | [ReadDir] | [ResumeFile] | @@ -559,7 +292,7 @@ impl SingleValueWireFormat for DirSizePayload { /// [ReadFile]: FileOperationMode::ReadFile /// [ReadDir]: FileOperationMode::ReadDir /// [ResumeFile]: FileOperationMode::ResumeFile -/// [Response]: RequestFileTransferRequest (RequestFileTransferResponse) +/// [Response]: RequestFileTransferResponse #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[derive(Clone, Copy, Debug, PartialEq)] @@ -575,386 +308,459 @@ pub struct PositionPayload { pub file_position: u64, } -impl WireFormat for PositionPayload { - // For PositionPayload - fn required_size(&self) -> usize { - 8 - } - - fn encode(&self, writer: &mut T) -> Result { - writer.write_u64::(self.file_position)?; - Ok(8) - } -} - -impl SingleValueWireFormat for PositionPayload { - fn decode(reader: &mut T) -> Result { - Ok(Self { - file_position: reader.read_u64::()?, - }) - } -} - /// Response to a [`RequestFileTransferRequest`] from the server /// /// The server will respond with a [`RequestFileTransferResponse`] to indicate the status of the request /// `DataFormatIdentifier` - Echoes the value of the request #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] -pub enum RequestFileTransferResponse { +pub enum RequestFileTransferResponse<'a> { /// Positive response to an [`AddFile`](FileOperationMode::AddFile) request. - AddFile(FileOperationMode, SentDataPayload, DataFormatIdentifier), + AddFile( + FileOperationMode, + #[cfg_attr(feature = "serde", serde(borrow))] SentDataPayload<'a>, + DataFormatIdentifier, + ), /// Positive response to a [`DeleteFile`](FileOperationMode::DeleteFile) request. DeleteFile(FileOperationMode), /// Positive response to a [`ReplaceFile`](FileOperationMode::ReplaceFile) request. - ReplaceFile(FileOperationMode, SentDataPayload, DataFormatIdentifier), + ReplaceFile( + FileOperationMode, + #[cfg_attr(feature = "serde", serde(borrow))] SentDataPayload<'a>, + DataFormatIdentifier, + ), /// Positive response to a [`ReadFile`](FileOperationMode::ReadFile) request, including file size. ReadFile( FileOperationMode, - SentDataPayload, + #[cfg_attr(feature = "serde", serde(borrow))] SentDataPayload<'a>, DataFormatIdentifier, FileSizePayload, ), /// Positive response to a [`ReadDir`](FileOperationMode::ReadDir) request, including directory size. ReadDir( FileOperationMode, - SentDataPayload, + #[cfg_attr(feature = "serde", serde(borrow))] SentDataPayload<'a>, DataFormatIdentifier, DirSizePayload, ), /// Positive response to a [`ResumeFile`](FileOperationMode::ResumeFile) request, including file position. ResumeFile( FileOperationMode, - SentDataPayload, + #[cfg_attr(feature = "serde", serde(borrow))] SentDataPayload<'a>, DataFormatIdentifier, PositionPayload, ), } -impl WireFormat for RequestFileTransferResponse { - // For RequestFileTransferResponse - fn required_size(&self) -> usize { - match self { - Self::AddFile(_, sent_data, data_format) - | Self::ReplaceFile(_, sent_data, data_format) => { - 1 + sent_data.required_size() + data_format.required_size() - } - Self::DeleteFile(_) => 1, - Self::ReadFile(_, sent_data, data_format, file_size) => { - 1 + sent_data.required_size() - + data_format.required_size() - + file_size.required_size() - } - Self::ReadDir(_, sent_data, data_format, dir_size) => { - 1 + sent_data.required_size() - + data_format.required_size() - + dir_size.required_size() - } - Self::ResumeFile(_, sent_data, data_format, position) => { - 1 + sent_data.required_size() - + data_format.required_size() - + position.required_size() - } - } +// --------------------------------------------------------------------------- +// Encode / Decode impls +// --------------------------------------------------------------------------- + +impl Encode for NamePayload<'_> { + fn encoded_size(&self) -> usize { + 1 + 2 + self.file_path_and_name.len() } - fn encode(&self, writer: &mut T) -> Result { - // Every variant has a mode of operation - let mut len = 1; - match self { - Self::AddFile(mode_of_operation, sent_data_payload, data_format_identifier) - | Self::ReplaceFile(mode_of_operation, sent_data_payload, data_format_identifier) => { - writer.write_u8((*mode_of_operation).into())?; - len += sent_data_payload.encode(writer)?; - len += data_format_identifier.encode(writer)?; - } - Self::DeleteFile(mode_of_operation) => { - writer.write_u8((*mode_of_operation).into())?; - } - Self::ReadFile( - mode_of_operation, - sent_data_payload, - data_format_identifier, - size_payload, - ) => { - writer.write_u8((*mode_of_operation).into())?; - len += sent_data_payload.encode(writer)?; - len += data_format_identifier.encode(writer)?; - len += size_payload.encode(writer)?; - } - Self::ReadDir( - mode_of_operation, - sent_data_payload, - data_format_identifier, - dir_size_payload, - ) => { - writer.write_u8((*mode_of_operation).into())?; - len += sent_data_payload.encode(writer)?; - len += data_format_identifier.encode(writer)?; - len += dir_size_payload.encode(writer)?; - } - Self::ResumeFile( + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.mode_of_operation)]) + .map_err(Error::io)?; + writer + .write_all(&self.file_path_and_name_length.to_be_bytes()) + .map_err(Error::io)?; + writer + .write_all(self.file_path_and_name.as_bytes()) + .map_err(Error::io)?; + Ok(self.encoded_size()) + } +} + +impl<'a> Decode<'a> for NamePayload<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 3 { + return Err(Error::InsufficientData(3)); + } + let mode_of_operation = FileOperationMode::try_from(buf[0])?; + let file_path_and_name_length = u16::from_be_bytes([buf[1], buf[2]]); + let name_len = file_path_and_name_length as usize; + let total = 3 + name_len; + if buf.len() < total { + return Err(Error::InsufficientData(total)); + } + let file_path_and_name = core::str::from_utf8(&buf[3..total]) + .map_err(|_| Error::IncorrectMessageLengthOrInvalidFormat)?; + Ok(( + Self { mode_of_operation, - sent_data_payload, - data_format_identifier, - position_payload, - ) => { - writer.write_u8((*mode_of_operation).into())?; - len += sent_data_payload.encode(writer)?; - len += data_format_identifier.encode(writer)?; - len += position_payload.encode(writer)?; - } + file_path_and_name_length, + file_path_and_name, + }, + &buf[total..], + )) + } +} + +impl Encode for SizePayload { + fn encoded_size(&self) -> usize { + 1 + 2 * self.file_size_parameter_length as usize + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + let n = self.file_size_parameter_length as usize; + writer + .write_all(&[self.file_size_parameter_length]) + .map_err(Error::io)?; + write_be_uint(self.file_size_uncompressed, n, writer)?; + write_be_uint(self.file_size_compressed, n, writer)?; + Ok(self.encoded_size()) + } +} + +impl<'a> Decode<'a> for SizePayload { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); } - Ok(len) + let file_size_parameter_length = buf[0]; + let n = file_size_parameter_length as usize; + let total = 1 + 2 * n; + if buf.len() < total { + return Err(Error::InsufficientData(total)); + } + let file_size_uncompressed = read_be_uint(&buf[1..], n)?; + let file_size_compressed = read_be_uint(&buf[1 + n..], n)?; + Ok(( + Self { + file_size_parameter_length, + file_size_uncompressed, + file_size_compressed, + }, + &buf[total..], + )) } } -impl SingleValueWireFormat for RequestFileTransferResponse { - fn decode(reader: &mut T) -> Result { - // Read the mode of operation - let mode_of_operation = FileOperationMode::try_from(reader.read_u8()?)?; - Ok(match mode_of_operation { - FileOperationMode::AddFile => Self::AddFile( - mode_of_operation, - SentDataPayload::decode(reader)?, - DataFormatIdentifier::decode(reader)?, - ), - FileOperationMode::DeleteFile => Self::DeleteFile(mode_of_operation), - FileOperationMode::ReplaceFile => Self::ReplaceFile( - mode_of_operation, - SentDataPayload::decode(reader)?, - DataFormatIdentifier::decode(reader)?, - ), - FileOperationMode::ReadFile => Self::ReadFile( - mode_of_operation, - SentDataPayload::decode(reader)?, - DataFormatIdentifier::decode(reader)?, - FileSizePayload::decode(reader)?, - ), - FileOperationMode::ReadDir => Self::ReadDir( - mode_of_operation, - SentDataPayload::decode(reader)?, - DataFormatIdentifier::decode(reader)?, - DirSizePayload::decode(reader)?, - ), - FileOperationMode::ResumeFile => Self::ResumeFile( - mode_of_operation, - SentDataPayload::decode(reader)?, - DataFormatIdentifier::decode(reader)?, - PositionPayload::decode(reader)?, - ), - FileOperationMode::ISOSAEReserved(_) => { - return Err(Error::InvalidFileOperationMode(mode_of_operation.into())); - } - }) +impl Encode for SentDataPayload<'_> { + fn encoded_size(&self) -> usize { + 1 + self.max_number_of_block_length.len() + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.length_format_identifier]) + .map_err(Error::io)?; + writer + .write_all(self.max_number_of_block_length) + .map_err(Error::io)?; + Ok(self.encoded_size()) } } -#[cfg(test)] -mod request_tests { - use super::*; - use crate::param_length_u128; - - // helper function to get some bytes to read from - #[allow(clippy::cast_possible_truncation)] - fn get_bytes(mode: FileOperationMode, file_name: &str, file_size: u128) -> Vec { - let mut bytes: Vec = Vec::new(); - bytes.push(mode.into()); // AddFile (u8) - // write file_name len as 2 bytes - bytes - .write_u16::(file_name.len() as u16) - .unwrap(); - bytes.extend_from_slice(file_name.as_bytes()); - - if mode != FileOperationMode::DeleteFile && mode != FileOperationMode::ReadDir { - bytes.push(0x00); // No compression or encryption (u8) +impl<'a> Decode<'a> for SentDataPayload<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); } - // only add file size if not DeleteFile, ReadDir, or ReadFile - if mode != FileOperationMode::DeleteFile - && mode != FileOperationMode::ReadDir - && mode != FileOperationMode::ReadFile - { - // count the number of bytes occupied by the file size - let num = param_length_u128(file_size) as u8; - // use write exact - bytes.write_u8(num).unwrap(); - // write the file size only as many bytes as needed - // Slice off only the number of bytes we need from the end of the file_size bytes - let source = file_size.to_be_bytes(); - // file_size_uncompressed - bytes.extend_from_slice(&source[16 - num as usize..]); - // file_size_compressed - bytes.extend_from_slice(&source[16 - num as usize..]); + let length_format_identifier = buf[0]; + let n = length_format_identifier as usize; + let total = 1 + n; + if buf.len() < total { + return Err(Error::InsufficientData(total)); } - bytes + Ok(( + Self { + length_format_identifier, + max_number_of_block_length: &buf[1..total], + }, + &buf[total..], + )) } +} - #[test] - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_lossless)] - fn add_file() { - let compare_string = "test.txt"; - let file_size: u128 = (u64::MAX as u128) + 1000u128; - let bytes = get_bytes(FileOperationMode::AddFile, compare_string, file_size); - let req: crate::RequestFileTransferRequest = - RequestFileTransferRequest::decode(&mut bytes.as_slice()).unwrap(); - - let mut written_bytes = Vec::new(); - let written = req.encode(&mut written_bytes).unwrap(); - assert_eq!(written, written_bytes.len()); - assert_eq!(written, req.required_size()); - - match req { - RequestFileTransferRequest::AddFile(pl, data_format_pl, file_size_pl) => { - assert_eq!(pl.mode_of_operation, FileOperationMode::AddFile); - assert_eq!(pl.file_path_and_name_length, compare_string.len() as u16); - assert_eq!(pl.file_path_and_name, compare_string); - assert_eq!(data_format_pl, DataFormatIdentifier::new(0, 0).unwrap()); - assert_eq!(file_size_pl.file_size_parameter_length, 9); - assert_eq!(file_size_pl.file_size_uncompressed, file_size); - assert_eq!(file_size_pl.file_size_compressed, file_size); - } - _ => panic!("Expected AddFile"), +impl Encode for FileSizePayload { + fn encoded_size(&self) -> usize { + 2 + 2 * self.file_size_parameter_length as usize + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + let n = self.file_size_parameter_length as usize; + writer + .write_all(&self.file_size_parameter_length.to_be_bytes()) + .map_err(Error::io)?; + write_be_uint(self.file_size_uncompressed, n, writer)?; + write_be_uint(self.file_size_compressed, n, writer)?; + Ok(self.encoded_size()) + } +} + +impl<'a> Decode<'a> for FileSizePayload { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let file_size_parameter_length = u16::from_be_bytes([buf[0], buf[1]]); + let n = file_size_parameter_length as usize; + let total = 2 + 2 * n; + if buf.len() < total { + return Err(Error::InsufficientData(total)); } + let file_size_uncompressed = read_be_uint(&buf[2..], n)?; + let file_size_compressed = read_be_uint(&buf[2 + n..], n)?; + Ok(( + Self { + file_size_parameter_length, + file_size_uncompressed, + file_size_compressed, + }, + &buf[total..], + )) } +} - #[test] - #[allow(clippy::cast_possible_truncation)] - fn delete_file() { - let compare_string = "/var/tmp/delete_file.bin"; - let bytes = get_bytes(FileOperationMode::DeleteFile, compare_string, 0); - let req = RequestFileTransferRequest::decode(&mut bytes.as_slice()).unwrap(); - - let mut written_bytes = Vec::new(); - let written = req.encode(&mut written_bytes).unwrap(); - assert_eq!(written, written_bytes.len()); - assert_eq!(written, req.required_size()); - - match req { - RequestFileTransferRequest::DeleteFile(pl) => { - assert_eq!(pl.mode_of_operation, FileOperationMode::DeleteFile); - assert_eq!(pl.file_path_and_name_length, compare_string.len() as u16); - assert_eq!(pl.file_path_and_name, compare_string); - } - _ => panic!("Expected DeleteFile"), +impl Encode for DirSizePayload { + fn encoded_size(&self) -> usize { + 2 + self.dir_info_parameter_length as usize + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + let n = self.dir_info_parameter_length as usize; + writer + .write_all(&self.dir_info_parameter_length.to_be_bytes()) + .map_err(Error::io)?; + write_be_uint(self.dir_info_length, n, writer)?; + Ok(self.encoded_size()) + } +} + +impl<'a> Decode<'a> for DirSizePayload { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let dir_info_parameter_length = u16::from_be_bytes([buf[0], buf[1]]); + let n = dir_info_parameter_length as usize; + let total = 2 + n; + if buf.len() < total { + return Err(Error::InsufficientData(total)); } + let dir_info_length = read_be_uint(&buf[2..], n)?; + Ok(( + Self { + dir_info_parameter_length, + dir_info_length, + }, + &buf[total..], + )) } +} - #[test] - #[allow(clippy::cast_possible_truncation)] - fn write_add_file() { - let compare_string = "test.txt"; - let file_size: u128 = 0x1234; - let bytes = get_bytes(FileOperationMode::AddFile, compare_string, file_size); - let req = RequestFileTransferRequest::decode(&mut bytes.as_slice()).unwrap(); - - let mut bytes = Vec::new(); - let written = req.encode(&mut bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(written, req.required_size()); - - // Should be equivalent to our helper function - let expected_bytes = get_bytes(FileOperationMode::AddFile, compare_string, file_size); - assert_eq!(bytes, expected_bytes); +impl Encode for PositionPayload { + fn encoded_size(&self) -> usize { + 8 } - #[test] - #[allow(clippy::cast_possible_truncation)] - fn write_delete_file() { - let compare_string = "/var/tmp/delete_file.bin"; - let req = RequestFileTransferRequest::DeleteFile(NamePayload { - mode_of_operation: FileOperationMode::DeleteFile, - file_path_and_name_length: compare_string.len() as u16, - file_path_and_name: compare_string.to_string(), - }); - let mut bytes = Vec::new(); - let written = req.encode(&mut bytes).unwrap(); - // Should be equivalent to our helper function - let expected_bytes = get_bytes(FileOperationMode::DeleteFile, compare_string, 0); - assert_eq!(bytes, expected_bytes); - assert_eq!(bytes.len(), written); - assert_eq!(req.required_size(), written); + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&self.file_position.to_be_bytes()) + .map_err(Error::io)?; + Ok(8) } +} - #[test] - #[allow(clippy::cast_possible_truncation)] - fn replace_file() { - let compare_string = "/opt/testing/replace_file.bin"; - let file_size: u128 = 0x1234; - let bytes = get_bytes(FileOperationMode::ReplaceFile, compare_string, file_size); - let req = RequestFileTransferRequest::decode(&mut bytes.as_slice()).unwrap(); - - let mut written_bytes = Vec::new(); - let written = req.encode(&mut written_bytes).unwrap(); - assert_eq!(written, written_bytes.len()); - assert_eq!(written, req.required_size()); - - match req { - RequestFileTransferRequest::ReplaceFile(pl, data_format_pl, file_size_pl) => { - assert_eq!(pl.mode_of_operation, FileOperationMode::ReplaceFile); - assert_eq!(pl.file_path_and_name_length, compare_string.len() as u16); - assert_eq!(pl.file_path_and_name, compare_string); - assert_eq!(data_format_pl, DataFormatIdentifier::new(0, 0).unwrap()); - assert_eq!(file_size_pl.file_size_parameter_length, 2); - assert_eq!(file_size_pl.file_size_uncompressed, file_size); - assert_eq!(file_size_pl.file_size_compressed, file_size); +impl<'a> Decode<'a> for PositionPayload { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 8 { + return Err(Error::InsufficientData(8)); + } + let file_position = u64::from_be_bytes([ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], + ]); + Ok((Self { file_position }, &buf[8..])) + } +} + +impl Encode for RequestFileTransferRequest<'_> { + fn encoded_size(&self) -> usize { + match self { + Self::AddFile(name, _, size) + | Self::ReplaceFile(name, _, size) + | Self::ResumeFile(name, _, size) => name.encoded_size() + 1 + size.encoded_size(), + Self::ReadFile(name, _) => name.encoded_size() + 1, + Self::DeleteFile(name) | Self::ReadDir(name) => name.encoded_size(), + } + } + + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + let mut len; + match self { + Self::AddFile(name, dfi, size) + | Self::ReplaceFile(name, dfi, size) + | Self::ResumeFile(name, dfi, size) => { + len = name.encode(writer)?; + writer.write_all(&[u8::from(*dfi)]).map_err(Error::io)?; + len += 1; + len += size.encode(writer)?; + } + Self::ReadFile(name, dfi) => { + len = name.encode(writer)?; + writer.write_all(&[u8::from(*dfi)]).map_err(Error::io)?; + len += 1; + } + Self::DeleteFile(name) | Self::ReadDir(name) => { + len = name.encode(writer)?; } - _ => panic!("Expected ReplaceFile"), } + Ok(len) } +} - #[test] - #[allow(clippy::cast_possible_truncation)] - fn read_file() { - let compare_string = "/opt/testing/just_reading_stuff.txt"; - let file_size: u128 = 0x0; - let bytes = get_bytes(FileOperationMode::ReadFile, compare_string, file_size); - let req = RequestFileTransferRequest::decode(&mut bytes.as_slice()).unwrap(); - - let mut written_bytes = Vec::new(); - let written = req.encode(&mut written_bytes).unwrap(); - assert_eq!(written, written_bytes.len()); - assert_eq!(written, req.required_size()); - - match req { - RequestFileTransferRequest::ReadFile(pl, data_format_pl) => { - assert_eq!(pl.mode_of_operation, FileOperationMode::ReadFile); - assert_eq!(pl.file_path_and_name_length, compare_string.len() as u16); - assert_eq!(pl.file_path_and_name, compare_string); - assert_eq!(data_format_pl, DataFormatIdentifier::new(0, 0).unwrap()); +impl<'a> Decode<'a> for RequestFileTransferRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + let (name, rest) = NamePayload::decode(buf)?; + match name.mode_of_operation { + FileOperationMode::DeleteFile => Ok((Self::DeleteFile(name), rest)), + FileOperationMode::ReadDir => Ok((Self::ReadDir(name), rest)), + FileOperationMode::ReadFile => { + if rest.is_empty() { + return Err(Error::InsufficientData(1)); + } + let dfi = DataFormatIdentifier::from(rest[0]); + Ok((Self::ReadFile(name, dfi), &rest[1..])) + } + mode @ (FileOperationMode::AddFile + | FileOperationMode::ReplaceFile + | FileOperationMode::ResumeFile) => { + if rest.is_empty() { + return Err(Error::InsufficientData(1)); + } + let dfi = DataFormatIdentifier::from(rest[0]); + let (size, rest) = SizePayload::decode(&rest[1..])?; + let value = match mode { + FileOperationMode::AddFile => Self::AddFile(name, dfi, size), + FileOperationMode::ReplaceFile => Self::ReplaceFile(name, dfi, size), + FileOperationMode::ResumeFile => Self::ResumeFile(name, dfi, size), + _ => unreachable!(), + }; + Ok((value, rest)) } - _ => panic!("Expected ReadFile"), + FileOperationMode::ISOSAEReserved(b) => Err(Error::InvalidFileOperationMode(b)), } } +} - #[test] - #[allow(clippy::cast_possible_truncation)] - fn resume_file() { - let compare_string = "/var/tmp/resume_file.bin"; - let file_size = 0x1234; - let bytes = get_bytes(FileOperationMode::ResumeFile, compare_string, file_size); - let req = RequestFileTransferRequest::decode(&mut bytes.as_slice()).unwrap(); - let mut written_bytes = Vec::new(); - let written = req.encode(&mut written_bytes).unwrap(); - assert_eq!(written, written_bytes.len()); - assert_eq!(written, req.required_size()); - - match req { - RequestFileTransferRequest::ResumeFile(pl, data_format_pl, file_size_pl) => { - assert_eq!(pl.mode_of_operation, FileOperationMode::ResumeFile); - assert_eq!(pl.file_path_and_name_length, compare_string.len() as u16); - assert_eq!(pl.file_path_and_name, compare_string); - assert_eq!(data_format_pl, DataFormatIdentifier::new(0, 0).unwrap()); - assert_eq!(file_size_pl.file_size_parameter_length, 2); - assert_eq!(file_size_pl.file_size_uncompressed, file_size); - assert_eq!(file_size_pl.file_size_compressed, file_size); +impl Encode for RequestFileTransferResponse<'_> { + fn encoded_size(&self) -> usize { + match self { + Self::DeleteFile(_) => 1, + Self::AddFile(_, sent, _) | Self::ReplaceFile(_, sent, _) => { + 1 + sent.encoded_size() + 1 } - _ => panic!("Expected ResumeFile"), + Self::ReadFile(_, sent, _, fs) => 1 + sent.encoded_size() + 1 + fs.encoded_size(), + Self::ReadDir(_, sent, _, ds) => 1 + sent.encoded_size() + 1 + ds.encoded_size(), + Self::ResumeFile(_, sent, _, pos) => 1 + sent.encoded_size() + 1 + pos.encoded_size(), } } + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + let mut len = 1; + match self { + Self::DeleteFile(mode) => { + writer.write_all(&[u8::from(*mode)]).map_err(Error::io)?; + } + Self::AddFile(mode, sent, dfi) | Self::ReplaceFile(mode, sent, dfi) => { + writer.write_all(&[u8::from(*mode)]).map_err(Error::io)?; + len += sent.encode(writer)?; + writer.write_all(&[u8::from(*dfi)]).map_err(Error::io)?; + len += 1; + } + Self::ReadFile(mode, sent, dfi, fs) => { + writer.write_all(&[u8::from(*mode)]).map_err(Error::io)?; + len += sent.encode(writer)?; + writer.write_all(&[u8::from(*dfi)]).map_err(Error::io)?; + len += 1; + len += fs.encode(writer)?; + } + Self::ReadDir(mode, sent, dfi, ds) => { + writer.write_all(&[u8::from(*mode)]).map_err(Error::io)?; + len += sent.encode(writer)?; + writer.write_all(&[u8::from(*dfi)]).map_err(Error::io)?; + len += 1; + len += ds.encode(writer)?; + } + Self::ResumeFile(mode, sent, dfi, pos) => { + writer.write_all(&[u8::from(*mode)]).map_err(Error::io)?; + len += sent.encode(writer)?; + writer.write_all(&[u8::from(*dfi)]).map_err(Error::io)?; + len += 1; + len += pos.encode(writer)?; + } + } + Ok(len) + } +} + +impl<'a> Decode<'a> for RequestFileTransferResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let mode = FileOperationMode::try_from(buf[0])?; + let rest = &buf[1..]; + match mode { + FileOperationMode::DeleteFile => Ok((Self::DeleteFile(mode), rest)), + FileOperationMode::AddFile | FileOperationMode::ReplaceFile => { + let (sent, rest) = SentDataPayload::decode(rest)?; + if rest.is_empty() { + return Err(Error::InsufficientData(1)); + } + let dfi = DataFormatIdentifier::from(rest[0]); + let rest = &rest[1..]; + let value = match mode { + FileOperationMode::AddFile => Self::AddFile(mode, sent, dfi), + FileOperationMode::ReplaceFile => Self::ReplaceFile(mode, sent, dfi), + _ => unreachable!(), + }; + Ok((value, rest)) + } + FileOperationMode::ReadFile => { + let (sent, rest) = SentDataPayload::decode(rest)?; + if rest.is_empty() { + return Err(Error::InsufficientData(1)); + } + let dfi = DataFormatIdentifier::from(rest[0]); + let (fs, rest) = FileSizePayload::decode(&rest[1..])?; + Ok((Self::ReadFile(mode, sent, dfi, fs), rest)) + } + FileOperationMode::ReadDir => { + let (sent, rest) = SentDataPayload::decode(rest)?; + if rest.is_empty() { + return Err(Error::InsufficientData(1)); + } + let dfi = DataFormatIdentifier::from(rest[0]); + let (ds, rest) = DirSizePayload::decode(&rest[1..])?; + Ok((Self::ReadDir(mode, sent, dfi, ds), rest)) + } + FileOperationMode::ResumeFile => { + let (sent, rest) = SentDataPayload::decode(rest)?; + if rest.is_empty() { + return Err(Error::InsufficientData(1)); + } + let dfi = DataFormatIdentifier::from(rest[0]); + let (pos, rest) = PositionPayload::decode(&rest[1..])?; + Ok((Self::ResumeFile(mode, sent, dfi, pos), rest)) + } + FileOperationMode::ISOSAEReserved(b) => Err(Error::InvalidFileOperationMode(b)), + } + } +} + +#[cfg(test)] +mod request_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + #[test] fn test_file_operation_mode() { use FileOperationMode::*; @@ -969,208 +775,223 @@ mod request_tests { FileOperationMode::try_from(0x07).unwrap() ); } -} -#[cfg(test)] -mod response_tests { + fn name_payload(mode: FileOperationMode, path: &str) -> NamePayload<'_> { + NamePayload { + mode_of_operation: mode, + file_path_and_name_length: path.len() as u16, + file_path_and_name: path, + } + } - use crate::{param_length_u32, param_length_u128}; + #[test] + fn name_payload_roundtrip() { + let path = "/tmp/foo.bin"; + let n = name_payload(FileOperationMode::AddFile, path); + let mut buf = [0u8; 64]; + let written = Encode::encode(&n, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, n.encoded_size()); + let (decoded, rest) = NamePayload::decode(&buf[..written]).unwrap(); + assert!(rest.is_empty()); + assert_eq!(decoded, n); + assert_encode_size_agrees(&n); + } - use super::*; + #[test] + fn size_payload_roundtrip() { + let s = SizePayload { + file_size_parameter_length: 9, + file_size_uncompressed: (u64::MAX as u128) + 1000, + file_size_compressed: 0x12_3456, + }; + let mut buf = [0u8; 32]; + let written = Encode::encode(&s, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, s.encoded_size()); + let (decoded, rest) = SizePayload::decode(&buf[..written]).unwrap(); + assert!(rest.is_empty()); + assert_eq!(decoded, s); + assert_encode_size_agrees(&s); + } - fn get_bytes( - mode: FileOperationMode, - max_block_len: u32, - data_format: u8, - file_size: u128, - file_position: u64, - ) -> Vec { - let mut bytes: Vec = Vec::new(); - bytes.push(mode.into()); - - // SentDataPayload - if mode != FileOperationMode::DeleteFile { - let len_max_block_len = param_length_u32(max_block_len); - bytes.write_u8(len_max_block_len).unwrap(); - let source = max_block_len.to_be_bytes(); - bytes.extend_from_slice(&source[4 - len_max_block_len as usize..]); - - let mut data_format = data_format; - if mode == FileOperationMode::ReadDir { - data_format = 0x00; - } - // DataFormatIdentifier - bytes.write_u8(data_format).unwrap(); - } + #[test] + fn add_file_request_roundtrip() { + let path = "test.txt"; + let req = RequestFileTransferRequest::AddFile( + name_payload(FileOperationMode::AddFile, path), + DataFormatIdentifier::from(0x00), + SizePayload { + file_size_parameter_length: 2, + file_size_uncompressed: 0x1234, + file_size_compressed: 0x1234, + }, + ); + let mut buf = [0u8; 64]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, req.encoded_size()); + let (decoded, rest) = RequestFileTransferRequest::decode(&buf[..written]).unwrap(); + assert!(rest.is_empty()); + assert_eq!(decoded, req); + assert_encode_size_agrees(&req); + } - // File or dir size - let num = param_length_u128(file_size); - if mode == FileOperationMode::ReadFile { - print!("{mode:?}"); - - bytes.write_u16::(num).unwrap(); - let source = file_size.to_be_bytes(); - // Compressed - bytes.extend_from_slice(&source[16 - num as usize..]); - // Uncompressed - bytes.extend_from_slice(&source[16 - num as usize..]); - } else if mode == FileOperationMode::ReadDir { - bytes.write_u16::(num).unwrap(); - let source = file_size.to_be_bytes(); - // Compressed - bytes.extend_from_slice(&source[16 - num as usize..]); - } + #[test] + fn delete_file_request_roundtrip() { + let path = "/var/tmp/delete_file.bin"; + let req = RequestFileTransferRequest::DeleteFile(name_payload( + FileOperationMode::DeleteFile, + path, + )); + let mut buf = [0u8; 64]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, req.encoded_size()); + let (decoded, rest) = RequestFileTransferRequest::decode(&buf[..written]).unwrap(); + assert!(rest.is_empty()); + assert_eq!(decoded, req); + assert_encode_size_agrees(&req); + } - if mode == FileOperationMode::ResumeFile { - bytes.write_u64::(file_position).unwrap(); - } - bytes + #[test] + fn read_file_request_roundtrip() { + let path = "/etc/passwd"; + let req = RequestFileTransferRequest::ReadFile( + name_payload(FileOperationMode::ReadFile, path), + DataFormatIdentifier::from(0x11), + ); + let mut buf = [0u8; 64]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, req.encoded_size()); + let (decoded, rest) = RequestFileTransferRequest::decode(&buf[..written]).unwrap(); + assert!(rest.is_empty()); + assert_eq!(decoded, req); + assert_encode_size_agrees(&req); } #[test] - fn response_add() { - let bytes = get_bytes(FileOperationMode::AddFile, 0x1234, 0x00, 0x1234, 0); - let reader = &mut &bytes[..]; - let resp = RequestFileTransferResponse::decode(reader).unwrap(); - assert!(reader.is_empty()); - - let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(resp.required_size(), bytes.len()); - - match resp { - RequestFileTransferResponse::AddFile(mode, sent_data, data_format) => { - assert_eq!(mode, FileOperationMode::AddFile); - assert_eq!(sent_data.length_format_identifier, 2); - assert_eq!(sent_data.max_number_of_block_length, vec![0x12, 0x34]); - assert_eq!(data_format, DataFormatIdentifier::new(0, 0).unwrap()); - } - _ => panic!("Expected AddFile"), - } + fn read_dir_request_roundtrip() { + let path = "/var/log"; + let req = + RequestFileTransferRequest::ReadDir(name_payload(FileOperationMode::ReadDir, path)); + let mut buf = [0u8; 64]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + let (decoded, _) = RequestFileTransferRequest::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, req); + assert_encode_size_agrees(&req); } #[test] - fn delete_file() { - let bytes = get_bytes(FileOperationMode::DeleteFile, 0, 0, 0, 0); - let reader = &mut &bytes[..]; - let resp = RequestFileTransferResponse::decode(reader).unwrap(); - assert!(reader.is_empty()); - - let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(resp.required_size(), bytes.len()); - - match resp { - RequestFileTransferResponse::DeleteFile(mode) => { - assert_eq!(mode, FileOperationMode::DeleteFile); - } - _ => panic!("Expected DeleteFile"), + fn resume_file_request_roundtrip() { + let path = "/big/file.bin"; + let req = RequestFileTransferRequest::ResumeFile( + name_payload(FileOperationMode::ResumeFile, path), + DataFormatIdentifier::from(0x00), + SizePayload { + file_size_parameter_length: 4, + file_size_uncompressed: 0xDEAD_BEEF, + file_size_compressed: 0xDEAD_BEEF, + }, + ); + let mut buf = [0u8; 64]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + let (decoded, _) = RequestFileTransferRequest::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, req); + assert_encode_size_agrees(&req); + } +} + +#[cfg(test)] +mod response_tests { + use super::*; + use crate::test_util::assert_encode_size_agrees; + + fn sent_data<'a>(block: &'a [u8]) -> SentDataPayload<'a> { + SentDataPayload { + length_format_identifier: block.len() as u8, + max_number_of_block_length: block, } } #[test] - fn replace_file() { - let bytes = get_bytes(FileOperationMode::ReplaceFile, 0x1_1234, 0, 0, 0); - let reader = &mut &bytes[..]; - let resp = RequestFileTransferResponse::decode(reader).unwrap(); - assert!(reader.is_empty()); - - let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(resp.required_size(), bytes.len()); - - match resp { - RequestFileTransferResponse::ReplaceFile(mode, sent_data, data_format) => { - assert_eq!(mode, FileOperationMode::ReplaceFile); - assert_eq!(sent_data.length_format_identifier, 3); - assert_eq!(sent_data.max_number_of_block_length, vec![0x01, 0x12, 0x34]); - assert_eq!(data_format, DataFormatIdentifier::new(0, 0).unwrap()); - } - _ => panic!("Expected ReplaceFile"), - } + fn add_file_response_roundtrip() { + let block = [0x10u8, 0x00]; + let resp = RequestFileTransferResponse::AddFile( + FileOperationMode::AddFile, + sent_data(&block), + DataFormatIdentifier::from(0x00), + ); + let mut buf = [0u8; 32]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, resp.encoded_size()); + let (decoded, rest) = RequestFileTransferResponse::decode(&buf[..written]).unwrap(); + assert!(rest.is_empty()); + assert_eq!(decoded, resp); + assert_encode_size_agrees(&resp); } #[test] - fn read_file() { - let bytes = get_bytes(FileOperationMode::ReadFile, 0x1, 0x11, 0x11_1111_1111, 0); - let reader = &mut &bytes[..]; - let resp = RequestFileTransferResponse::decode(reader).unwrap(); - assert!(reader.is_empty()); - - let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(resp.required_size(), bytes.len()); - - match resp { - RequestFileTransferResponse::ReadFile(mode, sent_data, df, size) => { - assert_eq!(mode, FileOperationMode::ReadFile); - assert_eq!(sent_data.length_format_identifier, 1); - assert_eq!(sent_data.max_number_of_block_length, vec![0x01]); - assert_eq!(df, DataFormatIdentifier::new(0x01, 0x01).unwrap()); - assert_eq!(size.file_size_parameter_length, 5); - assert_eq!(size.file_size_uncompressed, 0x11_1111_1111); - assert_eq!(size.file_size_compressed, 0x11_1111_1111); - } - _ => panic!("Expected ReadFile"), - } + fn delete_file_response_roundtrip() { + let resp = RequestFileTransferResponse::DeleteFile(FileOperationMode::DeleteFile); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 1); + let (decoded, _) = RequestFileTransferResponse::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, resp); + assert_encode_size_agrees(&resp); } #[test] - fn read_dir() { - let bytes = get_bytes(FileOperationMode::ReadDir, 0x1_1234, 0, 0x11_1111_1111, 0); - let reader = &mut &bytes[..]; - let resp = RequestFileTransferResponse::decode(reader).unwrap(); - assert!(reader.is_empty()); - - let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(resp.required_size(), bytes.len()); - - match resp { - RequestFileTransferResponse::ReadDir(mode, sent_data, df, size) => { - assert_eq!(mode, FileOperationMode::ReadDir); - assert_eq!(sent_data.length_format_identifier, 3); - assert_eq!(sent_data.max_number_of_block_length, vec![0x01, 0x12, 0x34]); - assert_eq!(df, DataFormatIdentifier::new(0, 0).unwrap()); - assert_eq!(size.dir_info_parameter_length, 5); - assert_eq!(size.dir_info_length, 0x11_1111_1111); - } - _ => panic!("Expected ReadDir"), - } + fn read_file_response_roundtrip() { + let block = [0x04u8, 0x00]; + let resp = RequestFileTransferResponse::ReadFile( + FileOperationMode::ReadFile, + sent_data(&block), + DataFormatIdentifier::from(0x00), + FileSizePayload { + file_size_parameter_length: 4, + file_size_uncompressed: 0xAABB_CCDD, + file_size_compressed: 0x1122_3344, + }, + ); + let mut buf = [0u8; 64]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + let (decoded, _) = RequestFileTransferResponse::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, resp); + assert_encode_size_agrees(&resp); + } + + #[test] + fn read_dir_response_roundtrip() { + let block = [0x04u8, 0x00]; + let resp = RequestFileTransferResponse::ReadDir( + FileOperationMode::ReadDir, + sent_data(&block), + DataFormatIdentifier::from(0x00), + DirSizePayload { + dir_info_parameter_length: 4, + dir_info_length: 0x1234_5678, + }, + ); + let mut buf = [0u8; 64]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + let (decoded, _) = RequestFileTransferResponse::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, resp); + assert_encode_size_agrees(&resp); } #[test] - fn resume_file() { - let bytes = get_bytes( + fn resume_file_response_roundtrip() { + let block = [0x04u8, 0x00]; + let resp = RequestFileTransferResponse::ResumeFile( FileOperationMode::ResumeFile, - 0x1_1234, - 0x11, - 0x11_1111_1111, - 0x1234_5678_9ABC_DEF0, + sent_data(&block), + DataFormatIdentifier::from(0x00), + PositionPayload { + file_position: 0xDEAD_BEEF_CAFE_BABE, + }, ); - let reader = &mut &bytes[..]; - let resp = RequestFileTransferResponse::decode(reader).unwrap(); - assert!(reader.is_empty()); - - let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(resp.required_size(), bytes.len()); - - match resp { - RequestFileTransferResponse::ResumeFile(mode, sent_data, df, pos) => { - assert_eq!(mode, FileOperationMode::ResumeFile); - assert_eq!(sent_data.length_format_identifier, 3); - assert_eq!(sent_data.max_number_of_block_length, vec![0x01, 0x12, 0x34]); - assert_eq!(df, DataFormatIdentifier::new(1, 1).unwrap()); - assert_eq!(pos.file_position, 0x1234_5678_9ABC_DEF0); - } - _ => panic!("Expected ResumeFile"), - } + let mut buf = [0u8; 64]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + let (decoded, _) = RequestFileTransferResponse::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, resp); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/routine_control.rs b/src/services/routine_control.rs index 87004d6..c432578 100644 --- a/src/services/routine_control.rs +++ b/src/services/routine_control.rs @@ -1,226 +1,210 @@ -//! Routine Control (0x31) Service is used to perform functions on the ECU that are may not be covered by other services. +//! Routine Control (0x31) Service is used to perform functions on the ECU that may not be covered by other services. //! -//! It can also be used to check the ECU’s health, erase memory, or other custom manufacturer/supplier routines. +//! It can also be used to check the ECU's health, erase memory, or other custom manufacturer/supplier routines. //! However, some routines may have side effects or require certain preconditions to be met. -use crate::{ - Error, Identifier, IterableWireFormat, RoutineControlSubFunction, SingleValueWireFormat, - WireFormat, -}; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{Read, Write}; - -/// Used by a client to execute a defined sequence of events and obtain any relevant results -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +use crate::common::SuppressablePositiveResponse; +use crate::{Decode, Encode, Error, RoutineControlSubFunction}; + +/// Used by a client to execute a defined sequence of events and obtain any relevant results. +/// +/// The payload is the routine identifier (2 bytes, big-endian) followed by any optional +/// routine input parameters, exactly as it appears on the wire after the sub-function byte. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] -pub struct RoutineControlRequest { - /// The routine control operation (start, stop, or request results). - pub sub_function: RoutineControlSubFunction, - /// The identifier of the routine to control. - pub routine_id: RoutineIdentifier, - /// Optional payload data for the routine (e.g. input parameters). - pub data: Option, +pub struct RoutineControlRequest<'d> { + sub_function: SuppressablePositiveResponse, + raw_payload: &'d [u8], } -impl - RoutineControlRequest -{ - pub(crate) fn new( +impl<'d> RoutineControlRequest<'d> { + /// Create a new `RoutineControlRequest`. + #[must_use] + pub const fn new( + suppress_positive_response: bool, sub_function: RoutineControlSubFunction, - routine_id: RoutineIdentifier, - data: Option, + raw_payload: &'d [u8], ) -> Self { Self { - sub_function, - routine_id, - data, + sub_function: SuppressablePositiveResponse::new( + suppress_positive_response, + sub_function, + ), + raw_payload, } } + + /// Whether the server should suppress the positive response (SPRMIB). + #[must_use] + pub fn suppress_positive_response(&self) -> bool { + self.sub_function.suppress_positive_response() + } + + /// The routine control operation (start, stop, or request results). + #[must_use] + pub fn sub_function(&self) -> RoutineControlSubFunction { + self.sub_function.value() + } + + /// The raw payload bytes: routine identifier followed by optional parameters. + #[must_use] + pub const fn raw_payload(&self) -> &[u8] { + self.raw_payload + } } -impl WireFormat - for RoutineControlRequest -{ - fn required_size(&self) -> usize { - 3 + match &self.data { - Some(record) => record.required_size(), - None => 0, - } +impl Encode for RoutineControlRequest<'_> { + fn encoded_size(&self) -> usize { + 1 + self.raw_payload.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.sub_function))?; - self.routine_id.encode(writer)?; - if let Some(record) = &self.data { - record.encode(writer)?; - } - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.sub_function)]) + .map_err(Error::io)?; + writer.write_all(self.raw_payload).map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat - for RoutineControlRequest -{ - fn decode(reader: &mut T) -> Result { - let sub_function = RoutineControlSubFunction::from(reader.read_u8()?); - let routine_id = RoutineIdentifier::decode(reader)?; - let data = RoutinePayload::decode_next(reader)?; - Ok(Self { - sub_function, - routine_id, - data, - }) +impl<'a> Decode<'a> for RoutineControlRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let sub_function = SuppressablePositiveResponse::try_from(buf[0])?; + Ok(( + Self { + sub_function, + raw_payload: &buf[1..], + }, + &[], + )) } } -/// `RoutineControlResponse` is a variable length field that can contain the status of the routine -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] +/// `RoutineControlResponse` is a variable-length response that can contain routine status. +/// +/// The status record is the routine identifier echo plus any routine-info / status bytes, +/// held as raw bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] -pub struct RoutineControlResponse { - /// The sub-function echoes the routine control request +pub struct RoutineControlResponse<'d> { + /// The sub-function echoed from the routine control request. pub routine_control_type: RoutineControlSubFunction, - - /// Should contain the `routine_info` (u8) and the `routine_status_record` (u8 * n) information. n can be 0 - /// - /// `routine_info`: The routine information that the response is for (vehicle manufacturer specific) - /// `routine_status_record`: The status of the routine (optional) - /// - /// Mandatory for any routine where the `routine_status_record` is defined by ISO/SAE specs, even if it is 0 bytes. - /// Optional if the routine is defined by a manufacturer. - pub routine_status_record: RoutineInfoStatusRecord, + /// Raw routine status record bytes (routine identifier + routine info + status). + pub raw_status_record: &'d [u8], } -impl RoutineControlResponse { - pub(crate) fn new( +impl<'d> RoutineControlResponse<'d> { + /// Create a new `RoutineControlResponse`. + #[must_use] + pub const fn new( routine_control_type: RoutineControlSubFunction, - data: RoutineStatusRecord, + raw_status_record: &'d [u8], ) -> Self { Self { routine_control_type, - routine_status_record: data, + raw_status_record, } } - - /// Get the raw data of the status record - /// # Errors - /// - if the stream is not in the expected format - /// - if the stream contains partial data - pub fn status_record_data(&self) -> Result, Error> { - let mut writer: Vec = Vec::new(); - self.routine_status_record.encode(&mut writer)?; - - Ok(writer) - } } -impl WireFormat for RoutineControlResponse { - fn required_size(&self) -> usize { - // control type + (routine identifier + routine info + status record) - 1 + self.routine_status_record.required_size() +impl Encode for RoutineControlResponse<'_> { + fn encoded_size(&self) -> usize { + 1 + self.raw_status_record.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.routine_control_type.into())?; - self.routine_status_record.encode(writer)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.routine_control_type)]) + .map_err(Error::io)?; + writer + .write_all(self.raw_status_record) + .map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat - for RoutineControlResponse -{ - fn decode(reader: &mut T) -> Result { - let routine_control_type = RoutineControlSubFunction::from(reader.read_u8()?); - // Reads the identifier, then can read 0 bytes, 1 byte, or more - let routine_status_record = RoutineStatusRecord::decode(reader)?; - Ok(Self { - routine_control_type, - routine_status_record, - }) +impl<'a> Decode<'a> for RoutineControlResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let routine_control_type = RoutineControlSubFunction::try_from(buf[0])?; + Ok(( + Self { + routine_control_type, + raw_status_record: &buf[1..], + }, + &[], + )) } } #[cfg(test)] -mod request { +mod test { use super::*; - use crate::impl_identifier; + use crate::Decode; + use crate::test_util::assert_encode_size_agrees; - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - struct TestIdentifier(pub u16); - impl_identifier!(TestIdentifier); - - impl From for TestIdentifier { - fn from(value: u16) -> Self { - TestIdentifier(value) - } + #[test] + fn encode_routine_control_request_tx() { + // RID 0xFF00 (EraseMemory) + 1 parameter byte + let payload = [0xFF, 0x00, 0xAA]; + let req = + RoutineControlRequest::new(false, RoutineControlSubFunction::StartRoutine, &payload); + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x01, 0xFF, 0x00, 0xAA]); + assert_encode_size_agrees(&req); } - impl From for u16 { - fn from(val: TestIdentifier) -> Self { - val.0 - } + #[test] + fn encode_routine_control_response_tx() { + let record = [0xFF, 0x00, 0x10]; + let resp = RoutineControlResponse::new(RoutineControlSubFunction::StartRoutine, &record); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &[0x01, 0xFF, 0x00, 0x10]); + assert_encode_size_agrees(&resp); } - type RoutineControlRequestType = RoutineControlRequest>; - #[test] - fn simple_request() { - // Fake data: StartRoutine, RoutineID of 0x8606 for "Start O2 Sensor Heater Test" or something - let bytes: [u8; 6] = [0x01, 0x00, 0x01, 0x02, 0x03, 0x04]; - let req: RoutineControlRequestType = - RoutineControlRequest::decode(&mut bytes.as_slice()).unwrap(); - - assert_eq!(u8::from(req.sub_function), 0x01); - assert_eq!(req.routine_id, TestIdentifier::from(0x0001)); - let data = req.data.clone().unwrap(); - assert_eq!(data, vec![0x02, 0x03, 0x04]); - - let mut buf = Vec::new(); - let written = req.encode(&mut buf).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(written, req.required_size()); - - let new_req: RoutineControlRequestType = RoutineControlRequest::new( - RoutineControlSubFunction::StopRoutine, - TestIdentifier::from(0x0002), - Some(vec![]), - ); - - assert_eq!(new_req.sub_function, RoutineControlSubFunction::StopRoutine); - assert_eq!(new_req.routine_id, TestIdentifier::from(0x0002)); + fn decode_routine_control_request_with_suppress_bit() { + // sub 0x81 = StartRoutine (0x01) + SPRMIB (0x80), then RID 0xFF00 + param 0xAA + let bytes = [0x81, 0xFF, 0x00, 0xAA]; + let (req, rest) = ::decode(&bytes).unwrap(); + assert!(rest.is_empty()); + assert!(req.suppress_positive_response()); + assert_eq!(req.sub_function(), RoutineControlSubFunction::StartRoutine); + assert_eq!(req.raw_payload(), &[0xFF, 0x00, 0xAA]); + // round-trips back to the same bytes + let mut buf = [0u8; 8]; + let written = Encode::encode(&req, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &bytes); + assert_encode_size_agrees(&req); } #[test] - fn simple_response() { - let bytes: [u8; 6] = [0x01, 0x00, 0x01, 0x02, 0x03, 0x04]; - let resp: RoutineControlResponse> = - RoutineControlResponse::decode(&mut bytes.as_slice()).unwrap(); + fn decode_routine_control_request_rejects_reserved_subfunction() { + // 0x7F (low 7 bits = 0x7F) is a reserved routineControlType + let bytes = [0x7F, 0xFF, 0x00]; + assert!(::decode(&bytes).is_err()); + } + #[test] + fn decode_routine_control_response() { + let bytes = [0x01, 0xFF, 0x00, 0x10]; + let (resp, rest) = ::decode(&bytes).unwrap(); + assert!(rest.is_empty()); assert_eq!( resp.routine_control_type, RoutineControlSubFunction::StartRoutine ); - // Vec as payload just reads until the end, including the identifier - assert_eq!( - resp.routine_status_record, - vec![0x00, 0x01, 0x02, 0x03, 0x04] - ); - - let mut buf = Vec::new(); - let written = resp.encode(&mut buf).unwrap(); - assert_eq!(written, bytes.len()); - assert_eq!(written, resp.required_size()); - - let new_resp: RoutineControlResponse> = - RoutineControlResponse::new(RoutineControlSubFunction::StopRoutine, buf); - - assert_eq!( - new_resp.routine_control_type, - RoutineControlSubFunction::StopRoutine - ); + assert_eq!(resp.raw_status_record, &[0xFF, 0x00, 0x10]); + let mut buf = [0u8; 8]; + let written = Encode::encode(&resp, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(&buf[..written], &bytes); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/security_access.rs b/src/services/security_access.rs index bae59ee..4e280d4 100644 --- a/src/services/security_access.rs +++ b/src/services/security_access.rs @@ -1,10 +1,6 @@ //! `SecurityAccess` (0x27) service implementation -use crate::{ - Error, NegativeResponseCode, SecurityAccessType, SingleValueWireFormat, - SuppressablePositiveResponse, WireFormat, -}; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use std::io::{Read, Write}; +use crate::common::SuppressablePositiveResponse; +use crate::{Decode, Encode, Error, NegativeResponseCode, SecurityAccessType}; /// List of allowed [`NegativeResponseCode`] variants for the `SecurityAccess` service const SECURITY_ACCESS_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 8] = [ @@ -37,21 +33,21 @@ const SECURITY_ACCESS_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 8] = [ /// The server will then validate the key and respond with a positive or negative response. /// Successful verification of the key will result in the server unlocking the requested security level. /// Suppressing a positive response to this request is allowed. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub struct SecurityAccessRequest { +/// +/// Zero-alloc request for security access. Borrows from the caller. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SecurityAccessRequest<'d> { access_type: SuppressablePositiveResponse, - request_data: Vec, + request_data: &'d [u8], } -impl SecurityAccessRequest { - /// Create a new '`SecurityAccessRequest`' - pub(crate) fn new( +impl<'d> SecurityAccessRequest<'d> { + /// Create a new security access request. + #[must_use] + pub const fn new( suppress_positive_response: bool, access_type: SecurityAccessType, - request_data: Vec, + request_data: &'d [u8], ) -> Self { Self { access_type: SuppressablePositiveResponse::new(suppress_positive_response, access_type), @@ -73,8 +69,8 @@ impl SecurityAccessRequest { /// Getter for the request data #[must_use] - pub fn request_data(&self) -> &[u8] { - &self.request_data + pub const fn request_data(&self) -> &[u8] { + self.request_data } /// Get the allowed [`NegativeResponseCode`] variants for this request @@ -84,57 +80,49 @@ impl SecurityAccessRequest { } } -impl WireFormat for SecurityAccessRequest { - fn required_size(&self) -> usize { - 1 + self.request_data().len() +impl Encode for SecurityAccessRequest<'_> { + fn encoded_size(&self) -> usize { + 1 + self.request_data.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.access_type))?; - writer.write_all(&self.request_data)?; - Ok(self.required_size()) - } - - fn is_positive_response_suppressed(&self) -> bool { - self.suppress_positive_response() + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.access_type)]) + .map_err(Error::io)?; + writer.write_all(self.request_data).map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat for SecurityAccessRequest { - fn decode(reader: &mut T) -> Result { - let access_type = SuppressablePositiveResponse::try_from(reader.read_u8()?)?; - let mut request_data: Vec = Vec::new(); - _ = reader.read_to_end(&mut request_data)?; - Ok(Self { - access_type, - request_data, - }) +impl<'a> Decode<'a> for SecurityAccessRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let access_type = SuppressablePositiveResponse::try_from(buf[0])?; + Ok(( + Self { + access_type, + request_data: &buf[1..], + }, + &[], + )) } } -/// Response to `SecurityAccessRequest` -/// -/// ## Request Seed -/// -/// When responding to a seed request, the `security_seed` field shall contain the seed value. -/// -/// ## Send Key -/// -/// The positive response to a `SendKey` request shall not have any data in the security seed field. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub struct SecurityAccessResponse { +/// Zero-alloc response for security access. Borrows from the caller. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SecurityAccessResponse<'d> { /// The security access type echoed from the request. pub access_type: SecurityAccessType, /// The security seed bytes (empty for a `SendKey` positive response). - pub security_seed: Vec, + pub security_seed: &'d [u8], } -impl SecurityAccessResponse { - /// Create a new '`SecurityAccessResponse`' - pub(crate) fn new(access_type: SecurityAccessType, security_seed: Vec) -> Self { +impl<'d> SecurityAccessResponse<'d> { + /// Create a new security access response. + #[must_use] + pub const fn new(access_type: SecurityAccessType, security_seed: &'d [u8]) -> Self { Self { access_type, security_seed, @@ -142,72 +130,86 @@ impl SecurityAccessResponse { } } -impl WireFormat for SecurityAccessResponse { - fn required_size(&self) -> usize { +impl Encode for SecurityAccessResponse<'_> { + fn encoded_size(&self) -> usize { 1 + self.security_seed.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.access_type))?; - writer.write_all(&self.security_seed)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.access_type)]) + .map_err(Error::io)?; + writer.write_all(self.security_seed).map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat for SecurityAccessResponse { - fn decode(reader: &mut T) -> Result { - let access_type = SecurityAccessType::try_from(reader.read_u8()?)?; - let mut security_seed = Vec::new(); - let _ = reader.read_to_end(&mut security_seed)?; - Ok(Self { - access_type, - security_seed, - }) +impl<'a> Decode<'a> for SecurityAccessResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let access_type = SecurityAccessType::try_from(buf[0])?; + Ok(( + Self { + access_type, + security_seed: &buf[1..], + }, + &[], + )) } } #[cfg(test)] mod request { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn request_seed() { let bytes: [u8; 6] = [ 0x01, // aka SecurityAccessType::RequestSeed(0x01) 0x00, 0x01, 0x02, 0x03, 0x04, // fake data ]; - let req = SecurityAccessRequest::decode(&mut bytes.as_slice()).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); - assert_eq!( - req.access_type, - SuppressablePositiveResponse::new(false, SecurityAccessType::RequestSeed(0x01)) - ); + assert_eq!(req.access_type(), SecurityAccessType::RequestSeed(0x01)); + assert_eq!(req.request_data(), &[0x00, 0x01, 0x02, 0x03, 0x04]); let mut buf = Vec::new(); - let written = req.encode(&mut buf).unwrap(); + let written = Encode::encode(&req, &mut buf).unwrap(); assert_eq!(written, bytes.len()); - assert_eq!(written, req.required_size()); + assert_eq!(written, req.encoded_size()); + assert_encode_size_agrees(&req); } } #[cfg(test)] mod response { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn response_send() { let bytes: [u8; 6] = [ 0x02, // aka SecurityAccessType::SendKey(0x02) 0x00, 0x01, 0x02, 0x03, 0x04, // fake data ]; - let resp = SecurityAccessResponse::decode(&mut bytes.as_slice()).unwrap(); + let (resp, _) = ::decode(&bytes).unwrap(); assert_eq!(resp.access_type, SecurityAccessType::SendKey(0x02)); - assert_eq!(resp.security_seed, vec![0x00, 0x01, 0x02, 0x03, 0x04]); + assert_eq!(resp.security_seed, &[0x00, 0x01, 0x02, 0x03, 0x04]); let mut buf = Vec::new(); - let written = resp.encode(&mut buf).unwrap(); + let written = Encode::encode(&resp, &mut buf).unwrap(); assert_eq!(written, bytes.len()); - assert_eq!(written, resp.required_size()); + assert_eq!(written, resp.encoded_size()); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/tester_present.rs b/src/services/tester_present.rs index eaeccce..1204771 100644 --- a/src/services/tester_present.rs +++ b/src/services/tester_present.rs @@ -1,9 +1,6 @@ //! `TesterPresent` (0x3E) service implementation -use crate::{ - Error, NegativeResponseCode, SingleValueWireFormat, SuppressablePositiveResponse, WireFormat, -}; - -use byteorder::{ReadBytesExt, WriteBytesExt}; +use crate::common::SuppressablePositiveResponse; +use crate::{Decode, Encode, Error, NegativeResponseCode}; const TESTER_PRESENT_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 2] = [ NegativeResponseCode::SubFunctionNotSupported, @@ -26,13 +23,6 @@ enum ZeroSubFunction { ISOSAEReserved(u8), } -impl ZeroSubFunction { - #[inline] - fn new() -> Self { - Self::default() - } -} - impl Default for ZeroSubFunction { #[inline] fn default() -> Self { @@ -71,8 +61,9 @@ pub struct TesterPresentRequest { impl TesterPresentRequest { /// Create a new `TesterPresentRequest` - pub(crate) fn new(suppress_positive_response: bool) -> Self { - Self::with_subfunction(suppress_positive_response, ZeroSubFunction::new()) + #[must_use] + pub fn new(suppress_positive_response: bool) -> Self { + Self::with_subfunction(suppress_positive_response, ZeroSubFunction::default()) } fn with_subfunction( @@ -100,25 +91,26 @@ impl TesterPresentRequest { } } -impl WireFormat for TesterPresentRequest { - fn required_size(&self) -> usize { +impl Encode for TesterPresentRequest { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.zero_sub_function))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.zero_sub_function)]) + .map_err(Error::io)?; Ok(1) } - - fn is_positive_response_suppressed(&self) -> bool { - self.suppress_positive_response() - } } -impl SingleValueWireFormat for TesterPresentRequest { - fn decode(reader: &mut T) -> Result { - let zero_sub_function = SuppressablePositiveResponse::try_from(reader.read_u8()?)?; - Ok(Self { zero_sub_function }) +impl<'a> Decode<'a> for TesterPresentRequest { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let zero_sub_function = SuppressablePositiveResponse::try_from(buf[0])?; + Ok((Self { zero_sub_function }, &buf[1..])) } } @@ -133,34 +125,49 @@ pub struct TesterPresentResponse { impl TesterPresentResponse { /// Create a new `TesterPresentResponse` - pub(crate) fn new() -> Self { + #[must_use] + pub fn new() -> Self { Self { - zero_sub_function: ZeroSubFunction::new(), + zero_sub_function: ZeroSubFunction::default(), } } } -impl WireFormat for TesterPresentResponse { - fn required_size(&self) -> usize { +impl Default for TesterPresentResponse { + fn default() -> Self { + Self::new() + } +} + +impl Encode for TesterPresentResponse { + fn encoded_size(&self) -> usize { 1 } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(u8::from(self.zero_sub_function))?; + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[u8::from(self.zero_sub_function)]) + .map_err(Error::io)?; Ok(1) } } -impl SingleValueWireFormat for TesterPresentResponse { - fn decode(reader: &mut T) -> Result { - let zero_sub_function = ZeroSubFunction::try_from(reader.read_u8()?)?; - Ok(Self { zero_sub_function }) +impl<'a> Decode<'a> for TesterPresentResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + let zero_sub_function = ZeroSubFunction::try_from(buf[0])?; + Ok((Self { zero_sub_function }, &buf[1..])) } } #[cfg(test)] mod test { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::{vec, vec::Vec}; #[test] fn try_from_all_zero_subfunction() { @@ -193,11 +200,14 @@ mod test { } } + #[cfg(feature = "alloc")] fn make_request(byte: u8) -> Result { let bytes = vec![byte]; - TesterPresentRequest::decode(&mut bytes.as_slice()) + let (val, _) = ::decode(&bytes)?; + Ok(val) } + #[cfg(feature = "alloc")] #[test] fn read_request_type() { for i in 0..u8::MAX { @@ -231,30 +241,35 @@ mod test { } } + #[cfg(feature = "alloc")] #[test] fn write_request_type() { let test_type = TesterPresentRequest::new(false); let mut buffer = Vec::new(); - test_type.encode(&mut buffer).unwrap(); + Encode::encode(&test_type, &mut buffer).unwrap(); let expected_bytes = vec![0]; assert_eq!(buffer, expected_bytes); + assert_encode_size_agrees(&test_type); } + #[cfg(feature = "alloc")] #[test] fn read_response_type() { let bytes = vec![0u8]; - let test_type = TesterPresentResponse::decode(&mut bytes.as_slice()).unwrap(); + let (test_type, _) = ::decode(&bytes).unwrap(); assert_eq!(test_type, TesterPresentResponse::new()); } + #[cfg(feature = "alloc")] #[test] fn write_response_type() { let test_type = TesterPresentResponse::new(); let mut buffer = Vec::new(); - test_type.encode(&mut buffer).unwrap(); + Encode::encode(&test_type, &mut buffer).unwrap(); let expected_bytes = vec![0]; assert_eq!(buffer, expected_bytes); + assert_encode_size_agrees(&test_type); } } diff --git a/src/services/transfer_data.rs b/src/services/transfer_data.rs index c24234e..d06e0f8 100644 --- a/src/services/transfer_data.rs +++ b/src/services/transfer_data.rs @@ -1,7 +1,6 @@ //! `TransferData` (0x36) service implementation -use byteorder::{ReadBytesExt, WriteBytesExt}; -use crate::{Error, SingleValueWireFormat, WireFormat}; +use crate::{Decode, Encode, Error}; /// A request to the server to transfer data (either upload or download) /// @@ -11,32 +10,30 @@ use crate::{Error, SingleValueWireFormat, WireFormat}; /// /// Step 1 Response: The server sends a [`RequestDownloadResponse`](crate::RequestDownloadResponse) or `RequestUploadResponse` message to the client /// -/// Step 2: The client shall send many `TransferDataRequest` messages written in blocks +/// Step 2: The client shall send many [`TransferDataRequest`] messages written in blocks /// to the server with a max number of bytes equal to `MNROB_B`# from the `RequestDownloadResponse` message /// 74 .. 20 .. 00 81 /// RSID .. LFID .. `MNROB_B`# /// -/// Step 2 Response: The server sends a [`crate::TransferDataResponse`] message confirming the block sequence +/// Step 2 Response: The server sends a [`TransferDataResponse`] message confirming the block sequence /// /// Step 3: The client sends a [`crate::UdsServiceType::RequestTransferExit`] message to the server (SID 0x37) /// /// Step 3 Response: The server sends a [`crate::UdsServiceType::RequestTransferExit`] response message to the client (RID 0x77) -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -#[non_exhaustive] -pub struct TransferDataRequest { - /// Starts at 0x01 from the server when a `RequestDownload` or `RequestUpload` or `RequestFileTransfer` is received - /// Increments by 0x01 for each `TransferDataRequest` message - /// At 0xFF the counter wraps around to 0x00 +/// +/// Zero-alloc request to transfer data. Borrows from the caller. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TransferDataRequest<'d> { + /// Block sequence counter (wraps 0xFF → 0x00). pub block_sequence_counter: u8, - /// The data to be transferred, the server sends the amount of data (# of bytes) it can handle in the - /// [`crate::RequestDownloadResponse`] message - pub data: Vec, + /// The data to be transferred. + pub data: &'d [u8], } -impl TransferDataRequest { - pub(crate) fn new(block_sequence_counter: u8, data: Vec) -> Self { +impl<'d> TransferDataRequest<'d> { + /// Create a new transfer data request. + #[must_use] + pub const fn new(block_sequence_counter: u8, data: &'d [u8]) -> Self { Self { block_sequence_counter, data, @@ -44,56 +41,48 @@ impl TransferDataRequest { } } -impl WireFormat for TransferDataRequest { - fn required_size(&self) -> usize { +impl Encode for TransferDataRequest<'_> { + fn encoded_size(&self) -> usize { 1 + self.data.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.block_sequence_counter)?; - writer.write_all(&self.data)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.block_sequence_counter]) + .map_err(Error::io)?; + writer.write_all(self.data).map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat for TransferDataRequest { - fn decode(reader: &mut T) -> Result { - let block_sequence_counter = reader.read_u8()?; - let mut data = Vec::new(); - reader.read_to_end(&mut data)?; - Ok(Self { - block_sequence_counter, - data, - }) +impl<'a> Decode<'a> for TransferDataRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok(( + Self { + block_sequence_counter: buf[0], + data: &buf[1..], + }, + &[], + )) } } -/// Positive response to a [`TransferDataRequest`]. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, PartialEq)] -#[non_exhaustive] -pub struct TransferDataResponse { - /// Starts at 0x01 from the server when a `RequestDownload` or `RequestUpload` or `RequestFileTransfer` is received - /// Increments by 0x01 for each `TransferDataRequest` message - /// At 0xFF the counter wraps around to 0x00 - /// - /// This is an ECHO of the `block_sequence_counter` from the [`TransferDataRequest`] message - /// Check against the request to ensure the correct block is being acknowledged - /// If the `block_sequence_counter` is not as expected or does not arrive, the client should retransmit the block +/// Zero-alloc response for transfer data. Borrows from the caller. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct TransferDataResponse<'d> { + /// Echo of the block sequence counter. pub block_sequence_counter: u8, - - /// Contains data required by the client to support the transfer of data. - /// Vehicle manufacturer specific - /// - /// For download (client to server), this might be a checksum for the client to verify correct transfer - /// This should not repeat the data sent from the client - /// For upload (server to client), this will include the data from the server - pub data: Vec, + /// Response data (vendor-specific). + pub data: &'d [u8], } -impl TransferDataResponse { - pub(crate) fn new(block_sequence_counter: u8, data: Vec) -> Self { +impl<'d> TransferDataResponse<'d> { + /// Create a new transfer data response. + #[must_use] + pub const fn new(block_sequence_counter: u8, data: &'d [u8]) -> Self { Self { block_sequence_counter, data, @@ -101,68 +90,81 @@ impl TransferDataResponse { } } -impl WireFormat for TransferDataResponse { - fn required_size(&self) -> usize { +impl Encode for TransferDataResponse<'_> { + fn encoded_size(&self) -> usize { 1 + self.data.len() } - fn encode(&self, writer: &mut T) -> Result { - writer.write_u8(self.block_sequence_counter)?; - writer.write_all(&self.data)?; - Ok(self.required_size()) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&[self.block_sequence_counter]) + .map_err(Error::io)?; + writer.write_all(self.data).map_err(Error::io)?; + Ok(self.encoded_size()) } } -impl SingleValueWireFormat for TransferDataResponse { - fn decode(reader: &mut T) -> Result { - let block_sequence_counter = reader.read_u8()?; - let mut data = Vec::new(); - reader.read_to_end(&mut data)?; - Ok(Self { - block_sequence_counter, - data, - }) +impl<'a> Decode<'a> for TransferDataResponse<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.is_empty() { + return Err(Error::InsufficientData(1)); + } + Ok(( + Self { + block_sequence_counter: buf[0], + data: &buf[1..], + }, + &[], + )) } } #[cfg(test)] mod request { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; #[test] fn test_transfer_data_request() { - let bytes = [0x01, 0x02, 0x03, 0x04]; - let req = TransferDataRequest::new(0x01, bytes.to_vec()); - let bytes = req.data.clone(); - let expected = vec![0x01, 0x02, 0x03, 0x04]; + let data = [0x01, 0x02, 0x03, 0x04]; + let req = TransferDataRequest::new(0x01, &data); assert_eq!(1, req.block_sequence_counter); - assert_eq!(bytes, expected); + assert_eq!(req.data, &[0x01, 0x02, 0x03, 0x04]); } + #[cfg(feature = "alloc")] #[test] fn read_request() { let bytes = [0x01, 0x02, 0x03, 0x04]; - let req = TransferDataRequest::decode(&mut bytes.as_slice()).unwrap(); + let (req, _) = ::decode(&bytes).unwrap(); let mut written_bytes = Vec::new(); - let written = req.encode(&mut written_bytes).unwrap(); + let written = Encode::encode(&req, &mut written_bytes).unwrap(); assert_eq!(written, written_bytes.len()); - assert_eq!(written, req.required_size()); + assert_eq!(written, req.encoded_size()); + assert_encode_size_agrees(&req); } } #[cfg(test)] mod response { use super::*; + use crate::{Decode, Encode, test_util::assert_encode_size_agrees}; + #[cfg(feature = "alloc")] + use alloc::vec::Vec; + #[cfg(feature = "alloc")] #[test] fn simple_response() { let bytes = [0x01, 0x02, 0x03, 0x04]; - let resp = TransferDataResponse::decode(&mut bytes.as_slice()).unwrap(); + let (resp, _) = ::decode(&bytes).unwrap(); let mut written_bytes = Vec::new(); - let written = resp.encode(&mut written_bytes).unwrap(); + let written = Encode::encode(&resp, &mut written_bytes).unwrap(); assert_eq!(written, written_bytes.len()); - assert_eq!(written, resp.required_size()); + assert_eq!(written, resp.encoded_size()); + assert_encode_size_agrees(&resp); } } diff --git a/src/services/write_data_by_identifier.rs b/src/services/write_data_by_identifier.rs index 91003da..3dc7d35 100644 --- a/src/services/write_data_by_identifier.rs +++ b/src/services/write_data_by_identifier.rs @@ -1,5 +1,5 @@ //! `WriteDataByIdentifier` (0x2E) service implementation -use crate::{Error, Identifier, NegativeResponseCode, SingleValueWireFormat, WireFormat}; +use crate::{Decode, Encode, Error, NegativeResponseCode}; const WRITE_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ NegativeResponseCode::IncorrectMessageLengthOrInvalidFormat, @@ -9,196 +9,132 @@ const WRITE_DID_NEGATIVE_RESPONSE_CODES: [NegativeResponseCode; 5] = [ NegativeResponseCode::GeneralProgrammingFailure, ]; +/// Zero-alloc request to write data by identifier. Borrows the raw payload from the caller. +/// +/// The payload is the DID (2 bytes, big-endian) followed by the data record, exactly as +/// it appears on the wire after the service byte. +/// /// See ISO-14229-1:2020, Section 11.7.2.1 -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, Eq, PartialEq)] -#[non_exhaustive] -pub struct WriteDataByIdentifierRequest { - /// The payload to write, which includes the DID and data. - pub payload: Payload, +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WriteDataByIdentifierRequest<'d> { + /// The raw payload bytes: DID followed by the data record. + pub payload: &'d [u8], } -impl WriteDataByIdentifierRequest { - /// Create a new request with the given payload. - pub fn new(payload: Payload) -> Self { +impl<'d> WriteDataByIdentifierRequest<'d> { + /// Create a new write-by-identifier request from raw payload bytes. + #[must_use] + pub const fn new(payload: &'d [u8]) -> Self { Self { payload } } - /// Get the allowed Nack codes for this request + /// Get the allowed [`NegativeResponseCode`] variants for this request. #[must_use] pub fn allowed_nack_codes() -> &'static [NegativeResponseCode] { &WRITE_DID_NEGATIVE_RESPONSE_CODES } } -impl WireFormat for WriteDataByIdentifierRequest { - fn required_size(&self) -> usize { - self.payload.required_size() +impl Encode for WriteDataByIdentifierRequest<'_> { + fn encoded_size(&self) -> usize { + self.payload.len() } - fn encode(&self, writer: &mut T) -> Result { - // Payload must implement the extra bytes, because `decode` needs to know how to interpret payload message - self.payload.encode(writer) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer.write_all(self.payload).map_err(Error::io)?; + Ok(self.payload.len()) } } -impl SingleValueWireFormat - for WriteDataByIdentifierRequest -{ - fn decode(reader: &mut R) -> Result { - let payload = Payload::decode(reader)?; - Ok(Self { payload }) +impl<'a> Decode<'a> for WriteDataByIdentifierRequest<'a> { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + Ok((Self { payload: buf }, &[])) } } -/////////////////////////////////////////////////////////////////////////////////////////////////// - +/// Positive response to `WriteDataByIdentifier`: echoes the DID that was written. +/// /// See ISO-14229-1:2020, Section 11.7.3.1 -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] -pub struct WriteDataByIdentifierResponse { +pub struct WriteDataByIdentifierResponse { /// The DID that was written to. - pub identifier: DataIdentifier, + pub identifier: u16, } -impl WriteDataByIdentifierResponse { +impl WriteDataByIdentifierResponse { /// Create a new response echoing the identifier that was written. - pub fn new(identifier: DataIdentifier) -> Self { + #[must_use] + pub const fn new(identifier: u16) -> Self { Self { identifier } } } -impl WireFormat for WriteDataByIdentifierResponse { - fn required_size(&self) -> usize { - self.identifier.required_size() +impl Encode for WriteDataByIdentifierResponse { + fn encoded_size(&self) -> usize { + 2 } - fn encode(&self, writer: &mut T) -> Result { - // Payload must implement the extra bytes, because `decode` needs to know how to interpret payload message - self.identifier.encode(writer) + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result { + writer + .write_all(&self.identifier.to_be_bytes()) + .map_err(Error::io)?; + Ok(2) } } -impl SingleValueWireFormat - for WriteDataByIdentifierResponse -{ - fn decode(reader: &mut R) -> Result { - let identifier = DataIdentifier::decode(reader)?; - Ok(Self::new(identifier)) +impl<'a> Decode<'a> for WriteDataByIdentifierResponse { + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error> { + if buf.len() < 2 { + return Err(Error::InsufficientData(2)); + } + let identifier = u16::from_be_bytes([buf[0], buf[1]]); + Ok((Self { identifier }, &buf[2..])) } } -/////////////////////////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod test { use super::*; - use crate::impl_identifier; - use byteorder::WriteBytesExt; - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, PartialEq)] - pub enum TestIdentifier { - Abracadabra = 0xBEEF, - } - impl_identifier!(TestIdentifier); - impl From for TestIdentifier { - fn from(value: u16) -> Self { - match value { - 0xBEEF => TestIdentifier::Abracadabra, - _ => panic!("Invalid test identifier: {value}"), - } - } - } - - impl From for u16 { - fn from(value: TestIdentifier) -> Self { - match value { - TestIdentifier::Abracadabra => 0xBEEF, - } - } - } - - impl PartialEq for TestIdentifier { - fn eq(&self, other: &u16) -> bool { - match self { - TestIdentifier::Abracadabra => *other == 0xBEEF, - } - } - } + use crate::test_util::assert_encode_size_agrees; - /////////////////////////////////////////////////////////////////////////////////////////////// - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, PartialEq)] - enum TestPayload { - Abracadabra(u8), - } - - impl WireFormat for TestPayload { - fn encode(&self, writer: &mut T) -> Result { - let id_bytes: u16 = match self { - TestPayload::Abracadabra(_) => 0xBEEF, - }; - - writer.write_all(&id_bytes.to_be_bytes())?; - - match self { - TestPayload::Abracadabra(value) => { - writer.write_u8(*value)?; - Ok(self.required_size()) - } - } - } - - fn required_size(&self) -> usize { - 3 - } + #[test] + fn test_write_response_encode() { + let response = WriteDataByIdentifierResponse::new(0xBEEF); + let mut buf = [0u8; 4]; + let written = Encode::encode(&response, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 2); + assert_eq!(buf[0], 0xBE); + assert_eq!(buf[1], 0xEF); + assert_encode_size_agrees(&response); } - impl SingleValueWireFormat for TestPayload { - fn decode(reader: &mut T) -> Result { - let mut buf = [0u8; 2]; - reader.read_exact(&mut buf)?; - - let value = u16::from_be_bytes(buf); - - if value == TestIdentifier::Abracadabra as u16 { - let mut byte = [0u8; 1]; - reader.read_exact(&mut byte)?; - Ok(TestPayload::Abracadabra(byte[0])) - } else { - Err(Error::NoDataAvailable) - } - } + #[test] + fn test_write_request_encode() { + // DID 0xF186 + one data byte 0x01 + let payload = [0xF1, 0x86, 0x01]; + let request = WriteDataByIdentifierRequest::new(&payload); + let mut buf = [0u8; 8]; + let written = Encode::encode(&request, &mut buf.as_mut_slice()).unwrap(); + assert_eq!(written, 3); + assert_eq!(&buf[..3], &[0xF1, 0x86, 0x01]); + assert_encode_size_agrees(&request); } - /////////////////////////////////////////////////////////////////////////////////////////////// - #[test] - fn test_write_request() { - let request = WriteDataByIdentifierRequest::new(TestPayload::Abracadabra(42)); - - let mut written_bytes = Vec::new(); - let written = request.encode(&mut written_bytes).unwrap(); - assert_eq!(written, request.required_size()); - assert_eq!(written, written_bytes.len()); - - let request2 = - WriteDataByIdentifierRequest::::decode(&mut written_bytes.as_slice()) - .unwrap(); - assert_eq!(request, request2); + fn write_response_roundtrip() { + let response = WriteDataByIdentifierResponse::new(0xF186); + let mut buf = [0u8; 4]; + let written = Encode::encode(&response, &mut buf.as_mut_slice()).unwrap(); + let (decoded, rest) = + ::decode(&buf[..written]).unwrap(); + assert_eq!(decoded, response); + assert!(rest.is_empty()); } #[test] - fn test_write_response() { - let response = WriteDataByIdentifierResponse::new(TestIdentifier::Abracadabra); - - let mut written_bytes = Vec::new(); - let written = response.encode(&mut written_bytes).unwrap(); - assert_eq!(written, written_bytes.len()); - assert_eq!(written, response.required_size()); + fn write_response_decode_rejects_short_buffer() { + let err = ::decode(&[0x01]); + assert!(matches!(err, Err(Error::InsufficientData(2)))); } } diff --git a/src/test_util.rs b/src/test_util.rs new file mode 100644 index 0000000..72e8991 --- /dev/null +++ b/src/test_util.rs @@ -0,0 +1,19 @@ +//! Test-only helpers shared across the crate. + +use crate::Encode; + +/// Assert that an [`Encode`] value writes exactly `encoded_size()` bytes. +/// +/// Guards against the two methods drifting, which would corrupt callers that pre-size +/// a buffer from `encoded_size()`. +pub(crate) fn assert_encode_size_agrees(value: &T) { + let mut buf = [0u8; 512]; + let mut writer = buf.as_mut_slice(); + let written = value.encode(&mut writer).unwrap(); + assert_eq!( + written, + value.encoded_size(), + "encode wrote {written} bytes but encoded_size() reported {}", + value.encoded_size() + ); +} diff --git a/src/traits.rs b/src/traits.rs index adc380d..b666ad3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,369 +1,69 @@ use crate::Error; -use byteorder::{BigEndian, WriteBytesExt}; -/// Base trait for types that can be serialized to a byte stream. -/// -/// `WireFormat` provides the encoding half of the serialization contract. -/// Decoding is split into two separate traits with distinct return types: -/// - [`SingleValueWireFormat`] for types whose decode always produces a value -/// - [`IterableWireFormat`] for types that may return `None` when a stream is exhausted -/// -/// This split enforces at compile time the distinction between types that always -/// decode successfully (given valid data) and types that can signal "no more items." -pub trait WireFormat: Sized { - /// Returns the number of bytes required to serialize this value. - fn required_size(&self) -> usize; +// --------------------------------------------------------------------------- +// New no_std-compatible traits (TX: Encode, RX: Decode / DecodeIter) +// --------------------------------------------------------------------------- - /// Serialize a value to a byte stream. - /// Returns the number of bytes written. - /// # Errors - /// - If the data cannot be written to the stream - fn encode(&self, writer: &mut T) -> Result; +/// TX-side trait: encode a value into an [`embedded_io::Write`] implementor. +pub trait Encode { + /// Number of bytes this value will write. + fn encoded_size(&self) -> usize; - /// For some UDS messages, positive replies can be suppressed via the SPRMIB (bit 7 position) of the request. + /// Serialize into `writer`, returning the number of bytes written. /// - /// Default to false, meaning that the positive response is not suppressed. Some services do not support this feature, - /// so this function should not be used to assume that a positive response can be suppressed. - fn is_positive_response_suppressed(&self) -> bool { - false - } -} - -/// Types whose decode always produces a value. An empty stream is an error, not `None`. -/// -/// This trait enforces at compile time that `decode` cannot return `None`. -/// The return type is `Result` rather than `Result, Error>`. -pub trait SingleValueWireFormat: WireFormat { - /// Deserialize a value from a byte stream. /// # Errors - /// - if the stream is empty - /// - if the stream is not in the expected format - /// - if the stream contains partial data - fn decode(reader: &mut T) -> Result; -} - -struct WireFormatIterator<'a, T, R> { - reader: &'a mut R, - _phantom: std::marker::PhantomData, + /// Returns [`Error::IoError`] if the writer fails. + fn encode(&self, writer: &mut impl embedded_io::Write) -> Result; } -/// For types that can appear in lists of unknown length, this trait provides an iterator -/// that can be used to deserialize a stream of values. -impl Iterator for WireFormatIterator<'_, T, R> { - type Item = Result; - fn next(&mut self) -> Option { - match T::decode_next(self.reader.by_ref()) { - Ok(Some(value)) => Some(Ok(value)), - Ok(None) => None, - Err(e) => Some(Err(e)), - } - } -} - -/// Types that can be decoded from a stream of unknown length. +/// RX-side trait: zero-copy decode from a byte slice. /// -/// `decode_next` returns `Ok(None)` when the stream is exhausted, allowing -/// iteration over variable-length sequences without prior knowledge of their size. -pub trait IterableWireFormat: WireFormat { - /// Attempt to decode the next value from the stream. - /// Returns `Ok(None)` if the stream is exhausted. - /// # Errors - /// - if the stream contains partial or invalid data - fn decode_next(reader: &mut T) -> Result, Error>; - - /// Return an iterator that decodes successive values from the stream until exhausted. - fn decode_iter(reader: &mut T) -> impl Iterator> { - WireFormatIterator { - reader, - _phantom: std::marker::PhantomData, - } - } -} - -#[cfg(feature = "serde")] -mod maybe_serde { - // When `serde` feature is ON, require Serialize + Deserialize - pub trait Bound: serde::Serialize + for<'de> serde::Deserialize<'de> {} - impl Bound for T where T: serde::Serialize + for<'de> serde::Deserialize<'de> {} -} -#[cfg(not(feature = "serde"))] -mod maybe_serde { - // When `serde` feature is OFF, require nothing - pub trait Bound {} - impl Bound for T {} -} - -#[cfg(feature = "utoipa")] -mod maybe_utoipa { - // When `utoipa` feature is ON, require ToSchema - pub trait Bound: utoipa::ToSchema {} - impl Bound for T where T: utoipa::ToSchema {} -} - -#[cfg(not(feature = "utoipa"))] -mod maybe_utoipa { - // When `utoipa` feature is OFF, require nothing - pub trait Bound {} - impl Bound for T {} -} - -/// Trait for types that can be used as identifiers (ie Data Identifiers and Routine Identifiers) +/// Implementations borrow directly from the input buffer where possible. The decoded +/// value points into `buf` and is valid only as long as `buf` lives — for C developers +/// new to Rust, think of it like a `struct` overlaid on a `char buf[]`. Copy out any +/// fields you need to retain beyond the buffer's lifetime. /// -/// Use the [`impl_identifier!`] macro to implement this trait for your types. -pub trait Identifier: TryFrom + Into + Clone + Copy + maybe_serde::Bound { - /// Returns a `Vec` from a reader that contains a list of Identifier values +/// [`decode`](Self::decode) returns the value together with the unconsumed remainder of +/// the buffer, so leaf and sequence decoders can be composed. Frame-level decoders +/// (`Request`, `Response`) consume the whole buffer and return an empty remainder; use +/// [`decode_exact`](Self::decode_exact) when a buffer must contain exactly one value. +pub trait Decode<'a>: Sized { + /// Decode from `buf`, returning `(value, remaining_bytes)`. + /// /// # Errors - /// - if the list is not in the expected format - /// - if the list contains partial data - fn parse_from_list(reader: &mut R) -> Result, Error> { - // Create an iterator to collect. Will use the blanket implementation of IterableWireFormat for Identifier - // to read the values from the reader - WireFormatIterator { - reader, - _phantom: std::marker::PhantomData, - } - .collect() - } + /// Returns an error if `buf` is too short or contains invalid data. + fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Error>; - /// Intended to be used in a payload where the identifier is the first value and not a list of identifiers - /// IE `DataIdentifer` (DID) payloads and `RoutineIdentifier` (RID) payloads + /// Decode from `buf`, requiring the entire buffer to be consumed. /// - /// Returns the identifier, or None if the reader is empty - /// - /// ## Example reading a payload that has multiple identifiers - /// ```rust,ignore - /// while let Some(identifier) = MyIdentifier::parse_from_payload(&mut buffer).unwrap() { - /// match identifier { - /// MyIdentifier::Identifier1 | MyIdentifier::Identifier2 => { - /// let payload = MyPayload::decode(&mut buffer).unwrap(); - /// } - /// // No payload for Identifier3 - /// MyIdentifier::MyIdentifier3 => (), - /// MyIdentifier::UDSIdentifier(_) => (), - /// } - /// } - /// ``` + /// Use this when `buf` is expected to contain exactly one value and any + /// trailing bytes indicate a malformed frame. /// /// # Errors - /// - if the stream is not in the expected format - /// - if the stream contains partial data - fn parse_from_payload(reader: &mut R) -> Result, Error> { - Self::decode_next(reader) - } -} - -/// Implement the [`Identifier`] trait for a type. -/// -/// The type must already implement `TryFrom`, `Into`, `Clone`, and `Copy`. -/// -/// # Example -/// ```rust,ignore -/// impl_identifier!(MyIdentifierEnum); -/// ``` -#[macro_export] -macro_rules! impl_identifier { - ($($t:ty),+ $(,)?) => { - $(impl $crate::Identifier for $t {})+ - }; -} - -/// Marker subtrait of [`Identifier`] to distinguish routine identifiers from data identifiers. -pub trait RoutineIdentifier: Identifier {} - -/// Blanket implementation of [`WireFormat`] for types that implement [`Identifier`] -impl WireFormat for T -where - T: Identifier, -{ - fn required_size(&self) -> usize { - 2 - } - - fn encode(&self, writer: &mut W) -> Result { - writer.write_u16::((*self).into())?; - Ok(2) - } -} - -/// Blanket implementation of [`SingleValueWireFormat`] for types that implement [`Identifier`] -impl SingleValueWireFormat for T -where - T: Identifier, -{ - fn decode(reader: &mut R) -> Result { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 | 1 => return Err(Error::IncorrectMessageLengthOrInvalidFormat), - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - - match Self::try_from(u16::from_be_bytes(identifier_data)) { - Ok(identifier) => Ok(identifier), - Err(_) => Err(Error::InvalidDiagnosticIdentifier(u16::from_be_bytes( - identifier_data, - ))), + /// Returns [`Error::IncorrectMessageLengthOrInvalidFormat`] if any bytes + /// remain after decoding, or whatever error [`decode`](Self::decode) + /// produces. + fn decode_exact(buf: &'a [u8]) -> Result { + let (value, rest) = Self::decode(buf)?; + if rest.is_empty() { + Ok(value) + } else { + Err(Error::IncorrectMessageLengthOrInvalidFormat) } } } -/// Blanket implementation of [`IterableWireFormat`] for types that implement [`Identifier`] -impl IterableWireFormat for T -where - T: Identifier, -{ - fn decode_next(reader: &mut R) -> Result, Error> { - let mut identifier_data: [u8; 2] = [0; 2]; - match reader.read(&mut identifier_data)? { - 0 => return Ok(None), - 1 => return Err(Error::IncorrectMessageLengthOrInvalidFormat), - 2 => (), - _ => unreachable!("Impossible to read more than 2 bytes into 2 byte array"), - } - - match Self::try_from(u16::from_be_bytes(identifier_data)) { - Ok(identifier) => Ok(Some(identifier)), - Err(_) => Err(Error::InvalidDiagnosticIdentifier(u16::from_be_bytes( - identifier_data, - ))), - } - } -} - -/// A trait that defines the user-defined diagnostic definitions/specifiers for UDS requests and responses. +/// RX-side trait: streaming / iterable zero-copy decode. /// -/// Used to specify the types of the identifiers and payloads used in UDS requests and responses. -/// It allows for flexibility in defining custom data types while adhering to the UDS protocol. -pub trait DiagnosticDefinition: 'static { - /// UDS Data Identifier +/// Used for variable-length sequences where the number of items is not known +/// ahead of time. Each call consumes one item and returns the remainder, or +/// `Ok(None)` when the buffer is exhausted. +pub trait DecodeIter<'a>: Sized { + /// Try to decode the next item from `buf`. /// - /// Requests : [`ReadDataByIdentifierRequest`](crate::ReadDataByIdentifierRequest), [`WriteDataByIdentifierRequest`](crate::WriteDataByIdentifierRequest), and [`ReadDTCInfoRequest`](crate::ReadDTCInfoRequest) - /// Responses: [`ReadDataByIdentifierResponse`](crate::ReadDataByIdentifierResponse), [`WriteDataByIdentifierResponse`](crate::WriteDataByIdentifierResponse), and [`ReadDTCInfoResponse`](crate::ReadDTCInfoResponse) - type DID: Identifier - + Clone - + std::fmt::Debug - + Send - + Sync - + PartialEq - + 'static - + maybe_serde::Bound - + maybe_utoipa::Bound; - /// Response payload for [`ReadDataByIdentifierRequest`](crate::ReadDataByIdentifierRequest) - type DiagnosticPayload: SingleValueWireFormat - + IterableWireFormat - + Clone - + std::fmt::Debug - + Send - + Sync - + PartialEq - + maybe_serde::Bound - + maybe_utoipa::Bound - + 'static; - - /// UDS Routine Identifier + /// Returns `Ok(None)` when the buffer is empty (sequence exhausted). /// - /// This is used to identify the routine to be controlled in a [`RoutineControlRequest`](crate::RoutineControlRequest) - type RID: RoutineIdentifier - + Clone - + std::fmt::Debug - + Send - + Sync - + PartialEq - + 'static - + maybe_serde::Bound - + maybe_utoipa::Bound; - /// Payload for both requests and responses of [`RoutineControlRequest`](crate::RoutineControlRequest) and [`RoutineControlResponse`](crate::RoutineControlResponse) - type RoutinePayload: SingleValueWireFormat - + IterableWireFormat - + Clone - + std::fmt::Debug - + Send - + Sync - + PartialEq - + 'static - + maybe_serde::Bound - + maybe_utoipa::Bound; -} - -/// tests -#[cfg(test)] -mod tests { - use super::*; - use crate::{Identifier, UDSIdentifier}; - use byteorder::ReadBytesExt; - use std::io::Cursor; - - #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - #[repr(u16)] - pub enum MyIdentifier { - Identifier1 = 0x0101, - Identifier2 = 0x0202, - Identifier3 = 0x0303, - UDSIdentifier(UDSIdentifier), - } - impl_identifier!(MyIdentifier); - - impl From for MyIdentifier { - fn from(value: u16) -> Self { - match value { - 0x0101 => MyIdentifier::Identifier1, - 0x0202 => MyIdentifier::Identifier2, - 0x0303 => MyIdentifier::Identifier3, - _ => MyIdentifier::UDSIdentifier(UDSIdentifier::try_from(value).unwrap()), - } - } - } - - impl From for u16 { - fn from(value: MyIdentifier) -> Self { - match value { - MyIdentifier::Identifier1 => 0x0101, - MyIdentifier::Identifier2 => 0x0202, - MyIdentifier::Identifier3 => 0x0303, - MyIdentifier::UDSIdentifier(identifier) => u16::from(identifier), - } - } - } - - #[derive(Debug)] - pub struct MyPayload { - identifier: MyIdentifier, - u8_value: u8, - } - - #[test] - fn test_identifier() { - let mut buffer = Cursor::new(vec![0u8; 2]); - let identifier = MyIdentifier::Identifier1; - identifier.encode(&mut buffer).unwrap(); - buffer.set_position(0); - let read_identifier = MyIdentifier::parse_from_list(&mut buffer).unwrap(); - assert_eq!(identifier, read_identifier[0]); - } - - #[test] - #[allow(clippy::match_same_arms)] - fn test_payload() { - let mut buffer = Cursor::new(vec![0x01, 0x01, 0xFF, 0x02, 0x02, 0xFF, 0x03, 0x03]); - // Read until the end of the buffer - while let Some(identifier) = MyIdentifier::parse_from_payload(&mut buffer).unwrap() { - match identifier { - MyIdentifier::Identifier1 | MyIdentifier::Identifier2 => { - let payload = MyPayload { - identifier, - u8_value: buffer.read_u8().unwrap(), - }; - assert!(matches!( - payload.identifier, - MyIdentifier::Identifier1 | MyIdentifier::Identifier2 - )); - assert_eq!(payload.u8_value, 0xFF); - } - MyIdentifier::Identifier3 => (), - MyIdentifier::UDSIdentifier(_) => (), - } - } - println!("Testing printing"); - } + /// # Errors + /// Returns an error if the buffer contains a partial or invalid item. + fn decode_next(buf: &'a [u8]) -> Result, Error>; }