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}