rafia_stpa/stpa/
hazards.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    sync::Arc,
21};
22
23use csv;
24use serde::{Deserialize, Serialize};
25use serde_yaml;
26
27use crate::{
28    define_default_link_type_impls,
29    stpa::{LoadError, CYCLE_DETECTED_MSG},
30};
31
32use super::StpaData;
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct HazardCsv {
36    #[serde(rename = "Hazard Id")]
37    pub id: String,
38    #[serde(rename = "Hazard description: behaviour, state (condition) / event (time limited)")]
39    pub description: String,
40    #[serde(rename = "Hazard Link To Loss(es)")]
41    pub losses: String,
42    #[serde(rename = "Notes")]
43    pub notes: String,
44}
45
46#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
47pub struct LossLink(Arc<str>);
48
49define_default_link_type_impls!(LossLink);
50
51#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
52pub struct Hazard {
53    pub id: String,
54    pub description: String,
55    pub losses: Vec<LossLink>,
56    pub notes: String,
57}
58
59impl Hazard {
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 hazard
66        if currently_expanding.contains(&self.id) {
67            return Ok(serde_json::json!({
68                "hazard": {
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!({"hazard": self});
78
79        let losses = self
80            .losses
81            .iter()
82            .map(|loss| {
83                stpa_data
84                    .describe_inner(loss.0.as_ref(), currently_expanding)
85                    .map_err(|e| {
86                        format!(
87                            "Failed while resolving loss id {loss} of hazard {}: {e}",
88                            self.id
89                        )
90                    })
91            })
92            .collect::<Result<Vec<_>, _>>()?;
93        obj["hazard"]["losses"] = serde_json::json!(losses);
94
95        let popped = currently_expanding.pop();
96        debug_assert_eq!(popped.as_ref(), Some(&self.id));
97        Ok(obj)
98    }
99}
100
101impl From<HazardCsv> for Hazard {
102    fn from(csv: HazardCsv) -> Self {
103        Hazard {
104            id: csv.id,
105            description: csv.description,
106            losses: csv
107                .losses
108                .split(",")
109                .map(|s| LossLink(Arc::from(s.trim())))
110                .collect(),
111            notes: csv.notes,
112        }
113    }
114}
115
116#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
117pub struct Hazards {
118    pub hazards: Vec<Hazard>,
119}
120
121impl Hazards {
122    pub fn from_yaml(text: &str) -> Result<Hazards, LoadError> {
123        serde_yaml::from_str::<Hazards>(text).map_err(LoadError::Yaml)
124    }
125    pub fn to_yaml(&self) -> Result<String, LoadError> {
126        serde_yaml::to_string(&self).map_err(LoadError::Yaml)
127    }
128    pub fn from_csv(filename: &Path) -> Result<Hazards, LoadError> {
129        let mut hazards = vec![];
130        let file = File::open(filename)?;
131        for hazard in csv::ReaderBuilder::new()
132            .from_reader(file)
133            .deserialize::<HazardCsv>()
134        {
135            hazards.push(hazard?.into());
136        }
137        Ok(Hazards { hazards })
138    }
139    pub fn from_file(filename: &Path) -> Result<Hazards, LoadError> {
140        match filename.extension().and_then(OsStr::to_str) {
141            Some("csv") => Self::from_csv(filename),
142            Some("yml") | Some("yaml") => {
143                let text = fs::read_to_string(filename)?;
144                Self::from_yaml(&text)
145            }
146            _ => Err(LoadError::NotSupportedFormat),
147        }
148    }
149    pub fn find(&self, id: &str) -> Option<&Hazard> {
150        self.hazards.iter().find(|hazard| hazard.id == id)
151    }
152}