use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub enum CxxStandard {
C23,
#[default]
Cpp26,
}
impl CxxStandard {
pub(super) fn as_str(self) -> &'static str {
match self {
Self::C23 => "c23",
Self::Cpp26 => "c++26",
}
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub enum CxxFpModel {
#[default]
Precise,
Fast,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CxxSource {
pub identifier: String,
pub contents: String,
pub standard: CxxStandard,
pub headers: BTreeMap<String, String>,
pub include_paths: Vec<String>,
pub defines: Vec<(String, String)>,
pub roots: Vec<String>,
#[serde(default)]
pub fp_model: CxxFpModel,
#[serde(default)]
pub source_identifiers: BTreeMap<String, String>,
#[serde(skip)]
pub preamble: Option<CxxPreamble>,
}
#[derive(Clone)]
pub struct CxxPreamble(pub(super) std::sync::Arc<super::native::Preamble>);
impl std::fmt::Debug for CxxPreamble {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CxxPreamble").finish_non_exhaustive()
}
}
impl CxxSource {
pub fn new(identifier: impl Into<String>, contents: impl Into<String>) -> Self {
Self {
identifier: identifier.into(),
contents: contents.into(),
standard: CxxStandard::default(),
headers: BTreeMap::new(),
include_paths: Vec::new(),
defines: Vec::new(),
roots: Vec::new(),
fp_model: CxxFpModel::default(),
source_identifiers: BTreeMap::new(),
preamble: None,
}
}
pub(super) fn validate(&self) -> Result<()> {
if self.identifier.is_empty()
|| self.identifier.contains('\0')
|| self
.headers
.keys()
.any(|key| key.is_empty() || key.contains('\0'))
|| self.include_paths.iter().any(|key| key.contains('\0'))
{
return Err(Error::Message(
"invalid C/C++ source identifier or include path".into(),
));
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Source {
Loom {
identifier: String,
contents: String,
},
Cxx(CxxSource),
}
impl Source {
pub fn loom(identifier: impl Into<String>, contents: impl Into<String>) -> Self {
Self::Loom {
identifier: identifier.into(),
contents: contents.into(),
}
}
pub(super) fn validate(&self) -> Result<()> {
match self {
Self::Loom { identifier, .. } if identifier.is_empty() || identifier.contains('\0') => {
Err(Error::Message("invalid Loom source identifier".into()))
}
Self::Cxx(source) => source.validate(),
Self::Loom { .. } => Ok(()),
}
}
}
pub(super) fn virtual_path(path: &str) -> Result<String> {
if path.contains('\0') || path.contains('\\') {
return Err(Error::Message("invalid virtual include path".into()));
}
let mut parts = Vec::new();
for part in path.split('/') {
match part {
"" | "." => {}
".." => {
if parts.pop().is_none() {
return Err(Error::Message(
"virtual include escapes its source root".into(),
));
}
}
part => parts.push(part),
}
}
let prefix = if path.starts_with('/') { "/" } else { "" };
Ok(format!("{prefix}{}", parts.join("/")))
}
impl CxxSource {
pub(super) fn preamble_key(&self) -> Result<String> {
Ok(crate::bundle::digest(&serde_json::to_vec(&(
&self.identifier,
self.standard,
&self.headers,
&self.include_paths,
&self.defines,
&self.source_identifiers,
))?))
}
pub(super) fn normalize(&mut self) -> Result<()> {
self.validate()?;
self.identifier = virtual_path(&self.identifier)?;
if self.identifier.is_empty() || self.identifier == "/" {
return Err(Error::Message("source identifier must name a file".into()));
}
let mut headers = BTreeMap::new();
for (path, contents) in &self.headers {
let path = virtual_path(path)?;
if path.is_empty()
|| path == "/"
|| path == self.identifier
|| headers.insert(path, contents.clone()).is_some()
{
return Err(Error::Message(
"empty or aliased virtual header path".into(),
));
}
}
self.headers = headers;
for path in &mut self.include_paths {
*path = virtual_path(path)?;
}
let mut identifiers = BTreeMap::new();
for (path, identifier) in &self.source_identifiers {
if identifier.is_empty()
|| identifier.contains('\0')
|| identifiers
.insert(virtual_path(path)?, identifier.clone())
.is_some()
{
return Err(Error::Message(
"invalid or aliased source identifier mapping".into(),
));
}
}
let mut logical = std::collections::BTreeSet::new();
for path in std::iter::once(&self.identifier).chain(self.headers.keys()) {
if !logical.insert(identifiers.get(path).unwrap_or(path)) {
return Err(Error::Message(
"source identifier mapping merges distinct sources".into(),
));
}
}
self.source_identifiers = identifiers;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_paths_are_lexical_and_ambiguous_aliases_are_rejected() {
assert_eq!(
virtual_path("/src/./nested/../factor.h").unwrap(),
"/src/factor.h"
);
assert!(virtual_path("../secret.h").is_err());
assert!(virtual_path("/../../secret.h").is_err());
let mut source = CxxSource::new("src/kernel.cpp", "");
source
.headers
.insert("src/nested/../factor.h".into(), "1".into());
source.headers.insert("src/factor.h".into(), "2".into());
assert!(source.normalize().is_err());
}
}