1use crate::{Error, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
8pub enum CxxStandard {
9 C23,
11 #[default]
13 Cpp26,
14}
15impl CxxStandard {
16 pub(super) fn as_str(self) -> &'static str {
17 match self {
18 Self::C23 => "c23",
19 Self::Cpp26 => "c++26",
20 }
21 }
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct CxxSource {
28 pub identifier: String,
30 pub contents: String,
32 pub standard: CxxStandard,
34 pub headers: BTreeMap<String, String>,
36 pub include_paths: Vec<String>,
38 pub defines: Vec<(String, String)>,
40 pub roots: Vec<String>,
42 pub approximate_functions: bool,
44}
45impl CxxSource {
46 pub fn new(identifier: impl Into<String>, contents: impl Into<String>) -> Self {
48 Self {
49 identifier: identifier.into(),
50 contents: contents.into(),
51 standard: CxxStandard::default(),
52 headers: BTreeMap::new(),
53 include_paths: Vec::new(),
54 defines: Vec::new(),
55 roots: Vec::new(),
56 approximate_functions: false,
57 }
58 }
59 pub(super) fn validate(&self) -> Result<()> {
60 if self.identifier.is_empty()
61 || self.identifier.contains('\0')
62 || self
63 .headers
64 .keys()
65 .any(|key| key.is_empty() || key.contains('\0'))
66 || self.include_paths.iter().any(|key| key.contains('\0'))
67 {
68 return Err(Error::Message(
69 "invalid C/C++ source identifier or include path".into(),
70 ));
71 }
72 Ok(())
73 }
74}
75
76#[derive(Clone, Debug, Serialize, Deserialize)]
78pub enum Source {
79 Loom {
81 identifier: String,
83 contents: String,
85 },
86 Cxx(CxxSource),
88}
89impl Source {
90 pub fn loom(identifier: impl Into<String>, contents: impl Into<String>) -> Self {
92 Self::Loom {
93 identifier: identifier.into(),
94 contents: contents.into(),
95 }
96 }
97 pub(super) fn validate(&self) -> Result<()> {
98 match self {
99 Self::Loom { identifier, .. } if identifier.is_empty() || identifier.contains('\0') => {
100 Err(Error::Message("invalid Loom source identifier".into()))
101 }
102 Self::Cxx(source) => source.validate(),
103 Self::Loom { .. } => Ok(()),
104 }
105 }
106}
107
108pub(super) fn virtual_path(path: &str) -> Result<String> {
111 if path.contains('\0') || path.contains('\\') {
112 return Err(Error::Message("invalid virtual include path".into()));
113 }
114 let mut parts = Vec::new();
115 for part in path.split('/') {
116 match part {
117 "" | "." => {}
118 ".." => {
119 if parts.pop().is_none() {
120 return Err(Error::Message(
121 "virtual include escapes its source root".into(),
122 ));
123 }
124 }
125 part => parts.push(part),
126 }
127 }
128 let prefix = if path.starts_with('/') { "/" } else { "" };
129 Ok(format!("{prefix}{}", parts.join("/")))
130}
131impl CxxSource {
132 pub(super) fn normalize(&mut self) -> Result<()> {
133 self.validate()?;
134 self.identifier = virtual_path(&self.identifier)?;
135 if self.identifier.is_empty() || self.identifier == "/" {
136 return Err(Error::Message("source identifier must name a file".into()));
137 }
138 let mut headers = BTreeMap::new();
139 for (path, contents) in &self.headers {
140 let path = virtual_path(path)?;
141 if path.is_empty()
142 || path == "/"
143 || path == self.identifier
144 || headers.insert(path, contents.clone()).is_some()
145 {
146 return Err(Error::Message(
147 "empty or aliased virtual header path".into(),
148 ));
149 }
150 }
151 self.headers = headers;
152 for path in &mut self.include_paths {
153 *path = virtual_path(path)?;
154 }
155 Ok(())
156 }
157}
158#[cfg(test)]
159mod tests {
160 use super::*;
161 #[test]
162 fn virtual_paths_are_lexical_and_ambiguous_aliases_are_rejected() {
163 assert_eq!(
164 virtual_path("/src/./nested/../factor.h").unwrap(),
165 "/src/factor.h"
166 );
167 assert!(virtual_path("../secret.h").is_err());
168 assert!(virtual_path("/../../secret.h").is_err());
169 let mut source = CxxSource::new("src/kernel.cpp", "");
170 source
171 .headers
172 .insert("src/nested/../factor.h".into(), "1".into());
173 source.headers.insert("src/factor.h".into(), "2".into());
174 assert!(source.normalize().is_err());
175 }
176}