Skip to main content

deps_core/
package.rs

1//! Newtypes distinguishing package names, version requirement strings, and
2//! concrete resolved versions.
3//!
4//! [`PackageName`], [`VersionReq`], and [`ConcreteVersion`] wrap `String` to
5//! give these kinds of manifest and registry data their own types, so a
6//! function that expects one cannot accidentally be called with another.
7//! None of these types validate, trim, or normalize their contents:
8//! ecosystem-specific normalization (case folding, separator rewriting,
9//! etc.) belongs to `EcosystemFormatter`, not to these types. See each
10//! type's documentation for details.
11
12use std::borrow::Borrow;
13use std::fmt;
14
15/// A package/crate name as it appears in a manifest file.
16///
17/// This is deliberately permissive: it stores whatever bytes the manifest
18/// contained, including the empty string, leading/trailing whitespace, or
19/// non-ASCII characters. No validation, trimming, or normalization is
20/// performed by this type.
21///
22/// For several ecosystems this is not a "package name" in the narrow sense
23/// but a registry lookup key: Maven and Gradle store `"group:artifact"`, Swift
24/// stores `"owner/repo"`, and Go stores a URL-like module path. All of these
25/// are valid `PackageName` values. Ecosystem-specific normalization (case
26/// folding, separator rewriting, etc.) is the responsibility of
27/// `EcosystemFormatter`, not this type — do not add validation rules here,
28/// as it would silently break those ecosystems.
29///
30/// This type has no [`Display`](fmt::Display) impl, and its [`Debug`](fmt::Debug) impl
31/// redacts via [`crate::redact::redact_declaration_key`] rather than deriving (#1217) —
32/// see [`Self::for_tracing`] for why, and use [`Self::as_str`]/[`Self::as_ref`]/
33/// [`Self::into_string`] for the raw value.
34///
35/// # Examples
36///
37/// ```
38/// use deps_core::PackageName;
39///
40/// let name = PackageName::new("serde");
41/// assert_eq!(name.as_str(), "serde");
42/// assert_eq!(name, "serde");
43/// ```
44#[derive(Clone, PartialEq, Eq, Hash)]
45pub struct PackageName(String);
46
47impl PackageName {
48    /// Wraps `value` as a `PackageName`, unchanged.
49    ///
50    /// This never fails and never modifies its input: an empty string is a
51    /// valid `PackageName`, as is a string with surrounding whitespace.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use deps_core::PackageName;
57    ///
58    /// let name = PackageName::new(String::from("tokio"));
59    /// assert_eq!(name.as_str(), "tokio");
60    /// ```
61    pub fn new(value: impl Into<String>) -> Self {
62        Self(value.into())
63    }
64
65    /// Returns the package name as a string slice.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use deps_core::PackageName;
71    ///
72    /// let name = PackageName::new("axum");
73    /// assert_eq!(name.as_str(), "axum");
74    /// ```
75    #[must_use]
76    pub fn as_str(&self) -> &str {
77        &self.0
78    }
79
80    /// Consumes the `PackageName`, returning the wrapped `String`.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use deps_core::PackageName;
86    ///
87    /// let name = PackageName::new("axum");
88    /// let owned: String = name.into_string();
89    /// assert_eq!(owned, "axum");
90    /// ```
91    #[must_use]
92    pub fn into_string(self) -> String {
93        self.0
94    }
95
96    /// Renders this name for `tracing`/log output, redacting it via
97    /// [`crate::redact::redact_declaration_key`] first (#1209).
98    ///
99    /// A manifest can hold a credential in a name-shaped field (e.g. a Maven
100    /// `group:artifact:secret@host` coordinate produced by property
101    /// interpolation), and this type's raw accessors ([`Self::as_str`], [`Self::as_ref`],
102    /// [`Self::into_string`]) return the string verbatim — so any `tracing` field or span
103    /// built from a `PackageName` must go through this method instead. (`?name` and
104    /// `Debug`-derived output are already safe — see [`Self`]'s `Debug` impl — but `%name`,
105    /// which requires `Display`, does not compile at all, since `PackageName` has none.) An
106    /// ordinary package name (including scoped ones like `@types/node` or
107    /// `com.google.guava:guava`) passes through unchanged.
108    ///
109    /// The result is also swept for control/format characters (`\n`, `\r`, a raw ESC byte, and
110    /// the wider Unicode `Cc`/`Cf`/`Zl`/`Zp` set), via
111    /// [`redact_declaration_key`](crate::redact::redact_declaration_key)'s own final pass — a
112    /// manifest-controlled name has no credential shape to trigger the redaction above, but could
113    /// otherwise forge a whole `tracing` log line or terminal escape sequence verbatim (#1469).
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use deps_core::PackageName;
119    ///
120    /// let name = PackageName::new("com.google.guava:guava");
121    /// assert_eq!(name.for_tracing(), "com.google.guava:guava");
122    ///
123    /// let name = PackageName::new("com.google.guava:deploy:TOKEN@git.internal.corp");
124    /// assert_eq!(name.for_tracing(), "***@git.internal.corp");
125    ///
126    /// let name = PackageName::new("lodash\nERROR deps_lsp: FORGED\x1b[31mRED");
127    /// assert_eq!(name.for_tracing(), "lodash ERROR deps_lsp: FORGED [31mRED");
128    /// ```
129    #[must_use]
130    pub fn for_tracing(&self) -> String {
131        crate::redact::redact_declaration_key(&self.0).into_owned()
132    }
133}
134
135impl fmt::Debug for PackageName {
136    /// Forwards to the redacted text's own `Debug` (a quoted string), not a struct-wrapper
137    /// rendering — mirrors [`crate::redact::RedactedName`]'s `Debug` impl. This makes
138    /// `?name`, and any `#[derive(Debug)]` struct/enum that embeds a `PackageName`, safe by
139    /// construction against the credential-in-a-name-shaped-field case [`Self::for_tracing`]
140    /// documents (#1217).
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        fmt::Debug::fmt(&crate::redact::redact_declaration_key(&self.0), f)
143    }
144}
145
146impl From<String> for PackageName {
147    fn from(value: String) -> Self {
148        Self(value)
149    }
150}
151
152impl From<&str> for PackageName {
153    fn from(value: &str) -> Self {
154        Self(value.to_string())
155    }
156}
157
158impl AsRef<str> for PackageName {
159    fn as_ref(&self) -> &str {
160        &self.0
161    }
162}
163
164/// Enables `&str` lookups into `HashMap<PackageName, _>`/`HashSet<PackageName>`.
165///
166/// Sound because derived `Hash`/`Eq` on `PackageName(String)` delegate to
167/// `String`'s implementations, which in turn are defined to match `str`'s
168/// exactly (`String: Borrow<str>` in `std` rests on the same guarantee) — so
169/// `PackageName` and the `str` it borrows always hash and compare equal.
170impl Borrow<str> for PackageName {
171    fn borrow(&self) -> &str {
172        &self.0
173    }
174}
175
176impl PartialEq<str> for PackageName {
177    fn eq(&self, other: &str) -> bool {
178        self.0 == other
179    }
180}
181
182impl PartialEq<&str> for PackageName {
183    fn eq(&self, other: &&str) -> bool {
184        self.0 == *other
185    }
186}
187
188/// A package name that failed a [`PackageNaming::validate_package_name`] lint.
189///
190/// This is not a construction-time gate — [`PackageName::new`] stays infallible — it
191/// only carries *why* a name looks wrong so an LSP diagnostic can say something more
192/// specific than "invalid name".
193///
194/// Also the payload of [`DepsError::InvalidPackageName`] (#1514): `deps-go`'s
195/// `validate_module_path` returns this type directly, propagated into `DepsError` via
196/// `#[from]`, so a malformed Go module path shares this same "why was the name rejected"
197/// carrier instead of a second, ecosystem-local error type.
198///
199/// [`PackageNaming::validate_package_name`]: crate::lsp_helpers::PackageNaming::validate_package_name
200/// [`DepsError::InvalidPackageName`]: crate::DepsError::InvalidPackageName
201///
202/// # Examples
203///
204/// ```
205/// use deps_core::InvalidPackageName;
206///
207/// let err = InvalidPackageName::new("name is longer than 214 characters");
208/// assert_eq!(err.reason(), "name is longer than 214 characters");
209/// assert_eq!(err.to_string(), "name is longer than 214 characters");
210/// ```
211#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
212#[error("{0}")]
213pub struct InvalidPackageName(std::borrow::Cow<'static, str>);
214
215impl InvalidPackageName {
216    /// Creates an `InvalidPackageName` carrying `reason` as the explanation.
217    pub fn new(reason: impl Into<std::borrow::Cow<'static, str>>) -> Self {
218        Self(reason.into())
219    }
220
221    /// Returns why the name was rejected.
222    #[must_use]
223    pub fn reason(&self) -> &str {
224        &self.0
225    }
226}
227
228/// A version requirement string as it appears in a manifest file.
229///
230/// This is deliberately permissive: it stores whatever bytes the manifest
231/// contained (e.g. `"^1.0"`, `">=2.0,<3.0"`, `"*"`), including the empty
232/// string. No parsing, validation, trimming, or normalization is performed
233/// by this type — ecosystems that need to parse a requirement (via `semver`,
234/// `node-semver`, `pep440_rs`, etc.) do so from [`VersionReq::as_str`], not
235/// from this type. Note that `deps-go`'s `GoDependency.version` also uses this
236/// type even though it holds an exact pinned version (e.g. `"v1.9.1"`), not a
237/// range or constraint — Go modules don't have a separate "requirement"
238/// concept, so the exact version doubles as the requirement.
239///
240/// # Examples
241///
242/// ```
243/// use deps_core::VersionReq;
244///
245/// let req = VersionReq::new("^1.0");
246/// assert_eq!(req.as_str(), "^1.0");
247/// assert_eq!(req, "^1.0");
248/// ```
249#[derive(Debug, Clone, PartialEq, Eq, Hash)]
250pub struct VersionReq(String);
251
252impl VersionReq {
253    /// Wraps `value` as a `VersionReq`, unchanged.
254    ///
255    /// This never fails and never modifies its input: an empty string is a
256    /// valid `VersionReq`, as is a string with surrounding whitespace.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use deps_core::VersionReq;
262    ///
263    /// let req = VersionReq::new(String::from(">=1.0"));
264    /// assert_eq!(req.as_str(), ">=1.0");
265    /// ```
266    pub fn new(value: impl Into<String>) -> Self {
267        Self(value.into())
268    }
269
270    /// Returns the version requirement as a string slice.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use deps_core::VersionReq;
276    ///
277    /// let req = VersionReq::new("~1.2");
278    /// assert_eq!(req.as_str(), "~1.2");
279    /// ```
280    #[must_use]
281    pub fn as_str(&self) -> &str {
282        &self.0
283    }
284
285    /// Consumes the `VersionReq`, returning the wrapped `String`.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use deps_core::VersionReq;
291    ///
292    /// let req = VersionReq::new("~1.2");
293    /// let owned: String = req.into_string();
294    /// assert_eq!(owned, "~1.2");
295    /// ```
296    #[must_use]
297    pub fn into_string(self) -> String {
298        self.0
299    }
300}
301
302impl fmt::Display for VersionReq {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        f.write_str(&self.0)
305    }
306}
307
308impl From<String> for VersionReq {
309    fn from(value: String) -> Self {
310        Self(value)
311    }
312}
313
314impl From<&str> for VersionReq {
315    fn from(value: &str) -> Self {
316        Self(value.to_string())
317    }
318}
319
320impl AsRef<str> for VersionReq {
321    fn as_ref(&self) -> &str {
322        &self.0
323    }
324}
325
326impl PartialEq<str> for VersionReq {
327    fn eq(&self, other: &str) -> bool {
328        self.0 == other
329    }
330}
331
332impl PartialEq<&str> for VersionReq {
333    fn eq(&self, other: &&str) -> bool {
334        self.0 == *other
335    }
336}
337
338/// A concrete, resolved version string as returned by a registry.
339///
340/// This is deliberately permissive: it stores whatever bytes the registry
341/// returned (e.g. `"1.2.3"`, `"2.0.0-beta.1"`), including the empty string.
342/// No parsing, validation, trimming, or normalization is performed by this
343/// type — ecosystems that need to parse a version (via `semver`,
344/// `node-semver`, `pep440_rs`, etc.) do so from [`ConcreteVersion::as_str`],
345/// not from this type. It is distinct from [`VersionReq`]: a `VersionReq` is
346/// a constraint written in a manifest (`"^1.0"`), while a `ConcreteVersion`
347/// is a single resolved version (`"1.0.4"`).
348///
349/// # Examples
350///
351/// ```
352/// use deps_core::ConcreteVersion;
353///
354/// let version = ConcreteVersion::new("1.2.3");
355/// assert_eq!(version.as_str(), "1.2.3");
356/// assert_eq!(version, "1.2.3");
357/// ```
358#[derive(Debug, Clone, PartialEq, Eq, Hash)]
359pub struct ConcreteVersion(String);
360
361impl ConcreteVersion {
362    /// Wraps `value` as a `ConcreteVersion`, unchanged.
363    ///
364    /// This never fails and never modifies its input: an empty string is a
365    /// valid `ConcreteVersion`, as is a string with surrounding whitespace.
366    ///
367    /// # Examples
368    ///
369    /// ```
370    /// use deps_core::ConcreteVersion;
371    ///
372    /// let version = ConcreteVersion::new(String::from("1.0.0"));
373    /// assert_eq!(version.as_str(), "1.0.0");
374    /// ```
375    pub fn new(value: impl Into<String>) -> Self {
376        Self(value.into())
377    }
378
379    /// Returns the concrete version as a string slice.
380    ///
381    /// # Examples
382    ///
383    /// ```
384    /// use deps_core::ConcreteVersion;
385    ///
386    /// let version = ConcreteVersion::new("4.5.6");
387    /// assert_eq!(version.as_str(), "4.5.6");
388    /// ```
389    #[must_use]
390    pub fn as_str(&self) -> &str {
391        &self.0
392    }
393
394    /// Consumes the `ConcreteVersion`, returning the wrapped `String`.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use deps_core::ConcreteVersion;
400    ///
401    /// let version = ConcreteVersion::new("4.5.6");
402    /// let owned: String = version.into_string();
403    /// assert_eq!(owned, "4.5.6");
404    /// ```
405    #[must_use]
406    pub fn into_string(self) -> String {
407        self.0
408    }
409
410    /// Whether `self` and `other` are the same version once semver build metadata
411    /// (`+...`, which carries no precedence) is ignored.
412    ///
413    /// Follows SemVer 2.0.0 precedence only; ecosystems where a `+N` suffix denotes a
414    /// distinct release (e.g. pub.dev build numbers) are out of scope.
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// use deps_core::ConcreteVersion;
420    ///
421    /// let resolved = ConcreteVersion::new("1.2.3");
422    /// assert!(resolved.eq_ignoring_build_metadata(&ConcreteVersion::new("1.2.3+build.7")));
423    /// assert!(!resolved.eq_ignoring_build_metadata(&ConcreteVersion::new("1.2.4+build.7")));
424    /// ```
425    #[must_use]
426    pub fn eq_ignoring_build_metadata(&self, other: &Self) -> bool {
427        strip_build_metadata(&self.0) == strip_build_metadata(&other.0)
428    }
429}
430
431impl fmt::Display for ConcreteVersion {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        f.write_str(&self.0)
434    }
435}
436
437impl From<String> for ConcreteVersion {
438    fn from(value: String) -> Self {
439        Self(value)
440    }
441}
442
443impl From<&str> for ConcreteVersion {
444    fn from(value: &str) -> Self {
445        Self(value.to_string())
446    }
447}
448
449impl AsRef<str> for ConcreteVersion {
450    fn as_ref(&self) -> &str {
451        &self.0
452    }
453}
454
455impl PartialEq<str> for ConcreteVersion {
456    fn eq(&self, other: &str) -> bool {
457        self.0 == other
458    }
459}
460
461impl PartialEq<&str> for ConcreteVersion {
462    fn eq(&self, other: &&str) -> bool {
463        self.0 == *other
464    }
465}
466
467/// `version` without its semver build metadata (`+...`), which carries no precedence.
468pub(crate) fn strip_build_metadata(version: &str) -> &str {
469    version
470        .split_once('+')
471        .map_or(version, |(precedence, _)| precedence)
472}
473
474/// Length, in bytes, of an `@scope/pkg` or unscoped `pkg` name at the start of `rest`.
475///
476/// Follows the scoped-package grammar shared by npm-style ecosystems: the name runs up to
477/// (but excludes) the first `@` or `/` after it, without mistaking a scoped name's own
478/// leading `@` or inner `/` for that boundary.
479///
480/// Shared by `deps-npm`'s `npm:` alias parsing (issue #654) and `deps-deno`'s
481/// `jsr:`/`npm:` specifier grammar (`deps_deno::specifier::parse_specifier`) — both need
482/// the identical scope-aware boundary, and a per-crate reimplementation had drifted looser
483/// than this one (accepting `"@/pkg"`, `"@scope/"`, and a lone `"@"` as valid names).
484///
485/// Returns `None` for a malformed scoped name (empty scope `"@/pkg"`, missing `/` `"@std"`,
486/// or an empty package segment `"@std/"`) or an empty unscoped name (`rest` is empty, or
487/// starts with `@`/`/`).
488///
489/// # Examples
490///
491/// ```
492/// use deps_core::package::npm_style_name_boundary;
493///
494/// assert_eq!(npm_style_name_boundary("react@^18.0.0"), Some(5));
495/// assert_eq!(npm_style_name_boundary("@scope/pkg@^1.0.0"), Some(10));
496/// assert_eq!(npm_style_name_boundary("@scope/pkg"), Some(10));
497/// assert_eq!(npm_style_name_boundary("@/pkg"), None);
498/// assert_eq!(npm_style_name_boundary("@scope/"), None);
499/// assert_eq!(npm_style_name_boundary("@"), None);
500/// assert_eq!(npm_style_name_boundary(""), None);
501/// ```
502#[expect(
503    clippy::string_slice,
504    reason = "slash_rel comes from str::find('/'), an ASCII byte, so slash_rel + 1 is always \
505              a char boundary"
506)]
507#[must_use]
508pub fn npm_style_name_boundary(rest: &str) -> Option<usize> {
509    if let Some(after_at) = rest.strip_prefix('@') {
510        let slash_rel = after_at.find('/')?;
511        if slash_rel == 0 {
512            return None; // empty scope, e.g. "@/pkg"
513        }
514        let after_slash = &after_at[slash_rel + 1..];
515        let pkg_end_rel = after_slash.find(['@', '/']).unwrap_or(after_slash.len());
516        if pkg_end_rel == 0 {
517            return None; // empty pkg name, e.g. "@scope/"
518        }
519        Some(1 + slash_rel + 1 + pkg_end_rel)
520    } else {
521        let end = rest.find(['@', '/']).unwrap_or(rest.len());
522        (end > 0).then_some(end)
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::{ConcreteVersion, PackageName, VersionReq};
529
530    #[test]
531    fn eq_ignoring_build_metadata_cases() {
532        for (a, b, expected) in [
533            ("1.2.3", "1.2.3", true),
534            ("1.2.3+a", "1.2.3+b", true),
535            ("1.2.3", "1.2.3+b", true),
536            ("1.2.3+a", "1.2.3", true),
537            ("1.2.3", "1.2.4+b", false),
538            ("1.2.3-beta.1", "1.2.3+b", false),
539            ("1.2.3-beta.1+a", "1.2.3-beta.1+b", true),
540            ("1.2.3+", "1.2.3", true),
541            ("1.2.3+a+b", "1.2.3+c", true),
542            ("1.2.3+a+b", "1.2.4+a+b", false),
543        ] {
544            assert_eq!(
545                ConcreteVersion::new(a).eq_ignoring_build_metadata(&ConcreteVersion::new(b)),
546                expected,
547                "{a} vs {b}"
548            );
549        }
550    }
551
552    #[test]
553    fn package_name_round_trips_empty_string() {
554        let name = PackageName::new("");
555        assert_eq!(name.as_str(), "");
556        assert_eq!(name, "");
557    }
558
559    #[test]
560    fn package_name_round_trips_surrounding_whitespace() {
561        let name = PackageName::new("  serde  ");
562        assert_eq!(name.as_str(), "  serde  ");
563        assert_eq!(name, "  serde  ");
564    }
565
566    #[test]
567    fn package_name_round_trips_non_ascii() {
568        let name = PackageName::new("パッケージ");
569        assert_eq!(name.as_str(), "パッケージ");
570        assert_eq!(name, "パッケージ");
571    }
572
573    #[test]
574    fn package_name_compares_byte_wise_like_string() {
575        assert_ne!(PackageName::new("Foo"), PackageName::new("foo"));
576        assert_eq!(
577            PackageName::new("Foo") == PackageName::new("foo"),
578            "Foo" == "foo"
579        );
580    }
581
582    #[test]
583    fn package_name_into_string_roundtrip() {
584        let original = String::from("tokio");
585        let name = PackageName::new(original.clone());
586        assert_eq!(name.into_string(), original);
587    }
588
589    #[test]
590    fn package_name_debug_redacts_credential_shape() {
591        let name = PackageName::new("com.google.guava:deploy:TOKEN@git.internal.corp");
592        assert_eq!(format!("{name:?}"), "\"***@git.internal.corp\"");
593    }
594
595    #[test]
596    fn package_name_debug_leaves_ordinary_name_untouched() {
597        let name = PackageName::new("com.google.guava:guava");
598        assert_eq!(format!("{name:?}"), "\"com.google.guava:guava\"");
599    }
600
601    /// #1469 (CWE-117/CWE-150): a manifest-controlled name with no credential shape at all still
602    /// carries `\n`/`\r`/ESC through untouched, letting it forge a fake `tracing` log line or
603    /// terminal escape sequence in `deps-lsp`'s stderr output. Adapted from the issue's PoC.
604    #[test]
605    fn for_tracing_neutralizes_control_characters() {
606        for forged in [
607            "lodash\nERROR deps_lsp: FORGED\x1b[31mRED",
608            "ok\r\nWARN forged2",
609        ] {
610            let rendered = PackageName::new(forged).for_tracing();
611            assert!(
612                !rendered.contains('\n'),
613                "{rendered:?} must not contain \\n"
614            );
615            assert!(
616                !rendered.contains('\r'),
617                "{rendered:?} must not contain \\r"
618            );
619            assert!(
620                !rendered.contains('\x1b'),
621                "{rendered:?} must not contain ESC"
622            );
623        }
624    }
625
626    /// #1469: the same sweep must also apply once the credential-shape gate fires, so a
627    /// control character hiding past a redacted credential can't survive either.
628    #[test]
629    fn for_tracing_neutralizes_control_characters_alongside_credential_redaction() {
630        let rendered =
631            PackageName::new("com.google.guava:deploy:TOKEN@git.internal\ncorp").for_tracing();
632        assert!(!rendered.contains('\n'));
633        assert!(!rendered.contains("TOKEN"));
634    }
635
636    #[test]
637    fn package_name_hashmap_reachable_by_str_and_by_package_name() {
638        use std::collections::HashMap;
639
640        let mut map: HashMap<PackageName, u32> = HashMap::new();
641        map.insert(PackageName::new("serde"), 1);
642
643        assert_eq!(map.get("serde"), Some(&1));
644        assert_eq!(map.get(&PackageName::new("serde")), Some(&1));
645    }
646
647    #[test]
648    fn version_req_round_trips_empty_string() {
649        let req = VersionReq::new("");
650        assert_eq!(req.as_str(), "");
651        assert_eq!(req, "");
652    }
653
654    #[test]
655    fn version_req_round_trips_surrounding_whitespace() {
656        let req = VersionReq::new("  ^1.0  ");
657        assert_eq!(req.as_str(), "  ^1.0  ");
658        assert_eq!(req, "  ^1.0  ");
659    }
660
661    #[test]
662    fn version_req_round_trips_non_ascii() {
663        let req = VersionReq::new("非対応");
664        assert_eq!(req.as_str(), "非対応");
665        assert_eq!(req, "非対応");
666    }
667
668    #[test]
669    fn version_req_compares_byte_wise_like_string() {
670        assert_ne!(VersionReq::new("^1.0"), VersionReq::new("^1.0 "));
671    }
672
673    #[test]
674    fn version_req_into_string_roundtrip() {
675        let original = String::from("~2.3.4");
676        let req = VersionReq::new(original.clone());
677        assert_eq!(req.into_string(), original);
678    }
679
680    #[test]
681    fn concrete_version_round_trips_empty_string() {
682        let version = ConcreteVersion::new("");
683        assert_eq!(version.as_str(), "");
684        assert_eq!(version, "");
685    }
686
687    #[test]
688    fn concrete_version_round_trips_surrounding_whitespace() {
689        let version = ConcreteVersion::new("  1.0.0  ");
690        assert_eq!(version.as_str(), "  1.0.0  ");
691        assert_eq!(version, "  1.0.0  ");
692    }
693
694    #[test]
695    fn concrete_version_round_trips_non_ascii() {
696        let version = ConcreteVersion::new("バージョン");
697        assert_eq!(version.as_str(), "バージョン");
698        assert_eq!(version, "バージョン");
699    }
700
701    #[test]
702    fn concrete_version_compares_byte_wise_like_string() {
703        assert_ne!(
704            ConcreteVersion::new("1.0.0"),
705            ConcreteVersion::new("1.0.0 ")
706        );
707    }
708
709    #[test]
710    fn concrete_version_into_string_roundtrip() {
711        let original = String::from("2.3.4");
712        let version = ConcreteVersion::new(original.clone());
713        assert_eq!(version.into_string(), original);
714    }
715}