rafia_stpa/stpa/
ucas.rs

1// *****************************************************************************
2// Copyright (c) 2025 Codethink
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License, v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is
12// available at https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16use std::{
17    ffi::OsStr,
18    fs::{self, File},
19    path::Path,
20};
21
22use csv;
23use serde::{Deserialize, Serialize};
24use serde_yaml;
25
26use crate::stpa::{InteractionLink, LoadError, UcaContextLink, UcaType, CYCLE_DETECTED_MSG};
27
28use super::{ConstraintLink, StpaData};
29
30//"UCA Id","UCA Definition","CA","UCAType","UCA Context","Constraint Id"
31#[derive(Debug, Serialize, Deserialize)]
32pub struct UcaCsv {
33    #[serde(rename = "UCA Id")]
34    pub id: String,
35    #[serde(rename = "UCA Definition")]
36    pub uca_definition: String,
37    #[serde(rename = "CA")]
38    pub ca: String,
39    #[serde(rename = "UCAType")]
40    pub uca_type: String,
41    #[serde(rename = "UCA Context")]
42    pub uca_context: String,
43    #[serde(rename = "Constraint Id")]
44    pub constraint_id: String,
45    //#[serde(rename = "Additional Constraints")]
46    //pub additional_constraints: String,
47}
48
49#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
50pub struct Uca {
51    pub id: String,
52    pub uca_definition: String,
53    pub ca: InteractionLink,
54    pub uca_type: UcaType,
55    pub uca_context: UcaContextLink,
56    pub constraint_id: Vec<ConstraintLink>,
57}
58
59impl Uca {
60    pub fn describe(
61        &self,
62        stpa_data: &StpaData,
63        currently_expanding: &mut Vec<String>,
64    ) -> Result<serde_json::Value, String> {
65        // check we aren't currently expanding this ca_analysis
66        if currently_expanding.contains(&self.id) {
67            return Ok(serde_json::json!({
68                "uca": {
69                    "id": self.id,
70                    "message": CYCLE_DETECTED_MSG,
71                }
72            }));
73        } else {
74            currently_expanding.push(self.id.clone());
75        }
76
77        let mut obj = serde_json::json!({"uca": self});
78
79        obj["uca"]["ca"] = stpa_data.describe_inner(self.ca.0.as_ref(), currently_expanding)?;
80        obj["uca"]["uca_context"] =
81            stpa_data.describe_inner(self.uca_context.0.as_ref(), currently_expanding)?;
82
83        let ucas = self
84            .constraint_id
85            .iter()
86            .map(|constraint| {
87                stpa_data
88                    .describe_inner(constraint.0.as_ref(), currently_expanding)
89                    .map_err(|e| {
90                        format!(
91                            "Failed while resolving constraint id {constraint} of uca {}: {e}",
92                            self.id
93                        )
94                    })
95            })
96            .collect::<Result<Vec<_>, _>>()?;
97
98        obj["uca"]["constraint_id"] = serde_json::json!(ucas);
99
100        let popped = currently_expanding.pop();
101        debug_assert_eq!(popped.as_ref(), Some(&self.id));
102        Ok(obj)
103    }
104}
105
106impl From<UcaCsv> for Uca {
107    fn from(csv: UcaCsv) -> Self {
108        Uca {
109            id: csv.id,
110            uca_definition: csv.uca_definition,
111            ca: InteractionLink::from(csv.ca),
112            uca_type: UcaType::from(csv.uca_type.as_str()),
113            uca_context: UcaContextLink::from(csv.uca_context),
114            constraint_id: csv
115                .constraint_id
116                .split(",")
117                .filter(|e| !e.is_empty())
118                .map(|s| ConstraintLink::from(s.trim().to_string()))
119                .collect(),
120        }
121    }
122}
123
124#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
125pub struct Ucas {
126    pub ucas: Vec<Uca>,
127}
128
129impl Ucas {
130    pub fn from_yaml(text: &str) -> Result<Ucas, LoadError> {
131        serde_yaml::from_str::<Ucas>(text).map_err(LoadError::Yaml)
132    }
133    pub fn to_yaml(&self) -> Result<String, LoadError> {
134        serde_yaml::to_string(&self).map_err(LoadError::Yaml)
135    }
136    pub fn from_csv(filename: &Path) -> Result<Ucas, LoadError> {
137        let mut ucas = vec![];
138        let file = File::open(filename)?;
139        for uca in csv::ReaderBuilder::new()
140            .from_reader(file)
141            .deserialize::<UcaCsv>()
142        {
143            ucas.push(uca?.into());
144        }
145        Ok(Ucas { ucas })
146    }
147    pub fn from_file(filename: &Path) -> Result<Ucas, LoadError> {
148        match filename.extension().and_then(OsStr::to_str) {
149            Some("csv") => Self::from_csv(filename),
150            Some("yml") | Some("yaml") => {
151                let text = fs::read_to_string(filename)?;
152                Self::from_yaml(&text)
153            }
154            _ => Err(LoadError::NotSupportedFormat),
155        }
156    }
157    pub fn find(&self, id: &str) -> Option<&Uca> {
158        self.ucas.iter().find(|uca| uca.id == id)
159    }
160}