1use std::{cmp::Ordering, fmt};
2
3use lazy_static::lazy_static;
4use regex::Regex;
5
6#[cfg(feature = "serde")]
7use serde::{
8 ser::{SerializeStruct, Serializer},
9 Serialize,
10};
11
12#[cfg(feature = "semver-1")]
13use semver_1::{BuildMetadata, Prerelease};
14
15lazy_static! {
16 static ref RELEASE_REGEX: Regex = Regex::new(r#"^(@?[^@]+)@(.+?)$"#).unwrap();
17 static ref VERSION_REGEX: Regex = Regex::new(
18 r"(?x)
19 ^
20 (?P<major>[0-9][0-9]*)
21 (?:\.(?P<minor>[0-9][0-9]*))?
22 (?:\.(?P<patch>[0-9][0-9]*))?
23 (?:\.(?P<revision>[0-9][0-9]*))?
24 (?:
25 (?P<prerelease>
26 (?:-|[a-z])
27 (?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)?
28 (?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*)
29 )?
30 (?:\+(?P<build_code>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
31 $
32 "
33 )
34 .unwrap();
35 static ref HEX_REGEX: Regex = Regex::new(r#"^[a-fA-F0-9]+$"#).unwrap();
36 static ref VALID_API_ATTRIBUTE_REGEX: Regex = Regex::new(r"^[^\\/\r\n\t\x7f\x00-\x1f]*\z").unwrap();
39}
40
41#[derive(Debug, Clone, PartialEq)]
43#[cfg_attr(feature = "serde", derive(Serialize))]
44pub struct InvalidVersion;
45
46impl std::error::Error for InvalidVersion {}
47
48impl fmt::Display for InvalidVersion {
49 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50 write!(f, "invalid version")
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub enum InvalidRelease {
57 TooLong,
59 RestrictedName,
61 BadCharacters,
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub enum InvalidEnvironment {
68 TooLong,
70 RestrictedName,
72 BadCharacters,
74}
75
76impl std::error::Error for InvalidRelease {}
77
78impl fmt::Display for InvalidRelease {
79 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80 write!(
81 f,
82 "invalid release: {}",
83 match *self {
84 InvalidRelease::BadCharacters => "bad characters in release name",
85 InvalidRelease::RestrictedName => "restricted release name",
86 InvalidRelease::TooLong => "release name too long",
87 }
88 )
89 }
90}
91
92impl std::error::Error for InvalidEnvironment {}
93
94impl fmt::Display for InvalidEnvironment {
95 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
96 write!(
97 f,
98 "invalid environment: {}",
99 match *self {
100 InvalidEnvironment::BadCharacters => "bad characters in environment name",
101 InvalidEnvironment::RestrictedName => "restricted environment name",
102 InvalidEnvironment::TooLong => "environment name too long",
103 }
104 )
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct Version<'a> {
111 raw: &'a str,
112 major: &'a str,
113 minor: &'a str,
114 patch: &'a str,
115 revision: &'a str,
116 pre: &'a str,
117 before_code: &'a str,
118 build_code: &'a str,
119 components: u8,
120}
121
122#[cfg(feature = "serde")]
123impl<'a> Serialize for Version<'a> {
124 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
125 where
126 S: Serializer,
127 {
128 let mut state = serializer.serialize_struct("Version", 5)?;
129 state.serialize_field("major", &self.major())?;
130 state.serialize_field("minor", &self.minor())?;
131 state.serialize_field("patch", &self.patch())?;
132 state.serialize_field("revision", &self.revision())?;
133 state.serialize_field("pre", &self.pre())?;
134 state.serialize_field("build_code", &self.build_code())?;
135 state.serialize_field("raw_short", &self.raw_short())?;
136 state.serialize_field("components", &self.components())?;
137 state.serialize_field("raw_quad", &self.raw_quad())?;
138 state.end()
139 }
140}
141
142fn is_build_hash(s: &str) -> bool {
143 match s.len() {
144 12 | 16 | 20 | 32 | 40 | 64 => HEX_REGEX.is_match(s),
145 _ => false,
146 }
147}
148
149impl<'a> Version<'a> {
150 pub fn parse(version: &'a str) -> Result<Version<'a>, InvalidVersion> {
152 let caps = if let Some(caps) = VERSION_REGEX.captures(version) {
153 caps
154 } else {
155 return Err(InvalidVersion);
156 };
157
158 let components = 1
159 + caps.get(2).map_or(0, |_| 1)
160 + caps.get(3).map_or(0, |_| 1)
161 + caps.get(4).map_or(0, |_| 1);
162
163 if components == 1 && caps.get(5).is_some_and(|x| !x.as_str().starts_with('-')) {
167 return Err(InvalidVersion);
168 }
169
170 let before_code = match caps.get(6) {
171 Some(cap) => &version[..cap.start() - 1],
172 None => version,
173 };
174
175 Ok(Version {
176 raw: version,
177 major: caps.get(1).map(|x| x.as_str()).unwrap_or_default(),
178 minor: caps.get(2).map(|x| x.as_str()).unwrap_or_default(),
179 patch: caps.get(3).map(|x| x.as_str()).unwrap_or_default(),
180 revision: caps.get(4).map(|x| x.as_str()).unwrap_or_default(),
181 pre: caps
182 .get(5)
183 .map(|x| {
184 let mut pre = x.as_str();
185 if pre.starts_with('-') {
186 pre = &pre[1..];
187 }
188 pre
189 })
190 .unwrap_or(""),
191 before_code,
192 build_code: caps.get(6).map(|x| x.as_str()).unwrap_or(""),
193 components,
194 })
195 }
196
197 #[cfg(feature = "semver")]
201 pub fn as_semver(&self) -> semver::Version {
202 fn split(s: &str) -> Vec<semver::Identifier> {
203 s.split('.')
204 .map(|item| {
205 if let Ok(val) = item.parse::<u64>() {
206 semver::Identifier::Numeric(val)
207 } else {
208 semver::Identifier::AlphaNumeric(item.into())
209 }
210 })
211 .collect()
212 }
213
214 semver::Version {
215 major: self.major(),
216 minor: self.minor(),
217 patch: self.patch(),
218 pre: split(self.pre),
219 build: split(self.build_code),
220 }
221 }
222
223 #[cfg(feature = "semver-1")]
227 pub fn as_semver1(&self) -> semver_1::Version {
228 let pre = if self.pre.is_empty() {
229 Prerelease::EMPTY
230 } else {
231 Prerelease::new(self.pre).unwrap_or(Prerelease::EMPTY)
232 };
233
234 let build = if self.build_code.is_empty() {
235 BuildMetadata::EMPTY
236 } else {
237 BuildMetadata::new(self.build_code).unwrap_or(BuildMetadata::EMPTY)
238 };
239
240 semver_1::Version {
241 major: self.major(),
242 minor: self.minor(),
243 patch: self.patch(),
244 pre,
245 build,
246 }
247 }
248
249 pub fn major(&self) -> u64 {
251 self.major.parse().unwrap_or_default()
252 }
253
254 pub fn minor(&self) -> u64 {
256 self.minor.parse().unwrap_or_default()
257 }
258
259 pub fn patch(&self) -> u64 {
261 self.patch.parse().unwrap_or_default()
262 }
263
264 pub fn revision(&self) -> u64 {
266 self.revision.parse().unwrap_or_default()
267 }
268
269 pub fn pre(&self) -> Option<&'a str> {
271 if self.pre.is_empty() {
272 None
273 } else {
274 Some(self.pre)
275 }
276 }
277
278 pub fn build_code(&self) -> Option<&'a str> {
280 if self.build_code.is_empty() {
281 None
282 } else {
283 Some(self.build_code)
284 }
285 }
286
287 pub fn build_number(&self) -> Option<u64> {
289 self.build_code().and_then(|val| val.parse().ok())
290 }
291
292 pub fn components(&self) -> u8 {
294 self.components
295 }
296
297 pub fn raw(&self) -> &'a str {
301 self.raw
302 }
303
304 pub fn raw_short(&self) -> &'a str {
311 self.before_code
312 }
313
314 pub fn triple(&self) -> (u64, u64, u64) {
316 (self.major(), self.minor(), self.patch())
317 }
318
319 pub fn quad(&self) -> (u64, u64, u64, u64) {
321 (self.major(), self.minor(), self.patch(), self.revision())
322 }
323
324 pub fn raw_quad(&self) -> (&'a str, Option<&'a str>, Option<&'a str>, Option<&'a str>) {
326 (
327 self.major,
328 (self.components > 1).then_some(self.minor),
329 (self.components > 2).then_some(self.patch),
330 (self.components > 3).then_some(self.revision),
331 )
332 }
333}
334
335impl<'a> Ord for Version<'a> {
336 fn cmp(&self, other: &Self) -> Ordering {
337 match self.quad().cmp(&other.quad()) {
338 Ordering::Equal => {
339 match (self.pre(), other.pre()) {
341 (None, Some(_)) => return Ordering::Greater,
342 (Some(_), None) => return Ordering::Less,
343 (Some(self_pre), Some(other_pre)) => {
344 match self_pre.cmp(other_pre) {
345 Ordering::Equal => {}
346 other => return other,
347 };
348 }
349 (None, None) => {}
350 }
351
352 if let (Some(self_num), Some(other_num)) =
354 (self.build_number(), other.build_number())
355 {
356 return self_num.cmp(&other_num);
357 }
358
359 self.build_code().cmp(&other.build_code())
361 }
362 other => other,
363 }
364 }
365}
366
367impl<'a> PartialOrd for Version<'a> {
368 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
369 Some(self.cmp(other))
370 }
371}
372
373impl<'a> PartialEq for Version<'a> {
374 fn eq(&self, other: &Self) -> bool {
375 self.quad() == other.quad()
376 && self.pre() == other.pre()
377 && self.build_code() == other.build_code()
378 }
379}
380
381impl<'a> Eq for Version<'a> {}
382
383impl<'a> fmt::Display for Version<'a> {
384 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
385 write!(f, "{}", self.raw)?;
386 Ok(())
387 }
388}
389
390#[derive(Debug, Clone, PartialEq)]
392pub struct Release<'a> {
393 raw: &'a str,
394 package: &'a str,
395 version_raw: &'a str,
396 version: Option<Version<'a>>,
397}
398
399#[cfg(feature = "serde")]
400impl<'a> Serialize for Release<'a> {
401 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
402 where
403 S: Serializer,
404 {
405 let mut state = serializer.serialize_struct("Release", 6)?;
406 state.serialize_field("package", &self.package())?;
407 state.serialize_field("version_raw", &self.version_raw())?;
408 state.serialize_field("version_parsed", &self.version())?;
409 state.serialize_field("build_hash", &self.build_hash())?;
410 state.serialize_field("description", &self.describe().to_string())?;
411 state.end()
412 }
413}
414
415pub fn validate_release(release: &str) -> Result<(), InvalidRelease> {
417 if release.len() > 200 {
418 Err(InvalidRelease::TooLong)
419 } else if release == "." || release == ".." || release.eq_ignore_ascii_case("latest") {
420 Err(InvalidRelease::RestrictedName)
421 } else if !VALID_API_ATTRIBUTE_REGEX.is_match(release) {
422 Err(InvalidRelease::BadCharacters)
423 } else {
424 Ok(())
425 }
426}
427
428pub fn validate_environment(environment: &str) -> Result<(), InvalidEnvironment> {
430 if environment.len() > 64 {
431 Err(InvalidEnvironment::TooLong)
432 } else if environment == "." || environment == ".." || environment.eq_ignore_ascii_case("none")
433 {
434 Err(InvalidEnvironment::RestrictedName)
435 } else if !VALID_API_ATTRIBUTE_REGEX.is_match(environment) {
436 Err(InvalidEnvironment::BadCharacters)
437 } else {
438 Ok(())
439 }
440}
441
442impl<'a> Release<'a> {
443 pub fn parse(release: &'a str) -> Result<Release<'a>, InvalidRelease> {
445 let release = release.trim();
446 validate_release(release)?;
447 if let Some(caps) = RELEASE_REGEX.captures(release) {
448 let package = caps.get(1).unwrap().as_str();
449 let version_raw = caps.get(2).unwrap().as_str();
450 if !is_build_hash(version_raw) {
451 let version = Version::parse(version_raw).ok();
452 return Ok(Release {
453 raw: release,
454 package,
455 version_raw,
456 version,
457 });
458 } else {
459 return Ok(Release {
460 raw: release,
461 package,
462 version_raw,
463 version: None,
464 });
465 }
466 }
467 Ok(Release {
468 raw: release,
469 package: "",
470 version_raw: release,
471 version: None,
472 })
473 }
474
475 pub fn raw(&self) -> &'a str {
479 self.raw
480 }
481
482 pub fn package(&self) -> Option<&'a str> {
484 if self.package.is_empty() {
485 None
486 } else {
487 Some(self.package)
488 }
489 }
490
491 pub fn version_raw(&self) -> &'a str {
496 self.version_raw
497 }
498
499 pub fn version(&self) -> Option<&Version<'a>> {
501 self.version.as_ref()
502 }
503
504 pub fn build_hash(&self) -> Option<&'a str> {
506 self.version
507 .as_ref()
508 .and_then(|x| x.build_code())
509 .filter(|x| is_build_hash(x))
510 .or_else(|| {
511 if is_build_hash(self.version_raw()) {
512 Some(self.version_raw())
513 } else {
514 None
515 }
516 })
517 }
518
519 pub fn describe(&self) -> ReleaseDescription<'_> {
525 ReleaseDescription(self)
526 }
527}
528
529#[derive(Debug)]
531pub struct ReleaseDescription<'a>(&'a Release<'a>);
532
533impl<'a> fmt::Display for ReleaseDescription<'a> {
534 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
535 let short_hash = self
536 .0
537 .build_hash()
538 .map(|hash| hash.get(..12).unwrap_or(hash));
539
540 if let Some(ver) = self.0.version() {
541 write!(f, "{}", ver.raw_short())?;
542 if let Some(short_hash) = short_hash {
543 write!(f, " ({})", short_hash)?;
544 } else if let Some(build_code) = ver.build_code() {
545 write!(f, " ({})", build_code)?;
546 }
547 } else if let Some(short_hash) = short_hash {
548 write!(f, "{}", short_hash)?;
549 } else {
550 write!(f, "{}", self.0)?;
551 }
552 Ok(())
553 }
554}
555
556impl<'a> fmt::Display for Release<'a> {
557 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
558 let mut have_package = false;
559 if let Some(package) = self.package() {
560 write!(f, "{}", package)?;
561 have_package = true;
562 }
563 if let Some(version) = self.version() {
564 if have_package {
565 write!(f, "@")?;
566 }
567 write!(f, "{}", version)?;
568 } else {
569 if have_package {
570 write!(f, "@")?;
571 }
572 write!(f, "{}", self.version_raw)?;
573 }
574 Ok(())
575 }
576}
577
578#[test]
579fn test_release_validation() {
580 assert_eq!(
581 validate_release("latest"),
582 Err(InvalidRelease::RestrictedName)
583 );
584 assert_eq!(validate_release("."), Err(InvalidRelease::RestrictedName));
585 assert_eq!(validate_release(".."), Err(InvalidRelease::RestrictedName));
586 assert_eq!(
587 validate_release("foo\nbar"),
588 Err(InvalidRelease::BadCharacters)
589 );
590 assert_eq!(validate_release("good"), Ok(()));
591}
592
593#[test]
594fn test_environment_validation() {
595 assert_eq!(
596 validate_environment("none"),
597 Err(InvalidEnvironment::RestrictedName)
598 );
599 assert_eq!(
600 validate_environment("."),
601 Err(InvalidEnvironment::RestrictedName)
602 );
603 assert_eq!(
604 validate_environment(".."),
605 Err(InvalidEnvironment::RestrictedName)
606 );
607 assert_eq!(
608 validate_environment("f4f3db928593f258e1d850997be07b577f0779cc5549f9968bae625ea001175bX"),
609 Err(InvalidEnvironment::TooLong)
610 );
611 assert_eq!(
612 validate_environment("foo\nbar"),
613 Err(InvalidEnvironment::BadCharacters)
614 );
615 assert_eq!(validate_environment("good"), Ok(()));
616}
617
618#[test]
619fn test_version_ordering() {
620 macro_rules! ver {
621 ($v:expr) => {
622 Version::parse($v).unwrap()
623 };
624 }
625
626 assert_eq!(ver!("1.0.0"), ver!("1.0.0"));
627 assert_ne!(ver!("1.1.0"), ver!("1.0.0"));
628 assert_eq!(ver!("1.0"), ver!("1.0.0"));
629 assert_eq!(ver!("1.0dev"), ver!("1.0.0-dev"));
630 assert_eq!(ver!("1.0dev"), ver!("1.0.0.0-dev"));
631 assert_ne!(ver!("1.0dev"), ver!("1.0.0.0-dev1"));
632
633 assert!(ver!("1.0dev") >= ver!("1.0.0.0-dev"));
634 assert!(ver!("1.0dev") < ver!("1.0.0.0"));
635
636 assert!(ver!("1.0.0") > ver!("1.0.0-rc1"));
637 assert!(ver!("1.0.0") >= ver!("1.0.0-rc1"));
638 assert!(ver!("1.0.0-rc1") > ver!("0.9"));
639 assert!(ver!("1.0.0+10") < ver!("1.0.0+20"));
640 assert!(ver!("1.0.0+a") < ver!("1.0.0+b"));
641
642 assert!(ver!("1.0") < ver!("2.0"));
643 assert!(ver!("1.1.0") < ver!("10.0"));
644 assert!(ver!("1.1.0") < ver!("1.1.1"));
645 assert!(ver!("1.1.0.1") < ver!("1.1.0.2"));
646 assert!(ver!("1.1.0.1") > ver!("1.1.0.0"));
647 assert!(ver!("1.1.0.1") > ver!("1.0.0.0"));
648 assert!(ver!("1.1.0.1") > ver!("1.0.42.0"));
649
650 assert!(ver!("1.0+abcd") < ver!("1.0+abcde"));
651}