ips/libips/src/fmri.rs

1014 lines
38 KiB
Rust
Raw Normal View History

// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of the
// MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/.
//! FMRI (Fault Management Resource Identifier) implementation
//!
//! An FMRI is a unique identifier for a package in the IPS system.
//! It follows the format: pkg://publisher/package_name@version
//! where:
//! - publisher is optional
//! - version is optional and follows the format: release[,branch][-build]:timestamp
//! - release is a dot-separated vector of digits (e.g., 5.11)
//! - branch is optional and is a dot-separated vector of digits (e.g., 1)
//! - build is optional and is a dot-separated vector of digits (e.g., 2020.0.1.0)
//! - timestamp is optional and is a hexadecimal string (e.g., 20200421T195136Z)
//!
//! The dot-separated vector components (release, branch, build) can be converted to and from
//! semver::Version objects using the provided conversion methods:
//! - release_to_semver
//! - branch_to_semver
//! - build_to_semver
//! - from_semver
//!
//! Examples:
//! - pkg:///sunos/coreutils@5.11,1:[hex-timestamp-1]
//! - pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z
//! - pkg:/system/library@0.5.11-2020.0.1.19563
//! - xvm@0.5.11-2015.0.2.0
//!
//! # Examples
//!
//! ```
//! use libips::fmri::{Fmri, Version};
//!
//! // Parse an FMRI
//! let fmri = Fmri::parse("pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z").unwrap();
//!
//! // Convert the release component to a semver::Version
//! if let Some(version) = &fmri.version {
//! let semver = version.release_to_semver().unwrap();
//! assert_eq!(semver.major, 1);
//! assert_eq!(semver.minor, 18);
//! assert_eq!(semver.patch, 0);
//! }
//!
//! // Create a Version from semver::Version components
//! let release = semver::Version::new(5, 11, 0);
//! let branch = Some(semver::Version::new(1, 0, 0));
//! let build = Some(semver::Version::new(2020, 0, 1));
//! let timestamp = Some("20200421T195136Z".to_string());
//!
//! let version = Version::from_semver(release, branch, build, timestamp);
//! assert_eq!(version.release, "5.11.0");
//! assert_eq!(version.branch, Some("1.0.0".to_string()));
//! assert_eq!(version.build, Some("2020.0.1".to_string()));
//! assert_eq!(version.timestamp, Some("20200421T195136Z".to_string()));
//! ```
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
use serde::{Serialize, Deserialize};
use diff::Diff;
/// Errors that can occur when parsing an FMRI
#[derive(Debug, Error, PartialEq)]
pub enum FmriError {
#[error("invalid FMRI format")]
InvalidFormat,
#[error("invalid version format")]
InvalidVersionFormat,
#[error("invalid release format")]
InvalidReleaseFormat,
#[error("invalid branch format")]
InvalidBranchFormat,
#[error("invalid build format")]
InvalidBuildFormat,
#[error("invalid timestamp format")]
InvalidTimestampFormat,
}
/// A version component of an FMRI
///
/// A version consists of:
/// - release: a dot-separated vector of digits (e.g., 5.11)
/// - branch: optional, a dot-separated vector of digits (e.g., 1)
/// - build: optional, a dot-separated vector of digits (e.g., 2020.0.1.0)
/// - timestamp: optional, a hexadecimal string (e.g., 20200421T195136Z)
///
/// The dot-separated vector components (release, branch, build) can be converted to and from
/// semver::Version objects using the provided conversion methods:
/// - release_to_semver
/// - branch_to_semver
/// - build_to_semver
/// - to_semver
///
/// New Version objects can be created from semver::Version objects using:
/// - new_semver
/// - with_branch_semver
/// - with_build_semver
/// - with_timestamp_semver
/// - from_semver
///
/// # Examples
///
/// ```
/// use libips::fmri::Version;
///
/// // Create a Version from strings
/// let version = Version::new("5.11");
///
/// // Convert to semver::Version
/// let semver = version.release_to_semver().unwrap();
/// assert_eq!(semver.major, 5);
/// assert_eq!(semver.minor, 11);
/// assert_eq!(semver.patch, 0);
///
/// // Create a Version from semver::Version
/// let semver_version = semver::Version::new(1, 2, 3);
/// let version = Version::new_semver(semver_version);
/// assert_eq!(version.release, "1.2.3");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Diff)]
#[diff(attr(
#[derive(Debug, PartialEq)]
))]
pub struct Version {
/// The release component (e.g., 5.11)
pub release: String,
/// The branch component (e.g., 1)
pub branch: Option<String>,
/// The build component (e.g., 2020.0.1.0)
pub build: Option<String>,
/// The timestamp component (e.g., 20200421T195136Z)
pub timestamp: Option<String>,
}
impl Version {
/// Create a new Version with the given release
pub fn new(release: &str) -> Self {
Version {
release: release.to_string(),
branch: None,
build: None,
timestamp: None,
}
}
/// Helper method to pad a version string to ensure it has at least MAJOR.MINOR.PATCH components
///
/// This method takes a dot-separated version string and ensures it has at least three components
/// by padding with zeros if necessary. If the string has more than three components, only the
/// first three are used.
fn pad_version_string(version_str: &str) -> String {
let parts: Vec<&str> = version_str.split('.').collect();
match parts.len() {
1 => format!("{}.0.0", parts[0]),
2 => format!("{}.{}.0", parts[0], parts[1]),
3 => format!("{}.{}.{}", parts[0], parts[1], parts[2]),
_ => format!("{}.{}.{}", parts[0], parts[1], parts[2]), // Use only the first three parts
}
}
/// Convert the release component to a semver::Version
///
/// This method attempts to parse the release component as a semver::Version.
/// If the release component doesn't have enough parts (e.g., "5.11" instead of "5.11.0"),
/// it will be padded with zeros to make it a valid semver version.
/// If the release component has more than three parts, only the first three will be used.
pub fn release_to_semver(&self) -> Result<semver::Version, semver::Error> {
let version_str = Self::pad_version_string(&self.release);
version_str.parse()
}
/// Convert the branch component to a semver::Version
///
/// This method attempts to parse the branch component as a semver::Version.
/// If the branch component doesn't have enough parts (e.g., "1" instead of "1.0.0"),
/// it will be padded with zeros to make it a valid semver version.
/// If the branch component has more than three parts, only the first three will be used.
/// Returns None if the branch component is None.
pub fn branch_to_semver(&self) -> Option<Result<semver::Version, semver::Error>> {
self.branch.as_ref().map(|branch| {
let version_str = Self::pad_version_string(branch);
version_str.parse()
})
}
/// Convert the build component to a semver::Version
///
/// This method attempts to parse the build component as a semver::Version.
/// If the build component doesn't have enough parts (e.g., "1" instead of "1.0.0"),
/// it will be padded with zeros to make it a valid semver version.
/// If the build component has more than three parts, only the first three will be used.
/// Returns None if the build component is None.
pub fn build_to_semver(&self) -> Option<Result<semver::Version, semver::Error>> {
self.build.as_ref().map(|build| {
let version_str = Self::pad_version_string(build);
version_str.parse()
})
}
/// Create a new Version with the given semver::Version as release
///
/// This method creates a new Version with the given semver::Version as release.
/// The semver::Version is converted to a string.
pub fn new_semver(release: semver::Version) -> Self {
Version {
release: release.to_string(),
branch: None,
build: None,
timestamp: None,
}
}
/// Create a Version from semver::Version components
///
/// This method creates a Version from semver::Version components.
/// The semver::Version components are converted to strings.
pub fn from_semver(
release: semver::Version,
branch: Option<semver::Version>,
build: Option<semver::Version>,
timestamp: Option<String>,
) -> Self {
Version {
release: release.to_string(),
branch: branch.map(|v| v.to_string()),
build: build.map(|v| v.to_string()),
timestamp,
}
}
/// Create a new Version with the given semver::Version as release and branch
///
/// This method creates a new Version with the given semver::Version as release and branch.
/// The semver::Version objects are converted to strings.
pub fn with_branch_semver(release: semver::Version, branch: semver::Version) -> Self {
Version {
release: release.to_string(),
branch: Some(branch.to_string()),
build: None,
timestamp: None,
}
}
/// Create a new Version with the given semver::Version as release, branch, and build
///
/// This method creates a new Version with the given semver::Version as release, branch, and build.
/// The semver::Version objects are converted to strings.
pub fn with_build_semver(
release: semver::Version,
branch: Option<semver::Version>,
build: semver::Version,
) -> Self {
Version {
release: release.to_string(),
branch: branch.map(|v| v.to_string()),
build: Some(build.to_string()),
timestamp: None,
}
}
/// Create a new Version with the given semver::Version as release, branch, build, and timestamp
///
/// This method creates a new Version with the given semver::Version as release, branch, build, and timestamp.
/// The semver::Version objects are converted to strings.
pub fn with_timestamp_semver(
release: semver::Version,
branch: Option<semver::Version>,
build: Option<semver::Version>,
timestamp: &str,
) -> Self {
Version {
release: release.to_string(),
branch: branch.map(|v| v.to_string()),
build: build.map(|v| v.to_string()),
timestamp: Some(timestamp.to_string()),
}
}
/// Get all version components as semver::Version objects
///
/// This method returns all version components as semver::Version objects.
/// If a component is not present or cannot be parsed, it will be None.
pub fn to_semver(&self) -> (Result<semver::Version, semver::Error>, Option<Result<semver::Version, semver::Error>>, Option<Result<semver::Version, semver::Error>>) {
let release = self.release_to_semver();
let branch = self.branch_to_semver();
let build = self.build_to_semver();
(release, branch, build)
}
/// Check if this version is compatible with semver
///
/// This method checks if all components of this version can be parsed as semver::Version objects.
pub fn is_semver_compatible(&self) -> bool {
let (release, branch, build) = self.to_semver();
let release_ok = release.is_ok();
let branch_ok = branch.map_or(true, |r| r.is_ok());
let build_ok = build.map_or(true, |r| r.is_ok());
release_ok && branch_ok && build_ok
}
/// Create a new Version with the given release and branch
pub fn with_branch(release: &str, branch: &str) -> Self {
Version {
release: release.to_string(),
branch: Some(branch.to_string()),
build: None,
timestamp: None,
}
}
/// Create a new Version with the given release, branch, and build
pub fn with_build(release: &str, branch: Option<&str>, build: &str) -> Self {
Version {
release: release.to_string(),
branch: branch.map(|b| b.to_string()),
build: Some(build.to_string()),
timestamp: None,
}
}
/// Create a new Version with the given release, branch, build, and timestamp
pub fn with_timestamp(
release: &str,
branch: Option<&str>,
build: Option<&str>,
timestamp: &str,
) -> Self {
Version {
release: release.to_string(),
branch: branch.map(|b| b.to_string()),
build: build.map(|b| b.to_string()),
timestamp: Some(timestamp.to_string()),
}
}
/// Parse a version string into a Version
///
/// The version string should be in the format: release[,branch][-build][:timestamp]
pub fn parse(version_str: &str) -> Result<Self, FmriError> {
let mut version = Version {
release: String::new(),
branch: None,
build: None,
timestamp: None,
};
// Split by colon to separate timestamp
let parts: Vec<&str> = version_str.split(':').collect();
if parts.len() > 2 {
return Err(FmriError::InvalidVersionFormat);
}
// If there's a timestamp, parse it
if parts.len() == 2 {
let timestamp = parts[1];
// Reject empty timestamps
if timestamp.is_empty() {
return Err(FmriError::InvalidTimestampFormat);
}
if !timestamp.chars().all(|c| c.is_ascii_hexdigit() || c == 'T' || c == 'Z') {
return Err(FmriError::InvalidTimestampFormat);
}
version.timestamp = Some(timestamp.to_string());
}
// Split the first part by dash to separate build
let parts: Vec<&str> = parts[0].split('-').collect();
if parts.len() > 2 {
return Err(FmriError::InvalidVersionFormat);
}
// If there's a build, parse it
if parts.len() == 2 {
let build = parts[1];
if !Self::is_valid_dot_vector(build) {
return Err(FmriError::InvalidBuildFormat);
}
version.build = Some(build.to_string());
}
// Split the first part by comma to separate release and branch
let parts: Vec<&str> = parts[0].split(',').collect();
if parts.len() > 2 {
return Err(FmriError::InvalidVersionFormat);
}
// Parse the release
let release = parts[0];
if !Self::is_valid_dot_vector(release) {
return Err(FmriError::InvalidReleaseFormat);
}
version.release = release.to_string();
// If there's a branch, parse it
if parts.len() == 2 {
let branch = parts[1];
if !Self::is_valid_dot_vector(branch) {
return Err(FmriError::InvalidBranchFormat);
}
version.branch = Some(branch.to_string());
}
Ok(version)
}
/// Check if a string is a valid dot-separated vector of digits
///
/// This method uses semver for validation when possible, but also accepts
/// dot-separated vectors with fewer than 3 components (which are not valid semver).
fn is_valid_dot_vector(s: &str) -> bool {
if s.is_empty() {
return false;
}
// First check if it's a valid dot-separated vector of digits
let parts: Vec<&str> = s.split('.').collect();
for part in &parts {
if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
return false;
}
}
// If it has at least 3 components, try to parse it as a semver version
if parts.len() >= 3 {
// Create a version string with exactly MAJOR.MINOR.PATCH
let version_str = format!("{}.{}.{}", parts[0], parts[1], parts[2]);
// Try to parse it as a semver version
if let Err(_) = semver::Version::parse(&version_str) {
return false;
}
}
true
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.release)?;
if let Some(branch) = &self.branch {
write!(f, ",{}", branch)?;
}
if let Some(build) = &self.build {
write!(f, "-{}", build)?;
}
if let Some(timestamp) = &self.timestamp {
write!(f, ":{}", timestamp)?;
}
Ok(())
}
}
impl FromStr for Version {
type Err = FmriError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
/// An FMRI (Fault Management Resource Identifier)
///
/// An FMRI is a unique identifier for a package in the IPS system.
/// It follows the format: pkg://publisher/package_name@version
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Diff)]
#[diff(attr(
#[derive(Debug, PartialEq)]
))]
pub struct Fmri {
/// The scheme (e.g., pkg)
pub scheme: String,
/// The publisher (e.g., openindiana.org)
pub publisher: Option<String>,
/// The package name (e.g., web/server/nginx)
pub name: String,
/// The version
pub version: Option<Version>,
}
impl Fmri {
/// Create a new FMRI with the given name
pub fn new(name: &str) -> Self {
Fmri {
scheme: "pkg".to_string(),
publisher: None,
name: name.to_string(),
version: None,
}
}
/// Create a new FMRI with the given name and version
pub fn with_version(name: &str, version: Version) -> Self {
Fmri {
scheme: "pkg".to_string(),
publisher: None,
name: name.to_string(),
version: Some(version),
}
}
/// Create a new FMRI with the given publisher, name, and version
pub fn with_publisher(publisher: &str, name: &str, version: Option<Version>) -> Self {
Fmri {
scheme: "pkg".to_string(),
publisher: Some(publisher.to_string()),
name: name.to_string(),
version,
}
}
/// Get the stem of the FMRI (the package name without version)
pub fn stem(&self) -> &str {
&self.name
}
/// Get the version of the FMRI as a string
pub fn version(&self) -> String {
match &self.version {
Some(v) => v.to_string(),
None => String::new(),
}
}
/// Parse an FMRI string into an Fmri
///
/// The FMRI string should be in the format: [scheme://][publisher/]name[@version]
pub fn parse(fmri_str: &str) -> Result<Self, FmriError> {
let mut fmri = Fmri {
scheme: "pkg".to_string(),
publisher: None,
name: String::new(),
version: None,
};
// Split by @ to separate name and version
let parts: Vec<&str> = fmri_str.split('@').collect();
if parts.len() > 2 {
return Err(FmriError::InvalidFormat);
}
// If there's a version, parse it
if parts.len() == 2 {
let version = Version::parse(parts[1])?;
fmri.version = Some(version);
}
// Parse the name part
let name_part = parts[0];
// Check if there's a scheme with a publisher (pkg://publisher/name)
if let Some(scheme_end) = name_part.find("://") {
fmri.scheme = name_part[0..scheme_end].to_string();
// Extract the rest after the scheme
let rest = &name_part[scheme_end + 3..];
// Check if there's a publisher
if let Some(publisher_end) = rest.find('/') {
// If there's a non-empty publisher, set it
if publisher_end > 0 {
fmri.publisher = Some(rest[0..publisher_end].to_string());
}
// Set the name
fmri.name = rest[publisher_end + 1..].to_string();
} else {
// No publisher, just a name
fmri.name = rest.to_string();
}
}
// Check if there's a scheme without a publisher (pkg:/name)
else if let Some(scheme_end) = name_part.find(":/") {
fmri.scheme = name_part[0..scheme_end].to_string();
// Extract the rest after the scheme
let rest = &name_part[scheme_end + 2..];
// Set the name
fmri.name = rest.to_string();
}
else {
// No scheme, just a name
fmri.name = name_part.to_string();
}
// Validate that the name is not empty
if fmri.name.is_empty() {
return Err(FmriError::InvalidFormat);
}
Ok(fmri)
}
}
impl fmt::Display for Fmri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// For FMRIs without a publisher, we should use the format pkg:///name
// For FMRIs with a publisher, we should use the format pkg://publisher/name
if let Some(publisher) = &self.publisher {
write!(f, "{}://{}/", self.scheme, publisher)?;
} else {
write!(f, "{}:///", self.scheme)?;
}
write!(f, "{}", self.name)?;
if let Some(version) = &self.version {
write!(f, "@{}", version)?;
}
Ok(())
}
}
impl FromStr for Fmri {
type Err = FmriError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_semver_conversion() {
// Test release_to_semver
let version = Version::new("5.11");
let semver = version.release_to_semver().unwrap();
assert_eq!(semver.major, 5);
assert_eq!(semver.minor, 11);
assert_eq!(semver.patch, 0);
// Test with a full semver version
let version = Version::new("1.2.3");
let semver = version.release_to_semver().unwrap();
assert_eq!(semver.major, 1);
assert_eq!(semver.minor, 2);
assert_eq!(semver.patch, 3);
// Test branch_to_semver
let version = Version::with_branch("5.11", "1");
let semver = version.branch_to_semver().unwrap().unwrap();
assert_eq!(semver.major, 1);
assert_eq!(semver.minor, 0);
assert_eq!(semver.patch, 0);
// Test with a full semver version
let mut version = Version::new("5.11");
version.branch = Some("1.2.3".to_string());
let semver = version.branch_to_semver().unwrap().unwrap();
assert_eq!(semver.major, 1);
assert_eq!(semver.minor, 2);
assert_eq!(semver.patch, 3);
// Test build_to_semver
let mut version = Version::new("5.11");
version.build = Some("2020.0.1.0".to_string());
let semver = version.build_to_semver().unwrap().unwrap();
assert_eq!(semver.major, 2020);
assert_eq!(semver.minor, 0);
assert_eq!(semver.patch, 1);
// Test from_semver
let release = semver::Version::new(5, 11, 0);
let branch = Some(semver::Version::new(1, 0, 0));
let build = Some(semver::Version::new(2020, 0, 1));
let timestamp = Some("20200421T195136Z".to_string());
let version = Version::from_semver(release, branch, build, timestamp);
assert_eq!(version.release, "5.11.0");
assert_eq!(version.branch, Some("1.0.0".to_string()));
assert_eq!(version.build, Some("2020.0.1".to_string()));
assert_eq!(version.timestamp, Some("20200421T195136Z".to_string()));
}
#[test]
fn test_new_semver_constructors() {
// Test new_semver
let semver_version = semver::Version::new(1, 2, 3);
let version = Version::new_semver(semver_version);
assert_eq!(version.release, "1.2.3");
assert_eq!(version.branch, None);
assert_eq!(version.build, None);
assert_eq!(version.timestamp, None);
// Test with_branch_semver
let release = semver::Version::new(5, 11, 0);
let branch = semver::Version::new(1, 0, 0);
let version = Version::with_branch_semver(release, branch);
assert_eq!(version.release, "5.11.0");
assert_eq!(version.branch, Some("1.0.0".to_string()));
assert_eq!(version.build, None);
assert_eq!(version.timestamp, None);
// Test with_build_semver
let release = semver::Version::new(5, 11, 0);
let branch = Some(semver::Version::new(1, 0, 0));
let build = semver::Version::new(2020, 0, 1);
let version = Version::with_build_semver(release, branch, build);
assert_eq!(version.release, "5.11.0");
assert_eq!(version.branch, Some("1.0.0".to_string()));
assert_eq!(version.build, Some("2020.0.1".to_string()));
assert_eq!(version.timestamp, None);
// Test with_timestamp_semver
let release = semver::Version::new(5, 11, 0);
let branch = Some(semver::Version::new(1, 0, 0));
let build = Some(semver::Version::new(2020, 0, 1));
let timestamp = "20200421T195136Z";
let version = Version::with_timestamp_semver(release, branch, build, timestamp);
assert_eq!(version.release, "5.11.0");
assert_eq!(version.branch, Some("1.0.0".to_string()));
assert_eq!(version.build, Some("2020.0.1".to_string()));
assert_eq!(version.timestamp, Some("20200421T195136Z".to_string()));
}
#[test]
fn test_to_semver() {
// Test to_semver with all components
let mut version = Version::new("5.11");
version.branch = Some("1.2.3".to_string());
version.build = Some("2020.0.1".to_string());
let (release, branch, build) = version.to_semver();
assert!(release.is_ok());
let release = release.unwrap();
assert_eq!(release.major, 5);
assert_eq!(release.minor, 11);
assert_eq!(release.patch, 0);
assert!(branch.is_some());
let branch = branch.unwrap().unwrap();
assert_eq!(branch.major, 1);
assert_eq!(branch.minor, 2);
assert_eq!(branch.patch, 3);
assert!(build.is_some());
let build = build.unwrap().unwrap();
assert_eq!(build.major, 2020);
assert_eq!(build.minor, 0);
assert_eq!(build.patch, 1);
// Test is_semver_compatible
assert!(version.is_semver_compatible());
// Test with invalid semver
let mut version = Version::new("5.11");
version.branch = Some("invalid".to_string());
assert!(!version.is_semver_compatible());
}
#[test]
fn test_semver_validation() {
// Test valid dot-separated vectors
assert!(Version::is_valid_dot_vector("5"));
assert!(Version::is_valid_dot_vector("5.11"));
assert!(Version::is_valid_dot_vector("5.11.0"));
assert!(Version::is_valid_dot_vector("2020.0.1.0"));
// Test invalid dot-separated vectors
assert!(!Version::is_valid_dot_vector(""));
assert!(!Version::is_valid_dot_vector(".11"));
assert!(!Version::is_valid_dot_vector("5."));
assert!(!Version::is_valid_dot_vector("5..11"));
assert!(!Version::is_valid_dot_vector("5a.11"));
// Test semver validation
assert!(Version::is_valid_dot_vector("1.2.3"));
assert!(Version::is_valid_dot_vector("0.0.0"));
assert!(Version::is_valid_dot_vector("999999.999999.999999"));
}
#[test]
fn test_version_parse() {
// Test parsing a release
let version = Version::parse("5.11").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, None);
assert_eq!(version.build, None);
assert_eq!(version.timestamp, None);
// Test parsing a release and branch
let version = Version::parse("5.11,1").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, Some("1".to_string()));
assert_eq!(version.build, None);
assert_eq!(version.timestamp, None);
// Test parsing a release, branch, and build
let version = Version::parse("5.11,1-2020.0.1.0").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, Some("1".to_string()));
assert_eq!(version.build, Some("2020.0.1.0".to_string()));
assert_eq!(version.timestamp, None);
// Test parsing a release and build (no branch)
let version = Version::parse("5.11-2020.0.1.0").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, None);
assert_eq!(version.build, Some("2020.0.1.0".to_string()));
assert_eq!(version.timestamp, None);
// Test parsing a release, branch, build, and timestamp
let version = Version::parse("5.11,1-2020.0.1.0:20200421T195136Z").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, Some("1".to_string()));
assert_eq!(version.build, Some("2020.0.1.0".to_string()));
assert_eq!(version.timestamp, Some("20200421T195136Z".to_string()));
// Test parsing a release and timestamp (no branch or build)
let version = Version::parse("5.11:20200421T195136Z").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, None);
assert_eq!(version.build, None);
assert_eq!(version.timestamp, Some("20200421T195136Z".to_string()));
// Test parsing a release, branch, and timestamp (no build)
let version = Version::parse("5.11,1:20200421T195136Z").unwrap();
assert_eq!(version.release, "5.11");
assert_eq!(version.branch, Some("1".to_string()));
assert_eq!(version.build, None);
assert_eq!(version.timestamp, Some("20200421T195136Z".to_string()));
}
#[test]
fn test_version_display() {
// Test displaying a release
let version = Version::new("5.11");
assert_eq!(version.to_string(), "5.11");
// Test displaying a release and branch
let version = Version::with_branch("5.11", "1");
assert_eq!(version.to_string(), "5.11,1");
// Test displaying a release, branch, and build
let version = Version::with_build("5.11", Some("1"), "2020.0.1.0");
assert_eq!(version.to_string(), "5.11,1-2020.0.1.0");
// Test displaying a release and build (no branch)
let version = Version::with_build("5.11", None, "2020.0.1.0");
assert_eq!(version.to_string(), "5.11-2020.0.1.0");
// Test displaying a release, branch, build, and timestamp
let version = Version::with_timestamp("5.11", Some("1"), Some("2020.0.1.0"), "20200421T195136Z");
assert_eq!(version.to_string(), "5.11,1-2020.0.1.0:20200421T195136Z");
// Test displaying a release and timestamp (no branch or build)
let version = Version::with_timestamp("5.11", None, None, "20200421T195136Z");
assert_eq!(version.to_string(), "5.11:20200421T195136Z");
// Test displaying a release, branch, and timestamp (no build)
let version = Version::with_timestamp("5.11", Some("1"), None, "20200421T195136Z");
assert_eq!(version.to_string(), "5.11,1:20200421T195136Z");
}
#[test]
fn test_fmri_parse() {
// Test parsing a name only
let fmri = Fmri::parse("sunos/coreutils").unwrap();
assert_eq!(fmri.scheme, "pkg");
assert_eq!(fmri.publisher, None);
assert_eq!(fmri.name, "sunos/coreutils");
assert_eq!(fmri.version, None);
// Test parsing a name and version
let fmri = Fmri::parse("sunos/coreutils@5.11,1:20200421T195136Z").unwrap();
assert_eq!(fmri.scheme, "pkg");
assert_eq!(fmri.publisher, None);
assert_eq!(fmri.name, "sunos/coreutils");
assert_eq!(
fmri.version,
Some(Version {
release: "5.11".to_string(),
branch: Some("1".to_string()),
build: None,
timestamp: Some("20200421T195136Z".to_string()),
})
);
// Test parsing with scheme
let fmri = Fmri::parse("pkg://sunos/coreutils").unwrap();
assert_eq!(fmri.scheme, "pkg");
assert_eq!(fmri.publisher, Some("sunos".to_string()));
assert_eq!(fmri.name, "coreutils");
assert_eq!(fmri.version, None);
// Test parsing with scheme and empty publisher
let fmri = Fmri::parse("pkg:///sunos/coreutils").unwrap();
assert_eq!(fmri.scheme, "pkg");
assert_eq!(fmri.publisher, None);
assert_eq!(fmri.name, "sunos/coreutils");
assert_eq!(fmri.version, None);
// Test parsing with scheme, publisher, and version
let fmri = Fmri::parse("pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z").unwrap();
assert_eq!(fmri.scheme, "pkg");
assert_eq!(fmri.publisher, Some("openindiana.org".to_string()));
assert_eq!(fmri.name, "web/server/nginx");
assert_eq!(
fmri.version,
Some(Version {
release: "1.18.0".to_string(),
branch: Some("5.11".to_string()),
build: Some("2020.0.1.0".to_string()),
timestamp: Some("20200421T195136Z".to_string()),
})
);
// Test parsing with scheme and version
let fmri = Fmri::parse("pkg:/system/library@0.5.11-2020.0.1.19563").unwrap();
assert_eq!(fmri.scheme, "pkg");
assert_eq!(fmri.publisher, None);
assert_eq!(fmri.name, "system/library");
assert_eq!(
fmri.version,
Some(Version {
release: "0.5.11".to_string(),
branch: None,
build: Some("2020.0.1.19563".to_string()),
timestamp: None,
})
);
}
#[test]
fn test_fmri_display() {
// Test displaying a name only
let fmri = Fmri::new("sunos/coreutils");
assert_eq!(fmri.to_string(), "pkg:///sunos/coreutils");
// Test displaying a name and version
let version = Version::with_timestamp("5.11", Some("1"), None, "20200421T195136Z");
let fmri = Fmri::with_version("sunos/coreutils", version);
assert_eq!(fmri.to_string(), "pkg:///sunos/coreutils@5.11,1:20200421T195136Z");
// Test displaying with publisher
let fmri = Fmri::with_publisher("openindiana.org", "web/server/nginx", None);
assert_eq!(fmri.to_string(), "pkg://openindiana.org/web/server/nginx");
// Test displaying with publisher and version
let version = Version::with_timestamp("1.18.0", Some("5.11"), Some("2020.0.1.0"), "20200421T195136Z");
let fmri = Fmri::with_publisher("openindiana.org", "web/server/nginx", Some(version));
assert_eq!(
fmri.to_string(),
"pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z"
);
}
#[test]
fn test_version_errors() {
// Test invalid release format
assert_eq!(Version::parse(""), Err(FmriError::InvalidReleaseFormat));
assert_eq!(Version::parse(".11"), Err(FmriError::InvalidReleaseFormat));
assert_eq!(Version::parse("5."), Err(FmriError::InvalidReleaseFormat));
assert_eq!(Version::parse("5..11"), Err(FmriError::InvalidReleaseFormat));
assert_eq!(Version::parse("5a.11"), Err(FmriError::InvalidReleaseFormat));
// Test invalid branch format
assert_eq!(Version::parse("5.11,"), Err(FmriError::InvalidBranchFormat));
assert_eq!(Version::parse("5.11,.1"), Err(FmriError::InvalidBranchFormat));
assert_eq!(Version::parse("5.11,1."), Err(FmriError::InvalidBranchFormat));
assert_eq!(Version::parse("5.11,1..2"), Err(FmriError::InvalidBranchFormat));
assert_eq!(Version::parse("5.11,1a.2"), Err(FmriError::InvalidBranchFormat));
// Test invalid build format
assert_eq!(Version::parse("5.11-"), Err(FmriError::InvalidBuildFormat));
assert_eq!(Version::parse("5.11-.1"), Err(FmriError::InvalidBuildFormat));
assert_eq!(Version::parse("5.11-1."), Err(FmriError::InvalidBuildFormat));
assert_eq!(Version::parse("5.11-1..2"), Err(FmriError::InvalidBuildFormat));
assert_eq!(Version::parse("5.11-1a.2"), Err(FmriError::InvalidBuildFormat));
// Test invalid timestamp format
assert_eq!(Version::parse("5.11:"), Err(FmriError::InvalidTimestampFormat));
assert_eq!(Version::parse("5.11:xyz"), Err(FmriError::InvalidTimestampFormat));
// Test invalid version format
assert_eq!(Version::parse("5.11,1,2"), Err(FmriError::InvalidVersionFormat));
assert_eq!(Version::parse("5.11-1-2"), Err(FmriError::InvalidVersionFormat));
assert_eq!(Version::parse("5.11:1:2"), Err(FmriError::InvalidVersionFormat));
}
#[test]
fn test_fmri_errors() {
// Test invalid format
assert_eq!(Fmri::parse(""), Err(FmriError::InvalidFormat));
assert_eq!(Fmri::parse("pkg://"), Err(FmriError::InvalidFormat));
assert_eq!(Fmri::parse("pkg:///"), Err(FmriError::InvalidFormat));
assert_eq!(Fmri::parse("pkg://publisher/"), Err(FmriError::InvalidFormat));
assert_eq!(Fmri::parse("@5.11"), Err(FmriError::InvalidFormat));
assert_eq!(Fmri::parse("name@version@extra"), Err(FmriError::InvalidFormat));
// Test invalid version
assert_eq!(Fmri::parse("name@"), Err(FmriError::InvalidReleaseFormat));
assert_eq!(Fmri::parse("name@5.11,"), Err(FmriError::InvalidBranchFormat));
assert_eq!(Fmri::parse("name@5.11-"), Err(FmriError::InvalidBuildFormat));
assert_eq!(Fmri::parse("name@5.11:"), Err(FmriError::InvalidTimestampFormat));
}
}