Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ce26646
fix(match_spec):parse '*' as a native glob matchspec instead of None(…
KartikDua1504 Feb 19, 2026
69de673
fix(match_spec): parse asterisk as a native glob matchspec
KartikDua1504 Feb 19, 2026
ff54e6f
fix(matchspec): remove Option wrapper, clean dead error, and update p…
KartikDua1504 Feb 20, 2026
0006ac8
refactor(matchspec): make MatchSpec name field mandatory by removing …
KartikDua1504 Feb 21, 2026
e47327e
Merge branch 'main' into feat/matchspec-glob-star
KartikDua1504 Feb 21, 2026
7e74722
fix(gateway): remove obsolete nameless matchspec test and resolve ups…
KartikDua1504 Feb 21, 2026
420cacd
fix(solve): remove obsolete unwrap calls in benchmarks and fix clippy…
KartikDua1504 Feb 21, 2026
9f160b1
style: fix clippy warnings, format code, and update docstrings
KartikDua1504 Feb 21, 2026
697307b
Update crates/rattler_conda_types/src/match_spec/mod.rs
KartikDua1504 Feb 23, 2026
0260ac8
fix(matchspec): remove Option wrapper, clean dead error, and update p…
KartikDua1504 Feb 20, 2026
478c686
(fix) : name handling in install/installer/mod.rs
KartikDua1504 Feb 24, 2026
6847a9b
(fix) : name handling in install/installer/mod.rs
KartikDua1504 Feb 24, 2026
5ab6f3c
(fix): made required changes to /rattler_conda/types/src/match_spec/m…
KartikDua1504 Feb 24, 2026
c460e8e
(fix): made required changes to /rattler_conda/types/src/match_spec/m…
KartikDua1504 Feb 24, 2026
c2b4b3d
(fix): made required changes to /rattler_conda/types/src/match_spec/m…
KartikDua1504 Feb 24, 2026
882505c
(fix): added changes in crates/rattler_lock/src/conda.rs
KartikDua1504 Feb 24, 2026
9b609e4
(fix): crates/rattler_lock/conda.rs
KartikDua1504 Feb 24, 2026
f42d451
Update crates/rattler_conda_types/src/match_spec/parse.rs
KartikDua1504 Feb 25, 2026
873d747
Update crates/rattler_lock/src/conda.rs
KartikDua1504 Feb 25, 2026
3d22ab0
Update crates/rattler_lock/src/conda.rs
KartikDua1504 Feb 25, 2026
97190b8
(fix): incorporated suggestions and changed multiple files.
KartikDua1504 Feb 25, 2026
717e206
fixes
pavelzw Feb 25, 2026
a93b3bd
fix
pavelzw Feb 25, 2026
53af324
fix
pavelzw Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions crates/rattler/src/install/installer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ fn create_spec_mapping(specs: &[MatchSpec]) -> std::collections::HashMap<Package
let mut mapping = std::collections::HashMap::new();

for spec in specs {
if let Some(PackageNameMatcher::Exact(name)) = &spec.name {
if let PackageNameMatcher::Exact(name) = &spec.name {
mapping
.entry(name.clone())
.or_insert_with(Vec::new)
Expand Down Expand Up @@ -1087,11 +1087,11 @@ mod tests {
let specs = vec![
MatchSpec::from_str("python ~=3.11.0", Strict).unwrap(),
// Create a nameless spec by removing the name
MatchSpec {
name: None,
version: Some(">=1.0".parse().unwrap()),
..Default::default()
},
MatchSpec::from_nameless(
rattler_conda_types::NamelessMatchSpec::default(),
"*".parse::<rattler_conda_types::PackageNameMatcher>()
.unwrap(),
),
];

let mapping = create_spec_mapping(&specs);
Expand Down
144 changes: 104 additions & 40 deletions crates/rattler_conda_types/src/match_spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use serde::{Deserialize, Deserializer, Serialize};
use serde_with::{serde_as, skip_serializing_none};
use std::fmt::{Debug, Display, Formatter};
use std::hash::Hash;
use std::str::FromStr;
use std::sync::Arc;
use url::Url;

Expand Down Expand Up @@ -92,35 +91,35 @@ use parse::escape_bracket_value;
///
/// let channel_config = ChannelConfig::default_with_root_dir(std::env::current_dir().unwrap());
/// let spec = MatchSpec::from_str("foo 1.0.* py27_0", Strict).unwrap();
/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo"))));
/// assert_eq!(spec.name, PackageNameMatcher::Exact(PackageName::new_unchecked("foo")));
/// assert_eq!(spec.version, Some(VersionSpec::from_str("1.0.*", Strict).unwrap()));
/// assert_eq!(spec.build, Some(StringMatcher::from_str("py27_0").unwrap()));
///
/// let spec = MatchSpec::from_str("foo ==1.0 py27_0", Strict).unwrap();
/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo"))));
/// assert_eq!(spec.name, PackageNameMatcher::Exact(PackageName::new_unchecked("foo")));
/// assert_eq!(spec.version, Some(VersionSpec::from_str("==1.0", Strict).unwrap()));
/// assert_eq!(spec.build, Some(StringMatcher::from_str("py27_0").unwrap()));
///
/// let spec = MatchSpec::from_str(r#"conda-forge::foo[version="1.0.*"]"#, Strict).unwrap();
/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo"))));
/// assert_eq!(spec.name, PackageNameMatcher::Exact(PackageName::new_unchecked("foo")));
/// assert_eq!(spec.version, Some(VersionSpec::from_str("1.0.*", Strict).unwrap()));
/// assert_eq!(spec.channel, Some(Channel::from_str("conda-forge", &channel_config).map(|channel| Arc::new(channel)).unwrap()));
///
/// let spec = MatchSpec::from_str(r#"conda-forge::foo >=1.0[subdir="linux-64"]"#, Strict).unwrap();
/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo"))));
/// assert_eq!(spec.name, PackageNameMatcher::Exact(PackageName::new_unchecked("foo")));
/// assert_eq!(spec.version, Some(VersionSpec::from_str(">=1.0", Strict).unwrap()));
/// assert_eq!(spec.channel, Some(Channel::from_str("conda-forge", &channel_config).map(|channel| Arc::new(channel)).unwrap()));
/// assert_eq!(spec.subdir, Some("linux-64".to_string()));
/// assert_eq!(spec, MatchSpec::from_str("conda-forge/linux-64::foo >=1.0", Strict).unwrap());
///
/// let spec = MatchSpec::from_str("*/linux-64::foo >=1.0", Strict).unwrap();
/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo"))));
/// assert_eq!(spec.name, PackageNameMatcher::Exact(PackageName::new_unchecked("foo")));
/// assert_eq!(spec.version, Some(VersionSpec::from_str(">=1.0", Strict).unwrap()));
/// assert_eq!(spec.channel, Some(Channel::from_str("*", &channel_config).map(|channel| Arc::new(channel)).unwrap()));
/// assert_eq!(spec.subdir, Some("linux-64".to_string()));
///
/// let spec = MatchSpec::from_str(r#"foo[build="py2*"]"#, Strict).unwrap();
/// assert_eq!(spec.name, Some(PackageNameMatcher::Exact(PackageName::new_unchecked("foo"))));
/// assert_eq!(spec.name, PackageNameMatcher::Exact(PackageName::new_unchecked("foo")));
/// assert_eq!(spec.build, Some(StringMatcher::from_str("py2*").unwrap()));
/// ```
///
Expand All @@ -137,10 +136,10 @@ use parse::escape_bracket_value;
/// Alternatively, an exact spec is given by `*[sha256=01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b]`.
#[skip_serializing_none]
#[serde_as]
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
pub struct MatchSpec {
/// The name of the package
pub name: Option<PackageNameMatcher>,
pub name: PackageNameMatcher,
/// The version spec of the package (e.g. `1.2.3`, `>=1.2.3`, `1.2.*`)
pub version: Option<VersionSpec>,
/// The build string of the package (e.g. `py37_0`, `py37h6de7cb9_0`, `py*`)
Expand Down Expand Up @@ -173,6 +172,29 @@ pub struct MatchSpec {
pub track_features: Option<Vec<String>>,
}

impl Default for MatchSpec {
Comment thread
KartikDua1504 marked this conversation as resolved.
fn default() -> Self {
Self {
// We must explicitly set the name to "*" because PackageNameMatcher has no default
name: "*".parse().expect("wildcard always parses"),
version: None,
build: None,
build_number: None,
file_name: None,
extras: None,
channel: None,
subdir: None,
namespace: None,
md5: None,
sha256: None,
url: None,
license: None,
condition: None,
track_features: None,
}
}
}

impl Display for MatchSpec {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(channel) = &self.channel {
Expand All @@ -190,10 +212,7 @@ impl Display for MatchSpec {
write!(f, "::")?;
}

match &self.name {
Some(name) => write!(f, "{name}")?,
None => write!(f, "*")?,
}
write!(f, "{}", self.name)?;

if let Some(version) = &self.version {
write!(f, " {version}")?;
Expand Down Expand Up @@ -255,7 +274,7 @@ impl Display for MatchSpec {

impl MatchSpec {
/// Decomposes this instance into a [`NamelessMatchSpec`] and a name.
pub fn into_nameless(self) -> (Option<PackageNameMatcher>, NamelessMatchSpec) {
pub fn into_nameless(self) -> (PackageNameMatcher, NamelessMatchSpec) {
(
self.name,
NamelessMatchSpec {
Expand All @@ -282,19 +301,19 @@ impl MatchSpec {
/// Not having a package name is considered not virtual.
/// Matching both virtual and non-virtual packages is considered not virtual.
pub fn is_virtual(&self) -> bool {
self.name.as_ref().is_some_and(|name| match name {
match &self.name {
PackageNameMatcher::Exact(name) => name.as_normalized().starts_with("__"),
PackageNameMatcher::Glob(pattern) => pattern.as_str().starts_with("__"),
PackageNameMatcher::Regex(regex) => regex.as_str().starts_with(r"^__"),
})
}
}
}

// Enable constructing a match spec from a package name.
impl From<PackageName> for MatchSpec {
fn from(value: PackageName) -> Self {
Self {
name: Some(PackageNameMatcher::Exact(value)),
name: PackageNameMatcher::Exact(value),
..Default::default()
}
}
Expand Down Expand Up @@ -396,7 +415,7 @@ impl From<MatchSpec> for NamelessMatchSpec {

impl MatchSpec {
/// Constructs a [`MatchSpec`] from a [`NamelessMatchSpec`] and a name.
pub fn from_nameless(spec: NamelessMatchSpec, name: Option<PackageNameMatcher>) -> Self {
pub fn from_nameless(spec: NamelessMatchSpec, name: PackageNameMatcher) -> Self {
Self {
name,
version: spec.version,
Expand Down Expand Up @@ -501,10 +520,8 @@ impl Matches<PackageRecord> for NamelessMatchSpec {
impl Matches<PackageRecord> for MatchSpec {
/// Match a [`MatchSpec`] against a [`PackageRecord`]
fn matches(&self, other: &PackageRecord) -> bool {
if let Some(name) = self.name.as_ref() {
if !name.matches(&other.name) {
return false;
}
if !self.name.matches(&other.name) {
return false;
}

if let Some(spec) = self.version.as_ref() {
Expand Down Expand Up @@ -592,10 +609,8 @@ impl Matches<RepoDataRecord> for NamelessMatchSpec {
impl Matches<GenericVirtualPackage> for MatchSpec {
/// Match a [`MatchSpec`] against a [`GenericVirtualPackage`]
fn matches(&self, other: &GenericVirtualPackage) -> bool {
if let Some(name) = self.name.as_ref() {
if !name.matches(&other.name) {
return false;
}
if !self.name.matches(&other.name) {
return false;
}

if let Some(spec) = self.version.as_ref() {
Expand Down Expand Up @@ -648,12 +663,11 @@ impl TryFrom<Url> for MatchSpec {

let archive_identifier = CondaArchiveIdentifier::try_from_filename(filename)
.ok_or(MatchSpecUrlError::InvalidFilename(filename.to_string()))?;

spec.name = Some(
PackageNameMatcher::from_str(&archive_identifier.identifier.name)
.map_err(|e| MatchSpecUrlError::InvalidPackageName(e.to_string()))?,
);

spec.name = archive_identifier
.identifier
.name
.parse::<PackageNameMatcher>()
.map_err(|e| MatchSpecUrlError::InvalidPackageName(e.to_string()))?;
Ok(spec)
}
}
Expand Down Expand Up @@ -694,7 +708,7 @@ mod tests {
match_spec::Matches, package::DistArchiveIdentifier,
parse_mode::ParseStrictnessWithNameMatcher, MatchSpec, NamelessMatchSpec, PackageName,
PackageRecord, ParseMatchSpecError, ParseStrictness::*, RepoDataRecord, StringMatcher,
Version, VersionSpec,
Version,
};
use insta::assert_snapshot;
use std::hash::{Hash, Hasher};
Expand All @@ -710,20 +724,68 @@ mod tests {

#[test]
fn test_name_asterisk() {
use crate::match_spec::package_name_matcher::PackageNameMatcher;
use crate::{MatchSpec, ParseMatchSpecOptions, ParseStrictness::Lenient, VersionSpec};

// Explicitly configure the parser to allow glob matching
let options = ParseMatchSpecOptions::from(Lenient).with_exact_names_only(false);

// Test that MatchSpec can be created with an asterisk as the package name
let spec = MatchSpec::from_str("*[license=MIT]", Lenient).unwrap();
assert_eq!(spec.name, None);
let spec = MatchSpec::from_str("*[license=MIT]", options).unwrap();

// It should now correctly be identified as a Glob instead of None!
assert_eq!(spec.name, PackageNameMatcher::from_str("*").unwrap());
assert_eq!(spec.license, Some("MIT".to_string()));

// Test with a version
let spec = MatchSpec::from_str("* >=1.0", Lenient).unwrap();
assert_eq!(spec.name, None);
let spec = MatchSpec::from_str("* >=1.0", options).unwrap();
assert_eq!(spec.name, PackageNameMatcher::from_str("*").unwrap());
assert_eq!(
spec.version,
Some(VersionSpec::from_str(">=1.0", Lenient).unwrap())
);
}

#[test]
fn test_name_asterisk_edge_cases() {
use crate::match_spec::package_name_matcher::PackageNameMatcher;
use crate::{
MatchSpec, ParseMatchSpecError, ParseMatchSpecOptions, ParseStrictness::Strict,
VersionSpec,
};

// EDGE CASE 1: The Security Boundary
// In Strict mode (exact_names_only = true), a standalone `*` SHOULD be rejected.
let strict_spec = MatchSpec::from_str("*", Strict);
match strict_spec {
Err(ParseMatchSpecError::OnlyExactPackageNameMatchersAllowedGlob(g)) => {
assert_eq!(g, "*");
}
other => panic!("Strict mode failed to block the glob! Got: {other:?}"),
}

// EDGE CASE 2: The Kitchen Sink
// Testing `*` as a glob buried inside a highly complex spec string
let complex_str = "conda-forge/linux-64::*[version=\">=2.0\", build=\"*_cpython\"]";

// We explicitly allow globs for this specific parse
let options = ParseMatchSpecOptions::from(Strict).with_exact_names_only(false);
let spec =
MatchSpec::from_str(complex_str, options).expect("Failed to parse complex glob spec");

// Verify every single piece was parsed correctly around the `*`
assert_eq!(
spec.name,
PackageNameMatcher::from_str("*").expect("invalid package name matcher")
);
assert_eq!(spec.channel.unwrap().name(), "conda-forge");
assert_eq!(spec.subdir, Some("linux-64".to_string()));
assert_eq!(
spec.version,
Some(VersionSpec::from_str(">=2.0", Strict).expect("invalid version spec"))
);
assert!(spec.build.is_some(), "Build string matcher was dropped");
}

#[test]
fn test_nameless_matchspec_format_eq() {
let spec = NamelessMatchSpec::from_str("*[version==1.0, sha256=aaac4bc9c6916ecc0e33137431645b029ade22190c7144eead61446dcbcc6f97, md5=dede6252c964db3f3e41c7d30d07f6bf]", Lenient).unwrap();
Expand Down Expand Up @@ -1042,8 +1104,10 @@ mod tests {
let spec = MatchSpec::from_str("__virtual_name >=12", Strict).unwrap();
assert!(spec.is_virtual());

let spec =
MatchSpec::from_nameless(NamelessMatchSpec::from_str(">=12", Strict).unwrap(), None);
let spec = MatchSpec::from_nameless(
NamelessMatchSpec::from_str(">=12", Strict).unwrap(),
"dummy".parse().unwrap(),
);
assert!(!spec.is_virtual());

let spec = MatchSpec::from_str(
Expand Down
Loading
Loading