use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use camino::Utf8PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::condition::Condition;
use crate::profile::{ProfileDefinition, ProfileName, ResolvedProfile};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileFlags {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub defines: Vec<String>,
#[serde(
default,
rename = "include-dirs",
skip_serializing_if = "Vec::is_empty"
)]
pub include_dirs: Vec<Utf8PathBuf>,
#[serde(default, rename = "cflags", skip_serializing_if = "Vec::is_empty")]
pub cflags: Vec<String>,
#[serde(default, rename = "cxxflags", skip_serializing_if = "Vec::is_empty")]
pub cxxflags: Vec<String>,
#[serde(default, rename = "ldflags", skip_serializing_if = "Vec::is_empty")]
pub ldflags: Vec<String>,
#[serde(default, rename = "link-libs", skip_serializing_if = "Vec::is_empty")]
pub link_libs: Vec<String>,
}
impl ProfileFlags {
pub fn is_empty(&self) -> bool {
self.defines.is_empty()
&& self.include_dirs.is_empty()
&& self.cflags.is_empty()
&& self.cxxflags.is_empty()
&& self.ldflags.is_empty()
&& self.link_libs.is_empty()
}
pub fn validate(&self) -> Result<(), BuildFlagsValidationError> {
for define in &self.defines {
if define.is_empty() {
return Err(BuildFlagsValidationError::EmptyDefine);
}
if define.starts_with('=') {
return Err(BuildFlagsValidationError::DefineMissingName {
raw: define.clone(),
});
}
}
for dir in &self.include_dirs {
validate_include_dir(dir.as_std_path())?;
}
for lib in &self.link_libs {
if !is_safe_link_lib(lib) {
return Err(BuildFlagsValidationError::InvalidLinkLib { raw: lib.clone() });
}
}
Ok(())
}
}
pub fn is_safe_link_lib(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphanumeric() || first == '_') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '+' | '-'))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConditionalProfileFlags {
pub condition: Condition,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<ProfileName>,
#[serde(flatten, default, skip_serializing_if = "ProfileFlags::is_empty")]
pub flags: ProfileFlags,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileSettings {
#[serde(default, skip_serializing_if = "ProfileFlags::is_empty")]
pub general: ProfileFlags,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conditional: Vec<ConditionalProfileFlags>,
}
impl ProfileSettings {
pub fn is_empty(&self) -> bool {
self.general.is_empty() && self.conditional.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedProfileFlags {
pub defines: Vec<String>,
pub include_dirs: Vec<Utf8PathBuf>,
pub system_include_dirs: Vec<Utf8PathBuf>,
pub extra_compile_args: Vec<String>,
pub cflags: Vec<String>,
pub cxxflags: Vec<String>,
pub ldflags: Vec<String>,
pub link_libs: Vec<String>,
}
impl ResolvedProfileFlags {
pub fn is_empty(&self) -> bool {
self.defines.is_empty()
&& self.include_dirs.is_empty()
&& self.system_include_dirs.is_empty()
&& self.extra_compile_args.is_empty()
&& self.cflags.is_empty()
&& self.cxxflags.is_empty()
&& self.ldflags.is_empty()
&& self.link_libs.is_empty()
}
pub fn as_json(&self) -> serde_json::Value {
serde_json::json!({
"defines": self.defines,
"include_dirs": self
.include_dirs
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>(),
"system_include_dirs": self
.system_include_dirs
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>(),
"extra_compile_args": self.extra_compile_args,
"cflags": self.cflags,
"cxxflags": self.cxxflags,
"ldflags": self.ldflags,
"link_libs": self.link_libs,
})
}
}
pub fn resolve_build_flags(
package: &ProfileSettings,
profile: Option<&ResolvedProfile>,
definitions: &BTreeMap<ProfileName, ProfileDefinition>,
ctx: &crate::condition::ConditionContext<'_>,
package_trusted: bool,
) -> ResolvedProfileFlags {
let mut out = ResolvedProfileFlags::default();
apply_package_layer(&mut out, &package.general, package_trusted);
for conditional in package
.conditional
.iter()
.filter(|layer| layer.profile.is_none() && layer.condition.evaluate(ctx))
{
apply_package_layer(&mut out, &conditional.flags, package_trusted);
}
if let Some(profile) = profile {
for name in &profile.inherits_chain {
if let Some(flags) = definitions
.get(name)
.and_then(|definition| definition.build.as_ref())
{
apply_layer(&mut out, flags);
}
for conditional in package.conditional.iter().filter(|layer| {
layer.profile.as_ref() == Some(name) && layer.condition.evaluate(ctx)
}) {
apply_package_layer(&mut out, &conditional.flags, package_trusted);
}
}
}
finalize(&mut out);
out
}
fn apply_package_layer(
out: &mut ResolvedProfileFlags,
layer: &ProfileFlags,
package_trusted: bool,
) {
if package_trusted {
apply_layer(out, layer);
return;
}
let mut safe = layer.clone();
safe.cflags.clear();
safe.cxxflags.clear();
safe.ldflags.clear();
apply_layer(out, &safe);
}
macro_rules! append_profile_flag_layer {
($target:expr, $layer:expr) => {{
let target = $target;
let layer = $layer;
target.defines.extend(layer.defines.iter().cloned());
extend_dedup_first_occurrence(&mut target.include_dirs, &layer.include_dirs);
target.cflags.extend(layer.cflags.iter().cloned());
target.cxxflags.extend(layer.cxxflags.iter().cloned());
target.ldflags.extend(layer.ldflags.iter().cloned());
extend_dedup_first_occurrence(&mut target.link_libs, &layer.link_libs);
}};
}
fn extend_dedup_first_occurrence<T: PartialEq + Clone>(target: &mut Vec<T>, items: &[T]) {
for item in items {
if !target.iter().any(|existing| existing == item) {
target.push(item.clone());
}
}
}
impl ProfileFlags {
pub(crate) fn append_layer(&mut self, layer: &ProfileFlags) {
append_profile_flag_layer!(self, layer);
}
}
fn apply_layer(target: &mut ResolvedProfileFlags, layer: &ProfileFlags) {
append_profile_flag_layer!(target, layer);
}
fn finalize(target: &mut ResolvedProfileFlags) {
let dedup: BTreeSet<String> = target.defines.drain(..).collect();
target.defines = dedup.into_iter().collect();
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum BuildFlagsValidationError {
#[error("[profile] declares an empty define entry")]
EmptyDefine,
#[error("[profile] define entry {raw:?} is missing a name")]
DefineMissingName { raw: String },
#[error(
"[profile] link library {raw:?} is not a valid library name; use a bare name like \"pthread\" (no leading `-`, path separators, or whitespace)"
)]
InvalidLinkLib { raw: String },
#[error(
"[profile] include directory {path:?} must be a relative path; absolute paths are not allowed"
)]
AbsoluteIncludeDir { path: String },
#[error(
"[profile] include directory {path:?} must not contain `..`; include search paths cannot escape the package root"
)]
IncludeDirHasParent { path: String },
#[error("[profile] include directory {path:?} contains a non-UTF-8 component")]
NonUtf8IncludeDir { path: String },
}
fn validate_include_dir(dir: &Path) -> Result<(), BuildFlagsValidationError> {
if dir.is_absolute() {
return Err(BuildFlagsValidationError::AbsoluteIncludeDir {
path: display_path(dir),
});
}
for component in dir.components() {
match component {
std::path::Component::ParentDir => {
return Err(BuildFlagsValidationError::IncludeDirHasParent {
path: display_path(dir),
});
}
std::path::Component::Prefix(_) | std::path::Component::RootDir => {
return Err(BuildFlagsValidationError::AbsoluteIncludeDir {
path: display_path(dir),
});
}
std::path::Component::Normal(part) => {
if part.to_str().is_none() {
return Err(BuildFlagsValidationError::NonUtf8IncludeDir {
path: display_path(dir),
});
}
}
std::path::Component::CurDir => {}
}
}
Ok(())
}
fn display_path(dir: &Path) -> String {
dir.display().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiler::{CompilerIdentity, CompilerKind, CompilerVersion};
use crate::condition::{ConditionContext, ConditionKey, TargetPlatform};
use crate::profile::{
ProfileDefinition, ProfileName, ProfileSelection, ResolvedProfile, resolve_profile,
};
use std::collections::BTreeMap;
fn host_for(os: &str) -> TargetPlatform {
let mut p = TargetPlatform::current();
p.os = os.to_owned();
p
}
fn profile_name(value: &str) -> ProfileName {
ProfileName::new(value).unwrap()
}
fn profile_definition(
name: &str,
inherits: Option<&str>,
ldflags: &[&str],
) -> (ProfileName, ProfileDefinition) {
let name = profile_name(name);
(
name.clone(),
ProfileDefinition {
name,
inherits: inherits.map(profile_name),
debug: None,
opt_level: None,
assertions: None,
build: Some(ProfileFlags {
ldflags: ldflags.iter().map(|flag| (*flag).to_owned()).collect(),
..Default::default()
}),
},
)
}
fn profile_definitions() -> BTreeMap<ProfileName, ProfileDefinition> {
BTreeMap::from([
profile_definition("release", None, &["release"]),
profile_definition("static", Some("release"), &["static"]),
])
}
fn selected_profile(
name: &str,
definitions: &BTreeMap<ProfileName, ProfileDefinition>,
) -> ResolvedProfile {
resolve_profile(
&ProfileSelection::from_name(profile_name(name)),
definitions,
)
.unwrap()
}
fn os_condition(os: &str) -> Condition {
Condition::KeyValue {
key: ConditionKey::Os,
value: os.to_owned(),
}
}
fn resolve_without_profile(
package: &ProfileSettings,
ctx: &ConditionContext<'_>,
package_trusted: bool,
) -> ResolvedProfileFlags {
resolve_build_flags(package, None, &BTreeMap::new(), ctx, package_trusted)
}
#[test]
fn named_target_profile_layers_interleave_with_profile_chain() {
let definitions = profile_definitions();
let selected = selected_profile("static", &definitions);
let mut settings = ProfileSettings::default();
settings.general.ldflags = vec!["base".into()];
settings.conditional = vec![
ConditionalProfileFlags {
condition: os_condition("linux"),
profile: None,
flags: ProfileFlags {
ldflags: vec!["linux-base".into()],
..Default::default()
},
},
ConditionalProfileFlags {
condition: os_condition("linux"),
profile: Some(profile_name("release")),
flags: ProfileFlags {
ldflags: vec!["linux-release".into()],
..Default::default()
},
},
ConditionalProfileFlags {
condition: os_condition("linux"),
profile: Some(profile_name("static")),
flags: ProfileFlags {
ldflags: vec!["linux-static".into()],
..Default::default()
},
},
];
let resolved = resolve_build_flags(
&settings,
Some(&selected),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(
resolved.ldflags,
vec![
"base",
"linux-base",
"release",
"linux-release",
"static",
"linux-static",
],
);
}
#[test]
fn named_target_profile_layers_require_profile_and_target_matches() {
let definitions = profile_definitions();
let settings = ProfileSettings {
conditional: vec![
ConditionalProfileFlags {
condition: os_condition("linux"),
profile: Some(profile_name("release")),
flags: ProfileFlags {
ldflags: vec!["linux-release".into()],
..Default::default()
},
},
ConditionalProfileFlags {
condition: os_condition("linux"),
profile: Some(profile_name("undeclared")),
flags: ProfileFlags {
ldflags: vec!["inert".into()],
..Default::default()
},
},
],
..Default::default()
};
let release = selected_profile("release", &definitions);
let release_linux = resolve_build_flags(
&settings,
Some(&release),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(release_linux.ldflags, vec!["release", "linux-release"]);
let static_profile = selected_profile("static", &definitions);
let static_linux = resolve_build_flags(
&settings,
Some(&static_profile),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(
static_linux.ldflags,
vec!["release", "linux-release", "static"],
);
let dev = selected_profile("dev", &definitions);
let dev_linux = resolve_build_flags(
&settings,
Some(&dev),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert!(dev_linux.ldflags.is_empty());
let static_macos = resolve_build_flags(
&settings,
Some(&static_profile),
&definitions,
&ConditionContext::platform_only(&host_for("macos")),
true,
);
assert_eq!(static_macos.ldflags, vec!["release", "static"]);
}
#[test]
fn matching_named_target_profile_layers_keep_manifest_order() {
let definitions = profile_definitions();
let release = selected_profile("release", &definitions);
let mut host = host_for("linux");
host.arch = "x86_64".into();
let settings = ProfileSettings {
conditional: vec![
ConditionalProfileFlags {
condition: os_condition("linux"),
profile: Some(profile_name("release")),
flags: ProfileFlags {
ldflags: vec!["linux".into()],
..Default::default()
},
},
ConditionalProfileFlags {
condition: Condition::KeyValue {
key: ConditionKey::Arch,
value: "x86_64".into(),
},
profile: Some(profile_name("release")),
flags: ProfileFlags {
ldflags: vec!["x86_64".into()],
..Default::default()
},
},
],
..Default::default()
};
let resolved = resolve_build_flags(
&settings,
Some(&release),
&definitions,
&ConditionContext::platform_only(&host),
true,
);
assert_eq!(resolved.ldflags, vec!["release", "linux", "x86_64"]);
}
#[test]
fn empty_settings_resolve_to_empty_flags() {
let p = ProfileSettings::default();
let r = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert!(r.is_empty());
}
#[test]
fn defines_merge_dedup_and_sort() {
let mut p = ProfileSettings::default();
p.general.defines = vec!["B".into(), "A".into(), "B".into()];
let r = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(r.defines, vec!["A".to_owned(), "B".to_owned()]);
}
#[test]
fn include_dirs_keep_first_occurrence_order() {
let mut p = ProfileSettings::default();
p.general.include_dirs = vec![
Utf8PathBuf::from("include"),
Utf8PathBuf::from("third_party/include"),
Utf8PathBuf::from("include"),
];
let r = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(
r.include_dirs,
vec![
Utf8PathBuf::from("include"),
Utf8PathBuf::from("third_party/include"),
]
);
}
#[test]
fn matching_conditional_layer_is_applied() {
let mut p = ProfileSettings::default();
p.general.defines = vec!["BASE".into()];
p.conditional.push(ConditionalProfileFlags {
condition: Condition::KeyValue {
key: ConditionKey::Os,
value: "linux".into(),
},
profile: None,
flags: ProfileFlags {
defines: vec!["LINUX_ONLY".into()],
..Default::default()
},
});
let r = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(r.defines, vec!["BASE".to_owned(), "LINUX_ONLY".to_owned()]);
}
#[test]
fn non_matching_conditional_layer_is_skipped() {
let mut p = ProfileSettings::default();
p.general.defines = vec!["BASE".into()];
p.conditional.push(ConditionalProfileFlags {
condition: Condition::KeyValue {
key: ConditionKey::Os,
value: "macos".into(),
},
profile: None,
flags: ProfileFlags {
defines: vec!["MAC_ONLY".into()],
..Default::default()
},
});
let r = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(r.defines, vec!["BASE".to_owned()]);
}
#[test]
fn profile_layer_appends_after_target_conditional() {
let mut p = ProfileSettings::default();
p.general.cxxflags = vec!["-fPIC".into()];
p.conditional.push(ConditionalProfileFlags {
condition: Condition::KeyValue {
key: ConditionKey::Os,
value: "linux".into(),
},
profile: None,
flags: ProfileFlags {
cxxflags: vec!["-flto=thin".into()],
..Default::default()
},
});
let prof = ProfileFlags {
cxxflags: vec!["-Wall".into()],
..Default::default()
};
let release_name = profile_name("release");
let definitions = BTreeMap::from([(
release_name.clone(),
ProfileDefinition {
name: release_name,
inherits: None,
debug: None,
opt_level: None,
assertions: None,
build: Some(prof),
},
)]);
let selected = selected_profile("release", &definitions);
let r = resolve_build_flags(
&p,
Some(&selected),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(
r.cxxflags,
vec![
"-fPIC".to_owned(),
"-flto=thin".to_owned(),
"-Wall".to_owned(),
]
);
}
#[test]
fn untrusted_package_drops_command_flags_but_keeps_defines_and_includes() {
let mut p = ProfileSettings::default();
p.general.defines = vec!["DEP_DEFINE".into()];
p.general.include_dirs = vec![Utf8PathBuf::from("dep/include")];
p.general.cflags = vec!["-fplugin=evil.so".into()];
p.general.cxxflags = vec!["-Xclang".into(), "-load".into()];
p.general.ldflags = vec!["-fuse-ld=/tmp/evil".into()];
p.conditional.push(ConditionalProfileFlags {
condition: Condition::KeyValue {
key: ConditionKey::Os,
value: "linux".into(),
},
profile: None,
flags: ProfileFlags {
cxxflags: vec!["-B.".into()],
ldflags: vec!["-specs=evil.specs".into()],
..Default::default()
},
});
let untrusted = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
false,
);
assert!(
untrusted.cflags.is_empty(),
"untrusted cflags must be dropped"
);
assert!(
untrusted.cxxflags.is_empty(),
"untrusted cxxflags must be dropped"
);
assert!(
untrusted.ldflags.is_empty(),
"untrusted ldflags must be dropped"
);
assert_eq!(untrusted.defines, vec!["DEP_DEFINE".to_owned()]);
assert_eq!(
untrusted.include_dirs,
vec![Utf8PathBuf::from("dep/include")]
);
let trusted = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert_eq!(trusted.cflags, vec!["-fplugin=evil.so".to_owned()]);
assert_eq!(
trusted.cxxflags,
vec!["-Xclang".to_owned(), "-load".to_owned(), "-B.".to_owned()]
);
assert_eq!(
trusted.ldflags,
vec![
"-fuse-ld=/tmp/evil".to_owned(),
"-specs=evil.specs".to_owned()
]
);
}
#[test]
fn untrusted_package_still_receives_trusted_profile_layer() {
let mut p = ProfileSettings::default();
p.general.cxxflags = vec!["-fplugin=evil.so".into()];
let prof = ProfileFlags {
cxxflags: vec!["-O2".into()],
ldflags: vec!["-s".into()],
..Default::default()
};
let release_name = profile_name("release");
let definitions = BTreeMap::from([(
release_name.clone(),
ProfileDefinition {
name: release_name,
inherits: None,
debug: None,
opt_level: None,
assertions: None,
build: Some(prof),
},
)]);
let selected = selected_profile("release", &definitions);
let r = resolve_build_flags(
&p,
Some(&selected),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
false,
);
assert_eq!(r.cxxflags, vec!["-O2".to_owned()]);
assert_eq!(r.ldflags, vec!["-s".to_owned()]);
}
#[test]
fn untrusted_named_overlay_drops_command_flags_but_keeps_safe_fields() {
let mut package = ProfileSettings::default();
package.conditional.push(ConditionalProfileFlags {
condition: os_condition("linux"),
profile: Some(profile_name("release")),
flags: ProfileFlags {
defines: vec!["SAFE_NAMED_OVERLAY".into()],
cxxflags: vec!["-B.".into()],
ldflags: vec!["-specs=evil.specs".into()],
..Default::default()
},
});
let release_name = profile_name("release");
let definitions = BTreeMap::from([(
release_name.clone(),
ProfileDefinition {
name: release_name,
inherits: None,
debug: None,
opt_level: None,
assertions: None,
build: Some(ProfileFlags {
cxxflags: vec!["-O2".into()],
ldflags: vec!["-s".into()],
..Default::default()
}),
},
)]);
let selected = selected_profile("release", &definitions);
let resolved = resolve_build_flags(
&package,
Some(&selected),
&definitions,
&ConditionContext::platform_only(&host_for("linux")),
false,
);
assert_eq!(resolved.defines, vec!["SAFE_NAMED_OVERLAY"]);
assert_eq!(resolved.cxxflags, vec!["-O2"]);
assert_eq!(resolved.ldflags, vec!["-s"]);
}
#[test]
fn feature_conditional_layer_gated_by_enabled_features() {
let mut p = ProfileSettings::default();
p.conditional.push(ConditionalProfileFlags {
condition: Condition::Feature("single-threaded".into()),
profile: None,
flags: ProfileFlags {
defines: vec!["SQLITE_THREADSAFE=0".into()],
..Default::default()
},
});
let enabled: BTreeSet<String> = BTreeSet::from(["single-threaded".to_owned()]);
let on = resolve_without_profile(
&p,
&ConditionContext::with_features(&host_for("linux"), &enabled),
true,
);
assert_eq!(on.defines, vec!["SQLITE_THREADSAFE=0".to_owned()]);
let off = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
true,
);
assert!(
off.defines.is_empty(),
"feature-off must not apply the layer: {:?}",
off.defines
);
}
#[test]
fn compiler_conditional_layer_gated_by_detected_identity() {
let mut p = ProfileSettings::default();
p.conditional.push(ConditionalProfileFlags {
condition: Condition::parse_inner(r#"all(cxx = "clang", cxx_version = ">=18")"#)
.unwrap(),
profile: None,
flags: ProfileFlags {
cxxflags: vec!["-stdlib=libc++".into()],
..Default::default()
},
});
let host = host_for("linux");
let clang18 = CompilerIdentity {
kind: CompilerKind::Clang,
version: CompilerVersion::parse("18.1.3"),
target: None,
raw_version_line: "clang version 18.1.3".into(),
};
let gcc13 = CompilerIdentity {
kind: CompilerKind::Gcc,
version: CompilerVersion::parse("13.3.0"),
target: None,
raw_version_line: "g++ 13.3.0".into(),
};
let matching = ConditionContext::platform_only(&host).with_compilers(None, Some(&clang18));
let on = resolve_without_profile(&p, &matching, true);
assert_eq!(on.cxxflags, vec!["-stdlib=libc++".to_owned()]);
let other = ConditionContext::platform_only(&host).with_compilers(None, Some(&gcc13));
let off = resolve_without_profile(&p, &other, true);
assert!(off.cxxflags.is_empty());
let undetected = ConditionContext::platform_only(&host);
assert!(
resolve_without_profile(&p, &undetected, true)
.cxxflags
.is_empty()
);
}
#[test]
fn link_libs_merge_dedup_preserving_order() {
let mut p = ProfileSettings::default();
p.general.link_libs = vec!["pthread".into(), "m".into()];
p.conditional.push(ConditionalProfileFlags {
condition: Condition::KeyValue {
key: ConditionKey::Family,
value: "unix".into(),
},
profile: None,
flags: ProfileFlags {
link_libs: vec!["dl".into(), "m".into()],
..Default::default()
},
});
let mut host = host_for("linux");
host.family = "unix".into();
let r = resolve_without_profile(&p, &ConditionContext::platform_only(&host), true);
assert_eq!(
r.link_libs,
vec!["pthread".to_owned(), "m".to_owned(), "dl".to_owned()]
);
}
#[test]
fn link_libs_survive_untrusted_packages() {
let mut p = ProfileSettings::default();
p.general.link_libs = vec!["pthread".into()];
p.general.ldflags = vec!["-fuse-ld=/tmp/evil".into()];
let r = resolve_without_profile(
&p,
&ConditionContext::platform_only(&host_for("linux")),
false,
);
assert_eq!(r.link_libs, vec!["pthread".to_owned()]);
assert!(r.ldflags.is_empty(), "untrusted ldflags must be dropped");
}
#[test]
fn is_safe_link_lib_accepts_bare_library_names() {
for good in ["pthread", "m", "dl", "stdc++", "c++_shared", "libname_v1.2"] {
assert!(is_safe_link_lib(good), "expected {good:?} to be accepted");
}
}
#[test]
fn is_safe_link_lib_rejects_flags_paths_and_whitespace() {
for bad in [
"",
"-lm",
"-Wl,--foo",
"+atomic",
".hidden",
"../foo",
"a/b",
"/abs",
"has space",
"tab\tname",
] {
assert!(!is_safe_link_lib(bad), "expected {bad:?} to be rejected");
}
}
#[test]
fn validate_rejects_flag_like_link_lib() {
for bad in ["-lm", "-Wl,--foo", "../escape", "a/b", "has space", ""] {
let decl = ProfileFlags {
link_libs: vec![bad.into()],
..Default::default()
};
assert!(
matches!(
decl.validate(),
Err(BuildFlagsValidationError::InvalidLinkLib { .. })
),
"expected {bad:?} to be rejected"
);
}
}
#[test]
fn validate_accepts_real_link_lib_names() {
let decl = ProfileFlags {
link_libs: vec!["pthread".into(), "dl".into(), "m".into(), "stdc++".into()],
..Default::default()
};
assert!(decl.validate().is_ok());
}
#[test]
fn validate_rejects_absolute_include_dir() {
let decl = ProfileFlags {
include_dirs: vec![Utf8PathBuf::from("/etc/include")],
..Default::default()
};
let err = decl.validate().unwrap_err();
assert!(matches!(
err,
BuildFlagsValidationError::AbsoluteIncludeDir { .. }
));
}
#[test]
fn validate_rejects_parent_traversal_include_dir() {
let decl = ProfileFlags {
include_dirs: vec![Utf8PathBuf::from("../sneaky")],
..Default::default()
};
let err = decl.validate().unwrap_err();
assert!(matches!(
err,
BuildFlagsValidationError::IncludeDirHasParent { .. }
));
}
#[test]
fn validate_rejects_empty_define() {
let decl = ProfileFlags {
defines: vec![String::new()],
..Default::default()
};
assert!(matches!(
decl.validate().unwrap_err(),
BuildFlagsValidationError::EmptyDefine
));
}
#[test]
fn validate_rejects_define_missing_name() {
let decl = ProfileFlags {
defines: vec!["=oops".into()],
..Default::default()
};
assert!(matches!(
decl.validate().unwrap_err(),
BuildFlagsValidationError::DefineMissingName { .. }
));
}
#[test]
fn resolved_flags_with_only_system_include_dirs_are_not_empty() {
let flags = ResolvedProfileFlags {
system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
..Default::default()
};
assert!(!flags.is_empty());
}
#[test]
fn resolved_flags_json_includes_system_include_dirs() {
let flags = ResolvedProfileFlags {
system_include_dirs: vec![Utf8PathBuf::from("/opt/dep/include")],
..Default::default()
};
assert_eq!(
flags.as_json()["system_include_dirs"],
serde_json::json!(["/opt/dep/include"]),
);
}
}