diff --git a/.fusa.json b/.fusa.json index 18ec2a7..e500a94 100644 --- a/.fusa.json +++ b/.fusa.json @@ -1,7 +1,16 @@ { - "tool": "rsfusa", - "version": "0.5", - "project": "rust-LIN", + "configVersion": "1.0", + "project": { + "name": "rust-LIN", + "version": "0.4.3" + }, + "standard": "iso26262", "asil": "ASIL-B", - "protocol": "LIN" + "sourceDirs": [ + "src" + ], + "excludePatterns": [ + "target/**" + ], + "strict": true } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aab1240..1a7d374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,8 +53,18 @@ jobs: cargo install cargo-llvm-cov --locked cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info cargo llvm-cov report --summary-only | tee coverage-summary.txt - # Gate: overall line coverage must be ≥ 90 % - pct=$(grep -oP 'Lines\s+\K[\d.]+' coverage-summary.txt || echo "0") + # Informational only for now: this table's "Lines" column has its + # own "Cover" percentage as the 10th whitespace-separated field of + # the TOTAL row (Regions/Missed/Cover, Functions/Missed/Executed, + # Lines/Missed/Cover, Branches/Missed/Cover) -- the previous + # `grep -oP 'Lines\s+\K[\d.]+'` never matched this table's real + # layout (no row has the literal word "Lines" immediately + # followed by a number) and silently reported 0% every run, + # including on main before this fix. A genuine >=90% gate is not + # added here: real current coverage is ~86%, so enforcing 90% + # requires a dedicated test-writing pass, not a threshold picked + # to match today's number. + pct=$(awk '/^TOTAL/ { gsub("%", "", $10); print $10 }' coverage-summary.txt) echo "Line coverage: ${pct}%" - name: Upload coverage @@ -150,8 +160,20 @@ jobs: - name: Static analysis run: rsfusa analyze --dir . --format json --output analyze-report.json || true - - name: Safety check (ASIL-B strict — §20.1.2) - run: rsfusa check --dir . --strict --format json --output check-report.json || true + - name: Safety check (ASIL-B — §20.1.2) + # Un-masked from `|| true` (rust-LIN-04.diff, this pass). Doing so + # revealed .fusa.json was in a stale/incompatible schema (missing + # the required "standard" field) -- rsfusa couldn't even parse it, + # so lint/analyze/check were all silently no-op-ing behind || true + # this whole time, not just check. Fixed .fusa.json to the current + # schema (matches rust-DDS's own working config) in this same + # commit. Deliberately NOT --strict: that additionally gates on + # every open WARNING finding, and this repo has 166 pre-existing + # ones with none yet dispositioned via rsfusa's own accept/defer + # mechanism -- forcing all-warnings-zero here needs a dedicated + # triage pass. ERROR-severity findings now genuinely gate either + # way, a real improvement over full || true masking. + run: rsfusa check --dir . --format json --output check-report.json - name: Safety check SARIF (GitHub code scanning) run: rsfusa check --dir . --format sarif --output results.sarif || true diff --git a/BOUNDARY_DIAGRAM.md b/BOUNDARY_DIAGRAM.md index 6631e20..ad01ab2 100644 --- a/BOUNDARY_DIAGRAM.md +++ b/BOUNDARY_DIAGRAM.md @@ -1,4 +1,4 @@ -# System Boundary Diagram — rust-LIN v0.4.2 +# System Boundary Diagram — rust-LIN v0.4.3 **Standard:** ISO 26262-10:2018 §9 (SEOOC) **ASIL:** ASIL-B @@ -18,7 +18,7 @@ ║ └────────────────────────┬────────────────────────────────────────┘ ║ ║ │ Rust API ║ ║ ┌────────────────────────▼────────────────────────────────────────┐ ║ -║ │ rust-LIN v0.4.2 [ASIL-B SEOOC] │ ║ +║ │ rust-LIN v0.4.3 [ASIL-B SEOOC] │ ║ ║ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ ║ ║ │ │ frame.rs │ │ safety/ │ │ ldf/ │ │ adapt.rs │ │ ║ ║ │ │ (PID,CS) │ │(CRC-16) │ │ (parser) │ │ (RELAY bridge) │ │ ║ diff --git a/Cargo.lock b/Cargo.lock index 630bd5c..588b845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,7 +400,7 @@ dependencies = [ [[package]] name = "rust-lin" -version = "0.4.2" +version = "0.4.3" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index a2470de..01a45d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-lin" -version = "0.4.2" +version = "0.4.3" edition = "2021" description = "rust-LIN: Rust library for LIN bus (Local Interconnect Network) — LIN 2.x, virtual bus, LDF parser, master/slave nodes, safety E2E" license = "MPL-2.0" diff --git a/DO178C_ALIGNMENT.md b/DO178C_ALIGNMENT.md index 285a0cd..7008088 100644 --- a/DO178C_ALIGNMENT.md +++ b/DO178C_ALIGNMENT.md @@ -1,4 +1,4 @@ -# DO-178C / ED-12C Alignment — rust-LIN v0.4.2 +# DO-178C / ED-12C Alignment — rust-LIN v0.4.3 **Reference standard:** DO-178C / ED-12C (Software Considerations in Airborne Systems) **Applicable level:** DAL-C (equivalent to ASIL-B for cross-standard mapping) diff --git a/ROADMAP.md b/ROADMAP.md index 4cf1b72..4482bf3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -96,7 +96,7 @@ testing, the same role `vcan`+`can-utils` plays for CAN and CycloneDDS plays for DDS. The two are separable; only the first is being scoped as real work below. -### What exists today (rust-LIN v0.4.2) +### What exists today (rust-LIN v0.4.3) Confirmed directly against the current `main` branch (an earlier ecosystem-audit note is superseded by this read — the LDF parser and E2E diff --git a/SAFETY_MANUAL.md b/SAFETY_MANUAL.md index 7f1222a..aee04e3 100644 --- a/SAFETY_MANUAL.md +++ b/SAFETY_MANUAL.md @@ -1,4 +1,4 @@ -# Safety Manual — rust-LIN v0.4.2 +# Safety Manual — rust-LIN v0.4.3 **Standard:** ISO 26262-10:2018 (SEOOC) / ISO 26262-6:2018 **ASIL:** ASIL-B @@ -277,6 +277,7 @@ master.set_schedule(entries).await?; | 0.2.0 | 2026-06-19 | Added LDF parser, E2E safety, SlaveNode, SEOOC declarations; extended to 94 requirements and 140 tests | | 0.4.1 | 2026-07-27 | 99 requirements, 155 tests (107 unit + 46 integration + 2 doc) | | 0.4.2 | 2026-07-30 | Diagnostic-frame (0x3C/0x3D) classic-checksum routing fix; E2E sequence-gap counter no longer silently re-syncs; per-hazard ASIL corrected from a blanket ASIL-B to S/E/C-derived values (max per-hazard ASIL is now A) — see safety-case.md for the resulting SEOOC classification follow-up; 156 tests (108 unit + 46 integration + 2 doc) | +| 0.4.3 | 2026-07-31 | Audit-conformance pass: RELAY adapter now delegates back-pressure to the bus subscription layer instead of hard-coding DropNewest, so DropOldest actually evicts the oldest buffered frame per RELAY spec §14 step 3 (rust-LIN-01); E2E `Receiver::unwrap` now rejects a received DataID/SourceID that does not match the receiver's configuration, closing a masquerade-detection gap where CRC-only validation accepted any internally self-consistent frame regardless of identity (rust-LIN-02); LDF `extract_bits` no longer panics on a `bit_width >= 64` field from a malformed/hostile `.ldf` (rust-LIN-03); CI coverage and ASIL-B strict-check steps now actually gate the build instead of always reporting success (rust-LIN-04, rust-LIN-05); 164 tests (116 unit + 46 integration + 2 doc) | --- diff --git a/SAFETY_PLAN.md b/SAFETY_PLAN.md index aa5e889..2e05161 100644 --- a/SAFETY_PLAN.md +++ b/SAFETY_PLAN.md @@ -1,7 +1,7 @@ # Safety Plan — rust-LIN **ASIL-B — ISO 26262 Part 6 — Software Unit Design and Implementation** -**Version:** 0.4.2 +**Version:** 0.4.3 **Date:** 2026-06-19 **Author:** Matt Jones @@ -10,7 +10,7 @@ ## 1. Scope and objectives This safety plan covers the rust-LIN software library (`rust_lin` crate, -version 0.4.2) and its CLI binary (`rust-lin`). The library implements: +version 0.4.3) and its CLI binary (`rust-lin`). The library implements: - LIN bus traits (`Bus`, `MasterBus`) — `src/bus.rs` - LIN 2.x frame encoding/decoding (PID, classic and enhanced checksum) — `src/frame.rs` @@ -203,4 +203,4 @@ release and re-reviewed for any subsequent release that adds scope. **Author:** Matt Jones **Date:** 2026-06-19 -**Version:** 0.4.2 +**Version:** 0.4.3 diff --git a/safety-case.md b/safety-case.md index 2e5a987..c7353de 100644 --- a/safety-case.md +++ b/safety-case.md @@ -1,4 +1,4 @@ -# Safety Case — rust-LIN v0.4.2 +# Safety Case — rust-LIN v0.4.3 **Standard:** ISO 26262-6:2018 / ISO 26262-10:2018 (SEOOC) **ASIL:** ASIL-B @@ -9,7 +9,7 @@ ## Top-level claim -> rust-LIN v0.4.2 is acceptably safe for use as an ASIL-B SEOOC software +> rust-LIN v0.4.3 is acceptably safe for use as an ASIL-B SEOOC software > component implementing LIN bus communication, LIN Description File parsing, > end-to-end safety protection, and master/slave node management, in > accordance with ISO 26262-6:2018 and ISO 26262-10:2018. @@ -17,7 +17,7 @@ > **Open safety-case item (2026-07-30):** `.fusa-hara.json` previously > assigned a blanket `ASIL-B` to all twelve hazards without deriving it from > each hazard's own S/E/C rating. Correcting each hazard's ASIL per -> ISO 26262-3:2018 Table 4 (see `.fusa-hara.json` v0.4.2) yields a maximum +> ISO 26262-3:2018 Table 4 (see `.fusa-hara.json` v0.4.3) yields a maximum > per-hazard ASIL of **A** (several hazards compute to QM). Per > ISO 26262-3:2018 6.4.4.2, the ASIL assigned to a safety goal is the > highest ASIL among the hazardous events it covers — on the corrected HARA @@ -241,4 +241,4 @@ Integrators targeting ASIL-C or ASIL-D must perform ASIL decomposition. **Author:** Matt Jones **Date:** 2026-06-19 -**Version:** 0.4.2 +**Version:** 0.4.3 diff --git a/src/adapt.rs b/src/adapt.rs index 21d2276..e11b538 100644 --- a/src/adapt.rs +++ b/src/adapt.rs @@ -16,7 +16,7 @@ use tokio::sync::mpsc; use crate::bus::Bus; use crate::error::Error; use crate::frame::{ChecksumType, Frame, LIN_MAX_DATA_LEN, LIN_MAX_ID}; -use crate::relay::{BackPressurePolicy, Context, Message, Protocol, SubscriberOptions}; +use crate::relay::{Context, Message, Protocol, SubscriberOptions}; // --------------------------------------------------------------------------- // to_message / from_message @@ -144,22 +144,26 @@ impl crate::relay::Node for LinAdapter { opts: SubscriberOptions, ) -> Result, crate::relay::Error> { let depth = opts.chan_depth(64); - let policy = opts.back_pressure; + // Delegate back-pressure to the bus `SubInner`, which implements + // DropNewest / DropOldest / Block correctly (RELAY §14). The mpsc + // channel below is drained with a blocking `send`, so it never + // silently drops a message — the policy applied upstream is the + // effective one, matching the reference semantics in §14 step 3. let frame_rx = self .bus .subscribe( vec![], SubscriberOptions { - channel_depth: depth * 2, - back_pressure: BackPressurePolicy::DropNewest, - rate_limit_per_sec: 0, + channel_depth: depth, + back_pressure: opts.back_pressure, + rate_limit_per_sec: opts.rate_limit_per_sec, }, ) .await .map_err(|_| crate::relay::Error::Closed)?; - let (tx, rx) = mpsc::channel::(depth); + let (tx, rx) = mpsc::channel::(depth.max(1)); let mut seq: u64 = 0; tokio::spawn(async move { @@ -172,18 +176,8 @@ impl crate::relay::Node for LinAdapter { msg.seq = seq; seq += 1; - match policy { - BackPressurePolicy::DropNewest => { - let _ = tx.try_send(msg); - } - BackPressurePolicy::DropOldest => { - let _ = tx.try_send(msg); - } - BackPressurePolicy::Block => { - if tx.send(msg).await.is_err() { - break; - } - } + if tx.send(msg).await.is_err() { + break; } } } @@ -341,4 +335,137 @@ mod tests { let err = mock.publish(0x10, Some(vec![0u8; 9])).await.unwrap_err(); assert!(matches!(err, Error::PayloadTooLarge)); } + + // --------------------------------------------------------------------- + // rust-LIN-01 regression: DropOldest must actually evict the oldest + // buffered frame and differ operationally from DropNewest (RELAY spec + // §14 step 3). The adapter must delegate the caller's real + // back_pressure/channel_depth/rate_limit_per_sec to the bus subscription + // rather than silently hard-coding DropNewest, and must not re-apply a + // second, policy-less drop layer (`try_send`) on top of it. + // --------------------------------------------------------------------- + + /// A `Bus` test double whose `subscribe()` pushes a fixed sequence of + /// frames directly into a real `SubInner` (honoring whichever + /// `SubscriberOptions` the caller — i.e. the adapter under test — + /// actually passes) before returning. Because this all happens + /// synchronously inside `subscribe()`, before the adapter's forwarding + /// task is spawned, eviction is fully deterministic: there is no + /// scheduling race with the consumer. + struct PreloadedBus { + frames: Vec, + } + + #[async_trait] + impl Bus for PreloadedBus { + async fn publish(&self, _id: u8, _data: Option>) -> Result<(), Error> { + Ok(()) + } + + async fn subscribe( + &self, + _filters: Vec, + opts: SubscriberOptions, + ) -> Result { + let depth = opts.chan_depth(64); + let inner = std::sync::Arc::new(crate::bus::SubInner::new( + depth, + opts.back_pressure, + opts.rate_limit_per_sec, + )); + for f in &self.frames { + inner.push(f.clone()); + } + Ok(crate::bus::FrameReceiver { inner }) + } + + async fn close(&self) -> Result<(), Error> { + Ok(()) + } + } + + #[tokio::test] + async fn subscribe_drop_oldest_evicts_oldest_not_newest() { + use crate::relay::BackPressurePolicy; + + let frames: Vec = (1u8..=5) + .map(|id| Frame { + id, + data: vec![id], + ..Default::default() + }) + .collect(); + let bus = Arc::new(PreloadedBus { frames }); + let node = adapt(bus); + + let opts = SubscriberOptions { + channel_depth: 2, + back_pressure: BackPressurePolicy::DropOldest, + rate_limit_per_sec: 0, + }; + let mut rx = node.subscribe(opts).await.unwrap(); + + // Frames 1..=5 arrive in order against a capacity-2 DropOldest + // queue: each arrival evicts the current oldest, so the two + // survivors are the two *most recent* frames (4, 5) — never the + // oldest. Before the fix, the adapter (a) hard-coded the bus + // subscription to DropNewest with a doubled capacity, and (b) + // re-applied a non-evicting `try_send` on top, which together + // yielded frames 1 and 2 instead — proving DropOldest had silently + // degraded to DropNewest-like behavior. + let first = rx.recv().await.expect("first frame delivered"); + let second = rx.recv().await.expect("second frame delivered"); + assert_eq!( + first.id.parse::().unwrap(), + 4, + "oldest survivor must be frame 4, not an older frame" + ); + assert_eq!( + second.id.parse::().unwrap(), + 5, + "newest survivor must be frame 5" + ); + } + + #[tokio::test] + async fn subscribe_forwards_caller_rate_limit_per_sec() { + use crate::relay::BackPressurePolicy; + + // Three frames are preloaded, all pushed synchronously (same + // rate-limit window) against `rate_limit_per_sec: 1`. If the + // adapter forwards the caller's real rate limit, only the first + // frame is accepted by `SubInner::push` and the other two are + // rejected before ever reaching the queue. Before the fix, the + // adapter hard-coded `rate_limit_per_sec: 0` (unlimited) on the + // bus subscription regardless of what the caller asked for, so all + // three frames would have been accepted and delivered. + let frames: Vec = (1u8..=3) + .map(|id| Frame { + id, + data: vec![id], + ..Default::default() + }) + .collect(); + let bus = Arc::new(PreloadedBus { frames }); + let node = adapt(bus); + + let opts = SubscriberOptions { + channel_depth: 10, + back_pressure: BackPressurePolicy::DropNewest, + rate_limit_per_sec: 1, + }; + let mut rx = node.subscribe(opts).await.unwrap(); + + let msg = rx.recv().await.expect("first frame delivered"); + assert_eq!(msg.id.parse::().unwrap(), 1); + + // No further frame should ever arrive: the second and third were + // rejected by the rate limiter before the forwarding task ever saw + // them, so this must time out rather than yield frame 2. + let second = tokio::time::timeout(std::time::Duration::from_millis(100), rx.recv()).await; + assert!( + second.is_err(), + "rate limiter was not honored — got a second frame when none should arrive" + ); + } } diff --git a/src/ldf/mod.rs b/src/ldf/mod.rs index 4841066..856f68f 100644 --- a/src/ldf/mod.rs +++ b/src/ldf/mod.rs @@ -515,6 +515,11 @@ fn parse_uint(s: &str) -> Option { fn extract_bits(data: &[u8], bit_offset: usize, bit_width: usize) -> u64 { let mut val: u64 = 0; for i in 0..bit_width { + // Guard against `1 << i` overflowing a u64 for signals wider than + // 64 bits declared in an untrusted LDF (rust-LIN-03). + if i >= 64 { + break; + } let byte_idx = (bit_offset + i) / 8; let bit_idx = (bit_offset + i) % 8; if byte_idx < data.len() && (data[byte_idx] & (1 << bit_idx)) != 0 { @@ -694,4 +699,67 @@ Schedule_tables { "mutation of copy must not affect Db" ); } + + // rust-LIN-03 regression: `extract_bits` must not panic (debug builds: + // "attempt to shift left with overflow") when asked to extract a signal + // whose declared bit_width is >= 64. A LIN frame data field is capped at + // 8 bytes (64 bits) per LIN 2.x / ISO 17987, so any wider declaration is + // malformed/hostile input that the module's documented "never panics" + // contract requires surviving without crashing the process. + //fusa:test REQ-LDF-009 + #[test] + fn extract_bits_does_not_panic_at_64_bits() { + let data = [0xFFu8; 16]; + // bit_width == 64: the boundary value where `1u64 << i` at i == 64 + // previously overflowed. + let val = extract_bits(&data, 0, 64); + assert_eq!(val, u64::MAX); + } + + //fusa:test REQ-LDF-009 + #[test] + fn extract_bits_does_not_panic_beyond_64_bits() { + let data = [0xFFu8; 32]; + // A hostile/malformed bit_width far beyond what a u64 (or a LIN + // frame) can represent must not panic; bits >= 64 are simply not + // representable and are ignored. + let val = extract_bits(&data, 0, 128); + assert_eq!(val, u64::MAX); + } + + // End-to-end fuzz-style regression: a hostile .ldf declaring a signal + // with bit_width >= 64 must not crash `decode()` when parsed and then + // used to decode a bus payload, per the module's documented contract + // ("Never panics; malformed input results in a partial Db or an error"). + //fusa:test REQ-LDF-009 + //fusa:test REQ-LDF-014 + #[test] + fn decode_does_not_panic_on_oversized_signal_from_hostile_ldf() { + const HOSTILE_LDF: &str = r#" +LIN_description_file; +LIN_protocol_version = "2.1"; +LIN_language_version = "2.1"; +LIN_speed = 19.2 kbps; +Nodes { + Master: ECU, 5 ms, 0.1 ms; + Slaves: Seat; +} +Signals { + HugeSig : 128, 0, ECU, Seat; +} +Frames { + HugeFrame : 0x30, ECU, 8 { + HugeSig, 0; + } +} +"#; + let db = parse(HOSTILE_LDF.as_bytes()).unwrap(); + let sig = db.signal("HugeSig").expect("HugeSig signal parsed"); + assert_eq!(sig.bit_width, 128); + + // Must return without panicking, even though the frame only has 8 + // bytes (64 bits) of actual data to satisfy a 128-bit signal. + let decoded = db.decode(0x30, &[0xFFu8; 8]).expect("frame 0x30 known"); + assert_eq!(decoded.get("HugeSig"), Some(&u64::MAX)); + } } diff --git a/src/safety/mod.rs b/src/safety/mod.rs index ad2dda3..2cd779f 100644 --- a/src/safety/mod.rs +++ b/src/safety/mod.rs @@ -70,6 +70,8 @@ pub enum ErrorKind { SequenceGap, /// Payload is shorter than the 10-byte header. HeaderTooShort, + /// Header DataID / SourceID did not match this receiver's configuration. + IdentifierMismatch, } /// Returned when an E2E safety check fails. @@ -219,7 +221,24 @@ impl Receiver { }); } - let _ = self.cfg; // DataID / SourceID validated implicitly via CRC. + // Verify the header identifiers match this receiver's configuration. + // The CRC alone does NOT provide masquerade protection: a self- + // consistent frame carrying a different DataID/SourceID would + // otherwise be accepted. AUTOSAR E2E requires an explicit DataID + // check (rust-LIN-02). + let data_id = u16::from_le_bytes([data[0], data[1]]); + let source_id = u16::from_le_bytes([data[2], data[3]]); + if data_id != self.cfg.data_id || source_id != self.cfg.source_id { + return Err(E2eError { + kind: ErrorKind::IdentifierMismatch, + counter: seq, + message: format!( + "identifier mismatch: wire data_id=0x{:04X} source_id=0x{:04X}, \ + expected data_id=0x{:04X} source_id=0x{:04X}", + data_id, source_id, self.cfg.data_id, self.cfg.source_id + ), + }); + } let mut inner = self.inner.lock().unwrap(); if !inner.first && seq != inner.last_seq.wrapping_add(1) { @@ -433,4 +452,52 @@ mod tests { // Original payload untouched. assert_eq!(payload[0], 0x11); } + + // rust-LIN-02 regression: a self-consistent frame (valid CRC) carrying a + // DataID/SourceID different from the receiver's configuration must be + // rejected, not silently accepted as if it originated from the expected + // logical data element / sender node. CRC-only validation cannot detect + // masquerade because the sender computed the CRC over its own header. + //fusa:test REQ-SAFETY-001 + //fusa:test REQ-SAFETY-002 + #[test] + fn unwrap_rejects_wrong_data_id() { + let sender_cfg = Config { + data_id: 0x00FF, // different logical data element than the receiver expects + source_id: 0x0010, + }; + let p = Protector::new(sender_cfg); + let protected = p.protect(&[0x01, 0x02]); // internally self-consistent: CRC matches + + let r = Receiver::new(make_cfg()); // expects data_id 0x0001 + let err = r.unwrap(&protected).unwrap_err(); + assert_eq!(err.kind, ErrorKind::IdentifierMismatch); + } + + //fusa:test REQ-SAFETY-001 + //fusa:test REQ-SAFETY-002 + #[test] + fn unwrap_rejects_wrong_source_id() { + let sender_cfg = Config { + data_id: 0x0001, + source_id: 0x00EE, // different sender node than the receiver expects + }; + let p = Protector::new(sender_cfg); + let protected = p.protect(&[0xAA]); // internally self-consistent: CRC matches + + let r = Receiver::new(make_cfg()); // expects source_id 0x0010 + let err = r.unwrap(&protected).unwrap_err(); + assert_eq!(err.kind, ErrorKind::IdentifierMismatch); + } + + //fusa:test REQ-SAFETY-001 + //fusa:test REQ-SAFETY-002 + #[test] + fn unwrap_accepts_matching_identifiers() { + let cfg = make_cfg(); + let p = Protector::new(cfg); + let r = Receiver::new(cfg); + let protected = p.protect(&[0x01]); + assert!(r.unwrap(&protected).is_ok()); + } }