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}