cargo-cyclonedx 0.3.1

CycloneDX Software Bill of Materials (SBOM) for Rust Crates
Documentation
/*
 * This file is part of CycloneDX Rust Cargo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) OWASP Foundation. All Rights Reserved.
 */
use std::{io, str::FromStr};

use lazy_static::lazy_static;
use regex::Regex;
use serde::Serialize;
use thiserror::Error;
use xml_writer::XmlWriter;

use crate::traits::ToXml;

lazy_static! {
    static ref EMAIL_REGEX: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#).expect("Could not compile email regex");
}

#[derive(Debug, Error, PartialEq)]
pub enum AuthorParseError {
    #[error("Could not parse email: {0}")]
    Email(String),
}

#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Author {
    name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
}

impl Author {
    fn new(name: String, email: Option<String>) -> Result<Self, AuthorParseError> {
        if let Some(email) = &email {
            if !EMAIL_REGEX.is_match(&email) {
                return Err(AuthorParseError::Email(email.to_owned()));
            }
        }

        Ok(Self { name, email })
    }
}

impl ToXml for Vec<Author> {
    fn to_xml<W: io::Write>(&self, xml: &mut XmlWriter<W>) -> io::Result<()> {
        xml.begin_elem("authors")?;

        for author in self {
            author.to_xml(xml)?;
        }

        xml.end_elem()
    }
}

impl FromStr for Author {
    type Err = AuthorParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.ends_with('>') {
            let mut parts = s[..s.len() - 1].rsplitn(2, '<');
            let maybe_email = parts.next();
            let maybe_name = parts.next();

            let email = maybe_email.map(String::from);

            if let Some(name) = maybe_name {
                Self::new(name.trim().to_string(), email)
            } else {
                Self::new(s.to_string(), email)
            }
        } else {
            Self::new(s.to_string(), None)
        }
    }
}

impl ToXml for Author {
    fn to_xml<W: io::Write>(&self, xml: &mut XmlWriter<W>) -> io::Result<()> {
        xml.begin_elem("author")?;

        xml.elem_text("name", &self.name)?;

        if let Some(email) = &self.email {
            xml.elem_text("email", email)?;
        }

        xml.end_elem()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    fn author(name: &'static str, email: impl Into<Option<&'static str>>) -> Author {
        Author::new(name.into(), email.into().map(String::from))
            .expect("Failed to construct author for test")
    }

    #[test]
    fn name_with_email() {
        assert_eq!(
            author("Steve Springett", "steve.springett@owasp.org"),
            "Steve Springett <steve.springett@owasp.org>"
                .parse()
                .expect("Failed to parse string as author")
        );
    }

    #[test]
    fn author_without_email() {
        assert_eq!(
            author("Steve Springett", None),
            "Steve Springett"
                .parse()
                .expect("Failed to parse string as author")
        );
    }

    #[test]
    fn author_with_invalid_email() {
        let error = Author::new("Ferris".to_string(), Some("invalid email".to_string()))
            .expect_err("Should have gotten an email error");
        assert_eq!(error, AuthorParseError::Email("invalid email".to_string()));
    }
}