1/*
2 * SPDX-FileCopyrightText: 2026 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14use std::{
15 path::PathBuf,
16 sync::{Arc, RwLock as StdRwLock},
17 time::Duration,
18};
19
20pub mod request_guard;
21
22use aide::{
23 axum::{
24 ApiRouter as Router,
25 routing::{self, get_with},
26 },
27 transform::TransformOperation,
28};
29use async_trait::async_trait;
30use axum::{
31 Json,
32 body::Bytes,
33 extract::{Query, State},
34 http::{HeaderMap, HeaderValue, StatusCode, header::RETRY_AFTER},
35 middleware,
36 response::{IntoResponse, Response},
37};
38use axum_extra::extract::WithRejection;
39use cda_interfaces::{
40 Connectivity, FunctionalDescriptionConfig, HashMap, HashMapExtensions as _, SchemaProvider,
41 UdsEcu, VariantState,
42 communication_control::{ActivationCause, CommunicationAccess, CommunicationGuard},
43 datatypes::ComponentsConfig,
44 diagservices::{FieldParseError, UdsPayloadData},
45 file_manager::FileManager,
46 runtime_update_api::LockStateProvider,
47 util::std_ext::lock_write,
48};
49use cda_plugin_security::{SecurityPluginLoader, security_plugin_middleware};
50use error::{ApiError, api_error_from_diag_response};
51use http::{Uri, header};
52use indexmap::IndexMap;
53pub use locks::Locks;
54use schemars::Schema;
55use sovd_interfaces::{
56 IncludeSchemaQuery, Resource,
57 components::{ComponentsResponse, ecu as sovd_ecu},
58 error::DataError,
59};
60use tokio::sync::RwLock;
61use uuid::Uuid;
62
63use crate::{
64 VendorErrorCode,
65 sovd::components::ecu::{
66 configurations, data, data_categories, faults, genericservice, modes, operations,
67 x_single_ecu_jobs, x_sovd2uds_bulk_data, x_sovd2uds_download,
68 },
69};
70
71pub(crate) mod apps;
72pub(crate) mod components;
73pub(crate) mod docs;
74pub(crate) mod error;
75pub(crate) mod functions;
76mod lock_state;
77pub(crate) mod locks;
78
79trait IntoSovd {
80 type SovdType;
81 fn into_sovd(self) -> Self::SovdType;
82}
83
84trait IntoSovdWithSchema {
85 type SovdType;
86 fn into_sovd_with_schema(self, include_schema: bool) -> Result<Self::SovdType, ApiError>;
87}
88
89impl IntoSovd for cda_interfaces::EcuState {
90 type SovdType = sovd_ecu::State;
91
92 fn into_sovd(self) -> Self::SovdType {
93 match (&self.connectivity, &self.variant_state) {
94 (_, VariantState::Duplicate) => sovd_ecu::State::Duplicate,
95 (Connectivity::Online, VariantState::Detected { .. }) => sovd_ecu::State::Online,
96 (Connectivity::Online, VariantState::NotDetected) => sovd_ecu::State::NoVariantDetected,
97 (Connectivity::Online, VariantState::NotTested) => sovd_ecu::State::NotTested,
98 (Connectivity::Offline, VariantState::NotTested) => sovd_ecu::State::Offline,
99 (Connectivity::Offline, VariantState::Detected { .. } | VariantState::NotDetected) => {
100 sovd_ecu::State::Disconnected
101 }
102 }
103 }
104}
105
106#[derive(Debug, thiserror::Error)]
107pub(crate) enum SovdError {
108 #[error("Failed to create route: {0}")]
109 RouteError(String),
110}
111
112/// Synchronous lock for short execution-map operations so reservation cleanup can run in `Drop`.
113/// Lock guards must never be held across an `.await`.
114pub(crate) type ExecutionLock<E> = StdRwLock<HashMap<String, IndexMap<Uuid, E>>>;
115
116#[derive(Clone)]
117pub(crate) struct WebserverEcuState<T: UdsEcu + Clone, U: FileManager> {
118 ecu_name: String,
119 uds: T,
120 locks: Arc<Locks>,
121 // Map of Execution Id -> ComParamMap
122 comparam_executions: Arc<RwLock<IndexMap<Uuid, ComparamExecution>>>,
123 communication_access: Arc<dyn CommunicationAccess>,
124 // Map of Service Name -> (Execution Id -> ServiceExecution) for ECU routine operations
125 pub(crate) service_executions: Arc<ExecutionLock<ServiceExecution>>,
126 flash_data: Arc<RwLock<sovd_interfaces::sovd2uds::FileList>>,
127 flash_functional_class_default: Arc<str>,
128 mdd_embedded_files: Arc<U>,
129}
130
131pub(crate) fn with_retry_after(mut response: Response, retry_after: Option<Duration>) -> Response {
132 if let Some(retry_after) = retry_after {
133 let seconds = retry_after.as_secs();
134 let value = HeaderValue::try_from(seconds.to_string())
135 .expect("numeric Retry-After value is always valid");
136 response.headers_mut().insert(RETRY_AFTER, value);
137 }
138 response
139}
140
141/// Acquires a communication lease or starts authorized on-demand activation before
142/// returning a retryable SOVD error.
[docs] 143/// [[ dimpl~deferred-sovd-admission, Deferred diagnostic request admission, dimpl ]]
144pub(crate) fn acquire_communication_activity(
145 communication_access: &dyn CommunicationAccess,
146) -> Result<CommunicationGuard, ApiError> {
147 match communication_access.acquire() {
148 Ok(activity) => Ok(activity),
149 Err(error) => {
150 communication_access.request_activate(ActivationCause::DiagnosticRequest);
151 Err(ApiError::from_communication_error(
152 error,
153 communication_access.retry_after(),
154 ))
155 }
156 }
157}
158
159/// Shared behavior for execution-state types (`ServiceExecution` for single-ECU
160/// operations, `FgServiceExecution` for functional-group operations).
161pub(crate) trait ExecutionStatus {
162 fn execution_status(&self) -> &sovd_ecu::operations::ExecutionStatus;
163 /// Returns `true` if an HTTP handler currently has exclusive processing access to this
164 /// execution. Used to avoid sending multiple UDS requests simultaneously for the same
165 /// execution.
166 fn is_in_flight(&self) -> bool;
167 /// Sets whether an HTTP handler currently has exclusive processing access to this execution.
168 /// This is independent of the diagnostic execution status.
169 fn set_in_flight(&mut self, in_flight: bool);
170 /// Should return `false` if an execution was created as a placeholder,
171 /// but not finalized.
172 fn is_created(&self) -> bool;
173 fn set_created(&mut self, created: bool);
174 /// Creates a placeholder entry used to reserve a slot in the executions map
175 /// before the UDS command is sent.
176 fn placeholder(communication_guard: CommunicationGuard) -> Self
177 where
178 Self: Sized;
179}
180
181/// Rolls back a newly inserted execution unless it is committed as asynchronous.
182/// The execution entry owns the communication lease; rollback releases it by removing the entry.
183pub(crate) struct ExecutionReservation<E> {
184 executions: Arc<ExecutionLock<E>>,
185 service: String,
186 exec_id: Uuid,
187 state: ReservationState,
188}
189
190#[derive(PartialEq)]
191enum ReservationState {
192 Pending,
193 Committed,
194}
195
196impl<E> ExecutionReservation<E> {
197 fn new(executions: Arc<ExecutionLock<E>>, service: String, exec_id: Uuid) -> Self {
198 Self {
199 executions,
200 service,
201 exec_id,
202 state: ReservationState::Pending,
203 }
204 }
205
206 /// Commits the reserved entry as a persistent asynchronous execution.
207 ///
208 /// The entry already owns the communication lease. While the reservation is pending, its
209 /// `Drop` removes that entry as a rollback. Committing disables only that rollback; the entry
210 /// remains stored and releases its lease automatically when it is completed or removed.
211 pub(crate) fn commit_async(mut self, update_fn: impl FnOnce(&mut E)) -> Uuid
212 where
213 E: ExecutionStatus,
214 {
215 finalize_execution(&self.executions, &self.service, &self.exec_id, update_fn);
216 self.state = ReservationState::Committed;
217 self.exec_id
218 }
219}
220
221impl<E> Drop for ExecutionReservation<E> {
222 fn drop(&mut self) {
223 if self.state == ReservationState::Pending {
224 remove_reserved_execution(&self.executions, &self.service, &self.exec_id);
225 }
226 }
227}
228
229/// Owns a communication-use slot; dropping it releases that slot.
230struct ExecutionLease {
231 _guard: CommunicationGuard,
232}
233
234impl std::fmt::Debug for ExecutionLease {
235 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 formatter.write_str("ExecutionLease")
237 }
238}
239
240pub(crate) struct ComparamExecution {
241 execution: sovd_ecu::operations::comparams::Execution,
242 _lease: ExecutionLease,
243}
244
245impl ComparamExecution {
246 pub(crate) fn new(
247 execution: sovd_ecu::operations::comparams::Execution,
248 communication_guard: CommunicationGuard,
249 ) -> Self {
250 Self {
251 execution,
252 _lease: ExecutionLease {
253 _guard: communication_guard,
254 },
255 }
256 }
257
258 pub(crate) fn snapshot(&self) -> sovd_ecu::operations::comparams::Execution {
259 self.execution.clone()
260 }
261}
262
263impl std::ops::Deref for ComparamExecution {
264 type Target = sovd_ecu::operations::comparams::Execution;
265
266 fn deref(&self) -> &Self::Target {
267 &self.execution
268 }
269}
270
271impl std::ops::DerefMut for ComparamExecution {
272 fn deref_mut(&mut self) -> &mut Self::Target {
273 &mut self.execution
274 }
275}
276
277/// Stored state for a single ECU routine execution (async lifecycle).
278#[derive(Debug)]
279pub(crate) struct ServiceExecution {
280 pub parameters: serde_json::Map<String, serde_json::Value>,
281 pub status: sovd_ecu::operations::ExecutionStatus,
282 pub in_flight: bool,
283 pub is_created: bool,
284 communication_lease: Option<ExecutionLease>,
285}
286
287impl Clone for ServiceExecution {
288 fn clone(&self) -> Self {
289 Self {
290 parameters: self.parameters.clone(),
291 status: self.status.clone(),
292 in_flight: self.in_flight,
293 is_created: self.is_created,
294 communication_lease: None,
295 }
296 }
297}
298
299impl ServiceExecution {
300 pub(crate) fn complete(&mut self) {
301 self.status = sovd_ecu::operations::ExecutionStatus::Completed;
302 self.communication_lease = None;
303 }
304}
305
306impl ExecutionStatus for ServiceExecution {
307 fn execution_status(&self) -> &sovd_ecu::operations::ExecutionStatus {
308 &self.status
309 }
310 fn is_in_flight(&self) -> bool {
311 self.in_flight
312 }
313 fn set_in_flight(&mut self, in_flight: bool) {
314 self.in_flight = in_flight;
315 }
316 fn is_created(&self) -> bool {
317 self.is_created
318 }
319 fn set_created(&mut self, created: bool) {
320 self.is_created = created;
321 }
322 fn placeholder(communication_guard: CommunicationGuard) -> Self {
323 ServiceExecution {
324 parameters: serde_json::Map::new(),
325 status: sovd_ecu::operations::ExecutionStatus::Running,
326 in_flight: false,
327 is_created: false,
328 communication_lease: Some(ExecutionLease {
329 _guard: communication_guard,
330 }),
331 }
332 }
333}
334
335/// Stored state for a functional-group routine execution (async lifecycle).
336/// Unlike `ServiceExecution`, parameters are keyed by ECU name so that
337/// per-ECU identity is preserved across the execution lifecycle.
338#[derive(Debug)]
339pub(crate) struct FgServiceExecution {
340 pub parameters: HashMap<String, serde_json::Map<String, serde_json::Value>>,
341 pub status: sovd_ecu::operations::ExecutionStatus,
342 pub in_flight: bool,
343 pub is_created: bool,
344 communication_lease: Option<ExecutionLease>,
345}
346
347impl Clone for FgServiceExecution {
348 fn clone(&self) -> Self {
349 Self {
350 parameters: self.parameters.clone(),
351 status: self.status.clone(),
352 in_flight: self.in_flight,
353 is_created: self.is_created,
354 communication_lease: None,
355 }
356 }
357}
358
359impl FgServiceExecution {
360 pub(crate) fn complete(&mut self) {
361 self.status = sovd_ecu::operations::ExecutionStatus::Completed;
362 self.communication_lease = None;
363 }
364}
365
366impl ExecutionStatus for FgServiceExecution {
367 fn execution_status(&self) -> &sovd_ecu::operations::ExecutionStatus {
368 &self.status
369 }
370 fn is_in_flight(&self) -> bool {
371 self.in_flight
372 }
373 fn set_in_flight(&mut self, in_flight: bool) {
374 self.in_flight = in_flight;
375 }
376 fn is_created(&self) -> bool {
377 self.is_created
378 }
379 fn set_created(&mut self, created: bool) {
380 self.is_created = created;
381 }
382 fn placeholder(communication_guard: CommunicationGuard) -> Self {
383 FgServiceExecution {
384 parameters: HashMap::new(),
385 status: sovd_ecu::operations::ExecutionStatus::Running,
386 in_flight: false,
387 is_created: false,
388 communication_lease: Some(ExecutionLease {
389 _guard: communication_guard,
390 }),
391 }
392 }
393}
394
395/// Implementation of [`LockStateProvider`] that reads from the in-memory [`Locks`] state.
396pub struct SovdLockStateProvider {
397 locks: Arc<RwLock<Arc<Locks>>>,
398}
399
400impl SovdLockStateProvider {
401 /// Creates a new provider wrapping the given shared [`Locks`] state.
402 #[must_use]
403 pub fn new(locks: Arc<Locks>) -> Self {
404 Self {
405 locks: Arc::new(RwLock::new(locks)),
406 }
407 }
408
409 /// Verifies lock state permits a runtime configuration update.
410 ///
411 /// # Errors
412 /// Returns an error if any ECU or functional-group lock is currently held.
413 pub async fn prepare_runtime_update(&self) -> Result<(), locks::LockUpdateError> {
414 self.locks
415 .read()
416 .await
417 .as_ref()
418 .prepare_runtime_update()
419 .await
420 }
421
422 pub async fn current_locks(&self) -> Arc<Locks> {
423 self.locks.read().await.clone()
424 }
425}
426
427#[async_trait]
428impl LockStateProvider for SovdLockStateProvider {
429 async fn vehicle_lock_owner_sub(&self) -> Option<String> {
430 let locks = self.locks.read().await.clone();
431 locks.vehicle_lock_owner_sub().await
432 }
433
434 async fn has_non_vehicle_locks(&self) -> bool {
435 let locks = self.locks.read().await.clone();
436 locks.has_non_vehicle_locks().await
437 }
438}
439
440#[async_trait]
441impl cda_interfaces::request_authorization::RequestLockProvider for SovdLockStateProvider {
442 async fn current_lock(
443 &self,
444 target: &cda_interfaces::request_authorization::UdsRequestTarget,
445 ) -> Option<cda_interfaces::request_authorization::RequestLockContext> {
446 // Resolve the current `Locks` per call so runtime reloads are observed, and release
447 // the provider guard before inspecting lock state.
448 let locks = self.locks.read().await.clone();
449 locks.request_lock_for(target).await
450 }
451}
452
453/// Resets an execution's request-level concurrency marker when processing ends or is cancelled.
454pub(crate) struct ExecutionRequestGuard<'a, T: ExecutionStatus> {
455 executions: &'a ExecutionLock<T>,
456 service: String,
457 exec_id: Uuid,
458 snapshot: T,
459}
460
461impl<T: ExecutionStatus> ExecutionRequestGuard<'_, T> {
462 pub(crate) fn snapshot(&self) -> &T {
463 &self.snapshot
464 }
465}
466
467impl<T: ExecutionStatus> Drop for ExecutionRequestGuard<'_, T> {
468 fn drop(&mut self) {
469 if let Some(exec) = lock_write(self.executions)
470 .get_mut(&self.service)
471 .and_then(|entries| entries.get_mut(&self.exec_id))
472 {
473 exec.set_in_flight(false);
474 }
475 }
476}
477
478/// Acquires a write lock on `executions`, looks up `exec_id` under the given
479/// `service` key, marks it `in_flight = true`, and returns a guard containing a
480/// snapshot of the execution. Returns `Err(ErrorWrapper)` on not-found or conflict.
481pub(crate) fn guard_execution<'a, T: ExecutionStatus + Clone>(
482 executions: &'a ExecutionLock<T>,
483 service: &str,
484 exec_id: Uuid,
485 include_schema: bool,
486 conflict_msg: &str,
487) -> Result<ExecutionRequestGuard<'a, T>, error::ErrorWrapper> {
488 let snapshot = {
489 let mut guard = lock_write(executions);
490 let op_map = guard
491 .get_mut(service)
492 .and_then(|m| m.get_mut(&exec_id))
493 // Treat placeholders (is_created == false) as non-existent.
494 .filter(|e| e.is_created());
495 match op_map {
496 None => {
497 return Err(error::ErrorWrapper {
498 error: error::ApiError::NotFound(Some(format!(
499 "Execution with id {exec_id} not found"
500 ))),
501 include_schema,
502 });
503 }
504 Some(exec) if exec.is_in_flight() => {
505 return Err(error::ErrorWrapper {
506 error: error::ApiError::Conflict(conflict_msg.to_owned()),
507 include_schema,
508 });
509 }
510 Some(exec) => {
511 exec.set_in_flight(true);
512 exec.clone()
513 }
514 }
515 };
516 Ok(ExecutionRequestGuard {
517 executions,
518 service: service.to_owned(),
519 exec_id,
520 snapshot,
521 })
522}
523
524/// Checks for a running-execution conflict and, if none exists,
525/// inserts a placeholder entry so that a second concurrent POST for the same
526/// operation will see a `409 Conflict`.
527///
528/// On success the returned [`Uuid`] identifies the reserved execution slot.
529/// The inserted placeholder owns the communication guard and releases it when
530/// the execution is completed or removed.
531///
532/// Runtime-update admission is enforced by the request guard and communication
533/// access, so this function only reserves a per-service execution slot.
534pub(crate) fn reserve_execution<E: ExecutionStatus>(
535 executions: &ExecutionLock<E>,
536 service: &str,
537 display_name: &str,
538 include_schema: bool,
539 id: Uuid,
540 communication_guard: CommunicationGuard,
541) -> Result<Uuid, error::ErrorWrapper> {
542 let mut guard = lock_write(executions);
543 let has_running = guard.get(service).is_some_and(|m| {
544 m.values()
545 .any(|e| *e.execution_status() == sovd_ecu::operations::ExecutionStatus::Running)
546 });
547 if has_running {
548 return Err(error::ErrorWrapper {
549 error: error::ApiError::Conflict(format!(
550 "An execution for operation '{display_name}' is already in progress"
551 )),
552 include_schema,
553 });
554 }
555 let mut entry = E::placeholder(communication_guard);
556 entry.set_in_flight(true);
557 guard
558 .entry(service.to_owned())
559 .or_default()
560 .insert(id, entry);
561 Ok(id)
562}
563
564/// Publishes a communication lease before making its execution visible, so every
565/// visible execution has an owner that can release the lease.
566pub(crate) fn acquire_and_reserve_execution<E: ExecutionStatus>(
567 communication_access: &dyn CommunicationAccess,
568 executions: Arc<ExecutionLock<E>>,
569 service: &str,
570 display_name: &str,
571 include_schema: bool,
572) -> Result<ExecutionReservation<E>, error::ErrorWrapper> {
573 let communication_guard =
574 acquire_communication_activity(communication_access).map_err(|error| {
575 error::ErrorWrapper {
576 error,
577 include_schema,
578 }
579 })?;
580 let exec_id = Uuid::new_v4();
581 reserve_execution(
582 &executions,
583 service,
584 display_name,
585 include_schema,
586 exec_id,
587 communication_guard,
588 )?;
589 Ok(ExecutionReservation::new(
590 executions,
591 service.to_owned(),
592 exec_id,
593 ))
594}
595
596/// Updates a previously reserved execution with the received parameters,
597/// sets `is_created(true)`, and clears the `in_flight` flag.
598/// After this step GET/DELETE requests can be called for this execution.
599pub(crate) fn finalize_execution<E: ExecutionStatus>(
600 executions: &ExecutionLock<E>,
601 service: &str,
602 exec_id: &Uuid,
603 update_fn: impl FnOnce(&mut E),
604) {
605 let mut guard = lock_write(executions);
606 if let Some(exec) = guard.get_mut(service).and_then(|m| m.get_mut(exec_id)) {
607 update_fn(exec);
608 exec.set_created(true);
609 exec.set_in_flight(false);
610 }
611}
612
613/// Removes a previously reserved execution slot. Called on error or after
614/// a synchronous operation completes (sync operations do not persist
615/// execution state).
616pub(crate) fn remove_reserved_execution<E>(
617 executions: &ExecutionLock<E>,
618 service: &str,
619 exec_id: &Uuid,
620) {
621 let mut guard = lock_write(executions);
622 if let Some(map) = guard.get_mut(service) {
623 map.shift_remove(exec_id);
624 if map.is_empty() {
625 guard.remove(service);
626 }
627 }
628}
629
630#[derive(Clone)]
631pub(crate) struct WebserverState<T: UdsEcu + Clone> {
632 uds: T,
633 locks: Arc<Locks>,
634 flash_data: Arc<RwLock<sovd_interfaces::sovd2uds::FileList>>,
635 flash_functional_class_default: Arc<str>,
636 components_config: Arc<RwLock<ComponentsConfig>>,
637 communication_access: Arc<dyn CommunicationAccess>,
638}
639
640pub(crate) fn resource_response(
641 host: &str,
642 uri: &Uri,
643 resources: Vec<(&str, Option<&str>)>,
644 include_schema: bool,
645) -> Response {
646 let base_path = format!("http://{host}{uri}");
647 let items = resources
648 .into_iter()
649 .map(|(name, href)| sovd_interfaces::Resource {
650 name: name.to_string(),
651 href: format!("{base_path}/{}", href.unwrap_or(name)),
652 id: None,
653 })
654 .collect();
655
656 let schema = if include_schema {
657 Some(crate::sovd::create_schema!(sovd_interfaces::Resource))
658 } else {
659 None
660 };
661
662 let components = sovd_interfaces::ResourceResponse { items, schema };
663 (StatusCode::OK, Json(components)).into_response()
664}
665
666/// Flash API related configuration.
667pub struct FlashConfig {
668 /// Folder that contains the flash files.
669 pub files_path: String,
670 /// Functional class used to resolve the flash services when the client provides none.
671 pub functional_class_default: String,
672}
673
674pub async fn route<T: UdsEcu + SchemaProvider + Clone, U: FileManager, S: SecurityPluginLoader>(
675 functional_group_config: FunctionalDescriptionConfig,
676 components_config: ComponentsConfig,
677 uds: &T,
678 flash_config: FlashConfig,
679 mut file_manager: HashMap<String, U>,
680 locks: Arc<Locks>,
681 communication_access: Arc<dyn CommunicationAccess>,
682) -> Router {
683 let flash_data = Arc::new(RwLock::new(sovd_interfaces::sovd2uds::FileList {
684 files: Vec::new(),
685 path: Some(PathBuf::from(flash_config.files_path)),
686 schema: None,
687 }));
688 let state = WebserverState {
689 uds: uds.clone(),
690 locks,
691 flash_data: Arc::clone(&flash_data),
692 flash_functional_class_default: Arc::from(flash_config.functional_class_default),
693 components_config: Arc::new(RwLock::new(components_config)),
694 communication_access,
695 };
696
697 let router = components_route::<T, U>(state.clone(), &mut file_manager).await;
698
699 vehicle_route::<T, S>(state, router, functional_group_config)
700 .await
701 .layer(middleware::from_fn(security_plugin_middleware::<S>))
702 .with_state(uds.clone())
703}
704
705async fn vehicle_route<T: UdsEcu + SchemaProvider + Clone, S: SecurityPluginLoader>(
706 state: WebserverState<T>,
707 router: Router<WebserverState<T>>,
708 functional_group_config: FunctionalDescriptionConfig,
709) -> Router<T> {
710 let router = router.nest_api_service(
711 "/vehicle/v15/functions",
712 functions::functional_groups::create_functional_group_routes(
713 state.clone(),
714 functional_group_config,
715 )
716 .await,
717 );
718 router
719 .api_route(
720 "/vehicle/v15/locks",
721 routing::post_with(locks::vehicle::post, locks::vehicle::docs_post)
722 .get_with(locks::vehicle::get, locks::vehicle::docs_get),
723 )
724 .api_route(
725 "/vehicle/v15/locks/{lock}",
726 routing::get_with(locks::vehicle::lock::get, locks::vehicle::lock::docs_get)
727 .put_with(locks::vehicle::lock::put, locks::vehicle::lock::docs_put)
728 .delete_with(
729 locks::vehicle::lock::delete,
730 locks::vehicle::lock::docs_delete,
731 ),
732 )
733 .route("/vehicle/v15/apps", routing::get(apps::get))
734 .route(
735 "/vehicle/v15/apps/sovd2uds",
736 routing::get(apps::sovd2uds::get),
737 )
738 .route(
739 "/vehicle/v15/apps/sovd2uds/bulk-data",
740 routing::get(apps::sovd2uds::bulk_data::get),
741 )
742 .api_route(
743 "/vehicle/v15/apps/sovd2uds/bulk-data/flashfiles",
744 routing::get_with(
745 apps::sovd2uds::bulk_data::flash_files::get,
746 apps::sovd2uds::bulk_data::flash_files::docs_get,
747 ),
748 )
749 .route("/vehicle/v15/authorize", routing::post(S::authorize))
750 .with_state(state)
751 .api_route(
752 "/vehicle/v15/apps/sovd2uds/data/networkstructure",
753 routing::get_with(
754 apps::sovd2uds::data::networkstructure::get::<T>,
755 apps::sovd2uds::data::networkstructure::docs_get,
756 ),
757 )
758}
759
760async fn get_components<T: UdsEcu + SchemaProvider + Clone>(
761 State(state): State<WebserverState<T>>,
762 WithRejection(Query(query), _): WithRejection<Query<IncludeSchemaQuery>, ApiError>,
763) -> Response {
764 fn ecu_to_resource(ecu: String) -> Resource {
765 Resource {
766 href: format!("http://localhost:20002/Vehicle/v15/components/{ecu}"),
767 id: Some(ecu.to_lowercase()),
768 name: ecu,
769 }
770 }
771 let ecus = state.uds.get_physical_ecus().await;
772 let components_config = state.components_config.read().await;
773 let mut additional_fields: HashMap<String, Vec<Resource>> = HashMap::new();
774 for (key, conditions) in &components_config.additional_fields {
775 let items = state
776 .uds
777 .get_ecus_with_sds(true, conditions)
778 .await
779 .into_iter()
780 .map(ecu_to_resource)
781 .collect::<Vec<_>>();
782 additional_fields.insert(key.to_owned(), items);
783 }
784
785 let mut schema = if query.include_schema {
786 Some(create_schema!(ComponentsResponse<Resource>))
787 } else {
788 None
789 };
790 if !additional_fields.is_empty()
791 && let Some(ref mut schema) = schema
792 {
793 let subschema = create_schema!(Resource);
794 for entry in additional_fields.keys() {
795 if let Some(properties) = schema.get_mut("properties").and_then(|v| v.as_object_mut()) {
796 properties.insert(entry.to_owned(), subschema.clone().to_value());
797 }
798 }
799 }
800 (
801 StatusCode::OK,
802 Json(ComponentsResponse::<Resource> {
803 items: ecus.into_iter().map(ecu_to_resource).collect::<Vec<_>>(),
804 additional_fields,
805 schema,
806 }),
807 )
808 .into_response()
809}
810
811fn docs_components(op: TransformOperation) -> TransformOperation {
812 op.description("Get a list of the available components with their paths")
813 .response_with::<200, Json<sovd_interfaces::ResourceResponse>, _>(|res| {
814 res.example(sovd_interfaces::ResourceResponse {
815 items: vec![sovd_interfaces::Resource {
816 href: "http://localhost:20002/Vehicle/v15/components/my_ecu".into(),
817 id: Some("my_ecu".into()),
818 name: "My ECU".into(),
819 }],
820 schema: None,
821 })
822 })
823}
824
825async fn components_route<T: UdsEcu + SchemaProvider + Clone, U: FileManager + 'static>(
826 state: WebserverState<T>,
827 file_manager: &mut HashMap<String, U>,
828) -> Router<WebserverState<T>> {
829 let mut router = Router::new().api_route(
830 "/vehicle/v15/components",
831 get_with(get_components, docs_components),
832 );
833 let mut ecus = state.uds.get_physical_ecus().await;
834 for ecu_name in ecus.drain(0..) {
835 match ecu_route::<T, U>(&ecu_name, &state, file_manager) {
836 Ok((ecu_path, nested)) => {
837 router = router.nest_api_service(&ecu_path, nested);
838 }
839 Err(e) => {
840 tracing::error!(error = %e, "Failed to create route for ECU '{ecu_name}'");
841 }
842 }
843 }
844 router.with_state(state)
845}
846
847#[allow(
848 clippy::too_many_lines,
849 reason = "Route creation kept together for structural clarity"
850)]
851fn ecu_route<T: UdsEcu + SchemaProvider + Clone, U: FileManager + 'static>(
852 ecu_name: &str,
853 state: &WebserverState<T>,
854 file_manager: &mut HashMap<String, U>,
855) -> Result<(String, Router), SovdError> {
856 let ecu_lower = ecu_name.to_lowercase();
857 let ecu_state = WebserverEcuState {
858 ecu_name: ecu_lower.clone(),
859 uds: state.uds.clone(),
860 locks: Arc::<Locks>::clone(&state.locks),
861 comparam_executions: Arc::new(RwLock::new(IndexMap::new())),
862 communication_access: Arc::clone(&state.communication_access),
863 service_executions: Arc::new(StdRwLock::new(HashMap::default())),
864 flash_data: Arc::clone(&state.flash_data),
865 flash_functional_class_default: Arc::clone(&state.flash_functional_class_default),
866 mdd_embedded_files: Arc::new(file_manager.remove(&ecu_lower).ok_or_else(|| {
867 SovdError::RouteError(format!(
868 "FileManager for ECU '{ecu_name}' not found in provided FileManager map"
869 ))
870 })?),
871 };
872 let ecu_path = format!("/vehicle/v15/components/{ecu_lower}");
873
874 let router = Router::new()
875 .api_route(
876 "/",
877 routing::get_with(components::ecu::get, components::ecu::docs_get)
878 .post_with(components::ecu::post, components::ecu::docs_put)
879 .put_with(components::ecu::put, components::ecu::docs_put),
880 )
881 .api_route(
882 "/locks",
883 routing::post_with(locks::ecu::post, locks::ecu::docs_post)
884 .get_with(locks::ecu::get, locks::ecu::docs_get),
885 )
886 .api_route(
887 "/locks/{lock}",
888 routing::delete_with(locks::ecu::lock::delete, locks::ecu::lock::docs_delete)
889 .put_with(locks::ecu::lock::put, locks::ecu::lock::docs_put)
890 .get_with(locks::ecu::lock::get, locks::ecu::lock::docs_get),
891 )
892 .api_route(
893 "/configurations",
894 routing::get_with(configurations::get, configurations::docs_get),
895 )
896 .api_route(
897 "/configurations/{service}",
898 routing::put_with(
899 configurations::diag_service::put,
900 configurations::diag_service::docs_put,
901 )
902 .get_with(data::diag_service::get, data::diag_service::docs_get),
903 )
904 .api_route(
905 "/configurations/{service}/docs",
906 routing::get_with(
907 configurations::diag_service::docs_endpoint::get,
908 configurations::diag_service::docs_endpoint::docs_transform,
909 ),
910 )
911 .api_route("/data", routing::get_with(data::get, data::docs_get))
912 .api_route(
913 "/data-categories",
914 routing::get_with(data_categories::get, data_categories::docs_get),
915 )
916 .api_route(
917 "/data/{service}",
918 routing::get_with(data::diag_service::get, data::diag_service::docs_get)
919 .put_with(data::diag_service::put, data::diag_service::docs_put),
920 )
921 .api_route(
922 "/data/{service}/docs",
923 routing::get_with(
924 data::diag_service::docs_endpoint::get,
925 data::diag_service::docs_endpoint::docs_transform,
926 ),
927 )
928 .api_route(
929 "/genericservice",
930 routing::put_with(genericservice::put, genericservice::docs_put),
931 )
932 .api_route(
933 "/operations",
934 routing::get_with(operations::get, operations::docs_get),
935 )
936 .api_route(
937 "/operations/{service}",
938 routing::get_with(operations::service::get, operations::service::docs_get),
939 )
940 .api_route(
941 "/operations/{service}/docs",
942 routing::get_with(
943 operations::service::docs_endpoint::get,
944 operations::service::docs_endpoint::docs_transform,
945 ),
946 )
947 .api_route(
948 "/operations/comparam/executions",
949 routing::get_with(
950 operations::comparams::executions::get,
951 operations::comparams::executions::docs_get,
952 )
953 .post_with(
954 operations::comparams::executions::post,
955 operations::comparams::executions::docs_post,
956 ),
957 )
958 .api_route(
959 "/operations/comparam/executions/{id}",
960 routing::get_with(
961 operations::comparams::executions::id::get,
962 operations::comparams::executions::id::docs_get,
963 )
964 .delete_with(
965 operations::comparams::executions::id::delete,
966 operations::comparams::executions::id::docs_delete,
967 )
968 .put_with(
969 operations::comparams::executions::id::put,
970 operations::comparams::executions::id::docs_put,
971 ),
972 )
973 .api_route(
974 "/operations/{service}/executions",
975 routing::get_with(
976 operations::service::executions::get,
977 operations::service::executions::docs_get,
978 )
979 .post_with(
980 operations::service::executions::post,
981 operations::service::executions::docs_post,
982 ),
983 )
984 .api_route(
985 "/operations/{service}/executions/{id}",
986 routing::get_with(
987 operations::service::executions::id::get,
988 operations::service::executions::id::docs_get,
989 )
990 .delete_with(
991 operations::service::executions::id::delete,
992 operations::service::executions::id::docs_delete,
993 ),
994 )
995 .api_route("/modes", routing::get_with(modes::get, modes::docs_get))
996 .api_route(
997 &format!("/modes/{}", sovd_interfaces::common::modes::SESSION_ID),
998 routing::get_with(modes::session::get, modes::session::docs_get)
999 .put_with(modes::session::put, modes::session::docs_put),
1000 )
1001 .api_route(
1002 &format!("/modes/{}", sovd_interfaces::common::modes::SECURITY_ID),
1003 routing::get_with(modes::security::get, modes::security::docs_get)
1004 .put_with(modes::security::put, modes::security::docs_put),
1005 )
1006 .api_route(
1007 &format!("/modes/{}", sovd_interfaces::common::modes::COMM_CONTROL_ID),
1008 routing::get_with(modes::commctrl::get, modes::commctrl::docs_get)
1009 .put_with(modes::commctrl::put, modes::commctrl::docs_put),
1010 )
1011 .api_route(
1012 &format!("/modes/{}", sovd_interfaces::common::modes::DTC_SETTING_ID),
1013 routing::get_with(modes::dtcsetting::get, modes::dtcsetting::docs_get)
1014 .put_with(modes::dtcsetting::put, modes::dtcsetting::docs_put),
1015 )
1016 .api_route(
1017 "/x-single-ecu-jobs",
1018 routing::get_with(
1019 x_single_ecu_jobs::single_ecu::get,
1020 x_single_ecu_jobs::single_ecu::docs_get,
1021 ),
1022 )
1023 .api_route(
1024 "/x-single-ecu-jobs/{job_name}",
1025 routing::get_with(
1026 x_single_ecu_jobs::single_ecu::name::get,
1027 x_single_ecu_jobs::single_ecu::name::docs_get,
1028 ),
1029 )
1030 .route(
1031 "/x-sovd2uds-download",
1032 routing::get(x_sovd2uds_download::get),
1033 )
1034 .api_route(
1035 "/x-sovd2uds-download/requestdownload",
1036 routing::put_with(
1037 x_sovd2uds_download::request_download::put,
1038 x_sovd2uds_download::request_download::docs_put,
1039 ),
1040 )
1041 .api_route(
1042 "/x-sovd2uds-download/flashtransfer",
1043 routing::post_with(
1044 x_sovd2uds_download::flash_transfer::post,
1045 x_sovd2uds_download::flash_transfer::docs_post,
1046 )
1047 .get_with(
1048 x_sovd2uds_download::flash_transfer::get,
1049 x_sovd2uds_download::flash_transfer::docs_get,
1050 ),
1051 )
1052 .api_route(
1053 "/x-sovd2uds-download/flashtransfer/{id}",
1054 routing::get_with(
1055 x_sovd2uds_download::flash_transfer::id::get,
1056 x_sovd2uds_download::flash_transfer::id::docs_get,
1057 )
1058 .delete_with(
1059 x_sovd2uds_download::flash_transfer::id::delete,
1060 x_sovd2uds_download::flash_transfer::id::docs_delete,
1061 ),
1062 )
1063 .api_route(
1064 "/x-sovd2uds-download/transferexit",
1065 routing::put_with(
1066 x_sovd2uds_download::transferexit::put,
1067 x_sovd2uds_download::transferexit::docs_put,
1068 ),
1069 )
1070 .route(
1071 "/x-sovd2uds-bulk-data",
1072 routing::get(x_sovd2uds_bulk_data::get),
1073 )
1074 .api_route(
1075 "/x-sovd2uds-bulk-data/mdd-embedded-files",
1076 routing::get_with(
1077 x_sovd2uds_bulk_data::mdd_embedded_files::get,
1078 x_sovd2uds_bulk_data::mdd_embedded_files::docs_get,
1079 ),
1080 )
1081 .api_route(
1082 "/x-sovd2uds-bulk-data/mdd-embedded-files/{id}",
1083 routing::get_with(
1084 x_sovd2uds_bulk_data::mdd_embedded_files::id::get,
1085 x_sovd2uds_bulk_data::mdd_embedded_files::id::docs_get,
1086 ),
1087 )
1088 .api_route(
1089 "/faults",
1090 routing::get_with(faults::get, faults::docs_get)
1091 .delete_with(faults::delete, faults::docs_delete),
1092 )
1093 .api_route(
1094 "/faults/{id}",
1095 routing::get_with(faults::id::get, faults::id::docs_get)
1096 .delete_with(faults::id::delete, faults::id::docs_delete),
1097 )
1098 .with_state(ecu_state)
1099 .with_path_items(crate::openapi::defunct_lock_path)
1100 .with_path_items(|op| op.tag(ecu_name));
1101
1102 Ok((ecu_path, router))
1103}
1104
1105fn get_payload_data<'a, T>(
1106 content_type: Option<&mime::Mime>,
1107 headers: &HeaderMap,
1108 body: &'a Bytes,
1109) -> Result<Option<UdsPayloadData>, ApiError>
1110where
1111 T: sovd_interfaces::Payload + serde::de::Deserialize<'a>,
1112{
1113 let Some(content_type) = content_type else {
1114 return Ok(None);
1115 };
1116 Ok(match (content_type.type_(), content_type.subtype()) {
1117 (mime::APPLICATION, mime::JSON) => {
1118 let sovd_request = serde_json::from_slice::<T>(body)
1119 .map_err(|e| ApiError::BadRequest(format!("Invalid JSON: {e:?}")))?;
1120 Some(UdsPayloadData::ParameterMap(sovd_request.get_data_map()))
1121 }
1122 (mime::APPLICATION, mime::OCTET_STREAM) => get_octet_stream_payload(headers, body)?,
1123 _ => {
1124 return Err(ApiError::BadRequest(format!(
1125 "Unsupported mime-type: {content_type:?}"
1126 )));
1127 }
1128 })
1129}
1130
1131fn get_octet_stream_payload(
1132 headers: &HeaderMap,
1133 body: &Bytes,
1134) -> Result<Option<UdsPayloadData>, ApiError> {
1135 let content_length = headers
1136 .get(header::CONTENT_LENGTH)
1137 .ok_or_else(|| ApiError::BadRequest("Missing Content-Length".to_owned()))
1138 .and_then(|v| {
1139 v.to_str()
1140 .map_err(|e| ApiError::BadRequest(format!("Invalid Content-Length: {e:?}")))
1141 .and_then(|v| {
1142 v.parse::<usize>()
1143 .map_err(|e| ApiError::BadRequest(format!("Invalid Content-Length: {e}")))
1144 })
1145 })?;
1146
1147 if content_length == 0 {
1148 return Ok(None);
1149 }
1150
1151 let mut data = body.to_vec();
1152
1153 if data.len() < content_length {
1154 return Err(ApiError::BadRequest(format!(
1155 "Invalid Content-Length: {content_length} is bigger than the size of the data {}",
1156 data.len()
1157 )));
1158 }
1159
1160 data.truncate(content_length);
1161
1162 Ok(Some(UdsPayloadData::Raw(data)))
1163}
1164
1165/// Helper Fn to convert a `serde_json::Value` into a `schemars::Schema`, without cloning
1166fn value_to_schema(mut value: serde_json::Value) -> Result<Schema, ApiError> {
1167 let value = value
1168 .as_object_mut()
1169 .map(std::mem::take)
1170 .ok_or(ApiError::InternalServerError(Some(
1171 "Failed to create schema".to_string(),
1172 )))?;
1173 Ok(schemars::Schema::from(value))
1174}
1175
1176/// Helper Fn to remove descriptions from a schema, in cases where a
1177/// schema reduced on the necessary parameters for automated parsing is
1178/// desired.
1179///
1180/// Due to schemars not offering an option to skip generating
1181/// the description from rusts docstrings as a workaround the generated
1182/// json Value of the schema is traversed recursively and all descriptions
1183/// are removed.
1184fn remove_descriptions_recursive(value: &mut serde_json::Value) {
1185 if let Some(obj) = value.as_object_mut() {
1186 obj.remove("description");
1187 for v in obj.values_mut() {
1188 if v.is_object() || v.is_array() {
1189 remove_descriptions_recursive(v);
1190 }
1191 }
1192 } else if let Some(arr) = value.as_array_mut() {
1193 for v in arr {
1194 if v.is_object() || v.is_array() {
1195 remove_descriptions_recursive(v);
1196 }
1197 }
1198 }
1199}
1200
1201/// This Macro allows to generate a schema for Responses including
1202/// the inlined schema for the target field.
1203///
1204/// # Arguments
1205/// - `base_type`: The base type for the response schema.
1206/// - `target_field`: The field in the base type where the sub schema should be inserted.
1207/// - `sub_schema`: The sub schema to be inserted.
1208///
1209/// # Returns
1210/// A codeblock that returns the enriched response schema
1211macro_rules! create_response_schema {
1212 ($base_type:ty, $target_field:expr, $sub_schema:ident) => {{
1213 use schemars::JsonSchema as _;
1214
1215 use crate::sovd::error::VendorErrorCode;
1216
1217 let mut generator = schemars::SchemaGenerator::new(
1218 schemars::generate::SchemaSettings::draft07().with(|s| s.inline_subschemas = true),
1219 );
1220 let mut schema = <$base_type>::json_schema(&mut generator);
1221
1222 if let Some(props) = schema.get_mut("properties") {
1223 if let Some(obj) = props.as_object_mut() {
1224 let value = match $sub_schema {
1225 None => serde_json::Value::Null,
1226 Some(s) => s.to_value(),
1227 };
1228 obj.insert($target_field.into(), value);
1229 if let Some(errs) = obj.get_mut("errors") {
1230 crate::sovd::remove_descriptions_recursive(&mut *errs);
1231 }
1232 }
1233 }
1234
1235 schema
1236 }};
1237}
1238pub(crate) use create_response_schema;
1239
1240/// This Macro allows to generate a schema for a type.
1241/// Ensures that the schema is generated with inlined subschemas
1242/// and draft07 settings.
1243#[macro_export]
1244macro_rules! create_schema {
1245 ($type_:ty) => {{
1246 #[allow(
1247 unused_imports,
1248 reason = "Import may already be in scope at the macro call site"
1249 )]
1250 use schemars::JsonSchema as _;
1251
1252 let mut generator = schemars::SchemaGenerator::new(
1253 schemars::generate::SchemaSettings::draft07().with(|s| s.inline_subschemas = true),
1254 );
1255 <$type_>::json_schema(&mut generator)
1256 }};
1257}
1258pub use create_schema;
1259
1260pub(crate) mod static_data {
1261 use aide::{
1262 axum::{ApiRouter, routing},
1263 transform::TransformOperation,
1264 };
1265 use axum::{
1266 Json,
1267 extract::{Query, State},
1268 response::{IntoResponse, Response},
1269 };
1270 use http::StatusCode;
1271
1272 use crate::{dynamic_router::DynamicRouter, sovd::error::ApiError};
1273
1274 /// Add an endpoint serving static data.
1275 /// For example it can be used, to serve version information.
1276 /// The standard defines these routes for version data:
1277 /// * `/vehicle/v15/apps/sovd2uds/data/version`.
1278 /// * `/vehicle/v15/data/version`
1279 /// # Arguments
1280 /// * `dynamic_router` - The dynamic router to add the endpoint to.
1281 /// * `data` - The version data to return.
1282 /// * `path` - The path to serve the data from.
1283 /// There is no processing of this, it will be returned as is in the response.
1284 pub async fn add_static_data_endpoint(
1285 dynamic_router: &DynamicRouter,
1286 data: serde_json::Map<String, serde_json::Value>,
1287 path: &str,
1288 ) {
1289 let data_docs = data.clone();
1290 let router = ApiRouter::new()
1291 .api_route(
1292 path,
1293 routing::get_with(get, move |transformation| {
1294 docs_get(transformation, data_docs.clone())
1295 }),
1296 )
1297 .with_state(data);
1298 dynamic_router.add_routes(router).await;
1299 }
1300
1301 pub(crate) async fn get(
1302 State(state): State<serde_json::Map<String, serde_json::Value>>,
1303 Query(query): Query<sovd_interfaces::IncludeSchemaQuery>,
1304 ) -> Response {
1305 let mut response_map = state.clone();
1306 if query.include_schema {
1307 let schema = match serde_json::to_value(
1308 create_schema!(serde_json::Map<String, serde_json::Value>),
1309 ) {
1310 Ok(s) => s,
1311 Err(e) => {
1312 return ApiError::InternalServerError(Some(format!(
1313 "Failed to build static data with schema: {e}"
1314 )))
1315 .into_response();
1316 }
1317 };
1318
1319 response_map.insert("schema".to_string(), schema);
1320 }
1321 (StatusCode::OK, Json(response_map)).into_response()
1322 }
1323
1324 pub(crate) fn docs_get(
1325 op: TransformOperation,
1326 data: serde_json::Map<String, serde_json::Value>,
1327 ) -> TransformOperation {
1328 op.description("Get static information")
1329 .response_with::<200, Json<serde_json::Map<String, serde_json::Value>>, _>(|res| {
1330 let mut example = data;
1331 example.insert("schema".to_string(), serde_json::Value::Null);
1332 res.description("Successful response").example(example)
1333 })
1334 }
1335}
1336
1337/// Wrapper Struct around [`FieldParseError`] to allow implementing
1338/// [From] for [`DataError`<VendorErrorCode>]
1339struct FieldParseErrorWrapper(FieldParseError);
1340impl From<FieldParseErrorWrapper> for DataError<VendorErrorCode> {
1341 fn from(value: FieldParseErrorWrapper) -> Self {
1342 let value: FieldParseError = value.0;
1343 Self {
1344 path: value.path,
1345 error: sovd_interfaces::error::ApiErrorResponse {
1346 message: "Failed to parse parameter".to_owned(),
1347 error_code: sovd_interfaces::error::ErrorCode::InvalidResponseContent,
1348 vendor_code: None,
1349 parameters: Some(
1350 [
1351 ("details", value.error.details),
1352 ("value", value.error.value),
1353 ]
1354 .into_iter()
1355 .map(|(k, v)| (k.to_string(), serde_json::Value::String(v)))
1356 .collect(),
1357 ),
1358 error_source: None,
1359 schema: None,
1360 },
1361 }
1362 }
1363}
1364
1365fn field_parse_errors_to_json(
1366 errors: impl IntoIterator<Item = FieldParseError>,
1367 data_field_ref: &str,
1368) -> Vec<DataError<VendorErrorCode>> {
1369 errors
1370 .into_iter()
1371 .map(|v| {
1372 let mut data_error = DataError::from(FieldParseErrorWrapper(v));
1373 data_error.path = format!("/{data_field_ref}{}", data_error.path);
1374 data_error
1375 })
1376 .collect()
1377}
1378
1379impl IntoSovd for FieldParseError {
1380 type SovdType = DataError<VendorErrorCode>;
1381
1382 fn into_sovd(self) -> Self::SovdType {
1383 FieldParseErrorWrapper(self).into()
1384 }
1385}
1386
1387#[cfg(test)]
1388pub(crate) mod tests {
1389 use std::sync::atomic::{AtomicUsize, Ordering};
1390
1391 use cda_interfaces::{
1392 UdsEcu,
1393 communication_control::{
1394 ActivationCause, CommunicationAccess, CommunicationError, CommunicationState,
1395 VariantDetectionMode,
1396 },
1397 file_manager::FileManager,
1398 util::std_ext::lock_read,
1399 };
1400 use cda_plugin_communication_management::lifecycle::enabled_communication_access_for_test;
1401 use sovd_interfaces::sovd2uds::FileList;
1402
1403 use super::*;
1404
1405 struct DeferredCommunicationAccess {
1406 activation_requests: AtomicUsize,
1407 }
1408
1409 struct CountingCommunicationAccess {
1410 /// Used as counter for active instances.
1411 /// As tests hold this too active 'access' instances are `Arc::strong_count` - 1
1412 active: Arc<()>,
1413 }
1414
1415 impl CommunicationAccess for CountingCommunicationAccess {
1416 fn state(&self) -> CommunicationState {
1417 CommunicationState::Enabled
1418 }
1419
1420 fn acquire(&self) -> Result<CommunicationGuard, CommunicationError> {
1421 Ok(CommunicationGuard::new(Arc::clone(&self.active)))
1422 }
1423
1424 fn request_activate(&self, _cause: ActivationCause) -> CommunicationState {
1425 CommunicationState::Enabled
1426 }
1427
1428 fn retry_after(&self) -> Duration {
1429 Duration::ZERO
1430 }
1431
1432 fn variant_detection(&self) -> VariantDetectionMode {
1433 VariantDetectionMode::Always
1434 }
1435 }
1436
1437 impl CommunicationAccess for DeferredCommunicationAccess {
1438 fn state(&self) -> CommunicationState {
1439 CommunicationState::Disabled
1440 }
1441
1442 fn acquire(&self) -> Result<CommunicationGuard, CommunicationError> {
1443 Err(CommunicationError::Disabled)
1444 }
1445
1446 fn request_activate(&self, _cause: ActivationCause) -> CommunicationState {
1447 self.activation_requests.fetch_add(1, Ordering::SeqCst);
1448 CommunicationState::Disabled
1449 }
1450
1451 fn retry_after(&self) -> Duration {
1452 Duration::from_secs(7)
1453 }
1454
1455 fn variant_detection(&self) -> VariantDetectionMode {
1456 VariantDetectionMode::Always
1457 }
1458 }
1459
[docs]1460 /// [[ test~deferred-sovd-admission, Deferred request admission triggers activation and returns a retry hint, test ]]
1461 #[test]
1462 fn communication_denial_requests_activation_and_returns_retry_hint() {
1463 let access = DeferredCommunicationAccess {
1464 activation_requests: AtomicUsize::new(0),
1465 };
1466
1467 let Err(error) = acquire_communication_activity(&access) else {
1468 panic!("disabled communication must reject acquisition");
1469 };
1470
1471 assert_eq!(access.activation_requests.load(Ordering::SeqCst), 1);
1472 let ApiError::ServiceUnavailable {
1473 retry_after,
1474 vendor_code,
1475 ..
1476 } = error
1477 else {
1478 panic!("disabled communication must return service unavailable");
1479 };
1480 assert_eq!(retry_after, Some(Duration::from_secs(7)));
1481 assert_eq!(vendor_code, Some(VendorErrorCode::CommunicationNotReady));
1482 }
1483
1484 #[test]
1485 fn request_guard_drop_resets_in_flight() {
1486 let exec_id = Uuid::new_v4();
1487 let mut entries = IndexMap::new();
1488 entries.insert(
1489 exec_id,
1490 ServiceExecution {
1491 parameters: serde_json::Map::new(),
1492 status: sovd_ecu::operations::ExecutionStatus::Running,
1493 in_flight: false,
1494 is_created: true,
1495 communication_lease: None,
1496 },
1497 );
1498 let executions = StdRwLock::new(HashMap::from_iter([("routine".to_owned(), entries)]));
1499
1500 let Ok(request_guard) = guard_execution(
1501 &executions,
1502 "routine",
1503 exec_id,
1504 false,
1505 "execution already in flight",
1506 ) else {
1507 panic!("execution must be guarded");
1508 };
1509 assert!(
1510 lock_read(&executions)
1511 .get("routine")
1512 .and_then(|entries| entries.get(&exec_id))
1513 .is_some_and(|execution| execution.in_flight)
1514 );
1515
1516 drop(request_guard);
1517
1518 assert!(
1519 lock_read(&executions)
1520 .get("routine")
1521 .and_then(|entries| entries.get(&exec_id))
1522 .is_some_and(|execution| !execution.in_flight)
1523 );
1524 }
1525
1526 #[test]
1527 fn reservation_drop_releases_execution_and_lease() {
1528 let executions = Arc::new(StdRwLock::new(HashMap::default()));
1529 let access = CountingCommunicationAccess {
1530 active: Arc::new(()),
1531 };
1532
1533 let Ok(reservation) = acquire_and_reserve_execution::<ServiceExecution>(
1534 &access,
1535 Arc::clone(&executions),
1536 "routine",
1537 "routine",
1538 false,
1539 ) else {
1540 panic!("enabled communication must reserve execution");
1541 };
1542 let id = reservation.exec_id;
1543
1544 assert_eq!(Arc::strong_count(&access.active), 2);
1545 assert!(
1546 lock_read(&executions)
1547 .get("routine")
1548 .is_some_and(|entries| entries.contains_key(&id))
1549 );
1550
1551 drop(reservation);
1552
1553 assert_eq!(Arc::strong_count(&access.active), 1);
1554 assert!(lock_read(&executions).get("routine").is_none());
1555 }
1556
1557 #[test]
1558 fn comparam_execution_removal_releases_lease() {
1559 let access = CountingCommunicationAccess {
1560 active: Arc::new(()),
1561 };
1562 let Ok(communication_guard) = access.acquire() else {
1563 panic!("enabled communication must provide a guard");
1564 };
1565 let id = Uuid::new_v4();
1566 let mut executions = IndexMap::new();
1567 executions.insert(
1568 id,
1569 ComparamExecution::new(
1570 sovd_ecu::operations::comparams::Execution {
1571 capability: sovd_ecu::operations::comparams::executions::Capability::Execute,
1572 status: sovd_ecu::operations::comparams::executions::Status::Running,
1573 comparam_override: HashMap::default(),
1574 },
1575 communication_guard,
1576 ),
1577 );
1578
1579 assert_eq!(Arc::strong_count(&access.active), 2);
1580 executions.shift_remove(&id);
1581 assert_eq!(Arc::strong_count(&access.active), 1);
1582 }
1583
1584 #[test]
1585 fn async_handoff_preserves_execution_and_releases_lease_on_completion() {
1586 let executions = Arc::new(StdRwLock::new(HashMap::default()));
1587 let access = CountingCommunicationAccess {
1588 active: Arc::new(()),
1589 };
1590 let Ok(reservation) = acquire_and_reserve_execution::<ServiceExecution>(
1591 &access,
1592 Arc::clone(&executions),
1593 "routine",
1594 "routine",
1595 false,
1596 ) else {
1597 panic!("enabled communication must reserve execution");
1598 };
1599
1600 let id = reservation.commit_async(|execution| {
1601 execution
1602 .parameters
1603 .insert("result".to_owned(), serde_json::json!("ok"));
1604 });
1605
1606 assert_eq!(Arc::strong_count(&access.active), 2);
1607 let mut executions_guard = lock_write(&executions);
1608 let execution = executions_guard
1609 .get_mut("routine")
1610 .and_then(|entries| entries.get_mut(&id))
1611 .expect("async execution");
1612 assert!(execution.is_created && !execution.in_flight);
1613 execution.complete();
1614 drop(executions_guard);
1615
1616 assert_eq!(Arc::strong_count(&access.active), 1);
1617 assert!(
1618 lock_read(&executions)
1619 .get("routine")
1620 .is_some_and(|entries| entries.contains_key(&id))
1621 );
1622 }
1623
1624 #[test]
1625 fn failed_reservation_releases_acquired_lease() {
1626 let mut entries = IndexMap::new();
1627 entries.insert(
1628 Uuid::new_v4(),
1629 ServiceExecution {
1630 parameters: serde_json::Map::new(),
1631 status: sovd_ecu::operations::ExecutionStatus::Running,
1632 in_flight: false,
1633 is_created: true,
1634 communication_lease: None,
1635 },
1636 );
1637 let mut execution_map = HashMap::default();
1638 execution_map.insert("routine".to_owned(), entries);
1639 let executions = Arc::new(StdRwLock::new(execution_map));
1640 let access = CountingCommunicationAccess {
1641 active: Arc::new(()),
1642 };
1643
1644 let result = acquire_and_reserve_execution::<ServiceExecution>(
1645 &access, executions, "routine", "routine", false,
1646 );
1647 let Err(error) = result else {
1648 panic!("second running execution must be rejected");
1649 };
1650
1651 assert!(matches!(error.error, ApiError::Conflict(_)));
1652 assert_eq!(Arc::strong_count(&access.active), 1);
1653 }
1654
1655 pub fn create_test_webserver_state<T: UdsEcu + Clone, U: FileManager>(
1656 ecu_name: String,
1657 uds: T,
1658 file_manager: U,
1659 ) -> WebserverEcuState<T, U> {
1660 WebserverEcuState {
1661 ecu_name,
1662 uds,
1663 locks: Arc::new(Locks::new()),
1664 comparam_executions: Arc::new(RwLock::new(IndexMap::new())),
1665 communication_access: enabled_communication_access_for_test(),
1666 service_executions: Arc::new(StdRwLock::new(HashMap::default())),
1667 flash_data: Arc::new(RwLock::new(FileList {
1668 files: Vec::new(),
1669 path: Some(PathBuf::new()),
1670 schema: None,
1671 })),
1672 flash_functional_class_default: Arc::from("flash_download_upload"),
1673 mdd_embedded_files: Arc::new(file_manager),
1674 }
1675 }
1676}