use std::path::{Path, PathBuf};
use std::str::FromStr;
use serde::Deserialize;
use crate::layer::{LayerId, LayerIdError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Channel {
Qualified,
Rolling,
}
impl Channel {
pub fn as_str(self) -> &'static str {
match self {
Channel::Qualified => "qualified",
Channel::Rolling => "rolling",
}
}
}
impl std::str::FromStr for Channel {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
match s {
"qualified" => Ok(Channel::Qualified),
"rolling" => Ok(Channel::Rolling),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ExportKind {
Cargo,
CratesVendor,
BazelRegistry,
BazelDistdir,
Vsix,
Sdk,
}
impl ExportKind {
pub fn as_str(self) -> &'static str {
match self {
ExportKind::Cargo => "cargo",
ExportKind::CratesVendor => "crates-vendor",
ExportKind::BazelRegistry => "bazel-registry",
ExportKind::BazelDistdir => "bazel-distdir",
ExportKind::Vsix => "vsix",
ExportKind::Sdk => "sdk",
}
}
pub fn is_sourced(self) -> bool {
matches!(self, ExportKind::Sdk)
}
pub const ALL: &'static [ExportKind] = &[
ExportKind::Cargo,
ExportKind::CratesVendor,
ExportKind::BazelRegistry,
ExportKind::BazelDistdir,
ExportKind::Vsix,
ExportKind::Sdk,
];
}
impl std::fmt::Display for ExportKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ExportKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
ExportKind::ALL
.iter()
.find(|k| k.as_str() == s)
.copied()
.ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShimOrder {
BeforeShims,
AfterShims,
}
impl ShimOrder {
pub fn as_str(self) -> &'static str {
match self {
ShimOrder::BeforeShims => "before-shims",
ShimOrder::AfterShims => "after-shims",
}
}
}
impl FromStr for ShimOrder {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
match s {
"before-shims" => Ok(ShimOrder::BeforeShims),
"after-shims" => Ok(ShimOrder::AfterShims),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportEnv {
pub script: String,
pub path: ShimOrder,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportDecl {
pub kind: ExportKind,
pub out: String,
pub select: Option<Vec<String>>,
pub env: Option<ExportEnv>,
}
impl ExportDecl {
pub fn dir(&self, project_root: &Path) -> PathBuf {
project_root.join(&self.out)
}
pub fn env_script(&self, project_root: &Path) -> Option<PathBuf> {
self.env
.as_ref()
.map(|e| self.dir(project_root).join(&e.script))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pin {
pub realm: Option<String>,
pub channel: Channel,
pub layer: LayerId,
pub digest: Option<String>,
pub tools: Option<Vec<String>>,
pub exports: Vec<ExportDecl>,
}
#[derive(Debug, thiserror::Error)]
pub enum PinError {
#[error("failed to read {path}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("{path}: not valid varve.toml")]
Toml {
path: String,
#[source]
source: Box<toml::de::Error>,
},
#[error("{path}: manifest-version {found} is not supported (this varve understands version 1)")]
UnsupportedManifestVersion { path: String, found: i64 },
#[error("{path}: invalid layer identifier")]
Layer {
path: String,
#[source]
source: LayerIdError,
},
#[error(
"{path}: digest '{found}' is not a valid digest: expected 'sha256:' followed by 64 hex characters"
)]
MalformedDigest { path: String, found: String },
#[error(
"{path}: tool name {name:?} is not a plain name — a tool is looked up INSIDE \
the pinned layer, so a path would resolve outside it. Name the tool only, \
e.g. tools = [\"rivet\"]."
)]
ToolNameIsAPath { path: String, name: String },
#[error("{path}: tools list is present but empty — omit it to select every tool in the layer")]
EmptyTools { path: String },
#[error(
"{path}: export kind {kind:?} is not one this varve can produce — expected one of {expected}"
)]
UnknownExportKind {
path: String,
kind: String,
expected: String,
},
#[error(
"{path}: export destination {out:?} is not usable ({why}) — an export directory is \
RELATIVE to the directory holding varve.toml, so the declaration travels with the \
repository and means the same thing on every machine"
)]
ExportOutEscapes {
path: String,
out: String,
why: String,
},
#[error(
"{path}: exports {first:?} and {second:?} both write to {out:?} — the second would \
overwrite the first's stamp, and `verify` would then check one export twice while \
never checking the other at all"
)]
DuplicateExportOut {
path: String,
out: String,
first: String,
second: String,
},
#[error(
"{path}: export to {out:?} has an empty select list — omit it to export the whole layer"
)]
EmptyExportSelect { path: String, out: String },
#[error(
"{path}: export to {out:?} selects {name:?}, which is not a plain payload name — a \
selection indexes the VERIFIED layer, so a path would reach outside it"
)]
ExportSelectIsAPath {
path: String,
out: String,
name: String,
},
#[error(
"{path}: export to {out:?} is a {kind} export, which is consumed by POINTING at it, not \
by sourcing it — an [export.env] here would be accepted, ignored, and believed. Only \
these kinds are entered as an environment: {sourced}"
)]
ExportEnvNotSourced {
path: String,
out: String,
kind: String,
sourced: String,
},
#[error(
"{path}: export to {out:?} declares an environment but not where it sits relative to \
varve's shims. Add `path = \"before-shims\"` if this environment's bin is meant to win \
on PATH, or `path = \"after-shims\"` if varve's pinned tools are. Undeclared, `verify` \
cannot tell a legitimate sourced SDK from a hijacked PATH (REQ-SHADOW-001), and \
guessing wrong either misses a real one or cries wolf on a correct setup"
)]
ExportEnvNeedsShimOrder { path: String, out: String },
#[error(
"{path}: export to {out:?} declares path = {found:?} — expected \"before-shims\" or \
\"after-shims\""
)]
UnknownShimOrder {
path: String,
out: String,
found: String,
},
#[error(
"{path}: export to {out:?} sources {script:?}, which is not usable ({why}) — the script \
is relative to the export directory, and it must stay inside it"
)]
ExportScriptEscapes {
path: String,
out: String,
script: String,
why: String,
},
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPin {
#[serde(rename = "manifest-version")]
manifest_version: i64,
toolchain: RawToolchain,
#[serde(default, rename = "export")]
exports: Vec<RawExport>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawExport {
kind: String,
out: String,
select: Option<Vec<String>>,
env: Option<RawExportEnv>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawExportEnv {
script: String,
path: Option<String>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawToolchain {
#[serde(default)]
realm: Option<String>,
channel: Channel,
layer: String,
digest: Option<String>,
tools: Option<Vec<String>>,
}
impl Pin {
pub fn parse(content: &str, origin: &str) -> Result<Self, PinError> {
let raw: RawPin = toml::from_str(content).map_err(|source| PinError::Toml {
path: origin.to_string(),
source: Box::new(source),
})?;
if raw.manifest_version != 1 {
return Err(PinError::UnsupportedManifestVersion {
path: origin.to_string(),
found: raw.manifest_version,
});
}
let layer = LayerId::from_str(&raw.toolchain.layer).map_err(|source| PinError::Layer {
path: origin.to_string(),
source,
})?;
if let Some(digest) = &raw.toolchain.digest {
let hex = digest
.strip_prefix("sha256:")
.ok_or_else(|| PinError::MalformedDigest {
path: origin.to_string(),
found: digest.clone(),
})?;
if hex.len() != 64 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(PinError::MalformedDigest {
path: origin.to_string(),
found: digest.clone(),
});
}
}
if let Some(tools) = &raw.toolchain.tools {
if tools.is_empty() {
return Err(PinError::EmptyTools {
path: origin.to_string(),
});
}
for name in tools {
let plain = !name.is_empty()
&& name != "."
&& name != ".."
&& !name.contains('/')
&& !name.contains('\\')
&& !name.contains('\0');
if !plain {
return Err(PinError::ToolNameIsAPath {
path: origin.to_string(),
name: name.clone(),
});
}
}
}
let exports = parse_exports(&raw.exports, origin)?;
Ok(Pin {
realm: raw.toolchain.realm,
channel: raw.toolchain.channel,
layer,
digest: raw.toolchain.digest,
tools: raw.toolchain.tools,
exports,
})
}
pub fn load(path: &Path) -> Result<Self, PinError> {
let content = std::fs::read_to_string(path).map_err(|source| PinError::Io {
path: path.display().to_string(),
source,
})?;
Self::parse(&content, &path.display().to_string())
}
}
fn contained_relative_fault(value: &str) -> Option<String> {
if value.is_empty() {
return Some("empty".into());
}
if value.starts_with('/') || value.starts_with('\\') || value.contains(':') {
return Some("absolute".into());
}
if value.contains('\0') {
return Some("contains a NUL".into());
}
for component in value.split(['/', '\\']) {
if component == ".." {
return Some("climbs out with '..'".into());
}
}
if value.split(['/', '\\']).all(|c| c.is_empty() || c == ".") {
return Some("names no directory".into());
}
None
}
fn parse_exports(raw: &[RawExport], origin: &str) -> Result<Vec<ExportDecl>, PinError> {
let mut decls: Vec<ExportDecl> = Vec::with_capacity(raw.len());
for e in raw {
let kind = ExportKind::from_str(&e.kind).map_err(|()| PinError::UnknownExportKind {
path: origin.to_string(),
kind: e.kind.clone(),
expected: ExportKind::ALL
.iter()
.map(|k| k.as_str())
.collect::<Vec<_>>()
.join(", "),
})?;
if let Some(why) = contained_relative_fault(&e.out) {
return Err(PinError::ExportOutEscapes {
path: origin.to_string(),
out: e.out.clone(),
why,
});
}
if let Some(first) = decls.iter().find(|d| d.out == e.out) {
return Err(PinError::DuplicateExportOut {
path: origin.to_string(),
out: e.out.clone(),
first: first.kind.to_string(),
second: kind.to_string(),
});
}
if let Some(select) = &e.select {
if select.is_empty() {
return Err(PinError::EmptyExportSelect {
path: origin.to_string(),
out: e.out.clone(),
});
}
for name in select {
let plain = !name.is_empty()
&& name != "."
&& name != ".."
&& !name.contains('/')
&& !name.contains('\\')
&& !name.contains('\0');
if !plain {
return Err(PinError::ExportSelectIsAPath {
path: origin.to_string(),
out: e.out.clone(),
name: name.clone(),
});
}
}
}
let env = match &e.env {
None => None,
Some(raw_env) => {
if !kind.is_sourced() {
return Err(PinError::ExportEnvNotSourced {
path: origin.to_string(),
out: e.out.clone(),
kind: kind.to_string(),
sourced: ExportKind::ALL
.iter()
.filter(|k| k.is_sourced())
.map(|k| k.as_str())
.collect::<Vec<_>>()
.join(", "),
});
}
if let Some(why) = contained_relative_fault(&raw_env.script) {
return Err(PinError::ExportScriptEscapes {
path: origin.to_string(),
out: e.out.clone(),
script: raw_env.script.clone(),
why,
});
}
let Some(order) = &raw_env.path else {
return Err(PinError::ExportEnvNeedsShimOrder {
path: origin.to_string(),
out: e.out.clone(),
});
};
let path = ShimOrder::from_str(order).map_err(|()| PinError::UnknownShimOrder {
path: origin.to_string(),
out: e.out.clone(),
found: order.clone(),
})?;
Some(ExportEnv {
script: raw_env.script.clone(),
path,
})
}
};
decls.push(ExportDecl {
kind,
out: e.out.clone(),
select: e.select.clone(),
env,
});
}
Ok(decls)
}
#[derive(Debug, PartialEq, Eq)]
pub enum DeclaredExportStatus {
Current,
Missing,
Stale { stamped: String, current: String },
KindMismatch { declared: String, stamped: String },
Unreadable(String),
}
impl DeclaredExportStatus {
pub fn is_current(&self) -> bool {
matches!(self, DeclaredExportStatus::Current)
}
}
pub fn check_declared_export(
decl: &ExportDecl,
project_root: &Path,
current_manifest_digest: &str,
) -> DeclaredExportStatus {
use crate::exportstamp::{ExportStampError, ExportStatus, read_stamp, status};
let dir = decl.dir(project_root);
match read_stamp(&dir) {
Err(ExportStampError::Missing(_)) => DeclaredExportStatus::Missing,
Err(other) => DeclaredExportStatus::Unreadable(other.to_string()),
Ok(stamp) => {
if stamp.kind != decl.kind.as_str() {
return DeclaredExportStatus::KindMismatch {
declared: decl.kind.as_str().to_string(),
stamped: stamp.kind,
};
}
match status(&stamp, current_manifest_digest) {
ExportStatus::Current => DeclaredExportStatus::Current,
ExportStatus::Stale { stamped, current } => {
DeclaredExportStatus::Stale { stamped, current }
}
}
}
}
}
pub fn env_lines(pin: &Pin, project_root: &Path, shim_env: Option<&Path>) -> Vec<String> {
let mut lines = Vec::new();
let sourced = |order: ShimOrder, lines: &mut Vec<String>| {
for decl in pin
.exports
.iter()
.filter(|d| d.env.as_ref().is_some_and(|e| e.path == order))
{
if let Some(script) = decl.env_script(project_root) {
lines.push(format!(
"# {} export {} — declared {} (REQ-EXPORTDECL-001 clause 5)",
decl.kind,
decl.out,
order.as_str()
));
lines.push(format!(". \"{}\"", script.display()));
}
}
};
sourced(ShimOrder::AfterShims, &mut lines);
if let Some(env) = shim_env {
lines.push("# varve's shims".to_string());
lines.push(format!(". \"{}\"", env.display()));
}
sourced(ShimOrder::BeforeShims, &mut lines);
lines
}
#[derive(Debug, PartialEq, Eq)]
pub enum ShadowDeclaration<'a> {
Expected(&'a ExportDecl),
ContradictsDeclaration(&'a ExportDecl),
Undeclared,
}
fn is_within(dir: &Path, path: &Path) -> bool {
let real = |p: &Path| p.canonicalize().unwrap_or_else(|_| p.to_path_buf());
path.starts_with(dir) || real(path).starts_with(real(dir))
}
pub fn classify_shadowing<'a>(
pin: &'a Pin,
project_root: &Path,
found: &Path,
) -> ShadowDeclaration<'a> {
for decl in &pin.exports {
let Some(env) = &decl.env else {
continue;
};
if is_within(&decl.dir(project_root), found) {
return match env.path {
ShimOrder::BeforeShims => ShadowDeclaration::Expected(decl),
ShimOrder::AfterShims => ShadowDeclaration::ContradictsDeclaration(decl),
};
}
}
ShadowDeclaration::Undeclared
}
#[cfg(test)]
mod tests {
use super::*;
const FULL: &str = r#"
manifest-version = 1
[toolchain]
channel = "qualified"
layer = "2026.07.0"
digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
tools = ["rivet", "synth"]
"#;
#[test]
fn parses_a_complete_pin() {
let pin = Pin::parse(FULL, "varve.toml").unwrap();
assert_eq!(pin.channel, Channel::Qualified);
assert_eq!(pin.layer, LayerId::from_str("2026.07.0").unwrap());
assert_eq!(
pin.digest.as_deref(),
Some("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
);
assert_eq!(
pin.tools.as_deref(),
Some(&["rivet".to_string(), "synth".to_string()][..])
);
}
#[test]
fn digest_and_tools_are_optional() {
let pin = Pin::parse(
"manifest-version = 1\n[toolchain]\nchannel = \"rolling\"\nlayer = \"2026.08.0\"\n",
"varve.toml",
)
.unwrap();
assert_eq!(pin.channel, Channel::Rolling);
assert_eq!(pin.digest, None);
assert_eq!(pin.tools, None);
}
#[test]
fn rejects_unsupported_manifest_version() {
let err = Pin::parse(
"manifest-version = 2\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\n",
"varve.toml",
)
.unwrap_err();
assert!(
matches!(err, PinError::UnsupportedManifestVersion { found: 2, .. }),
"got: {err}"
);
}
#[test]
fn rejects_two_part_layer_with_the_grammar_guidance() {
let err = Pin::parse(
"manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07\"\n",
"varve.toml",
)
.unwrap_err();
let PinError::Layer { source, .. } = &err else {
panic!("got: {err}");
};
assert!(matches!(source, LayerIdError::MissingPatch(_)));
assert!(
source.to_string().contains("three-part"),
"the chain must teach the grammar: {source}"
);
}
#[test]
fn rejects_unknown_keys_instead_of_ignoring_them() {
let err = Pin::parse(
"manifest-version = 1\nsurprise = true\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\n",
"varve.toml",
)
.unwrap_err();
assert!(matches!(err, PinError::Toml { .. }), "got: {err}");
}
#[test]
fn rejects_unknown_channel() {
let err = Pin::parse(
"manifest-version = 1\n[toolchain]\nchannel = \"latest\"\nlayer = \"2026.07.0\"\n",
"varve.toml",
)
.unwrap_err();
assert!(matches!(err, PinError::Toml { .. }), "got: {err}");
}
#[test]
fn rejects_malformed_digest() {
for bad in [
"sha256:short",
"md5:aaaa",
"aaaaaaaa",
"sha256:GGGG",
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
] {
let toml = format!(
"manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ndigest = \"{bad}\"\n"
);
let err = Pin::parse(&toml, "varve.toml").unwrap_err();
assert!(
matches!(err, PinError::MalformedDigest { .. }),
"input {bad:?} got: {err}"
);
}
}
#[test]
fn rejects_a_tool_name_that_is_a_path() {
for hostile in [
"/usr/bin/id",
"../../usr/bin/id",
"sub/dir",
"..",
".",
"",
"C:\\Windows\\system32\\cmd.exe",
] {
let content = format!(
"manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"{}\"]\n",
hostile.replace('\\', "\\\\")
);
assert!(
Pin::parse(&content, "varve.toml").is_err(),
"tool name {hostile:?} must be refused — it escapes the layer"
);
}
let ok = "manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = [\"rivet\", \"synth-c\", \"cargo_x\"]\n";
assert!(Pin::parse(ok, "varve.toml").is_ok());
}
#[test]
fn rejects_empty_tools_list() {
let err = Pin::parse(
"manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.07.0\"\ntools = []\n",
"varve.toml",
)
.unwrap_err();
assert!(matches!(err, PinError::EmptyTools { .. }), "got: {err}");
}
#[test]
fn errors_name_the_offending_file() {
let err = Pin::parse("nonsense", "proj/sub/varve.toml").unwrap_err();
assert!(
err.to_string().contains("proj/sub/varve.toml"),
"diagnostic must carry the path: {err}"
);
}
}
#[cfg(test)]
mod export_tests {
use super::*;
use crate::exportstamp::{ExportStamp, write_stamp};
const HEAD: &str =
"manifest-version = 1\n[toolchain]\nchannel = \"qualified\"\nlayer = \"2026.08.0\"\n";
fn parse(exports: &str) -> Result<Pin, PinError> {
Pin::parse(&format!("{HEAD}{exports}"), "varve.toml")
}
const DECLARED: &str = r#"
[[export]]
kind = "cargo"
out = "vendor/registry"
[[export]]
kind = "vsix"
out = ".vscode/varve-extensions"
select = ["rust-lang.rust-analyzer", "vadimcn.vscode-lldb"]
[[export]]
kind = "sdk"
out = "toolchains/poky"
select = ["poky-cortexa53"]
[export.env]
script = "environment-setup-cortexa53-poky-linux"
path = "before-shims"
"#;
#[test]
fn a_project_declares_its_exports_in_the_pin_it_already_has() {
let pin = parse(DECLARED).unwrap();
assert_eq!(pin.exports.len(), 3);
assert_eq!(pin.exports[0].kind, ExportKind::Cargo);
assert_eq!(pin.exports[0].out, "vendor/registry");
assert_eq!(
pin.exports[0].select, None,
"no subset means the whole layer"
);
assert_eq!(pin.exports[0].env, None);
assert_eq!(pin.exports[1].kind, ExportKind::Vsix);
assert_eq!(
pin.exports[1].select.as_deref(),
Some(
&[
"rust-lang.rust-analyzer".to_string(),
"vadimcn.vscode-lldb".to_string()
][..]
)
);
let sdk = &pin.exports[2];
assert_eq!(sdk.kind, ExportKind::Sdk);
let env = sdk.env.as_ref().expect("an sdk is entered, not pointed at");
assert_eq!(env.script, "environment-setup-cortexa53-poky-linux");
assert_eq!(env.path, ShimOrder::BeforeShims);
let root = Path::new("/repo");
assert_eq!(sdk.dir(root), Path::new("/repo/toolchains/poky"));
assert_eq!(
sdk.env_script(root).unwrap(),
Path::new("/repo/toolchains/poky/environment-setup-cortexa53-poky-linux")
);
assert_eq!(pin.exports[0].env_script(root), None);
assert!(Pin::parse(HEAD, "varve.toml").unwrap().exports.is_empty());
}
#[test]
fn the_declared_kind_is_one_varve_can_actually_produce() {
let err = parse("[[export]]\nkind = \"npm\"\nout = \"x\"\n").unwrap_err();
let msg = err.to_string();
assert!(matches!(err, PinError::UnknownExportKind { .. }), "{msg}");
for known in ExportKind::ALL {
assert!(
msg.contains(known.as_str()),
"the refusal must list {known}, or the author has nothing to correct to: {msg}"
);
}
for (kind, wire) in [
(ExportKind::Cargo, "cargo"),
(ExportKind::CratesVendor, "crates-vendor"),
(ExportKind::BazelRegistry, "bazel-registry"),
(ExportKind::BazelDistdir, "bazel-distdir"),
(ExportKind::Vsix, "vsix"),
(ExportKind::Sdk, "sdk"),
] {
assert_eq!(kind.as_str(), wire);
assert_eq!(ExportKind::from_str(wire).unwrap(), kind);
}
assert_eq!(ExportKind::ALL.len(), 6, "ALL must list every variant");
}
#[test]
fn a_destination_that_leaves_the_repository_is_refused() {
for bad in [
"/etc",
"../outside",
"a/../../outside",
"",
".",
"./",
"C:\\x",
] {
let err = parse(&format!(
"[[export]]\nkind = \"cargo\"\nout = \"{}\"\n",
bad.replace('\\', "\\\\")
))
.unwrap_err();
assert!(
matches!(err, PinError::ExportOutEscapes { .. }),
"out {bad:?} must be refused, got: {err}"
);
}
for good in ["vendor", "vendor/registry", "a/b/c"] {
assert!(
parse(&format!("[[export]]\nkind = \"cargo\"\nout = \"{good}\"\n")).is_ok(),
"{good} is an ordinary export directory"
);
}
}
#[test]
fn two_exports_may_not_share_one_directory() {
let err = parse(
"[[export]]\nkind = \"cargo\"\nout = \"vendor\"\n\
[[export]]\nkind = \"vsix\"\nout = \"vendor\"\n",
)
.unwrap_err();
assert!(
matches!(err, PinError::DuplicateExportOut { .. }),
"got: {err}"
);
let msg = err.to_string();
assert!(
msg.contains("cargo") && msg.contains("vsix"),
"names both: {msg}"
);
}
#[test]
fn a_subset_selection_names_payloads_not_paths() {
for bad in ["../evil", "a/b", "", ".", "..", "a\\b"] {
let err = parse(&format!(
"[[export]]\nkind = \"cargo\"\nout = \"v\"\nselect = [\"{}\"]\n",
bad.replace('\\', "\\\\")
))
.unwrap_err();
assert!(
matches!(err, PinError::ExportSelectIsAPath { .. }),
"select {bad:?} must be refused, got: {err}"
);
}
let err = parse("[[export]]\nkind = \"cargo\"\nout = \"v\"\nselect = []\n").unwrap_err();
assert!(
matches!(err, PinError::EmptyExportSelect { .. }),
"got: {err}"
);
}
#[test]
fn an_environment_must_say_where_it_sits_relative_to_the_shims() {
let err = parse(
"[[export]]\nkind = \"sdk\"\nout = \"t\"\n[export.env]\nscript = \"env-setup\"\n",
)
.unwrap_err();
assert!(
matches!(err, PinError::ExportEnvNeedsShimOrder { .. }),
"got: {err}"
);
let msg = err.to_string();
assert!(msg.contains("before-shims"), "offers the answers: {msg}");
assert!(msg.contains("after-shims"), "offers the answers: {msg}");
assert!(
msg.contains("REQ-SHADOW-001"),
"says WHY it is needed, not just that it is: {msg}"
);
for (value, want) in [
("before-shims", ShimOrder::BeforeShims),
("after-shims", ShimOrder::AfterShims),
] {
let pin = parse(&format!(
"[[export]]\nkind = \"sdk\"\nout = \"t\"\n[export.env]\nscript = \"e\"\npath = \"{value}\"\n"
))
.unwrap();
assert_eq!(pin.exports[0].env.as_ref().unwrap().path, want);
assert_eq!(want.as_str(), value);
}
let err = parse(
"[[export]]\nkind = \"sdk\"\nout = \"t\"\n[export.env]\nscript = \"e\"\npath = \"first\"\n",
)
.unwrap_err();
assert!(
matches!(err, PinError::UnknownShimOrder { .. }),
"got: {err}"
);
}
#[test]
fn only_an_export_that_is_sourced_may_declare_an_environment() {
let err = parse(
"[[export]]\nkind = \"cargo\"\nout = \"v\"\n[export.env]\nscript = \"e\"\npath = \"after-shims\"\n",
)
.unwrap_err();
assert!(
matches!(err, PinError::ExportEnvNotSourced { .. }),
"got: {err}"
);
assert!(
err.to_string().contains("sdk"),
"names what IS sourced: {err}"
);
assert!(ExportKind::Sdk.is_sourced());
for pointed in ExportKind::ALL.iter().filter(|k| **k != ExportKind::Sdk) {
assert!(
!pointed.is_sourced(),
"{pointed} is consumed by pointing at it, not by sourcing it"
);
}
let err = parse(
"[[export]]\nkind = \"sdk\"\nout = \"t\"\n[export.env]\nscript = \"../../etc/profile\"\npath = \"after-shims\"\n",
)
.unwrap_err();
assert!(
matches!(err, PinError::ExportScriptEscapes { .. }),
"got: {err}"
);
}
fn stamped(dir: &std::path::Path, kind: &str, digest: &str) {
write_stamp(
dir,
&ExportStamp {
layer: "2026.08.0".into(),
manifest_digest: digest.into(),
kind: kind.into(),
},
)
.unwrap();
}
#[test]
fn every_declared_export_is_checked_and_an_absent_one_fails() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let pin = parse(DECLARED).unwrap();
let current = "sha256:aaaa";
for decl in &pin.exports {
assert_eq!(
check_declared_export(decl, root, current),
DeclaredExportStatus::Missing,
"{} must fail while it does not exist",
decl.out
);
}
for decl in &pin.exports {
stamped(&decl.dir(root), decl.kind.as_str(), current);
let got = check_declared_export(decl, root, current);
assert!(
got.is_current(),
"{} should be fresh, got {got:?}",
decl.out
);
}
let moved = "sha256:bbbb";
assert_eq!(
check_declared_export(&pin.exports[0], root, moved),
DeclaredExportStatus::Stale {
stamped: current.into(),
current: moved.into(),
}
);
let vsix = &pin.exports[1];
stamped(&vsix.dir(root), "cargo", current);
assert_eq!(
check_declared_export(vsix, root, current),
DeclaredExportStatus::KindMismatch {
declared: "vsix".into(),
stamped: "cargo".into(),
}
);
let cargo = &pin.exports[0];
std::fs::write(
cargo.dir(root).join(crate::exportstamp::STAMP_FILE),
b"{not json",
)
.unwrap();
assert!(matches!(
check_declared_export(cargo, root, current),
DeclaredExportStatus::Unreadable(_)
));
}
#[test]
fn is_current_is_false_for_every_status_that_is_not_current() {
assert!(DeclaredExportStatus::Current.is_current());
for status in [
DeclaredExportStatus::Missing,
DeclaredExportStatus::Stale {
stamped: "sha256:aaaa".into(),
current: "sha256:bbbb".into(),
},
DeclaredExportStatus::KindMismatch {
declared: "vsix".into(),
stamped: "cargo".into(),
},
DeclaredExportStatus::Unreadable("truncated".into()),
] {
assert!(
!status.is_current(),
"{status:?} must not report itself current"
);
}
}
#[test]
fn an_export_reached_through_a_symlink_is_the_same_export() {
let tmp = tempfile::tempdir().unwrap();
let real_dir = tmp.path().join("real/export");
std::fs::create_dir_all(real_dir.join("bin")).unwrap();
std::fs::write(real_dir.join("bin/gcc"), b"#!/bin/sh\n").unwrap();
let link = tmp.path().join("link");
#[cfg(unix)]
std::os::unix::fs::symlink(tmp.path().join("real"), &link).unwrap();
#[cfg(not(unix))]
return;
let through_link = link.join("export/bin/gcc");
assert!(
!through_link.starts_with(&real_dir),
"the fixture must be lexically outside, or it proves nothing"
);
assert!(
is_within(&real_dir, &through_link),
"an export reached through a symlinked checkout is the same export"
);
assert!(!is_within(&real_dir, &tmp.path().join("elsewhere/bin/gcc")));
}
#[test]
fn a_declared_sdk_environment_is_not_reported_as_a_hijack() {
let root = Path::new("/repo");
let pin = parse(DECLARED).unwrap();
let sdk_gcc = Path::new("/repo/toolchains/poky/sysroots/x86_64/usr/bin/gcc");
match classify_shadowing(&pin, root, sdk_gcc) {
ShadowDeclaration::Expected(d) => assert_eq!(d.out, "toolchains/poky"),
other => panic!("a declared before-shims SDK must be expected, got {other:?}"),
}
assert_eq!(
classify_shadowing(&pin, root, Path::new("/usr/local/bin/gcc")),
ShadowDeclaration::Undeclared
);
assert_eq!(
classify_shadowing(&pin, root, Path::new("/repo/vendor/registry/gcc")),
ShadowDeclaration::Undeclared
);
let after = parse(
"[[export]]\nkind = \"sdk\"\nout = \"toolchains/poky\"\n[export.env]\nscript = \"e\"\npath = \"after-shims\"\n",
)
.unwrap();
match classify_shadowing(&after, root, sdk_gcc) {
ShadowDeclaration::ContradictsDeclaration(d) => {
assert_eq!(d.out, "toolchains/poky")
}
other => panic!("expected ContradictsDeclaration, got {other:?}"),
}
}
#[test]
fn one_command_sources_the_whole_environment_in_the_declared_path_order() {
let root = Path::new("/repo");
let shim_env = Path::new("/home/u/.varve/env");
let pin = parse(
"[[export]]\nkind = \"cargo\"\nout = \"vendor\"\n\
[[export]]\nkind = \"sdk\"\nout = \"early\"\n\
[export.env]\nscript = \"env-setup-early\"\npath = \"before-shims\"\n\
[[export]]\nkind = \"sdk\"\nout = \"late\"\n\
[export.env]\nscript = \"env-setup-late\"\npath = \"after-shims\"\n",
)
.unwrap();
let sourced: Vec<String> = env_lines(&pin, root, Some(shim_env))
.into_iter()
.filter(|l| l.starts_with(". "))
.collect();
assert_eq!(
sourced,
vec![
". \"/repo/late/env-setup-late\"".to_string(),
". \"/home/u/.varve/env\"".to_string(),
". \"/repo/early/env-setup-early\"".to_string(),
],
"after-shims is sourced FIRST so the shims land ahead of it"
);
assert!(
!env_lines(&pin, root, Some(shim_env))
.join("\n")
.contains("vendor")
);
let no_shims: Vec<String> = env_lines(&pin, root, None)
.into_iter()
.filter(|l| l.starts_with(". "))
.collect();
assert_eq!(no_shims.len(), 2);
assert!(!no_shims.iter().any(|l| l.contains(".varve/env")));
}
}