use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Dep {
pub nome: String,
pub versao: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fonte: Option<DepSource>,
#[serde(default, skip_serializing_if = "is_false")]
pub opcional: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub caracteristicas: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, gen_platform::IsVariant)]
#[serde(tag = "tipo", rename_all = "lowercase")]
pub enum DepSource {
Git {
repo: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
tag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
branch: Option<String>,
},
Path { caminho: String },
}
impl DepSource {
#[must_use]
pub fn default_github(org: &str, nome: &str) -> Self {
Self::Git {
repo: format!("github:{org}/{nome}"),
tag: None,
rev: None,
branch: None,
}
}
#[must_use]
pub fn sole_pin(&self) -> Option<&str> {
match self {
Self::Git {
tag, rev, branch, ..
} => rev.as_deref().or(tag.as_deref()).or(branch.as_deref()),
Self::Path { .. } => None,
}
}
pub fn validate(&self, nome: &str) -> Result<(), DepError> {
match self {
Self::Git {
repo,
tag,
rev,
branch,
} => {
if repo.is_empty() {
return Err(DepError::fonte_repo_empty(nome));
}
if let Err(reason) = crate::render::is_git_repo_url(repo) {
return Err(DepError::fonte_repo_shape(nome, repo, reason));
}
let pins: [(&'static str, Option<&String>); 3] = [
(":tag", tag.as_ref()),
(":rev", rev.as_ref()),
(":branch", branch.as_ref()),
];
let set: Vec<&'static str> =
pins.iter().filter_map(|(n, v)| v.map(|_| *n)).collect();
match set.len() {
0 => {
return Err(DepError::fonte_pin_missing(nome));
}
1 => {
for (pin, value) in pins {
if value.is_some_and(String::is_empty) {
return Err(DepError::fonte_pin_empty(nome, pin));
}
}
}
_ => {
return Err(DepError::fonte_pin_ambiguous(nome, &set.join(", ")));
}
}
for (pin, value) in [(":tag", tag.as_ref()), (":branch", branch.as_ref())] {
if let Some(v) = value
&& let Err(reason) = crate::render::is_git_ref_name(v)
{
return Err(DepError::fonte_pin_shape(nome, pin, v, reason));
}
}
if let Some(v) = rev.as_ref()
&& let Err(reason) = crate::render::is_git_oid(v)
{
return Err(DepError::fonte_pin_shape(nome, ":rev", v, reason));
}
Ok(())
}
Self::Path { caminho } => Self::validate_caminho(nome, caminho),
}
}
#[allow(
clippy::too_many_lines,
reason = "the per-arm cascade is structurally flat by design — every \
`:caminho` arm carries its own typed [`DepError`] variant + \
per-arm Why comment, so collapsing the cascade onto a generic \
[`crate::render`] predicate would regress the per-arm self-locating \
diagnostic the `feira lint` consumer surface depends on"
)]
fn validate_caminho(nome: &str, caminho: &str) -> Result<(), DepError> {
if caminho.is_empty() {
return Err(DepError::fonte_caminho_empty(nome));
}
if std::path::Path::new(caminho).is_absolute() {
return Err(DepError::fonte_caminho_absolute(nome, caminho));
}
if caminho.starts_with('~') {
return Err(DepError::fonte_caminho_tilde_expansion(nome, caminho));
}
if caminho.starts_with('$') {
return Err(DepError::fonte_caminho_var_expansion(nome, caminho));
}
if caminho.starts_with(' ') {
return Err(DepError::fonte_caminho_leading_whitespace(nome, caminho));
}
if caminho.starts_with('-') {
return Err(DepError::fonte_caminho_leading_hyphen(nome, caminho));
}
for &b in caminho.as_bytes() {
if b < 0x20 || b == 0x7F {
return Err(DepError::fonte_caminho_control_char(nome, caminho, b));
}
}
for &b in caminho.as_bytes() {
if b == b'\\' {
return Err(DepError::fonte_caminho_backslash(nome, caminho));
}
}
for &b in caminho.as_bytes() {
if b == b'<' || b == b'>' {
return Err(DepError::fonte_caminho_shell_redirection(nome, caminho, b));
}
}
for &b in caminho.as_bytes() {
if b == b'|' {
return Err(DepError::fonte_caminho_shell_pipe(nome, caminho));
}
}
for &b in caminho.as_bytes() {
if b == b';' {
return Err(DepError::fonte_caminho_shell_semicolon(nome, caminho));
}
}
for &b in caminho.as_bytes() {
if b == b'&' {
return Err(DepError::fonte_caminho_shell_background(nome, caminho));
}
}
for &b in caminho.as_bytes() {
if b == b'`' {
return Err(DepError::fonte_caminho_shell_command_substitution(
nome, caminho,
));
}
}
for &b in caminho.as_bytes() {
if b == b'*' || b == b'?' {
return Err(DepError::fonte_caminho_shell_glob(nome, caminho, b));
}
}
for &b in caminho.as_bytes() {
if b == b'(' || b == b')' {
return Err(DepError::fonte_caminho_shell_subshell_grouping(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'{' || b == b'}' {
return Err(DepError::fonte_caminho_shell_brace_expansion(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'[' || b == b']' {
return Err(DepError::fonte_caminho_shell_bracket_expansion(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'\'' || b == b'"' {
return Err(DepError::fonte_caminho_shell_quote_grouping(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'#' {
return Err(DepError::fonte_caminho_shell_comment(nome, caminho, b));
}
}
for &b in caminho.as_bytes() {
if b == b'%' {
return Err(DepError::fonte_caminho_url_percent_encoding(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'$' {
return Err(DepError::fonte_caminho_shell_variable_expansion(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'!' {
return Err(DepError::fonte_caminho_shell_history_expansion(
nome, caminho, b,
));
}
}
for &b in caminho.as_bytes() {
if b == b'^' {
return Err(DepError::fonte_caminho_shell_history_substitution(
nome, caminho, b,
));
}
}
if caminho.as_bytes().last() == Some(&b'/') {
return Err(DepError::fonte_caminho_trailing_slash(nome, caminho));
}
Ok(())
}
}
impl Dep {
#[must_use]
pub const fn nome(&self) -> &str {
self.nome.as_str()
}
#[must_use]
pub const fn versao_requirement(&self) -> &str {
self.versao.as_str()
}
#[must_use]
pub const fn fonte(&self) -> Option<&DepSource> {
self.fonte.as_ref()
}
#[must_use]
pub const fn caracteristicas(&self) -> &[String] {
self.caracteristicas.as_slice()
}
#[must_use]
pub const fn opcional(&self) -> bool {
self.opcional
}
#[must_use]
pub fn simple(nome: impl Into<String>, versao: impl Into<String>) -> Self {
Self {
nome: nome.into(),
versao: versao.into(),
fonte: None,
opcional: false,
caracteristicas: Vec::new(),
}
}
#[must_use]
pub fn git(
nome: impl Into<String>,
versao: impl Into<String>,
repo: impl Into<String>,
tag: impl Into<String>,
) -> Self {
Self {
nome: nome.into(),
versao: versao.into(),
fonte: Some(DepSource::Git {
repo: repo.into(),
tag: Some(tag.into()),
rev: None,
branch: None,
}),
opcional: false,
caracteristicas: Vec::new(),
}
}
pub fn validate(&self) -> Result<(), DepError> {
if self.nome.is_empty() {
return Err(DepError::NomeEmpty);
}
if let Err(reason) = crate::render::is_dns_1123_label(&self.nome) {
return Err(DepError::nome_invalid(&self.nome, reason));
}
crate::render::require_valid_versao_requirement(
self.versao_requirement(),
|| DepError::versao_empty(&self.nome),
|reason| DepError::versao_invalid(&self.nome, self.versao_requirement(), reason),
)?;
if let Some(fonte) = self.fonte() {
fonte.validate(&self.nome)?;
}
self.validate_caracteristicas()?;
Ok(())
}
fn validate_caracteristicas(&self) -> Result<(), DepError> {
let mut seen = std::collections::HashSet::new();
for c in self.caracteristicas() {
if c.is_empty() {
return Err(DepError::caracteristica_empty(&self.nome));
}
if let Err(reason) = crate::render::is_cargo_feature_name(c) {
return Err(DepError::caracteristica_invalid(&self.nome, c, reason));
}
crate::render::insert_first_seen(&mut seen, c.as_str(), || {
DepError::caracteristica_duplicate(&self.nome, c)
})?;
}
Ok(())
}
}
pub fn validate_no_self_dep(
deps: &[Dep],
deps_dev: &[Dep],
parent_nome: &str,
) -> Result<(), DepError> {
for dep in deps {
if dep.nome() == parent_nome {
return Err(DepError::dep_is_self(
parent_nome,
crate::render::DEP_AUTHOR_KEY_DEPS,
));
}
}
for dep in deps_dev {
if dep.nome() == parent_nome {
return Err(DepError::dep_is_self(
parent_nome,
crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
));
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, gen_platform::IsVariant)]
pub enum DepList {
Prod,
Dev,
}
impl DepList {
pub const ALL: &'static [Self] = &[Self::Prod, Self::Dev];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Prod => crate::render::DEP_AUTHOR_KEY_DEPS,
Self::Dev => crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
}
}
#[must_use]
pub fn from_wire(s: &str) -> Option<Self> {
match s {
crate::render::DEP_AUTHOR_KEY_DEPS => Some(Self::Prod),
crate::render::DEP_AUTHOR_KEY_DEPS_DEV => Some(Self::Dev),
_ => None,
}
}
}
impl std::fmt::Display for DepList {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl AsRef<str> for DepList {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl TryFrom<&str> for DepList {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::from_wire(s).ok_or(())
}
}
impl From<DepList> for &'static str {
fn from(list: DepList) -> &'static str {
list.as_str()
}
}
impl From<&DepList> for &'static str {
fn from(list: &DepList) -> &'static str {
list.as_str()
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum DepError {
#[error(
":deps entry has empty :nome (every dep must name a target caixa; \
omit the entry instead of carrying an empty name)"
)]
NomeEmpty,
#[error(
":deps entry :nome {nome:?} is not a valid DNS-1123 label: {reason} \
(the value flows verbatim as the target caixa's `:nome`, the rendered \
`lareira-<nome>` Helm chart name segment, the `LABEL_PROGRAM` label \
value, and the resolver's checkout-directory leaf — each apiserver-side \
schema rejects non-DNS-1123 names at admission time; use a lowercase \
RFC 1123 label like `\"caixa-teia\"` or `\"pleme-mesh\"`, 1..=63 bytes, \
pattern `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)"
)]
NomeInvalid { nome: String, reason: String },
#[error(
":deps entry {nome:?} has empty :versao (every dep must pin a semver \
constraint that resolves through the lacre pipeline)"
)]
VersaoEmpty { nome: String },
#[error(
":deps entry {nome:?} :versao {versao:?} is not a valid semver \
requirement: {reason} (use Cargo-shaped forms like `\"^0.1\"`, \
`\"~0.1.2\"`, `\"0.1.0\"`, or `\"*\"` — the same shape `:membros :versao` \
and `:children :versao` carry; the lacre pipeline resolves all three \
through the same parser)"
)]
VersaoInvalid {
nome: String,
versao: String,
reason: String,
},
#[error(
":deps entry {nome:?} :fonte (:tipo git …) has empty :repo \
(every git source must name a repo — use a `github:org/repo` \
shorthand, an `https://…` URL, or an ssh-git URL; omit the \
entire :fonte block to fall back to the default-host resolver \
convention)"
)]
FonteRepoEmpty { nome: String },
#[error(
":deps entry {nome:?} :fonte (:tipo git …) :repo {repo:?} has \
invalid value-shape: {reason} (the value flows verbatim into the \
caixa-resolver's `git clone <repo>` subprocess invocation; every \
documented form carries a `:` separator and no whitespace / \
control / non-ASCII bytes — use a `github:org/repo` shorthand, \
an `https://host/path` / `ssh://[user@]host/path` / \
`git://host/path` / `file:///path` URL, or the `git@host:path` \
scp-style SSH form)"
)]
FonteRepoShape {
nome: String,
repo: String,
reason: String,
},
#[error(
":deps entry {nome:?} :fonte (:tipo git …) has no pin set \
(set exactly one of :tag, :rev, or :branch so the resolver \
can pick a reproducible commit; omit the entire :fonte block \
to fall back to the default-host resolver convention, which \
resolves the latest tag matching :versao)"
)]
FontePinMissing { nome: String },
#[error(
":deps entry {nome:?} :fonte (:tipo git …) has multiple pins \
set ({pins}); exactly one of :tag, :rev, or :branch must be \
set so the resolver's checkout target is unambiguous (the \
resolver's silent precedence is :rev > :tag > :branch — if \
you intended one specifically, drop the others)"
)]
FontePinAmbiguous { nome: String, pins: String },
#[error(
":deps entry {nome:?} :fonte (:tipo git …) has empty {pin} \
(a set pin must name a non-empty git ref; drop the {pin} key \
entirely to fall through to another pin axis)"
)]
FontePinEmpty { nome: String, pin: String },
#[error(
":deps entry {nome:?} :fonte (:tipo git …) {pin} {value:?} has invalid \
value-shape: {reason} (the git porcelain enforces the same shape at \
`git fetch` / `git checkout` time on every pin; use a leaf refname \
like `\"v0.1.0\"` for `:tag` or `\"main\"` / `\"feature/foo\"` for \
`:branch`, or a full 40/64 lowercase-hex commit OID for `:rev` — \
drop any `refs/heads/` or `refs/tags/` prefix the caixa-resolver \
prepends at clone time, and avoid abbreviated SHAs which are \
ambiguous across repository history)"
)]
FontePinShape {
nome: String,
pin: String,
value: String,
reason: String,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) has empty :caminho \
(every path source must name a non-empty filesystem path; \
omit the entire :fonte block to fall back to the default-host \
resolver convention)"
)]
FonteCaminhoEmpty { nome: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} is \
absolute (the lacre pipeline embeds the value verbatim in its \
per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so an absolute path makes the \
BLAKE3 closure differ across machines — defeating the \
reproducibility contract that's load-bearing for CSE; express \
the path relative to the caixa.lisp location, e.g. \
\"../caixa-teia\" for a sibling workspace dep)"
)]
FonteCaminhoAbsolute { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} starts \
with `~` (the leading-tilde is a shell-expansion convention, not a \
POSIX path component — `Path::is_absolute` returns false on it, so \
the b94fd83 absolute-path gate doesn't catch it, but the lacre \
pipeline embeds the value verbatim in its per-dep content-address \
`path:{caminho}` at caixa-resolver/src/resolve.rs:189 and the \
caixa-resolver folds it through `Path::join` without `~`-expansion, \
so the build looks for a literal `./{caminho}` subdirectory and \
fails at resolve time far from the source caixa.lisp; even worse, a \
future caixa-resolver pass that *does* expand `~` would silently \
re-open the host-layout-leak the b94fd83 absolute gate closes — \
Alice's `~` resolves to `/home/alice`, Bob's to `/home/bob`, two CI \
runners with different `$HOME` layouts resolve to two distinct paths \
for the byte-identical caixa, defeating the THEORY.md §V.2 render-\
determinism contract; express the path relative to the caixa.lisp \
location, e.g. \"../caixa-teia\" for a sibling workspace dep, or \
spell out the full relative path explicitly if a workstation-rooted \
dep is genuinely intended)"
)]
FonteCaminhoTildeExpansion { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} starts \
with `$` (the leading-`$` is a shell-variable-expansion convention, \
not a POSIX path component — `Path::is_absolute` returns false on it \
and the a5c248e tilde gate doesn't catch it, but the lacre pipeline \
embeds the value verbatim in its per-dep content-address \
`path:{caminho}` at caixa-resolver/src/resolve.rs:189 and the \
caixa-resolver folds it through `Path::join` without `$`-expansion, \
so the build looks for a literal `./{caminho}` subdirectory and \
fails at resolve time far from the source caixa.lisp; even worse, a \
future caixa-resolver pass that *does* expand `$VAR` (the canonical \
shell-convention idiom that CI's `${{WORKSPACE}}` paste-idiom \
invites) would silently re-open the host-layout-leak the b94fd83 \
absolute gate closes — Alice's `$HOME` resolves to `/home/alice`, \
Bob's to `/home/bob`, two CI runners with different `${{WORKSPACE}}` \
layouts resolve to two distinct paths for the byte-identical caixa, \
defeating the THEORY.md §V.2 render-determinism contract; express \
the path relative to the caixa.lisp location, e.g. \"../caixa-teia\" \
for a sibling workspace dep, or spell out the full relative path \
explicitly if a workstation-rooted dep is genuinely intended)"
)]
FonteCaminhoVarExpansion { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} starts \
with a space (the leading ASCII space `0x20` is the orthogonal \
paste-from-aligned-doc footgun that silently passes \
`Path::is_absolute` and every prior leading-byte arm — \
`\" ../caixa-teia\"` resolves via `Path::join` to a literal \
`./ ../caixa-teia` subdirectory the resolver fails to find at \
resolve time with a non-self-locating `No such file or directory` \
error far from the source caixa.lisp; the lacre pipeline embeds \
the value verbatim in its per-dep content-address `path:{caminho}` \
at caixa-resolver/src/resolve.rs:189, so byte-divergent / \
semantic-identical caixa values (` ../caixa-teia` vs \
`../caixa-teia`) yield two distinct BLAKE3 closures across two \
workstations whose authors differ only in paste-from-aligned- \
caixa.lisp-doc whitespace habits — the most insidious failure \
mode the typed slot can carry (no error surfaces; the divergence \
is invisible until two machines compare lacres), defeating the \
THEORY.md §V.2 render-determinism contract. The canonical \
paste-from-aligned-`:deps`-block footgun (every `:fonte` form in \
a multi-entry `:deps` block sits at the same column — an author \
selecting `\"<sp><sp><sp>../caixa-teia\"` and pasting it from \
the rendered alignment into a fresh entry preserves the leading \
whitespace verbatim); peer `:fonte :repo` axis already rejects \
leading whitespace via `is_git_repo_url`, `:fonte :tag` / \
`:fonte :branch` via `is_git_ref_name`, `:descricao` via \
`is_chart_description_shape`, `:licenca` via \
`is_spdx_expression_shape`. Drop the leading space; express the \
path as a bare relative single-token like \"../caixa-teia\")"
)]
FonteCaminhoLeadingWhitespace { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} starts \
with `-` (the canonical CLI-argument-injection footgun on the \
`:caminho` axis — the lacre pipeline embeds the value verbatim in \
its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189 and the caixa-resolver folds it \
through `Path::join` looking for a literal `./{caminho}` \
subdirectory. Every downstream subprocess that consumes the resolved \
path — `git -C {caminho} <verb>`, `terraform -chdir={caminho}`, \
`nix build --path {caminho}`, `find {caminho}`, `stat {caminho}`, \
`cp -r {caminho} …`, `rm -rf {caminho}` — reinterprets a leading-`-` \
value as a CLI flag rather than a positional path when the invocation \
does not carry a `--` argument-list terminator between the flag block \
and the path (the common case at every porcelain entry point). The \
canonical footguns: `:caminho \"-rf\"` (bare short-flag paste), \
`:caminho \"-C\"` (`git -C -C` config-injection paste), \
`:caminho \"--upload-pack=cat /etc/passwd\"` (the canonical long-flag \
CLI-arg-injection vector at every git porcelain entry point that \
consumes a path or URL argument, peer with is_git_repo_url's \
leading-`-` arm on the sibling `:fonte :repo` axis), \
`:caminho \"--config=…\"` (`git -c foo=bar` config-override paste). \
POSIX `std::path::Path` treats a leading `-` as a literal filename \
byte so the resolver folds `\"-rf\"` through `Path::join` and looks \
for a literal `./-rf` subdirectory that fails at resolve time with a \
non-self-locating `No such file or directory` error far from the \
source caixa.lisp — but on any downstream shell-out without `--` the \
reinterpretation is silent and the failure mode is arbitrary-\
argument-injection. Peer arms: `is_git_repo_url` (render.rs:2037) \
rejects leading `-` on `:fonte :repo` for `git clone <repo>` CLI-\
arg-injection; `is_git_ref_name` (render.rs:1381, 5a28454) rejects \
leading `-` on `:fonte :tag` / `:branch` for `git checkout <ref>` \
CLI-arg-injection; `is_dns_1123_label` rejects leading `-` on every \
DNS-1123-shaped axis (top-level Caixa `:nome`, `:membros :caixa`, \
`:children :caixa`, `:deps :nome`, cluster names); \
`is_cargo_feature_name` rejects leading `-` on `:caracteristicas`; \
the feira `init` / `add <nome>` positional gate (868c191) rejects \
leading `-` on the CLI positional itself. Express the path as a bare \
relative single-token like \"../caixa-teia\" — the sibling-workspace \
directory name carries no leading-hyphen semantic, and `./` / `../` \
prefixes structurally partition the leading-byte set to safe values.)"
)]
FonteCaminhoLeadingHyphen { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains \
ASCII control byte 0x{byte:02x} (POSIX paths reject NUL `0x00` outright — \
every `std::fs` syscall routes the path through `CString::new` which \
fails with `NulError` at resolve time; the lacre pipeline embeds the \
value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189 so a control byte anywhere in the \
value lands in the BLAKE3 closure and breaks the THEORY.md §V.2 render-\
determinism contract — the canonical paste-from-multiline-doc \
(`\\n`/`\\r`), paste-from-aligned-table (`\\t`), or paste-from-binary-\
blob (`0x00`-DEL) footgun every peer single-token-shaped axis \
(`:fonte :repo`, `:fonte :tag`/`:branch`, the Helm chart-string axes) \
already gates against. Express the path as a relative single-line ASCII \
string, e.g. \"../caixa-teia\" for a sibling workspace dep)"
)]
FonteCaminhoControlChar {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains `\\` \
(POSIX `std::path::Path` treats `\\` as a literal byte inside a single path \
component, so `..\\caixa-teia` is one directory named literally `..\\caixa-teia` — \
not the parent's sibling — and the caixa-resolver folds the value through \
`Path::join` looking for a literal `./{caminho}` subdirectory that fails at \
resolve time with a non-self-locating `No such file or directory` error far \
from the source caixa.lisp; Windows `std::path::Path` treats `\\` as a \
primary path separator equal to `/`, so byte-identical caixa.lisp values \
resolve to two distinct directories across runner OSes — the lacre pipeline \
embeds the value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, defeating the THEORY.md §V.2 render-\
determinism contract via the cross-host-OS-separator divergence vector. The \
canonical Windows-Explorer `Copy as path` / PowerShell `Get-Location` \
paste-idiom footgun; peer `:fonte :tag` / `:fonte :branch` axis already \
rejects `\\` via `is_git_ref_name` for the same Windows-path-leak reason, \
and `:entrada :paths` rejects `\\` via `is_gateway_api_http_path`'s eleven-\
byte RFC-3986-reserved set. Express the path with `/` as the separator, e.g. \
\"../caixa-teia\" for a sibling workspace dep)"
)]
FonteCaminhoBackslash { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
redirection metacharacter 0x{byte:02x} `{ch}` (every interactive shell — bash / \
zsh / fish / nushell — lexes `<` and `>` as input / output redirection \
operators, so `:caminho \"../caixa-teia>build.log\"` is the canonical \
paste-from-shell-pipeline footgun where an author copies a `command > log` \
tail without trimming the redirect; POSIX `std::path::Path` treats both bytes \
as literal path-component bytes, so the resolver folds the value through \
`Path::new(caminho).join(<file>)` looking for a literal `./{caminho}` \
subdirectory and fails at resolve time with a non-self-locating `No such \
file or directory` error far from the source caixa.lisp. The lacre pipeline \
embeds the value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so the byte lands in the BLAKE3 closure \
and rides into every shell-spawned subprocess (the resolver's `git clone`, a \
future `feira tofu` shell-out, a future operator-side `nix` spawn) as the \
canonical CRLF-at-subprocess-argument / shell-metachar injection surface every \
peer single-token-shaped typed slot already closes. The peer `:fonte :tag` / \
`:fonte :branch` axis already rejects `<` / `>` via `is_git_ref_name`, and \
`:entrada :paths` rejects them via `is_gateway_api_http_path`'s eleven-byte \
RFC-3986-reserved set. Express the path as a bare relative single-token like \
\"../caixa-teia\" — the sibling-workspace directory name carries no shell-\
redirection semantic.",
ch = *byte as char
)]
FonteCaminhoShellRedirection {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-pipe \
metacharacter `|` (every interactive shell — bash / zsh / fish / nushell — lexes \
`|` as the pipe operator that wires one command's stdout to the next command's \
stdin, so `:caminho \"../caixa-teia | grep foo\"` is the canonical paste-from-\
shell-history footgun where an author copies a `ls ../caixa-teia | grep` line \
without trimming the pipeline tail, and `:caminho \"../foo||bar\"` is the \
symmetric `cmd-a || cmd-b` short-circuit-OR paste shape; POSIX `std::path::Path` \
treats `|` as a literal path-component byte, so the resolver folds the value \
through `Path::new(caminho).join(<file>)` looking for a literal `./{caminho}` \
subdirectory and fails at resolve time with a non-self-locating `No such file or \
directory` error far from the source caixa.lisp. The lacre pipeline embeds the \
value verbatim in its per-dep content-address `path:{caminho}` at caixa-resolver/\
src/resolve.rs:189, so the byte lands in the BLAKE3 closure and rides into every \
shell-spawned subprocess (the resolver's `git clone`, a future `feira tofu` \
shell-out, a future operator-side `nix` spawn) as the canonical CRLF-at-\
subprocess-argument / shell-metachar injection surface every peer single-token-\
shaped typed slot already closes. The peer `:entrada :paths` axis rejects `|` \
via `is_gateway_api_http_path`'s eleven-byte RFC-3986-reserved set. Express the \
path as a bare relative single-token like \"../caixa-teia\" — the sibling-\
workspace directory name carries no shell-pipe semantic."
)]
FonteCaminhoShellPipe { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
command-separator metacharacter `;` (every interactive shell — bash / zsh / fish \
/ nushell — lexes `;` as the sequential-command terminator that fires the next \
command regardless of the prior command's exit status, so `:caminho \
\"../caixa-teia; rm -rf build\"` is the canonical paste-from-shell-one-liner \
footgun where an author copies a `cd path; do-thing` chain without trimming \
the cleanup tail, and `:caminho \"../foo;;bar\"` is the symmetric POSIX `case` \
arm `;;` terminator paste shape; POSIX `std::path::Path` treats `;` as a \
literal path-component byte, so the resolver folds the value through \
`Path::new(caminho).join(<file>)` looking for a literal `./{caminho}` \
subdirectory and fails at resolve time with a non-self-locating `No such file \
or directory` error far from the source caixa.lisp. The lacre pipeline embeds \
the value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so the byte lands in the BLAKE3 closure and \
rides into every shell-spawned subprocess (the resolver's `git clone`, a \
future `feira tofu` shell-out, a future operator-side `nix` spawn) as the \
canonical shell-metachar injection surface every peer single-token-shaped \
typed slot already closes. The peer `:entrada :paths` axis rejects `;` via \
`is_gateway_api_http_path`'s eleven-byte RFC-3986-reserved set. Express the \
path as a bare relative single-token like \"../caixa-teia\" — the sibling-\
workspace directory name carries no shell-command-separator semantic."
)]
FonteCaminhoShellSemicolon { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
background / list-AND metacharacter `&` (every interactive shell — bash / zsh \
/ fish / nushell — lexes `&` two ways: single `&` as the background-task \
terminator detaching the prior command and returning control immediately to \
the prompt, double `&&` as the logical-AND list operator firing the next \
command only if the prior succeeded; POSIX `std::path::Path` treats it as a \
literal byte. The canonical paste-from-shell-prompt footgun is a `cd path & \
sleep 1` background-launch one-liner or a `cd path && make install` build-\
chain idiom selected whole into the `:caminho` slot — the prior `;` arm at \
05c358e closed the sequential-command-separator vector, this arm closes the \
orthogonal background-task / logical-AND vector on the same paste-from-shell-\
prompt class. The lacre pipeline embeds the value verbatim in its per-dep \
content-address `path:{caminho}` at caixa-resolver/src/resolve.rs:189, so the \
byte lands in the BLAKE3 closure and rides into every shell-spawned \
subprocess (the resolver's `git clone`, a future `feira tofu` shell-out, a \
future operator-side `nix` spawn) as the canonical shell-metachar injection \
surface every peer single-token-shaped typed slot already closes. The peer \
`:entrada :paths` axis rejects `&` via `is_gateway_api_http_path`'s eleven-\
byte RFC-3986-reserved set. Express the path as a bare relative single-token \
like \"../caixa-teia\" — the sibling-workspace directory name carries no \
shell-background / logical-AND semantic."
)]
FonteCaminhoShellBackground { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
command-substitution metacharacter `` ` `` (every POSIX shell — sh / bash / zsh / \
dash / ksh / fish / nushell — lexes the byte as the legacy command-substitution \
wrapper that runs the enclosed command and substitutes its standard-output \
verbatim into the surrounding word, so a backticked `whoami` expands to the \
current user's name and a backticked `cat /etc/passwd` expands to the file's \
contents — the canonical CWE-78 shell-command-injection vector; POSIX \
`std::path::Path` treats the byte as a literal path-component byte. The canonical \
paste-from-shell-prompt footgun is a `cd ../path/<backtick>whoami<backtick>` legacy substitution \
one-liner or a `cd <backtick>pwd<backtick>/path` working-directory expansion idiom selected whole \
into the `:caminho` slot — the prior `&` arm at e12e4f3 closed the shell-\
background / logical-AND vector, this arm closes the orthogonal command-\
substitution vector on the same paste-from-shell-prompt class (the modern `$()` \
form is gated at leading position by the f4efe9c `FonteCaminhoVarExpansion` arm; \
the legacy backtick form is the orthogonal axis). The lacre pipeline embeds the \
value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so the byte lands in the BLAKE3 closure and \
rides into every shell-spawned subprocess (the resolver's `git clone`, a future \
`feira tofu` shell-out, a future operator-side `nix` spawn) as the canonical \
shell-metachar injection surface every peer single-token-shaped typed slot \
already closes. The peer `:entrada :paths` axis rejects the byte via \
`is_gateway_api_http_path`'s eleven-byte RFC-3986-reserved set. Express the path \
as a bare relative single-token like \"../caixa-teia\" — the sibling-workspace \
directory name carries no shell-command-substitution semantic."
)]
FonteCaminhoShellCommandSubstitution { nome: String, caminho: String },
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
glob / pathname-expansion metacharacter 0x{byte:02x} `{ch}` (every POSIX shell — \
sh / bash / zsh / dash / ksh / fish / nushell — lexes `*` and `?` as pathname-\
expansion wildcards: `*` matches any sequence of characters in a path component \
and `?` matches exactly one character, so a `:caminho \"../caixa-teia/*\"` is the \
canonical paste-from-shell-listing footgun where an author copies a \
`ls ../caixa-teia/*` listing without trimming the wildcard, and `:caminho \
\"../foo?\"` is the symmetric single-char-wildcard paste shape; POSIX \
`std::path::Path` treats both bytes as literal path-component bytes, so the \
resolver folds the value through `Path::new(caminho).join(<file>)` looking for \
a literal `./{caminho}` subdirectory and fails at resolve time with a non-self-\
locating `No such file or directory` error far from the source caixa.lisp. The \
lacre pipeline embeds the value verbatim in its per-dep content-address \
`path:{caminho}` at caixa-resolver/src/resolve.rs:189, so the byte lands in the \
BLAKE3 closure and rides into every shell-spawned subprocess (the resolver's \
`git clone`, a future `feira tofu` shell-out, a future operator-side `nix` \
spawn) as the canonical shell-metachar / glob-expansion surface every peer \
single-token-shaped typed slot already closes. The peer `:entrada :paths` axis \
rejects `*` and `?` via `is_gateway_api_http_path`'s eleven-byte RFC-3986-\
reserved set. Express the path as a bare relative single-token like \
\"../caixa-teia\" — the sibling-workspace directory name carries no shell-glob \
/ pathname-expansion semantic.",
ch = *byte as char
)]
FonteCaminhoShellGlob {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
subshell-grouping metacharacter 0x{byte:02x} `{ch}` (every POSIX shell — sh / bash / \
zsh / dash / ksh / fish / nushell — lexes `(` and `)` as the subshell-grouping \
operator: `(<cmd>)` runs `<cmd>` in a child shell with a fresh environment scope \
(the canonical `(cd <path> && <cmd>)` shell-history one-liner scopes a `cd` to one \
subshell without disturbing the parent's working directory), and `$(<cmd>)` is the \
modern Bourne command-substitution shape the leading-`$` `FonteCaminhoVarExpansion` \
arm closes the leading byte of — together the two arms now structurally exclude the \
entire `$(<cmd>)` substitution surface from the typed `:caminho` accepted set. \
POSIX `std::path::Path` treats the byte as a literal path-component byte, so a \
`:caminho \"../caixa-teia/$(date)/build\"` (the canonical paste-from-shell-history \
modern-command-substitution footgun) or `:caminho \"../(cd foo && pwd)/caixa-teia\"` \
(the symmetric subshell-grouping working-directory-probe paste idiom) silently passes \
every prior arm and the resolver folds the value through `Path::new(caminho).join(\
<file>)` looking for a literal subdirectory and fails at resolve time with a non-\
self-locating `No such file or directory` error far from the source caixa.lisp. The \
lacre pipeline embeds the value verbatim in its per-dep content-address `path:\
{caminho}` at caixa-resolver/src/resolve.rs:189, so the byte lands in the BLAKE3 \
closure and rides into every shell-spawned subprocess (the resolver's `git clone`, a \
future `feira tofu` shell-out, a future operator-side `nix` spawn) as the canonical \
shell-metachar / subshell-grouping surface every peer single-token-shaped typed slot \
already closes. The peer `:fonte :repo` axis (3b99147) closes the same byte under the \
same shell-subshell-grouping / RFC-3986-sub-delims banner on `is_git_repo_url`, \
together with the leading-`$` `FonteCaminhoVarExpansion` arm closing the leading byte \
of every `$(<cmd>)` shape. Express the path as a bare relative single-token \
like \"../caixa-teia\" — the sibling-workspace directory name carries no shell-\
subshell-grouping semantic.",
ch = *byte as char
)]
FonteCaminhoShellSubshellGrouping {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
brace-expansion / URI-Template placeholder metacharacter 0x{byte:02x} `{ch}` \
(every POSIX-derived brace-expanding shell — bash / zsh / ksh / fish — lexes `{{` / \
`}}` as the brace-expansion operator: `{{a,b,c}}` expands to the cross-product of \
comma-separated members and `{{1..10}}` expands to the integer range — the \
canonical `mkdir -p ../{{caixa-teia,caixa-helm,caixa-flux}}` / `cp file{{,.bak}}` \
idiom every shell-history block carries; RFC 6570 reserves the matched pair for \
URI Template placeholders (the canonical `https://{{host}}/{{org}}/{{repo}}` \
substitution shape every OpenAPI / Swagger / Postman / GitHub Octokit client \
library / Helm chart-URL fragment carries) and the Mustache / Handlebars / \
Tera / Jinja2 / Go html/template doubled-brace substitution form every IaC \
templating engine (Helm, Kustomize, Terraform's `${{var}}` cousin) emits. POSIX \
`std::path::Path` treats the byte as a literal path-component byte, so a \
`:caminho \"../{{caixa-teia,caixa-helm}}/build\"` (the canonical paste-from-\
shell-history brace-expansion fan-across-siblings footgun) or `:caminho \"../{{{{org}}}}/\
caixa-teia\"` (the symmetric paste-from-templated-doc URI-Template placeholder \
idiom every README quick-start / OpenAPI spec / Helm chart `home:` field carries) \
silently passes every prior arm and the resolver folds the value through \
`Path::new(caminho).join(<file>)` looking for a literal subdirectory and fails at \
resolve time with a non-self-locating `No such file or directory` error far from \
the source caixa.lisp. The lacre pipeline embeds the value verbatim in its \
per-dep content-address `path:{caminho}` at caixa-resolver/src/resolve.rs:189, \
so the byte lands in the BLAKE3 closure and rides into every shell-spawned \
subprocess (the resolver's `git clone`, a future `feira tofu` shell-out, a \
future operator-side `nix` spawn) as the canonical shell-metachar / brace-\
expansion / URI-Template-placeholder surface every peer single-token-shaped \
typed slot already closes. The peer `:fonte :repo` axis (42d8f9d) closes the \
same byte pair on `is_git_repo_url` under the same RFC-3986-'delims' / \
RFC-6570-URI-Template / shell-brace-expansion banner. Express the path as a \
bare relative single-token like \"../caixa-teia\" — the sibling-workspace \
directory name carries no shell-brace-expansion / URI-Template-placeholder \
semantic; if two siblings actually need pinning, author two separate `:deps` \
entries rather than one brace-expanded `:caminho` value.",
ch = *byte as char
)]
FonteCaminhoShellBraceExpansion {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
bracket-expansion / POSIX glob-character-class / shell-`test`-builtin metacharacter \
0x{byte:02x} `{ch}` (every POSIX shell — sh / bash / zsh / dash / ksh / fish / nushell \
— lexes the bracket pair as the glob character-class operator: `[abc]` matches one of \
`a` / `b` / `c`, `[a-z]` matches any lowercase ASCII letter, `[^x]` negates — the \
canonical `ls *.[ch]` C-source-file glob and `cd ../caixa-[a-z]*` lowercase-sibling \
glob every shell-history block carries; the bracket pair additionally carries the \
POSIX `test` / `[` builtin command (`[ -d ../caixa-teia ] && cd ...` every shell-\
script conditional uses) and bash's `[[ ... ]]` extended-test grammar; beyond shell \
the pair is the TOML inline-array delimiter (`features = [\"a\", \"b\"]` — the \
canonical paste-from-Cargo-manifest cross-idiom-leak vector), the YAML flow-sequence \
delimiter (`paths: [/a, /b]` — the canonical paste-from-values.yaml cross-idiom \
leak), the JSON array delimiter, and the POSIX-ERE / PCRE bracket-expression anchor. \
POSIX `std::path::Path` treats the byte as a literal path-component byte, so a \
`:caminho \"../caixa-[a-z]/build\"` (the canonical paste-from-shell-history glob-\
character-class fan-across-siblings footgun) or `:caminho \"../[caixa-teia]/build\"` \
(the symmetric paste-from-TOML-array / paste-from-YAML-flow-sequence cross-idiom \
leak) silently passes every prior arm and the resolver folds the value through \
`Path::new(caminho).join(<file>)` looking for a literal subdirectory and fails at \
resolve time with a non-self-locating `No such file or directory` error far from \
the source caixa.lisp. The lacre pipeline embeds the value verbatim in its per-dep \
content-address `path:{caminho}` at caixa-resolver/src/resolve.rs:189, so the byte \
lands in the BLAKE3 closure and rides into every shell-spawned subprocess (the \
resolver's `git clone`, a future `feira tofu` shell-out, a future operator-side \
`nix` spawn) as the canonical shell-metachar / glob-character-class / TOML-array \
surface every peer single-token-shaped typed slot already closes. Express the path \
as a bare relative single-token like \"../caixa-teia\" — the sibling-workspace \
directory name carries no shell-bracket-expansion / glob-character-class / array-\
literal semantic; if a family of sibling caixas actually needs pinning, author \
separate `:deps` entries rather than one character-class-expanded `:caminho` value.",
ch = *byte as char
)]
FonteCaminhoShellBracketExpansion {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
quote-grouping / cross-config-DSL string-literal-delimiter metacharacter \
0x{byte:02x} `{ch}` (every POSIX shell — sh / bash / zsh / dash / ksh / fish / \
nushell — lexes `'` as the strong string-literal delimiter (`'…'` — no expansion) \
and `\"` as the weak string-literal delimiter (`\"…\"` — variable- / command-\
substitution-preserving); the canonical `cd '../caixa-teia'` shell-history idiom \
every path-with-embedded-whitespace paste block carries and the symmetric \
`git clone \"$REPO\"` weak-quoted CI-manifest shape both leak the pair verbatim. \
Beyond shell the pair is the JSON string-literal delimiter (`\"key\": \"value\"` — \
the canonical paste-from-JSON-config cross-idiom-leak vector), the YAML double- \
and single-quoted flow-scalar delimiter (`path: \"../caixa-teia\"` — the canonical \
paste-from-values.yaml / paste-from-K8s-YAML-manifest cross-idiom leak), the TOML \
basic and literal string delimiter (`path = \"../caixa-teia\"` — the canonical \
paste-from-Cargo-manifest cross-idiom leak), the tatara-lisp string-literal \
delimiter itself (`(:caminho \"../caixa-teia\")` — the canonical \"I copied the \
entire `:caminho \"...\"` slot rather than just the string body\" author-surface \
footgun), and RFC 3986 §2.2's `gen-delims` / `sub-delims` grammar which excludes \
both bytes from the `pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"` \
production. POSIX `std::path::Path` treats the byte as a literal path-component \
byte, so a `:caminho \"'../caixa-teia'\"` (the canonical paste-from-shell-history \
strong-quoted sibling-workspace path footgun) or `:caminho \"\\\"../caixa-teia\\\"\"` \
(the symmetric weak-quoted paste-from-JSON / paste-from-YAML flow-scalar / paste-\
from-TOML basic-string / paste-from-tatara-lisp string-literal cross-idiom-leak \
shape) silently passes every prior arm and the resolver folds the value through \
`Path::new(caminho).join(<file>)` looking for a literal subdirectory and fails at \
resolve time with a non-self-locating `No such file or directory` error far from \
the source caixa.lisp. The lacre pipeline embeds the value verbatim in its per-dep \
content-address `path:{caminho}` at caixa-resolver/src/resolve.rs:189, so the byte \
lands in the BLAKE3 closure and rides into every shell-spawned subprocess (the \
resolver's `git clone`, a future `feira tofu` shell-out, a future operator-side \
`nix` spawn) as the canonical shell-metachar / string-literal-delimiter surface \
every peer single-token-shaped typed slot already closes. The peer `:fonte :repo` \
axis closes both bytes under the same shell-quote-grouping / RFC-3986-sub-delims \
banner (e7a109f `'` shell-single-quote + 4267d8b `\"` shell-double-quote on \
`is_git_repo_url`). Express the path as a bare relative single-token like \
\"../caixa-teia\" — the sibling-workspace directory name carries no shell-quote-\
grouping / string-literal-delimiter semantic; strip the outer quote pair from the \
paste (the tatara-lisp `:caminho \"...\"` slot already carries the string-literal \
quoting on the outer syntactic layer, so an inner quote pair would nest and \
desugar to a broken layer).",
ch = *byte as char
)]
FonteCaminhoShellQuoteGrouping {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
comment / URL-fragment-identifier / YAML-comment cross-config-DSL metacharacter \
0x{byte:02x} `{ch}` (every POSIX shell — sh / bash / zsh / dash / ksh / fish / \
nushell — lexes an unquoted `#` at the head of a word or after unquoted \
whitespace as the comment-lead per POSIX.1-2017 §2.3 Token Recognition step 6, \
discarding the byte and everything after it to the end of the physical line \
before command parsing (`cd ../caixa-teia # legacy sibling` — the canonical \
paste-from-shell-history-with-trailing-annotation shape every operator-notebook \
and CI-manifest carries); YAML 1.2 §6.6 makes `#` the comment-lead at any \
position preceded by whitespace or at line-start (`path: ../caixa-teia # pin` \
— the canonical paste-from-values.yaml / paste-from-K8s-manifest cross-idiom-\
leak); RFC 3986 §3.5 reserves `#` as the URL fragment-identifier delimiter (the \
canonical paste-from-browser-address-bar `github.com/foo/bar#readme` / \
`github.com/foo/bar#L42` permalink shape, and the symmetric Nix-flake-ref \
cross-idiom leak `github:foo/bar#packageName` where `#` selects a flake \
output); the same cross-config-DSL surface extends to HCL / Terraform / Nix \
flake attributes / dotenv `.env` / gitconfig / .gitignore / ini / TOML where \
`#` is likewise the comment-lead. POSIX `std::path::Path` treats the byte as a \
literal path-component byte, so a `:caminho \"../caixa-teia # legacy sibling\"` \
(the canonical paste-from-shell-history-with-trailing-annotation footgun), \
`:caminho \"../caixa-teia # pin\"` (the symmetric YAML flow-scalar paste-with-\
trailing-comment shape), or `:caminho \"../caixa-teia#readme\"` (the URL \
fragment paste-from-browser-address-bar shape) silently passes every prior arm \
and the resolver folds the value through `Path::new(caminho).join(<file>)` \
looking for a literal subdirectory named `../caixa-teia # legacy sibling` and \
fails at resolve time with a non-self-locating `No such file or directory` \
error far from the source caixa.lisp — while every downstream shell / YAML / \
URL parser silently truncates the value at the `#` byte to `../caixa-teia`, so \
a `feira tofu` shell-out and a `nix flake check` on an emitted YAML `path:` \
scalar disagree with the resolver on which directory the value names. The \
lacre pipeline embeds the value verbatim in its per-dep content-address \
`path:{caminho}` at caixa-resolver/src/resolve.rs:189, so the byte lands in \
the BLAKE3 closure and rides into every shell-spawned subprocess (the \
resolver's `git clone`, a future `feira tofu` shell-out, a future operator-\
side `nix` spawn) as the canonical shell-metachar / comment-lead / URL-\
fragment-delimiter surface every peer single-token-shaped typed slot already \
closes. The peer `:fonte :repo` axis closes the byte under the same URL-\
fragment-identifier banner (a68f818 `#` on `is_git_repo_url`). Express the \
path as a bare relative single-token like \"../caixa-teia\" — the sibling-\
workspace directory name carries no shell-comment / URL-fragment / YAML-\
comment semantic; move any trailing annotation to a tatara-lisp `;`-comment \
on the surrounding form (`;; legacy sibling` above the `(:caminho ...)` slot) \
and drop any `#fragment` tail entirely (fragment identifiers select \
renderings, not directories, and `:caminho` names a directory).",
ch = *byte as char
)]
FonteCaminhoShellComment {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains URL-\
percent-encoding-escape / printf-format-specifier / bash-job-control-specifier \
/ YAML-directive-lead metacharacter 0x{byte:02x} `{ch}` (RFC 3986 §2.1 reserves \
`%` as the URL percent-encoding-escape — `%HH` is the mandatory encoding \
mechanism for every byte outside the `unreserved` alphanumeric / `-` / `.` / \
`_` / `~` set, and `%` itself must be percent-encoded as `%25` to appear \
literally inside a URL value. The canonical paste-from-browser-address-bar \
percent-encoded-space footgun (an author copies `../caixa%20teia` out of a URL-\
encoded README hyperlink / browser address bar / percent-encoded permalink \
expecting `%20` to decode to a literal space at the filesystem layer) locks two \
distinct BLAKE3 closures (`path:../caixa%20teia` vs `path:../caixa teia`) for \
what the author intended as the byte-identical sibling-workspace dep. POSIX \
`std::path::Path` treats the byte as a literal path-component byte, so \
`Path::join` looks for a literal `./{caminho}` subdirectory and fails at \
resolve time with a non-self-locating `No such file or directory` error far \
from the source caixa.lisp — while every downstream URL parser / shell printf \
builtin / YAML directive parser silently reinterprets the byte to a different \
value than the resolver's `Path::join` sees. Beyond the URL-encoding hazard, \
`%` is the C / POSIX printf format-directive lead-in (`%s`, `%d`, `%02x` — \
wired into every POSIX shell's `printf` builtin, the canonical CWE-134 format-\
string-injection vector); the bash / zsh / ksh job-control-specifier lead-in \
(`%1` names \"job 1\", `%%` names \"the current job\", `%foo` names \"the most \
recent job whose command started with `foo`\" — a future `kill %1` invocation \
silently redirects the signal to a wrong target); the YAML 1.2 §6.8.1 \
directive lead-in (`%YAML 1.2` / `%TAG` — the paste-from-top-of-doc YAML \
directive block cross-idiom leak); and the Windows-shell env-var-reference \
lead-in (`%PATH%` — the paste-from-`.bat` / paste-from-PowerShell-`%env:PATH%` \
cross-idiom leak). The lacre pipeline embeds the value verbatim in its per-dep \
content-address `path:{caminho}` at caixa-resolver/src/resolve.rs:189, so the \
byte lands in the BLAKE3 closure and rides into every shell-spawned subprocess \
(the resolver's `git clone`, a future `feira tofu` shell-out, a future \
operator-side `nix` spawn) as the canonical URL-percent-encoding-escape / \
printf-format-specifier / job-control-specifier surface every peer single-\
token-shaped typed slot already closes. The peer `:fonte :repo` axis closes \
the byte under the same URL-percent-encoding-escape banner (a323db8 `%` on \
`is_git_repo_url`). Express the path as a bare relative single-token like \
\"../caixa-teia\" — the sibling-workspace directory name carries no URL-\
percent-encoding-escape / format-specifier / job-control semantic; substitute \
any `%20` percent-encoded-space with a literal space then reject the whole \
value at the leading-whitespace / embedded-`?` arm on the same axis (a caixa \
directory name never carries an embedded space in practice); drop any \
`%2F`-encoded path separator in favor of a literal `/`; and drop any leading \
`%YAML` / `%PATH%` cross-idiom-leak prefix entirely.",
ch = *byte as char
)]
FonteCaminhoUrlPercentEncoding {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
variable-expansion / command-substitution / arithmetic-expansion / cross-config-\
DSL-interpolation metacharacter 0x{byte:02x} `{ch}` (every POSIX shell — sh / \
bash / zsh / dash / ksh / busybox ash / fish / nushell — lexes `$` per \
POSIX.1-2017 §2.6 as the variable-expansion `$<name>` / braced-form `${{<name>}}` \
/ command-substitution `$(<cmd>)` / arithmetic-expansion `$((<expr>))` operator; \
Nix uses `${{var}}` as the string-interpolation lead, Make uses `$(var)` / `$@` \
/ `$<` for variables and automatic-variables, JavaScript / TypeScript template \
literals use `${{expr}}` for interpolation, envsubst / Kubernetes / OpenShift \
templates use `${{VAR}}` for env-var-reference, PHP uses `$_GET` / `$_ENV` for \
superglobals, Perl uses `$foo` for scalars, SASS / SCSS uses `$primary-color` \
for variables, and PostgreSQL / SQLite use `$1` / `$2` for bind parameters — \
the byte is a first-class parser byte in nearly every config / templating / \
build-system DSL the substrate's paste-idiom surface routinely crosses. POSIX \
`std::path::Path` treats the byte as a literal path-component byte, so the \
canonical paste-from-shell-one-liner `:caminho \"../foo$HOME/bar\"` / paste-\
from-CI-manifest `:caminho \"../foo${{WORKSPACE}}/bar\"` / paste-from-shell-\
prompt `:caminho \"../foo$(whoami)/bar\"` footguns silently pass every prior \
cascade arm (`$` isn't `\\` / `<` / `>` / `|` / `;` / `&` / backtick / `*` / \
`?` / `(` / `)` / `{{` / `}}` / `[` / `]` / `'` / `\"` / `#` / `%`) and route \
through `Path::new(caminho).join(<file>)` looking for a literal `./{caminho}` \
subdirectory that fails at resolve time with a non-self-locating `No such file \
or directory` error far from the source caixa.lisp. The lacre pipeline embeds \
the value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so byte-identical caixa.lisp values differing \
only in whether the author substituted `$HOME` / `${{WORKSPACE}}` at author \
time lock to two distinct BLAKE3 closures across two workstations whose \
downstream envsubst / Nix / Make / K8s-template layers differ in `$VAR` \
recognition — defeating the THEORY.md §V.2 render-determinism contract on the \
same axis every prior `:caminho` arm protects. Beyond the determinism vector, \
`$` at any position in a value flowing verbatim into a shell-spawned subprocess \
is the canonical CWE-78 shell-command-injection surface every peer single-\
token-shaped typed slot already closes: peer `:fonte :repo` axis rejects `$` \
under the shell-variable-expansion / URL-sub-delim banner via `is_git_repo_url` \
(b9d187c), peer `:fonte :tag` / `:fonte :branch` axes reject `$` as part of \
`is_git_ref_name`'s printable-ASCII-restricted grammar (`git check-ref-format` \
rejects the byte outright), and peer `:entrada :paths` axis rejects `$` via \
`is_gateway_api_http_path`'s eleven-byte RFC-3986-reserved set. The leading-`$` \
position on the same axis routes through `FonteCaminhoVarExpansion` at the \
f4efe9c leading-byte arm; this arm closes the last positional gap on the byte \
so every position — leading and embedded — is structurally rejected. Substitute \
the `$VAR` / `${{VAR}}` / `$(cmd)` template with the literal value at author \
time, or express the path as a bare relative single-token like \
\"../caixa-teia\" — the sibling-workspace directory name carries no shell-\
variable-expansion / command-substitution / arithmetic-expansion semantic.",
ch = *byte as char
)]
FonteCaminhoShellVariableExpansion {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
history-expansion / RFC-3986-sub-delims / bang-operator metacharacter 0x{byte:02x} \
`{ch}` (every interactive POSIX shell with history enabled — bash / ksh / zsh's \
`bashcompat` / csh / tcsh — lexes `!` as the history-expansion prefix per bash \
reference §9.3: `!command` re-runs the most recent history entry beginning with \
`command`, `!!` re-runs the prior command verbatim, `!$` substitutes the last \
word of the prior command, `!:N` substitutes the Nth word of the prior command, \
and the substitution fires at every history-expansion-enabled shell context — \
`set -o histexpand` is bash's default for interactive sessions and the layer \
every `feira tofu` / `git clone` / `nix flake check` subprocess-argument \
invocation crosses when spawned under `bash -i`. Beyond shell history, RFC 3986 \
§2.2 lists `!` in the `sub-delims` set (the URL grammar admits the byte inside \
a path segment, but every WHATWG-conformant special-scheme URL parser percent-\
encodes it inside a query component via the 'special-query percent-encode set' \
the peer `is_git_repo_url` `*` / `(` / `)` / `'` arms close on); the byte is \
also the C / C++ / Rust / JavaScript / Python bang-operator (logical-negation \
prefix — the paste-from-source-code idiom where an author copies \
`!path.exists()` out of a Rust snippet and the trailing punctuation crosses \
the string-literal boundary); the canonical English-typography emphasis / \
exclamation mark (the paste-from-prose enthusiasm-form idiom where an author \
writes `:caminho \"../caixa-teia!\"` expecting the substrate to coerce it to a \
kebab-case slug); and the Nix flake-ref attribute-selection operator surface. \
POSIX `std::path::Path` treats `!` as a literal path-component byte, so the \
canonical paste-from-shell-history footgun `:caminho \"../caixa-teia!sudo\"` \
(an author copies a `cd ../caixa-teia && !sudo make install` one-liner from a \
quick-start README and the trailing `!sudo` rides in verbatim as a history-\
expansion reference), the symmetric `:caminho \"../foo!!/bar\"` (the `!!` \
repeat-prior-command paste idiom), the English-typography `:caminho \
\"../caixa-teia!\"` (paste-from-prose enthusiasm-form), and the last-word-\
substitution `:caminho \"../foo!$\"` shape silently pass every prior cascade \
arm (`!` isn't `\\` / `<` / `>` / `|` / `;` / `&` / backtick / `*` / `?` / `(` \
/ `)` / `{{` / `}}` / `[` / `]` / `'` / `\"` / `#` / `%` / `$`) and route \
through `Path::new(caminho).join(<file>)` looking for a literal `./{caminho}` \
subdirectory that fails at resolve time with a non-self-locating `No such file \
or directory` error far from the source caixa.lisp. The lacre pipeline embeds \
the value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so the byte lands in the BLAKE3 closure and \
rides into every shell-spawned subprocess (the resolver's `git clone`, a \
future `feira tofu` shell-out, a future operator-side `nix flake check` spawn) \
as the canonical shell-history-expansion / RFC-3986-sub-delims surface every \
peer single-token-shaped typed slot already closes. The peer `:fonte :repo` \
axis closes the byte under the same shell-history-expansion / RFC-3986-sub-\
delims banner (7d53c68 `!` on `is_git_repo_url`). Express the path as a bare \
relative single-token like \"../caixa-teia\" — the sibling-workspace directory \
name carries no shell-history-expansion / bang-operator semantic; drop any \
`!sudo` / `!!` / `!$` history-expansion trailing paste-from-shell-history \
idiom; and drop any trailing English-typography exclamation mark that pasted \
from prose.",
ch = *byte as char
)]
FonteCaminhoShellHistoryExpansion {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} contains shell-\
history-substitution / RFC-3986-'unwise' / regex-negation metacharacter 0x{byte:02x} \
`{ch}` (POSIX bash's `set -o histexpand` mode — the default for every interactive \
session and every `bash -i` subprocess-argument context `feira tofu` / `git clone` / \
`nix flake check` cross — lexes `^old^new^` per bash reference §9.3 as the 'quick \
substitution' history operator that rewrites the prior command's `old` string to \
`new` and re-executes it verbatim, the canonical typo-correction one-liner idiom \
(`git clone <bad-url>` → `^bad^good` typo-fix-and-rerun the paste-from-shell-\
history author trims only the leading `git clone` prefix from). RFC 3986 §2 lists \
`^` in the 'unwise' set every URL parser is required to percent-encode-or-refuse at \
the wire boundary, and the WHATWG URL spec's 'fragment percent-encode set' maps \
`^` → `%5E` at the query / fragment component transition, so `Path::join` on the \
literal value diverges from every downstream `feira tofu` curl-invocation / \
artifact-registry-fetch that percent-encodes the byte before the wire. `^` is also \
the regex character-class negation prefix (`[^abc]`), the bitwise XOR operator in \
C / C++ / Rust / Python / JavaScript / Nix / Go, the Windows `cmd.exe` escape \
metacharacter, and the LaTeX / Markdown / BibTeX superscript operator. POSIX \
`std::path::Path` treats `^` as a literal path-component byte, so \
`:caminho \"../foo^bar/baz\"` (embedded quick-substitution), `:caminho \
\"../foo^\"` (trailing history-substitution-open shape), or `:caminho \"../x^y\"` \
(XOR-expression paste-from-source) silently pass every prior cascade arm (`^` \
isn't `\\` / `<` / `>` / `|` / `;` / `&` / backtick / `*` / `?` / `(` / `)` / `{{` \
/ `}}` / `[` / `]` / `'` / `\"` / `#` / `%` / `$` / `!`) and route through \
`Path::new(caminho).join(<file>)` looking for a literal `./{caminho}` subdirectory \
that fails at resolve time with a non-self-locating `No such file or directory` \
error far from the source caixa.lisp. The lacre pipeline embeds the value verbatim \
in its per-dep content-address `path:{caminho}` at caixa-resolver/src/resolve.rs:189, \
so the byte lands in the BLAKE3 closure and rides into every shell-spawned \
subprocess (the resolver's `git clone`, a future `feira tofu` shell-out, a future \
operator-side `nix flake check` spawn) as the canonical shell-history-substitution \
/ RFC-3986-unwise surface every peer single-token-shaped typed slot already closes. \
The peer `:fonte :repo` axis closes the byte under the same shell-history-\
substitution / RFC-3986-unwise banner (49e142f `^` on `is_git_repo_url`). Together \
with the immediate-predecessor `!` arm (6a04767) this arm closes the full `set -o \
histexpand` operator surface on the `:caminho` axis — the `!command` / `!!` / `!$` \
prefix form via `!`, the `^old^new^` quick-substitution form via `^`. Express the \
path as a bare relative single-token like \"../caixa-teia\" — the sibling-workspace \
directory name carries no shell-history-substitution / regex-negation / XOR-operator \
semantic; drop any `^old^new` history-substitution paste-from-shell-history idiom; \
drop any trailing `^` history-substitution-open fragment.",
ch = *byte as char
)]
FonteCaminhoShellHistorySubstitution {
nome: String,
caminho: String,
byte: u8,
},
#[error(
":deps entry {nome:?} :fonte (:tipo path …) :caminho {caminho:?} has a trailing \
`/` (the resolver's `Path::join` resolves `\"../caixa-teia\"` and \
`\"../caixa-teia/\"` to the same directory, but the lacre pipeline embeds the \
value verbatim in its per-dep content-address `path:{caminho}` at \
caixa-resolver/src/resolve.rs:189, so two authors whose only difference is \
shell tab-completion emit byte-divergent BLAKE3 closures for the same caixa — \
defeating the THEORY.md §V.2 render-determinism contract via the trailing-\
separator vector (the canonical paste-from-shell-tab-completion + paste-from-\
`pwd`-with-`/`-suffix footgun, and the canonical Cargo-style `path = \
\"../caixa-teia/\"` paste-from-Cargo-manifest cross-idiom leak). Drop the \
trailing `/`; every `:caminho` value names a sibling-workspace directory \
already, so the trailing separator carries no information. Use \
`\"../caixa-teia\"` rather than `\"../caixa-teia/\"`)"
)]
FonteCaminhoTrailingSlash { nome: String, caminho: String },
#[error(
"{list} carries duplicate entry :nome {nome:?} — every dep list keys its \
entries by caixa name (Cargo's [dependencies] / [dev-dependencies] tables \
apply the same set-not-multiset discipline; one package per table), and \
two entries naming the same caixa carry two version constraints / source \
pins / feature sets for one identity. The caixa-resolver's lacre pipeline \
consumes the list as a `HashMap`-keyed-by-`:nome` lookup: the second entry \
silently overwrites the first at the resolver-side `concrete_versao` step, \
and the dropped entry's pin / features never reach the closure — far from \
the source caixa.lisp, with no field naming which `:deps` entry was the \
silent loser. If two version constraints are genuinely needed (the rare \
multi-version closure case the lacre pipeline doesn't yet support), the \
author surface is two distinct caixa names (e.g. a `caixa-teia-v01` / \
`caixa-teia-v02` aliased pair); within one list, one entry per caixa name."
)]
DuplicateNome { nome: String, list: &'static str },
#[error(
":deps entry {nome:?} has empty :caracteristicas entry — every feature flag must \
name a non-empty identifier on the target caixa (Cargo's [dependencies.<dep>.features] \
applies the same per-entry non-empty discipline). An empty feature flag reaches the \
caixa-resolver's lacre pipeline as a no-op feature enable, silently dropping the \
author's intent far from the source caixa.lisp; drop the empty entry, or replace it \
with the canonical kebab-case feature name the target caixa declares."
)]
CaracteristicaEmpty { nome: String },
#[error(
":deps entry {nome:?} :caracteristicas entry {caracteristica:?} is not a valid Cargo \
feature name: {reason} (the value flows verbatim into Cargo's \
[dependencies.<dep>.features] list and Cargo's `restricted_names::validate_feature_name` \
parser enforces the same shape at `cargo metadata` time; use a single-token \
identifier like `\"http\"`, `\"derive\"`, or `\"runtime-tokio\"` — kebab-case ASCII \
alphanumeric with `-`, `_`, `+`, or `.` as continuation characters, starting with \
an ASCII alphanumeric or `_`)"
)]
CaracteristicaInvalid {
nome: String,
caracteristica: String,
reason: String,
},
#[error(
":deps entry {nome:?} :caracteristicas carries duplicate feature {caracteristica:?} — \
every feature-flag list keys its entries by name (Cargo's \
[dependencies.<dep>.features] applies the same set-not-multiset discipline; one entry \
per feature per dep), and two entries naming the same feature are a redundant \
set-membership declaration for one identity (the feature-toggle slot is set-shaped; \
enabling a feature twice has no additional semantic). The caixa-resolver's lacre \
pipeline consumes the list as a set-shaped feature toggle — the resolver enables the \
feature once regardless of declaration count, so the duplicate's pin / position never \
reaches the closure with no field naming the silent loser. One entry per feature per \
dep; if two distinct features are intended, name each verbatim."
)]
CaracteristicaDuplicate {
nome: String,
caracteristica: String,
},
#[error(
"{list} entry :nome {nome:?} names the caixa itself — a caixa cannot depend \
on itself (the lacre closure's dep-graph traversal is rooted at the caixa's \
:nome, and a self-dep would be a one-node cycle the caixa-resolver either \
rejects mid-traversal far from the source caixa.lisp or recurses on until \
it exhausts its stack). Every :nome is globally-unique substrate identity, \
so a :deps / :deps-dev entry whose :nome equals the parent caixa's :nome \
*is* the parent itself, not a coincidentally-named peer. Drop the \
self-referential dep entry — to reference code from this caixa, use \
:bibliotecas / :exe / :servicos (the substrate-blessed shape for \
referencing the caixa's own code surface) instead."
)]
DepIsSelf { nome: String, list: &'static str },
}
macro_rules! fonte_caminho_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl DepError {
$(
#[doc = concat!(
"Construct a [`DepError::",
stringify!($variant),
"`] naming the offending `:deps :nome` + `:fonte ",
"(:tipo path …) :caminho` pair. Folds the uniform ",
"`Self::",
stringify!($variant),
" { nome: nome.to_string(), caminho: caminho.to_string() }` ",
"two-slot struct-literal onto one substrate primitive so ",
"every [`DepSource::validate_caminho`] wire-up on this ",
"variant reads through one dispatch rather than the ",
"pre-lift four-line open-coded block."
)]
#[must_use]
pub fn $ctor(nome: &str, caminho: &str) -> Self {
Self::$variant {
nome: nome.to_string(),
caminho: caminho.to_string(),
}
}
)*
}
};
}
fonte_caminho_ctors! {
fonte_caminho_absolute => FonteCaminhoAbsolute,
fonte_caminho_tilde_expansion => FonteCaminhoTildeExpansion,
fonte_caminho_var_expansion => FonteCaminhoVarExpansion,
fonte_caminho_leading_whitespace => FonteCaminhoLeadingWhitespace,
fonte_caminho_leading_hyphen => FonteCaminhoLeadingHyphen,
fonte_caminho_backslash => FonteCaminhoBackslash,
fonte_caminho_shell_pipe => FonteCaminhoShellPipe,
fonte_caminho_shell_semicolon => FonteCaminhoShellSemicolon,
fonte_caminho_shell_background => FonteCaminhoShellBackground,
fonte_caminho_shell_command_substitution => FonteCaminhoShellCommandSubstitution,
fonte_caminho_trailing_slash => FonteCaminhoTrailingSlash,
}
macro_rules! fonte_caminho_byte_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl DepError {
$(
#[doc = concat!(
"Construct a [`DepError::",
stringify!($variant),
"`] naming the offending `:deps :nome` + `:fonte ",
"(:tipo path …) :caminho` pair + the offending `byte: u8` ",
"classification. Folds the uniform `Self::",
stringify!($variant),
" { nome: nome.to_string(), caminho: caminho.to_string(), ",
"byte }` three-slot struct-literal onto one substrate ",
"primitive so every [`DepSource::validate_caminho`] wire-up ",
"on this variant reads through one dispatch rather than ",
"the pre-lift five-line open-coded block."
)]
#[must_use]
pub fn $ctor(nome: &str, caminho: &str, byte: u8) -> Self {
Self::$variant {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
}
}
)*
}
};
}
fonte_caminho_byte_ctors! {
fonte_caminho_control_char => FonteCaminhoControlChar,
fonte_caminho_shell_redirection => FonteCaminhoShellRedirection,
fonte_caminho_shell_glob => FonteCaminhoShellGlob,
fonte_caminho_shell_subshell_grouping => FonteCaminhoShellSubshellGrouping,
fonte_caminho_shell_brace_expansion => FonteCaminhoShellBraceExpansion,
fonte_caminho_shell_bracket_expansion => FonteCaminhoShellBracketExpansion,
fonte_caminho_shell_quote_grouping => FonteCaminhoShellQuoteGrouping,
fonte_caminho_shell_comment => FonteCaminhoShellComment,
fonte_caminho_url_percent_encoding => FonteCaminhoUrlPercentEncoding,
fonte_caminho_shell_variable_expansion => FonteCaminhoShellVariableExpansion,
fonte_caminho_shell_history_expansion => FonteCaminhoShellHistoryExpansion,
fonte_caminho_shell_history_substitution => FonteCaminhoShellHistorySubstitution,
}
macro_rules! dep_nome_only_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl DepError {
$(
#[doc = concat!(
"Construct a [`DepError::",
stringify!($variant),
"`] naming the offending `:deps :nome`. Folds the ",
"uniform `Self::",
stringify!($variant),
" { nome: nome.to_string() }` one-field ",
"struct-literal onto one substrate primitive so every ",
"in-crate wire-up on this variant reads through one ",
"dispatch rather than the pre-lift three-line ",
"open-coded block."
)]
#[must_use]
pub fn $ctor(nome: &str) -> Self {
Self::$variant { nome: nome.to_string() }
}
)*
}
};
}
dep_nome_only_ctors! {
versao_empty => VersaoEmpty,
fonte_repo_empty => FonteRepoEmpty,
fonte_pin_missing => FontePinMissing,
fonte_caminho_empty => FonteCaminhoEmpty,
caracteristica_empty => CaracteristicaEmpty,
}
macro_rules! dep_nome_list_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl DepError {
$(
#[doc = concat!(
"Construct a [`DepError::",
stringify!($variant),
"`] naming the offending `:deps :nome` and the ",
"author-surface list tag (`:deps` vs. `:deps-dev`) ",
"the diagnostic points the author back at. Folds ",
"the uniform `Self::",
stringify!($variant),
" { nome: nome.to_string(), list }` two-field ",
"struct-literal onto one substrate primitive so ",
"every in-crate wire-up on this variant reads ",
"through one dispatch rather than the pre-lift ",
"open-coded struct-literal block."
)]
#[must_use]
pub fn $ctor(nome: &str, list: &'static str) -> Self {
Self::$variant { nome: nome.to_string(), list }
}
)*
}
};
}
dep_nome_list_ctors! {
duplicate_nome => DuplicateNome,
dep_is_self => DepIsSelf,
}
macro_rules! dep_nome_axis_reason_ctors {
($($ctor:ident => $variant:ident { $axis:ident }),* $(,)?) => {
impl DepError {
$(
#[doc = concat!(
"Construct a [`DepError::",
stringify!($variant),
"`] naming the offending `:deps :nome`, the offending ",
"`:", stringify!($axis), "` axis value, and the ",
"parser-shaped rejection `reason`. Folds the uniform ",
"`Self::",
stringify!($variant),
" { nome: nome.to_string(), ",
stringify!($axis),
": ",
stringify!($axis),
".to_string(), reason }` three-field struct-literal ",
"onto one substrate primitive so every in-crate ",
"wire-up on this variant reads through one dispatch ",
"rather than the pre-lift five-line open-coded block. ",
"The `nome: &str` and `",
stringify!($axis),
": &str` parameters accept `&str` literals and ",
"`&String` (via Deref coercion) so every existing ",
"wire-up threads through the ctor without a ",
"pre-conversion; the `reason: String` parameter takes ",
"an owned `String` (not `impl Into<String>`) matching ",
"the paired `crate::render::*` predicate's ",
"`Result<(), String>` return shape every wire-up ",
"already holds owned at the call site."
)]
#[must_use]
pub fn $ctor(nome: &str, $axis: &str, reason: String) -> Self {
Self::$variant {
nome: nome.to_string(),
$axis: $axis.to_string(),
reason,
}
}
)*
}
};
}
dep_nome_axis_reason_ctors! {
versao_invalid => VersaoInvalid { versao },
fonte_repo_shape => FonteRepoShape { repo },
caracteristica_invalid => CaracteristicaInvalid { caracteristica },
}
macro_rules! dep_nome_axis_ctors {
($($ctor:ident => $variant:ident { $axis:ident }),* $(,)?) => {
impl DepError {
$(
#[doc = concat!(
"Construct a [`DepError::",
stringify!($variant),
"`] naming the offending `:deps :nome` and the ",
"offending `:", stringify!($axis), "` axis value. ",
"Folds the uniform `Self::",
stringify!($variant),
" { nome: nome.to_string(), ",
stringify!($axis),
": ",
stringify!($axis),
".to_string() }` two-field struct-literal onto one ",
"substrate primitive so every in-crate wire-up on ",
"this variant reads through one dispatch rather than ",
"the pre-lift four-line open-coded block. Both `nome: ",
"&str` and `",
stringify!($axis),
": &str` parameters accept `&str` literals and ",
"`&String` (via Deref coercion) so every existing ",
"wire-up threads through the ctor without a pre-",
"conversion."
)]
#[must_use]
pub fn $ctor(nome: &str, $axis: &str) -> Self {
Self::$variant {
nome: nome.to_string(),
$axis: $axis.to_string(),
}
}
)*
}
};
}
dep_nome_axis_ctors! {
fonte_pin_empty => FontePinEmpty { pin },
fonte_pin_ambiguous => FontePinAmbiguous { pins },
caracteristica_duplicate => CaracteristicaDuplicate { caracteristica },
}
impl DepError {
#[must_use]
pub fn fonte_pin_shape(nome: &str, pin: &str, value: &str, reason: String) -> Self {
Self::FontePinShape {
nome: nome.to_string(),
pin: pin.to_string(),
value: value.to_string(),
reason,
}
}
#[must_use]
pub fn nome_invalid(nome: &str, reason: String) -> Self {
Self::NomeInvalid {
nome: nome.to_string(),
reason,
}
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!*b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_dep_is_minimal() {
let d = Dep::simple("caixa-teia", "^0.1");
assert_eq!(d.nome, "caixa-teia");
assert_eq!(d.versao, "^0.1");
assert!(d.fonte.is_none());
assert!(!d.opcional());
assert!(d.caracteristicas().is_empty());
}
#[test]
fn dep_string_scalar_accessor_pair_is_const_fn() {
const fn nome_via_const_fn(d: &Dep) -> &str {
d.nome()
}
const fn versao_via_const_fn(d: &Dep) -> &str {
d.versao_requirement()
}
for (nome, versao) in [
("caixa-teia", "^0.1"),
("caixa-mesh", "~0.2.3"),
("caixa-helm", "*"),
] {
let d = Dep::simple(nome, versao);
assert_eq!(nome_via_const_fn(&d), d.nome());
assert_eq!(versao_via_const_fn(&d), d.versao_requirement());
assert_eq!(d.nome(), nome);
assert_eq!(d.versao_requirement(), versao);
}
}
#[test]
fn dep_outer_accessor_family_is_const_fn() {
const fn fonte_via_const_fn(d: &Dep) -> Option<&DepSource> {
d.fonte()
}
const fn caracteristicas_via_const_fn(d: &Dep) -> &[String] {
d.caracteristicas()
}
let empty = Dep::simple("caixa-teia", "^0.1");
assert!(fonte_via_const_fn(&empty).is_none());
assert_eq!(fonte_via_const_fn(&empty), empty.fonte());
assert!(caracteristicas_via_const_fn(&empty).is_empty());
assert_eq!(
caracteristicas_via_const_fn(&empty),
empty.caracteristicas()
);
let git = Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0");
assert!(fonte_via_const_fn(&git).is_some());
assert_eq!(fonte_via_const_fn(&git), git.fonte());
assert_eq!(caracteristicas_via_const_fn(&git), git.caracteristicas());
let mut with_features = Dep::simple("caixa-teia", "^0.1");
with_features.caracteristicas = vec!["feat-a".into(), "feat-b".into()];
assert_eq!(caracteristicas_via_const_fn(&with_features).len(), 2);
assert_eq!(
caracteristicas_via_const_fn(&with_features),
with_features.caracteristicas()
);
}
#[test]
fn git_dep_carries_tag() {
let d = Dep::git("t", "*", "github:o/r", "v1");
match d.fonte {
Some(DepSource::Git {
ref repo, ref tag, ..
}) => {
assert_eq!(repo, "github:o/r");
assert_eq!(tag.as_deref(), Some("v1"));
}
_ => panic!("expected Git source"),
}
}
#[test]
fn validate_accepts_simple_dep() {
Dep::simple("caixa-teia", "^0.1").validate().unwrap();
}
#[test]
fn validate_rejects_empty_nome() {
let mut d = Dep::simple("placeholder", "^0.1");
d.nome = String::new();
assert_eq!(d.validate().unwrap_err(), DepError::NomeEmpty);
}
#[test]
fn validate_rejects_empty_versao() {
let mut d = Dep::simple("caixa-teia", "ignored");
d.versao = String::new();
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::VersaoEmpty { ref nome } if nome == "caixa-teia"),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_with_uppercase() {
let d = Dep::simple("Caixa-Teia", "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome == "Caixa-Teia" && reason.contains("uppercase")
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_with_underscore() {
let d = Dep::simple("caixa_teia", "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome == "caixa_teia" && reason.contains('_')
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_with_dot() {
let d = Dep::simple("caixa.teia", "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome == "caixa.teia" && reason.contains('.')
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_with_leading_hyphen() {
let d = Dep::simple("-caixa-teia", "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome == "-caixa-teia" && reason.contains("alphanumeric")
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_with_trailing_hyphen() {
let d = Dep::simple("caixa-teia-", "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome == "caixa-teia-" && reason.contains("alphanumeric")
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_with_slash() {
let d = Dep::simple("pleme-io/caixa-teia", "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome == "pleme-io/caixa-teia" && reason.contains('/')
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_nome_too_long() {
let long = "a".repeat(64);
let d = Dep::simple(&long, "^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::NomeInvalid { ref nome, ref reason }
if nome.len() == 64 && reason.contains("max length of 63")
),
"got {err:?}"
);
}
#[test]
fn validate_accepts_canonical_nome_labels() {
for nome in [
"caixa-teia",
"caixa-resolver2",
"2nd-tier-cache",
"x",
"abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0",
] {
Dep::simple(nome, "^0.1")
.validate()
.unwrap_or_else(|e| panic!("canonical label {nome:?} must validate, got {e:?}"));
}
}
#[test]
fn nome_empty_takes_precedence_over_nome_invalid() {
let mut d = Dep::simple("placeholder", "^0.1");
d.nome = String::new();
assert_eq!(d.validate().unwrap_err(), DepError::NomeEmpty);
}
#[test]
fn nome_invalid_fires_before_versao_empty() {
let mut d = Dep::simple("Caixa-Teia", "^0.1");
d.versao = String::new();
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::NomeInvalid { ref nome, .. } if nome == "Caixa-Teia"),
"got {err:?}"
);
}
#[test]
fn nome_invalid_fires_before_versao_invalid() {
let d = Dep::simple("Caixa-Teia", "^^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::NomeInvalid { ref nome, .. } if nome == "Caixa-Teia"),
"got {err:?}"
);
}
#[test]
fn nome_invalid_fires_before_fonte_invalid() {
let mut d = Dep::simple("Caixa-Teia", "^0.1");
d.fonte = Some(DepSource::Git {
repo: String::new(),
tag: None,
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::NomeInvalid { ref nome, .. } if nome == "Caixa-Teia"),
"got {err:?}"
);
}
#[test]
fn nome_invalid_diagnostic_carries_offending_name() {
let d = Dep::simple("Caixa_Teia", "^0.1");
let err = d.validate().unwrap_err();
let DepError::NomeInvalid { nome, reason } = err else {
panic!("expected NomeInvalid, got other variant");
};
assert_eq!(nome, "Caixa_Teia");
assert!(
!reason.is_empty(),
"NomeInvalid `reason` must carry the predicate's wording verbatim"
);
}
#[test]
fn validate_rejects_invalid_versao_requirement() {
let d = Dep::simple("caixa-teia", "^bad-version");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::VersaoInvalid { ref nome, ref versao, .. }
if nome == "caixa-teia" && versao == "^bad-version"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_versao_with_double_caret_typo() {
let d = Dep::simple("caixa-teia", "^^0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::VersaoInvalid { ref nome, ref versao, .. }
if nome == "caixa-teia" && versao == "^^0.1"
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_versao_with_v_prefixed_tag() {
let d = Dep::simple("caixa-teia", "v0.1");
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::VersaoInvalid { ref nome, ref versao, .. }
if nome == "caixa-teia" && versao == "v0.1"
),
"got {err:?}"
);
}
#[test]
fn validate_accepts_canonical_versao_forms() {
for form in [
"^0.1", "~0.1.2", "0.1.0", "*", ">=0.1, <2", ] {
Dep::simple("caixa-teia", form)
.validate()
.unwrap_or_else(|e| panic!("canonical form {form:?} must validate, got {e:?}"));
}
}
#[test]
fn versao_empty_takes_precedence_over_invalid() {
let mut d = Dep::simple("caixa-teia", "ignored");
d.versao = String::new();
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::VersaoEmpty { ref nome } if nome == "caixa-teia"),
"got {err:?}"
);
}
#[test]
fn nome_empty_takes_precedence_over_versao_invalid() {
let mut d = Dep::simple("placeholder", "^bad");
d.nome = String::new();
let err = d.validate().unwrap_err();
assert_eq!(err, DepError::NomeEmpty);
}
#[test]
fn versao_invalid_diagnostic_carries_offending_versao() {
let d = Dep::simple("caixa-teia", "not-a-req");
let err = d.validate().unwrap_err();
let DepError::VersaoInvalid {
nome,
versao,
reason,
} = err
else {
panic!("expected VersaoInvalid, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(versao, "not-a-req");
assert!(
!reason.is_empty(),
"VersaoInvalid `reason` must carry the parser's wording verbatim"
);
}
fn dep_with_fonte(fonte: DepSource) -> Dep {
let mut d = Dep::simple("caixa-teia", "^0.1");
d.fonte = Some(fonte);
d
}
#[test]
fn validate_accepts_git_fonte_with_tag() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_git_fonte_with_rev() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("c0ffee0123abcdef0123456789abcdef01234567".into()),
branch: None,
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_git_fonte_with_branch() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: Some("main".into()),
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_path_fonte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia".into(),
});
d.validate().unwrap();
}
#[test]
fn validate_rejects_git_fonte_with_empty_repo() {
let d = dep_with_fonte(DepSource::Git {
repo: String::new(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteRepoEmpty { ref nome } if nome == "caixa-teia"),
"got {err:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_trailing_space() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia ".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "github:pleme-io/caixa-teia ");
assert!(
reason.contains("whitespace"),
"reason must surface the whitespace arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_starting_with_dash() {
let d = dep_with_fonte(DepSource::Git {
repo: "-upload-pack=evil".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { repo, reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(repo, "-upload-pack=evil");
assert!(
reason.contains("must not start with `-`"),
"reason must surface the leading-`-` arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_embedded_newline() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia\nrm -rf /".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("control character"),
"reason must surface the control-char arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_tab() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia\t".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteRepoShape { ref reason, .. }
if reason.contains("whitespace")
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_non_ascii() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/café".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteRepoShape { ref reason, .. }
if reason.contains("non-ASCII")
),
"got {err:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_fragment_anchor() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia#readme");
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm, got {reason:?}"
);
assert!(
reason.contains("fragment"),
"reason must name the URL fragment grammar, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_flake_ref_fragment() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia#caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm, got {reason:?}"
);
assert!(
reason.contains("Nix flake"),
"reason must name the Nix-flake-ref cross-idiom footgun, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_query_string() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia?tab=readme-ov-file".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(
repo,
"https://github.com/pleme-io/caixa-teia?tab=readme-ov-file"
);
assert!(
reason.contains("must not contain `?`"),
"reason must surface the query-`?` arm, got {reason:?}"
);
assert!(
reason.contains("query"),
"reason must name the URL query grammar, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_utm_tracker() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia?utm_source=twitter&utm_campaign=launch"
.into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `?`"),
"reason must surface the query-`?` arm, got {reason:?}"
);
assert!(
reason.contains("campaign-tracker"),
"reason must name the campaign-tracker paste footgun, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_query_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme?ref=main".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before query-`?` when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_control_char_fires_before_fragment() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia\n#readme".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("control character"),
"reason must surface the control-char arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_embedded_backslash() {
let d = dep_with_fonte(DepSource::Git {
repo: "file:///C:\\Users\\me\\caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "file:///C:\\Users\\me\\caixa-teia");
assert!(
reason.contains("must not contain `\\`"),
"reason must surface the backslash-`\\` arm, got {reason:?}"
);
assert!(
reason.contains("Windows"),
"reason must name the Windows-path-confusion footgun, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_mangled_https_backslashes() {
let d = dep_with_fonte(DepSource::Git {
repo: "https:\\\\github.com\\pleme-io\\caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `\\`"),
"reason must surface the backslash-`\\` arm, got {reason:?}"
);
assert!(
reason.contains("path separator") || reason.contains("path-segment separator"),
"reason must name the URL path-segment separator grammar, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_backslash_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme\\foo".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before backslash-`\\` when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_uri_template_placeholder() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/{org}/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/{org}/caixa-teia");
assert!(
reason.contains("must not contain `{`"),
"reason must surface the open-brace `{{` arm, got {reason:?}"
);
assert!(
reason.contains("URI Template") || reason.contains("RFC 6570"),
"reason must name the RFC 6570 URI Template grammar, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_handlebars_doubled_brace() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/{{org}}/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `{`"),
"reason must surface the open-brace `{{` arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_closing_brace_only() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia}".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `}`"),
"reason must surface the close-brace `}}` arm, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_template_placeholder_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme{org}".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before template-`{{` when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_output_redirection() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia>build.log".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia>build.log");
assert!(
reason.contains("must not contain `>`"),
"reason must surface the output-redirection `>` arm, got {reason:?}"
);
assert!(
reason.contains("redirection") || reason.contains("'delims'"),
"reason must name the shell-redirection / RFC-3986-unwise rationale, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_input_redirection() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia<input.url".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `<`"),
"reason must surface the input-redirection `<` arm, got {reason:?}"
);
assert!(
reason.contains("RFC 3986") || reason.contains("'unwise'"),
"reason must name the RFC 3986 'unwise' grammar, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_backtick_command_substitution() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/`whoami`/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/`whoami`/caixa-teia");
assert!(
reason.contains("must not contain `` ` ``"),
"reason must surface the backtick command-substitution arm, got {reason:?}"
);
assert!(
reason.contains("command-substitution") || reason.contains("'unwise'"),
"reason must name the shell-command-substitution / RFC-3986-unwise rationale, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_backtick_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme/`whoami`".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before backtick when `#` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_shell_redirection_fires_before_backtick_when_redirection_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia>build.log/`whoami`".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `>`"),
"reason must surface the shell-redirection `>` arm (fires before backtick when \
`>` byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_shell_redirection_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme>build.log".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before shell-redirection `>` when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_pipe() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia|tee build.log".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia|tee build.log");
assert!(
reason.contains("must not contain `|`"),
"reason must surface the shell-pipe arm, got {reason:?}"
);
assert!(
reason.contains("pipe") || reason.contains("'unwise'"),
"reason must name the shell-pipe / RFC-3986-unwise rationale, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_pipe_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme|tee".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before pipe when `#` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_backtick_fires_before_pipe_when_backtick_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia/`whoami`|tee".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `` ` ``"),
"reason must surface the backtick arm (fires before pipe when `` ` `` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_command_separator() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia; rm -rf build".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia; rm -rf build");
assert!(
reason.contains("must not contain `;`"),
"reason must surface the shell-command-separator arm, got {reason:?}"
);
assert!(
reason.contains("sequential-command") || reason.contains("'sub-delims'"),
"reason must name the shell-command-separator / RFC-3986-sub-delims \
rationale, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_semicolon_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme; rm".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before semicolon when `#` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_pipe_fires_before_semicolon_when_pipe_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia|tee; rm".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `|`"),
"reason must surface the pipe arm (fires before semicolon when `|` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_background() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia&sleep".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia&sleep");
assert!(
reason.contains("must not contain `&`"),
"reason must surface the shell-background / logical-AND arm, got {reason:?}"
);
assert!(
reason.contains("background-task") || reason.contains("'sub-delims'"),
"reason must name the shell-background / RFC-3986-sub-delims rationale, \
got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_logical_and() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia&&echo".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `&`"),
"reason must surface the shell-background / logical-AND arm on the doubled-`&&` \
shape too, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_background_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme&sleep".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before background-`&` when `#` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_semicolon_fires_before_background_when_semicolon_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia;rm&sleep".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `;`"),
"reason must surface the semicolon arm (fires before background-`&` when `;` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_variable_expansion() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/$ORG/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/$ORG/caixa-teia");
assert!(
reason.contains("must not contain `$`"),
"reason must surface the shell-variable-expansion arm, got {reason:?}"
);
assert!(
reason.contains("variable-expansion") || reason.contains("'sub-delims'"),
"reason must name the shell-variable-expansion / RFC-3986-sub-delims \
rationale, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_braced_variable_expansion() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/${WORKSPACE}/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `$`"),
"reason must surface the shell-variable-expansion arm on the braced `${{...}}` \
shape too, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_var_expansion_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme$HOME".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before var-expansion-`$` when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_background_fires_before_var_expansion_when_background_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia&sleep$HOME".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `&`"),
"reason must surface the background-`&` arm (fires before var-expansion-`$` when \
`&` byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_glob_wildcard() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-*".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-*");
assert!(
reason.contains("must not contain `*`"),
"reason must surface the shell-glob arm, got {reason:?}"
);
assert!(
reason.contains("pathname-expansion") || reason.contains("'sub-delims'"),
"reason must name the shell-glob / pathname-expansion / \
RFC-3986-sub-delims rationale, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_recursive_shell_glob() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/**/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `*`"),
"reason must surface the shell-glob arm on the `**` recursive-glob shape too, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_glob_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme*tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before glob-`*` when `#` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_var_expansion_fires_before_glob_when_var_expansion_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/$ORG-caixa-*".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `$`"),
"reason must surface the var-expansion-`$` arm (fires before glob-`*` when `$` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_subshell_open_paren() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/(foo|bar)/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/(foo|bar)/caixa-teia");
assert!(
reason.contains("must not contain `(`"),
"reason must surface the subshell-open-paren arm, got {reason:?}"
);
assert!(
reason.contains("subshell-grouping") || reason.contains("'sub-delims'"),
"reason must name the shell-subshell-grouping / RFC-3986-sub-delims rationale, \
got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_subshell_close_paren() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia)tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `)`"),
"reason must surface the subshell-close-paren arm on the bare `)` shape, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_subshell_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme(tail)".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before subshell-`(` when `#` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_glob_fires_before_subshell_when_glob_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia-*-(date)".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `*`"),
"reason must surface the glob-`*` arm (fires before subshell-`(` when `*` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_double_quote() {
let d = dep_with_fonte(DepSource::Git {
repo: "\"https://github.com/pleme-io/caixa-teia\"".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "\"https://github.com/pleme-io/caixa-teia\"");
assert!(
reason.contains("must not contain `\"`"),
"reason must surface the shell-double-quote arm, got {reason:?}"
);
assert!(
reason.contains("double-quote") || reason.contains("'delims'"),
"reason must name the shell-double-quote / RFC-3986-delims rationale, \
got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_stray_trailing_double_quote() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia\"".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `\"`"),
"reason must surface the shell-double-quote arm on the trailing-`\"` shape, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_double_quote_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme\"tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before double-quote when `#` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_subshell_fires_before_double_quote_when_subshell_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia(date)\"tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `(`"),
"reason must surface the subshell-`(` arm (fires before double-quote when `(` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_single_quote() {
let d = dep_with_fonte(DepSource::Git {
repo: "'https://github.com/pleme-io/caixa-teia'".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "'https://github.com/pleme-io/caixa-teia'");
assert!(
reason.contains("must not contain `'`"),
"reason must surface the shell-single-quote arm, got {reason:?}"
);
assert!(
reason.contains("single-quote") || reason.contains("strong-quote"),
"reason must name the shell-single-quote / strong-quote rationale, \
got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_english_apostrophe() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/repo's-fork".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `'`"),
"reason must surface the shell-single-quote arm on the mid-string \
apostrophe shape, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_single_quote_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme'tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before single-quote when `#` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_double_quote_fires_before_single_quote_when_double_quote_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia\"mid'tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `\"`"),
"reason must surface the double-quote arm (fires before single-quote when `\"` \
byte appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_history_expansion() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia!sudo".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia!sudo");
assert!(
reason.contains("must not contain `!`"),
"reason must surface the shell-history-expansion arm, got {reason:?}"
);
assert!(
reason.contains("history-expansion") || reason.contains("bang"),
"reason must name the shell-history-expansion / bang rationale, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_double_bang_history_reference() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia!!".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `!`"),
"reason must surface the shell-history-expansion arm on the trailing-`!!` shape, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_bang_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme!tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before bang when `#` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_single_quote_fires_before_bang_when_single_quote_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia'mid!tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `'`"),
"reason must surface the single-quote arm (fires before bang when `'` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_list_separator_comma() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia,github:pleme-io/caixa-feira".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(
repo,
"github:pleme-io/caixa-teia,github:pleme-io/caixa-feira"
);
assert!(
reason.contains("must not contain `,`"),
"reason must surface the list-separator-comma arm, got {reason:?}"
);
assert!(
reason.contains("list-separator") || reason.contains("sub-delims"),
"reason must name the list-separator / RFC-3986-sub-delims rationale, \
got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_trailing_comma() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-feira,".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `,`"),
"reason must surface the list-separator-comma arm on the trailing-`,` shape, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_comma_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme,tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before comma when `#` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_bang_fires_before_comma_when_bang_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia!mid,tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `!`"),
"reason must surface the bang arm (fires before comma when `!` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_shell_env_var_assignment() {
let d = dep_with_fonte(DepSource::Git {
repo: "GIT_TERMINAL_PROMPT=0 https://github.com/pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(
repo,
"GIT_TERMINAL_PROMPT=0 https://github.com/pleme-io/caixa-teia"
);
assert!(
reason.contains("must not contain `=`"),
"reason must surface the equals-`=` arm on the env-var-assignment \
paste shape, got {reason:?}"
);
assert!(
reason.contains("env-var-assignment") || reason.contains("KEY=VALUE"),
"reason must name the shell-env-var-assignment rationale, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_gitconfig_url_key_prefix() {
let d = dep_with_fonte(DepSource::Git {
repo: "url=https://github.com/pleme-io/caixa-feira".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `=`"),
"reason must surface the equals-`=` arm on the `url=…` gitconfig \
paste shape, got {reason:?}"
);
assert!(
reason.contains("key-value-separator") || reason.contains("sub-delims"),
"reason must name the key-value-separator / RFC-3986-sub-delims \
rationale, got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_equals_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#readme=tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before equals when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_comma_fires_before_equals_when_comma_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia,mid=tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `,`"),
"reason must surface the comma arm (fires before equals when `,` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_percent_encoded_space() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia%20test".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia%20test");
assert!(
reason.contains("must not contain `%`"),
"reason must surface the percent-`%` arm on the percent-encoded-space \
paste shape, got {reason:?}"
);
assert!(
reason.contains("percent-encoding") || reason.contains("%25"),
"reason must name the percent-encoding / `%25` re-encoding rationale, \
got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_percent_encoded_slash() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io%2Fcaixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `%`"),
"reason must surface the percent-`%` arm on the over-encoded-path \
shape, got {reason:?}"
);
assert!(
reason.contains("render-determinism") || reason.contains("BLAKE3"),
"reason must name the render-determinism / BLAKE3-closure rationale, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_fragment_fires_before_percent_when_fragment_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia#sec%20tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `#`"),
"reason must surface the fragment-`#` arm (fires before percent when \
`#` byte appears first in value), got {reason:?}"
);
}
#[test]
fn fonte_repo_equals_fires_before_percent_when_equals_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia=mid%20tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `=`"),
"reason must surface the equals arm (fires before percent when `=` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_caret_history_substitution() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia^typo^fix".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "https://github.com/pleme-io/caixa-teia^typo^fix");
assert!(
reason.contains("must not contain `^`"),
"reason must surface the caret-`^` arm on the paste-from-shell-history \
shape, got {reason:?}"
);
assert!(
reason.contains("history-substitution") || reason.contains("%5E"),
"reason must name the shell-history-substitution / `%5E` wire-encoding \
rationale, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_carrying_caret_regex_anchor() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/^archived".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `^`"),
"reason must surface the caret-`^` arm on the regex-anchor shape, \
got {reason:?}"
);
assert!(
reason.contains("render-determinism") || reason.contains("BLAKE3"),
"reason must name the render-determinism / BLAKE3-closure rationale, \
got {reason:?}"
);
}
#[test]
fn fonte_repo_percent_fires_before_caret_when_percent_first() {
let d = dep_with_fonte(DepSource::Git {
repo: "https://github.com/pleme-io/caixa-teia%20mid^tail".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not contain `%`"),
"reason must surface the percent arm (fires before caret when `%` byte \
appears first in value), got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_missing_colon_separator() {
let d = dep_with_fonte(DepSource::Git {
repo: "pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must contain a `:`"),
"reason must surface the missing-`:` arm, got {reason:?}"
);
assert!(
reason.contains("github:"),
"reason must name the canonical `github:` shorthand prefix, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_leading_colon() {
let d = dep_with_fonte(DepSource::Git {
repo: ":pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("must not start with `:`"),
"reason must surface the leading-`:` arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_repo_too_long() {
let too_long = format!(
"github:pleme-io/{}",
"x".repeat(crate::render::GIT_REPO_URL_MAX_LEN)
);
let d = dep_with_fonte(DepSource::Git {
repo: too_long.clone(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { reason, .. } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert!(
reason.contains("2048"),
"reason must name the cap, got {reason:?}"
);
}
#[test]
fn validate_accepts_canonical_git_fonte_repo_shapes() {
for repo in [
"github:pleme-io/caixa-teia",
"gitlab:pleme-io/caixa-teia",
"codeberg:pleme-io/caixa-teia",
"sourcehut:~pleme-io/caixa-teia",
"https://github.com/pleme-io/caixa-teia",
"https://github.com/pleme-io/caixa-teia.git",
"http://example.com/pleme-io/caixa-teia.git",
"ssh://git@github.com/pleme-io/caixa-teia.git",
"ssh://git@git.example.com:2222/pleme-io/caixa-teia.git",
"git@github.com:pleme-io/caixa-teia.git",
"git@git.example.com:team/private.git",
"git://git.example.com/pleme-io/caixa-teia.git",
"file:///tmp/caixa-teia",
] {
let d = dep_with_fonte(DepSource::Git {
repo: repo.into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
d.validate()
.unwrap_or_else(|e| panic!("canonical repo {repo:?} must validate, got {e:?}"));
}
}
#[test]
fn fonte_repo_empty_takes_precedence_over_shape() {
let d = dep_with_fonte(DepSource::Git {
repo: String::new(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteRepoEmpty { .. }),
"got {err:?}"
);
}
#[test]
fn fonte_repo_shape_fires_before_pin_missing() {
let d = dep_with_fonte(DepSource::Git {
repo: "pleme-io/caixa-teia".into(), tag: None,
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteRepoShape { .. }),
"got {err:?}"
);
}
#[test]
fn fonte_repo_shape_diagnostic_carries_offending_repo_verbatim() {
let d = dep_with_fonte(DepSource::Git {
repo: "pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FonteRepoShape { nome, repo, reason } = err else {
panic!("expected FonteRepoShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(repo, "pleme-io/caixa-teia");
assert!(
!reason.is_empty(),
"FonteRepoShape `reason` must carry the predicate's wording verbatim"
);
}
#[test]
fn validate_rejects_git_fonte_with_no_pin() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FontePinMissing { ref nome } if nome == "caixa-teia"),
"got {err:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_ambiguous_tag_and_branch() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: Some("main".into()),
});
let err = d.validate().unwrap_err();
let DepError::FontePinAmbiguous { nome, pins } = err else {
panic!("expected FontePinAmbiguous");
};
assert_eq!(nome, "caixa-teia");
assert!(pins.contains(":tag"));
assert!(pins.contains(":branch"));
assert!(!pins.contains(":rev"));
}
#[test]
fn validate_rejects_git_fonte_with_ambiguous_tag_and_rev() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: Some("c0ffee".into()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinAmbiguous { nome, pins } = err else {
panic!("expected FontePinAmbiguous");
};
assert_eq!(nome, "caixa-teia");
assert!(pins.contains(":tag"));
assert!(pins.contains(":rev"));
}
#[test]
fn validate_rejects_git_fonte_with_all_three_pins() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: Some("c0ffee".into()),
branch: Some("main".into()),
});
let err = d.validate().unwrap_err();
let DepError::FontePinAmbiguous { nome, pins } = err else {
panic!("expected FontePinAmbiguous");
};
assert_eq!(nome, "caixa-teia");
assert!(pins.contains(":tag"));
assert!(pins.contains(":rev"));
assert!(pins.contains(":branch"));
}
#[test]
fn validate_rejects_git_fonte_with_empty_tag_pin() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some(String::new()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinEmpty { nome, pin } = err else {
panic!("expected FontePinEmpty");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(pin, ":tag");
}
#[test]
fn validate_rejects_git_fonte_with_empty_rev_pin() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some(String::new()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinEmpty { nome, pin } = err else {
panic!("expected FontePinEmpty");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(pin, ":rev");
}
#[test]
fn validate_rejects_path_fonte_with_empty_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: String::new(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoEmpty { ref nome } if nome == "caixa-teia"),
"got {err:?}"
);
}
#[test]
fn validate_rejects_path_fonte_with_absolute_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/home/me/work/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoAbsolute { nome, caminho } = err else {
panic!("expected FonteCaminhoAbsolute, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "/home/me/work/caixa-teia");
}
#[test]
fn validate_accepts_path_fonte_with_parent_escape_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia".into(),
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_path_fonte_with_deeply_nested_relative_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "vendor/forks/caixa-teia".into(),
});
d.validate().unwrap();
}
#[test]
fn validate_rejects_path_fonte_with_tilde_prefix_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "~/work/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoTildeExpansion { nome, caminho } = err else {
panic!("expected FonteCaminhoTildeExpansion, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "~/work/caixa-teia");
}
#[test]
fn validate_rejects_path_fonte_with_bare_tilde_caminho() {
for s in ["~", "~alice/dev/caixa-teia", "~/", "~root/work"] {
let d = dep_with_fonte(DepSource::Path { caminho: s.into() });
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoTildeExpansion { .. }),
"{s:?} → {err:?}",
);
}
}
#[test]
fn validate_accepts_path_fonte_with_mid_path_tilde_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo~bar/caixa-teia".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_empty_fires_before_tilde_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: String::new(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoEmpty { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_tilde_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "~alice/dev/caixa-teia".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("~alice/dev/caixa-teia"),
"diagnostic must quote the offending caminho: {rendered}",
);
assert!(
rendered.contains('~'),
"diagnostic must reference the tilde footgun: {rendered}",
);
}
#[test]
fn validate_rejects_path_fonte_with_dollar_prefix_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$HOME/work/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoVarExpansion { nome, caminho } = err else {
panic!("expected FonteCaminhoVarExpansion, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "$HOME/work/caixa-teia");
}
#[test]
fn validate_rejects_path_fonte_with_dollar_brace_prefix_caminho() {
for s in [
"${HOME}/work/caixa-teia",
"${WORKSPACE}/caixa-teia",
"$XDG_CONFIG_HOME/caixa",
"$",
] {
let d = dep_with_fonte(DepSource::Path { caminho: s.into() });
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"{s:?} → {err:?}",
);
}
}
#[test]
fn validate_rejects_path_fonte_with_mid_path_dollar_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$bar/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellVariableExpansion { byte: b'$', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_tilde_fires_before_var_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "~/work/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoTildeExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "${WORKSPACE}/caixa-teia".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("${WORKSPACE}/caixa-teia"),
"diagnostic must quote the offending caminho: {rendered}",
);
assert!(
rendered.contains('$'),
"diagnostic must reference the dollar footgun: {rendered}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_nul() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa\0teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoControlChar {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoControlChar, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa\0teia");
assert_eq!(byte, 0x00);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_newline() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia\n".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoControlChar { byte, .. } = err else {
panic!("expected FonteCaminhoControlChar, got {err:?}");
};
assert_eq!(byte, 0x0A);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_carriage_return() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia\r".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoControlChar { byte, .. } = err else {
panic!("expected FonteCaminhoControlChar, got {err:?}");
};
assert_eq!(byte, 0x0D);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_tab() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa\tteia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoControlChar { byte, .. } = err else {
panic!("expected FonteCaminhoControlChar, got {err:?}");
};
assert_eq!(byte, 0x09);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_del() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa\x7fteia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoControlChar { byte, .. } = err else {
panic!("expected FonteCaminhoControlChar, got {err:?}");
};
assert_eq!(byte, 0x7F);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_high_bit_utf8() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../café-teia/foo".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_var_fires_before_control_char() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$HOME\n".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_leading_space_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: " ../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoLeadingWhitespace { nome, caminho } = err else {
panic!("expected FonteCaminhoLeadingWhitespace, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, " ../caixa-teia");
}
#[test]
fn validate_rejects_path_fonte_with_multiple_leading_spaces_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: " ../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoLeadingWhitespace { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_mid_path_space_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../my dir/caixa-teia".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_var_fires_before_leading_whitespace() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$VAR".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_leading_whitespace_fires_before_control_char() {
let d = dep_with_fonte(DepSource::Path {
caminho: " ../foo\n".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoLeadingWhitespace { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_leading_whitespace_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: " ../caixa-teia".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains(" ../caixa-teia"),
"diagnostic must quote the offending caminho: {rendered}",
);
assert!(
rendered.contains("space"),
"diagnostic must name the space footgun: {rendered}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_control_char() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd\n".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_leading_hyphen_caminho() {
for caminho in [
"-rf",
"-C",
"--upload-pack=cat /etc/passwd",
"--config=core.merge=ours",
"-",
] {
let d = dep_with_fonte(DepSource::Path {
caminho: caminho.into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoLeadingHyphen { nome, caminho: got } = err else {
panic!("expected FonteCaminhoLeadingHyphen for {caminho:?}, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(got, caminho);
}
}
#[test]
fn validate_accepts_path_fonte_with_mid_path_hyphen_caminho() {
for caminho in [
"../caixa-teia",
"../caixa-teia/-hidden",
"./my-lib",
"../foo-bar/baz",
] {
let d = dep_with_fonte(DepSource::Path {
caminho: caminho.into(),
});
d.validate()
.unwrap_or_else(|e| panic!("mid-path `-` caminho {caminho:?} must pass: {e:?}"));
}
}
#[test]
fn fonte_caminho_leading_whitespace_fires_before_leading_hyphen() {
let d = dep_with_fonte(DepSource::Path {
caminho: " -rf".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoLeadingWhitespace { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_leading_hyphen_fires_before_control_char() {
let d = dep_with_fonte(DepSource::Path {
caminho: "-rf\n".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoLeadingHyphen { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_leading_hyphen_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "--upload-pack=cat /etc/passwd".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("--upload-pack=cat /etc/passwd"),
"diagnostic must quote the offending caminho: {rendered}",
);
assert!(
rendered.contains("CLI-argument-injection"),
"diagnostic must name the CLI-argument-injection vector: {rendered}",
);
assert!(
rendered.contains("`-`"),
"diagnostic must name the offending byte: {rendered}",
);
}
#[test]
fn fonte_caminho_control_diagnostic_carries_offending_dep_caminho_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa\tteia".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa\tteia"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x09"),
"diagnostic must name the offending byte in hex: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_backslash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoBackslash { nome, caminho } = err else {
panic!("expected FonteCaminhoBackslash, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "..\\caixa-teia");
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_windows_drive_letter() {
let d = dep_with_fonte(DepSource::Path {
caminho: "C:\\work\\caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_trailing_backslash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia\\".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_legitimate_forward_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/foo/bar".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_control_char_fires_before_backslash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa\0teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_backslash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd\\foo".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_fires_before_backslash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$WORKSPACE\\caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("..\\caixa-teia"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains('\\'),
"diagnostic must reference the backslash footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoTrailingSlash { nome, caminho } = err else {
panic!("expected FonteCaminhoTrailingSlash, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia/");
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_bare_dot_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "./".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoTrailingSlash { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_consecutive_trailing_slashes() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia//".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoTrailingSlash { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_dotdot_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoTrailingSlash { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_internal_slashes() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/foo/bar".into(),
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_bare_dot() {
let d = dep_with_fonte(DepSource::Path {
caminho: ".".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_control_char_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_trailing_slash_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia/"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("trailing"),
"diagnostic must reference the trailing-slash footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_gt_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>build.log".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellRedirection {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellRedirection, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia>build.log");
assert_eq!(byte, b'>');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_lt_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia<input.lisp".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellRedirection { byte, .. } = err else {
panic!("expected FonteCaminhoShellRedirection, got {err:?}");
};
assert_eq!(byte, b'<');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_gt_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: ">../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_gt_append() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>>build.log".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/foo/bar".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia>build.log".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n>bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_redirection() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd>out".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo></".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_diagnostic_carries_offending_dep_caminho_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>build.log".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia>build.log"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x3e"),
"diagnostic must name the offending byte in hex: {rendered:?}",
);
assert!(
rendered.contains("redirection"),
"diagnostic must name the shell-redirection footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | grep foo".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellPipe { nome, caminho } = err else {
panic!("expected FonteCaminhoShellPipe, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia | grep foo");
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "|../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_pipe_or() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia||fallback".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia<input|tee".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'<', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia|tee".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n|bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_pipe() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd|tee".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo|tee/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | grep foo".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia | grep foo"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains('|'),
"diagnostic must reference the pipe footgun: {rendered:?}",
);
assert!(
rendered.contains("pipe"),
"diagnostic must name the shell-pipe footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; rm -rf build".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellSemicolon { nome, caminho } = err else {
panic!("expected FonteCaminhoShellSemicolon, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia; rm -rf build");
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: ";../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia;;next".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | tee; rm".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log; rm".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia;rm".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n;bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_semicolon() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd;rm".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo;rm/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; rm -rf build".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia; rm -rf build"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains(';'),
"diagnostic must reference the semicolon footgun: {rendered:?}",
);
assert!(
rendered.contains("command-separator"),
"diagnostic must name the shell-command-separator footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_ampersand() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & sleep 1".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellBackground { nome, caminho } = err else {
panic!("expected FonteCaminhoShellBackground, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia & sleep 1");
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_ampersand() {
let d = dep_with_fonte(DepSource::Path {
caminho: "&../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_ampersand() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia && make".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_ampersand() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_background() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; rm & sleep".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_background() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | tee & sleep".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_background() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log & sleep".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_background() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia & sleep".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_background() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n&sleep".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_background() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd & sleep".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo&sleep/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & sleep 1".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia & sleep 1"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains('&'),
"diagnostic must reference the ampersand footgun: {rendered:?}",
);
assert!(
rendered.contains("background") || rendered.contains("list-AND"),
"diagnostic must name the shell-background / logical-AND footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_backtick() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/`whoami`".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellCommandSubstitution { nome, caminho } = err else {
panic!("expected FonteCaminhoShellCommandSubstitution, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia/`whoami`");
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_backtick() {
let d = dep_with_fonte(DepSource::Path {
caminho: "`pwd`/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_trailing_backtick() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_balanced_backtick_pair() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`cat /etc/passwd`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_backtick() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & `sleep 1`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; `whoami`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | `tee log`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log `date`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia `whoami`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n`whoami`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd `whoami`".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_diagnostic_carries_offending_dep_and_caminho() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/`whoami`".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia/`whoami`"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains('`'),
"diagnostic must reference the backtick footgun: {rendered:?}",
);
assert!(
rendered.contains("command-substitution"),
"diagnostic must name the shell-command-substitution footgun: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_star_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellGlob {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellGlob, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia/*");
assert_eq!(byte, b'*');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_question_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo?".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellGlob { byte, .. } = err else {
panic!("expected FonteCaminhoShellGlob, got {err:?}");
};
assert_eq!(byte, b'?');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_star_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "*/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_star_recursive_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/**/foo".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_dotted_extension_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*.lisp".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`/*".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & ls /*".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; rm *".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | ls *".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log *".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia\\*".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n*".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_glob() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/*".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_glob_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo*/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_glob_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*.lisp".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia/*.lisp"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x2a"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("glob"),
"diagnostic must name the shell-glob footgun: {rendered:?}",
);
assert!(
rendered.contains("pathname-expansion"),
"diagnostic must reference the POSIX pathname-expansion vocabulary: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_modern_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/$(date)/build".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellSubshellGrouping {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellSubshellGrouping, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia/$(date)/build");
assert_eq!(byte, b'(');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_close_paren() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia)".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellSubshellGrouping { byte, .. } = err else {
panic!("expected FonteCaminhoShellSubshellGrouping, got {err:?}");
};
assert_eq!(byte, b')');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_open_paren() {
let d = dep_with_fonte(DepSource::Path {
caminho: "(cd foo)/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_balanced_subshell_grouping_pair() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../(pwd)/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_glob_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*(date)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`/$(date)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & (cd foo)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; (cd foo)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | (tee log)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log (cd foo)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia\\(cd foo)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n(cd bar)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/(cd foo)".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_expansion_fires_before_shell_subshell_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$(date)/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_subshell_grouping_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../(cd foo)/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_subshell_grouping_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/$(date)/build".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia/$(date)/build"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x28"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("subshell-grouping"),
"diagnostic must name the shell-subshell-grouping footgun: {rendered:?}",
);
assert!(
rendered.contains("command-substitution"),
"diagnostic must reference the `$(<cmd>)` command-substitution vocabulary: \
{rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_brace_expansion_fan_across_siblings() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{caixa-teia,caixa-helm}/build".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellBraceExpansion {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellBraceExpansion, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../{caixa-teia,caixa-helm}/build");
assert_eq!(byte, b'{');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_close_brace() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia}".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellBraceExpansion { byte, .. } = err else {
panic!("expected FonteCaminhoShellBraceExpansion, got {err:?}");
};
assert_eq!(byte, b'}');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_open_brace() {
let d = dep_with_fonte(DepSource::Path {
caminho: "{caixa-teia,caixa-helm}/build".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_uri_template_placeholder() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{{org}}/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_brace_range_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-v{1..10}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_subshell_grouping_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../(cd foo)/{a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_glob_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*{a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`/{a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & {a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; {a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | {tee,cat}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log {a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia\\{a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n{a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/{a,b}".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_expansion_fires_before_shell_brace_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "${ORG}/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_brace_expansion_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{caixa-teia,caixa-helm}/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_brace_expansion_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{caixa-teia,caixa-helm}/build".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../{caixa-teia,caixa-helm}/build"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x7b"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("brace-expansion"),
"diagnostic must name the shell-brace-expansion footgun: {rendered:?}",
);
assert!(
rendered.contains("URI Template"),
"diagnostic must reference the RFC-6570 URI-Template-placeholder vocabulary: \
{rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_bracket_glob_character_class() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-[a-z]/build".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellBracketExpansion {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellBracketExpansion, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-[a-z]/build");
assert_eq!(byte, b'[');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_close_bracket() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia]".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellBracketExpansion { byte, .. } = err else {
panic!("expected FonteCaminhoShellBracketExpansion, got {err:?}");
};
assert_eq!(byte, b']');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_open_bracket() {
let d = dep_with_fonte(DepSource::Path {
caminho: "[caixa-teia]/build".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBracketExpansion { byte: b'[', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_toml_inline_array() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../[\"a\", \"b\"]/caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBracketExpansion { byte: b'[', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_shell_test_builtin() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../[ -d caixa-teia ]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBracketExpansion { byte: b'[', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_brace_expansion_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{a,b}[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_subshell_grouping_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../(cd foo)/[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_glob_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`/[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & [ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; [ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | [tee]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log [ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia\\[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_expansion_fires_before_shell_bracket_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$DIR/[ch]".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_bracket_expansion_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../[a-z]/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBracketExpansion { byte: b'[', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_bracket_expansion_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-[a-z]/build".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-[a-z]/build"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x5b"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("bracket-expansion"),
"diagnostic must name the shell-bracket-expansion footgun: {rendered:?}",
);
assert!(
rendered.contains("glob-character-class"),
"diagnostic must reference the POSIX glob-character-class vocabulary: \
{rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_single_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "'../caixa-teia'".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellQuoteGrouping {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellQuoteGrouping, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "'../caixa-teia'");
assert_eq!(byte, b'\'');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "\"../caixa-teia\"".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellQuoteGrouping { byte, .. } = err else {
panic!("expected FonteCaminhoShellQuoteGrouping, got {err:?}");
};
assert_eq!(byte, b'"');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_double_quote() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../\"caixa-teia\"".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellQuoteGrouping { byte: b'"', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_yaml_flow_scalar_paste() {
let d = dep_with_fonte(DepSource::Path {
caminho: "path: \"../caixa-teia\"".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellQuoteGrouping { byte: b'"', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_bracket_expansion_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../[a-z]'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBracketExpansion { byte: b'[', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_brace_expansion_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{a,b}'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_subshell_grouping_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../(cd foo)/'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_glob_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`/'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia & 'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia; 'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia | 'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log 'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia\\'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_expansion_fires_before_shell_quote_grouping() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$DIR/'x'".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_quote_grouping_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../'caixa-teia'/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellQuoteGrouping { byte: b'\'', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_quote_grouping_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "'../caixa-teia'".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("'../caixa-teia'"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x27"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("quote-grouping"),
"diagnostic must name the shell-quote-grouping footgun: {rendered:?}",
);
assert!(
rendered.contains("string-literal"),
"diagnostic must reference the cross-config-DSL string-literal-delimiter \
vocabulary: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_shell_comment_lead() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia # legacy sibling".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellComment {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellComment, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia # legacy sibling");
assert_eq!(byte, b'#');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_yaml_comment_paste() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia # pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellComment { byte: b'#', .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_url_fragment_anchor() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia#readme".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellComment { byte: b'#', .. }),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "#../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellComment { byte: b'#', .. }),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_quote_grouping_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../'x'#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellQuoteGrouping { byte: b'\'', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_bracket_expansion_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../[a-z]#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBracketExpansion { byte: b'[', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_brace_expansion_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../{a,b}#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_subshell_grouping_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../(cd foo)#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_glob_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/*#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellGlob { byte: b'*', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_command_substitution_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../`whoami`#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellCommandSubstitution { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_background_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia&pin#tail".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellBackground { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_semicolon_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia;pin#tail".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellSemicolon { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_pipe_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia|pin#tail".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellPipe { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_redirection_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia>log#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellRedirection { byte: b'>', .. }
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa-teia#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo\n#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/foo#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_expansion_fires_before_shell_comment() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$DIR/foo#pin".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_comment_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia#pin/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellComment { byte: b'#', .. },),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_comment_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia#readme".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia#readme"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x23"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("shell-comment") || rendered.contains("comment-lead"),
"diagnostic must name the shell-comment footgun: {rendered:?}",
);
assert!(
rendered.contains("fragment") || rendered.contains("URL-fragment"),
"diagnostic must reference the URL-fragment-identifier vocabulary: \
{rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_url_percent_encoded_space() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa%20teia".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoUrlPercentEncoding {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoUrlPercentEncoding, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa%20teia");
assert_eq!(byte, b'%');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_url_percent_encoded_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa%2Fteia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoUrlPercentEncoding { byte: b'%', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_lone_percent() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia%foo".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoUrlPercentEncoding { byte: b'%', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_leading_yaml_directive() {
let d = dep_with_fonte(DepSource::Path {
caminho: "%YAML/../caixa-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoUrlPercentEncoding { byte: b'%', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_printf_format_specifier() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-%s-teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoUrlPercentEncoding { byte: b'%', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_percent() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_comment_fires_before_url_percent_encoding() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia#pin%20".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoShellComment { byte: b'#', .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_quote_grouping_fires_before_url_percent_encoding() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../'x'%20teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellQuoteGrouping { byte: b'\'', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_backslash_fires_before_url_percent_encoding() {
let d = dep_with_fonte(DepSource::Path {
caminho: "..\\caixa%20teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoBackslash { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_control_char_fires_before_url_percent_encoding() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa\0%20teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoControlChar { byte: 0x00, .. },),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_absolute_fires_before_url_percent_encoding() {
let d = dep_with_fonte(DepSource::Path {
caminho: "/etc/passwd%20".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoAbsolute { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_var_expansion_fires_before_url_percent_encoding() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$HOME/caixa%20teia".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_url_percent_encoding_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa%20teia/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoUrlPercentEncoding { byte: b'%', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_url_percent_encoding_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa%20teia".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa%20teia"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x25"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("percent-encoding") || rendered.contains("percent-encoded"),
"diagnostic must name the URL-percent-encoding-escape footgun: {rendered:?}",
);
assert!(
rendered.contains("printf") || rendered.contains("format-specifier"),
"diagnostic must reference the printf-format-specifier vocabulary: \
{rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_var_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$HOME/bar".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellVariableExpansion {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellVariableExpansion, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../foo$HOME/bar");
assert_eq!(byte, b'$');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_braced_var_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo${WORKSPACE}/bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellBraceExpansion { byte: b'{', .. }
| DepError::FonteCaminhoShellVariableExpansion { byte: b'$', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_command_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$(whoami)/bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellSubshellGrouping { byte: b'(', .. }
| DepError::FonteCaminhoShellVariableExpansion { byte: b'$', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_embedded_positional_parameter() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$1/bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellVariableExpansion { byte: b'$', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_dollar() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_var_expansion_fires_before_shell_variable_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "$HOME/foo$WORKSPACE/bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteCaminhoVarExpansion { .. }),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_url_percent_encoding_fires_before_shell_variable_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo%20$HOME/bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoUrlPercentEncoding { byte: b'%', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_variable_expansion_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$HOME/bar/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellVariableExpansion { byte: b'$', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_variable_expansion_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$HOME/bar".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../foo$HOME/bar"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x24"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("variable-expansion") || rendered.contains("variable expansion"),
"diagnostic must name the shell-variable-expansion footgun: {rendered:?}",
);
assert!(
rendered.contains("command-substitution") || rendered.contains("command substitution"),
"diagnostic must reference the command-substitution vocabulary: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_shell_history_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia!sudo".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellHistoryExpansion {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellHistoryExpansion, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../caixa-teia!sudo");
assert_eq!(byte, b'!');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_double_bang_history_reference() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo!!/bar".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistoryExpansion { byte: b'!', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_trailing_enthusiasm_bang() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia!".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistoryExpansion { byte: b'!', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_bang() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub-dir.v2".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_variable_expansion_fires_before_shell_history_expansion() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo$HOME/bar!sudo".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellVariableExpansion { byte: b'$', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_history_expansion_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia!sudo/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistoryExpansion { byte: b'!', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_history_expansion_diagnostic_carries_offending_dep_caminho_and_byte() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia!sudo".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../caixa-teia!sudo"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x21"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("history-expansion") || rendered.contains("history expansion"),
"diagnostic must name the shell-history-expansion footgun: {rendered:?}",
);
assert!(
rendered.contains("bang"),
"diagnostic must reference the bang-operator vocabulary: {rendered:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_shell_history_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo^bad^good".into(),
});
let err = d.validate().unwrap_err();
let DepError::FonteCaminhoShellHistorySubstitution {
nome,
caminho,
byte,
} = err
else {
panic!("expected FonteCaminhoShellHistorySubstitution, got {err:?}");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(caminho, "../foo^bad^good");
assert_eq!(byte, b'^');
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_regex_negation_anchor() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo/^archived".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistorySubstitution { byte: b'^', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_rejects_path_fonte_with_caminho_carrying_trailing_history_substitution_open() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia^".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistorySubstitution { byte: b'^', .. },
),
"got {err:?}",
);
}
#[test]
fn validate_accepts_path_fonte_with_caminho_carrying_no_caret() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../caixa-teia/sub_v2.rc".into(),
});
d.validate().unwrap();
}
#[test]
fn fonte_caminho_shell_history_expansion_fires_before_shell_history_substitution() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo!sudo^bad^good".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistoryExpansion { byte: b'!', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_history_substitution_fires_before_trailing_slash() {
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo^bad^good/".into(),
});
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::FonteCaminhoShellHistorySubstitution { byte: b'^', .. },
),
"got {err:?}",
);
}
#[test]
fn fonte_caminho_shell_history_substitution_diagnostic_carries_offending_dep_caminho_and_byte()
{
let d = dep_with_fonte(DepSource::Path {
caminho: "../foo^bad^good".into(),
});
let rendered = d.validate().unwrap_err().to_string();
assert!(
rendered.contains("caixa-teia"),
"diagnostic must name the offending dep: {rendered}",
);
assert!(
rendered.contains("../foo^bad^good"),
"diagnostic must quote the offending caminho verbatim: {rendered:?}",
);
assert!(
rendered.contains("0x5e") || rendered.contains("0x5E"),
"diagnostic must surface the offending byte hex: {rendered:?}",
);
assert!(
rendered.contains("history-substitution") || rendered.contains("history substitution"),
"diagnostic must name the shell-history-substitution footgun: {rendered:?}",
);
assert!(
rendered.contains("unwise"),
"diagnostic must reference the RFC-3986 'unwise' set vocabulary: {rendered:?}",
);
}
#[test]
fn fonte_repo_empty_fires_before_pin_missing() {
let d = dep_with_fonte(DepSource::Git {
repo: String::new(),
tag: None,
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::FonteRepoEmpty { .. }),
"got {err:?}"
);
}
#[test]
fn fonte_pin_missing_fires_before_pin_empty() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: None,
});
assert!(matches!(
d.validate().unwrap_err(),
DepError::FontePinMissing { .. }
));
}
#[test]
fn nome_empty_takes_precedence_over_fonte_invalid() {
let mut d = dep_with_fonte(DepSource::Git {
repo: String::new(),
tag: None,
rev: None,
branch: None,
});
d.nome = String::new();
assert_eq!(d.validate().unwrap_err(), DepError::NomeEmpty);
}
#[test]
fn versao_invalid_takes_precedence_over_fonte_invalid() {
let mut d = dep_with_fonte(DepSource::Git {
repo: String::new(),
tag: None,
rev: None,
branch: None,
});
d.versao = "v0.1".into();
let err = d.validate().unwrap_err();
assert!(
matches!(err, DepError::VersaoInvalid { ref nome, .. } if nome == "caixa-teia"),
"got {err:?}"
);
}
#[test]
fn fonte_invalid_diagnostic_carries_offending_nome() {
for (case, fonte) in [
(
"repo-empty",
DepSource::Git {
repo: String::new(),
tag: Some("v1".into()),
rev: None,
branch: None,
},
),
(
"repo-shape",
DepSource::Git {
repo: "github:p/x ".into(),
tag: Some("v1".into()),
rev: None,
branch: None,
},
),
(
"pin-missing",
DepSource::Git {
repo: "github:p/x".into(),
tag: None,
rev: None,
branch: None,
},
),
(
"pin-ambiguous",
DepSource::Git {
repo: "github:p/x".into(),
tag: Some("v1".into()),
rev: None,
branch: Some("main".into()),
},
),
(
"pin-empty",
DepSource::Git {
repo: "github:p/x".into(),
tag: Some(String::new()),
rev: None,
branch: None,
},
),
(
"caminho-empty",
DepSource::Path {
caminho: String::new(),
},
),
(
"caminho-absolute",
DepSource::Path {
caminho: "/home/me/work/caixa-teia".into(),
},
),
] {
let d = dep_with_fonte(fonte);
let msg = d
.validate()
.expect_err(&format!("{case}: expected fonte error"))
.to_string();
assert!(
msg.contains("\"caixa-teia\""),
"{case}: diagnostic must quote the offending :nome verbatim, got {msg:?}"
);
}
}
#[test]
fn validate_rejects_git_fonte_with_tag_carrying_trailing_space() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0 ".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
nome,
pin,
value,
reason,
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(pin, ":tag");
assert_eq!(value, "v0.1.0 ");
assert!(
reason.contains("whitespace"),
"reason must surface the whitespace arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_tag_carrying_lock_suffix() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0.lock".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":tag");
assert_eq!(value, "v0.1.0.lock");
assert!(
reason.contains(".lock"),
"reason must surface the .lock arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_branch_carrying_embedded_space() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: Some("feature/foo bar".into()),
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":branch");
assert_eq!(value, "feature/foo bar");
assert!(
reason.contains("whitespace"),
"reason must surface the whitespace arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_branch_carrying_qualified_prefix() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: Some("refs/heads/main".into()),
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":branch");
assert_eq!(value, "refs/heads/main");
assert!(
reason.contains("fully-qualified"),
"reason must surface the qualified-prefix arm, got {reason:?}"
);
assert!(
reason.contains("\"main\""),
"reason must quote the leaf the author probably meant, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_tag_carrying_qualified_prefix() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("refs/tags/v0.1.0".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":tag");
assert_eq!(value, "refs/tags/v0.1.0");
assert!(
reason.contains("fully-qualified"),
"reason must surface the qualified-prefix arm, got {reason:?}"
);
assert!(
reason.contains("\"v0.1.0\""),
"reason must quote the leaf the author probably meant, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_branch_named_at() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: Some("@".into()),
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape { pin, value, .. } = err else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":branch");
assert_eq!(value, "@");
}
#[test]
fn validate_rejects_git_fonte_with_tag_carrying_double_dot() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("../escape".into()),
rev: None,
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape { pin, value, .. } = err else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":tag");
assert_eq!(value, "../escape");
}
#[test]
fn validate_accepts_git_fonte_with_hierarchical_branch() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: None,
branch: Some("feature/checkout-rewrite".into()),
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_git_fonte_with_prerelease_tag() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0-alpha.1".into()),
rev: None,
branch: None,
});
d.validate().unwrap();
}
#[test]
fn validate_rejects_git_fonte_with_rev_carrying_refname_unfriendly_value() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("c0ffee:notarefname".into()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
nome,
pin,
value,
reason,
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(nome, "caixa-teia");
assert_eq!(pin, ":rev");
assert_eq!(value, "c0ffee:notarefname");
assert!(
!reason.is_empty(),
"FontePinShape `reason` must carry the predicate's wording verbatim"
);
}
#[test]
fn validate_accepts_git_fonte_with_rev_full_sha1() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("0123456789abcdef0123456789abcdef01234567".into()),
branch: None,
});
d.validate().unwrap();
}
#[test]
fn validate_accepts_git_fonte_with_rev_full_sha256() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into()),
branch: None,
});
d.validate().unwrap();
}
#[test]
fn validate_rejects_git_fonte_with_rev_abbreviated_prefix() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("c0ffee0".into()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":rev");
assert_eq!(value, "c0ffee0");
assert!(
reason.contains("abbreviated") || reason.contains("ambiguous"),
"reason must surface the abbreviation arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_rev_uppercase_hex() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("DEADBEEFCAFEBABE0123456789ABCDEF01234567".into()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":rev");
assert_eq!(value, "DEADBEEFCAFEBABE0123456789ABCDEF01234567");
assert!(
reason.contains("uppercase"),
"reason must surface the uppercase arm, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_rev_refname_value() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("main".into()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":rev");
assert_eq!(value, "main");
assert!(
!reason.is_empty(),
"FontePinShape reason must be non-empty for refname-shaped :rev"
);
}
#[test]
fn validate_rejects_git_fonte_with_rev_tag_shaped_value() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some("v0.1.0".into()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":rev");
assert_eq!(value, "v0.1.0");
assert!(
!reason.is_empty(),
"FontePinShape reason must be non-empty for tag-shaped :rev"
);
}
#[test]
fn validate_rejects_git_fonte_with_rev_too_long() {
let too_long: String = "0".repeat(41);
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some(too_long.clone()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":rev");
assert_eq!(value, too_long);
assert!(
reason.contains("41"),
"reason must surface the offending length verbatim, got {reason:?}"
);
}
#[test]
fn validate_rejects_git_fonte_with_rev_carrying_whitespace() {
let with_space = "0123456789abcdef0123456789abcdef01234567 ".to_string();
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: None,
rev: Some(with_space.clone()),
branch: None,
});
let err = d.validate().unwrap_err();
let DepError::FontePinShape {
pin, value, reason, ..
} = err
else {
panic!("expected FontePinShape, got other variant");
};
assert_eq!(pin, ":rev");
assert_eq!(value, with_space);
assert!(
!reason.is_empty(),
"FontePinShape reason must be non-empty for whitespace-bearing :rev"
);
}
#[test]
fn fonte_pin_shape_diagnostic_carries_offending_nome_pin_value_for_rev() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:p/x".into(),
tag: None,
rev: Some("not-a-sha".into()),
branch: None,
});
let msg = d
.validate()
.expect_err(":rev: expected FontePinShape")
.to_string();
assert!(
msg.contains("\"caixa-teia\""),
":rev: diagnostic must quote the offending :nome verbatim, got {msg:?}"
);
assert!(
msg.contains(":rev"),
":rev: diagnostic must name the offending pin axis, got {msg:?}"
);
assert!(
msg.contains("not-a-sha"),
":rev: diagnostic must quote the offending value verbatim, got {msg:?}"
);
}
#[test]
fn fonte_pin_empty_fires_before_pin_shape() {
let d = dep_with_fonte(DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some(String::new()),
rev: None,
branch: None,
});
assert!(matches!(
d.validate().unwrap_err(),
DepError::FontePinEmpty { ref pin, .. } if pin == ":tag"
));
}
#[test]
fn fonte_pin_shape_fires_after_repo_empty() {
let d = dep_with_fonte(DepSource::Git {
repo: String::new(),
tag: Some("v0.1.0 ".into()),
rev: None,
branch: None,
});
assert!(matches!(
d.validate().unwrap_err(),
DepError::FonteRepoEmpty { .. }
));
}
#[test]
fn fonte_pin_shape_diagnostic_carries_offending_nome_pin_value() {
for (pin_label, fonte) in [
(
":tag",
DepSource::Git {
repo: "github:p/x".into(),
tag: Some("v0.1.0~1".into()),
rev: None,
branch: None,
},
),
(
":branch",
DepSource::Git {
repo: "github:p/x".into(),
tag: None,
rev: None,
branch: Some("feature/foo*".into()),
},
),
] {
let d = dep_with_fonte(fonte);
let msg = d
.validate()
.expect_err(&format!("{pin_label}: expected FontePinShape"))
.to_string();
assert!(
msg.contains("\"caixa-teia\""),
"{pin_label}: diagnostic must quote the offending :nome verbatim, got {msg:?}"
);
assert!(
msg.contains(pin_label),
"{pin_label}: diagnostic must name the offending pin axis, got {msg:?}"
);
}
}
#[test]
fn git_source_json_round_trip() {
let src = DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
};
let s = serde_json::to_string(&src).unwrap();
assert!(s.contains(&format!(
r#""{tipo}":"{git}""#,
tipo = crate::render::DEP_SOURCE_KEY_TIPO,
git = crate::render::DEP_SOURCE_TIPO_GIT,
)));
assert!(s.contains(r#""repo":"github:pleme-io/caixa-teia""#));
assert!(s.contains(r#""tag":"v0.1.0""#));
assert!(!s.contains("rev"));
assert!(!s.contains("branch"));
let round: DepSource = serde_json::from_str(&s).unwrap();
assert_eq!(round, src);
}
#[test]
fn dep_source_git_serde_keys_match_lifted_dep_source_key_consts() {
let src = DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
};
let json = serde_json::to_value(&src).unwrap();
let obj = json.as_object().expect("Git serializes as a JSON object");
assert_eq!(
obj.get(crate::render::DEP_SOURCE_KEY_TIPO)
.and_then(serde_json::Value::as_str),
Some(crate::render::DEP_SOURCE_TIPO_GIT),
"DepSource::Git must serialize the tipo discriminator at DEP_SOURCE_KEY_TIPO \
with value DEP_SOURCE_TIPO_GIT — attribute drift or variant rename \
detected in {json}"
);
}
#[test]
fn dep_source_path_serde_keys_match_lifted_dep_source_key_consts() {
let src = DepSource::Path {
caminho: "../caixa-teia".into(),
};
let json = serde_json::to_value(&src).unwrap();
let obj = json.as_object().expect("Path serializes as a JSON object");
assert_eq!(
obj.get(crate::render::DEP_SOURCE_KEY_TIPO)
.and_then(serde_json::Value::as_str),
Some(crate::render::DEP_SOURCE_TIPO_PATH),
"DepSource::Path must serialize the tipo discriminator at DEP_SOURCE_KEY_TIPO \
with value DEP_SOURCE_TIPO_PATH — attribute drift or variant rename \
detected in {json}"
);
}
#[test]
fn dep_source_key_consts_are_pairwise_distinct() {
assert_ne!(
crate::render::DEP_SOURCE_KEY_TIPO,
crate::render::DEP_SOURCE_TIPO_GIT,
);
assert_ne!(
crate::render::DEP_SOURCE_KEY_TIPO,
crate::render::DEP_SOURCE_TIPO_PATH,
);
assert_ne!(
crate::render::DEP_SOURCE_TIPO_GIT,
crate::render::DEP_SOURCE_TIPO_PATH,
);
}
#[test]
fn dep_source_tipo_variant_consts_are_ascii_lowercase_shape() {
for (label, s) in [
("DEP_SOURCE_TIPO_GIT", crate::render::DEP_SOURCE_TIPO_GIT),
("DEP_SOURCE_TIPO_PATH", crate::render::DEP_SOURCE_TIPO_PATH),
] {
assert!(!s.is_empty(), "{label} must not be empty");
assert!(
s.bytes().all(|b| b.is_ascii_lowercase()),
"{label} must be ASCII-lowercase-only (matching \
rename_all = \"lowercase\"), got {s:?}",
);
}
}
fn dep_with_features(features: &[&str]) -> Dep {
Dep {
nome: "caixa-teia".into(),
versao: "^0.1".into(),
fonte: None,
opcional: false,
caracteristicas: features.iter().map(|s| (*s).into()).collect(),
}
}
#[test]
fn validate_rejects_empty_caracteristica() {
let d = dep_with_features(&[""]);
assert!(
matches!(d.validate().unwrap_err(), DepError::CaracteristicaEmpty { ref nome } if nome == "caixa-teia"),
"expected CaracteristicaEmpty, got {:?}",
d.validate(),
);
}
#[test]
fn validate_rejects_duplicate_caracteristica() {
let d = dep_with_features(&["http", "http"]);
assert!(
matches!(
d.validate().unwrap_err(),
DepError::CaracteristicaDuplicate { ref nome, ref caracteristica }
if nome == "caixa-teia" && caracteristica == "http"
),
"expected CaracteristicaDuplicate, got {:?}",
d.validate(),
);
}
#[test]
fn validate_accepts_distinct_caracteristicas() {
dep_with_features(&["http", "json", "tls"])
.validate()
.unwrap();
}
#[test]
fn validate_accepts_single_caracteristica() {
dep_with_features(&["http"]).validate().unwrap();
}
#[test]
fn validate_accepts_empty_caracteristicas_list() {
Dep::simple("caixa-teia", "^0.1").validate().unwrap();
assert!(dep_with_features(&[]).validate().is_ok());
}
#[test]
fn validate_caracteristica_empty_fires_before_duplicate() {
let d = dep_with_features(&["", "http", "http"]);
assert!(matches!(
d.validate().unwrap_err(),
DepError::CaracteristicaEmpty { .. }
));
}
#[test]
fn validate_caracteristica_duplicate_first_collision_determinism() {
let d = dep_with_features(&["http", "http", "http"]);
assert!(matches!(
d.validate().unwrap_err(),
DepError::CaracteristicaDuplicate { ref caracteristica, .. } if caracteristica == "http"
));
}
#[test]
fn validate_per_entry_shape_fires_before_caracteristicas() {
let d = Dep {
nome: "Caixa-Teia".into(), versao: "^0.1".into(),
fonte: None,
opcional: false,
caracteristicas: vec!["http".into(), "http".into()],
};
assert!(matches!(
d.validate().unwrap_err(),
DepError::NomeInvalid { .. }
));
}
#[test]
fn validate_rejects_caracteristica_with_leading_plus() {
let d = dep_with_features(&["+http"]);
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::CaracteristicaInvalid { ref nome, ref caracteristica, .. }
if nome == "caixa-teia" && caracteristica == "+http"
),
"expected CaracteristicaInvalid, got {err:?}"
);
}
#[test]
fn validate_rejects_caracteristica_with_leading_hyphen() {
let d = dep_with_features(&["-json"]);
let err = d.validate().unwrap_err();
assert!(
matches!(
err,
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == "-json"
),
"expected CaracteristicaInvalid, got {err:?}"
);
}
#[test]
fn validate_rejects_caracteristica_with_leading_dot() {
let d = dep_with_features(&[".feat"]);
let err = d.validate().unwrap_err();
assert!(matches!(
err,
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == ".feat"
));
}
#[test]
fn validate_rejects_caracteristica_with_whitespace() {
let d = dep_with_features(&["http feature"]);
let err = d.validate().unwrap_err();
assert!(matches!(
err,
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == "http feature"
));
}
#[test]
fn validate_rejects_caracteristica_with_comma() {
let d = dep_with_features(&["http,json"]);
let err = d.validate().unwrap_err();
assert!(matches!(
err,
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == "http,json"
));
}
#[test]
fn validate_rejects_caracteristica_with_slash() {
let d = dep_with_features(&["http/json"]);
let err = d.validate().unwrap_err();
assert!(matches!(
err,
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == "http/json"
));
}
#[test]
fn validate_rejects_caracteristica_with_non_ascii() {
let d = dep_with_features(&["caf\u{e9}"]);
let err = d.validate().unwrap_err();
assert!(matches!(
err,
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == "caf\u{e9}"
));
}
#[test]
fn validate_rejects_caracteristica_with_control_character() {
let d = dep_with_features(&["http\njson"]);
let err = d.validate().unwrap_err();
assert!(matches!(err, DepError::CaracteristicaInvalid { .. }));
}
#[test]
fn validate_accepts_canonical_caracteristicas_shapes() {
for s in [
"http",
"json",
"derive",
"serde_json",
"runtime-tokio",
"tokio.full",
"v0.1",
"http+json",
"_internal",
"__private",
"default",
"rt-multi-thread",
"feat.v2",
] {
let d = dep_with_features(&[s]);
d.validate().unwrap_or_else(|e| {
panic!("canonical Cargo feature name {s:?} must pass validate: {e:?}")
});
}
}
#[test]
fn validate_caracteristica_empty_fires_before_invalid() {
let d = dep_with_features(&["", "+http"]);
assert!(matches!(
d.validate().unwrap_err(),
DepError::CaracteristicaEmpty { .. }
));
}
#[test]
fn validate_caracteristica_invalid_fires_before_duplicate() {
let d = dep_with_features(&["+http", "+http"]);
assert!(matches!(
d.validate().unwrap_err(),
DepError::CaracteristicaInvalid { ref caracteristica, .. } if caracteristica == "+http"
));
}
#[test]
fn validate_rejects_caracteristica_at_65_byte_boundary() {
let max_ok = "a".repeat(64);
dep_with_features(&[&max_ok])
.validate()
.unwrap_or_else(|e| panic!("64-byte feature name must pass: {e:?}"));
let too_long = "a".repeat(65);
let d = dep_with_features(&[&too_long]);
assert!(matches!(
d.validate().unwrap_err(),
DepError::CaracteristicaInvalid { .. }
));
}
#[test]
fn validate_no_self_dep_rejects_self_in_deps() {
let deps = vec![
Dep::simple("caixa-teia", "^0.1"),
Dep::simple("orquestra", "^0.1"),
];
let err = validate_no_self_dep(&deps, &[], "orquestra").unwrap_err();
assert!(
matches!(err, DepError::DepIsSelf { ref nome, list } if nome == "orquestra" && list == crate::render::DEP_AUTHOR_KEY_DEPS),
"got {err:?}"
);
}
#[test]
fn validate_no_self_dep_rejects_self_in_deps_dev() {
let deps_dev = vec![Dep::simple("orquestra", "^0.1")];
let err = validate_no_self_dep(&[], &deps_dev, "orquestra").unwrap_err();
assert!(
matches!(err, DepError::DepIsSelf { ref nome, list } if nome == "orquestra" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
"got {err:?}"
);
}
#[test]
fn validate_no_self_dep_deps_fires_before_deps_dev() {
let deps = vec![Dep::simple("orquestra", "^0.1")];
let deps_dev = vec![Dep::simple("orquestra", "^0.2")];
let err = validate_no_self_dep(&deps, &deps_dev, "orquestra").unwrap_err();
assert!(
matches!(err, DepError::DepIsSelf { ref nome, list } if nome == "orquestra" && list == crate::render::DEP_AUTHOR_KEY_DEPS),
"got {err:?}"
);
}
#[test]
fn validate_no_self_dep_accepts_distinct_names() {
let deps = vec![
Dep::simple("caixa-teia", "^0.1"),
Dep::simple("caixa-arch", "^0.1"),
];
let deps_dev = vec![Dep::simple("caixa-test", "^0.1")];
validate_no_self_dep(&deps, &deps_dev, "orquestra").unwrap();
}
#[test]
fn validate_no_self_dep_empty_lists_pass() {
validate_no_self_dep(&[], &[], "orquestra").unwrap();
}
#[test]
fn validate_no_self_dep_diagnostic_carries_offending_list_and_nome() {
let deps_dev = vec![Dep::simple("orquestra", "^0.1")];
let rendered = validate_no_self_dep(&[], &deps_dev, "orquestra")
.unwrap_err()
.to_string();
assert!(
rendered.contains(crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
"diagnostic must name the offending list tag: {rendered}",
);
assert!(
rendered.contains("orquestra"),
"diagnostic must quote the parent caixa name: {rendered}",
);
assert!(
rendered.contains(":bibliotecas"),
"diagnostic must point at the corrective code-surface slot: {rendered}",
);
}
#[test]
fn validate_no_self_dep_accepts_coincidental_substring_match() {
let deps = vec![Dep::simple("orquestra-helper", "^0.1")];
validate_no_self_dep(&deps, &[], "orquestra").unwrap();
}
#[test]
fn dep_author_key_consts_pin_canonical_kebab_case_labels() {
assert_eq!(crate::render::DEP_AUTHOR_KEY_DEPS, ":deps");
assert_eq!(crate::render::DEP_AUTHOR_KEY_DEPS_DEV, ":deps-dev");
}
#[test]
fn dep_author_key_consts_are_pairwise_distinct() {
assert_ne!(
crate::render::DEP_AUTHOR_KEY_DEPS,
crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
"DEP_AUTHOR_KEY_{{DEPS,DEPS_DEV}} consts must be pairwise-distinct \
so a `DepError::DuplicateNome {{ list: … }}` diagnostic \
self-locates the offending block in the author's caixa.lisp",
);
}
#[test]
fn validate_no_self_dep_routes_through_lifted_dep_author_key_consts() {
let deps = vec![Dep::simple("orquestra", "^0.1")];
let err = validate_no_self_dep(&deps, &[], "orquestra").unwrap_err();
let DepError::DepIsSelf { list, .. } = err else {
panic!("expected DepIsSelf from :deps walk");
};
assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS);
let deps_dev = vec![Dep::simple("orquestra", "^0.1")];
let err = validate_no_self_dep(&[], &deps_dev, "orquestra").unwrap_err();
let DepError::DepIsSelf { list, .. } = err else {
panic!("expected DepIsSelf from :deps-dev walk");
};
assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS_DEV);
}
#[test]
fn dep_nome_returns_declared_nome_across_fonte_shapes() {
assert_eq!(Dep::simple("caixa-teia", "^0.1").nome(), "caixa-teia");
assert_eq!(
Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0").nome(),
"caixa-teia",
);
assert_eq!(
Dep {
nome: "caixa-teia".to_string(),
versao: "0.1.0".to_string(),
fonte: Some(DepSource::Path {
caminho: "../caixa-teia".to_string(),
}),
opcional: false,
caracteristicas: Vec::new(),
}
.nome(),
"caixa-teia",
);
assert_eq!(Dep::simple("", "^0.1").nome(), "");
}
#[test]
fn dep_nome_is_by_borrow_pointer_identity() {
let d = Dep::simple("caixa-teia", "^0.1");
assert!(std::ptr::eq(d.nome().as_ptr(), d.nome.as_ptr()));
}
#[test]
fn dep_versao_requirement_returns_declared_versao_across_fonte_shapes() {
assert_eq!(
Dep::simple("caixa-teia", "^0.1").versao_requirement(),
"^0.1",
);
assert_eq!(
Dep::git(
"caixa-teia",
"~0.1.2",
"github:pleme-io/caixa-teia",
"v0.1.0"
)
.versao_requirement(),
"~0.1.2",
);
assert_eq!(
Dep {
nome: "caixa-teia".to_string(),
versao: "0.1.0".to_string(),
fonte: Some(DepSource::Path {
caminho: "../caixa-teia".to_string(),
}),
opcional: false,
caracteristicas: Vec::new(),
}
.versao_requirement(),
"0.1.0",
);
assert_eq!(Dep::simple("caixa-teia", "*").versao_requirement(), "*");
assert_eq!(Dep::simple("caixa-teia", "").versao_requirement(), "");
}
#[test]
fn dep_versao_requirement_is_by_borrow_pointer_identity() {
let d = Dep::simple("caixa-teia", "^0.1");
assert!(std::ptr::eq(
d.versao_requirement().as_ptr(),
d.versao.as_ptr(),
));
}
#[test]
fn dep_validate_reads_requirement_through_accessor() {
Dep::simple("caixa-teia", "^0.1").validate().unwrap();
let err = Dep::simple("caixa-teia", "v0.1").validate().unwrap_err();
assert!(
matches!(
&err,
DepError::VersaoInvalid {
nome,
versao,
..
} if nome == "caixa-teia" && versao == "v0.1",
),
"expected VersaoInvalid quoting the accessor-projected requirement, got {err:?}",
);
let err = Dep::simple("caixa-teia", "").validate().unwrap_err();
assert!(
matches!(
&err,
DepError::VersaoEmpty { nome } if nome == "caixa-teia",
),
"expected VersaoEmpty from the empty-first arm, got {err:?}",
);
}
#[test]
fn dep_fonte_returns_declared_source_across_shapes() {
assert!(Dep::simple("caixa-teia", "^0.1").fonte().is_none());
let git = Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0");
match git.fonte() {
Some(DepSource::Git {
repo,
tag,
rev,
branch,
}) => {
assert_eq!(repo, "github:pleme-io/caixa-teia");
assert_eq!(tag.as_deref(), Some("v0.1.0"));
assert!(rev.is_none());
assert!(branch.is_none());
}
other => panic!("expected explicit git :fonte, got {other:?}"),
}
let path = Dep {
nome: "caixa-teia".to_string(),
versao: "0.1.0".to_string(),
fonte: Some(DepSource::Path {
caminho: "../caixa-teia".to_string(),
}),
opcional: false,
caracteristicas: Vec::new(),
};
match path.fonte() {
Some(DepSource::Path { caminho }) => {
assert_eq!(caminho, "../caixa-teia");
}
other => panic!("expected explicit path :fonte, got {other:?}"),
}
}
#[test]
fn dep_fonte_is_by_borrow_pointer_identity() {
let d = Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0");
let accessed = d.fonte().expect("git :fonte present") as *const DepSource;
let raw = d.fonte.as_ref().expect("git :fonte present") as *const DepSource;
assert!(std::ptr::eq(accessed, raw));
}
#[test]
fn dep_validate_reads_fonte_through_accessor() {
Dep::simple("caixa-teia", "^0.1").validate().unwrap();
Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0")
.validate()
.unwrap();
let bad = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: Some(DepSource::Git {
repo: String::new(),
tag: Some("v0.1.0".to_string()),
rev: None,
branch: None,
}),
opcional: false,
caracteristicas: Vec::new(),
};
let err = bad.validate().unwrap_err();
assert!(
matches!(
&err,
DepError::FonteRepoEmpty { nome } if nome == "caixa-teia",
),
"expected FonteRepoEmpty from the accessor-routed :fonte gate, got {err:?}",
);
}
#[test]
fn validate_no_self_dep_reads_parent_equality_through_accessor() {
let deps = vec![Dep::simple("orquestra", "^0.1")];
let err = validate_no_self_dep(&deps, &[], "orquestra").unwrap_err();
assert!(matches!(
err,
DepError::DepIsSelf {
ref nome,
list,
} if nome == "orquestra" && list == crate::render::DEP_AUTHOR_KEY_DEPS,
));
let deps_dev = vec![Dep::simple("orquestra", "^0.1")];
let err = validate_no_self_dep(&[], &deps_dev, "orquestra").unwrap_err();
assert!(matches!(
err,
DepError::DepIsSelf {
ref nome,
list,
} if nome == "orquestra" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
));
let deps = vec![Dep::simple("caixa-teia", "^0.1")];
validate_no_self_dep(&deps, &[], "orquestra").unwrap();
}
#[test]
fn dep_caracteristicas_returns_declared_features_across_shapes() {
assert!(
Dep::simple("caixa-teia", "^0.1")
.caracteristicas()
.is_empty(),
);
let one = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: false,
caracteristicas: vec!["http".to_string()],
};
assert_eq!(one.caracteristicas(), &["http".to_string()]);
let two = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: false,
caracteristicas: vec!["http".to_string(), "json".to_string()],
};
assert_eq!(
two.caracteristicas(),
&["http".to_string(), "json".to_string()],
);
}
#[test]
fn dep_caracteristicas_is_by_borrow_pointer_identity() {
let d = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: false,
caracteristicas: vec!["http".to_string(), "json".to_string()],
};
assert!(std::ptr::eq(
d.caracteristicas().as_ptr(),
d.caracteristicas.as_ptr(),
));
}
#[test]
fn dep_validate_reads_caracteristicas_through_accessor() {
Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: false,
caracteristicas: vec!["http".to_string(), "json".to_string()],
}
.validate()
.unwrap();
let err = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: false,
caracteristicas: vec![String::new()],
}
.validate()
.unwrap_err();
assert!(
matches!(
&err,
DepError::CaracteristicaEmpty { nome } if nome == "caixa-teia",
),
"expected CaracteristicaEmpty from the accessor-routed walk, got {err:?}",
);
let err = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: false,
caracteristicas: vec!["http".to_string(), "http".to_string()],
}
.validate()
.unwrap_err();
assert!(
matches!(
&err,
DepError::CaracteristicaDuplicate {
nome,
caracteristica,
} if nome == "caixa-teia" && caracteristica == "http",
),
"expected CaracteristicaDuplicate from the accessor-routed dedup, got {err:?}",
);
}
#[test]
fn dep_opcional_returns_declared_opcional_across_fonte_shapes() {
assert!(!Dep::simple("caixa-teia", "^0.1").opcional());
assert!(!Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0").opcional(),);
let plain_true = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional: true,
caracteristicas: Vec::new(),
};
assert!(plain_true.opcional());
let git_true = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: Some(DepSource::Git {
repo: "github:pleme-io/caixa-teia".to_string(),
tag: Some("v0.1.0".to_string()),
rev: None,
branch: None,
}),
opcional: true,
caracteristicas: Vec::new(),
};
assert!(git_true.opcional());
let path_true = Dep {
nome: "caixa-teia".to_string(),
versao: "0.1.0".to_string(),
fonte: Some(DepSource::Path {
caminho: "../caixa-teia".to_string(),
}),
opcional: true,
caracteristicas: Vec::new(),
};
assert!(path_true.opcional());
}
#[test]
fn dep_opcional_projects_bool_by_copy() {
for opcional in [false, true] {
let d = Dep {
nome: "caixa-teia".to_string(),
versao: "^0.1".to_string(),
fonte: None,
opcional,
caracteristicas: Vec::new(),
};
let first = d.opcional();
let second = d.opcional();
assert_eq!(
first, second,
"Dep::opcional must be idempotent — two successive calls \
on the same &self must return the same bool",
);
assert_eq!(
first, opcional,
"Dep::opcional must return :opcional verbatim by Copy — \
got {first}, expected {opcional}",
);
assert_eq!(
d.opcional(),
d.opcional,
"Dep::opcional accessor and self.opcional field access \
must byte-equal — a bit-flip drift would silently split \
the paired resolver-side drop-vs-error dispatch from \
the storage-side default-fill the [`Dep::simple`] / \
[`Dep::git`] constructor pair carries",
);
}
}
#[test]
fn sole_pin_returns_none_for_path_source() {
let s = DepSource::Path {
caminho: "../local-caixa".to_string(),
};
assert_eq!(s.sole_pin(), None);
}
#[test]
fn sole_pin_returns_none_for_unpinned_git_source() {
let s = DepSource::default_github("pleme-io", "caixa-teia");
assert_eq!(s.sole_pin(), None);
}
#[test]
fn sole_pin_returns_rev_when_only_rev_is_set() {
let s = DepSource::Git {
repo: "github:o/x".into(),
tag: None,
rev: Some("deadbeefcafebabe1234567890abcdef12345678".into()),
branch: None,
};
assert_eq!(
s.sole_pin(),
Some("deadbeefcafebabe1234567890abcdef12345678")
);
}
#[test]
fn sole_pin_returns_tag_when_only_tag_is_set() {
let s = DepSource::Git {
repo: "github:o/x".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
};
assert_eq!(s.sole_pin(), Some("v0.1.0"));
}
#[test]
fn sole_pin_returns_branch_when_only_branch_is_set() {
let s = DepSource::Git {
repo: "github:o/x".into(),
tag: None,
rev: None,
branch: Some("main".into()),
};
assert_eq!(s.sole_pin(), Some("main"));
}
#[test]
fn sole_pin_precedence_rev_beats_tag_and_branch() {
let s = DepSource::Git {
repo: "github:o/x".into(),
tag: Some("v1".into()),
rev: Some("deadbeefcafebabe1234567890abcdef12345678".into()),
branch: Some("main".into()),
};
assert_eq!(
s.sole_pin(),
Some("deadbeefcafebabe1234567890abcdef12345678")
);
}
#[test]
fn sole_pin_precedence_tag_beats_branch_when_no_rev() {
let s = DepSource::Git {
repo: "github:o/x".into(),
tag: Some("v1".into()),
rev: None,
branch: Some("main".into()),
};
assert_eq!(s.sole_pin(), Some("v1"));
}
#[test]
fn sole_pin_byte_equals_inline_rev_or_tag_or_branch_cascade() {
let vals = [Some("R".to_string()), None];
for tag in &vals {
for rev in &vals {
for branch in &vals {
let s = DepSource::Git {
repo: "github:o/x".into(),
tag: tag.clone(),
rev: rev.clone(),
branch: branch.clone(),
};
let expected = rev.as_deref().or(tag.as_deref()).or(branch.as_deref());
assert_eq!(
s.sole_pin(),
expected,
"sole_pin() must byte-equal \
rev.or(tag).or(branch) for \
(tag={tag:?}, rev={rev:?}, branch={branch:?}) — \
a drift would silently split caixa-resolver's \
fetch_git checkout target from caixa-crd's \
dep_into_ref git_ref fill",
);
}
}
}
}
#[test]
fn fonte_caminho_absolute_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_absolute("caixa-teia", "/home/me/work/caixa-teia"),
DepError::FonteCaminhoAbsolute {
nome: "caixa-teia".to_string(),
caminho: "/home/me/work/caixa-teia".to_string(),
},
"generated fonte_caminho_absolute ctor must produce byte-equal \
DepError to the open-coded struct-literal wrap on the same \
(&str, &str) fixture",
);
}
#[test]
fn fonte_caminho_tilde_expansion_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_tilde_expansion("caixa-teia", "~/work/caixa-teia"),
DepError::FonteCaminhoTildeExpansion {
nome: "caixa-teia".to_string(),
caminho: "~/work/caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_caminho_var_expansion_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_var_expansion("caixa-teia", "$HOME/work/caixa-teia"),
DepError::FonteCaminhoVarExpansion {
nome: "caixa-teia".to_string(),
caminho: "$HOME/work/caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_caminho_leading_whitespace_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_leading_whitespace("caixa-teia", " ../caixa-teia"),
DepError::FonteCaminhoLeadingWhitespace {
nome: "caixa-teia".to_string(),
caminho: " ../caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_caminho_leading_hyphen_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_leading_hyphen("caixa-teia", "-rf"),
DepError::FonteCaminhoLeadingHyphen {
nome: "caixa-teia".to_string(),
caminho: "-rf".to_string(),
},
);
}
#[test]
fn fonte_caminho_backslash_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_backslash("caixa-teia", "..\\caixa-teia"),
DepError::FonteCaminhoBackslash {
nome: "caixa-teia".to_string(),
caminho: "..\\caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_caminho_shell_pipe_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_shell_pipe("caixa-teia", "../caixa-teia|evil"),
DepError::FonteCaminhoShellPipe {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia|evil".to_string(),
},
);
}
#[test]
fn fonte_caminho_shell_semicolon_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_shell_semicolon("caixa-teia", "../caixa-teia;evil"),
DepError::FonteCaminhoShellSemicolon {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia;evil".to_string(),
},
);
}
#[test]
fn fonte_caminho_shell_background_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_shell_background("caixa-teia", "../caixa-teia&"),
DepError::FonteCaminhoShellBackground {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia&".to_string(),
},
);
}
#[test]
fn fonte_caminho_shell_command_substitution_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_shell_command_substitution(
"caixa-teia",
"../caixa-teia`whoami`",
),
DepError::FonteCaminhoShellCommandSubstitution {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia`whoami`".to_string(),
},
);
}
#[test]
fn fonte_caminho_trailing_slash_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_trailing_slash("caixa-teia", "../caixa-teia/"),
DepError::FonteCaminhoTrailingSlash {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia/".to_string(),
},
);
}
#[test]
fn fonte_caminho_ctors_route_nome_and_caminho_through_to_string() {
let nome = "sibling-teia";
let caminho = "../workspace/sibling";
let cases: [(DepError, DepError); 11] = [
(
DepError::fonte_caminho_absolute(nome, caminho),
DepError::FonteCaminhoAbsolute {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_tilde_expansion(nome, caminho),
DepError::FonteCaminhoTildeExpansion {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_var_expansion(nome, caminho),
DepError::FonteCaminhoVarExpansion {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_leading_whitespace(nome, caminho),
DepError::FonteCaminhoLeadingWhitespace {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_leading_hyphen(nome, caminho),
DepError::FonteCaminhoLeadingHyphen {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_backslash(nome, caminho),
DepError::FonteCaminhoBackslash {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_shell_pipe(nome, caminho),
DepError::FonteCaminhoShellPipe {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_shell_semicolon(nome, caminho),
DepError::FonteCaminhoShellSemicolon {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_shell_background(nome, caminho),
DepError::FonteCaminhoShellBackground {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_shell_command_substitution(nome, caminho),
DepError::FonteCaminhoShellCommandSubstitution {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
(
DepError::fonte_caminho_trailing_slash(nome, caminho),
DepError::FonteCaminhoTrailingSlash {
nome: nome.to_string(),
caminho: caminho.to_string(),
},
),
];
for (via_ctor, via_struct_literal) in cases {
assert_eq!(
via_ctor, via_struct_literal,
"fonte_caminho_ctors!-generated ctor must route (nome, caminho) \
through `.to_string()` in declared field order — a field-swap or \
silent-conversion regression surfaces here rather than at a \
downstream diagnostic-shape mismatch",
);
}
}
#[test]
fn versao_empty_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::versao_empty("caixa-teia"),
DepError::VersaoEmpty {
nome: "caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_repo_empty_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_repo_empty("caixa-teia"),
DepError::FonteRepoEmpty {
nome: "caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_pin_missing_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_pin_missing("caixa-teia"),
DepError::FontePinMissing {
nome: "caixa-teia".to_string(),
},
);
}
#[test]
fn fonte_caminho_empty_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_empty("caixa-teia"),
DepError::FonteCaminhoEmpty {
nome: "caixa-teia".to_string(),
},
);
}
#[test]
fn caracteristica_empty_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::caracteristica_empty("caixa-teia"),
DepError::CaracteristicaEmpty {
nome: "caixa-teia".to_string(),
},
);
}
#[test]
fn dep_nome_only_ctors_route_nome_through_to_string() {
let nome = "sibling-teia";
let cases: [(DepError, DepError); 5] = [
(
DepError::versao_empty(nome),
DepError::VersaoEmpty {
nome: nome.to_string(),
},
),
(
DepError::fonte_repo_empty(nome),
DepError::FonteRepoEmpty {
nome: nome.to_string(),
},
),
(
DepError::fonte_pin_missing(nome),
DepError::FontePinMissing {
nome: nome.to_string(),
},
),
(
DepError::fonte_caminho_empty(nome),
DepError::FonteCaminhoEmpty {
nome: nome.to_string(),
},
),
(
DepError::caracteristica_empty(nome),
DepError::CaracteristicaEmpty {
nome: nome.to_string(),
},
),
];
for (via_ctor, via_struct_literal) in cases {
assert_eq!(
via_ctor, via_struct_literal,
"dep_nome_only_ctors!-generated ctor must route `nome` \
through `.to_string()` onto the canonical `nome` field \
— a field-rename or silent-conversion regression surfaces \
here rather than at a downstream diagnostic-shape mismatch",
);
}
}
#[test]
fn duplicate_nome_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::duplicate_nome("caixa-teia", crate::render::DEP_AUTHOR_KEY_DEPS),
DepError::DuplicateNome {
nome: "caixa-teia".to_string(),
list: crate::render::DEP_AUTHOR_KEY_DEPS,
},
"generated duplicate_nome ctor must produce byte-equal \
`DepError::DuplicateNome` to the pre-lift struct-literal \
wrap on the same scalar fixtures",
);
}
#[test]
fn dep_is_self_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::dep_is_self("orquestra", crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
DepError::DepIsSelf {
nome: "orquestra".to_string(),
list: crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
},
"generated dep_is_self ctor must produce byte-equal \
`DepError::DepIsSelf` to the pre-lift struct-literal \
wrap on the same scalar fixtures",
);
}
#[test]
fn dep_nome_list_ctors_route_nome_and_list_through_uniformly() {
let nome = "sibling-teia";
let cases: [(DepError, DepError); 4] = [
(
DepError::duplicate_nome(nome, crate::render::DEP_AUTHOR_KEY_DEPS),
DepError::DuplicateNome {
nome: nome.to_string(),
list: crate::render::DEP_AUTHOR_KEY_DEPS,
},
),
(
DepError::duplicate_nome(nome, crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
DepError::DuplicateNome {
nome: nome.to_string(),
list: crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
},
),
(
DepError::dep_is_self(nome, crate::render::DEP_AUTHOR_KEY_DEPS),
DepError::DepIsSelf {
nome: nome.to_string(),
list: crate::render::DEP_AUTHOR_KEY_DEPS,
},
),
(
DepError::dep_is_self(nome, crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
DepError::DepIsSelf {
nome: nome.to_string(),
list: crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
},
),
];
for (via_ctor, via_struct_literal) in cases {
assert_eq!(
via_ctor, via_struct_literal,
"dep_nome_list_ctors!-generated ctor must route `nome` \
through `.to_string()` onto the canonical `nome` field \
and pass `list` verbatim onto the canonical `&'static str` \
`list` field — a field-rename, silent-conversion, or \
axis-swap regression surfaces here rather than at a \
downstream diagnostic-shape mismatch",
);
}
}
#[test]
fn fonte_pin_shape_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_pin_shape(
"caixa-teia",
":tag",
"v0.1.0 ",
"trailing whitespace".to_string(),
),
DepError::FontePinShape {
nome: "caixa-teia".to_string(),
pin: ":tag".to_string(),
value: "v0.1.0 ".to_string(),
reason: "trailing whitespace".to_string(),
},
"fonte_pin_shape ctor must produce byte-equal \
`DepError::FontePinShape` to the pre-lift struct-literal \
wrap on a refname-pin (`:tag` / `:branch`) fixture",
);
assert_eq!(
DepError::fonte_pin_shape(
"caixa-teia",
":rev",
"DEADBEEF",
"abbreviated OID rejected".to_string(),
),
DepError::FontePinShape {
nome: "caixa-teia".to_string(),
pin: ":rev".to_string(),
value: "DEADBEEF".to_string(),
reason: "abbreviated OID rejected".to_string(),
},
"fonte_pin_shape ctor must produce byte-equal \
`DepError::FontePinShape` to the pre-lift struct-literal \
wrap on a hex-OID-pin (`:rev`) fixture",
);
}
#[test]
fn fonte_pin_shape_ctor_routes_all_four_axes_through_to_string() {
let nome = "sibling-teia";
let pin = ":branch";
let value = "feature/bar";
let reason = "embedded space".to_string();
let via_ctor = DepError::fonte_pin_shape(nome, pin, value, reason.clone());
let via_struct_literal = DepError::FontePinShape {
nome: nome.to_string(),
pin: pin.to_string(),
value: value.to_string(),
reason: reason.clone(),
};
assert_eq!(
via_ctor, via_struct_literal,
"fonte_pin_shape ctor must route `nome` / `pin` / `value` \
through `.to_string()` onto their canonical fields and \
forward `reason` owned onto the canonical `reason` field \
— a field-rename, silent-conversion, or axis-swap \
regression surfaces here rather than at a downstream \
diagnostic-shape mismatch",
);
let DepError::FontePinShape {
nome: n,
pin: p,
value: v,
reason: r,
} = via_ctor
else {
panic!("fonte_pin_shape ctor produced non-FontePinShape variant")
};
assert_eq!(n, nome);
assert_eq!(p, pin);
assert_eq!(v, value);
assert_eq!(r, reason);
}
#[test]
fn fonte_caminho_control_char_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_control_char("caixa-teia", "../caixa-teia\x00foo", 0x00),
DepError::FonteCaminhoControlChar {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia\x00foo".to_string(),
byte: 0x00,
},
);
}
#[test]
fn fonte_caminho_shell_redirection_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_caminho_shell_redirection("caixa-teia", "../caixa-teia>log", b'>'),
DepError::FonteCaminhoShellRedirection {
nome: "caixa-teia".to_string(),
caminho: "../caixa-teia>log".to_string(),
byte: b'>',
},
);
}
#[test]
#[allow(
clippy::too_many_lines,
reason = "cross-axis routing pin sweeps twelve typed variants, one per \
byte-classification arm on the {nome,caminho,byte} envelope; \
the linear per-variant repetition is exactly what the sweep \
is pinning — a helper macro would hide the shape the fold is \
keying on"
)]
fn fonte_caminho_byte_ctors_route_nome_caminho_and_byte_through_to_string() {
let nome = "sibling-teia";
let caminho = "../workspace/sibling";
let byte = 0x2A_u8;
let cases: [(DepError, DepError); 12] = [
(
DepError::fonte_caminho_control_char(nome, caminho, byte),
DepError::FonteCaminhoControlChar {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_redirection(nome, caminho, byte),
DepError::FonteCaminhoShellRedirection {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_glob(nome, caminho, byte),
DepError::FonteCaminhoShellGlob {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_subshell_grouping(nome, caminho, byte),
DepError::FonteCaminhoShellSubshellGrouping {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_brace_expansion(nome, caminho, byte),
DepError::FonteCaminhoShellBraceExpansion {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_bracket_expansion(nome, caminho, byte),
DepError::FonteCaminhoShellBracketExpansion {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_quote_grouping(nome, caminho, byte),
DepError::FonteCaminhoShellQuoteGrouping {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_comment(nome, caminho, byte),
DepError::FonteCaminhoShellComment {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_url_percent_encoding(nome, caminho, byte),
DepError::FonteCaminhoUrlPercentEncoding {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_variable_expansion(nome, caminho, byte),
DepError::FonteCaminhoShellVariableExpansion {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_history_expansion(nome, caminho, byte),
DepError::FonteCaminhoShellHistoryExpansion {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
(
DepError::fonte_caminho_shell_history_substitution(nome, caminho, byte),
DepError::FonteCaminhoShellHistorySubstitution {
nome: nome.to_string(),
caminho: caminho.to_string(),
byte,
},
),
];
for (via_ctor, via_struct_literal) in cases {
assert_eq!(
via_ctor, via_struct_literal,
"fonte_caminho_byte_ctors!-generated ctor must route \
(nome, caminho, byte) through `.to_string()` / byte-\
identity in declared field order — a field-swap or \
silent-conversion regression surfaces here rather than \
at a downstream diagnostic-shape mismatch",
);
}
}
#[test]
fn dep_list_as_ref_str_routes_through_as_str_accessor() {
for &list in super::DepList::ALL {
assert_eq!(
<super::DepList as AsRef<str>>::as_ref(&list),
list.as_str(),
"AsRef<str> impl on DepList::{list:?} must byte-equal \
DepList::as_str on the same instance — divergence \
signals a silent detour off the substrate-primitive \
accessor"
);
}
}
#[test]
fn dep_list_as_ref_str_routes_through_display_via_shared_accessor() {
for &list in super::DepList::ALL {
let via_as_ref: &str = <super::DepList as AsRef<str>>::as_ref(&list);
let via_display: String = format!("{list}");
let via_accessor: &str = list.as_str();
assert_eq!(via_as_ref, via_accessor);
assert_eq!(via_display, via_accessor);
assert_eq!(via_as_ref, via_display.as_str());
}
}
#[test]
fn dep_list_try_from_str_routes_through_from_wire_accessor() {
for &list in super::DepList::ALL {
let wire = list.as_str();
assert_eq!(
<super::DepList as TryFrom<&str>>::try_from(wire),
Ok(list),
"TryFrom<&str> impl on DepList must round-trip \
DepList::{list:?}.as_str() = {wire:?} back to \
Ok(DepList::{list:?}) — divergence from \
DepList::from_wire signals a silent detour off the \
substrate-primitive accessor"
);
assert_eq!(
<super::DepList as TryFrom<&str>>::try_from(wire).ok(),
super::DepList::from_wire(wire),
"TryFrom<&str> ok()-projection on {wire:?} must byte-equal \
DepList::from_wire on the same input"
);
}
}
#[test]
fn dep_list_try_from_str_rejects_unknown_byte_strings() {
let rejected: &[&str] = &[
"",
" ",
"\t",
"\n",
":deps ",
" :deps",
":DEPS",
":Deps",
":Deps-Dev",
":deps_dev",
":deps-development",
":dev-deps",
":packages",
":packages-dev",
"deps",
"deps-dev",
"Prod",
"Dev",
"prod",
"dev",
"\":deps\"",
"\":deps-dev\"",
":deps\n",
":deps-dev\n",
];
for &input in rejected {
assert_eq!(
<super::DepList as TryFrom<&str>>::try_from(input),
Err(()),
"TryFrom<&str> impl on DepList must reject unknown \
byte-string {input:?} — divergence from \
DepList::from_wire on the same input signals a silent \
accept-set widening past the two lifted \
crate::render::DEP_AUTHOR_KEY_DEPS* wire constants"
);
assert_eq!(
<super::DepList as TryFrom<&str>>::try_from(input).ok(),
super::DepList::from_wire(input),
"TryFrom<&str> ok()-projection on {input:?} must byte-equal \
DepList::from_wire on the same input — divergence signals \
the two reverse-projection paths have drifted onto \
different accept-sets"
);
}
}
#[test]
fn dep_list_from_into_static_str_routes_through_as_str_accessor() {
const PROD: &str = super::DepList::Prod.as_str();
const DEV: &str = super::DepList::Dev.as_str();
for &list in super::DepList::ALL {
let via_trait: &'static str = <&'static str as From<super::DepList>>::from(list);
let via_method: &'static str = list.as_str();
assert_eq!(
via_trait, via_method,
"From<DepList> for &'static str impl must round-trip \
DepList::{list:?} to the same lifted \
crate::render::DEP_AUTHOR_KEY_DEPS* const \
DepList::as_str returns — divergence signals a silent \
detour off the substrate-primitive accessor"
);
let via_into: &'static str = list.into();
assert_eq!(
via_into, via_method,
"Into<&'static str>::into on DepList::{list:?} must \
byte-equal DepList::as_str on the same input — the \
blanket-derived Into shape must resolve to the same \
as_str dispatch as the explicit From impl"
);
}
assert_eq!(
[PROD, DEV],
[
crate::render::DEP_AUTHOR_KEY_DEPS,
crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
],
"const-context DepList::as_str must resolve to the two lifted \
DEP_AUTHOR_KEY_DEPS* consts — a future accidental downgrade \
of either arm to a non-const or non-static byte-string breaks \
the `&'static str`-lifetime promise the paired \
From<DepList> for &'static str impl carries by construction"
);
}
#[test]
fn dep_list_from_into_static_str_and_as_str_partition_the_emit_set() {
for &list in super::DepList::ALL {
let via_trait: &'static str = <&'static str as From<super::DepList>>::from(list);
let via_method: &'static str = list.as_str();
assert_eq!(
via_trait, via_method,
"From<DepList> for &'static str and DepList::as_str must \
resolve identically on DepList::{list:?} — divergence \
signals the two forward-projection paths have drifted \
onto different emit-sets"
);
}
for &list in super::DepList::ALL {
let emitted: &'static str = list.into();
let re_parsed: Result<super::DepList, ()> =
<super::DepList as TryFrom<&str>>::try_from(emitted);
assert_eq!(
re_parsed,
Ok(list),
"trait-idiomatic axis pair must round-trip \
DepList::{list:?} through `.into::<&'static str>()` and \
back through `TryFrom<&str>` — a break signals the \
forward-emit and reverse-parse axes have drifted onto \
different vocabularies"
);
}
}
#[test]
fn dep_list_from_borrowed_into_static_str_routes_through_as_str_accessor() {
const PROD: &str = super::DepList::Prod.as_str();
const DEV: &str = super::DepList::Dev.as_str();
for list in super::DepList::ALL {
let via_trait: &'static str = <&'static str as From<&super::DepList>>::from(list);
let via_method: &'static str = list.as_str();
assert_eq!(
via_trait, via_method,
"From<&DepList> for &'static str impl must round-trip \
&DepList::{list:?} to the same lifted \
crate::render::DEP_AUTHOR_KEY_DEPS* const \
DepList::as_str returns — divergence signals a silent \
detour off the substrate-primitive accessor"
);
let via_into: &'static str = list.into();
assert_eq!(
via_into, via_method,
"Into<&'static str>::into on &DepList::{list:?} must \
byte-equal DepList::as_str on the same input — the \
blanket-derived Into shape must resolve to the same \
as_str dispatch as the explicit From impl"
);
}
assert_eq!(
[PROD, DEV],
[
crate::render::DEP_AUTHOR_KEY_DEPS,
crate::render::DEP_AUTHOR_KEY_DEPS_DEV,
],
"const-context DepList::as_str must resolve to the two lifted \
DEP_AUTHOR_KEY_DEPS* consts — the borrowed-input \
From<&DepList> for &'static str impl inherits its `'static` \
lifetime promise from the same accessor the owned-input \
sibling routes through"
);
}
#[test]
fn dep_list_from_owned_and_borrowed_into_static_str_agree_on_every_arm() {
for &list in super::DepList::ALL {
let owned: &'static str = <&'static str as From<super::DepList>>::from(list);
let borrowed: &'static str = <&'static str as From<&super::DepList>>::from(&list);
assert_eq!(
owned, borrowed,
"From<DepList> and From<&DepList> for &'static str must \
resolve identically on DepList::{list:?} — divergence \
signals the owned-input and borrowed-input forward-\
projection paths have drifted onto different emit-sets"
);
}
let via_iter: Vec<&'static str> = super::DepList::ALL.iter().map(Into::into).collect();
let via_method: Vec<&'static str> =
super::DepList::ALL.iter().map(|l| l.as_str()).collect();
assert_eq!(
via_iter, via_method,
"`.iter().map(Into::into)` over DepList::ALL must byte-equal \
`.iter().map(|l| l.as_str())` on every arm — the borrowed-\
input `From<&DepList> for &'static str` axis is what makes \
the `.iter().map(Into::into)` shape route through the \
substrate-primitive `DepList::as_str` accessor rather than \
through a per-call-site `.copied()` / dereference detour"
);
}
}
#[cfg(test)]
mod dep_source_is_variant_tests {
use super::*;
fn all_variants() -> Vec<(DepSource, &'static str)> {
vec![
(
DepSource::Git {
repo: "github:pleme-io/caixa-teia".into(),
tag: Some("v0.1.0".into()),
rev: None,
branch: None,
},
"Git",
),
(
DepSource::Path {
caminho: "../caixa-teia".into(),
},
"Path",
),
]
}
fn predicate_row(s: &DepSource) -> [bool; 2] {
[s.is_git(), s.is_path()]
}
#[test]
fn dep_source_is_variant_predicates_partition_the_arm_set() {
let variants = all_variants();
for (idx, (variant, name)) in variants.iter().enumerate() {
let observed = predicate_row(variant);
let mut expected = [false; 2];
expected[idx] = true;
assert_eq!(
observed, expected,
"DepSource::{name} at declaration-order slot {idx} must \
satisfy exactly one is_* predicate (its own); observed \
row must equal the one-hot expected row — a drift \
would silently reroute one `:fonte`-arm consumer \
through the wrong predicate lane"
);
}
}
#[test]
fn dep_source_is_git_and_is_path_byte_equal_matches_shape() {
for (variant, name) in all_variants() {
let via_matches_git = matches!(variant, DepSource::Git { .. });
let via_predicate_git = variant.is_git();
assert_eq!(
via_predicate_git, via_matches_git,
"DepSource::{name}.is_git() must byte-equal \
matches!(_, DepSource::Git {{ .. }}) — otherwise a \
future converged consumer site would silently \
disagree with its pre-lift shape"
);
let via_matches_path = matches!(variant, DepSource::Path { .. });
let via_predicate_path = variant.is_path();
assert_eq!(
via_predicate_path, via_matches_path,
"DepSource::{name}.is_path() must byte-equal \
matches!(_, DepSource::Path {{ .. }}) — otherwise a \
future converged consumer site would silently \
disagree with its pre-lift shape"
);
}
}
#[test]
fn dep_source_constructors_route_through_paired_is_variant_predicate() {
let via_default_github = DepSource::default_github("pleme-io", "caixa-teia");
assert!(
via_default_github.is_git(),
"DepSource::default_github must materialize a Git-arm shape — \
a future constructor that routed through a non-Git arm \
(a registry-fetch pin, a `DepSource::Feira` promotion) \
would silently split the resolver's unpinned-shorthand \
materializer from the sole_pin() precedence cascade"
);
assert!(
!via_default_github.is_path(),
"DepSource::default_github must NOT materialize a Path-arm \
shape — the paired negation pin"
);
let via_dep_git = Dep::git("caixa-teia", "^0.1", "github:pleme-io/caixa-teia", "v0.1.0")
.fonte
.expect("Dep::git materializes a Some(fonte)");
assert!(
via_dep_git.is_git(),
"Dep::git's `:fonte` materialization must land on the Git \
arm — the author-surface pinned-git constructor's return \
must route through the paired predicate"
);
assert!(!via_dep_git.is_path(), "paired negation pin");
let via_path = DepSource::Path {
caminho: "../caixa-teia".into(),
};
assert!(
via_path.is_path(),
"the dev-mode Path-arm materialization must satisfy is_path()"
);
assert!(!via_path.is_git(), "paired negation pin");
}
#[test]
fn versao_invalid_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::versao_invalid("caixa-teia", "^0..1", "invalid comparator".to_string()),
DepError::VersaoInvalid {
nome: "caixa-teia".to_string(),
versao: "^0..1".to_string(),
reason: "invalid comparator".to_string(),
},
"versao_invalid ctor must produce byte-equal \
`DepError::VersaoInvalid` to the pre-lift struct-literal wrap",
);
}
#[test]
fn fonte_repo_shape_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_repo_shape(
"caixa-teia",
"-upload-pack=evil",
"leading dash rejected".to_string(),
),
DepError::FonteRepoShape {
nome: "caixa-teia".to_string(),
repo: "-upload-pack=evil".to_string(),
reason: "leading dash rejected".to_string(),
},
"fonte_repo_shape ctor must produce byte-equal \
`DepError::FonteRepoShape` to the pre-lift struct-literal wrap",
);
}
#[test]
fn caracteristica_invalid_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::caracteristica_invalid(
"caixa-teia",
"bad feature!",
"embedded space rejected".to_string(),
),
DepError::CaracteristicaInvalid {
nome: "caixa-teia".to_string(),
caracteristica: "bad feature!".to_string(),
reason: "embedded space rejected".to_string(),
},
"caracteristica_invalid ctor must produce byte-equal \
`DepError::CaracteristicaInvalid` to the pre-lift struct-literal wrap",
);
}
#[test]
fn dep_nome_axis_reason_ctors_route_nome_axis_and_reason_through_uniformly() {
let nome = "sibling-teia";
let axis = "distinct-axis-value";
let reason = "distinct rejection sentence".to_string();
assert_eq!(
DepError::versao_invalid(nome, axis, reason.clone()),
DepError::VersaoInvalid {
nome: nome.to_string(),
versao: axis.to_string(),
reason: reason.clone(),
},
"versao_invalid must route `nome` → `nome`, `axis` → `versao`, \
`reason` → `reason` in declared field order",
);
assert_eq!(
DepError::fonte_repo_shape(nome, axis, reason.clone()),
DepError::FonteRepoShape {
nome: nome.to_string(),
repo: axis.to_string(),
reason: reason.clone(),
},
"fonte_repo_shape must route `nome` → `nome`, `axis` → `repo`, \
`reason` → `reason` in declared field order",
);
assert_eq!(
DepError::caracteristica_invalid(nome, axis, reason.clone()),
DepError::CaracteristicaInvalid {
nome: nome.to_string(),
caracteristica: axis.to_string(),
reason: reason.clone(),
},
"caracteristica_invalid must route `nome` → `nome`, \
`axis` → `caracteristica`, `reason` → `reason` in declared \
field order",
);
}
#[test]
fn fonte_pin_empty_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_pin_empty("caixa-teia", ":tag"),
DepError::FontePinEmpty {
nome: "caixa-teia".to_string(),
pin: ":tag".to_string(),
},
"fonte_pin_empty ctor must produce byte-equal \
`DepError::FontePinEmpty` to the pre-lift struct-literal wrap \
on the same `(&str, &str)` fixture",
);
}
#[test]
fn fonte_pin_ambiguous_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::fonte_pin_ambiguous("caixa-teia", ":tag, :rev"),
DepError::FontePinAmbiguous {
nome: "caixa-teia".to_string(),
pins: ":tag, :rev".to_string(),
},
"fonte_pin_ambiguous ctor must produce byte-equal \
`DepError::FontePinAmbiguous` to the pre-lift struct-literal \
wrap on the same `(&str, &str)` fixture",
);
}
#[test]
fn caracteristica_duplicate_ctor_matches_struct_literal_wrap() {
assert_eq!(
DepError::caracteristica_duplicate("caixa-teia", "http"),
DepError::CaracteristicaDuplicate {
nome: "caixa-teia".to_string(),
caracteristica: "http".to_string(),
},
"caracteristica_duplicate ctor must produce byte-equal \
`DepError::CaracteristicaDuplicate` to the pre-lift \
struct-literal wrap on the same `(&str, &str)` fixture",
);
}
#[test]
fn fonte_pin_ambiguous_ctor_matches_owned_string_join_shape() {
let set: Vec<&'static str> = vec![":tag", ":rev"];
let joined: String = set.join(", ");
assert_eq!(
DepError::fonte_pin_ambiguous("caixa-teia", &joined),
DepError::FontePinAmbiguous {
nome: "caixa-teia".to_string(),
pins: ":tag, :rev".to_string(),
},
"fonte_pin_ambiguous ctor must accept an owned-`String` \
`&set.join(\", \")` carrier via Deref coercion — the exact \
shape the ambiguity-arm wire-up site passes into it",
);
}
#[test]
fn dep_nome_axis_ctors_route_nome_and_axis_through_to_string_uniformly() {
let nome = "sibling-teia";
let axis = "distinct-axis-value";
assert_eq!(
DepError::fonte_pin_empty(nome, axis),
DepError::FontePinEmpty {
nome: nome.to_string(),
pin: axis.to_string(),
},
"fonte_pin_empty must route `nome` → `nome`, `axis` → `pin` \
in declared field order",
);
assert_eq!(
DepError::fonte_pin_ambiguous(nome, axis),
DepError::FontePinAmbiguous {
nome: nome.to_string(),
pins: axis.to_string(),
},
"fonte_pin_ambiguous must route `nome` → `nome`, `axis` → `pins` \
in declared field order",
);
assert_eq!(
DepError::caracteristica_duplicate(nome, axis),
DepError::CaracteristicaDuplicate {
nome: nome.to_string(),
caracteristica: axis.to_string(),
},
"caracteristica_duplicate must route `nome` → `nome`, \
`axis` → `caracteristica` in declared field order",
);
}
#[test]
fn nome_invalid_ctor_matches_struct_literal_wrap() {
let nome = "Caixa-Teia";
let reason = crate::render::is_dns_1123_label(nome).unwrap_err();
let via_ctor = DepError::nome_invalid(nome, reason.clone());
let via_literal = DepError::NomeInvalid {
nome: nome.to_string(),
reason,
};
assert_eq!(
via_ctor, via_literal,
"nome_invalid(nome, reason) must byte-equal the open-coded \
NomeInvalid struct-literal on the same `(nome, reason)` fixture"
);
assert_eq!(
via_ctor.to_string(),
via_literal.to_string(),
"Display byte-string must byte-equal the open-coded struct-literal"
);
}
#[test]
fn nome_invalid_ctor_routes_nome_and_reason_through_to_string_uniformly() {
for nome in [
"Caixa-Teia",
"caixa_teia",
"caixa.teia",
"-caixa-teia",
"caixa-teia-",
"caixa/teia",
&"a".repeat(64),
] {
let reason = crate::render::is_dns_1123_label(nome)
.expect_err("fixture must be a DNS-1123-refused label");
let via_ctor = DepError::nome_invalid(nome, reason.clone());
let DepError::NomeInvalid {
nome: stored_nome,
reason: stored_reason,
} = via_ctor
else {
panic!("nome_invalid must construct NomeInvalid for {nome:?}");
};
assert_eq!(
stored_nome, nome,
"nome slot must round-trip verbatim through `.to_string()` for {nome:?}"
);
assert_eq!(
stored_reason, reason,
"reason slot must forward the owned `String` verbatim for {nome:?}"
);
}
}
#[test]
fn validate_nome_invalid_arm_routes_through_nome_invalid_ctor() {
let d = Dep::simple("Caixa_Teia", "^0.1");
let observed = d.validate().unwrap_err();
let reason = crate::render::is_dns_1123_label("Caixa_Teia")
.expect_err("fixture must be DNS-1123-refused");
let expected = DepError::nome_invalid("Caixa_Teia", reason);
assert_eq!(
observed, expected,
"Dep::validate's DNS-1123 refusal arm must byte-equal \
nome_invalid(nome, reason)"
);
assert_eq!(
observed.to_string(),
expected.to_string(),
"Display byte-string parity"
);
}
}