diff --git a/crates/rattler_conda_types/Cargo.toml b/crates/rattler_conda_types/Cargo.toml index 529082e4b..a9955697b 100644 --- a/crates/rattler_conda_types/Cargo.toml +++ b/crates/rattler_conda_types/Cargo.toml @@ -38,10 +38,11 @@ typed-path = { workspace = true } url = { workspace = true, features = ["serde"] } indexmap = { workspace = true } rattler_redaction = { version = "0.1.0", path = "../rattler_redaction" } +dirs = { workspace = true } [dev-dependencies] rand = { workspace = true } -insta = { workspace = true, features = ["yaml", "redactions", "toml", "glob"] } +insta = { workspace = true, features = ["yaml", "redactions", "toml", "glob", "filters"] } rattler_package_streaming = { path = "../rattler_package_streaming", default-features = false, features = ["rustls-tls"] } tempfile = { workspace = true } rstest = { workspace = true } diff --git a/crates/rattler_conda_types/src/channel/mod.rs b/crates/rattler_conda_types/src/channel/mod.rs index 8dc345008..221933e43 100644 --- a/crates/rattler_conda_types/src/channel/mod.rs +++ b/crates/rattler_conda_types/src/channel/mod.rs @@ -6,13 +6,12 @@ use std::{ }; use file_url::directory_path_to_url; +use rattler_redaction::Redact; use serde::{Deserialize, Serialize, Serializer}; use thiserror::Error; use typed_path::{Utf8NativePathBuf, Utf8TypedPath, Utf8TypedPathBuf}; use url::Url; -use rattler_redaction::Redact; - use super::{ParsePlatformError, Platform}; use crate::utils::{ path::is_path, @@ -107,7 +106,9 @@ impl NamedChannelOrUrl { NamedChannelOrUrl::Name(name) => { let mut base_url = config.channel_alias.clone(); if let Ok(mut segments) = base_url.path_segments_mut() { - segments.push(&name); + for segment in name.split(&['/', '\\']) { + segments.push(segment); + } } base_url } @@ -391,11 +392,11 @@ pub enum ParseChannelError { InvalidName(String), /// The root directory is not an absolute path - #[error("root directory from channel config is not an absolute path")] + #[error("root directory: '{0}' from channel config is not an absolute path")] NonAbsoluteRootDir(PathBuf), /// The root directory is not UTF-8 encoded. - #[error("root directory of channel config is not utf8 encoded")] + #[error("root directory: '{0}' of channel config is not utf8 encoded")] NotUtf8RootDir(PathBuf), } @@ -443,12 +444,24 @@ pub(crate) const fn default_platforms() -> &'static [Platform] { } /// Returns the specified path as an absolute path -fn absolute_path(path: &str, root_dir: &Path) -> Result { - let path = Utf8TypedPath::from(path); +fn absolute_path(path_str: &str, root_dir: &Path) -> Result { + let path = Utf8TypedPath::from(path_str); if path.is_absolute() { return Ok(path.normalize()); } + // Parse the `~/` as the home folder + if let Ok(user_path) = path.strip_prefix("~/") { + return Ok(Utf8TypedPathBuf::from( + dirs::home_dir() + .ok_or(ParseChannelError::InvalidPath(path.to_string()))? + .to_str() + .ok_or(ParseChannelError::NotUtf8RootDir(PathBuf::from(path_str)))?, + ) + .join(user_path) + .normalize()); + } + let root_dir_str = root_dir .to_str() .ok_or_else(|| ParseChannelError::NotUtf8RootDir(root_dir.to_path_buf()))?; @@ -467,6 +480,7 @@ fn absolute_path(path: &str, root_dir: &Path) -> Result Result, ParseMatchSpecError> { .map_err(ParseMatchSpecError::from); } // Is the spec a path, parse it as an url - if is_path(input) { + if is_absolute_path(input) { let path = Utf8TypedPath::from(input); return file_url::file_path_to_url(path) .map(Some) @@ -966,6 +966,10 @@ mod tests { // subdir in brackets take precedence "conda-forge/linux-32::python[version=3.9, subdir=linux-64]", "conda-forge/linux-32::python ==3.9[subdir=linux-64, build_number=\"0\"]", + "rust ~=1.2.3", + "~/channel/dir::package", + "~\\windows_channel::package", + "./relative/channel::package", ]; let evaluated: IndexMap<_, _> = specs @@ -982,7 +986,20 @@ mod tests { ) }) .collect(); - insta::assert_yaml_snapshot!(format!("test_from_string_{strictness:?}"), evaluated); + + // Strip absolute paths to this crate from the channels for testing + let crate_root = env!("CARGO_MANIFEST_DIR"); + let crate_path = Url::from_directory_path(std::path::Path::new(crate_root)).unwrap(); + let home = Url::from_directory_path(dirs::home_dir().unwrap()).unwrap(); + insta::with_settings!({filters => vec![ + (crate_path.as_str(), "file:///"), + (home.as_str(), "file:///"), + ]}, { + insta::assert_yaml_snapshot!( + format!("test_from_string_{strictness:?}"), + evaluated + ); + }); } #[rstest] @@ -1001,6 +1018,28 @@ mod tests { let specs = [ "2.7|>=3.6", "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2", + "~=1.2.3", + "*.* mkl", + "C:\\Users\\user\\conda-bld\\linux-64\\foo-1.0-py27_0.tar.bz2", + "=1.0=py27_0", + "==1.0=py27_0", + "https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda", + "https://repo.prefix.dev/ruben-arts/linux-64/boost-cpp-1.78.0-h75c5d50_1.tar.bz2", + "3.8.* *_cpython", + "=*=cuda*", + ">=1!164.3095,<1!165", + "/home/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2", + "[version=1.0.*]", + "[version=1.0.*, build_number=\">6\"]", + "==2.7.*.*|>=3.6", + "3.9", + "*", + "[version=3.9]", + "[version=3.9]", + "[version=3.9, subdir=linux-64]", + // subdir in brackets take precedence + "[version=3.9, subdir=linux-64]", + "==3.9[subdir=linux-64, build_number=\"0\"]", ]; let evaluated: IndexMap<_, _> = specs @@ -1145,9 +1184,6 @@ mod tests { let err = MatchSpec::from_str("bla/bla", Strict) .expect_err("Should try to parse as name not url"); assert_eq!(err.to_string(), "'bla/bla' is not a valid package name. Package names can only contain 0-9, a-z, A-Z, -, _, or ."); - - let err = MatchSpec::from_str("./test/file", Strict).expect_err("Invalid url"); - assert_eq!(err.to_string(), "invalid package path or url"); } #[test] @@ -1172,40 +1208,33 @@ mod tests { #[test] fn test_parse_channel_subdir() { - let (channel, subdir) = parse_channel_and_subdir("conda-forge").unwrap(); - assert_eq!( - channel.unwrap(), - Channel::from_str("conda-forge", &channel_config()).unwrap() - ); - assert_eq!(subdir, None); - - let (channel, subdir) = parse_channel_and_subdir("conda-forge/linux-64").unwrap(); - assert_eq!( - channel.unwrap(), - Channel::from_str("conda-forge", &channel_config()).unwrap() - ); - assert_eq!(subdir, Some("linux-64".to_string())); - - let (channel, subdir) = parse_channel_and_subdir("conda-forge/label/test").unwrap(); - assert_eq!( - channel.unwrap(), - Channel::from_str("conda-forge/label/test", &channel_config()).unwrap() - ); - assert_eq!(subdir, None); - - let (channel, subdir) = - parse_channel_and_subdir("conda-forge/linux-64/label/test").unwrap(); - assert_eq!( - channel.unwrap(), - Channel::from_str("conda-forge/linux-64/label/test", &channel_config()).unwrap() - ); - assert_eq!(subdir, None); + let test_cases = vec![ + ("conda-forge", Some("conda-forge"), None), + ( + "conda-forge/linux-64", + Some("conda-forge"), + Some("linux-64"), + ), + ( + "conda-forge/label/test", + Some("conda-forge/label/test"), + None, + ), + ( + "conda-forge/linux-64/label/test", + Some("conda-forge/linux-64/label/test"), + None, + ), + ("*/linux-64", Some("*"), Some("linux-64")), + ]; - let (channel, subdir) = parse_channel_and_subdir("*/linux-64").unwrap(); - assert_eq!( - channel.unwrap(), - Channel::from_str("*", &channel_config()).unwrap() - ); - assert_eq!(subdir, Some("linux-64".to_string())); + for (input, expected_channel, expected_subdir) in test_cases { + let (channel, subdir) = parse_channel_and_subdir(input).unwrap(); + assert_eq!( + channel.unwrap(), + Channel::from_str(expected_channel.unwrap(), &channel_config()).unwrap() + ); + assert_eq!(subdir, expected_subdir.map(|s| s.to_string())); + } } } diff --git a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Lenient.snap b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Lenient.snap index 9d6bfc745..226af1a8c 100644 --- a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Lenient.snap +++ b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Lenient.snap @@ -98,3 +98,18 @@ python=*: base_url: "https://conda.anaconda.org/conda-forge/" name: conda-forge subdir: linux-64 +rust ~=1.2.3: + name: rust + version: ~=1.2.3 +"~/channel/dir::package": + name: package + channel: + base_url: "file:///channel/dir/" + name: ~/channel/dir +"~\\windows_channel::package": + error: invalid channel +"./relative/channel::package": + name: package + channel: + base_url: "file:///relative/channel/" + name: "./relative/channel" diff --git a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Strict.snap b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Strict.snap index 8a77decf3..221d2dc94 100644 --- a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Strict.snap +++ b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_from_string_Strict.snap @@ -90,3 +90,18 @@ python=*: base_url: "https://conda.anaconda.org/conda-forge/" name: conda-forge subdir: linux-64 +rust ~=1.2.3: + name: rust + version: ~=1.2.3 +"~/channel/dir::package": + name: package + channel: + base_url: "file:///channel/dir/" + name: ~/channel/dir +"~\\windows_channel::package": + error: invalid channel +"./relative/channel::package": + name: package + channel: + base_url: "file:///relative/channel/" + name: "./relative/channel" diff --git a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Lenient.snap b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Lenient.snap index 81c734197..78a021e2f 100644 --- a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Lenient.snap +++ b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Lenient.snap @@ -6,3 +6,54 @@ expression: evaluated version: "==2.7|>=3.6" "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2": url: "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2" +~=1.2.3: + version: ~=1.2.3 +"*.* mkl": + version: "*" + build: mkl +"C:\\Users\\user\\conda-bld\\linux-64\\foo-1.0-py27_0.tar.bz2": + url: "file:///C:/Users/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2" +"=1.0=py27_0": + version: "==1.0" + build: py27_0 +"==1.0=py27_0": + version: "==1.0" + build: py27_0 +"https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda": + url: "https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda" +"https://repo.prefix.dev/ruben-arts/linux-64/boost-cpp-1.78.0-h75c5d50_1.tar.bz2": + url: "https://repo.prefix.dev/ruben-arts/linux-64/boost-cpp-1.78.0-h75c5d50_1.tar.bz2" +3.8.* *_cpython: + version: 3.8.* + build: "*_cpython" +"=*=cuda*": + version: "*" + build: cuda* +">=1!164.3095,<1!165": + version: ">=1!164.3095,<1!165" +/home/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2: + url: "file:///home/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2" +"[version=1.0.*]": + version: 1.0.* +"[version=1.0.*, build_number=\">6\"]": + version: 1.0.* + build_number: + op: Gt + rhs: 6 +"==2.7.*.*|>=3.6": + version: 2.7.*|>=3.6 +"3.9": + version: "==3.9" +"*": + version: "*" +"[version=3.9]": + version: "==3.9" +"[version=3.9, subdir=linux-64]": + version: "==3.9" + subdir: linux-64 +"==3.9[subdir=linux-64, build_number=\"0\"]": + version: "==3.9" + build_number: + op: Eq + rhs: 0 + subdir: linux-64 diff --git a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Strict.snap b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Strict.snap index 81c734197..75a46be02 100644 --- a/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Strict.snap +++ b/crates/rattler_conda_types/src/match_spec/snapshots/rattler_conda_types__match_spec__parse__tests__test_nameless_from_string_Strict.snap @@ -6,3 +6,51 @@ expression: evaluated version: "==2.7|>=3.6" "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2": url: "https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2" +~=1.2.3: + version: ~=1.2.3 +"*.* mkl": + version: "*" + build: mkl +"C:\\Users\\user\\conda-bld\\linux-64\\foo-1.0-py27_0.tar.bz2": + url: "file:///C:/Users/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2" +"=1.0=py27_0": + error: "The build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores" +"==1.0=py27_0": + error: "The build string '=py27_0' is not valid, it can only contain alphanumeric characters and underscores" +"https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda": + url: "https://conda.anaconda.org/conda-forge/linux-64/py-rattler-0.6.1-py39h8169da8_0.conda" +"https://repo.prefix.dev/ruben-arts/linux-64/boost-cpp-1.78.0-h75c5d50_1.tar.bz2": + url: "https://repo.prefix.dev/ruben-arts/linux-64/boost-cpp-1.78.0-h75c5d50_1.tar.bz2" +3.8.* *_cpython: + version: 3.8.* + build: "*_cpython" +"=*=cuda*": + error: "The build string '=cuda*' is not valid, it can only contain alphanumeric characters and underscores" +">=1!164.3095,<1!165": + version: ">=1!164.3095,<1!165" +/home/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2: + url: "file:///home/user/conda-bld/linux-64/foo-1.0-py27_0.tar.bz2" +"[version=1.0.*]": + version: 1.0.* +"[version=1.0.*, build_number=\">6\"]": + version: 1.0.* + build_number: + op: Gt + rhs: 6 +"==2.7.*.*|>=3.6": + error: "invalid version constraint: regex constraints are not supported" +"3.9": + version: "==3.9" +"*": + version: "*" +"[version=3.9]": + version: "==3.9" +"[version=3.9, subdir=linux-64]": + version: "==3.9" + subdir: linux-64 +"==3.9[subdir=linux-64, build_number=\"0\"]": + version: "==3.9" + build_number: + op: Eq + rhs: 0 + subdir: linux-64 diff --git a/crates/rattler_conda_types/src/utils/path.rs b/crates/rattler_conda_types/src/utils/path.rs index a4531aa38..12b038615 100644 --- a/crates/rattler_conda_types/src/utils/path.rs +++ b/crates/rattler_conda_types/src/utils/path.rs @@ -1,5 +1,21 @@ use itertools::Itertools; +/// Returns true if the specified string is considered to be an absolute path +pub(crate) fn is_absolute_path(path: &str) -> bool { + if path.contains("://") { + return false; + } + + // Check if the path starts with a common absolute path prefix + if path.starts_with('/') || path.starts_with("\\\\") { + return true; + } + + // A drive letter followed by a colon and a (backward or forward) slash + matches!(path.chars().take(3).collect_tuple(), + Some((letter, ':', '/' | '\\')) if letter.is_alphabetic()) +} + /// Returns true if the specified string is considered to be a path pub(crate) fn is_path(path: &str) -> bool { if path.contains("://") { @@ -9,7 +25,7 @@ pub(crate) fn is_path(path: &str) -> bool { // Check if the path starts with a common path prefix if path.starts_with("./") || path.starts_with("..") - || path.starts_with('~') + || path.starts_with("~/") || path.starts_with('/') || path.starts_with("\\\\") || path.starts_with("//") @@ -21,3 +37,39 @@ pub(crate) fn is_path(path: &str) -> bool { matches!(path.chars().take(3).collect_tuple(), Some((letter, ':', '/' | '\\')) if letter.is_alphabetic()) } + +mod tests { + #[test] + fn test_is_absolute_path() { + use super::is_absolute_path; + assert!(is_absolute_path("/foo")); + assert!(is_absolute_path("/C:/foo")); + assert!(is_absolute_path("C:/foo")); + assert!(is_absolute_path("\\\\foo")); + assert!(is_absolute_path("\\\\server\\foo")); + + assert!(!is_absolute_path("conda-forge/label/rust_dev")); + assert!(!is_absolute_path("~/foo")); + assert!(!is_absolute_path("./foo")); + assert!(!is_absolute_path("../foo")); + assert!(!is_absolute_path("foo")); + assert!(!is_absolute_path("~\\foo")); + } + + #[test] + fn test_is_path() { + use super::is_path; + assert!(is_path("/foo")); + assert!(is_path("/C:/foo")); + assert!(is_path("C:/foo")); + assert!(is_path("\\\\foo")); + assert!(is_path("\\\\server\\foo")); + + assert!(is_path("./conda-forge/label/rust_dev")); + assert!(is_path("~/foo")); + assert!(is_path("./foo")); + assert!(is_path("../foo")); + + assert!(!is_path("~\\foo")); + } +} diff --git a/py-rattler/Cargo.lock b/py-rattler/Cargo.lock index 5af18fc7d..47cd605f8 100644 --- a/py-rattler/Cargo.lock +++ b/py-rattler/Cargo.lock @@ -2744,6 +2744,7 @@ name = "rattler_conda_types" version = "0.27.0" dependencies = [ "chrono", + "dirs", "file_url", "fxhash", "glob",