├── .gitignore ├── .rustfmt.toml ├── rust-toolchain.toml ├── README.md ├── .github └── workflows │ ├── publish.yml │ ├── ci.yml │ └── release.yml ├── Cargo.toml ├── LICENSE ├── benches └── bench.rs ├── src ├── common.rs ├── range_set_or_tag.rs ├── specifier.rs ├── string.rs ├── lib.rs ├── jsr.rs ├── package.rs ├── range.rs └── npm.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /target 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | tab_spaces = 2 3 | edition = "2024" 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.91.0" 3 | components = ["clippy", "rustfmt"] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno_semver 2 | 3 | [![](https://img.shields.io/crates/v/deno_semver.svg)](https://crates.io/crates/deno_semver) 4 | 5 | Semver used in Deno's CLI. 6 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write 14 | steps: 15 | - name: Clone repository 16 | uses: actions/checkout@v5 17 | - uses: rust-lang/crates-io-auth-action@v1 18 | id: auth 19 | - run: cargo publish 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | tags: 9 | - '*' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | rust: 14 | name: deno_semver-ubuntu-latest-release 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 30 17 | 18 | env: 19 | CARGO_INCREMENTAL: 0 20 | GH_ACTIONS: 1 21 | RUST_BACKTRACE: full 22 | RUSTFLAGS: -D warnings 23 | 24 | steps: 25 | - name: Clone repository 26 | uses: actions/checkout@v5 27 | 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - uses: Swatinem/rust-cache@v2 31 | with: 32 | save-if: ${{ github.ref == 'refs/heads/main' }} 33 | 34 | - name: Format 35 | run: cargo fmt --all -- --check 36 | 37 | - name: Lint 38 | run: cargo clippy --all-targets --all-features --release 39 | 40 | - name: Build 41 | run: cargo build --all-targets --all-features --release 42 | - name: Test 43 | run: cargo test --all-targets --all-features --release 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | releaseKind: 7 | description: 'Kind of release' 8 | default: 'minor' 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | required: true 14 | 15 | jobs: 16 | rust: 17 | name: release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - name: Clone repository 23 | uses: actions/checkout@v5 24 | with: 25 | token: ${{ secrets.DENOBOT_PAT }} 26 | 27 | - uses: denoland/setup-deno@v2 28 | - uses: dsherret/rust-toolchain-file@v1 29 | 30 | - name: Tag and release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.DENOBOT_PAT }} 33 | GH_WORKFLOW_ACTOR: ${{ github.actor }} 34 | run: | 35 | git config user.email "denobot@users.noreply.github.com" 36 | git config user.name "denobot" 37 | deno run -A jsr:@deno/rust-automation@0.22.3/tasks/publish-release --${{github.event.inputs.releaseKind}} 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deno_semver" 3 | version = "0.9.1" 4 | edition = "2024" 5 | description = "Semver for Deno" 6 | homepage = "https://deno.land/" 7 | repository = "https://github.com/denoland/deno_semver" 8 | documentation = "https://docs.rs/deno_semver" 9 | authors = ["the Deno authors"] 10 | license = "MIT" 11 | 12 | [dependencies] 13 | monch = "0.5.0" 14 | once_cell = "1.17.0" 15 | serde = { version = "1.0.130", features = ["derive", "rc"] } 16 | thiserror = "2" 17 | url = "2.5.3" 18 | deno_error = "0.7.0" 19 | capacity_builder = { version = "0.5.0", features = ["ecow"] } 20 | ecow = { version = "0.2.3", features = ["serde"] } 21 | 22 | # todo(dsherret): remove after https://github.com/polazarus/hipstr/pull/39 is released 23 | [target.'cfg(any(unix, windows))'.dependencies] 24 | capacity_builder = { version = "0.5.0", features = ["ecow", "hipstr"] } 25 | hipstr = "0.6" 26 | 27 | [dev-dependencies] 28 | divan = "0.1.17" 29 | pretty_assertions = "1.0.0" 30 | serde_json = { version = "1.0.67", features = ["preserve_order"] } 31 | 32 | [[bench]] 33 | name = "bench" 34 | harness = false 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2025 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // Run registered benchmarks. 3 | divan::main(); 4 | } 5 | 6 | mod package_req { 7 | use deno_semver::package::PackageReq; 8 | 9 | #[divan::bench(sample_size = 1000)] 10 | fn from_str_loose() -> usize { 11 | PackageReq::from_str_loose("@deno/std@0.100.0") 12 | .unwrap() 13 | .name 14 | .len() 15 | } 16 | 17 | #[divan::bench(sample_size = 1000)] 18 | fn to_string_normalized() -> usize { 19 | PackageReq::from_str_loose("@deno/std@0.100.0") 20 | .unwrap() 21 | .to_string_normalized() 22 | .len() 23 | } 24 | } 25 | 26 | mod version { 27 | use deno_semver::Version; 28 | 29 | #[divan::bench(sample_size = 1000)] 30 | fn to_string() -> usize { 31 | version().to_string().len() 32 | } 33 | 34 | #[divan::bench(sample_size = 1000)] 35 | fn to_string_display() -> usize { 36 | format!("{}", version()).len() 37 | } 38 | 39 | fn version() -> Version { 40 | Version::parse_from_npm("1.1.1-pre").unwrap() 41 | } 42 | } 43 | 44 | mod version_req { 45 | use deno_semver::VersionReq; 46 | 47 | #[divan::bench(sample_size = 1000)] 48 | fn to_string() -> usize { 49 | version_req().to_string().len() 50 | } 51 | 52 | #[divan::bench(sample_size = 1000)] 53 | fn to_string_display() -> usize { 54 | format!("{}", version_req()).len() 55 | } 56 | 57 | #[divan::bench(sample_size = 1000)] 58 | fn to_string_normalized() -> usize { 59 | version_req().to_string_normalized().len() 60 | } 61 | 62 | fn version_req() -> VersionReq { 63 | VersionReq::parse_from_npm("^1.1.1-pre").unwrap() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use monch::*; 4 | 5 | use crate::CowVec; 6 | use crate::Partial; 7 | use crate::VersionBoundKind; 8 | use crate::VersionPreOrBuild; 9 | use crate::VersionRange; 10 | use crate::XRange; 11 | 12 | // logical-or ::= ( ' ' ) * '||' ( ' ' ) * 13 | pub fn logical_or(input: &str) -> ParseResult<'_, &str> { 14 | delimited(skip_whitespace, tag("||"), skip_whitespace)(input) 15 | } 16 | 17 | // logical-and ::= ( ' ' ) * '&&' ( ' ' ) * 18 | pub fn logical_and(input: &str) -> ParseResult<'_, &str> { 19 | delimited(skip_whitespace, tag("&&"), skip_whitespace)(input) 20 | } 21 | 22 | // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? 23 | pub fn partial<'a>( 24 | xr: impl Fn(&'a str) -> ParseResult<'a, XRange>, 25 | ) -> impl Fn(&'a str) -> ParseResult<'a, Partial> { 26 | move |input| { 27 | let (input, major) = xr(input)?; 28 | let (input, maybe_minor) = maybe(preceded(ch('.'), &xr))(input)?; 29 | let (input, maybe_patch) = if maybe_minor.is_some() { 30 | maybe(preceded(ch('.'), &xr))(input)? 31 | } else { 32 | (input, None) 33 | }; 34 | let (input, qual) = if maybe_patch.is_some() { 35 | maybe(qualifier)(input)? 36 | } else { 37 | (input, None) 38 | }; 39 | let qual = qual.unwrap_or_default(); 40 | Ok(( 41 | input, 42 | Partial { 43 | major, 44 | minor: maybe_minor.unwrap_or(XRange::Wildcard), 45 | patch: maybe_patch.unwrap_or(XRange::Wildcard), 46 | pre: qual.pre, 47 | build: qual.build, 48 | }, 49 | )) 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone, Default)] 54 | pub struct Qualifier { 55 | pub pre: CowVec, 56 | pub build: CowVec, 57 | } 58 | 59 | // qualifier ::= ( '-' pre )? ( '+' build )? 60 | pub fn qualifier(input: &str) -> ParseResult<'_, Qualifier> { 61 | let (input, pre_parts) = maybe(pre)(input)?; 62 | let (input, build_parts) = maybe(build)(input)?; 63 | Ok(( 64 | input, 65 | Qualifier { 66 | pre: pre_parts.unwrap_or_default(), 67 | build: build_parts.unwrap_or_default(), 68 | }, 69 | )) 70 | } 71 | 72 | // pre ::= parts 73 | fn pre(input: &str) -> ParseResult<'_, CowVec> { 74 | preceded(maybe(ch('-')), parts)(input) 75 | } 76 | 77 | // build ::= parts 78 | fn build(input: &str) -> ParseResult<'_, CowVec> { 79 | preceded(ch('+'), parts)(input) 80 | } 81 | 82 | // parts ::= part ( '.' part ) * 83 | fn parts(input: &str) -> ParseResult<'_, CowVec> { 84 | if_true( 85 | map(separated_list(part, ch('.')), |text| { 86 | text 87 | .into_iter() 88 | .map(VersionPreOrBuild::from_str) 89 | .collect::>() 90 | }), 91 | |items| !items.is_empty(), 92 | )(input) 93 | } 94 | 95 | // part ::= nr | [-0-9A-Za-z]+ 96 | fn part(input: &str) -> ParseResult<'_, &str> { 97 | // nr is in the other set, so don't bother checking for it 98 | if_true( 99 | take_while(|c| c.is_ascii_alphanumeric() || c == '-'), 100 | |result| !result.is_empty(), 101 | )(input) 102 | } 103 | 104 | #[derive(Debug, Clone, Copy)] 105 | pub enum PrimitiveKind { 106 | GreaterThan, 107 | LessThan, 108 | GreaterThanOrEqual, 109 | LessThanOrEqual, 110 | Equal, 111 | } 112 | 113 | pub fn primitive_kind(input: &str) -> ParseResult<'_, PrimitiveKind> { 114 | or5( 115 | map(tag(">="), |_| PrimitiveKind::GreaterThanOrEqual), 116 | map(tag("<="), |_| PrimitiveKind::LessThanOrEqual), 117 | map(ch('<'), |_| PrimitiveKind::LessThan), 118 | map(ch('>'), |_| PrimitiveKind::GreaterThan), 119 | map(ch('='), |_| PrimitiveKind::Equal), 120 | )(input) 121 | } 122 | 123 | #[derive(Debug, Clone)] 124 | pub struct Primitive { 125 | pub kind: PrimitiveKind, 126 | pub partial: Partial, 127 | } 128 | 129 | impl Primitive { 130 | pub fn into_version_range(self) -> VersionRange { 131 | let partial = self.partial; 132 | match self.kind { 133 | PrimitiveKind::Equal => partial.as_equal_range(), 134 | PrimitiveKind::GreaterThan => { 135 | partial.as_greater_than(VersionBoundKind::Exclusive) 136 | } 137 | PrimitiveKind::GreaterThanOrEqual => { 138 | partial.as_greater_than(VersionBoundKind::Inclusive) 139 | } 140 | PrimitiveKind::LessThan => { 141 | partial.as_less_than(VersionBoundKind::Exclusive) 142 | } 143 | PrimitiveKind::LessThanOrEqual => { 144 | partial.as_less_than(VersionBoundKind::Inclusive) 145 | } 146 | } 147 | } 148 | } 149 | 150 | pub fn primitive<'a>( 151 | partial: impl Fn(&'a str) -> ParseResult<'a, Partial>, 152 | ) -> impl Fn(&'a str) -> ParseResult<'a, Primitive> { 153 | move |input| { 154 | let (input, kind) = primitive_kind(input)?; 155 | let (input, _) = skip_whitespace(input)?; 156 | let (input, partial) = partial(input)?; 157 | Ok((input, Primitive { kind, partial })) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/range_set_or_tag.rs: -------------------------------------------------------------------------------- 1 | use capacity_builder::CapacityDisplay; 2 | use capacity_builder::StringAppendable; 3 | use capacity_builder::StringBuilder; 4 | use capacity_builder::StringType; 5 | use monch::*; 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | use crate::CowVec; 10 | use crate::Partial; 11 | use crate::SmallStackString; 12 | use crate::StackString; 13 | use crate::VersionRange; 14 | use crate::VersionRangeSet; 15 | use crate::XRange; 16 | use crate::common::logical_or; 17 | use crate::common::primitive; 18 | use crate::npm::is_valid_npm_tag; 19 | 20 | pub type PackageTag = SmallStackString; 21 | 22 | #[derive(Clone, Debug, PartialEq, Eq, Hash, CapacityDisplay)] 23 | pub enum RangeSetOrTag { 24 | RangeSet(VersionRangeSet), 25 | Tag(PackageTag), 26 | } 27 | 28 | impl<'a> StringAppendable<'a> for &'a RangeSetOrTag { 29 | fn append_to_builder( 30 | self, 31 | builder: &mut StringBuilder<'a, TString>, 32 | ) { 33 | match self { 34 | RangeSetOrTag::RangeSet(range_set) => builder.append(range_set), 35 | RangeSetOrTag::Tag(tag) => builder.append(tag), 36 | } 37 | } 38 | } 39 | 40 | impl Serialize for RangeSetOrTag { 41 | fn serialize(&self, serializer: S) -> Result 42 | where 43 | S: serde::Serializer, 44 | { 45 | serializer.serialize_str(&self.to_custom_string::()) 46 | } 47 | } 48 | 49 | impl<'de> Deserialize<'de> for RangeSetOrTag { 50 | fn deserialize(deserializer: D) -> Result 51 | where 52 | D: serde::Deserializer<'de>, 53 | { 54 | let s = String::deserialize(deserializer)?; 55 | let result = with_failure_handling(Self::parse)(&s); 56 | result.map_err(|err| serde::de::Error::custom(format!("{}", err))) 57 | } 58 | } 59 | 60 | impl RangeSetOrTag { 61 | pub(crate) fn parse(input: &str) -> ParseResult<'_, RangeSetOrTag> { 62 | if input.is_empty() { 63 | return Ok(( 64 | input, 65 | RangeSetOrTag::RangeSet(VersionRangeSet(CowVec::from([ 66 | VersionRange::all(), 67 | ]))), 68 | )); 69 | } 70 | 71 | let (input, mut ranges) = separated_list( 72 | |text| RangeOrInvalid::parse(text, range), 73 | logical_or, 74 | )(input)?; 75 | 76 | if ranges.len() == 1 { 77 | match ranges.remove(0) { 78 | RangeOrInvalid::Invalid(invalid) => { 79 | if is_valid_npm_tag(invalid.text) { 80 | return Ok(( 81 | input, 82 | RangeSetOrTag::Tag(PackageTag::from_str(invalid.text)), 83 | )); 84 | } else { 85 | return Err(invalid.failure); 86 | } 87 | } 88 | RangeOrInvalid::Range(range) => { 89 | // add it back 90 | ranges.push(RangeOrInvalid::Range(range)); 91 | } 92 | } 93 | } 94 | 95 | let ranges = ranges 96 | .into_iter() 97 | .map(|r| r.into_range()) 98 | .filter_map(|r| r.transpose()) 99 | .collect::, _>>()?; 100 | Ok((input, RangeSetOrTag::RangeSet(VersionRangeSet(ranges)))) 101 | } 102 | 103 | pub fn intersects(&self, other: &RangeSetOrTag) -> bool { 104 | match (self, other) { 105 | (RangeSetOrTag::RangeSet(a), RangeSetOrTag::RangeSet(b)) => { 106 | a.intersects_set(b) 107 | } 108 | (RangeSetOrTag::RangeSet(_), RangeSetOrTag::Tag(_)) 109 | | (RangeSetOrTag::Tag(_), RangeSetOrTag::RangeSet(_)) => false, 110 | (RangeSetOrTag::Tag(a), RangeSetOrTag::Tag(b)) => a == b, 111 | } 112 | } 113 | } 114 | 115 | pub(crate) enum RangeOrInvalid<'a> { 116 | Range(VersionRange), 117 | Invalid(InvalidRange<'a>), 118 | } 119 | 120 | impl<'a> RangeOrInvalid<'a> { 121 | pub fn into_range(self) -> Result, ParseError<'a>> { 122 | match self { 123 | RangeOrInvalid::Range(r) => { 124 | if r.is_none() { 125 | Ok(None) 126 | } else { 127 | Ok(Some(r)) 128 | } 129 | } 130 | RangeOrInvalid::Invalid(invalid) => Err(invalid.failure), 131 | } 132 | } 133 | 134 | pub fn parse( 135 | input: &'a str, 136 | parse_range: impl Fn(&'a str) -> ParseResult<'a, VersionRange>, 137 | ) -> ParseResult<'a, RangeOrInvalid<'a>> { 138 | let range_result = 139 | map_res(map(parse_range, Self::Range), |result| match result { 140 | Ok((input, range)) => { 141 | let is_end = input.is_empty() || input.trim_start().starts_with("||"); 142 | if is_end { 143 | Ok((input, range)) 144 | } else { 145 | ParseError::backtrace() 146 | } 147 | } 148 | Err(err) => Err(err), 149 | })(input); 150 | match range_result { 151 | Ok(result) => Ok(result), 152 | Err(failure) => { 153 | let (input, text) = invalid_range(input)?; 154 | Ok(( 155 | input, 156 | RangeOrInvalid::Invalid(InvalidRange { failure, text }), 157 | )) 158 | } 159 | } 160 | } 161 | } 162 | 163 | pub(crate) struct InvalidRange<'a> { 164 | pub failure: ParseError<'a>, 165 | pub text: &'a str, 166 | } 167 | 168 | fn invalid_range(input: &str) -> ParseResult<'_, &str> { 169 | let end_index = input.find("||").unwrap_or(input.len()); 170 | let (text, input) = input.split_at(end_index); 171 | let text = text.trim(); 172 | Ok((input, text)) 173 | } 174 | 175 | // range ::= simple ( ' ' simple ) 176 | fn range(input: &str) -> ParseResult<'_, VersionRange> { 177 | map(separated_list(simple, whitespace), |ranges| { 178 | let mut final_range = VersionRange::all(); 179 | for range in ranges { 180 | final_range = final_range.clamp(&range); 181 | } 182 | final_range 183 | })(input) 184 | } 185 | 186 | // simple ::= primitive | partial | tilde | caret 187 | fn simple(input: &str) -> ParseResult<'_, VersionRange> { 188 | or4( 189 | map(preceded(ch('~'), partial), |partial| { 190 | partial.as_tilde_version_range() 191 | }), 192 | map(preceded(ch('^'), partial), |partial| { 193 | partial.as_caret_version_range() 194 | }), 195 | map(primitive(partial), |primitive| { 196 | primitive.into_version_range() 197 | }), 198 | map(partial, |partial| partial.as_equal_range()), 199 | )(input) 200 | } 201 | 202 | // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? 203 | fn partial(input: &str) -> ParseResult<'_, Partial> { 204 | crate::common::partial(xr)(input) 205 | } 206 | 207 | // xr ::= '*' | nr 208 | fn xr(input: &str) -> ParseResult<'_, XRange> { 209 | or(map(tag("*"), |_| XRange::Wildcard), map(nr, XRange::Val))(input) 210 | } 211 | 212 | // nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * 213 | fn nr(input: &str) -> ParseResult<'_, u64> { 214 | // we do loose parsing to support people doing stuff like 01.02.03 215 | let (input, result) = 216 | if_not_empty(substring(skip_while(|c| c.is_ascii_digit())))(input)?; 217 | let val = match result.parse::() { 218 | Ok(val) => val, 219 | Err(err) => { 220 | return ParseError::fail( 221 | input, 222 | format!("Error parsing '{result}' to u64.\n\n{err:#}"), 223 | ); 224 | } 225 | }; 226 | Ok((input, val)) 227 | } 228 | -------------------------------------------------------------------------------- /src/specifier.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use monch::*; 4 | use thiserror::Error; 5 | 6 | use crate::CowVec; 7 | use crate::PackageTag; 8 | use crate::RangeSetOrTag; 9 | use crate::VersionPreOrBuild; 10 | use crate::VersionReq; 11 | use crate::range::Partial; 12 | use crate::range::VersionRange; 13 | use crate::range::VersionRangeSet; 14 | use crate::range::XRange; 15 | 16 | use crate::is_valid_tag; 17 | 18 | #[derive(Error, Debug, Clone, deno_error::JsError, PartialEq, Eq)] 19 | #[class(type)] 20 | #[error("Invalid specifier version requirement")] 21 | pub struct VersionReqSpecifierParseError { 22 | #[source] 23 | pub source: ParseErrorFailureError, 24 | } 25 | 26 | pub fn parse_version_req_from_specifier( 27 | text: &str, 28 | ) -> Result { 29 | with_failure_handling(|input| { 30 | map_res(version_range, |result| { 31 | let (new_input, range_result) = match result { 32 | Ok((input, range)) => (input, Ok(range)), 33 | // use an empty string because we'll consider it a tag 34 | Err(err) => ("", Err(err)), 35 | }; 36 | Ok(( 37 | new_input, 38 | VersionReq::from_raw_text_and_inner( 39 | crate::SmallStackString::from_str(input), 40 | match range_result { 41 | Ok(range) => { 42 | RangeSetOrTag::RangeSet(VersionRangeSet(CowVec::from([range]))) 43 | } 44 | Err(err) => { 45 | if is_valid_tag(input) { 46 | RangeSetOrTag::Tag(PackageTag::from_str(input)) 47 | } else if input.trim().is_empty() { 48 | return ParseError::fail(input, "Empty version constraint."); 49 | } else { 50 | return Err(err); 51 | } 52 | } 53 | }, 54 | ), 55 | )) 56 | })(input) 57 | })(text) 58 | .map_err(|err| VersionReqSpecifierParseError { source: err }) 59 | } 60 | 61 | // Note: Although the code below looks very similar to what's used for 62 | // parsing npm version requirements, the code here is more strict 63 | // in order to not allow for people to get ridiculous when using 64 | // npm/deno specifiers. 65 | // 66 | // A lot of the code below is adapted from https://github.com/npm/node-semver 67 | // which is Copyright (c) Isaac Z. Schlueter and Contributors (ISC License) 68 | 69 | // version_range ::= partial | tilde | caret 70 | fn version_range(input: &str) -> ParseResult<'_, VersionRange> { 71 | or3( 72 | map(preceded(ch('~'), partial), |partial| { 73 | partial.as_tilde_version_range() 74 | }), 75 | map(preceded(ch('^'), partial), |partial| { 76 | partial.as_caret_version_range() 77 | }), 78 | map(partial, |partial| partial.as_equal_range()), 79 | )(input) 80 | } 81 | 82 | // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? 83 | fn partial(input: &str) -> ParseResult<'_, Partial> { 84 | let (input, major) = xr()(input)?; 85 | let (input, maybe_minor) = maybe(preceded(ch('.'), xr()))(input)?; 86 | let (input, maybe_patch) = if maybe_minor.is_some() { 87 | maybe(preceded(ch('.'), xr()))(input)? 88 | } else { 89 | (input, None) 90 | }; 91 | let (input, qual) = if maybe_patch.is_some() { 92 | maybe(qualifier)(input)? 93 | } else { 94 | (input, None) 95 | }; 96 | let qual = qual.unwrap_or_default(); 97 | Ok(( 98 | input, 99 | Partial { 100 | major, 101 | minor: maybe_minor.unwrap_or(XRange::Wildcard), 102 | patch: maybe_patch.unwrap_or(XRange::Wildcard), 103 | pre: qual.pre, 104 | build: qual.build, 105 | }, 106 | )) 107 | } 108 | 109 | // xr ::= 'x' | 'X' | '*' | nr 110 | fn xr<'a>() -> impl Fn(&'a str) -> ParseResult<'a, XRange> { 111 | or( 112 | map(or3(tag("x"), tag("X"), tag("*")), |_| XRange::Wildcard), 113 | map(nr, XRange::Val), 114 | ) 115 | } 116 | 117 | // nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * 118 | fn nr(input: &str) -> ParseResult<'_, u64> { 119 | or(map(tag("0"), |_| 0), move |input| { 120 | let (input, result) = if_not_empty(substring(pair( 121 | if_true(next_char, |c| c.is_ascii_digit() && *c != '0'), 122 | skip_while(|c| c.is_ascii_digit()), 123 | )))(input)?; 124 | let val = match result.parse::() { 125 | Ok(val) => val, 126 | Err(err) => { 127 | return ParseError::fail( 128 | input, 129 | format!("Error parsing '{result}' to u64.\n\n{err:#}"), 130 | ); 131 | } 132 | }; 133 | Ok((input, val)) 134 | })(input) 135 | } 136 | 137 | #[derive(Debug, Clone, Default)] 138 | struct Qualifier { 139 | pre: CowVec, 140 | build: CowVec, 141 | } 142 | 143 | // qualifier ::= ( '-' pre )? ( '+' build )? 144 | fn qualifier(input: &str) -> ParseResult<'_, Qualifier> { 145 | let (input, pre_parts) = maybe(pre)(input)?; 146 | let (input, build_parts) = maybe(build)(input)?; 147 | Ok(( 148 | input, 149 | Qualifier { 150 | pre: pre_parts.unwrap_or_default(), 151 | build: build_parts.unwrap_or_default(), 152 | }, 153 | )) 154 | } 155 | 156 | // pre ::= parts 157 | fn pre(input: &str) -> ParseResult<'_, CowVec> { 158 | preceded(ch('-'), parts)(input) 159 | } 160 | 161 | // build ::= parts 162 | fn build(input: &str) -> ParseResult<'_, CowVec> { 163 | preceded(ch('+'), parts)(input) 164 | } 165 | 166 | // parts ::= part ( '.' part ) * 167 | fn parts(input: &str) -> ParseResult<'_, CowVec> { 168 | if_true( 169 | map(separated_list(part, ch('.')), |text| { 170 | text 171 | .into_iter() 172 | .map(VersionPreOrBuild::from_str) 173 | .collect::>() 174 | }), 175 | |items| !items.is_empty(), 176 | )(input) 177 | } 178 | 179 | // part ::= nr | [-0-9A-Za-z]+ 180 | fn part(input: &str) -> ParseResult<'_, &str> { 181 | // nr is in the other set, so don't bother checking for it 182 | if_true( 183 | take_while(|c| c.is_ascii_alphanumeric() || c == '-'), 184 | |result| !result.is_empty(), 185 | )(input) 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use super::super::Version; 191 | use super::*; 192 | 193 | struct VersionReqTester(VersionReq); 194 | 195 | impl VersionReqTester { 196 | fn new(text: &str) -> Self { 197 | Self(parse_version_req_from_specifier(text).unwrap()) 198 | } 199 | 200 | fn matches(&self, version: &str) -> bool { 201 | self.0.matches(&Version::parse_from_npm(version).unwrap()) 202 | } 203 | } 204 | 205 | #[test] 206 | fn version_req_exact() { 207 | let tester = VersionReqTester::new("1.0.1"); 208 | assert!(!tester.matches("1.0.0")); 209 | assert!(tester.matches("1.0.1")); 210 | assert!(!tester.matches("1.0.2")); 211 | assert!(!tester.matches("1.1.1")); 212 | 213 | // pre-release 214 | let tester = VersionReqTester::new("1.0.0-alpha.13"); 215 | assert!(tester.matches("1.0.0-alpha.13")); 216 | } 217 | 218 | #[test] 219 | fn version_req_minor() { 220 | let tester = VersionReqTester::new("1.1"); 221 | assert!(!tester.matches("1.0.0")); 222 | assert!(tester.matches("1.1.0")); 223 | assert!(tester.matches("1.1.1")); 224 | assert!(!tester.matches("1.2.0")); 225 | assert!(!tester.matches("1.2.1")); 226 | } 227 | 228 | #[test] 229 | fn version_req_caret() { 230 | let tester = VersionReqTester::new("^1.1.1"); 231 | assert!(!tester.matches("1.1.0")); 232 | assert!(tester.matches("1.1.1")); 233 | assert!(tester.matches("1.1.2")); 234 | assert!(tester.matches("1.2.0")); 235 | assert!(!tester.matches("2.0.0")); 236 | 237 | let tester = VersionReqTester::new("^0.1.1"); 238 | assert!(!tester.matches("0.0.0")); 239 | assert!(!tester.matches("0.1.0")); 240 | assert!(tester.matches("0.1.1")); 241 | assert!(tester.matches("0.1.2")); 242 | assert!(!tester.matches("0.2.0")); 243 | assert!(!tester.matches("1.0.0")); 244 | 245 | let tester = VersionReqTester::new("^0.0.1"); 246 | assert!(!tester.matches("0.0.0")); 247 | assert!(tester.matches("0.0.1")); 248 | assert!(!tester.matches("0.0.2")); 249 | assert!(!tester.matches("0.1.0")); 250 | assert!(!tester.matches("1.0.0")); 251 | } 252 | 253 | #[test] 254 | fn version_req_tilde() { 255 | let tester = VersionReqTester::new("~1.1.1"); 256 | assert!(!tester.matches("1.1.0")); 257 | assert!(tester.matches("1.1.1")); 258 | assert!(tester.matches("1.1.2")); 259 | assert!(!tester.matches("1.2.0")); 260 | assert!(!tester.matches("2.0.0")); 261 | 262 | let tester = VersionReqTester::new("~0.1.1"); 263 | assert!(!tester.matches("0.0.0")); 264 | assert!(!tester.matches("0.1.0")); 265 | assert!(tester.matches("0.1.1")); 266 | assert!(tester.matches("0.1.2")); 267 | assert!(!tester.matches("0.2.0")); 268 | assert!(!tester.matches("1.0.0")); 269 | 270 | let tester = VersionReqTester::new("~0.0.1"); 271 | assert!(!tester.matches("0.0.0")); 272 | assert!(tester.matches("0.0.1")); 273 | assert!(tester.matches("0.0.2")); // for some reason this matches, but not with ^ 274 | assert!(!tester.matches("0.1.0")); 275 | assert!(!tester.matches("1.0.0")); 276 | } 277 | 278 | #[test] 279 | fn version_req_pre_release() { 280 | let tester = VersionReqTester::new("^1.0.1-pre-release"); 281 | assert!(!tester.matches("1.0.0")); 282 | assert!(tester.matches("1.0.1")); 283 | assert!(tester.matches("1.0.1-pre-release")); 284 | 285 | // zero version 286 | let tester = VersionReqTester::new("^0.0.0-pre-release"); 287 | assert!(tester.matches("0.0.0")); 288 | assert!(!tester.matches("0.0.1")); 289 | assert!(tester.matches("0.0.0-pre-release")); 290 | } 291 | 292 | #[test] 293 | fn parses_tag() { 294 | let latest_tag = VersionReq::parse_from_specifier("latest").unwrap(); 295 | assert_eq!(latest_tag.tag().unwrap(), "latest"); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/string.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::borrow::Borrow; 4 | use std::ops::Deref; 5 | 6 | use capacity_builder::StringAppendable; 7 | use capacity_builder::StringType; 8 | use serde::Deserialize; 9 | use serde::Serialize; 10 | 11 | macro_rules! shared { 12 | ($ident:ident) => { 13 | impl $ident { 14 | #[inline(always)] 15 | pub fn from_cow(cow: std::borrow::Cow) -> Self { 16 | match cow { 17 | std::borrow::Cow::Borrowed(s) => Self::from_str(s), 18 | std::borrow::Cow::Owned(s) => Self::from_string(s), 19 | } 20 | } 21 | 22 | #[inline(always)] 23 | pub fn as_str(&self) -> &str { 24 | self.0.as_str() 25 | } 26 | 27 | #[inline(always)] 28 | pub fn push(&mut self, c: char) { 29 | self.0.push(c); 30 | } 31 | 32 | #[inline(always)] 33 | pub fn push_str(&mut self, s: &str) { 34 | self.0.push_str(s); 35 | } 36 | 37 | #[inline(always)] 38 | pub fn to_string(&self) -> String { 39 | self.0.to_string() 40 | } 41 | } 42 | 43 | impl std::fmt::Display for $ident { 44 | #[inline(always)] 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | self.0.fmt(f) 47 | } 48 | } 49 | 50 | impl Deref for $ident { 51 | type Target = str; 52 | 53 | #[inline(always)] 54 | fn deref(&self) -> &Self::Target { 55 | self.0.as_str() 56 | } 57 | } 58 | 59 | impl Borrow for $ident { 60 | #[inline(always)] 61 | fn borrow(&self) -> &str { 62 | self.as_str() 63 | } 64 | } 65 | 66 | impl AsRef for $ident { 67 | fn as_ref(&self) -> &std::path::Path { 68 | std::path::Path::new(self.0.as_str()) 69 | } 70 | } 71 | 72 | impl PartialEq for $ident { 73 | #[inline(always)] 74 | fn eq(&self, other: &str) -> bool { 75 | self.0.as_str() == other 76 | } 77 | } 78 | 79 | impl PartialEq<&str> for $ident { 80 | #[inline(always)] 81 | fn eq(&self, other: &&str) -> bool { 82 | self.0.as_str() == *other 83 | } 84 | } 85 | 86 | impl PartialEq for $ident { 87 | #[inline(always)] 88 | fn eq(&self, other: &String) -> bool { 89 | self.0.as_str() == other 90 | } 91 | } 92 | 93 | impl PartialEq<&String> for $ident { 94 | #[inline(always)] 95 | fn eq(&self, other: &&String) -> bool { 96 | self.0.as_str() == other.as_str() 97 | } 98 | } 99 | 100 | impl PartialEq<$ident> for str { 101 | #[inline(always)] 102 | fn eq(&self, other: &$ident) -> bool { 103 | self == other.0 104 | } 105 | } 106 | 107 | impl PartialEq<$ident> for &str { 108 | #[inline(always)] 109 | fn eq(&self, other: &$ident) -> bool { 110 | *self == other.0 111 | } 112 | } 113 | 114 | impl PartialEq<$ident> for String { 115 | #[inline(always)] 116 | fn eq(&self, other: &$ident) -> bool { 117 | self.as_str() == other.0.as_str() 118 | } 119 | } 120 | 121 | impl PartialEq<&$ident> for String { 122 | #[inline(always)] 123 | fn eq(&self, other: &&$ident) -> bool { 124 | self.as_str() == other.0.as_str() 125 | } 126 | } 127 | 128 | impl<'a> StringAppendable<'a> for &'a $ident { 129 | #[inline(always)] 130 | fn append_to_builder( 131 | self, 132 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 133 | ) { 134 | builder.append(self.0.as_str()) 135 | } 136 | } 137 | }; 138 | } 139 | 140 | #[cfg(any(unix, windows))] 141 | mod stack_string { 142 | use serde::Deserializer; 143 | use serde::de::Error; 144 | use serde::de::Visitor; 145 | use std::fmt; 146 | 147 | use super::*; 148 | 149 | /// A 24 byte string that uses the stack when < 24 bytes. 150 | #[derive( 151 | Debug, Default, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, 152 | )] 153 | pub struct StackString(hipstr::HipStr<'static>); 154 | 155 | // todo(dsherret): remove once https://github.com/polazarus/hipstr/pull/38 lands 156 | struct StackStringVisitor; 157 | 158 | impl Visitor<'_> for StackStringVisitor { 159 | type Value = StackString; 160 | 161 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 162 | formatter.write_str("a string") 163 | } 164 | 165 | #[inline(always)] 166 | fn visit_str(self, v: &str) -> Result 167 | where 168 | E: Error, 169 | { 170 | Ok(StackString::from_str(v)) 171 | } 172 | } 173 | 174 | impl<'de> serde::Deserialize<'de> for StackString { 175 | #[inline(always)] 176 | fn deserialize(deserializer: D) -> Result 177 | where 178 | D: Deserializer<'de>, 179 | { 180 | deserializer.deserialize_str(StackStringVisitor) 181 | } 182 | } 183 | 184 | shared!(StackString); 185 | 186 | impl StackString { 187 | #[inline(always)] 188 | pub fn with_capacity(size: usize) -> Self { 189 | Self(hipstr::HipStr::with_capacity(size)) 190 | } 191 | 192 | #[inline(always)] 193 | pub fn from_static(s: &'static str) -> Self { 194 | Self(hipstr::HipStr::from_static(s)) 195 | } 196 | 197 | /// Creates a `StackString` from a `&str`. 198 | #[inline(always)] 199 | #[allow(clippy::should_implement_trait)] 200 | pub fn from_str(s: &str) -> Self { 201 | Self(hipstr::HipStr::from(s)) 202 | } 203 | 204 | /// Creates a `StackString` from a `String`. 205 | /// 206 | /// Generally you don't want to end up with a `String` in the first 207 | /// place, which is why this struct doesn't implement `From` 208 | #[inline(always)] 209 | pub fn from_string(s: String) -> Self { 210 | Self(hipstr::HipStr::from(s)) 211 | } 212 | 213 | pub fn replace(&self, from: &str, to: &str) -> Self { 214 | // hipstr currently doesn't have a targeted replace method 215 | Self(self.0.replace(from, to).into()) 216 | } 217 | 218 | pub fn into_string(self) -> String { 219 | match self.0.into_string() { 220 | Ok(value) => value, 221 | Err(existing) => existing.to_string(), 222 | } 223 | } 224 | } 225 | 226 | impl StringType for StackString { 227 | type MutType = hipstr::HipStr<'static>; 228 | 229 | #[inline(always)] 230 | fn with_capacity( 231 | size: usize, 232 | ) -> Result { 233 | Ok(hipstr::HipStr::with_capacity(size)) 234 | } 235 | 236 | #[inline(always)] 237 | fn from_mut(inner: Self::MutType) -> Self { 238 | Self(inner) 239 | } 240 | } 241 | 242 | // Note: Do NOT implement `From` in order to discourage its use 243 | // because we shouldn't end up with a `String` in the first place. 244 | 245 | // It would be nice to only implement this for 'static strings, but unfortunately 246 | // rust has trouble giving nice error messages when trying to do that and requiring 247 | // having to write `StackString::from_str` in test code instead of `"something".into()` 248 | // is not very nice. 249 | impl From<&str> for StackString { 250 | #[inline(always)] 251 | fn from(s: &str) -> Self { 252 | Self(hipstr::HipStr::from(s)) 253 | } 254 | } 255 | } 256 | 257 | mod small_stack_string { 258 | use super::*; 259 | 260 | /// A 16 byte string that uses the stack when < 16 bytes. 261 | #[derive( 262 | Debug, 263 | Default, 264 | Clone, 265 | PartialOrd, 266 | Ord, 267 | PartialEq, 268 | Eq, 269 | Hash, 270 | Serialize, 271 | Deserialize, 272 | )] 273 | pub struct SmallStackString(ecow::EcoString); 274 | 275 | shared!(SmallStackString); 276 | 277 | impl SmallStackString { 278 | #[inline(always)] 279 | pub fn from_static(s: &'static str) -> Self { 280 | Self(ecow::EcoString::from(s)) 281 | } 282 | 283 | #[inline(always)] 284 | pub fn with_capacity(size: usize) -> Self { 285 | Self(ecow::EcoString::with_capacity(size)) 286 | } 287 | 288 | /// Creates a `SmallStackString` from a `&str`. 289 | #[inline(always)] 290 | #[allow(clippy::should_implement_trait)] 291 | pub fn from_str(s: &str) -> Self { 292 | Self(ecow::EcoString::from(s)) 293 | } 294 | 295 | /// Creates a `SmallStackString` from a `String`. 296 | /// 297 | /// Generally you don't want to end up with a `String` in the first 298 | /// place, which is why this struct doesn't implement `From` 299 | #[inline(always)] 300 | pub fn from_string(s: String) -> Self { 301 | Self(ecow::EcoString::from(s)) 302 | } 303 | 304 | pub fn replace(&self, from: &str, to: &str) -> Self { 305 | Self(self.0.replace(from, to)) 306 | } 307 | 308 | pub fn into_string(self) -> String { 309 | self.0.into() 310 | } 311 | } 312 | 313 | impl StringType for SmallStackString { 314 | type MutType = ecow::EcoString; 315 | 316 | #[inline(always)] 317 | fn with_capacity( 318 | size: usize, 319 | ) -> Result { 320 | Ok(ecow::EcoString::with_capacity(size)) 321 | } 322 | 323 | #[inline(always)] 324 | fn from_mut(inner: Self::MutType) -> Self { 325 | Self(inner) 326 | } 327 | } 328 | 329 | // Note: Do NOT implement `From` in order to discourage its use 330 | // because we shouldn't end up with a `String` in the first place. 331 | 332 | // It would be nice to only implement this for 'static strings, but unfortunately 333 | // rust has trouble giving nice error messages when trying to do that and requiring 334 | // having to write `SmallStackString::from_str` in test code instead of `"something".into()` 335 | // is not very nice. 336 | impl From<&str> for SmallStackString { 337 | #[inline(always)] 338 | fn from(s: &str) -> Self { 339 | Self(ecow::EcoString::from(s)) 340 | } 341 | } 342 | } 343 | 344 | // This module is for comparing the implementations above with a regular `String`. 345 | // For example, do `pub regular_string::RegularString as StackString;` 346 | mod regular_string { 347 | use super::*; 348 | 349 | #[derive( 350 | Debug, 351 | Default, 352 | Clone, 353 | PartialOrd, 354 | Ord, 355 | PartialEq, 356 | Eq, 357 | Hash, 358 | Serialize, 359 | Deserialize, 360 | )] 361 | pub struct RegularString(String); 362 | 363 | shared!(RegularString); 364 | 365 | impl RegularString { 366 | #[inline(always)] 367 | pub fn from_static(s: &'static str) -> Self { 368 | Self(String::from(s)) 369 | } 370 | 371 | #[inline(always)] 372 | pub fn with_capacity(size: usize) -> Self { 373 | Self(String::with_capacity(size)) 374 | } 375 | 376 | /// Creates a `SmallStackString` from a `&str`. 377 | #[inline(always)] 378 | #[allow(clippy::should_implement_trait)] 379 | pub fn from_str(s: &str) -> Self { 380 | Self(String::from(s)) 381 | } 382 | 383 | /// Creates a `SmallStackString` from a `String`. 384 | /// 385 | /// Generally you don't want to end up with a `String` in the first 386 | /// place, which is why this struct doesn't implement `From` 387 | #[inline(always)] 388 | pub fn from_string(s: String) -> Self { 389 | Self(s) 390 | } 391 | 392 | pub fn replace(&self, from: &str, to: &str) -> Self { 393 | Self(self.0.replace(from, to)) 394 | } 395 | 396 | pub fn into_string(self) -> String { 397 | self.0 398 | } 399 | } 400 | 401 | impl StringType for RegularString { 402 | type MutType = String; 403 | 404 | #[inline(always)] 405 | fn with_capacity( 406 | size: usize, 407 | ) -> Result { 408 | Ok(String::with_capacity(size)) 409 | } 410 | 411 | #[inline(always)] 412 | fn from_mut(inner: Self::MutType) -> Self { 413 | Self(inner) 414 | } 415 | } 416 | 417 | impl From<&str> for RegularString { 418 | #[inline(always)] 419 | fn from(s: &str) -> Self { 420 | Self(String::from(s)) 421 | } 422 | } 423 | } 424 | 425 | // this is here to allow easily swapping implementations 426 | pub use small_stack_string::SmallStackString; 427 | 428 | #[cfg(not(any(unix, windows)))] 429 | pub use regular_string::RegularString as StackString; 430 | #[cfg(any(unix, windows))] 431 | pub use stack_string::StackString; 432 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | #![deny(clippy::print_stderr)] 4 | #![deny(clippy::print_stdout)] 5 | 6 | use std::borrow::Cow; 7 | use std::cmp::Ordering; 8 | use std::fmt; 9 | use std::hash::Hash; 10 | 11 | use capacity_builder::CapacityDisplay; 12 | use capacity_builder::StringAppendable; 13 | use capacity_builder::StringBuilder; 14 | use capacity_builder::StringType; 15 | use once_cell::sync::Lazy; 16 | use serde::Deserialize; 17 | use serde::Serialize; 18 | use thiserror::Error; 19 | 20 | mod common; 21 | pub mod jsr; 22 | pub mod npm; 23 | pub mod package; 24 | mod range; 25 | mod range_set_or_tag; 26 | mod specifier; 27 | mod string; 28 | 29 | /// A smaller two-byte vector. 30 | pub type CowVec = ecow::EcoVec; 31 | pub use string::SmallStackString; 32 | pub use string::StackString; 33 | 34 | pub use self::specifier::VersionReqSpecifierParseError; 35 | 36 | pub use self::range::Partial; 37 | pub use self::range::RangeBound; 38 | pub use self::range::VersionBound; 39 | pub use self::range::VersionBoundKind; 40 | pub use self::range::VersionRange; 41 | pub use self::range::VersionRangeSet; 42 | pub use self::range::XRange; 43 | pub use self::range_set_or_tag::PackageTag; 44 | pub use self::range_set_or_tag::RangeSetOrTag; 45 | 46 | /// Specifier that points to the wildcard version. 47 | pub static WILDCARD_VERSION_REQ: Lazy = 48 | Lazy::new(|| VersionReq::parse_from_specifier("*").unwrap()); 49 | 50 | #[derive(Error, Debug, Clone, deno_error::JsError)] 51 | #[class(type)] 52 | #[error("Invalid version")] 53 | pub struct VersionParseError { 54 | #[source] 55 | source: monch::ParseErrorFailureError, 56 | } 57 | 58 | pub type VersionPreOrBuild = SmallStackString; 59 | 60 | #[derive(Clone, Debug, PartialEq, Eq, Default, Hash, CapacityDisplay)] 61 | pub struct Version { 62 | pub major: u64, 63 | pub minor: u64, 64 | pub patch: u64, 65 | pub pre: CowVec, 66 | pub build: CowVec, 67 | } 68 | 69 | impl<'a> StringAppendable<'a> for &'a Version { 70 | fn append_to_builder( 71 | self, 72 | builder: &mut StringBuilder<'a, TString>, 73 | ) { 74 | builder.append(self.major); 75 | builder.append('.'); 76 | builder.append(self.minor); 77 | builder.append('.'); 78 | builder.append(self.patch); 79 | if !self.pre.is_empty() { 80 | builder.append('-'); 81 | for (i, part) in self.pre.iter().enumerate() { 82 | if i > 0 { 83 | builder.append('.'); 84 | } 85 | builder.append(part); 86 | } 87 | } 88 | if !self.build.is_empty() { 89 | builder.append('+'); 90 | for (i, part) in self.build.iter().enumerate() { 91 | if i > 0 { 92 | builder.append('.'); 93 | } 94 | builder.append(part); 95 | } 96 | } 97 | } 98 | } 99 | 100 | impl Serialize for Version { 101 | fn serialize(&self, serializer: S) -> Result 102 | where 103 | S: serde::Serializer, 104 | { 105 | serializer.serialize_str(&self.to_string()) 106 | } 107 | } 108 | 109 | impl<'de> Deserialize<'de> for Version { 110 | fn deserialize(deserializer: D) -> Result 111 | where 112 | D: serde::Deserializer<'de>, 113 | { 114 | let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?; 115 | match Version::parse_standard(&text) { 116 | Ok(version) => Ok(version), 117 | Err(err) => Err(serde::de::Error::custom(err)), 118 | } 119 | } 120 | } 121 | 122 | impl Version { 123 | /// Parse a version. 124 | pub fn parse_standard(text: &str) -> Result { 125 | // re-use npm's loose version parsing 126 | Self::parse_from_npm(text) 127 | .map_err(|err| VersionParseError { source: err.source }) 128 | } 129 | 130 | /// Parse a version from npm. 131 | pub fn parse_from_npm( 132 | text: &str, 133 | ) -> Result { 134 | npm::parse_npm_version(text) 135 | } 136 | 137 | /// Creates a version requirement that's pinned to this version. 138 | pub fn into_req(self) -> VersionReq { 139 | VersionReq { 140 | raw_text: self.to_custom_string(), 141 | inner: RangeSetOrTag::RangeSet(VersionRangeSet(CowVec::from([ 142 | VersionRange { 143 | start: RangeBound::Version(VersionBound { 144 | kind: VersionBoundKind::Inclusive, 145 | version: self.clone(), 146 | }), 147 | end: RangeBound::Version(VersionBound { 148 | kind: VersionBoundKind::Inclusive, 149 | version: self, 150 | }), 151 | }, 152 | ]))), 153 | } 154 | } 155 | } 156 | 157 | impl std::cmp::PartialOrd for Version { 158 | fn partial_cmp(&self, other: &Self) -> Option { 159 | Some(self.cmp(other)) 160 | } 161 | } 162 | 163 | impl std::cmp::Ord for Version { 164 | fn cmp(&self, other: &Self) -> Ordering { 165 | let cmp_result = self.major.cmp(&other.major); 166 | if cmp_result != Ordering::Equal { 167 | return cmp_result; 168 | } 169 | 170 | let cmp_result = self.minor.cmp(&other.minor); 171 | if cmp_result != Ordering::Equal { 172 | return cmp_result; 173 | } 174 | 175 | let cmp_result = self.patch.cmp(&other.patch); 176 | if cmp_result != Ordering::Equal { 177 | return cmp_result; 178 | } 179 | 180 | // only compare the pre-release and not the build as node-semver does 181 | if self.pre.is_empty() && other.pre.is_empty() { 182 | Ordering::Equal 183 | } else if !self.pre.is_empty() && other.pre.is_empty() { 184 | Ordering::Less 185 | } else if self.pre.is_empty() && !other.pre.is_empty() { 186 | Ordering::Greater 187 | } else { 188 | let mut i = 0; 189 | loop { 190 | let a = self.pre.get(i); 191 | let b = other.pre.get(i); 192 | if a.is_none() && b.is_none() { 193 | return Ordering::Equal; 194 | } 195 | 196 | // https://github.com/npm/node-semver/blob/4907647d169948a53156502867ed679268063a9f/internal/identifiers.js 197 | let a = match a { 198 | Some(a) => a, 199 | None => return Ordering::Less, 200 | }; 201 | let b = match b { 202 | Some(b) => b, 203 | None => return Ordering::Greater, 204 | }; 205 | 206 | // prefer numbers 207 | if let Ok(a_num) = a.parse::() { 208 | if let Ok(b_num) = b.parse::() { 209 | let cmp_result = a_num.cmp(&b_num); 210 | if cmp_result != Ordering::Equal { 211 | return cmp_result; 212 | } 213 | } else { 214 | return Ordering::Less; 215 | } 216 | } else if b.parse::().is_ok() { 217 | return Ordering::Greater; 218 | } 219 | 220 | let cmp_result = a.cmp(b); 221 | if cmp_result != Ordering::Equal { 222 | return cmp_result; 223 | } 224 | i += 1; 225 | } 226 | } 227 | } 228 | } 229 | 230 | pub(crate) fn is_valid_tag(value: &str) -> bool { 231 | // we use the same rules as npm tags 232 | npm::is_valid_npm_tag(value) 233 | } 234 | 235 | #[derive(Error, Debug, Clone, deno_error::JsError, PartialEq, Eq)] 236 | #[class(type)] 237 | #[error("Invalid normalized version requirement")] 238 | pub struct VersionReqNormalizedParseError { 239 | #[source] 240 | source: monch::ParseErrorFailureError, 241 | } 242 | 243 | /// A version constraint. 244 | #[derive(Debug, Clone, Serialize, Deserialize)] 245 | pub struct VersionReq { 246 | raw_text: SmallStackString, 247 | inner: RangeSetOrTag, 248 | } 249 | 250 | impl PartialEq for VersionReq { 251 | fn eq(&self, other: &Self) -> bool { 252 | self.inner == other.inner 253 | } 254 | } 255 | 256 | impl Eq for VersionReq {} 257 | 258 | impl Hash for VersionReq { 259 | fn hash(&self, state: &mut H) { 260 | self.inner.hash(state); 261 | } 262 | } 263 | 264 | impl VersionReq { 265 | /// Creates a version requirement without examining the raw text. 266 | pub fn from_raw_text_and_inner( 267 | raw_text: SmallStackString, 268 | inner: RangeSetOrTag, 269 | ) -> Self { 270 | Self { raw_text, inner } 271 | } 272 | 273 | pub fn parse_from_normalized( 274 | text: &str, 275 | ) -> Result { 276 | use monch::*; 277 | 278 | with_failure_handling(|input| { 279 | map(RangeSetOrTag::parse, |inner| { 280 | VersionReq::from_raw_text_and_inner( 281 | crate::SmallStackString::from_str(input), 282 | inner, 283 | ) 284 | })(input) 285 | })(text) 286 | .map_err(|err| VersionReqNormalizedParseError { source: err }) 287 | } 288 | 289 | pub fn parse_from_specifier( 290 | specifier: &str, 291 | ) -> Result { 292 | specifier::parse_version_req_from_specifier(specifier) 293 | } 294 | 295 | pub fn parse_from_npm( 296 | text: &str, 297 | ) -> Result { 298 | npm::parse_npm_version_req(text) 299 | } 300 | 301 | /// The underlying `RangeSetOrTag`. 302 | pub fn inner(&self) -> &RangeSetOrTag { 303 | &self.inner 304 | } 305 | 306 | /// Gets if this version requirement overlaps another one. 307 | pub fn intersects(&self, other: &VersionReq) -> bool { 308 | self.inner.intersects(&other.inner) 309 | } 310 | 311 | pub fn tag(&self) -> Option<&str> { 312 | match &self.inner { 313 | RangeSetOrTag::RangeSet(_) => None, 314 | RangeSetOrTag::Tag(tag) => Some(tag.as_str()), 315 | } 316 | } 317 | 318 | pub fn range(&self) -> Option<&VersionRangeSet> { 319 | match &self.inner { 320 | RangeSetOrTag::RangeSet(range_set) => Some(range_set), 321 | RangeSetOrTag::Tag(_) => None, 322 | } 323 | } 324 | 325 | pub fn matches(&self, version: &Version) -> bool { 326 | match &self.inner { 327 | RangeSetOrTag::RangeSet(range_set) => range_set.satisfies(version), 328 | RangeSetOrTag::Tag(_) => panic!( 329 | "programming error: cannot use matches with a tag: {}", 330 | self.raw_text 331 | ), 332 | } 333 | } 334 | 335 | pub fn version_text(&self) -> &str { 336 | &self.raw_text 337 | } 338 | 339 | /// Outputs a normalized string representation of the version requirement. 340 | pub fn to_string_normalized(&self) -> String { 341 | self.inner().to_string() 342 | } 343 | } 344 | 345 | impl fmt::Display for VersionReq { 346 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 347 | write!(f, "{}", &self.raw_text) 348 | } 349 | } 350 | 351 | #[cfg(test)] 352 | mod test { 353 | use super::*; 354 | 355 | #[test] 356 | fn serialize_deserialize() { 357 | // should deserialize and serialize with loose parsing 358 | let text = "= v 1.2.3-pre.other+build.test"; 359 | let version: Version = 360 | serde_json::from_str(&format!("\"{text}\"")).unwrap(); 361 | let serialized_version = serde_json::to_string(&version).unwrap(); 362 | assert_eq!(serialized_version, "\"1.2.3-pre.other+build.test\""); 363 | } 364 | 365 | #[test] 366 | fn version_req_intersects() { 367 | let version = |text: &str| VersionReq::parse_from_npm(text).unwrap(); 368 | 369 | // overlapping requirements 370 | { 371 | let req_1_0_0 = version("1.0.0"); 372 | let req_caret_1 = version("^1"); 373 | assert!(req_1_0_0.intersects(&req_caret_1)); // Both represent the 1.0.0 version. 374 | } 375 | { 376 | let req_1_to_3 = version(">=1 <=3"); 377 | let req_2_to_4 = version(">=2 <=4"); 378 | assert!(req_1_to_3.intersects(&req_2_to_4)); // They overlap in the 2.0.0 - 3.0.0 range. 379 | } 380 | 381 | // non-overlapping requirements 382 | { 383 | let req_lt_1 = version("<1"); 384 | let req_gte_1 = version(">=1"); 385 | assert!(!req_lt_1.intersects(&req_gte_1)); // One is less than 1.0.0, the other is 1.0.0 or greater. 386 | } 387 | { 388 | let req_2_to_3 = version(">=2 <3"); 389 | let req_gte_3 = version(">=3"); 390 | assert!(!req_2_to_3.intersects(&req_gte_3)); // Non-overlapping. 391 | } 392 | { 393 | let req_1_incl_2_excl = version("^1"); 394 | let req_2_incl_unbounded = version(">=2"); 395 | assert!(!req_1_incl_2_excl.intersects(&req_2_incl_unbounded)); 396 | } 397 | 398 | // more specific requirements 399 | { 400 | let req_1_2_3 = version("1.2.3"); 401 | let req_1_2_x = version("1.2.x"); 402 | assert!(req_1_2_3.intersects(&req_1_2_x)); // both represent the 1.2.3 version. 403 | } 404 | { 405 | let req_tilde_1_2_3 = version("~1.2.3"); 406 | let req_1_4_0 = version("1.4.0"); 407 | assert!(!req_tilde_1_2_3.intersects(&req_1_4_0)); // no overlap with 1.4.0. 408 | } 409 | 410 | // wildcards 411 | { 412 | let req_star = version("*"); 413 | let req_1_0_0 = version("1.0.0"); 414 | let req_gte_1 = version(">=1"); 415 | assert!(req_star.intersects(&req_1_0_0)); // the '*' allows any version. 416 | assert!(req_star.intersects(&req_gte_1)); // again, '*' allows any version. 417 | } 418 | } 419 | 420 | #[test] 421 | fn version_req_eq() { 422 | let p1 = VersionReq::parse_from_specifier("1").unwrap(); 423 | let p2 = VersionReq::parse_from_specifier("1.x").unwrap(); 424 | assert_eq!(p1, p2); 425 | } 426 | 427 | #[test] 428 | fn version_cmp() { 429 | fn cmp(v1: &str, v2: &str) -> Ordering { 430 | let v1 = Version::parse_standard(v1).unwrap(); 431 | let v2 = Version::parse_standard(v2).unwrap(); 432 | v1.cmp(&v2) 433 | } 434 | 435 | assert_eq!(cmp("1.0.0", "1.0.0-pre"), Ordering::Greater); 436 | assert_eq!(cmp("0.0.0", "0.0.0-pre"), Ordering::Greater); 437 | assert_eq!(cmp("0.0.0-a", "0.0.0-b"), Ordering::Less); 438 | assert_eq!(cmp("0.0.0-a", "0.0.0-a"), Ordering::Equal); 439 | assert_eq!(cmp("2.0.0-rc.3.0.5", "2.0.0-rc.3.0.6"), Ordering::Less); 440 | assert_eq!(cmp("2.0.0-rc.3.0.5", "2.0.0-rc.3.1.0"), Ordering::Less); 441 | assert_eq!(cmp("2.0.0-rc.3.1.0", "2.0.0-rc.3.0.5"), Ordering::Greater); 442 | assert_eq!(cmp("2.0.0-rc.3.1.0", "2.0.0-rc.3.1.0"), Ordering::Equal); 443 | assert_eq!(cmp("2.0.0-rc.3.0.5", "2.0.0"), Ordering::Less); 444 | assert_eq!(cmp("2.0.0-rc.3.0.5", "2.1.0"), Ordering::Less); 445 | assert_eq!(cmp("2.0.0", "2.0.0-rc.3.0.5"), Ordering::Greater); 446 | } 447 | 448 | #[test] 449 | fn version_req_from_version() { 450 | let version = Version::parse_from_npm("1.2.3").unwrap(); 451 | let version_req = version.into_req(); 452 | 453 | assert!(!version_req.matches(&Version::parse_from_npm("1.2.2").unwrap())); 454 | assert!(version_req.matches(&Version::parse_from_npm("1.2.3").unwrap())); 455 | assert!(!version_req.matches(&Version::parse_from_npm("1.2.4").unwrap())); 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/jsr.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::borrow::Cow; 4 | 5 | use capacity_builder::CapacityDisplay; 6 | use capacity_builder::StringAppendable; 7 | use capacity_builder::StringType; 8 | use serde::Deserialize; 9 | use serde::Serialize; 10 | use thiserror::Error; 11 | use url::Url; 12 | 13 | use crate::package::PackageKind; 14 | use crate::package::PackageNv; 15 | use crate::package::PackageNvReference; 16 | use crate::package::PackageNvReferenceParseError; 17 | use crate::package::PackageReq; 18 | use crate::package::PackageReqParseError; 19 | use crate::package::PackageReqReference; 20 | use crate::package::PackageReqReferenceParseError; 21 | 22 | /// A reference to a JSR package's name, version constraint, and potential sub path. 23 | /// This contains all the information found in an npm specifier. 24 | /// 25 | /// This wraps PackageReqReference in order to prevent accidentally 26 | /// mixing this with other schemes. 27 | #[derive(Clone, Debug, PartialEq, Eq, Hash, CapacityDisplay)] 28 | pub struct JsrPackageReqReference(PackageReqReference); 29 | 30 | impl<'a> StringAppendable<'a> for &'a JsrPackageReqReference { 31 | fn append_to_builder( 32 | self, 33 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 34 | ) { 35 | builder.append("jsr:"); 36 | builder.append(&self.0); 37 | } 38 | } 39 | 40 | impl JsrPackageReqReference { 41 | pub fn new(inner: PackageReqReference) -> Self { 42 | Self(inner) 43 | } 44 | 45 | pub fn from_specifier( 46 | specifier: &Url, 47 | ) -> Result { 48 | Self::from_str(specifier.as_str()) 49 | } 50 | 51 | #[allow(clippy::should_implement_trait)] 52 | pub fn from_str( 53 | specifier: &str, 54 | ) -> Result { 55 | PackageReqReference::from_str(specifier, PackageKind::Jsr).map(Self) 56 | } 57 | 58 | pub fn req(&self) -> &PackageReq { 59 | &self.0.req 60 | } 61 | 62 | pub fn sub_path(&self) -> Option<&str> { 63 | self.0.sub_path.as_deref() 64 | } 65 | 66 | /// Package sub path normalized as a JSR export name. 67 | pub fn export_name(&self) -> Cow<'_, str> { 68 | normalized_export_name(self.sub_path()) 69 | } 70 | 71 | pub fn into_inner(self) -> PackageReqReference { 72 | self.0 73 | } 74 | } 75 | 76 | /// An JSR package name and version with a potential subpath. 77 | /// 78 | /// This wraps PackageNvReference in order to prevent accidentally 79 | /// mixing this with other schemes. 80 | #[derive( 81 | Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, CapacityDisplay, 82 | )] 83 | pub struct JsrPackageNvReference(PackageNvReference); 84 | 85 | impl JsrPackageNvReference { 86 | pub fn new(inner: PackageNvReference) -> Self { 87 | Self(inner) 88 | } 89 | 90 | pub fn from_specifier( 91 | specifier: &Url, 92 | ) -> Result { 93 | Self::from_str(specifier.as_str()) 94 | } 95 | 96 | #[allow(clippy::should_implement_trait)] 97 | pub fn from_str(nv: &str) -> Result { 98 | PackageNvReference::from_str(nv, PackageKind::Jsr).map(Self) 99 | } 100 | 101 | pub fn as_specifier(&self) -> Url { 102 | self.0.as_specifier(PackageKind::Jsr) 103 | } 104 | 105 | pub fn nv(&self) -> &PackageNv { 106 | &self.0.nv 107 | } 108 | 109 | pub fn sub_path(&self) -> Option<&str> { 110 | self.0.sub_path.as_deref() 111 | } 112 | 113 | /// Package sub path normalized as a JSR export name. 114 | pub fn export_name(&self) -> Cow<'_, str> { 115 | normalized_export_name(self.sub_path()) 116 | } 117 | 118 | pub fn into_inner(self) -> PackageNvReference { 119 | self.0 120 | } 121 | } 122 | 123 | impl<'a> StringAppendable<'a> for &'a JsrPackageNvReference { 124 | fn append_to_builder( 125 | self, 126 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 127 | ) { 128 | builder.append("jsr:"); 129 | builder.append(&self.0); 130 | } 131 | } 132 | 133 | impl Serialize for JsrPackageNvReference { 134 | fn serialize(&self, serializer: S) -> Result 135 | where 136 | S: serde::Serializer, 137 | { 138 | serializer.serialize_str(&self.to_string()) 139 | } 140 | } 141 | 142 | impl<'de> Deserialize<'de> for JsrPackageNvReference { 143 | fn deserialize(deserializer: D) -> Result 144 | where 145 | D: serde::Deserializer<'de>, 146 | { 147 | let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?; 148 | match Self::from_str(&text) { 149 | Ok(req) => Ok(req), 150 | Err(err) => Err(serde::de::Error::custom(err)), 151 | } 152 | } 153 | } 154 | 155 | pub fn normalized_export_name(sub_path: Option<&str>) -> Cow<'_, str> { 156 | let Some(sub_path) = sub_path else { 157 | return Cow::Borrowed("."); 158 | }; 159 | if sub_path.is_empty() || matches!(sub_path, "/" | ".") { 160 | Cow::Borrowed(".") 161 | } else { 162 | let sub_path = sub_path.strip_suffix('/').unwrap_or(sub_path); 163 | if sub_path.starts_with("./") { 164 | Cow::Borrowed(sub_path) 165 | } else { 166 | let sub_path = sub_path.strip_prefix('/').unwrap_or(sub_path); 167 | Cow::Owned(format!("./{}", sub_path)) 168 | } 169 | } 170 | } 171 | 172 | #[derive(Error, Debug, Clone, deno_error::JsError)] 173 | pub enum JsrDepPackageReqParseError { 174 | #[class(type)] 175 | #[error("Unexpected JSR dependency scheme '{}'. Expected 'npm:' or 'jsr:'", .0)] 176 | NotExpectedScheme(String), 177 | #[class(inherit)] 178 | #[error(transparent)] 179 | PackageReqParse(#[from] PackageReqParseError), 180 | } 181 | 182 | /// A package constraint for a JSR dependency which could be from npm or JSR. 183 | #[derive( 184 | Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, CapacityDisplay, 185 | )] 186 | pub struct JsrDepPackageReq { 187 | pub kind: PackageKind, 188 | pub req: PackageReq, 189 | } 190 | 191 | impl<'a> StringAppendable<'a> for &'a JsrDepPackageReq { 192 | fn append_to_builder( 193 | self, 194 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 195 | ) { 196 | builder.append(self.kind.scheme_with_colon()); 197 | builder.append(&self.req); 198 | } 199 | } 200 | 201 | impl Serialize for JsrDepPackageReq { 202 | fn serialize(&self, serializer: S) -> Result 203 | where 204 | S: serde::Serializer, 205 | { 206 | serializer.serialize_str(&self.to_string_normalized()) 207 | } 208 | } 209 | 210 | impl<'de> Deserialize<'de> for JsrDepPackageReq { 211 | fn deserialize(deserializer: D) -> Result 212 | where 213 | D: serde::Deserializer<'de>, 214 | { 215 | let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?; 216 | match Self::from_str_normalized(&text) { 217 | Ok(req) => Ok(req), 218 | Err(err) => Err(serde::de::Error::custom(err)), 219 | } 220 | } 221 | } 222 | 223 | impl JsrDepPackageReq { 224 | pub fn jsr(req: PackageReq) -> Self { 225 | Self { 226 | kind: PackageKind::Jsr, 227 | req, 228 | } 229 | } 230 | 231 | pub fn npm(req: PackageReq) -> Self { 232 | Self { 233 | kind: PackageKind::Npm, 234 | req, 235 | } 236 | } 237 | 238 | #[allow(clippy::should_implement_trait)] 239 | pub fn from_str(text: &str) -> Result { 240 | Self::from_str_inner(text, PackageReq::from_str) 241 | } 242 | 243 | pub fn from_str_loose( 244 | text: &str, 245 | ) -> Result { 246 | Self::from_str_inner(text, PackageReq::from_str_loose) 247 | } 248 | 249 | pub fn from_str_normalized( 250 | text: &str, 251 | ) -> Result { 252 | Self::from_str_inner(text, PackageReq::from_str_normalized) 253 | } 254 | 255 | fn from_str_inner( 256 | text: &str, 257 | parse_req: impl FnOnce(&str) -> Result, 258 | ) -> Result { 259 | if let Some(req) = text.strip_prefix("jsr:") { 260 | Ok(Self::jsr(parse_req(req)?)) 261 | } else if let Some(req) = text.strip_prefix("npm:") { 262 | Ok(Self::npm(parse_req(req)?)) 263 | } else { 264 | Err(JsrDepPackageReqParseError::NotExpectedScheme( 265 | text 266 | .split_once(':') 267 | .map(|(scheme, _)| scheme) 268 | .unwrap_or(text) 269 | .to_string(), 270 | )) 271 | } 272 | } 273 | 274 | /// Outputs a normalized string representation of this dependency. 275 | /// 276 | /// Note: The normalized string is not safe for a URL. It's best used for serialization. 277 | pub fn to_string_normalized(&self) -> crate::StackString { 278 | capacity_builder::StringBuilder::build(|builder| { 279 | builder.append(self.kind.scheme_with_colon()); 280 | builder.append(&self.req.name); 281 | builder.append('@'); 282 | builder.append(self.req.version_req.inner()); 283 | }) 284 | .unwrap() 285 | } 286 | } 287 | 288 | #[cfg(test)] 289 | mod test { 290 | use crate::package::PackageReqReferenceInvalidWithVersionParseError; 291 | 292 | use super::*; 293 | 294 | #[test] 295 | fn jsr_req_ref() { 296 | { 297 | let req_ref = JsrPackageReqReference::from_specifier( 298 | &Url::parse("jsr:/foo").unwrap(), 299 | ) 300 | .unwrap(); 301 | assert_eq!(req_ref.req().name, "foo"); 302 | assert_eq!(req_ref.req().version_req.to_string(), "*"); 303 | assert_eq!(req_ref.sub_path(), None); 304 | } 305 | { 306 | let req_ref = 307 | JsrPackageReqReference::from_str("jsr:foo@1/mod.ts").unwrap(); 308 | assert_eq!(req_ref.req().name, "foo"); 309 | assert_eq!(req_ref.req().version_req.to_string(), "1"); 310 | assert_eq!(req_ref.sub_path(), Some("mod.ts")); 311 | } 312 | { 313 | let req_ref = 314 | JsrPackageReqReference::from_str("jsr:@scope/foo@^1.0.0/mod.ts") 315 | .unwrap(); 316 | assert_eq!(req_ref.req().name, "@scope/foo"); 317 | assert_eq!(req_ref.req().version_req.to_string(), "^1.0.0"); 318 | assert_eq!(req_ref.sub_path(), Some("mod.ts")); 319 | } 320 | { 321 | assert_eq!( 322 | JsrPackageReqReference::from_str("jsr:@std/testing/bdd@1").unwrap_err(), 323 | PackageReqReferenceParseError::InvalidPathWithVersion(Box::new( 324 | PackageReqReferenceInvalidWithVersionParseError { 325 | kind: PackageKind::Jsr, 326 | current: "@std/testing/bdd@1".to_string(), 327 | suggested: "@std/testing@1/bdd".to_string(), 328 | } 329 | )), 330 | ); 331 | } 332 | } 333 | 334 | #[test] 335 | fn jsr_nv_ref() { 336 | { 337 | let nv_ref = JsrPackageNvReference::from_specifier( 338 | &Url::parse("jsr:/foo@1.0.0").unwrap(), 339 | ) 340 | .unwrap(); 341 | assert_eq!(nv_ref.nv().name, "foo"); 342 | assert_eq!(nv_ref.nv().version.to_string(), "1.0.0"); 343 | assert_eq!(nv_ref.sub_path(), None); 344 | assert_eq!(nv_ref.as_specifier().as_str(), "jsr:/foo@1.0.0"); 345 | } 346 | { 347 | let nv_ref = 348 | JsrPackageNvReference::from_str("jsr:foo@1.0.0/mod.ts").unwrap(); 349 | assert_eq!(nv_ref.nv().name, "foo"); 350 | assert_eq!(nv_ref.nv().version.to_string(), "1.0.0"); 351 | assert_eq!(nv_ref.sub_path(), Some("mod.ts")); 352 | assert_eq!(nv_ref.as_specifier().as_str(), "jsr:/foo@1.0.0/mod.ts"); 353 | } 354 | { 355 | let nv_ref = 356 | JsrPackageNvReference::from_str("jsr:@scope/foo@1.0.0/mod.ts").unwrap(); 357 | assert_eq!(nv_ref.nv().name, "@scope/foo"); 358 | assert_eq!(nv_ref.nv().version.to_string(), "1.0.0"); 359 | assert_eq!(nv_ref.sub_path(), Some("mod.ts")); 360 | assert_eq!( 361 | nv_ref.as_specifier().as_str(), 362 | "jsr:/@scope/foo@1.0.0/mod.ts" 363 | ); 364 | } 365 | } 366 | 367 | #[test] 368 | fn jsr_dep_package_req_display() { 369 | assert_eq!( 370 | JsrDepPackageReq::jsr(PackageReq::from_str("b@1").unwrap()).to_string(), 371 | "jsr:b@1" 372 | ); 373 | assert_eq!( 374 | JsrDepPackageReq::npm(PackageReq::from_str("c@1").unwrap()).to_string(), 375 | "npm:c@1" 376 | ); 377 | } 378 | 379 | #[test] 380 | fn test_normalized_export_name() { 381 | fn run_test(sub_path: &str, expected: &str) { 382 | assert_eq!(normalized_export_name(Some(sub_path)), expected); 383 | } 384 | 385 | run_test("mod.ts", "./mod.ts"); 386 | run_test("test/", "./test"); 387 | run_test("./test/", "./test"); 388 | run_test("./test", "./test"); 389 | run_test("/test", "./test"); 390 | run_test("", "."); 391 | } 392 | 393 | #[test] 394 | fn test_jsr_dep_pkg_req_from_str() { 395 | { 396 | let result = JsrDepPackageReq::from_str("jsr:a@^1.0").unwrap(); 397 | assert_eq!(result.kind, PackageKind::Jsr); 398 | assert_eq!(result.req.to_string(), "a@^1.0"); 399 | } 400 | { 401 | let result = JsrDepPackageReq::from_str("npm:a@^1.0").unwrap(); 402 | assert_eq!(result.kind, PackageKind::Npm); 403 | assert_eq!(result.req.to_string(), "a@^1.0"); 404 | } 405 | { 406 | let err = JsrDepPackageReq::from_str("other:a@^1.0").unwrap_err(); 407 | match err { 408 | JsrDepPackageReqParseError::NotExpectedScheme(scheme) => { 409 | assert_eq!(scheme, "other"); 410 | } 411 | _ => unreachable!(), 412 | } 413 | } 414 | } 415 | 416 | #[test] 417 | fn test_jsr_dep_pkg_req_serializable() { 418 | fn run_test(text: &str) { 419 | let start = JsrDepPackageReq::from_str_loose(text).unwrap(); 420 | let value = serde_json::to_value(&start).unwrap(); 421 | let deserialized: JsrDepPackageReq = 422 | serde_json::from_value(value).unwrap(); 423 | assert_eq!(deserialized, start); 424 | } 425 | 426 | run_test("jsr:a@1"); 427 | run_test("npm:a@1"); 428 | run_test("npm:a@1 - 1.5"); 429 | run_test("npm:a@1 || 2 || 3"); 430 | } 431 | 432 | #[test] 433 | fn test_jsr_dep_pkg_req_normalized() { 434 | #[track_caller] 435 | fn run_test(text: &str, expected: &str) { 436 | let start = JsrDepPackageReq::from_str_loose(text).unwrap(); 437 | let normalized = start.to_string_normalized(); 438 | assert_eq!(normalized, expected); 439 | // ensure it works when parsing back as loose 440 | assert_eq!( 441 | JsrDepPackageReq::from_str_loose(&normalized).unwrap(), 442 | start 443 | ); 444 | } 445 | 446 | // the main tests for this are done on RangeSet 447 | run_test("jsr:a", "jsr:a@*"); 448 | run_test("jsr:a@^1.0", "jsr:a@1"); 449 | run_test("jsr:a@1.2.3 || 1.4.5", "jsr:a@1.2.3 || 1.4.5"); // note: this is the serialized form--it's not a url 450 | } 451 | 452 | #[test] 453 | fn serialize_deserialize_tag_package_req_with_v() { 454 | // note: this specifier is a tag and not a version 455 | let package_req = JsrDepPackageReq::from_str("npm:test@v1.0").unwrap(); 456 | assert!(package_req.req.version_req.tag().is_some()); 457 | let json = serde_json::to_string(&package_req).unwrap(); 458 | assert_eq!(json, "\"npm:test@v1.0\""); 459 | let result = serde_json::from_str::(&json).unwrap(); 460 | assert!(result.req.version_req.tag().is_some()); 461 | assert_eq!(result, package_req); 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.10" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.1.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 16 | 17 | [[package]] 18 | name = "bitflags" 19 | version = "2.6.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 22 | 23 | [[package]] 24 | name = "capacity_builder" 25 | version = "0.5.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8f2d24a6dcf0cd402a21b65d35340f3a49ff3475dc5fdac91d22d2733e6641c6" 28 | dependencies = [ 29 | "capacity_builder_macros", 30 | "ecow", 31 | "hipstr", 32 | "itoa", 33 | ] 34 | 35 | [[package]] 36 | name = "capacity_builder_macros" 37 | version = "0.3.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b4a6cae9efc04cc6cbb8faf338d2c497c165c83e74509cf4dbedea948bbf6e5" 40 | dependencies = [ 41 | "quote", 42 | "syn 2.0.89", 43 | ] 44 | 45 | [[package]] 46 | name = "cfg-if" 47 | version = "1.0.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 50 | 51 | [[package]] 52 | name = "clap" 53 | version = "4.5.23" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 56 | dependencies = [ 57 | "clap_builder", 58 | ] 59 | 60 | [[package]] 61 | name = "clap_builder" 62 | version = "4.5.23" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 65 | dependencies = [ 66 | "anstyle", 67 | "clap_lex", 68 | "terminal_size", 69 | ] 70 | 71 | [[package]] 72 | name = "clap_lex" 73 | version = "0.7.4" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 76 | 77 | [[package]] 78 | name = "condtype" 79 | version = "1.3.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" 82 | 83 | [[package]] 84 | name = "ctor" 85 | version = "0.1.26" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" 88 | dependencies = [ 89 | "quote", 90 | "syn 1.0.109", 91 | ] 92 | 93 | [[package]] 94 | name = "deno_error" 95 | version = "0.7.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "dde60bd153886964234c5012d3d9caf788287f28d81fb24a884436904101ef10" 98 | dependencies = [ 99 | "deno_error_macro", 100 | "libc", 101 | ] 102 | 103 | [[package]] 104 | name = "deno_error_macro" 105 | version = "0.7.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "409f265785bd946d3006756955aaf40b0e4deb25752eae6a990afe54a31cfd83" 108 | dependencies = [ 109 | "proc-macro2", 110 | "quote", 111 | "syn 2.0.89", 112 | ] 113 | 114 | [[package]] 115 | name = "deno_semver" 116 | version = "0.9.1" 117 | dependencies = [ 118 | "capacity_builder", 119 | "deno_error", 120 | "divan", 121 | "ecow", 122 | "hipstr", 123 | "monch", 124 | "once_cell", 125 | "pretty_assertions", 126 | "serde", 127 | "serde_json", 128 | "thiserror", 129 | "url", 130 | ] 131 | 132 | [[package]] 133 | name = "diff" 134 | version = "0.1.13" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 137 | 138 | [[package]] 139 | name = "displaydoc" 140 | version = "0.2.5" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 143 | dependencies = [ 144 | "proc-macro2", 145 | "quote", 146 | "syn 2.0.89", 147 | ] 148 | 149 | [[package]] 150 | name = "divan" 151 | version = "0.1.17" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "e0583193020b29b03682d8d33bb53a5b0f50df6daacece12ca99b904cfdcb8c4" 154 | dependencies = [ 155 | "cfg-if", 156 | "clap", 157 | "condtype", 158 | "divan-macros", 159 | "libc", 160 | "regex-lite", 161 | ] 162 | 163 | [[package]] 164 | name = "divan-macros" 165 | version = "0.1.17" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "8dc51d98e636f5e3b0759a39257458b22619cac7e96d932da6eeb052891bb67c" 168 | dependencies = [ 169 | "proc-macro2", 170 | "quote", 171 | "syn 2.0.89", 172 | ] 173 | 174 | [[package]] 175 | name = "ecow" 176 | version = "0.2.3" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" 179 | dependencies = [ 180 | "serde", 181 | ] 182 | 183 | [[package]] 184 | name = "errno" 185 | version = "0.3.10" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 188 | dependencies = [ 189 | "libc", 190 | "windows-sys", 191 | ] 192 | 193 | [[package]] 194 | name = "form_urlencoded" 195 | version = "1.2.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 198 | dependencies = [ 199 | "percent-encoding", 200 | ] 201 | 202 | [[package]] 203 | name = "hashbrown" 204 | version = "0.12.3" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 207 | 208 | [[package]] 209 | name = "hipstr" 210 | version = "0.6.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "97971ffc85d4c98de12e2608e992a43f5294ebb625fdb045b27c731b64c4c6d6" 213 | dependencies = [ 214 | "serde", 215 | "serde_bytes", 216 | "sptr", 217 | ] 218 | 219 | [[package]] 220 | name = "icu_collections" 221 | version = "1.5.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 224 | dependencies = [ 225 | "displaydoc", 226 | "yoke", 227 | "zerofrom", 228 | "zerovec", 229 | ] 230 | 231 | [[package]] 232 | name = "icu_locid" 233 | version = "1.5.0" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 236 | dependencies = [ 237 | "displaydoc", 238 | "litemap", 239 | "tinystr", 240 | "writeable", 241 | "zerovec", 242 | ] 243 | 244 | [[package]] 245 | name = "icu_locid_transform" 246 | version = "1.5.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 249 | dependencies = [ 250 | "displaydoc", 251 | "icu_locid", 252 | "icu_locid_transform_data", 253 | "icu_provider", 254 | "tinystr", 255 | "zerovec", 256 | ] 257 | 258 | [[package]] 259 | name = "icu_locid_transform_data" 260 | version = "1.5.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 263 | 264 | [[package]] 265 | name = "icu_normalizer" 266 | version = "1.5.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 269 | dependencies = [ 270 | "displaydoc", 271 | "icu_collections", 272 | "icu_normalizer_data", 273 | "icu_properties", 274 | "icu_provider", 275 | "smallvec", 276 | "utf16_iter", 277 | "utf8_iter", 278 | "write16", 279 | "zerovec", 280 | ] 281 | 282 | [[package]] 283 | name = "icu_normalizer_data" 284 | version = "1.5.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 287 | 288 | [[package]] 289 | name = "icu_properties" 290 | version = "1.5.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 293 | dependencies = [ 294 | "displaydoc", 295 | "icu_collections", 296 | "icu_locid_transform", 297 | "icu_properties_data", 298 | "icu_provider", 299 | "tinystr", 300 | "zerovec", 301 | ] 302 | 303 | [[package]] 304 | name = "icu_properties_data" 305 | version = "1.5.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 308 | 309 | [[package]] 310 | name = "icu_provider" 311 | version = "1.5.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 314 | dependencies = [ 315 | "displaydoc", 316 | "icu_locid", 317 | "icu_provider_macros", 318 | "stable_deref_trait", 319 | "tinystr", 320 | "writeable", 321 | "yoke", 322 | "zerofrom", 323 | "zerovec", 324 | ] 325 | 326 | [[package]] 327 | name = "icu_provider_macros" 328 | version = "1.5.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 331 | dependencies = [ 332 | "proc-macro2", 333 | "quote", 334 | "syn 2.0.89", 335 | ] 336 | 337 | [[package]] 338 | name = "idna" 339 | version = "1.0.3" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 342 | dependencies = [ 343 | "idna_adapter", 344 | "smallvec", 345 | "utf8_iter", 346 | ] 347 | 348 | [[package]] 349 | name = "idna_adapter" 350 | version = "1.2.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 353 | dependencies = [ 354 | "icu_normalizer", 355 | "icu_properties", 356 | ] 357 | 358 | [[package]] 359 | name = "indexmap" 360 | version = "1.9.3" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 363 | dependencies = [ 364 | "autocfg", 365 | "hashbrown", 366 | ] 367 | 368 | [[package]] 369 | name = "itoa" 370 | version = "1.0.14" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 373 | 374 | [[package]] 375 | name = "libc" 376 | version = "0.2.162" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" 379 | 380 | [[package]] 381 | name = "linux-raw-sys" 382 | version = "0.4.14" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 385 | 386 | [[package]] 387 | name = "litemap" 388 | version = "0.7.3" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" 391 | 392 | [[package]] 393 | name = "monch" 394 | version = "0.5.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" 397 | 398 | [[package]] 399 | name = "once_cell" 400 | version = "1.18.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 403 | 404 | [[package]] 405 | name = "output_vt100" 406 | version = "0.1.3" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" 409 | dependencies = [ 410 | "winapi", 411 | ] 412 | 413 | [[package]] 414 | name = "percent-encoding" 415 | version = "2.3.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 418 | 419 | [[package]] 420 | name = "pretty_assertions" 421 | version = "1.3.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" 424 | dependencies = [ 425 | "ctor", 426 | "diff", 427 | "output_vt100", 428 | "yansi", 429 | ] 430 | 431 | [[package]] 432 | name = "proc-macro2" 433 | version = "1.0.92" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 436 | dependencies = [ 437 | "unicode-ident", 438 | ] 439 | 440 | [[package]] 441 | name = "quote" 442 | version = "1.0.37" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 445 | dependencies = [ 446 | "proc-macro2", 447 | ] 448 | 449 | [[package]] 450 | name = "regex-lite" 451 | version = "0.1.6" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" 454 | 455 | [[package]] 456 | name = "rustix" 457 | version = "0.38.42" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 460 | dependencies = [ 461 | "bitflags", 462 | "errno", 463 | "libc", 464 | "linux-raw-sys", 465 | "windows-sys", 466 | ] 467 | 468 | [[package]] 469 | name = "ryu" 470 | version = "1.0.13" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 473 | 474 | [[package]] 475 | name = "serde" 476 | version = "1.0.216" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 479 | dependencies = [ 480 | "serde_derive", 481 | ] 482 | 483 | [[package]] 484 | name = "serde_bytes" 485 | version = "0.11.15" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" 488 | dependencies = [ 489 | "serde", 490 | ] 491 | 492 | [[package]] 493 | name = "serde_derive" 494 | version = "1.0.216" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 497 | dependencies = [ 498 | "proc-macro2", 499 | "quote", 500 | "syn 2.0.89", 501 | ] 502 | 503 | [[package]] 504 | name = "serde_json" 505 | version = "1.0.95" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" 508 | dependencies = [ 509 | "indexmap", 510 | "itoa", 511 | "ryu", 512 | "serde", 513 | ] 514 | 515 | [[package]] 516 | name = "smallvec" 517 | version = "1.13.2" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 520 | 521 | [[package]] 522 | name = "sptr" 523 | version = "0.3.2" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" 526 | 527 | [[package]] 528 | name = "stable_deref_trait" 529 | version = "1.2.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 532 | 533 | [[package]] 534 | name = "syn" 535 | version = "1.0.109" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 538 | dependencies = [ 539 | "proc-macro2", 540 | "quote", 541 | "unicode-ident", 542 | ] 543 | 544 | [[package]] 545 | name = "syn" 546 | version = "2.0.89" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" 549 | dependencies = [ 550 | "proc-macro2", 551 | "quote", 552 | "unicode-ident", 553 | ] 554 | 555 | [[package]] 556 | name = "synstructure" 557 | version = "0.13.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 560 | dependencies = [ 561 | "proc-macro2", 562 | "quote", 563 | "syn 2.0.89", 564 | ] 565 | 566 | [[package]] 567 | name = "terminal_size" 568 | version = "0.4.1" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" 571 | dependencies = [ 572 | "rustix", 573 | "windows-sys", 574 | ] 575 | 576 | [[package]] 577 | name = "thiserror" 578 | version = "2.0.3" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" 581 | dependencies = [ 582 | "thiserror-impl", 583 | ] 584 | 585 | [[package]] 586 | name = "thiserror-impl" 587 | version = "2.0.3" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" 590 | dependencies = [ 591 | "proc-macro2", 592 | "quote", 593 | "syn 2.0.89", 594 | ] 595 | 596 | [[package]] 597 | name = "tinystr" 598 | version = "0.7.6" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 601 | dependencies = [ 602 | "displaydoc", 603 | "zerovec", 604 | ] 605 | 606 | [[package]] 607 | name = "unicode-ident" 608 | version = "1.0.8" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 611 | 612 | [[package]] 613 | name = "url" 614 | version = "2.5.3" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" 617 | dependencies = [ 618 | "form_urlencoded", 619 | "idna", 620 | "percent-encoding", 621 | ] 622 | 623 | [[package]] 624 | name = "utf16_iter" 625 | version = "1.0.5" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 628 | 629 | [[package]] 630 | name = "utf8_iter" 631 | version = "1.0.4" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 634 | 635 | [[package]] 636 | name = "winapi" 637 | version = "0.3.9" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 640 | dependencies = [ 641 | "winapi-i686-pc-windows-gnu", 642 | "winapi-x86_64-pc-windows-gnu", 643 | ] 644 | 645 | [[package]] 646 | name = "winapi-i686-pc-windows-gnu" 647 | version = "0.4.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 650 | 651 | [[package]] 652 | name = "winapi-x86_64-pc-windows-gnu" 653 | version = "0.4.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 656 | 657 | [[package]] 658 | name = "windows-sys" 659 | version = "0.59.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 662 | dependencies = [ 663 | "windows-targets", 664 | ] 665 | 666 | [[package]] 667 | name = "windows-targets" 668 | version = "0.52.6" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 671 | dependencies = [ 672 | "windows_aarch64_gnullvm", 673 | "windows_aarch64_msvc", 674 | "windows_i686_gnu", 675 | "windows_i686_gnullvm", 676 | "windows_i686_msvc", 677 | "windows_x86_64_gnu", 678 | "windows_x86_64_gnullvm", 679 | "windows_x86_64_msvc", 680 | ] 681 | 682 | [[package]] 683 | name = "windows_aarch64_gnullvm" 684 | version = "0.52.6" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 687 | 688 | [[package]] 689 | name = "windows_aarch64_msvc" 690 | version = "0.52.6" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 693 | 694 | [[package]] 695 | name = "windows_i686_gnu" 696 | version = "0.52.6" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 699 | 700 | [[package]] 701 | name = "windows_i686_gnullvm" 702 | version = "0.52.6" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 705 | 706 | [[package]] 707 | name = "windows_i686_msvc" 708 | version = "0.52.6" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 711 | 712 | [[package]] 713 | name = "windows_x86_64_gnu" 714 | version = "0.52.6" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 717 | 718 | [[package]] 719 | name = "windows_x86_64_gnullvm" 720 | version = "0.52.6" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 723 | 724 | [[package]] 725 | name = "windows_x86_64_msvc" 726 | version = "0.52.6" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 729 | 730 | [[package]] 731 | name = "write16" 732 | version = "1.0.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 735 | 736 | [[package]] 737 | name = "writeable" 738 | version = "0.5.5" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 741 | 742 | [[package]] 743 | name = "yansi" 744 | version = "0.5.1" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 747 | 748 | [[package]] 749 | name = "yoke" 750 | version = "0.7.4" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" 753 | dependencies = [ 754 | "serde", 755 | "stable_deref_trait", 756 | "yoke-derive", 757 | "zerofrom", 758 | ] 759 | 760 | [[package]] 761 | name = "yoke-derive" 762 | version = "0.7.4" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" 765 | dependencies = [ 766 | "proc-macro2", 767 | "quote", 768 | "syn 2.0.89", 769 | "synstructure", 770 | ] 771 | 772 | [[package]] 773 | name = "zerofrom" 774 | version = "0.1.4" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" 777 | dependencies = [ 778 | "zerofrom-derive", 779 | ] 780 | 781 | [[package]] 782 | name = "zerofrom-derive" 783 | version = "0.1.4" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" 786 | dependencies = [ 787 | "proc-macro2", 788 | "quote", 789 | "syn 2.0.89", 790 | "synstructure", 791 | ] 792 | 793 | [[package]] 794 | name = "zerovec" 795 | version = "0.10.4" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 798 | dependencies = [ 799 | "yoke", 800 | "zerofrom", 801 | "zerovec-derive", 802 | ] 803 | 804 | [[package]] 805 | name = "zerovec-derive" 806 | version = "0.10.3" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 809 | dependencies = [ 810 | "proc-macro2", 811 | "quote", 812 | "syn 2.0.89", 813 | ] 814 | -------------------------------------------------------------------------------- /src/package.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use capacity_builder::CapacityDisplay; 4 | use capacity_builder::StringAppendable; 5 | use capacity_builder::StringBuilder; 6 | use capacity_builder::StringType; 7 | use deno_error::JsError; 8 | use monch::ParseErrorFailure; 9 | use serde::Deserialize; 10 | use serde::Serialize; 11 | use std::borrow::Cow; 12 | use std::cmp::Ordering; 13 | use thiserror::Error; 14 | use url::Url; 15 | 16 | use crate::RangeSetOrTag; 17 | use crate::Version; 18 | use crate::VersionBoundKind; 19 | use crate::VersionRange; 20 | use crate::VersionRangeSet; 21 | use crate::VersionReq; 22 | use crate::VersionReqNormalizedParseError; 23 | use crate::VersionReqSpecifierParseError; 24 | use crate::WILDCARD_VERSION_REQ; 25 | use crate::npm::NpmVersionReqParseError; 26 | use crate::range::RangeBound; 27 | use crate::range::VersionBound; 28 | 29 | #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] 30 | pub enum PackageKind { 31 | Jsr, 32 | Npm, 33 | } 34 | 35 | impl PackageKind { 36 | pub fn scheme_with_colon(self) -> &'static str { 37 | match self { 38 | Self::Jsr => "jsr:", 39 | Self::Npm => "npm:", 40 | } 41 | } 42 | } 43 | 44 | #[derive(Error, Debug, Clone, JsError, PartialEq, Eq)] 45 | pub enum PackageReqReferenceParseError { 46 | #[class(type)] 47 | #[error("Not {} specifier", .0.scheme_with_colon())] 48 | NotExpectedScheme(PackageKind), 49 | #[class(inherit)] 50 | #[error(transparent)] 51 | Invalid(Box), 52 | #[class(inherit)] 53 | #[error(transparent)] 54 | InvalidPathWithVersion(Box), 55 | } 56 | 57 | #[derive(Error, Debug, Clone, JsError, PartialEq, Eq)] 58 | #[class(type)] 59 | #[error("Invalid package specifier '{specifier}'")] 60 | pub struct PackageReqReferenceInvalidParseError { 61 | pub specifier: String, 62 | #[source] 63 | pub source: PackageReqPartsParseError, 64 | } 65 | 66 | #[derive(Error, Debug, Clone, JsError, PartialEq, Eq)] 67 | #[class(type)] 68 | #[error("Invalid package specifier '{0}{1}'. Did you mean to write '{0}{2}'? If not, add a version requirement to the specifier.", .kind.scheme_with_colon(), current, suggested)] 69 | pub struct PackageReqReferenceInvalidWithVersionParseError { 70 | pub kind: PackageKind, 71 | pub current: String, 72 | pub suggested: String, 73 | } 74 | 75 | /// A reference to a package's name, version constraint, and potential sub path. 76 | /// 77 | /// This contains all the information found in a package specifier other than 78 | /// what kind of package specifier it was. 79 | #[derive(Clone, Debug, PartialEq, Eq, Hash, CapacityDisplay)] 80 | pub struct PackageReqReference { 81 | pub req: PackageReq, 82 | pub sub_path: Option, 83 | } 84 | 85 | impl<'a> StringAppendable<'a> for &'a PackageReqReference { 86 | fn append_to_builder( 87 | self, 88 | builder: &mut StringBuilder<'a, TString>, 89 | ) { 90 | builder.append(&self.req); 91 | if let Some(sub_path) = &self.sub_path { 92 | builder.append('/'); 93 | builder.append(sub_path); 94 | } 95 | } 96 | } 97 | 98 | impl PackageReqReference { 99 | #[allow(clippy::should_implement_trait)] 100 | pub(crate) fn from_str( 101 | specifier: &str, 102 | kind: PackageKind, 103 | ) -> Result { 104 | let original_text = specifier; 105 | let input = match specifier.strip_prefix(kind.scheme_with_colon()) { 106 | Some(input) => input, 107 | None => { 108 | // this is hit a lot when a url is not the expected scheme 109 | // so ensure nothing heavy occurs before this 110 | return Err(PackageReqReferenceParseError::NotExpectedScheme(kind)); 111 | } 112 | }; 113 | let (req, sub_path) = match PackageReq::parse_with_path_strict(input) { 114 | Ok(pkg_req) => pkg_req, 115 | Err(err) => { 116 | return Err(PackageReqReferenceParseError::Invalid(Box::new( 117 | PackageReqReferenceInvalidParseError { 118 | specifier: original_text.to_string(), 119 | source: err, 120 | }, 121 | ))); 122 | } 123 | }; 124 | let sub_path = if sub_path.is_empty() || sub_path == "/" { 125 | None 126 | } else { 127 | Some(PackageSubPath::from_str(sub_path)) 128 | }; 129 | 130 | if let Some(sub_path) = &sub_path 131 | && req.version_req.version_text() == "*" 132 | && let Some(at_index) = sub_path.rfind('@') 133 | { 134 | let (new_sub_path, version) = sub_path.split_at(at_index); 135 | return Err(PackageReqReferenceParseError::InvalidPathWithVersion( 136 | Box::new(PackageReqReferenceInvalidWithVersionParseError { 137 | kind, 138 | current: format!("{req}/{sub_path}"), 139 | suggested: format!("{req}{version}/{new_sub_path}"), 140 | }), 141 | )); 142 | } 143 | 144 | Ok(Self { req, sub_path }) 145 | } 146 | } 147 | 148 | #[derive(Error, Debug, Clone, JsError, PartialEq, Eq)] 149 | pub enum PackageReqPartsParseError { 150 | #[class(type)] 151 | #[error("Did not contain a package name")] 152 | NoPackageName, 153 | #[class(type)] 154 | #[error("Did not contain a valid package name")] 155 | InvalidPackageName, 156 | #[class(type)] 157 | #[error( 158 | "Packages in the format / must start with an '@' symbol" 159 | )] 160 | MissingAtSymbol, 161 | #[class(inherit)] 162 | #[error(transparent)] 163 | SpecifierVersionReq(VersionReqSpecifierParseError), 164 | #[class(inherit)] 165 | #[error(transparent)] 166 | NpmVersionReq(NpmVersionReqParseError), 167 | #[class(inherit)] 168 | #[error(transparent)] 169 | NormalizedVersionReq(VersionReqNormalizedParseError), 170 | } 171 | 172 | #[derive(Error, Debug, Clone, JsError)] 173 | #[class(type)] 174 | #[error("Invalid package requirement '{text}'")] 175 | pub struct PackageReqParseError { 176 | pub text: String, 177 | #[source] 178 | pub source: PackageReqPartsParseError, 179 | } 180 | 181 | pub type PackageName = crate::StackString; 182 | pub type PackageSubPath = crate::SmallStackString; 183 | 184 | /// The name and version constraint component of an `PackageReqReference`. 185 | #[derive(Clone, Debug, PartialEq, Eq, Hash, CapacityDisplay)] 186 | pub struct PackageReq { 187 | pub name: PackageName, 188 | pub version_req: VersionReq, 189 | } 190 | 191 | impl<'a> StringAppendable<'a> for &'a PackageReq { 192 | fn append_to_builder( 193 | self, 194 | builder: &mut StringBuilder<'a, TString>, 195 | ) { 196 | if self.version_req.version_text() == "*" { 197 | // do not write out the version requirement when it's the wildcard version 198 | builder.append(&self.name); 199 | } else { 200 | builder.append(&self.name); 201 | builder.append('@'); 202 | builder.append(&self.version_req.raw_text); 203 | } 204 | } 205 | } 206 | 207 | impl PackageReq { 208 | #[allow(clippy::should_implement_trait)] 209 | pub fn from_str(text: &str) -> Result { 210 | Self::from_str_inner(text, Self::parse_with_path_strict) 211 | } 212 | 213 | pub fn from_str_loose(text: &str) -> Result { 214 | Self::from_str_inner(text, Self::parse_with_path_loose) 215 | } 216 | 217 | pub fn from_str_normalized(text: &str) -> Result { 218 | Self::from_str_inner(text, Self::parse_with_path_normalized) 219 | } 220 | 221 | fn from_str_inner( 222 | text: &str, 223 | parse_with_path: impl FnOnce( 224 | &str, 225 | ) 226 | -> Result<(Self, &str), PackageReqPartsParseError>, 227 | ) -> Result { 228 | fn inner( 229 | text: &str, 230 | parse_with_path: impl FnOnce( 231 | &str, 232 | ) -> Result< 233 | (PackageReq, &str), 234 | PackageReqPartsParseError, 235 | >, 236 | ) -> Result { 237 | let (req, path) = parse_with_path(text)?; 238 | if !path.is_empty() { 239 | return Err(PackageReqPartsParseError::SpecifierVersionReq( 240 | VersionReqSpecifierParseError { 241 | source: ParseErrorFailure::new( 242 | &text[text.len() - path.len() - 1..], 243 | "Unexpected character '/'", 244 | ) 245 | .into_error(), 246 | }, 247 | )); 248 | } 249 | Ok(req) 250 | } 251 | 252 | match inner(text, parse_with_path) { 253 | Ok(req) => Ok(req), 254 | Err(err) => Err(PackageReqParseError { 255 | text: text.to_string(), 256 | source: if !text.starts_with('@') && text.contains('/') { 257 | PackageReqPartsParseError::MissingAtSymbol 258 | } else { 259 | err 260 | }, 261 | }), 262 | } 263 | } 264 | 265 | fn parse_with_path_strict( 266 | text: &str, 267 | ) -> Result<(Self, &str), PackageReqPartsParseError> { 268 | PackageReq::parse_with_path(text, |version| { 269 | VersionReq::parse_from_specifier(version) 270 | .map_err(PackageReqPartsParseError::SpecifierVersionReq) 271 | }) 272 | } 273 | 274 | fn parse_with_path_loose( 275 | text: &str, 276 | ) -> Result<(Self, &str), PackageReqPartsParseError> { 277 | PackageReq::parse_with_path(text, |version| { 278 | VersionReq::parse_from_npm(version) 279 | .map_err(PackageReqPartsParseError::NpmVersionReq) 280 | }) 281 | } 282 | 283 | fn parse_with_path_normalized( 284 | text: &str, 285 | ) -> Result<(Self, &str), PackageReqPartsParseError> { 286 | PackageReq::parse_with_path(text, |version| { 287 | VersionReq::parse_from_normalized(version) 288 | .map_err(PackageReqPartsParseError::NormalizedVersionReq) 289 | }) 290 | } 291 | 292 | fn parse_with_path( 293 | input: &str, 294 | parse_version_req: impl FnOnce( 295 | &str, 296 | ) 297 | -> Result, 298 | ) -> Result<(Self, &str), PackageReqPartsParseError> { 299 | // Strip leading slash, which might come from import map 300 | let input = input.strip_prefix('/').unwrap_or(input); 301 | // parse the first name part 302 | let (first_part, input) = input.split_once('/').unwrap_or((input, "")); 303 | if first_part.is_empty() { 304 | return Err(PackageReqPartsParseError::NoPackageName); 305 | } 306 | // if it starts with an @, parse the second name part 307 | let (maybe_scope, last_name_part, sub_path) = if first_part.starts_with('@') 308 | { 309 | let (second_part, input) = input.split_once('/').unwrap_or((input, "")); 310 | if second_part.is_empty() { 311 | return Err(PackageReqPartsParseError::InvalidPackageName); 312 | } 313 | (Some(first_part), second_part, input) 314 | } else { 315 | (None, first_part, input) 316 | }; 317 | 318 | let (last_name_part, version_req) = if let Some((last_name_part, version)) = 319 | last_name_part.rsplit_once('@') 320 | { 321 | (last_name_part, Some(parse_version_req(version)?)) 322 | } else { 323 | (last_name_part, None) 324 | }; 325 | Ok(( 326 | Self { 327 | name: match maybe_scope { 328 | Some(scope) => { 329 | let mut text = PackageName::with_capacity( 330 | scope.len() + 1 + last_name_part.len(), 331 | ); 332 | text.push_str(scope); 333 | text.push('/'); 334 | text.push_str(last_name_part); 335 | text 336 | } 337 | None => last_name_part.into(), 338 | }, 339 | version_req: version_req 340 | .unwrap_or_else(|| WILDCARD_VERSION_REQ.clone()), 341 | }, 342 | sub_path, 343 | )) 344 | } 345 | 346 | /// Outputs a normalized string representation of the package requirement. 347 | pub fn to_string_normalized(&self) -> crate::StackString { 348 | StringBuilder::build(|builder| { 349 | builder.append(&self.name); 350 | builder.append('@'); 351 | builder.append(self.version_req.inner()); 352 | }) 353 | .unwrap() 354 | } 355 | } 356 | 357 | impl Serialize for PackageReq { 358 | fn serialize(&self, serializer: S) -> Result 359 | where 360 | S: serde::Serializer, 361 | { 362 | serializer.serialize_str(&self.to_string_normalized()) 363 | } 364 | } 365 | 366 | impl<'de> Deserialize<'de> for PackageReq { 367 | fn deserialize(deserializer: D) -> Result 368 | where 369 | D: serde::Deserializer<'de>, 370 | { 371 | let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?; 372 | match Self::from_str_normalized(&text) { 373 | Ok(req) => Ok(req), 374 | Err(err) => Err(serde::de::Error::custom(err)), 375 | } 376 | } 377 | } 378 | 379 | impl PartialOrd for PackageReq { 380 | fn partial_cmp(&self, other: &Self) -> Option { 381 | Some(self.cmp(other)) 382 | } 383 | } 384 | 385 | // Sort the package requirements alphabetically then the version 386 | // requirement in a way that will lead to the least number of 387 | // duplicate packages (so sort None last since it's `*`), but 388 | // mostly to create some determinism around how these are resolved. 389 | impl Ord for PackageReq { 390 | fn cmp(&self, other: &Self) -> Ordering { 391 | // don't bother implementing Ord/PartialOrd on the lower level items 392 | // because it's not so useful and it causes them to have a `.clamp()` method 393 | // for Ord instead of their own defined methods 394 | fn cmp_version_range(a: &VersionRange, b: &VersionRange) -> Ordering { 395 | fn cmp_range_bound( 396 | a: &RangeBound, 397 | b: &RangeBound, 398 | cmp_version_bound: impl Fn(&VersionBound, &VersionBound) -> Ordering, 399 | ) -> Ordering { 400 | match (a, b) { 401 | (RangeBound::Unbounded, RangeBound::Unbounded) => Ordering::Equal, 402 | (RangeBound::Unbounded, RangeBound::Version(_)) => Ordering::Greater, 403 | (RangeBound::Version(_), RangeBound::Unbounded) => Ordering::Less, 404 | (RangeBound::Version(a), RangeBound::Version(b)) => { 405 | cmp_version_bound(a, b) 406 | } 407 | } 408 | } 409 | 410 | fn cmp_version_bound_kind_start( 411 | a: VersionBoundKind, 412 | b: VersionBoundKind, 413 | ) -> Ordering { 414 | match (a, b) { 415 | (VersionBoundKind::Inclusive, VersionBoundKind::Inclusive) 416 | | (VersionBoundKind::Exclusive, VersionBoundKind::Exclusive) => { 417 | Ordering::Equal 418 | } 419 | (VersionBoundKind::Exclusive, VersionBoundKind::Inclusive) => { 420 | Ordering::Less 421 | } 422 | (VersionBoundKind::Inclusive, VersionBoundKind::Exclusive) => { 423 | Ordering::Greater 424 | } 425 | } 426 | } 427 | 428 | fn cmp_range_bound_start(a: &RangeBound, b: &RangeBound) -> Ordering { 429 | cmp_range_bound(a, b, |a, b| { 430 | // prefer higher versions first 431 | match b.version.cmp(&a.version) { 432 | Ordering::Equal => cmp_version_bound_kind_start(a.kind, b.kind), 433 | ordering => ordering, 434 | } 435 | }) 436 | } 437 | 438 | fn cmp_range_bound_end(a: &RangeBound, b: &RangeBound) -> Ordering { 439 | cmp_range_bound(a, b, |a, b| { 440 | // prefer lower versions first 441 | match a.version.cmp(&b.version) { 442 | Ordering::Equal => cmp_version_bound_kind_start(b.kind, a.kind), // reversed 443 | ordering => ordering, 444 | } 445 | }) 446 | } 447 | 448 | match cmp_range_bound_start(&a.start, &b.start) { 449 | Ordering::Equal => cmp_range_bound_end(&a.end, &b.end), 450 | ordering => ordering, 451 | } 452 | } 453 | 454 | fn cmp_version_range_set( 455 | a: &VersionRangeSet, 456 | b: &VersionRangeSet, 457 | ) -> Ordering { 458 | for (a_item, b_item) in a.0.iter().zip(b.0.iter()) { 459 | match cmp_version_range(a_item, b_item) { 460 | Ordering::Equal => continue, 461 | ordering => return ordering, 462 | } 463 | } 464 | 465 | // prefer the one with the least number of items 466 | a.0.len().cmp(&b.0.len()) 467 | } 468 | 469 | fn cmp_specifier_version_req(a: &VersionReq, b: &VersionReq) -> Ordering { 470 | // ignore the raw text as it's only for displaying to the user 471 | match a.inner() { 472 | RangeSetOrTag::Tag(a_tag) => { 473 | match b.inner() { 474 | RangeSetOrTag::Tag(b_tag) => b_tag.cmp(a_tag), // sort descending 475 | RangeSetOrTag::RangeSet(_) => Ordering::Less, // prefer 'a' since tag 476 | } 477 | } 478 | RangeSetOrTag::RangeSet(a_set) => { 479 | match b.inner() { 480 | RangeSetOrTag::Tag(_) => Ordering::Greater, // prefer 'b' since tag 481 | RangeSetOrTag::RangeSet(b_set) => { 482 | cmp_version_range_set(a_set, b_set) 483 | } 484 | } 485 | } 486 | } 487 | } 488 | 489 | // compare the name, then the version req 490 | match self.name.cmp(&other.name) { 491 | Ordering::Equal => { 492 | cmp_specifier_version_req(&self.version_req, &other.version_req) 493 | } 494 | ordering => ordering, 495 | } 496 | } 497 | } 498 | 499 | #[derive(Debug, Error, Clone, JsError)] 500 | #[class(type)] 501 | #[error("Invalid package name and version reference '{text}'. {message}")] 502 | pub struct PackageNvReferenceParseError { 503 | pub message: String, 504 | pub text: String, 505 | } 506 | 507 | /// A package name and version with a potential subpath. 508 | #[derive( 509 | Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, CapacityDisplay, 510 | )] 511 | pub struct PackageNvReference { 512 | pub nv: PackageNv, 513 | pub sub_path: Option, 514 | } 515 | 516 | impl PackageNvReference { 517 | #[allow(clippy::should_implement_trait)] 518 | pub(crate) fn from_str( 519 | nv: &str, 520 | kind: PackageKind, 521 | ) -> Result { 522 | use monch::*; 523 | 524 | fn sub_path(input: &str) -> ParseResult<'_, &str> { 525 | let (input, _) = ch('/')(input)?; 526 | Ok(("", input)) 527 | } 528 | 529 | fn parse_ref<'a>( 530 | kind: PackageKind, 531 | ) -> impl Fn(&'a str) -> ParseResult<'a, PackageNvReference> { 532 | move |input| { 533 | let (input, _) = tag(kind.scheme_with_colon())(input)?; 534 | let (input, _) = maybe(ch('/'))(input)?; 535 | let (input, nv) = parse_nv(input)?; 536 | let (input, maybe_sub_path) = maybe(sub_path)(input)?; 537 | Ok(( 538 | input, 539 | PackageNvReference { 540 | nv, 541 | sub_path: maybe_sub_path.map(PackageSubPath::from_str), 542 | }, 543 | )) 544 | } 545 | } 546 | 547 | with_failure_handling(parse_ref(kind))(nv).map_err(|err| { 548 | PackageNvReferenceParseError { 549 | message: format!("{err:#}"), 550 | text: nv.to_string(), 551 | } 552 | }) 553 | } 554 | 555 | pub(crate) fn as_specifier(&self, kind: PackageKind) -> Url { 556 | let text = StringBuilder::::build(|builder| { 557 | builder.append(kind.scheme_with_colon()); 558 | builder.append('/'); 559 | builder.append(&self.nv.name); 560 | builder.append('@'); 561 | builder.append(&self.nv.version); 562 | if let Some(sub_path) = &self.sub_path { 563 | builder.append('/'); 564 | builder.append(sub_path); 565 | } 566 | }) 567 | .unwrap(); 568 | Url::parse(&text).unwrap() 569 | } 570 | } 571 | 572 | impl<'a> StringAppendable<'a> for &'a PackageNvReference { 573 | fn append_to_builder( 574 | self, 575 | builder: &mut StringBuilder<'a, TString>, 576 | ) { 577 | builder.append(&self.nv); 578 | if let Some(sub_path) = &self.sub_path { 579 | builder.append('/'); 580 | builder.append(sub_path); 581 | } 582 | } 583 | } 584 | 585 | #[derive(Debug, Error, Clone, JsError)] 586 | #[class(type)] 587 | #[error("Invalid package name and version '{text}'. {message}")] 588 | pub struct PackageNvParseError { 589 | pub message: String, 590 | pub text: String, 591 | } 592 | 593 | #[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, CapacityDisplay)] 594 | pub struct PackageNv { 595 | pub name: PackageName, 596 | pub version: Version, 597 | } 598 | 599 | impl std::fmt::Debug for PackageNv { 600 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 601 | // when debugging, it's easier to compare this 602 | write!(f, "{}@{}", self.name, self.version) 603 | } 604 | } 605 | 606 | impl<'a> StringAppendable<'a> for &'a PackageNv { 607 | fn append_to_builder( 608 | self, 609 | builder: &mut StringBuilder<'a, TString>, 610 | ) { 611 | builder.append(&self.name); 612 | builder.append('@'); 613 | builder.append(&self.version); 614 | } 615 | } 616 | 617 | impl Serialize for PackageNv { 618 | fn serialize(&self, serializer: S) -> Result 619 | where 620 | S: serde::Serializer, 621 | { 622 | serializer.serialize_str(&self.to_string()) 623 | } 624 | } 625 | 626 | impl<'de> Deserialize<'de> for PackageNv { 627 | fn deserialize(deserializer: D) -> Result 628 | where 629 | D: serde::Deserializer<'de>, 630 | { 631 | let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?; 632 | match Self::from_str(&text) { 633 | Ok(req) => Ok(req), 634 | Err(err) => Err(serde::de::Error::custom(err)), 635 | } 636 | } 637 | } 638 | 639 | impl PackageNv { 640 | #[allow(clippy::should_implement_trait)] 641 | pub fn from_str(nv: &str) -> Result { 642 | monch::with_failure_handling(parse_nv)(nv).map_err(|err| { 643 | PackageNvParseError { 644 | message: format!("{err:#}"), 645 | text: nv.to_string(), 646 | } 647 | }) 648 | } 649 | 650 | pub fn scope(&self) -> Option<&str> { 651 | if self.name.starts_with('@') { 652 | self.name.as_str().split('/').next() 653 | } else { 654 | None 655 | } 656 | } 657 | 658 | /// Converts the package nv into a package requirement. 659 | pub fn into_req(self) -> PackageReq { 660 | PackageReq { 661 | name: self.name, 662 | version_req: self.version.into_req(), 663 | } 664 | } 665 | } 666 | 667 | fn parse_nv(input: &str) -> monch::ParseResult<'_, PackageNv> { 668 | use monch::*; 669 | 670 | fn parse_name(input: &str) -> ParseResult<'_, &str> { 671 | if_not_empty(substring(move |input| { 672 | for (pos, c) in input.char_indices() { 673 | // first character might be a scope, so skip it 674 | if pos > 0 && c == '@' { 675 | return Ok((&input[pos..], ())); 676 | } 677 | } 678 | ParseError::backtrace() 679 | }))(input) 680 | } 681 | 682 | fn parse_version(input: &str) -> ParseResult<'_, &str> { 683 | if_not_empty(substring(skip_while(|c| !matches!(c, '_' | '/'))))(input) 684 | } 685 | 686 | let (input, name) = parse_name(input)?; 687 | let (input, _) = ch('@')(input)?; 688 | let at_version_input = input; 689 | let (input, version) = parse_version(input)?; 690 | match Version::parse_from_npm(version) { 691 | Ok(version) => Ok(( 692 | input, 693 | PackageNv { 694 | name: name.into(), 695 | version, 696 | }, 697 | )), 698 | Err(err) => ParseError::fail(at_version_input, format!("{err:#}")), 699 | } 700 | } 701 | 702 | #[cfg(test)] 703 | mod test { 704 | use std::cmp::Ordering; 705 | 706 | use crate::package::PackageReq; 707 | 708 | #[test] 709 | fn serialize_deserialize_package_req() { 710 | let package_req = PackageReq::from_str("test@^1.0").unwrap(); 711 | let json = serde_json::to_string(&package_req).unwrap(); 712 | assert_eq!(json, "\"test@1\""); 713 | let result = serde_json::from_str::(&json).unwrap(); 714 | assert_eq!(result, package_req); 715 | } 716 | 717 | #[test] 718 | fn serialize_deserialize_tag_package_req_with_v() { 719 | // note: this specifier is a tag and not a version 720 | let package_req = PackageReq::from_str("test@v1.0").unwrap(); 721 | assert!(package_req.version_req.tag().is_some()); 722 | let json = serde_json::to_string(&package_req).unwrap(); 723 | assert_eq!(json, "\"test@v1.0\""); 724 | let result = serde_json::from_str::(&json).unwrap(); 725 | assert!(result.version_req.tag().is_some()); 726 | assert_eq!(result, package_req); 727 | } 728 | 729 | #[test] 730 | fn serialize_deserialize_loose_package_req() { 731 | fn run_test(input: &str) { 732 | let package_req = PackageReq::from_str_loose(input).unwrap(); 733 | let json = serde_json::to_string(&package_req).unwrap(); 734 | let result = serde_json::from_str::(&json).unwrap(); 735 | assert_eq!(result, package_req); 736 | } 737 | 738 | // Basic version requirements 739 | run_test("pkg@1.2.3"); 740 | run_test("pkg@1.2"); 741 | run_test("pkg@1"); 742 | run_test("pkg@0.0.1"); 743 | 744 | // Caret requirements 745 | run_test("pkg@^1.2.3"); 746 | run_test("pkg@^0.2.3"); 747 | run_test("pkg@^0.0.3"); 748 | run_test("pkg@^1.2"); 749 | run_test("pkg@^1"); 750 | 751 | // Tilde requirements 752 | run_test("pkg@~1.2.3"); 753 | run_test("pkg@~1.2"); 754 | run_test("pkg@~1"); 755 | 756 | // Comparison operators 757 | run_test("pkg@>1.2.3"); 758 | run_test("pkg@>=1.2.3"); 759 | run_test("pkg@<2.0.0"); 760 | run_test("pkg@<=2.0.0"); 761 | run_test("pkg@=1.2.3"); 762 | 763 | // Wildcards 764 | run_test("pkg@*"); 765 | run_test("pkg@1.x"); 766 | run_test("pkg@1.2.x"); 767 | run_test("pkg@1.X"); 768 | run_test("pkg@1.2.X"); 769 | 770 | // Range requirements 771 | run_test("pkg@1.2.3 - 2.3.4"); 772 | run_test("pkg@1.0 - 2.0"); 773 | run_test("pkg@1 - 2"); 774 | run_test("pkg@>=1.2.3 <2.0.0"); 775 | run_test("pkg@>=1.0.0 <=2.0.0"); 776 | 777 | // OR requirements 778 | run_test("pkg@1.2.3 || 2.0.0"); 779 | run_test("pkg@^1.0.0 || ^2.0.0"); 780 | run_test("pkg@1 || 2 || 3"); 781 | run_test("pkg@>=1.0.0 <2.0.0 || >=3.0.0"); 782 | 783 | // Scoped packages 784 | run_test("@scope/pkg@1.2.3"); 785 | run_test("@scope/pkg@^1.0.0"); 786 | run_test("@scope/pkg@~1.2.3"); 787 | run_test("@scope/pkg@>=1.0.0"); 788 | run_test("@scope/pkg@*"); 789 | run_test("@scope/pkg@1.x"); 790 | run_test("@scope/pkg@1.2.3 - 2.0.0"); 791 | run_test("@scope/pkg@1 || 2"); 792 | 793 | // Complex version combinations 794 | run_test("pkg@>=1.2.3 <2.0.0 || >=3.0.0 <4.0.0"); 795 | run_test("pkg@^1.2.3 || ~2.3.4 || >=3.0.0"); 796 | run_test("@scope/pkg@>=1.0.0 <1.5.0 || >=2.0.0 <3.0.0"); 797 | 798 | // Pre-release versions 799 | run_test("pkg@1.2.3-alpha"); 800 | run_test("pkg@1.2.3-beta.1"); 801 | run_test("pkg@1.2.3-rc.1"); 802 | run_test("pkg@^1.2.3-alpha"); 803 | run_test("@scope/pkg@1.0.0-beta.2"); 804 | 805 | // Build metadata 806 | run_test("pkg@1.2.3+build.123"); 807 | run_test("pkg@1.2.3-alpha+build"); 808 | run_test("@scope/pkg@1.0.0+20130313144700"); 809 | 810 | // Edge cases 811 | run_test("pkg@0.0.0"); 812 | run_test("pkg@02.003.02"); 813 | run_test("pkg@999.999.999"); 814 | run_test("@my-scope/my-package@1.2.3"); 815 | run_test("@a/b@1.0.0"); 816 | 817 | // Multiple constraints 818 | run_test("pkg@>=1.2.3 <=2.0.0"); 819 | run_test("pkg@>1.0.0 <2.0.0"); 820 | run_test("pkg@>=0.1.0 <0.2.0"); 821 | 822 | // X-range variations 823 | run_test("pkg@1.2.*"); 824 | run_test("pkg@1.*"); 825 | run_test("pkg@*.x"); 826 | 827 | // Complex OR chains 828 | run_test("pkg@1.0.0 || 1.1.0 || 1.2.0"); 829 | run_test("pkg@^1.0.0 || ^2.0.0 || ^3.0.0"); 830 | run_test("@scope/pkg@>=1.0.0 || >=2.0.0 || >=3.0.0"); 831 | 832 | // Hyphen ranges with different precisions 833 | run_test("pkg@1.2.3 - 1.2.5"); 834 | run_test("pkg@1.0.0 - 1.9.9"); 835 | run_test("pkg@0.1.0 - 0.9.0"); 836 | } 837 | 838 | #[test] 839 | fn test_package_req_deserializable() { 840 | fn run_test(text: &str) { 841 | let start = PackageReq::from_str_loose(text).unwrap(); 842 | let value = serde_json::to_value(&start).unwrap(); 843 | let deserialized: PackageReq = serde_json::from_value(value).unwrap(); 844 | assert_eq!(deserialized, start); 845 | } 846 | 847 | run_test("a@1"); 848 | run_test("a@1"); 849 | run_test("a@1 - 1.5"); 850 | run_test("a@1 || 2 || 3"); 851 | } 852 | 853 | #[test] 854 | fn sorting_package_reqs() { 855 | fn cmp_req(a: &str, b: &str) -> Ordering { 856 | let a = PackageReq::from_str_loose(a).unwrap(); 857 | let b = PackageReq::from_str_loose(b).unwrap(); 858 | a.cmp(&b) 859 | } 860 | 861 | // sort by name 862 | assert_eq!(cmp_req("a", "b@1"), Ordering::Less); 863 | assert_eq!(cmp_req("b@1", "a"), Ordering::Greater); 864 | // prefer non-wildcard 865 | assert_eq!(cmp_req("a", "a@1"), Ordering::Greater); 866 | assert_eq!(cmp_req("a@1", "a"), Ordering::Less); 867 | // prefer tag 868 | assert_eq!(cmp_req("a@tag", "a"), Ordering::Less); 869 | assert_eq!(cmp_req("a", "a@tag"), Ordering::Greater); 870 | // sort tag descending 871 | assert_eq!(cmp_req("a@latest-v1", "a@latest-v2"), Ordering::Greater); 872 | assert_eq!(cmp_req("a@latest-v2", "a@latest-v1"), Ordering::Less); 873 | // sort version req descending 874 | assert_eq!(cmp_req("a@1", "a@2"), Ordering::Greater); 875 | assert_eq!(cmp_req("a@2", "a@1"), Ordering::Less); 876 | // prefer lower upper bound 877 | assert_eq!(cmp_req("a@3", "a@3.0.0"), Ordering::Greater); 878 | // prefer higher lower bound 879 | assert_eq!(cmp_req("a@>=3.0.0", "a@>3.0.0"), Ordering::Greater); 880 | assert_eq!(cmp_req("a@>=3.0.0", "a@>=3.0.0"), Ordering::Equal); 881 | assert_eq!(cmp_req("a@>3.0.0", "a@>=3.0.0"), Ordering::Less); 882 | // prefer lower upper bound (you end up with an intersection of both) 883 | assert_eq!(cmp_req("a@>=3.0.0 <=4", "a@>=3.0.0 <4"), Ordering::Greater); 884 | assert_eq!(cmp_req("a@>=3.0.0 <=4", "a@>=3.0.0 <=4"), Ordering::Equal); 885 | assert_eq!(cmp_req("a@>=3.0.0 <4", "a@>=3.0.0 <=4"), Ordering::Less); 886 | assert_eq!(cmp_req("a@>=3.0.0 <3.5", "a@>=3.0.0 <3.6"), Ordering::Less); 887 | // prefer one with less items when equal up to a point 888 | assert_eq!(cmp_req("a@>=3 || 4.x", "a@>=3 || 4.x"), Ordering::Equal); 889 | assert_eq!( 890 | cmp_req("a@>=3 || 4.x", "a@>=3 || 4.x || 5.x"), 891 | Ordering::Less 892 | ); 893 | assert_eq!( 894 | cmp_req("a@>=3 || 4.x || 5.x", "a@>=3 || 4.x"), 895 | Ordering::Greater 896 | ); 897 | } 898 | 899 | #[test] 900 | fn missing_at_symbol() { 901 | let err = PackageReq::from_str("scope/name").unwrap_err(); 902 | assert!( 903 | matches!( 904 | err.source, 905 | crate::package::PackageReqPartsParseError::MissingAtSymbol 906 | ), 907 | "{:#}", 908 | err 909 | ); 910 | } 911 | } 912 | -------------------------------------------------------------------------------- /src/range.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::cmp::Ordering; 4 | 5 | use capacity_builder::CapacityDisplay; 6 | use capacity_builder::StringAppendable; 7 | use capacity_builder::StringBuilder; 8 | use capacity_builder::StringType; 9 | use serde::Deserialize; 10 | use serde::Serialize; 11 | 12 | use crate::CowVec; 13 | use crate::VersionPreOrBuild; 14 | 15 | use super::Version; 16 | 17 | /// Collection of ranges. 18 | #[derive( 19 | Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, CapacityDisplay, 20 | )] 21 | pub struct VersionRangeSet(pub CowVec); 22 | 23 | impl VersionRangeSet { 24 | pub fn satisfies(&self, version: &Version) -> bool { 25 | self.0.iter().any(|r| r.satisfies(version)) 26 | } 27 | 28 | /// Gets if this set overlaps the other set. 29 | pub fn intersects_set(&self, other: &VersionRangeSet) -> bool { 30 | self 31 | .0 32 | .iter() 33 | .any(|a| other.0.iter().any(|b| a.intersects_range(b))) 34 | } 35 | } 36 | 37 | impl<'a> StringAppendable<'a> for &'a VersionRangeSet { 38 | fn append_to_builder( 39 | self, 40 | builder: &mut StringBuilder<'a, TString>, 41 | ) { 42 | match self.0.len() { 43 | 0 => builder.append('*'), 44 | _ => { 45 | for (i, range) in self.0.iter().enumerate() { 46 | if i > 0 { 47 | builder.append(" || "); 48 | } 49 | builder.append(range); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 57 | pub enum RangeBound { 58 | Version(VersionBound), 59 | Unbounded, // matches everything 60 | } 61 | 62 | impl RangeBound { 63 | pub fn inclusive(version: Version) -> Self { 64 | Self::version(VersionBoundKind::Inclusive, version) 65 | } 66 | 67 | pub fn exclusive(version: Version) -> Self { 68 | Self::version(VersionBoundKind::Exclusive, version) 69 | } 70 | 71 | pub fn version(kind: VersionBoundKind, version: Version) -> Self { 72 | Self::Version(VersionBound::new(kind, version)) 73 | } 74 | 75 | pub fn clamp_start(&self, other: &RangeBound) -> RangeBound { 76 | match &self { 77 | RangeBound::Unbounded => other.clone(), 78 | RangeBound::Version(self_bound) => RangeBound::Version(match &other { 79 | RangeBound::Unbounded => self_bound.clone(), 80 | RangeBound::Version(other_bound) => { 81 | match self_bound.version.cmp(&other_bound.version) { 82 | Ordering::Greater => self_bound.clone(), 83 | Ordering::Less => other_bound.clone(), 84 | Ordering::Equal => match self_bound.kind { 85 | VersionBoundKind::Exclusive => self_bound.clone(), 86 | VersionBoundKind::Inclusive => other_bound.clone(), 87 | }, 88 | } 89 | } 90 | }), 91 | } 92 | } 93 | 94 | pub fn clamp_end(&self, other: &RangeBound) -> RangeBound { 95 | match &self { 96 | RangeBound::Unbounded => other.clone(), 97 | RangeBound::Version(self_bound) => { 98 | RangeBound::Version(match other { 99 | RangeBound::Unbounded => self_bound.clone(), 100 | RangeBound::Version(other_bound) => { 101 | match self_bound.version.cmp(&other_bound.version) { 102 | // difference with above is the next two lines are switched 103 | Ordering::Greater => other_bound.clone(), 104 | Ordering::Less => self_bound.clone(), 105 | Ordering::Equal => match self_bound.kind { 106 | VersionBoundKind::Exclusive => self_bound.clone(), 107 | VersionBoundKind::Inclusive => other_bound.clone(), 108 | }, 109 | } 110 | } 111 | }) 112 | } 113 | } 114 | } 115 | 116 | pub fn has_pre_with_exact_major_minor_patch( 117 | &self, 118 | version: &Version, 119 | ) -> bool { 120 | if let RangeBound::Version(self_version) = &self 121 | && !self_version.version.pre.is_empty() 122 | && self_version.version.major == version.major 123 | && self_version.version.minor == version.minor 124 | && self_version.version.patch == version.patch 125 | { 126 | return true; 127 | } 128 | false 129 | } 130 | } 131 | 132 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 133 | pub enum VersionBoundKind { 134 | Inclusive, 135 | Exclusive, 136 | } 137 | 138 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] 139 | pub struct VersionBound { 140 | pub kind: VersionBoundKind, 141 | pub version: Version, 142 | } 143 | 144 | impl VersionBound { 145 | pub fn new(kind: VersionBoundKind, version: Version) -> Self { 146 | Self { kind, version } 147 | } 148 | } 149 | 150 | #[derive( 151 | Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, CapacityDisplay, 152 | )] 153 | pub struct VersionRange { 154 | pub start: RangeBound, 155 | pub end: RangeBound, 156 | } 157 | 158 | impl<'a> StringAppendable<'a> for &'a VersionRange { 159 | fn append_to_builder( 160 | self, 161 | builder: &mut StringBuilder<'a, TString>, 162 | ) { 163 | fn matches_tilde(start: &Version, end: &Version) -> bool { 164 | if !end.build.is_empty() || !end.pre.is_empty() { 165 | return false; 166 | } 167 | 168 | if start.major != end.major { 169 | return false; 170 | } 171 | 172 | if start.minor == end.minor { 173 | end.patch == start.patch && !start.pre.is_empty() 174 | } else { 175 | let Some(one_minor_higher) = start.minor.checked_add(1) else { 176 | return false; 177 | }; 178 | // ~0.1.2 is equivalent to >=0.1.2 <0.2.0 179 | // ~1.2.3 is equivalent to >=1.2.3 <1.3.0 180 | end.minor == one_minor_higher && end.patch == 0 181 | } 182 | } 183 | 184 | fn matches_caret(start: &Version, end: &Version) -> bool { 185 | if !end.build.is_empty() || !end.pre.is_empty() { 186 | return false; 187 | } 188 | 189 | if start.major == 0 { 190 | // ~0.0.x is different than ^0.0.x, so handle that 191 | if start.minor == 0 && end.minor == 0 { 192 | let Some(one_patch_higher) = start.patch.checked_add(1) else { 193 | return false; 194 | }; 195 | return end.patch == one_patch_higher; 196 | } 197 | return false; // it will always use normalize to tilde in this case 198 | } 199 | 200 | let Some(one_major_higher) = start.major.checked_add(1) else { 201 | return false; 202 | }; 203 | // ^1.2.3 is equivalent to >=1.2.3 <2.0.0 204 | end.major == one_major_higher && end.minor == 0 && end.patch == 0 205 | } 206 | 207 | fn is_zero_version(version: &Version) -> bool { 208 | version.major == 0 209 | && version.minor == 0 210 | && version.patch == 0 211 | && version.pre.is_empty() 212 | && version.build.is_empty() 213 | } 214 | 215 | match &self.start { 216 | RangeBound::Unbounded => match &self.end { 217 | RangeBound::Unbounded => builder.append('*'), 218 | RangeBound::Version(end) => match end.kind { 219 | VersionBoundKind::Inclusive => { 220 | builder.append("<="); 221 | builder.append(&end.version); 222 | } 223 | VersionBoundKind::Exclusive => { 224 | builder.append('<'); 225 | builder.append(&end.version); 226 | } 227 | }, 228 | }, 229 | RangeBound::Version(start) => match &self.end { 230 | RangeBound::Unbounded => match start.kind { 231 | VersionBoundKind::Inclusive => { 232 | builder.append(">="); 233 | builder.append(&start.version); 234 | } 235 | VersionBoundKind::Exclusive => { 236 | builder.append('>'); 237 | builder.append(&start.version); 238 | } 239 | }, 240 | RangeBound::Version(end) => match (start.kind, end.kind) { 241 | (VersionBoundKind::Inclusive, VersionBoundKind::Inclusive) => { 242 | if start.version == end.version { 243 | builder.append(&start.version); 244 | } else if is_zero_version(&start.version) { 245 | builder.append("<="); 246 | builder.append(&end.version); 247 | } else { 248 | builder.append(">="); 249 | builder.append(&start.version); 250 | builder.append(" <="); 251 | builder.append(&end.version); 252 | } 253 | } 254 | (VersionBoundKind::Inclusive, VersionBoundKind::Exclusive) => { 255 | if end.version.patch == 0 256 | && start.version.patch == 0 257 | && end.version.pre.is_empty() 258 | && end.version.build.is_empty() 259 | && start.version.pre.is_empty() 260 | && start.version.build.is_empty() 261 | { 262 | // check if we can write out `^1.0.0` as `1` 263 | if end.version.minor == 0 && start.version.minor == 0 { 264 | if let Some(one_major_higher) = 265 | start.version.major.checked_add(1) 266 | && end.version.major == one_major_higher 267 | { 268 | builder.append(start.version.major); 269 | return; 270 | } 271 | } else if start.version.major == end.version.major { 272 | // check if we can write out `~1.1.0` as `1.1` 273 | if let Some(one_minor_higher) = 274 | start.version.minor.checked_add(1) 275 | && end.version.minor == one_minor_higher 276 | { 277 | builder.append(start.version.major); 278 | builder.append('.'); 279 | builder.append(start.version.minor); 280 | return; 281 | } 282 | } 283 | } 284 | 285 | if matches_tilde(&start.version, &end.version) { 286 | builder.append('~'); 287 | builder.append(&start.version); 288 | } else if matches_caret(&start.version, &end.version) { 289 | builder.append('^'); 290 | builder.append(&start.version); 291 | } else if is_zero_version(&start.version) { 292 | builder.append('<'); 293 | builder.append(&end.version); 294 | } else { 295 | builder.append(">="); 296 | builder.append(&start.version); 297 | builder.append(" <"); 298 | builder.append(&end.version); 299 | } 300 | } 301 | (VersionBoundKind::Exclusive, VersionBoundKind::Inclusive) => { 302 | builder.append('>'); 303 | builder.append(&start.version); 304 | builder.append(" <="); 305 | builder.append(&end.version); 306 | } 307 | (VersionBoundKind::Exclusive, VersionBoundKind::Exclusive) => { 308 | builder.append('>'); 309 | builder.append(&start.version); 310 | builder.append(" <"); 311 | builder.append(&end.version); 312 | } 313 | }, 314 | }, 315 | } 316 | } 317 | } 318 | 319 | impl VersionRange { 320 | pub fn all() -> VersionRange { 321 | VersionRange { 322 | start: RangeBound::Unbounded, 323 | end: RangeBound::Unbounded, 324 | } 325 | } 326 | 327 | pub fn none() -> VersionRange { 328 | VersionRange { 329 | start: RangeBound::Version(VersionBound { 330 | kind: VersionBoundKind::Inclusive, 331 | version: Version::default(), 332 | }), 333 | end: RangeBound::Version(VersionBound { 334 | kind: VersionBoundKind::Exclusive, 335 | version: Version::default(), 336 | }), 337 | } 338 | } 339 | 340 | /// If this range won't match anything. 341 | pub fn is_none(&self) -> bool { 342 | if let RangeBound::Version(end) = &self.end { 343 | end.kind == VersionBoundKind::Exclusive 344 | && end.version.major == 0 345 | && end.version.minor == 0 346 | && end.version.patch == 0 347 | } else { 348 | false 349 | } 350 | } 351 | 352 | pub fn satisfies(&self, version: &Version) -> bool { 353 | let satisfies = self.min_satisfies(version) && self.max_satisfies(version); 354 | if satisfies && !version.pre.is_empty() { 355 | // check either side of the range has a pre and same version 356 | self.start.has_pre_with_exact_major_minor_patch(version) 357 | || self.end.has_pre_with_exact_major_minor_patch(version) 358 | } else { 359 | satisfies 360 | } 361 | } 362 | 363 | fn min_satisfies(&self, version: &Version) -> bool { 364 | match &self.start { 365 | RangeBound::Unbounded => true, 366 | RangeBound::Version(bound) => match version.cmp(&bound.version) { 367 | Ordering::Less => false, 368 | Ordering::Equal => bound.kind == VersionBoundKind::Inclusive, 369 | Ordering::Greater => true, 370 | }, 371 | } 372 | } 373 | 374 | fn max_satisfies(&self, version: &Version) -> bool { 375 | match &self.end { 376 | RangeBound::Unbounded => true, 377 | RangeBound::Version(bound) => match version.cmp(&bound.version) { 378 | Ordering::Less => true, 379 | Ordering::Equal => bound.kind == VersionBoundKind::Inclusive, 380 | Ordering::Greater => false, 381 | }, 382 | } 383 | } 384 | 385 | pub fn clamp(&self, range: &VersionRange) -> VersionRange { 386 | let start = self.start.clamp_start(&range.start); 387 | let end = self.end.clamp_end(&range.end); 388 | let start = match start { 389 | RangeBound::Unbounded => start, 390 | // clamp the start range to the end when greater 391 | RangeBound::Version(_) => start.clamp_end(&end), 392 | }; 393 | VersionRange { start, end } 394 | } 395 | 396 | /// Gets if this range overlaps the provided version. 397 | pub fn intersects_version(&self, other_version: &Version) -> bool { 398 | match &self.start { 399 | RangeBound::Unbounded => match &self.end { 400 | RangeBound::Unbounded => true, 401 | RangeBound::Version(end) => match other_version.cmp(&end.version) { 402 | Ordering::Less => true, 403 | Ordering::Equal => end.kind == VersionBoundKind::Inclusive, 404 | Ordering::Greater => false, 405 | }, 406 | }, 407 | RangeBound::Version(start) => match other_version.cmp(&start.version) { 408 | Ordering::Less => false, 409 | Ordering::Equal => start.kind == VersionBoundKind::Inclusive, 410 | Ordering::Greater => match &self.end { 411 | RangeBound::Unbounded => true, 412 | RangeBound::Version(end) => match other_version.cmp(&end.version) { 413 | Ordering::Less => true, 414 | Ordering::Equal => end.kind == VersionBoundKind::Inclusive, 415 | Ordering::Greater => false, 416 | }, 417 | }, 418 | }, 419 | } 420 | } 421 | 422 | /// Gets if this range overlaps the provided range at any point. 423 | pub fn intersects_range(&self, other_range: &VersionRange) -> bool { 424 | fn is_less_than_or_equal(a: &VersionBound, b: &VersionBound) -> bool { 425 | // note: we've picked the bounds "exclusive 3.0.0" and "inclusive 3.0.0" to always 426 | // return false for the purposes of this function and that's why this is internal 427 | // code. Due to this scenario, I don't believe it would make sense to move this 428 | // to a PartialOrd or Ord impl 429 | match a.kind { 430 | VersionBoundKind::Inclusive => match b.kind { 431 | VersionBoundKind::Inclusive => { 432 | matches!( 433 | a.version.cmp(&b.version), 434 | Ordering::Less | Ordering::Equal 435 | ) 436 | } 437 | VersionBoundKind::Exclusive => match a.version.cmp(&b.version) { 438 | Ordering::Equal => false, 439 | ordering => matches!(ordering, Ordering::Less | Ordering::Equal), 440 | }, 441 | }, 442 | VersionBoundKind::Exclusive => match b.kind { 443 | VersionBoundKind::Inclusive => match a.version.cmp(&b.version) { 444 | Ordering::Equal => false, 445 | ordering => matches!(ordering, Ordering::Less | Ordering::Equal), 446 | }, 447 | VersionBoundKind::Exclusive => match a.version.cmp(&b.version) { 448 | Ordering::Equal => false, 449 | ordering => matches!(ordering, Ordering::Less | Ordering::Equal), 450 | }, 451 | }, 452 | } 453 | } 454 | 455 | use RangeBound::*; 456 | 457 | match (&self.start, &self.end, &other_range.start, &other_range.end) { 458 | // either range is entirely unbounded 459 | (Unbounded, Unbounded, _, _) | (_, _, Unbounded, Unbounded) => true, 460 | // first range is unbounded on the left 461 | (Unbounded, Version(self_end), Version(range_start), _) => { 462 | is_less_than_or_equal(range_start, self_end) 463 | } 464 | // first range is unbounded on the right 465 | (Version(self_start), Unbounded, _, Version(range_end)) => { 466 | is_less_than_or_equal(self_start, range_end) 467 | } 468 | // second range is unbounded on the left 469 | (Version(self_start), _, Unbounded, Version(range_end)) => { 470 | is_less_than_or_equal(self_start, range_end) 471 | } 472 | // second range is unbounded on the right 473 | (_, Version(self_end), Version(range_start), Unbounded) => { 474 | is_less_than_or_equal(range_start, self_end) 475 | } 476 | // both versions are unbounded on the left 477 | (Unbounded, Version(self_end), Unbounded, Version(range_end)) => { 478 | is_less_than_or_equal(range_end, self_end) 479 | } 480 | // both versions are unbounded on the right 481 | (Version(self_start), Unbounded, Version(range_start), Unbounded) => { 482 | is_less_than_or_equal(self_start, range_start) 483 | } 484 | // Compare exact VersionBounds for both ranges 485 | ( 486 | Version(self_start), 487 | Version(self_end), 488 | Version(range_start), 489 | Version(range_end), 490 | ) => { 491 | is_less_than_or_equal(self_start, range_end) 492 | && is_less_than_or_equal(range_start, self_end) 493 | } 494 | } 495 | } 496 | } 497 | 498 | /// A range that could be a wildcard or number value. 499 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 500 | pub enum XRange { 501 | Wildcard, 502 | Val(u64), 503 | } 504 | 505 | /// A partial version. 506 | #[derive(Debug, Clone)] 507 | pub struct Partial { 508 | pub major: XRange, 509 | pub minor: XRange, 510 | pub patch: XRange, 511 | pub pre: CowVec, 512 | pub build: CowVec, 513 | } 514 | 515 | impl Partial { 516 | pub fn as_tilde_version_range(&self) -> VersionRange { 517 | // tilde ranges allow patch-level changes 518 | let end = match self.major { 519 | XRange::Wildcard => return VersionRange::all(), 520 | XRange::Val(major) => match self.minor { 521 | XRange::Wildcard => Version { 522 | major: major + 1, 523 | minor: 0, 524 | patch: 0, 525 | pre: Default::default(), 526 | build: Default::default(), 527 | }, 528 | XRange::Val(minor) => Version { 529 | major, 530 | minor: minor + 1, 531 | patch: 0, 532 | pre: Default::default(), 533 | build: Default::default(), 534 | }, 535 | }, 536 | }; 537 | VersionRange { 538 | start: self.as_lower_bound(), 539 | end: RangeBound::exclusive(end), 540 | } 541 | } 542 | 543 | pub fn as_caret_version_range(&self) -> VersionRange { 544 | // partial ranges allow patch and minor updates, except when 545 | // leading parts are < 1 in which case it will only bump the 546 | // first non-zero or patch part 547 | let end = match self.major { 548 | XRange::Wildcard => return VersionRange::all(), 549 | XRange::Val(major) => { 550 | let next_major = Version { 551 | major: major + 1, 552 | ..Default::default() 553 | }; 554 | if major > 0 { 555 | next_major 556 | } else { 557 | match self.minor { 558 | XRange::Wildcard => next_major, 559 | XRange::Val(minor) => { 560 | let next_minor = Version { 561 | minor: minor + 1, 562 | ..Default::default() 563 | }; 564 | if minor > 0 { 565 | next_minor 566 | } else { 567 | match self.patch { 568 | XRange::Wildcard => next_minor, 569 | XRange::Val(patch) => Version { 570 | patch: patch + 1, 571 | ..Default::default() 572 | }, 573 | } 574 | } 575 | } 576 | } 577 | } 578 | } 579 | }; 580 | VersionRange { 581 | start: self.as_lower_bound(), 582 | end: RangeBound::Version(VersionBound { 583 | kind: VersionBoundKind::Exclusive, 584 | version: end, 585 | }), 586 | } 587 | } 588 | 589 | pub fn as_lower_bound(&self) -> RangeBound { 590 | RangeBound::inclusive(Version { 591 | major: match self.major { 592 | XRange::Val(val) => val, 593 | XRange::Wildcard => 0, 594 | }, 595 | minor: match self.minor { 596 | XRange::Val(val) => val, 597 | XRange::Wildcard => 0, 598 | }, 599 | patch: match self.patch { 600 | XRange::Val(val) => val, 601 | XRange::Wildcard => 0, 602 | }, 603 | pre: self.pre.clone(), 604 | build: self.build.clone(), 605 | }) 606 | } 607 | 608 | pub fn as_upper_bound(&self) -> RangeBound { 609 | let mut end = Version::default(); 610 | let mut kind = VersionBoundKind::Inclusive; 611 | match self.patch { 612 | XRange::Wildcard => { 613 | end.minor += 1; 614 | kind = VersionBoundKind::Exclusive; 615 | } 616 | XRange::Val(val) => { 617 | end.patch = val; 618 | } 619 | } 620 | match self.minor { 621 | XRange::Wildcard => { 622 | end.minor = 0; 623 | end.major += 1; 624 | kind = VersionBoundKind::Exclusive; 625 | } 626 | XRange::Val(val) => { 627 | end.minor += val; 628 | } 629 | } 630 | match self.major { 631 | XRange::Wildcard => { 632 | return RangeBound::Unbounded; 633 | } 634 | XRange::Val(val) => { 635 | end.major += val; 636 | } 637 | } 638 | 639 | if kind == VersionBoundKind::Inclusive { 640 | end.pre = self.pre.clone(); 641 | } 642 | 643 | RangeBound::version(kind, end) 644 | } 645 | 646 | pub fn as_equal_range(&self) -> VersionRange { 647 | let major = match self.major { 648 | XRange::Wildcard => { 649 | return self.as_greater_range(VersionBoundKind::Inclusive); 650 | } 651 | XRange::Val(val) => val, 652 | }; 653 | let minor = match self.minor { 654 | XRange::Wildcard => { 655 | return self.as_greater_range(VersionBoundKind::Inclusive); 656 | } 657 | XRange::Val(val) => val, 658 | }; 659 | let patch = match self.patch { 660 | XRange::Wildcard => { 661 | return self.as_greater_range(VersionBoundKind::Inclusive); 662 | } 663 | XRange::Val(val) => val, 664 | }; 665 | let version = Version { 666 | major, 667 | minor, 668 | patch, 669 | pre: self.pre.clone(), 670 | build: self.build.clone(), 671 | }; 672 | VersionRange { 673 | start: RangeBound::inclusive(version.clone()), 674 | end: RangeBound::inclusive(version), 675 | } 676 | } 677 | 678 | pub fn as_greater_than( 679 | &self, 680 | mut start_kind: VersionBoundKind, 681 | ) -> VersionRange { 682 | let major = match self.major { 683 | XRange::Wildcard => match start_kind { 684 | VersionBoundKind::Inclusive => return VersionRange::all(), 685 | VersionBoundKind::Exclusive => return VersionRange::none(), 686 | }, 687 | XRange::Val(major) => major, 688 | }; 689 | let mut start = Version::default(); 690 | 691 | if start_kind == VersionBoundKind::Inclusive { 692 | start.pre = self.pre.clone(); 693 | } 694 | 695 | start.major = major; 696 | match self.minor { 697 | XRange::Wildcard => { 698 | if start_kind == VersionBoundKind::Exclusive { 699 | start_kind = VersionBoundKind::Inclusive; 700 | start.major += 1; 701 | } 702 | } 703 | XRange::Val(minor) => { 704 | start.minor = minor; 705 | } 706 | } 707 | match self.patch { 708 | XRange::Wildcard => { 709 | if start_kind == VersionBoundKind::Exclusive { 710 | start_kind = VersionBoundKind::Inclusive; 711 | start.minor += 1; 712 | } 713 | } 714 | XRange::Val(patch) => { 715 | start.patch = patch; 716 | 717 | if !self.pre.is_empty() && start_kind == VersionBoundKind::Exclusive { 718 | start.pre = self.pre.clone(); 719 | } 720 | } 721 | } 722 | 723 | VersionRange { 724 | start: RangeBound::version(start_kind, start), 725 | end: RangeBound::Unbounded, 726 | } 727 | } 728 | 729 | pub fn as_less_than(&self, mut end_kind: VersionBoundKind) -> VersionRange { 730 | let major = match self.major { 731 | XRange::Wildcard => match end_kind { 732 | VersionBoundKind::Inclusive => return VersionRange::all(), 733 | VersionBoundKind::Exclusive => return VersionRange::none(), 734 | }, 735 | XRange::Val(major) => major, 736 | }; 737 | let mut end = Version { 738 | major, 739 | ..Default::default() 740 | }; 741 | match self.minor { 742 | XRange::Wildcard => { 743 | if end_kind == VersionBoundKind::Inclusive { 744 | end.major += 1; 745 | } 746 | end_kind = VersionBoundKind::Exclusive; 747 | } 748 | XRange::Val(minor) => { 749 | end.minor = minor; 750 | } 751 | } 752 | match self.patch { 753 | XRange::Wildcard => { 754 | if end_kind == VersionBoundKind::Inclusive { 755 | end.minor += 1; 756 | } 757 | end_kind = VersionBoundKind::Exclusive; 758 | } 759 | XRange::Val(patch) => { 760 | end.patch = patch; 761 | 762 | if !self.pre.is_empty() && end_kind == VersionBoundKind::Exclusive { 763 | end.pre = self.pre.clone(); 764 | } 765 | } 766 | } 767 | if end_kind == VersionBoundKind::Inclusive { 768 | end.pre = self.pre.clone(); 769 | } 770 | VersionRange { 771 | // doesn't include 0.0.0 pre-release versions 772 | start: RangeBound::Version(VersionBound { 773 | kind: VersionBoundKind::Inclusive, 774 | version: Version::default(), 775 | }), 776 | end: RangeBound::version(end_kind, end), 777 | } 778 | } 779 | 780 | pub fn as_greater_range(&self, start_kind: VersionBoundKind) -> VersionRange { 781 | let major = match self.major { 782 | XRange::Wildcard => return VersionRange::all(), 783 | XRange::Val(major) => major, 784 | }; 785 | let mut start = Version::default(); 786 | let mut end = Version::default(); 787 | start.major = major; 788 | end.major = major; 789 | match self.patch { 790 | XRange::Wildcard => { 791 | if self.minor != XRange::Wildcard { 792 | end.minor += 1; 793 | } 794 | } 795 | XRange::Val(patch) => { 796 | start.patch = patch; 797 | end.patch = patch; 798 | } 799 | } 800 | match self.minor { 801 | XRange::Wildcard => { 802 | end.major += 1; 803 | } 804 | XRange::Val(minor) => { 805 | start.minor = minor; 806 | end.minor += minor; 807 | } 808 | } 809 | let end_kind = if start_kind == VersionBoundKind::Inclusive && start == end 810 | { 811 | VersionBoundKind::Inclusive 812 | } else { 813 | VersionBoundKind::Exclusive 814 | }; 815 | VersionRange { 816 | start: RangeBound::version(start_kind, start), 817 | end: RangeBound::version(end_kind, end), 818 | } 819 | } 820 | } 821 | 822 | #[cfg(test)] 823 | mod test { 824 | use crate::npm::parse_npm_version_req; 825 | 826 | use super::*; 827 | 828 | #[test] 829 | fn test_version_range_intersects_version() { 830 | let v1 = Version::parse_standard("1.0.0").unwrap(); 831 | let v1_5 = Version::parse_standard("1.5.0").unwrap(); 832 | let v2 = Version::parse_standard("2.0.0").unwrap(); 833 | let v3 = Version::parse_standard("3.0.0").unwrap(); 834 | let v0_5 = Version::parse_standard("0.5.0").unwrap(); 835 | let range_1_incl_2_incl = VersionRange { 836 | start: version_bound(VersionBoundKind::Inclusive, "1.0.0"), 837 | end: version_bound(VersionBoundKind::Inclusive, "2.0.0"), 838 | }; 839 | let range_1_incl_2_excl = VersionRange { 840 | start: version_bound(VersionBoundKind::Inclusive, "1.0.0"), 841 | end: version_bound(VersionBoundKind::Exclusive, "2.0.0"), 842 | }; 843 | let range_1_excl_2_incl = VersionRange { 844 | start: version_bound(VersionBoundKind::Exclusive, "1.0.0"), 845 | end: version_bound(VersionBoundKind::Inclusive, "2.0.0"), 846 | }; 847 | let range_unbounded_2_incl = VersionRange { 848 | start: RangeBound::Unbounded, 849 | end: version_bound(VersionBoundKind::Inclusive, "2.0.0"), 850 | }; 851 | let range_1_incl_unbounded = VersionRange { 852 | start: version_bound(VersionBoundKind::Inclusive, "1.0.0"), 853 | end: RangeBound::Unbounded, 854 | }; 855 | 856 | assert!(range_1_incl_2_incl.intersects_version(&v1)); 857 | assert!(range_1_incl_2_incl.intersects_version(&v2)); 858 | assert!(range_1_incl_2_excl.intersects_version(&v1_5)); 859 | assert!(!range_1_incl_2_excl.intersects_version(&v2)); 860 | assert!(!range_1_excl_2_incl.intersects_version(&v1)); 861 | assert!(range_unbounded_2_incl.intersects_version(&v0_5)); 862 | assert!(range_1_incl_unbounded.intersects_version(&v1)); 863 | assert!(range_1_incl_unbounded.intersects_version(&v3)); 864 | } 865 | 866 | #[test] 867 | fn test_version_range_intersects_range() { 868 | let range_1_incl_2_incl = VersionRange { 869 | start: version_bound(VersionBoundKind::Inclusive, "1.0.0"), 870 | end: version_bound(VersionBoundKind::Inclusive, "2.0.0"), 871 | }; 872 | let range_1_incl_3_excl = VersionRange { 873 | start: version_bound(VersionBoundKind::Inclusive, "1.0.0"), 874 | end: version_bound(VersionBoundKind::Exclusive, "3.0.0"), 875 | }; 876 | let range_2_incl_3_incl = VersionRange { 877 | start: version_bound(VersionBoundKind::Inclusive, "2.0.0"), 878 | end: version_bound(VersionBoundKind::Inclusive, "3.0.0"), 879 | }; 880 | let range_3_incl_unbounded = VersionRange { 881 | start: version_bound(VersionBoundKind::Inclusive, "3.0.0"), 882 | end: RangeBound::Unbounded, 883 | }; 884 | let range_unbounded_2_excl = VersionRange { 885 | start: RangeBound::Unbounded, 886 | end: version_bound(VersionBoundKind::Exclusive, "2.0.0"), 887 | }; 888 | let range_2_excl_3_excl = VersionRange { 889 | start: version_bound(VersionBoundKind::Exclusive, "2.0.0"), 890 | end: version_bound(VersionBoundKind::Exclusive, "3.0.0"), 891 | }; 892 | let range_3_excl_4_incl = VersionRange { 893 | start: version_bound(VersionBoundKind::Exclusive, "3.0.0"), 894 | end: version_bound(VersionBoundKind::Inclusive, "4.0.0"), 895 | }; 896 | 897 | // overlapping cases 898 | assert!(range_1_incl_2_incl.intersects_range(&range_1_incl_3_excl)); 899 | assert!(range_1_incl_3_excl.intersects_range(&range_2_incl_3_incl)); 900 | assert!(range_3_incl_unbounded.intersects_range(&range_2_incl_3_incl)); 901 | assert!(range_1_incl_2_incl.intersects_range(&range_unbounded_2_excl)); 902 | 903 | // non-overlapping cases 904 | assert!(!range_1_incl_2_incl.intersects_range(&range_3_incl_unbounded)); 905 | assert!(!range_unbounded_2_excl.intersects_range(&range_2_incl_3_incl)); 906 | assert!(!range_unbounded_2_excl.intersects_range(&range_3_incl_unbounded)); 907 | assert!(!range_2_excl_3_excl.intersects_range(&range_3_excl_4_incl)); 908 | } 909 | 910 | fn version_bound(kind: VersionBoundKind, ver: &str) -> RangeBound { 911 | RangeBound::Version(VersionBound { 912 | kind, 913 | version: Version::parse_standard(ver).unwrap(), 914 | }) 915 | } 916 | 917 | #[test] 918 | fn range_set_or_tag_display() { 919 | #[track_caller] 920 | fn run_test(input: &str, expected: &str) { 921 | let version_req = parse_npm_version_req(input).unwrap(); 922 | let output = version_req.inner().to_string(); 923 | assert_eq!(output, expected); 924 | let reparsed = parse_npm_version_req(&output).unwrap(); 925 | assert_eq!(reparsed.inner(), version_req.inner()); 926 | } 927 | 928 | // Basic caret and tilde tests 929 | run_test("^0.0.1", "^0.0.1"); 930 | run_test("^0.1.0", "0.1"); // normalizes 931 | 932 | // Zero major, minor, patch 933 | run_test("1.0.0", "1.0.0"); 934 | run_test("1", "1"); 935 | run_test("1.0", "1.0"); 936 | run_test("1.2", "1.2"); 937 | run_test("1.2.0", "1.2.0"); 938 | run_test("^1.0.0", "1"); 939 | run_test("~1.0.0", "1.0"); 940 | run_test("1.1.0 - 3.1.0", ">=1.1.0 <=3.1.0"); 941 | 942 | // Exact 943 | run_test("1.2.3", "1.2.3"); 944 | 945 | // More complex caret tests 946 | run_test("^0.2.3", "~0.2.3"); // normalizes 947 | run_test("^2.3.4", "^2.3.4"); 948 | 949 | // More complex tilde tests 950 | run_test("~0.2.3", "~0.2.3"); 951 | run_test("~2.3.4", "~2.3.4"); 952 | 953 | // Exact versions and simple ranges 954 | run_test("2.3.4", "2.3.4"); 955 | run_test(">2.3.4", ">2.3.4"); 956 | run_test(">=2.3.4", ">=2.3.4"); 957 | run_test("<2.3.4", "<2.3.4"); 958 | run_test("<=2.3.4", "<=2.3.4"); 959 | 960 | // Pre-release version tests 961 | run_test("^1.0.0-beta.1", "^1.0.0-beta.1"); 962 | run_test("~1.0.0-beta.1", "~1.0.0-beta.1"); 963 | run_test("1.0.0-beta.1", "1.0.0-beta.1"); 964 | 965 | // Build metadata tests 966 | run_test("1.0.0+build123", "1.0.0+build123"); 967 | run_test("^1.0.0+build123", "^1.0.0+build123"); 968 | run_test("~1.0.0+build123", "~1.0.0+build123"); 969 | 970 | // More edge cases with zero major versions 971 | run_test("^0.0.2", "^0.0.2"); 972 | run_test("^0.0.0", "^0.0.0"); 973 | run_test("^0.0.0-pre", "^0.0.0-pre"); 974 | run_test("0.0.1", "0.0.1"); 975 | 976 | // Wildcard ranges 977 | run_test("*", "*"); 978 | run_test(">=1.0.0 <2.0.0", "1"); // normalizes 979 | 980 | // Testing exact zero versions with pre-releases 981 | run_test("0.0.0-alpha", "0.0.0-alpha"); 982 | run_test("^0.0.0-alpha", "^0.0.0-alpha"); 983 | 984 | // Caret and tilde with pre-release and build metadata 985 | run_test("^1.0.0-beta.1+build123", "^1.0.0-beta.1+build123"); 986 | run_test("~1.0.0-beta.1+build123", "~1.0.0-beta.1+build123"); 987 | 988 | // Exact pre-release versions with build metadata 989 | run_test("1.0.0-beta.1+build123", "1.0.0-beta.1+build123"); 990 | 991 | // Ranges with different inclusive/exclusive combinations 992 | run_test(">1.0.0-beta.1 <=1.1.0", ">1.0.0-beta.1 <=1.1.0"); 993 | run_test(">1.0.0 <1.1.0", ">1.0.0 <1.1.0"); 994 | 995 | run_test( 996 | ">=2.0.0-rc.3.0.5 <2.0.0-rc.3.1.0", 997 | ">=2.0.0-rc.3.0.5 <2.0.0-rc.3.1.0", 998 | ); 999 | run_test( 1000 | ">2.0.0-rc.3.0.5 <2.0.0-rc.3.1.0", 1001 | ">2.0.0-rc.3.0.5 <2.0.0-rc.3.1.0", 1002 | ); 1003 | 1004 | run_test(">=2.0.0-rc.3.0.5 <2.1.0", "~2.0.0-rc.3.0.5"); 1005 | 1006 | // Pre-release with tilde normalization 1007 | run_test("^0.2.3-beta.1", "~0.2.3-beta.1"); // normalizes 1008 | 1009 | // Pre-release with zero major and complex range 1010 | run_test("^0.0.2-alpha", "^0.0.2-alpha"); 1011 | run_test("~0.0.2-alpha", "~0.0.2-alpha"); 1012 | 1013 | // Greater than pre-release version 1014 | run_test(">1.0.0-beta.1", ">1.0.0-beta.1"); 1015 | 1016 | // Less than pre-release version 1017 | run_test("<1.0.0-beta.1", "<1.0.0-beta.1"); 1018 | 1019 | // Or conditions 1020 | run_test("1 || 2 || 3", "1 || 2 || 3"); 1021 | } 1022 | } 1023 | -------------------------------------------------------------------------------- /src/npm.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2025 the Deno authors. All rights reserved. MIT license. 2 | 3 | use std::borrow::Cow; 4 | 5 | use capacity_builder::CapacityDisplay; 6 | use capacity_builder::StringAppendable; 7 | use capacity_builder::StringType; 8 | use deno_error::JsError; 9 | use monch::*; 10 | use thiserror::Error; 11 | 12 | use serde::Deserialize; 13 | use serde::Serialize; 14 | use url::Url; 15 | 16 | use crate::CowVec; 17 | use crate::PackageTag; 18 | use crate::common::logical_and; 19 | use crate::common::logical_or; 20 | use crate::common::primitive; 21 | use crate::common::primitive_kind; 22 | use crate::common::qualifier; 23 | use crate::package::PackageKind; 24 | use crate::package::PackageNv; 25 | use crate::package::PackageNvReference; 26 | use crate::package::PackageNvReferenceParseError; 27 | use crate::package::PackageReq; 28 | use crate::package::PackageReqReference; 29 | use crate::package::PackageReqReferenceParseError; 30 | use crate::range_set_or_tag::RangeOrInvalid; 31 | 32 | use super::Partial; 33 | use super::RangeSetOrTag; 34 | use super::Version; 35 | use super::VersionRange; 36 | use super::VersionRangeSet; 37 | use super::VersionReq; 38 | use super::XRange; 39 | 40 | pub fn is_valid_npm_tag(value: &str) -> bool { 41 | if value.trim().is_empty() { 42 | return false; 43 | } 44 | 45 | // a valid tag is anything that doesn't get url encoded 46 | // https://github.com/npm/npm-package-arg/blob/103c0fda8ed8185733919c7c6c73937cfb2baf3a/lib/npa.js#L399-L401 47 | value 48 | .chars() 49 | .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '~')) 50 | } 51 | 52 | // A lot of the below is a re-implementation of parts of https://github.com/npm/node-semver 53 | // which is Copyright (c) Isaac Z. Schlueter and Contributors (ISC License) 54 | 55 | #[derive(Error, Debug, Clone, JsError)] 56 | #[class(type)] 57 | #[error("Invalid npm version")] 58 | pub struct NpmVersionParseError { 59 | #[source] 60 | pub(crate) source: ParseErrorFailureError, 61 | } 62 | 63 | impl NpmVersionParseError { 64 | /// Message of the error excluding the code snippet. 65 | pub fn message(&self) -> &str { 66 | &self.source.message 67 | } 68 | } 69 | 70 | pub fn parse_npm_version(text: &str) -> Result { 71 | let text = text.trim(); 72 | with_failure_handling(|input| { 73 | let (input, _) = maybe(ch('='))(input)?; // skip leading = 74 | let (input, _) = skip_whitespace(input)?; 75 | let (input, _) = maybe(ch('v'))(input)?; // skip leading v 76 | let (input, _) = skip_whitespace(input)?; 77 | let (input, major) = nr(input)?; 78 | let (input, _) = ch('.')(input)?; 79 | let (input, minor) = nr(input)?; 80 | let (input, _) = ch('.')(input)?; 81 | let (input, patch) = nr(input)?; 82 | let (input, q) = maybe(qualifier)(input)?; 83 | let q = q.unwrap_or_default(); 84 | 85 | Ok(( 86 | input, 87 | Version { 88 | major, 89 | minor, 90 | patch, 91 | pre: q.pre, 92 | build: q.build, 93 | }, 94 | )) 95 | })(text) 96 | .map_err(|err| NpmVersionParseError { source: err }) 97 | } 98 | 99 | #[derive(Error, Debug, Clone, JsError, PartialEq, Eq)] 100 | #[class(type)] 101 | #[error("Invalid version requirement")] 102 | pub struct NpmVersionReqParseError { 103 | #[source] 104 | pub source: ParseErrorFailureError, 105 | } 106 | 107 | pub fn parse_npm_version_req( 108 | text: &str, 109 | ) -> Result { 110 | let text = text.trim(); 111 | with_failure_handling(|input| { 112 | map(inner, |inner| { 113 | VersionReq::from_raw_text_and_inner( 114 | crate::SmallStackString::from_str(input), 115 | inner, 116 | ) 117 | })(input) 118 | })(text) 119 | .map_err(|err| NpmVersionReqParseError { source: err }) 120 | } 121 | 122 | // https://github.com/npm/node-semver/tree/4907647d169948a53156502867ed679268063a9f#range-grammar 123 | // range-set ::= range ( logical-or range ) * 124 | // logical-or ::= ( ' ' ) * '||' ( ' ' ) * 125 | // range ::= hyphen | simple ( ' ' simple ) * | '' 126 | // hyphen ::= partial ' - ' partial 127 | // simple ::= primitive | partial | tilde | caret 128 | // primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial 129 | // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? 130 | // xr ::= 'x' | 'X' | '*' | nr 131 | // nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * 132 | // tilde ::= '~' partial 133 | // caret ::= '^' partial 134 | // qualifier ::= ( '-' pre )? ( '+' build )? 135 | // pre ::= parts 136 | // build ::= parts 137 | // parts ::= part ( '.' part ) * 138 | // part ::= nr | [-0-9A-Za-z]+ 139 | 140 | // range-set ::= range ( logical-or range ) * 141 | fn inner(input: &str) -> ParseResult<'_, RangeSetOrTag> { 142 | if input.is_empty() { 143 | return Ok(( 144 | input, 145 | RangeSetOrTag::RangeSet(VersionRangeSet(CowVec::from([ 146 | VersionRange::all(), 147 | ]))), 148 | )); 149 | } 150 | 151 | let (input, mut ranges) = separated_list( 152 | |text| RangeOrInvalid::parse(text, range), 153 | logical_or, 154 | )(input)?; 155 | 156 | if ranges.len() == 1 { 157 | match ranges.remove(0) { 158 | RangeOrInvalid::Invalid(invalid) => { 159 | if is_valid_npm_tag(invalid.text) { 160 | return Ok(( 161 | input, 162 | RangeSetOrTag::Tag(PackageTag::from_str(invalid.text)), 163 | )); 164 | } else { 165 | return Err(invalid.failure); 166 | } 167 | } 168 | RangeOrInvalid::Range(range) => { 169 | // add it back 170 | ranges.push(RangeOrInvalid::Range(range)); 171 | } 172 | } 173 | } 174 | 175 | let ranges = ranges 176 | .into_iter() 177 | .filter_map(|r| r.into_range().ok().flatten()) 178 | .collect::>(); 179 | Ok((input, RangeSetOrTag::RangeSet(VersionRangeSet(ranges)))) 180 | } 181 | 182 | // range ::= hyphen | simple ( ' ' simple ) * | '' 183 | fn range(input: &str) -> ParseResult<'_, VersionRange> { 184 | or( 185 | map(hyphen, |hyphen| VersionRange { 186 | start: hyphen.start.as_lower_bound(), 187 | end: hyphen.end.as_upper_bound(), 188 | }), 189 | map(separated_list(simple, range_separator), |ranges| { 190 | let mut final_range = VersionRange::all(); 191 | for range in ranges { 192 | final_range = final_range.clamp(&range); 193 | } 194 | final_range 195 | }), 196 | )(input) 197 | } 198 | 199 | fn range_separator(input: &str) -> ParseResult<'_, ()> { 200 | fn comma(input: &str) -> ParseResult<'_, ()> { 201 | map(delimited(skip_whitespace, ch(','), skip_whitespace), |_| ())(input) 202 | } 203 | 204 | or3(map(logical_and, |_| ()), comma, map(whitespace, |_| ()))(input) 205 | } 206 | 207 | #[derive(Debug, Clone)] 208 | struct Hyphen { 209 | start: Partial, 210 | end: Partial, 211 | } 212 | 213 | // hyphen ::= partial ' - ' partial 214 | fn hyphen(input: &str) -> ParseResult<'_, Hyphen> { 215 | let (input, first) = partial(input)?; 216 | let (input, _) = whitespace(input)?; 217 | let (input, _) = tag("-")(input)?; 218 | let (input, _) = whitespace(input)?; 219 | let (input, second) = partial(input)?; 220 | Ok(( 221 | input, 222 | Hyphen { 223 | start: first, 224 | end: second, 225 | }, 226 | )) 227 | } 228 | 229 | fn skip_whitespace_or_v(input: &str) -> ParseResult<'_, ()> { 230 | map( 231 | pair(skip_whitespace, pair(maybe(ch('v')), skip_whitespace)), 232 | |_| (), 233 | )(input) 234 | } 235 | 236 | // simple ::= primitive | partial | tilde | caret 237 | fn simple(input: &str) -> ParseResult<'_, VersionRange> { 238 | or4( 239 | map(preceded(tilde, partial), |partial| { 240 | partial.as_tilde_version_range() 241 | }), 242 | map(preceded(caret, partial), |partial| { 243 | partial.as_caret_version_range() 244 | }), 245 | map(primitive(partial), |primitive| { 246 | primitive.into_version_range() 247 | }), 248 | map(partial, |partial| partial.as_equal_range()), 249 | )(input) 250 | } 251 | 252 | fn tilde(input: &str) -> ParseResult<'_, ()> { 253 | fn raw_tilde(input: &str) -> ParseResult<'_, ()> { 254 | map( 255 | pair( 256 | terminated(or(tag("~>"), tag("~")), skip_while(|c| c == '=')), 257 | skip_whitespace_or_v, 258 | ), 259 | |_| (), 260 | )(input) 261 | } 262 | 263 | or( 264 | preceded(terminated(primitive_kind, whitespace), raw_tilde), 265 | raw_tilde, 266 | )(input) 267 | } 268 | 269 | fn caret(input: &str) -> ParseResult<'_, ()> { 270 | fn raw_caret(input: &str) -> ParseResult<'_, ()> { 271 | map( 272 | pair( 273 | terminated(tag("^"), skip_while(|c| c == '=')), 274 | skip_whitespace_or_v, 275 | ), 276 | |_| (), 277 | )(input) 278 | } 279 | 280 | or( 281 | preceded(terminated(primitive_kind, whitespace), raw_caret), 282 | raw_caret, 283 | )(input) 284 | } 285 | 286 | // partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )? 287 | fn partial(input: &str) -> ParseResult<'_, Partial> { 288 | let (input, _) = maybe(ch('v'))(input)?; // skip leading v 289 | crate::common::partial(xr)(input) 290 | } 291 | 292 | // xr ::= 'x' | 'X' | '*' | nr 293 | fn xr(input: &str) -> ParseResult<'_, XRange> { 294 | or( 295 | map(or3(tag("x"), tag("X"), tag("*")), |_| XRange::Wildcard), 296 | map(nr, XRange::Val), 297 | )(input) 298 | } 299 | 300 | // nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) * 301 | fn nr(input: &str) -> ParseResult<'_, u64> { 302 | // we do loose parsing to support people doing stuff like 01.02.03 303 | let (input, result) = 304 | if_not_empty(substring(skip_while(|c| c.is_ascii_digit())))(input)?; 305 | let val = match result.parse::() { 306 | Ok(val) => val, 307 | Err(err) => { 308 | return ParseError::fail( 309 | input, 310 | format!("Error parsing '{result}' to u64.\n\n{err:#}"), 311 | ); 312 | } 313 | }; 314 | Ok((input, val)) 315 | } 316 | 317 | /// A reference to an npm package's name, version constraint, and potential sub path. 318 | /// This contains all the information found in an npm specifier. 319 | /// 320 | /// This wraps PackageReqReference in order to prevent accidentally 321 | /// mixing this with other schemes. 322 | #[derive(Clone, Debug, PartialEq, Eq, Hash, CapacityDisplay)] 323 | pub struct NpmPackageReqReference(PackageReqReference); 324 | 325 | impl<'a> StringAppendable<'a> for &'a NpmPackageReqReference { 326 | fn append_to_builder( 327 | self, 328 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 329 | ) { 330 | builder.append("npm:"); 331 | builder.append(&self.0); 332 | } 333 | } 334 | 335 | impl NpmPackageReqReference { 336 | pub fn new(inner: PackageReqReference) -> Self { 337 | Self(inner) 338 | } 339 | 340 | pub fn from_specifier( 341 | specifier: &Url, 342 | ) -> Result { 343 | Self::from_str(specifier.as_str()) 344 | } 345 | 346 | #[allow(clippy::should_implement_trait)] 347 | pub fn from_str( 348 | specifier: &str, 349 | ) -> Result { 350 | PackageReqReference::from_str(specifier, PackageKind::Npm).map(Self) 351 | } 352 | 353 | pub fn req(&self) -> &PackageReq { 354 | &self.0.req 355 | } 356 | 357 | pub fn sub_path(&self) -> Option<&str> { 358 | self.0.sub_path.as_deref() 359 | } 360 | 361 | pub fn into_inner(self) -> PackageReqReference { 362 | self.0 363 | } 364 | } 365 | 366 | /// An npm package name and version with a potential subpath. 367 | /// 368 | /// This wraps PackageNvReference in order to prevent accidentally 369 | /// mixing this with other schemes. 370 | #[derive( 371 | Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, CapacityDisplay, 372 | )] 373 | pub struct NpmPackageNvReference(PackageNvReference); 374 | 375 | impl NpmPackageNvReference { 376 | pub fn new(inner: PackageNvReference) -> Self { 377 | Self(inner) 378 | } 379 | 380 | pub fn from_specifier( 381 | specifier: &Url, 382 | ) -> Result { 383 | Self::from_str(specifier.as_str()) 384 | } 385 | 386 | #[allow(clippy::should_implement_trait)] 387 | pub fn from_str(nv: &str) -> Result { 388 | PackageNvReference::from_str(nv, PackageKind::Npm).map(Self) 389 | } 390 | 391 | pub fn as_specifier(&self) -> Url { 392 | self.0.as_specifier(PackageKind::Npm) 393 | } 394 | 395 | pub fn nv(&self) -> &PackageNv { 396 | &self.0.nv 397 | } 398 | 399 | pub fn sub_path(&self) -> Option<&str> { 400 | self.0.sub_path.as_deref() 401 | } 402 | 403 | pub fn into_inner(self) -> PackageNvReference { 404 | self.0 405 | } 406 | } 407 | 408 | impl<'a> StringAppendable<'a> for &'a NpmPackageNvReference { 409 | fn append_to_builder( 410 | self, 411 | builder: &mut capacity_builder::StringBuilder<'a, TString>, 412 | ) { 413 | builder.append("npm:"); 414 | builder.append(&self.0); 415 | } 416 | } 417 | 418 | impl Serialize for NpmPackageNvReference { 419 | fn serialize(&self, serializer: S) -> Result 420 | where 421 | S: serde::Serializer, 422 | { 423 | serializer.serialize_str(&self.to_string()) 424 | } 425 | } 426 | 427 | impl<'de> Deserialize<'de> for NpmPackageNvReference { 428 | fn deserialize(deserializer: D) -> Result 429 | where 430 | D: serde::Deserializer<'de>, 431 | { 432 | let text: Cow<'de, str> = Deserialize::deserialize(deserializer)?; 433 | match Self::from_str(&text) { 434 | Ok(req) => Ok(req), 435 | Err(err) => Err(serde::de::Error::custom(err)), 436 | } 437 | } 438 | } 439 | 440 | #[cfg(test)] 441 | mod tests { 442 | use pretty_assertions::assert_eq; 443 | use std::cmp::Ordering; 444 | 445 | use crate::WILDCARD_VERSION_REQ; 446 | 447 | use super::*; 448 | 449 | struct NpmVersionReqTester(VersionReq); 450 | 451 | impl NpmVersionReqTester { 452 | fn new(text: &str) -> Self { 453 | Self(parse_npm_version_req(text).unwrap()) 454 | } 455 | 456 | fn matches(&self, version: &str) -> bool { 457 | self.0.matches(&parse_npm_version(version).unwrap()) 458 | } 459 | } 460 | 461 | #[test] 462 | pub fn npm_version_req_with_v() { 463 | assert!(parse_npm_version_req("v1.0.0").is_ok()); 464 | } 465 | 466 | #[test] 467 | pub fn npm_version_req_exact() { 468 | let tester = NpmVersionReqTester::new("2.1.2"); 469 | assert!(!tester.matches("2.1.1")); 470 | assert!(tester.matches("2.1.2")); 471 | assert!(!tester.matches("2.1.3")); 472 | 473 | let tester = NpmVersionReqTester::new("2.1.2 || 2.1.5"); 474 | assert!(!tester.matches("2.1.1")); 475 | assert!(tester.matches("2.1.2")); 476 | assert!(!tester.matches("2.1.3")); 477 | assert!(!tester.matches("2.1.4")); 478 | assert!(tester.matches("2.1.5")); 479 | assert!(!tester.matches("2.1.6")); 480 | } 481 | 482 | #[test] 483 | pub fn npm_version_req_minor() { 484 | let tester = NpmVersionReqTester::new("1.1"); 485 | assert!(!tester.matches("1.0.0")); 486 | assert!(tester.matches("1.1.0")); 487 | assert!(tester.matches("1.1.1")); 488 | assert!(!tester.matches("1.2.0")); 489 | assert!(!tester.matches("1.2.1")); 490 | } 491 | 492 | #[test] 493 | pub fn npm_version_req_ranges() { 494 | let tester = NpmVersionReqTester::new( 495 | ">= 2.1.2 < 3.0.0 || 5.x || ignored-invalid-range || $#$%^#$^#$^%@#$%SDF|||", 496 | ); 497 | assert!(!tester.matches("2.1.1")); 498 | assert!(tester.matches("2.1.2")); 499 | assert!(tester.matches("2.9.9")); 500 | assert!(!tester.matches("3.0.0")); 501 | assert!(tester.matches("5.0.0")); 502 | assert!(tester.matches("5.1.0")); 503 | assert!(!tester.matches("6.1.0")); 504 | } 505 | 506 | #[test] 507 | pub fn npm_version_req_and_range() { 508 | let tester = NpmVersionReqTester::new(">= 1.2 && <= 2.0.0"); 509 | assert!(!tester.matches("1.1.9")); 510 | assert!(tester.matches("1.2.2")); 511 | assert!(tester.matches("1.2.0")); 512 | assert!(tester.matches("1.9.9")); 513 | assert!(!tester.matches("2.1.1")); 514 | assert!(!tester.matches("2.0.1")); 515 | } 516 | 517 | #[test] 518 | pub fn npm_version_req_comma_range() { 519 | let tester = NpmVersionReqTester::new(">= 1.2, <= 2.0.0"); 520 | assert!(!tester.matches("1.1.9")); 521 | assert!(tester.matches("1.2.2")); 522 | assert!(tester.matches("1.2.0")); 523 | assert!(tester.matches("1.9.9")); 524 | assert!(!tester.matches("2.1.1")); 525 | assert!(!tester.matches("2.0.1")); 526 | } 527 | 528 | #[test] 529 | pub fn npm_version_req_with_tag() { 530 | let req = parse_npm_version_req("latest").unwrap(); 531 | assert_eq!(req.tag(), Some("latest")); 532 | } 533 | 534 | #[test] 535 | pub fn npm_version_req_prerelease() { 536 | let tester = NpmVersionReqTester::new("^1.0.0-beta-7"); 537 | assert!(tester.matches("1.0.0-beta-7")); 538 | let tester = NpmVersionReqTester::new("^0.0.1-beta-7"); 539 | assert!(tester.matches("0.0.1-beta-7")); 540 | } 541 | 542 | #[test] 543 | pub fn npm_version_req_0_version() { 544 | let tester = NpmVersionReqTester::new("^0.0.0-beta-7"); 545 | assert!(tester.matches("0.0.0-beta-7")); 546 | } 547 | 548 | macro_rules! assert_cmp { 549 | ($a:expr, $b:expr, $expected:expr) => { 550 | assert_eq!( 551 | $a.cmp(&$b), 552 | $expected, 553 | "expected {} to be {:?} {}", 554 | $a, 555 | $expected, 556 | $b 557 | ); 558 | }; 559 | } 560 | 561 | macro_rules! test_compare { 562 | ($a:expr, $b:expr, $expected:expr) => { 563 | let a = parse_npm_version($a).unwrap(); 564 | let b = parse_npm_version($b).unwrap(); 565 | assert_cmp!(a, b, $expected); 566 | }; 567 | } 568 | 569 | #[test] 570 | fn version_compare() { 571 | test_compare!("1.2.3", "2.3.4", Ordering::Less); 572 | test_compare!("1.2.3", "1.2.4", Ordering::Less); 573 | test_compare!("1.2.3", "1.2.3", Ordering::Equal); 574 | test_compare!("1.2.3", "1.2.2", Ordering::Greater); 575 | test_compare!("1.2.3", "1.1.5", Ordering::Greater); 576 | } 577 | 578 | #[test] 579 | fn version_compare_equal() { 580 | // https://github.com/npm/node-semver/blob/bce42589d33e1a99454530a8fd52c7178e2b11c1/test/fixtures/equality.js 581 | let fixtures = &[ 582 | ("1.2.3", "v1.2.3"), 583 | ("1.2.3", "=1.2.3"), 584 | ("1.2.3", "v 1.2.3"), 585 | ("1.2.3", "= 1.2.3"), 586 | ("1.2.3", " v1.2.3"), 587 | ("1.2.3", " =1.2.3"), 588 | ("1.2.3", " v 1.2.3"), 589 | ("1.2.3", " = 1.2.3"), 590 | ("1.2.3-0", "v1.2.3-0"), 591 | ("1.2.3-0", "=1.2.3-0"), 592 | ("1.2.3-0", "v 1.2.3-0"), 593 | ("1.2.3-0", "= 1.2.3-0"), 594 | ("1.2.3-0", " v1.2.3-0"), 595 | ("1.2.3-0", " =1.2.3-0"), 596 | ("1.2.3-0", " v 1.2.3-0"), 597 | ("1.2.3-0", " = 1.2.3-0"), 598 | ("1.2.3-1", "v1.2.3-1"), 599 | ("1.2.3-1", "=1.2.3-1"), 600 | ("1.2.3-1", "v 1.2.3-1"), 601 | ("1.2.3-1", "= 1.2.3-1"), 602 | ("1.2.3-1", " v1.2.3-1"), 603 | ("1.2.3-1", " =1.2.3-1"), 604 | ("1.2.3-1", " v 1.2.3-1"), 605 | ("1.2.3-1", " = 1.2.3-1"), 606 | ("1.2.3-beta", "v1.2.3-beta"), 607 | ("1.2.3-beta", "=1.2.3-beta"), 608 | ("1.2.3-beta", "v 1.2.3-beta"), 609 | ("1.2.3-beta", "= 1.2.3-beta"), 610 | ("1.2.3-beta", " v1.2.3-beta"), 611 | ("1.2.3-beta", " =1.2.3-beta"), 612 | ("1.2.3-beta", " v 1.2.3-beta"), 613 | ("1.2.3-beta", " = 1.2.3-beta"), 614 | ("1.2.3-beta+build", " = 1.2.3-beta+otherbuild"), 615 | ("1.2.3+build", " = 1.2.3+otherbuild"), 616 | ("1.2.3-beta+build", "1.2.3-beta+otherbuild"), 617 | ("1.2.3+build", "1.2.3+otherbuild"), 618 | (" v1.2.3+build", "1.2.3+otherbuild"), 619 | ]; 620 | for (a, b) in fixtures { 621 | test_compare!(a, b, Ordering::Equal); 622 | } 623 | } 624 | 625 | #[test] 626 | fn version_comparisons_test() { 627 | // https://github.com/npm/node-semver/blob/bce42589d33e1a99454530a8fd52c7178e2b11c1/test/fixtures/comparisons.js 628 | let fixtures = &[ 629 | ("0.0.0", "0.0.0-foo"), 630 | ("0.0.1", "0.0.0"), 631 | ("1.0.0", "0.9.9"), 632 | ("0.10.0", "0.9.0"), 633 | ("0.99.0", "0.10.0"), 634 | ("2.0.0", "1.2.3"), 635 | ("v0.0.0", "0.0.0-foo"), 636 | ("v0.0.1", "0.0.0"), 637 | ("v1.0.0", "0.9.9"), 638 | ("v0.10.0", "0.9.0"), 639 | ("v0.99.0", "0.10.0"), 640 | ("v2.0.0", "1.2.3"), 641 | ("0.0.0", "v0.0.0-foo"), 642 | ("0.0.1", "v0.0.0"), 643 | ("1.0.0", "v0.9.9"), 644 | ("0.10.0", "v0.9.0"), 645 | ("0.99.0", "v0.10.0"), 646 | ("2.0.0", "v1.2.3"), 647 | ("1.2.3", "1.2.3-asdf"), 648 | ("1.2.3", "1.2.3-4"), 649 | ("1.2.3", "1.2.3-4-foo"), 650 | ("1.2.3-5-foo", "1.2.3-5"), 651 | ("1.2.3-5", "1.2.3-4"), 652 | ("1.2.3-5-foo", "1.2.3-5-Foo"), 653 | ("3.0.0", "2.7.2+asdf"), 654 | ("1.2.3-a.10", "1.2.3-a.5"), 655 | ("1.2.3-a.b", "1.2.3-a.5"), 656 | ("1.2.3-a.b", "1.2.3-a"), 657 | ("1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"), 658 | ("1.2.3-r2", "1.2.3-r100"), 659 | ("1.2.3-r100", "1.2.3-R2"), 660 | ]; 661 | for (a, b) in fixtures { 662 | let a = parse_npm_version(a).unwrap(); 663 | let b = parse_npm_version(b).unwrap(); 664 | assert_cmp!(a, b, Ordering::Greater); 665 | assert_cmp!(b, a, Ordering::Less); 666 | assert_cmp!(a, a, Ordering::Equal); 667 | assert_cmp!(b, b, Ordering::Equal); 668 | } 669 | } 670 | 671 | #[test] 672 | fn range_parse() { 673 | // https://github.com/npm/node-semver/blob/4907647d169948a53156502867ed679268063a9f/test/fixtures/range-parse.js 674 | let fixtures = &[ 675 | // note: some of these tests previously used a `-0` pre-release (see link), 676 | // but I removed them because it didn't seem relevant 677 | ("1.0.0 - 2.0.0", ">=1.0.0 <=2.0.0"), 678 | ("1 - 2", ">=1.0.0 <3.0.0"), 679 | ("1.0 - 2.0", ">=1.0.0 <2.1.0"), 680 | ("1.0.0", "1.0.0"), 681 | (">=*", "*"), 682 | ("", "*"), 683 | ("*", "*"), 684 | ("*", "*"), 685 | (">=1.0.0", ">=1.0.0"), 686 | (">1.0.0", ">1.0.0"), 687 | ("<=2.0.0", "<=2.0.0"), 688 | ("1", ">=1.0.0 <2.0.0"), 689 | ("<=2.0.0", "<=2.0.0"), 690 | ("<=2.0.0", "<=2.0.0"), 691 | ("<2.0.0", "<2.0.0"), 692 | ("<2.0.0", "<2.0.0"), 693 | (">= 1.0.0", ">=1.0.0"), 694 | (">= 1.0.0", ">=1.0.0"), 695 | (">= 1.0.0", ">=1.0.0"), 696 | ("> 1.0.0", ">1.0.0"), 697 | ("> 1.0.0", ">1.0.0"), 698 | ("<= 2.0.0", "<=2.0.0"), 699 | ("<= 2.0.0", "<=2.0.0"), 700 | ("<= 2.0.0", "<=2.0.0"), 701 | ("< 2.0.0", "<2.0.0"), 702 | ("<\t2.0.0", "<2.0.0"), 703 | (">=0.1.97", ">=0.1.97"), 704 | (">=0.1.97", ">=0.1.97"), 705 | ("0.1.20 || 1.2.4", "0.1.20||1.2.4"), 706 | (">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"), 707 | (">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"), 708 | (">=0.2.3 || <0.0.1", ">=0.2.3||<0.0.1"), 709 | ("||", "*"), 710 | ("2.x.x", ">=2.0.0 <3.0.0"), 711 | ("1.2.x", ">=1.2.0 <1.3.0"), 712 | ("1.2.x || 2.x", ">=1.2.0 <1.3.0||>=2.0.0 <3.0.0"), 713 | ("1.2.x || 2.x", ">=1.2.0 <1.3.0||>=2.0.0 <3.0.0"), 714 | ("x", "*"), 715 | ("2.*.*", ">=2.0.0 <3.0.0"), 716 | ("1.2.*", ">=1.2.0 <1.3.0"), 717 | ("1.2.* || 2.*", ">=1.2.0 <1.3.0||>=2.0.0 <3.0.0"), 718 | ("*", "*"), 719 | ("2", ">=2.0.0 <3.0.0"), 720 | ("2.3", ">=2.3.0 <2.4.0"), 721 | ("~2.4", ">=2.4.0 <2.5.0"), 722 | ("~2.4", ">=2.4.0 <2.5.0"), 723 | ("~>3.2.1", ">=3.2.1 <3.3.0"), 724 | ("~1", ">=1.0.0 <2.0.0"), 725 | ("~>1", ">=1.0.0 <2.0.0"), 726 | ("~> 1", ">=1.0.0 <2.0.0"), 727 | ("~>=1", ">=1.0.0 <2.0.0"), 728 | ("~>==1", ">=1.0.0 <2.0.0"), 729 | ("~1.0", ">=1.0.0 <1.1.0"), 730 | ("~ 1.0", ">=1.0.0 <1.1.0"), 731 | ("~=1.0", ">=1.0.0 <1.1.0"), 732 | ("~==1.0", ">=1.0.0 <1.1.0"), 733 | ("^0", "<1.0.0"), 734 | ("^ 1", ">=1.0.0 <2.0.0"), 735 | ("^0.1", ">=0.1.0 <0.2.0"), 736 | ("^1.0", ">=1.0.0 <2.0.0"), 737 | ("^1.2", ">=1.2.0 <2.0.0"), 738 | ("^0.0.1", ">=0.0.1 <0.0.2"), 739 | ("^0.0.1-beta", ">=0.0.1-beta <0.0.2"), 740 | ("^0.1.2", ">=0.1.2 <0.2.0"), 741 | ("^1.2.3", ">=1.2.3 <2.0.0"), 742 | ("^1.2.3-beta.4", ">=1.2.3-beta.4 <2.0.0"), 743 | ("^=1.0.0", ">=1.0.0 <2.0.0"), 744 | ("^==1.0.0", ">=1.0.0 <2.0.0"), 745 | ("<1", "<1.0.0"), 746 | ("< 1", "<1.0.0"), 747 | (">=1", ">=1.0.0"), 748 | (">= 1", ">=1.0.0"), 749 | ("<1.2", "<1.2.0"), 750 | ("< 1.2", "<1.2.0"), 751 | ("1", ">=1.0.0 <2.0.0"), 752 | ("^ 1.2 ^ 1", ">=1.2.0 <2.0.0 >=1.0.0"), 753 | ("1.2 - 3.4.5", ">=1.2.0 <=3.4.5"), 754 | ("1.2.3 - 3.4", ">=1.2.3 <3.5.0"), 755 | ("1.2 - 3.4", ">=1.2.0 <3.5.0"), 756 | (">1", ">=2.0.0"), 757 | (">1.2", ">=1.3.0"), 758 | (">X", "<0.0.0"), 759 | ("* 2.x", "<0.0.0"), 761 | (">x 2.x || * || 01.02.03", ">1.2.3"), 763 | ("~1.2.3beta", ">=1.2.3-beta <1.3.0"), 764 | (">=09090", ">=9090.0.0"), 765 | ]; 766 | for (range_text, expected) in fixtures { 767 | let range = parse_npm_version_req(range_text).unwrap(); 768 | let expected_range = parse_npm_version_req(expected).unwrap(); 769 | assert_eq!( 770 | range.inner(), 771 | expected_range.inner(), 772 | "failed for {} and {}", 773 | range_text, 774 | expected 775 | ); 776 | } 777 | } 778 | 779 | #[test] 780 | fn range_satisfies() { 781 | // https://github.com/npm/node-semver/blob/4907647d169948a53156502867ed679268063a9f/test/fixtures/range-include.js 782 | let fixtures = &[ 783 | ("1.0.0 - 2.0.0", "1.2.3"), 784 | ("^1.2.3+build", "1.2.3"), 785 | ("^1.2.3+build", "1.3.0"), 786 | ("1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3"), 787 | ("1.2.3pre+asdf - 2.4.3-pre+asdf", "1.2.3"), 788 | ("1.2.3-pre+asdf - 2.4.3pre+asdf", "1.2.3"), 789 | ("1.2.3pre+asdf - 2.4.3pre+asdf", "1.2.3"), 790 | ("1.2.3-pre+asdf - 2.4.3-pre+asdf", "1.2.3-pre.2"), 791 | ("1.2.3-pre+asdf - 2.4.3-pre+asdf", "2.4.3-alpha"), 792 | ("1.2.3+asdf - 2.4.3+asdf", "1.2.3"), 793 | ("1.0.0", "1.0.0"), 794 | (">=*", "0.2.4"), 795 | ("", "1.0.0"), 796 | ("*", "1.2.3"), 797 | ("*", "v1.2.3"), 798 | (">=1.0.0", "1.0.0"), 799 | (">=1.0.0", "1.0.1"), 800 | (">=1.0.0", "1.1.0"), 801 | (">1.0.0", "1.0.1"), 802 | (">1.0.0", "1.1.0"), 803 | ("<=2.0.0", "2.0.0"), 804 | ("<=2.0.0", "1.9999.9999"), 805 | ("<=2.0.0", "0.2.9"), 806 | ("<2.0.0", "1.9999.9999"), 807 | ("<2.0.0", "0.2.9"), 808 | (">= 1.0.0", "1.0.0"), 809 | (">= 1.0.0", "1.0.1"), 810 | (">= 1.0.0", "1.1.0"), 811 | ("> 1.0.0", "1.0.1"), 812 | ("> 1.0.0", "1.1.0"), 813 | ("<= 2.0.0", "2.0.0"), 814 | ("<= 2.0.0", "1.9999.9999"), 815 | ("<= 2.0.0", "0.2.9"), 816 | ("< 2.0.0", "1.9999.9999"), 817 | ("<\t2.0.0", "0.2.9"), 818 | (">=0.1.97", "v0.1.97"), 819 | (">=0.1.97", "0.1.97"), 820 | ("0.1.20 || 1.2.4", "1.2.4"), 821 | (">=0.2.3 || <0.0.1", "0.0.0"), 822 | (">=0.2.3 || <0.0.1", "0.2.3"), 823 | (">=0.2.3 || <0.0.1", "0.2.4"), 824 | ("||", "1.3.4"), 825 | ("2.x.x", "2.1.3"), 826 | ("1.2.x", "1.2.3"), 827 | ("1.2.x || 2.x", "2.1.3"), 828 | ("1.2.x || 2.x", "1.2.3"), 829 | ("x", "1.2.3"), 830 | ("2.*.*", "2.1.3"), 831 | ("1.2.*", "1.2.3"), 832 | ("1.2.* || 2.*", "2.1.3"), 833 | ("1.2.* || 2.*", "1.2.3"), 834 | ("*", "1.2.3"), 835 | ("2", "2.1.2"), 836 | ("2.3", "2.3.1"), 837 | ("~0.0.1", "0.0.1"), 838 | ("~0.0.1", "0.0.2"), 839 | ("~x", "0.0.9"), // >=2.4.0 <2.5.0 840 | ("~2", "2.0.9"), // >=2.4.0 <2.5.0 841 | ("~2.4", "2.4.0"), // >=2.4.0 <2.5.0 842 | ("~2.4", "2.4.5"), 843 | ("~>3.2.1", "3.2.2"), // >=3.2.1 <3.3.0, 844 | ("~1", "1.2.3"), // >=1.0.0 <2.0.0 845 | ("~>1", "1.2.3"), 846 | ("~> 1", "1.2.3"), 847 | ("~1.0", "1.0.2"), // >=1.0.0 <1.1.0, 848 | ("~ 1.0", "1.0.2"), 849 | ("~ 1.0.3", "1.0.12"), 850 | ("~ 1.0.3alpha", "1.0.12"), 851 | (">=1", "1.0.0"), 852 | (">= 1", "1.0.0"), 853 | ("<1.2", "1.1.1"), 854 | ("< 1.2", "1.1.1"), 855 | ("~v0.5.4-pre", "0.5.5"), 856 | ("~v0.5.4-pre", "0.5.4"), 857 | ("=0.7.x", "0.7.2"), 858 | ("<=0.7.x", "0.7.2"), 859 | (">=0.7.x", "0.7.2"), 860 | ("<=0.7.x", "0.6.2"), 861 | ("~1.2.1 >=1.2.3", "1.2.3"), 862 | ("~1.2.1 =1.2.3", "1.2.3"), 863 | ("~1.2.1 1.2.3", "1.2.3"), 864 | ("~1.2.1 >=1.2.3 1.2.3", "1.2.3"), 865 | ("~1.2.1 1.2.3 >=1.2.3", "1.2.3"), 866 | ("~1.2.1 1.2.3", "1.2.3"), 867 | (">=1.2.1 1.2.3", "1.2.3"), 868 | ("1.2.3 >=1.2.1", "1.2.3"), 869 | (">=1.2.3 >=1.2.1", "1.2.3"), 870 | (">=1.2.1 >=1.2.3", "1.2.3"), 871 | (">=1.2", "1.2.8"), 872 | ("^1.2.3", "1.8.1"), 873 | ("^0.1.2", "0.1.2"), 874 | ("^0.1", "0.1.2"), 875 | ("^0.0.1", "0.0.1"), 876 | ("^1.2", "1.4.2"), 877 | ("^1.2 ^1", "1.4.2"), 878 | ("^1.2.3-alpha", "1.2.3-pre"), 879 | ("^1.2.0-alpha", "1.2.0-pre"), 880 | ("^0.0.1-alpha", "0.0.1-beta"), 881 | ("^0.0.1-alpha", "0.0.1"), 882 | ("^0.1.1-alpha", "0.1.1-beta"), 883 | ("^x", "1.2.3"), 884 | ("x - 1.0.0", "0.9.7"), 885 | ("x - 1.x", "0.9.7"), 886 | ("1.0.0 - x", "1.9.7"), 887 | ("1.x - x", "1.9.7"), 888 | ("<=7.x", "7.9.9"), 889 | // additional tests 890 | ("1.0.0-alpha.13", "1.0.0-alpha.13"), 891 | (">1.0.0-beta.1 <=1.1.0", "1.0.0"), 892 | (">1.0.0-beta.1 <=1.1.0", "1.0.0-beta.2"), 893 | (">1.0.0-beta.1 <=1.1.0", "1.1.0"), 894 | ("1 - 2.0.0-beta.2", "2.0.0-beta.1"), 895 | ]; 896 | for (req_text, version_text) in fixtures { 897 | let req = parse_npm_version_req(req_text).unwrap(); 898 | let version = parse_npm_version(version_text).unwrap(); 899 | assert!( 900 | req.matches(&version), 901 | "Checking {version_text} satisfies {req_text}" 902 | ); 903 | } 904 | } 905 | 906 | #[test] 907 | fn range_not_satisfies() { 908 | let fixtures = &[ 909 | ("1.0.0 - 2.0.0", "2.2.3"), 910 | ("1.2.3+asdf - 2.4.3+asdf", "1.2.3-pre.2"), 911 | ("1.2.3+asdf - 2.4.3+asdf", "2.4.3-alpha"), 912 | ("^1.2.3+build", "2.0.0"), 913 | ("^1.2.3+build", "1.2.0"), 914 | ("^1.2.3", "1.2.3-pre"), 915 | ("^1.2", "1.2.0-pre"), 916 | (">1.2", "1.3.0-beta"), 917 | ("<=1.2.3", "1.2.3-beta"), 918 | ("^1.2.3", "1.2.3-beta"), 919 | ("=0.7.x", "0.7.0-asdf"), 920 | (">=0.7.x", "0.7.0-asdf"), 921 | ("<=0.7.x", "0.7.0-asdf"), 922 | ("1", "1.0.0beta"), 923 | ("<1", "1.0.0beta"), 924 | ("< 1", "1.0.0beta"), 925 | ("1.0.0", "1.0.1"), 926 | (">=1.0.0", "0.0.0"), 927 | (">=1.0.0", "0.0.1"), 928 | (">=1.0.0", "0.1.0"), 929 | (">1.0.0", "0.0.1"), 930 | (">1.0.0", "0.1.0"), 931 | ("<=2.0.0", "3.0.0"), 932 | ("<=2.0.0", "2.9999.9999"), 933 | ("<=2.0.0", "2.2.9"), 934 | ("<2.0.0", "2.9999.9999"), 935 | ("<2.0.0", "2.2.9"), 936 | (">=0.1.97", "v0.1.93"), 937 | (">=0.1.97", "0.1.93"), 938 | ("0.1.20 || 1.2.4", "1.2.3"), 939 | (">=0.2.3 || <0.0.1", "0.0.3"), 940 | (">=0.2.3 || <0.0.1", "0.2.2"), 941 | ("2.x.x", "1.1.3"), 942 | ("2.x.x", "3.1.3"), 943 | ("1.2.x", "1.3.3"), 944 | ("1.2.x || 2.x", "3.1.3"), 945 | ("1.2.x || 2.x", "1.1.3"), 946 | ("2.*.*", "1.1.3"), 947 | ("2.*.*", "3.1.3"), 948 | ("1.2.*", "1.3.3"), 949 | ("1.2.* || 2.*", "3.1.3"), 950 | ("1.2.* || 2.*", "1.1.3"), 951 | ("2", "1.1.2"), 952 | ("2.3", "2.4.1"), 953 | ("~0.0.1", "0.1.0-alpha"), 954 | ("~0.0.1", "0.1.0"), 955 | ("~2.4", "2.5.0"), // >=2.4.0 <2.5.0 956 | ("~2.4", "2.3.9"), 957 | ("~>3.2.1", "3.3.2"), // >=3.2.1 <3.3.0 958 | ("~>3.2.1", "3.2.0"), // >=3.2.1 <3.3.0 959 | ("~1", "0.2.3"), // >=1.0.0 <2.0.0 960 | ("~>1", "2.2.3"), 961 | ("~1.0", "1.1.0"), // >=1.0.0 <1.1.0 962 | ("<1", "1.0.0"), 963 | (">=1.2", "1.1.1"), 964 | ("1", "2.0.0beta"), 965 | ("~v0.5.4-beta", "0.5.4-alpha"), 966 | ("=0.7.x", "0.8.2"), 967 | (">=0.7.x", "0.6.2"), 968 | ("<0.7.x", "0.7.2"), 969 | ("<1.2.3", "1.2.3-beta"), 970 | ("=1.2.3", "1.2.3-beta"), 971 | (">1.2", "1.2.8"), 972 | ("^0.0.1", "0.0.2-alpha"), 973 | ("^0.0.1", "0.0.2"), 974 | ("^1.2.3", "2.0.0-alpha"), 975 | ("^1.2.3", "1.2.2"), 976 | ("^1.2", "1.1.9"), 977 | ("*", "v1.2.3-foo"), 978 | ("^1.0.0", "2.0.0-rc1"), 979 | ("1 - 2", "2.0.0-pre"), 980 | ("1 - 2", "1.0.0-pre"), 981 | ("1.0 - 2", "1.0.0-pre"), 982 | ("1.1.x", "1.0.0-a"), 983 | ("1.1.x", "1.1.0-a"), 984 | ("1.1.x", "1.2.0-a"), 985 | ("1.x", "1.0.0-a"), 986 | ("1.x", "1.1.0-a"), 987 | ("1.x", "1.2.0-a"), 988 | (">=1.0.0 <1.1.0", "1.1.0"), 989 | (">=1.0.0 <1.1.0", "1.1.0-pre"), 990 | (">=1.0.0 <1.1.0-pre", "1.1.0-pre"), 991 | // additional tests 992 | (">1.0.0-beta.1 <=1.1.0", "1.0.0-beta.1"), 993 | (">1.0.0-beta.1 <=1.1.0", "1.1.0-beta.1"), 994 | ]; 995 | 996 | for (req_text, version_text) in fixtures { 997 | let req = parse_npm_version_req(req_text).unwrap(); 998 | let version = parse_npm_version(version_text).unwrap(); 999 | assert!( 1000 | !req.matches(&version), 1001 | "Checking {req_text} not satisfies {version_text}" 1002 | ); 1003 | } 1004 | } 1005 | 1006 | #[test] 1007 | fn range_primitive_kind_beside_caret_or_tilde_with_whitespace() { 1008 | // node semver should have enforced strictness, but it didn't 1009 | // and so we end up with a system that acts this way 1010 | let fixtures = &[ 1011 | (">= ^1.2.3", "1.2.3", true), 1012 | (">= ^1.2.3", "1.2.4", true), 1013 | (">= ^1.2.3", "1.9.3", true), 1014 | (">= ^1.2.3", "2.0.0", false), 1015 | (">= ^1.2.3", "1.2.2", false), 1016 | // this is considered the same as the above by node semver 1017 | ("> ^1.2.3", "1.2.3", true), 1018 | ("> ^1.2.3", "1.2.4", true), 1019 | ("> ^1.2.3", "1.9.3", true), 1020 | ("> ^1.2.3", "2.0.0", false), 1021 | ("> ^1.2.3", "1.2.2", false), 1022 | // this is also considered the same 1023 | ("< ^1.2.3", "1.2.3", true), 1024 | ("< ^1.2.3", "1.2.4", true), 1025 | ("< ^1.2.3", "1.9.3", true), 1026 | ("< ^1.2.3", "2.0.0", false), 1027 | ("< ^1.2.3", "1.2.2", false), 1028 | // same with this 1029 | ("<= ^1.2.3", "1.2.3", true), 1030 | ("<= ^1.2.3", "1.2.4", true), 1031 | ("<= ^1.2.3", "1.9.3", true), 1032 | ("<= ^1.2.3", "2.0.0", false), 1033 | ("<= ^1.2.3", "1.2.2", false), 1034 | // now try a ~, which should work the same as above, but for ~ 1035 | ("<= ~1.2.3", "1.2.3", true), 1036 | ("<= ~1.2.3", "1.2.4", true), 1037 | ("<= ~1.2.3", "1.9.3", false), 1038 | ("<= ~1.2.3", "2.0.0", false), 1039 | ("<= ~1.2.3", "1.2.2", false), 1040 | ]; 1041 | 1042 | for (req_text, version_text, satisfies) in fixtures { 1043 | let req = parse_npm_version_req(req_text).unwrap(); 1044 | let version = parse_npm_version(version_text).unwrap(); 1045 | assert_eq!( 1046 | req.matches(&version), 1047 | *satisfies, 1048 | "Checking {} {} satisfies {}", 1049 | req_text, 1050 | if *satisfies { "true" } else { "false" }, 1051 | version_text 1052 | ); 1053 | } 1054 | } 1055 | 1056 | #[test] 1057 | fn range_primitive_kind_beside_caret_or_tilde_no_whitespace() { 1058 | let fixtures = &[ 1059 | ">=^1.2.3", ">^1.2.3", "<^1.2.3", "<=^1.2.3", ">=~1.2.3", ">~1.2.3", 1060 | "<~1.2.3", "<=~1.2.3", 1061 | ]; 1062 | 1063 | for req_text in fixtures { 1064 | // when it has no space, this is considered invalid 1065 | // by node semver so we should error 1066 | assert!(parse_npm_version_req(req_text).is_err()); 1067 | } 1068 | } 1069 | 1070 | #[test] 1071 | fn parse_npm_package_req_ref() { 1072 | assert_eq!( 1073 | NpmPackageReqReference::from_str("npm:@package/test").unwrap(), 1074 | NpmPackageReqReference::new(PackageReqReference { 1075 | req: PackageReq { 1076 | name: "@package/test".into(), 1077 | version_req: WILDCARD_VERSION_REQ.clone(), 1078 | }, 1079 | sub_path: None, 1080 | }), 1081 | ); 1082 | 1083 | assert_eq!( 1084 | NpmPackageReqReference::from_str("npm:@package/test@1").unwrap(), 1085 | NpmPackageReqReference::new(PackageReqReference { 1086 | req: PackageReq { 1087 | name: "@package/test".into(), 1088 | version_req: VersionReq::parse_from_specifier("1").unwrap(), 1089 | }, 1090 | sub_path: None, 1091 | }) 1092 | ); 1093 | 1094 | assert_eq!( 1095 | NpmPackageReqReference::from_str("npm:@package/test@~1.1/sub_path") 1096 | .unwrap(), 1097 | NpmPackageReqReference::new(PackageReqReference { 1098 | req: PackageReq { 1099 | name: "@package/test".into(), 1100 | version_req: VersionReq::parse_from_specifier("~1.1").unwrap(), 1101 | }, 1102 | sub_path: Some("sub_path".into()), 1103 | }) 1104 | ); 1105 | 1106 | assert_eq!( 1107 | NpmPackageReqReference::from_str("npm:@package/test/sub_path").unwrap(), 1108 | NpmPackageReqReference::new(PackageReqReference { 1109 | req: PackageReq { 1110 | name: "@package/test".into(), 1111 | version_req: WILDCARD_VERSION_REQ.clone(), 1112 | }, 1113 | sub_path: Some("sub_path".into()), 1114 | }) 1115 | ); 1116 | 1117 | assert_eq!( 1118 | NpmPackageReqReference::from_str("npm:test").unwrap(), 1119 | NpmPackageReqReference::new(PackageReqReference { 1120 | req: PackageReq { 1121 | name: "test".into(), 1122 | version_req: WILDCARD_VERSION_REQ.clone(), 1123 | }, 1124 | sub_path: None, 1125 | }) 1126 | ); 1127 | 1128 | assert_eq!( 1129 | NpmPackageReqReference::from_str("npm:test@^1.2").unwrap(), 1130 | NpmPackageReqReference::new(PackageReqReference { 1131 | req: PackageReq { 1132 | name: "test".into(), 1133 | version_req: VersionReq::parse_from_specifier("^1.2").unwrap(), 1134 | }, 1135 | sub_path: None, 1136 | }) 1137 | ); 1138 | 1139 | assert_eq!( 1140 | NpmPackageReqReference::from_str("npm:test@~1.1/sub_path").unwrap(), 1141 | NpmPackageReqReference::new(PackageReqReference { 1142 | req: PackageReq { 1143 | name: "test".into(), 1144 | version_req: VersionReq::parse_from_specifier("~1.1").unwrap(), 1145 | }, 1146 | sub_path: Some("sub_path".into()), 1147 | }) 1148 | ); 1149 | 1150 | assert_eq!( 1151 | NpmPackageReqReference::from_str("npm:@package/test/sub_path").unwrap(), 1152 | NpmPackageReqReference::new(PackageReqReference { 1153 | req: PackageReq { 1154 | name: "@package/test".into(), 1155 | version_req: WILDCARD_VERSION_REQ.clone(), 1156 | }, 1157 | sub_path: Some("sub_path".into()), 1158 | }) 1159 | ); 1160 | 1161 | let err = NpmPackageReqReference::from_str("npm:@package").unwrap_err(); 1162 | match err { 1163 | PackageReqReferenceParseError::Invalid(err) => assert!(matches!( 1164 | err.source, 1165 | crate::package::PackageReqPartsParseError::InvalidPackageName 1166 | )), 1167 | _ => unreachable!(), 1168 | } 1169 | 1170 | // missing version req 1171 | let err = NpmPackageReqReference::from_str("npm:package@").unwrap_err(); 1172 | match err { 1173 | PackageReqReferenceParseError::Invalid(err) => match err.source { 1174 | crate::package::PackageReqPartsParseError::SpecifierVersionReq(err) => { 1175 | assert_eq!(err.source.message, "Empty version constraint."); 1176 | } 1177 | _ => unreachable!(), 1178 | }, 1179 | _ => unreachable!(), 1180 | } 1181 | 1182 | // should parse leading slash 1183 | assert_eq!( 1184 | NpmPackageReqReference::from_str("npm:/@package/test/sub_path").unwrap(), 1185 | NpmPackageReqReference::new(PackageReqReference { 1186 | req: PackageReq { 1187 | name: "@package/test".into(), 1188 | version_req: WILDCARD_VERSION_REQ.clone(), 1189 | }, 1190 | sub_path: Some("sub_path".into()), 1191 | }) 1192 | ); 1193 | assert_eq!( 1194 | NpmPackageReqReference::from_str("npm:/test").unwrap(), 1195 | NpmPackageReqReference::new(PackageReqReference { 1196 | req: PackageReq { 1197 | name: "test".into(), 1198 | version_req: WILDCARD_VERSION_REQ.clone(), 1199 | }, 1200 | sub_path: None, 1201 | }) 1202 | ); 1203 | assert_eq!( 1204 | NpmPackageReqReference::from_str("npm:/test/").unwrap(), 1205 | NpmPackageReqReference::new(PackageReqReference { 1206 | req: PackageReq { 1207 | name: "test".into(), 1208 | version_req: WILDCARD_VERSION_REQ.clone(), 1209 | }, 1210 | sub_path: None, 1211 | }) 1212 | ); 1213 | 1214 | // should error for no name 1215 | match NpmPackageReqReference::from_str("npm:/").unwrap_err() { 1216 | PackageReqReferenceParseError::Invalid(err) => assert!(matches!( 1217 | err.source, 1218 | crate::package::PackageReqPartsParseError::NoPackageName 1219 | )), 1220 | _ => unreachable!(), 1221 | } 1222 | match NpmPackageReqReference::from_str("npm://test").unwrap_err() { 1223 | PackageReqReferenceParseError::Invalid(err) => assert!(matches!( 1224 | err.source, 1225 | crate::package::PackageReqPartsParseError::NoPackageName 1226 | )), 1227 | _ => unreachable!(), 1228 | } 1229 | 1230 | // path with `@` shouldn't error 1231 | assert_eq!( 1232 | NpmPackageReqReference::from_str("npm:package@^5.0.0-beta.35/@some/path") 1233 | .unwrap(), 1234 | NpmPackageReqReference::new(PackageReqReference { 1235 | req: PackageReq { 1236 | name: "package".into(), 1237 | version_req: VersionReq::parse_from_specifier("^5.0.0-beta.35") 1238 | .unwrap(), 1239 | }, 1240 | sub_path: Some("@some/path".into()), 1241 | }), 1242 | ); 1243 | } 1244 | 1245 | #[test] 1246 | fn package_nv_ref() { 1247 | let package_nv_ref = 1248 | NpmPackageNvReference::from_str("npm:package@1.2.3/test").unwrap(); 1249 | assert_eq!( 1250 | package_nv_ref, 1251 | NpmPackageNvReference(PackageNvReference { 1252 | nv: PackageNv { 1253 | name: "package".into(), 1254 | version: Version::parse_from_npm("1.2.3").unwrap(), 1255 | }, 1256 | sub_path: Some("test".into()) 1257 | }) 1258 | ); 1259 | assert_eq!( 1260 | package_nv_ref.as_specifier().as_str(), 1261 | "npm:/package@1.2.3/test" 1262 | ); 1263 | 1264 | // no path 1265 | let package_nv_ref = 1266 | NpmPackageNvReference::from_str("npm:package@1.2.3").unwrap(); 1267 | assert_eq!( 1268 | package_nv_ref, 1269 | NpmPackageNvReference(PackageNvReference { 1270 | nv: PackageNv { 1271 | name: "package".into(), 1272 | version: Version::parse_from_npm("1.2.3").unwrap(), 1273 | }, 1274 | sub_path: None 1275 | }) 1276 | ); 1277 | assert_eq!(package_nv_ref.as_specifier().as_str(), "npm:/package@1.2.3"); 1278 | } 1279 | 1280 | #[test] 1281 | fn zero_version_with_pre_release_matches() { 1282 | { 1283 | let req = parse_npm_version_req("<1.0.0-0").unwrap(); 1284 | assert!(req.matches(&Version::parse_from_npm("0.0.0").unwrap())); 1285 | assert!(!req.matches(&Version::parse_from_npm("0.0.0-pre").unwrap())); 1286 | } 1287 | { 1288 | let req = parse_npm_version_req("<1.0.0").unwrap(); 1289 | assert!(req.matches(&Version::parse_from_npm("0.0.0").unwrap())); 1290 | assert!(!req.matches(&Version::parse_from_npm("0.0.0-pre").unwrap())); 1291 | } 1292 | { 1293 | let req = parse_npm_version_req("^0").unwrap(); 1294 | assert!(req.matches(&Version::parse_from_npm("0.0.0").unwrap())); 1295 | assert!(!req.matches(&Version::parse_from_npm("0.0.0-pre").unwrap())); 1296 | } 1297 | { 1298 | let req = parse_npm_version_req("*").unwrap(); 1299 | assert!(req.matches(&Version::parse_from_npm("0.0.0").unwrap())); 1300 | assert!(!req.matches(&Version::parse_from_npm("0.0.0-pre").unwrap())); 1301 | } 1302 | } 1303 | 1304 | #[test] 1305 | fn test_is_valid_npm_tag() { 1306 | assert_eq!(is_valid_npm_tag("latest"), true); 1307 | assert_eq!(is_valid_npm_tag(""), false); 1308 | assert_eq!(is_valid_npm_tag("SD&*($#&%*(#*$%"), false); 1309 | } 1310 | } 1311 | --------------------------------------------------------------------------------