1use std::{
17 collections::HashSet,
18 ffi::OsStr,
19 fmt::{self, Display},
20 fs::{self, remove_file, File, OpenOptions},
21 io::{self, Write},
22 option::Option,
23 path::Path,
24 str::FromStr,
25 sync::Arc,
26};
27
28#[cfg(feature = "clap")]
29use clap::ValueEnum;
30use csv;
31#[cfg(feature = "pyo3")]
32use pyo3::prelude::*;
33use serde::{Deserialize, Serialize};
34use serde_yaml;
35use strum::EnumIter;
36use thiserror::Error;
37
38#[cfg(feature = "pyo3")]
39use crate::trudag::{
40 create_link, create_node, find_root, remove_node, rooted_relative_path, set_node,
41 set_node_links, RootError, TrudagError,
42};
43use crate::{
44 define_default_link_type_impls,
45 stpa::{LoadError, CYCLE_DETECTED_MSG},
46};
47
48use super::{CausalScenarioLink, ConstraintLink, HazardLink, StpaData, StpaProject};
49
50#[derive(Debug, Clone)]
51#[cfg_attr(feature = "clap", derive(ValueEnum))]
52pub enum RefType {
53 File,
54 StpaDescribe,
55}
56
57impl Display for RefType {
58 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59 match self {
60 RefType::File => write!(f, "file"),
61 RefType::StpaDescribe => write!(f, "stpa-describe"),
62 }
63 }
64}
65
66impl FromStr for RefType {
67 type Err = ();
68
69 fn from_str(value: &str) -> Result<Self, Self::Err> {
70 match value.to_lowercase().as_str() {
71 "file" => Ok(RefType::File),
72 "stpa-describe" => Ok(RefType::StpaDescribe),
73 _ => Err(()),
74 }
75 }
76}
77
78#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
79pub struct UcaLink(Arc<str>);
80
81#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
82pub struct CsLink(Arc<str>);
83
84#[derive(Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize)]
85pub struct TsfLink(Arc<str>);
86
87define_default_link_type_impls!(TsfLink);
88define_default_link_type_impls!(UcaLink);
89define_default_link_type_impls!(CsLink);
90
91#[derive(Debug, Serialize, Deserialize)]
92pub struct ConstraintCsv {
93 #[serde(rename = "Constraint Id")]
94 pub id: String,
95 #[serde(rename = "Description")]
96 pub description: String,
97 #[serde(rename = "Constraint Type")]
98 pub constraint_type: String,
99 #[serde(rename = "Link to Constraint(s)")]
100 pub link_to_contraints: String,
101 #[serde(rename = "Link to Hazard(s)")]
102 pub link_to_hazards: String,
103 #[serde(rename = "Links to UCA")]
104 pub link_to_uca: String,
105 #[serde(rename = "Links to CS")]
106 pub link_to_cs: String,
107 #[serde(rename = "Links to TSF")]
108 pub link_to_tsf: String,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
125struct TrudagRef {
126 #[serde(rename = "type")]
127 trudag_type: String,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 path: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 project: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 sha: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 item: Option<String>,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139struct TrudagYaml {
140 references: Vec<TrudagRef>,
141 level: String,
142 normative: bool,
143}
144
145fn to_trudag_id(input: &str) -> String {
146 input.replace(".", "_").replace("-", "_").to_uppercase()
147}
148
149#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
150pub struct Constraint {
151 pub id: String,
152 pub description: String,
153 pub constraint_type: ConstraintType,
154 pub link_to_constraints: Vec<ConstraintLink>,
155 pub link_to_hazards: Vec<HazardLink>,
156 pub link_to_uca: Vec<UcaLink>,
157 pub link_to_cs: Vec<CausalScenarioLink>,
158 pub link_to_tsf: Vec<String>,
159}
160
161impl Constraint {
162 pub fn describe(
163 &self,
164 stpa_data: &StpaData,
165 currently_expanding: &mut Vec<String>,
166 ) -> Result<serde_json::Value, String> {
167 if currently_expanding.contains(&self.id) {
169 return Ok(serde_json::json!({
170 "constraint": {
171 "id": self.id,
172 "message": CYCLE_DETECTED_MSG,
173 }
174 }));
175 } else {
176 currently_expanding.push(self.id.clone());
177 }
178
179 let mut obj = serde_json::json!({"constraint": self});
180
181 let constraints = self
182 .link_to_constraints
183 .iter()
184 .map(|constraint| {
185 stpa_data.describe_inner(constraint.0.as_ref(), currently_expanding).map_err(|e| {
186 format!(
187 "Failed while resolving link to constraint id {constraint:?} of constraint {:?}: {e}",
188 self.id
189 )
190 })
191 })
192 .collect::<Result<Vec<_>, _>>()?;
193 obj["constraint"]["link_to_constraints"] = serde_json::json!(constraints);
194
195 let hazards = self
196 .link_to_hazards
197 .iter()
198 .map(|hazard| {
199 stpa_data
200 .describe_inner(hazard.0.as_ref(), currently_expanding)
201 .map_err(|e| {
202 format!(
203 "Failed while resolving hazard id {hazard:?} of constraint {:?}: {e}",
204 self.id
205 )
206 })
207 })
208 .collect::<Result<Vec<_>, _>>()?;
209 obj["constraint"]["link_to_hazards"] = serde_json::json!(hazards);
210
211 let ucas = self
212 .link_to_uca
213 .iter()
214 .map(|uca| {
215 stpa_data
216 .describe_inner(uca.0.as_ref(), currently_expanding)
217 .map_err(|e| {
218 format!(
219 "Failed while resolving UCA id {uca:?} of constraint {:?}: {e}",
220 self.id
221 )
222 })
223 })
224 .collect::<Result<Vec<_>, _>>()?;
225 obj["constraint"]["link_to_uca"] = serde_json::json!(ucas);
226
227 let cs = self
228 .link_to_cs
229 .iter()
230 .map(|cs| {
231 stpa_data
232 .describe_inner(cs.0.as_ref(), currently_expanding)
233 .map_err(|e| {
234 format!(
235 "Failed while resolving CS id {cs:?} of constraint {:?}: {e}",
236 self.id
237 )
238 })
239 })
240 .collect::<Result<Vec<_>, _>>()?;
241 obj["constraint"]["link_to_cs"] = serde_json::json!(cs);
242
243 let popped = currently_expanding.pop();
244 debug_assert_eq!(popped.as_ref(), Some(&self.id));
245 Ok(obj)
246 }
247
248 pub fn trudag_filename(&self, group: &str) -> String {
250 format!("{}.md", self.trudag_name(group))
251 }
252
253 pub fn trudag_name(&self, group: &str) -> String {
257 format!("{group}-{}", self.trudag_id())
258 }
259
260 pub fn trudag_id(&self) -> String {
264 to_trudag_id(&self.id)
265 }
266
267 #[cfg(feature = "pyo3")]
268 fn to_trudag(
273 &self,
274 root: &Path,
275 stpa_project: &StpaProject,
276 level: String,
277 reftype: &RefType,
278 ) -> String {
279 let stpa_project_file =
280 rooted_relative_path(root, stpa_project.project_file.as_ref().unwrap())
281 .unwrap()
282 .to_string_lossy()
283 .to_string();
284 let constraint_path = rooted_relative_path(
285 root,
286 &stpa_project
287 .root
288 .as_ref()
289 .unwrap()
290 .join(stpa_project.constraints.as_ref().unwrap()),
291 )
292 .unwrap()
293 .to_string_lossy()
294 .to_string();
295 let references = match reftype {
296 RefType::File => {
297 let mut full_refs = vec![TrudagRef {
298 path: Some(constraint_path),
299 trudag_type: "file".to_string(),
300 sha: None,
301 item: None,
302 project: None,
303 }];
304
305 if !self.link_to_hazards.is_empty() {
306 let hazard_path = rooted_relative_path(
307 root,
308 &stpa_project
309 .root
310 .as_ref()
311 .unwrap()
312 .join(stpa_project.hazards.as_ref().unwrap()),
313 )
314 .unwrap()
315 .to_string_lossy()
316 .to_string();
317 full_refs.push(TrudagRef {
318 trudag_type: "file".to_string(),
319 path: Some(hazard_path),
320 sha: None,
321 item: None,
322 project: None,
323 })
324 }
325 if !self.link_to_uca.is_empty() {
326 let uca_path = rooted_relative_path(
327 root,
328 &stpa_project
329 .root
330 .as_ref()
331 .unwrap()
332 .join(stpa_project.uca.as_ref().unwrap()),
333 )
334 .unwrap()
335 .to_string_lossy()
336 .to_string();
337 full_refs.push(TrudagRef {
338 trudag_type: "file".to_string(),
339 path: Some(uca_path),
340 sha: None,
341 item: None,
342 project: None,
343 })
344 }
345 if !self.link_to_cs.is_empty() {
346 let scenario_path = rooted_relative_path(
347 root,
348 &stpa_project
349 .root
350 .as_ref()
351 .unwrap()
352 .join(stpa_project.scenarios.as_ref().unwrap()),
353 )
354 .unwrap()
355 .to_string_lossy()
356 .to_string();
357 full_refs.push(TrudagRef {
358 trudag_type: "file".to_string(),
359 path: Some(scenario_path),
360 sha: None,
361 item: None,
362 project: None,
363 })
364 }
365 full_refs
366 }
367 RefType::StpaDescribe => vec![TrudagRef {
368 project: Some(stpa_project_file),
369 path: None,
370 trudag_type: "stpa-describe".to_string(),
371 sha: None,
372 item: Some(self.id.to_string()),
373 }],
374 };
375 let meta = TrudagYaml {
376 references,
377 level,
378 normative: true,
379 };
380
381 let meta = serde_yaml::to_string(&meta).unwrap();
382 let mut out = String::new();
383 out.push_str("---\n");
384 out.push_str(&meta);
385
386 out.push_str("---\n\n");
387 out.push_str(&self.description);
388 out.push('\n');
389
390 out
391 }
392}
393
394impl From<ConstraintCsv> for Constraint {
395 fn from(csv: ConstraintCsv) -> Self {
396 Constraint {
397 id: csv.id,
398 description: csv.description,
399 constraint_type: ConstraintType::from(csv.constraint_type.as_str()),
400 link_to_constraints: csv
401 .link_to_contraints
402 .split(",")
403 .filter(|e| !e.is_empty())
404 .map(|s| ConstraintLink::from(s.trim()))
405 .collect(),
406 link_to_hazards: csv
407 .link_to_hazards
408 .split(",")
409 .filter(|e| !e.is_empty())
410 .map(|s| HazardLink::from(s.trim()))
411 .collect(),
412 link_to_uca: csv
413 .link_to_uca
414 .split(",")
415 .filter(|e| !e.is_empty())
416 .map(|s| UcaLink::from(s.trim()))
417 .collect(),
418 link_to_cs: csv
419 .link_to_cs
420 .split(",")
421 .filter(|e| !e.is_empty())
422 .map(|s| CausalScenarioLink::from(s.trim()))
423 .collect(),
424 link_to_tsf: csv
425 .link_to_tsf
426 .split(",")
427 .filter(|e| !e.is_empty())
428 .map(|s| s.trim().to_owned())
429 .collect(),
430 }
431 }
432}
433
434#[cfg(feature = "pyo3")]
435#[derive(Error, Debug)]
436pub enum ToTrudagError {
437 #[error("Could not find root")]
438 Root(#[from] RootError),
439 #[error("Project does not exist")]
440 NoProject(io::Error, String),
441 #[error("Could not create a link")]
442 Create(TrudagError, String),
443 #[error("Could not link a item")]
444 Link(TrudagError, String, String),
445 #[error("Could not remove a item")]
446 Remove(TrudagError, String),
447 #[error("Could not clear a item")]
448 Clear(TrudagError, String),
449 #[error("Could not save")]
450 Save(#[from] std::io::Error),
451 #[error("Python trudag error")]
452 ErrorPython(#[from] PyErr),
453}
454
455#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
456pub struct Constraints {
457 pub constraints: Vec<Constraint>,
458}
459
460impl Constraints {
461 pub fn from_yaml(text: &str) -> Result<Constraints, LoadError> {
462 serde_yaml::from_str::<Constraints>(text).map_err(LoadError::Yaml)
463 }
464 pub fn to_yaml(&self) -> Result<String, LoadError> {
465 serde_yaml::to_string(&self).map_err(LoadError::Yaml)
466 }
467 pub fn from_csv(filename: &Path) -> Result<Constraints, LoadError> {
468 let mut constraints = vec![];
469 let file = File::open(filename)?;
470 for constraint in csv::ReaderBuilder::new()
471 .from_reader(file)
472 .deserialize::<ConstraintCsv>()
473 {
474 constraints.push(constraint?.into());
475 }
476 Ok(Constraints { constraints })
477 }
478 pub fn from_file(filename: &Path) -> Result<Constraints, LoadError> {
479 match filename.extension().and_then(OsStr::to_str) {
480 Some("csv") => Self::from_csv(filename),
481 Some("yml") | Some("yaml") => {
482 let text = fs::read_to_string(filename)?;
483 Self::from_yaml(&text)
484 }
485 _ => Err(LoadError::NotSupportedFormat),
486 }
487 }
488 #[cfg(feature = "pyo3")]
489 pub fn to_trudag(
490 &self,
491 folder: &Path,
492 stpa_project: &StpaProject,
493 group: &str,
494 reftype: RefType,
495 set_links: bool,
496 ) -> Result<(), ToTrudagError> {
497 let folder = folder.canonicalize().map_err(|e| {
498 ToTrudagError::NoProject(
499 e,
500 folder
501 .to_str()
502 .map(|str| str.to_owned())
503 .unwrap_or(format!("{folder:?}")),
504 )
505 })?;
506
507 let trudag_root = find_root(&folder)?;
508
509 let paths = fs::read_dir(&folder).unwrap();
510
511 let trudag_graph = Python::with_gil(|py| {
512 let trudag_dotstop_graph = py.import("trudag.dotstop.core.graph")?;
513 let from_dir = trudag_dotstop_graph.getattr("build_trustable_graph")?;
514 let trudag_graph = from_dir.call(
515 (trudag_root.join(".dotstop.dot"), trudag_root.clone()),
516 None,
517 )?;
518 let trudag_graph = trudag_graph.unbind();
519 PyResult::Ok(trudag_graph)
520 })?;
521
522 let mut reused_constraints = HashSet::new();
525 for path in paths {
526 let path = path.unwrap();
527 let path = path.path();
528 if path.is_file() {
529 let constraint_file_name = path.file_name().unwrap().to_string_lossy();
530 let constraint = path.file_stem().unwrap().to_string_lossy();
531 if let Some(existing) = self.constraints.iter().find(|cmp_constraint| {
532 cmp_constraint.trudag_filename(group).as_str() == constraint_file_name
533 }) {
534 reused_constraints.insert(existing.id.to_owned());
535 } else {
536 remove_node(&trudag_root, &trudag_graph, &constraint)
537 .map_err(|e| ToTrudagError::Remove(e, constraint.to_string()))?;
538
539 remove_file(&path)?;
540 println!("Removed {path:?}");
541 }
542 }
543 }
544
545 for (index, constraint) in self.constraints.iter().enumerate() {
546 if !reused_constraints.contains(&constraint.id) {
547 create_node(
548 &trudag_root,
549 &trudag_graph,
550 group,
551 None,
552 &folder,
553 constraint,
554 )
555 .map_err(|e| ToTrudagError::Create(e, format!("{constraint:?}")))?;
556 }
557 let filename = folder.join(constraint.trudag_filename(group));
558 let trudag = constraint.to_trudag(
559 &trudag_root,
560 stpa_project,
561 format!("1.{}", index + 1),
562 &reftype,
563 );
564 let mut f = OpenOptions::new()
565 .write(true)
566 .truncate(true)
567 .open(filename)?;
568 f.write_all(trudag.as_bytes())?;
569 println!("Created {}", constraint.id)
570 }
571
572 let trudag_graph = Python::with_gil(|py| {
573 let trudag_dotstop_graph = py.import("trudag.dotstop.core.graph")?;
574 let from_dir = trudag_dotstop_graph.getattr("build_trustable_graph")?;
575 let trudag_graph = from_dir.call(
576 (trudag_root.join(".dotstop.dot"), trudag_root.clone()),
577 None,
578 )?;
579 let trudag_graph = trudag_graph.unbind();
580 PyResult::Ok(trudag_graph)
581 })?;
582
583 for constraint in &self.constraints {
586 for tsf in &constraint.link_to_tsf {
587 create_link(&trudag_root, &trudag_graph, group, constraint, tsf).map_err(|e| {
588 ToTrudagError::Link(e, format!("{constraint:?}"), tsf.to_string())
589 })?;
590 }
591 for constraint_link in &constraint.link_to_constraints {
592 create_link(
593 &trudag_root,
594 &trudag_graph,
595 group,
596 constraint,
597 &format!("{group}-{}", to_trudag_id(&constraint_link.0)),
598 )
599 .map_err(|e| {
600 ToTrudagError::Link(e, format!("{constraint:?}"), constraint_link.0.to_string())
601 })?;
602 }
603 println!("Linked {}", constraint.id)
604 }
605 for constraint in &self.constraints {
607 set_node(&trudag_root, &trudag_graph, group, constraint)
608 .map_err(|e| ToTrudagError::Clear(e, format!("{constraint:?}")))?;
609 if set_links {
610 set_node_links(&trudag_root, &trudag_graph, group, constraint)
611 .map_err(|e| ToTrudagError::Clear(e, format!("{constraint:?}")))?;
612 }
613 println!("Set {}", constraint.id)
614 }
615 Ok(())
616 }
617 pub fn find(&self, id: &str) -> Option<&Constraint> {
618 self.constraints
619 .iter()
620 .find(|constraint| constraint.id == id)
621 }
622}
623
624#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumIter)]
625pub enum ConstraintType {
626 SystemLevelConstraint,
627 ControllerFunctionalConstraint,
628 CausalScenarioConstraint,
629 Missing,
630}
631
632impl fmt::Display for ConstraintType {
633 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
634 match self {
635 ConstraintType::SystemLevelConstraint => {
636 write!(f, "System Level Constraint")
637 }
638 ConstraintType::ControllerFunctionalConstraint => {
639 write!(f, "Constroller Functional Constartint")
640 }
641 ConstraintType::CausalScenarioConstraint => {
642 write!(f, "Causal Scenario Constraint")
643 }
644 ConstraintType::Missing => {
645 write!(f, "Constraint not definded or missing")
646 }
647 }
648 }
649}
650
651impl From<&str> for ConstraintType {
652 fn from(raw: &str) -> Self {
653 match raw.trim() {
654 "SLC" => ConstraintType::SystemLevelConstraint,
655 "CFC" => ConstraintType::ControllerFunctionalConstraint,
656 "CSC" => ConstraintType::CausalScenarioConstraint,
657 _ => ConstraintType::Missing,
658 }
659 }
660}