deb-version7 0.1.1

Parsing and comparing of Debian package versions
Documentation
// Rust port of lib/dpkg/t/t-version.c
//
// Copyright © 2009-2014 Guillem Jover <guillem@debian.org>
// Copyright © 2024 Kunal Mehta <legoktm@debian.org>
//
// This is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::parsehelp::parseversion;
use crate::version::dpkg_version_compare;
use crate::{DebVersion, Error};
use std::cmp::Ordering;

macro_rules! dpkg_version_object {
    ($epoch: literal, $version: literal) => {
        DebVersion {
            epoch: $epoch,
            version: $version.parse().unwrap(),
            revision: None,
        }
    };
    ($epoch: literal, $version: literal, $revision: literal) => {
        DebVersion {
            epoch: $epoch,
            version: $version.parse().unwrap(),
            revision: Some($revision.parse().unwrap()),
        }
    };
}

#[test]
fn test_version_compare() {
    assert_eq!(
        dpkg_version_compare(&dpkg_version_object!(0, ""), &dpkg_version_object!(0, "")),
        Ordering::Equal
    );

    assert_eq!(
        dpkg_version_compare(&dpkg_version_object!(1, ""), &dpkg_version_object!(2, "")),
        Ordering::Less
    );

    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "1", "1"),
            &dpkg_version_object!(0, "2", "1")
        ),
        Ordering::Less
    );

    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "1", "1"),
            &dpkg_version_object!(0, "1", "2")
        ),
        Ordering::Less
    );

    // Test for version equality.
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "0", "0"),
            &dpkg_version_object!(0, "0", "0")
        ),
        Ordering::Equal
    );

    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "0", "00"),
            &dpkg_version_object!(0, "00", "0")
        ),
        Ordering::Equal
    );

    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(1, "2", "3"),
            &dpkg_version_object!(1, "2", "3")
        ),
        Ordering::Equal
    );

    // Test for epoch difference.
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "0", "0"),
            &dpkg_version_object!(1, "0", "0")
        ),
        Ordering::Less
    );
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(1, "0", "0"),
            &dpkg_version_object!(0, "0", "0")
        ),
        Ordering::Greater
    );

    // Test for version component difference.
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "a", "0"),
            &dpkg_version_object!(0, "b", "0")
        ),
        Ordering::Less
    );
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "b", "0"),
            &dpkg_version_object!(0, "a", "0")
        ),
        Ordering::Greater
    );

    // Test for revision component difference.
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "0", "a"),
            &dpkg_version_object!(0, "0", "b")
        ),
        Ordering::Less
    );
    assert_eq!(
        dpkg_version_compare(
            &dpkg_version_object!(0, "0", "b"),
            &dpkg_version_object!(0, "0", "a")
        ),
        Ordering::Greater
    );
}

// FIXME: test_version_relate

#[test]
fn test_version_parse() {
    // Test 0 versions.
    assert_eq!(parseversion("0").unwrap(), dpkg_version_object!(0, "0"));

    assert_eq!(parseversion("0:0").unwrap(), dpkg_version_object!(0, "0"));

    assert_eq!(
        parseversion("0:0-0").unwrap(),
        dpkg_version_object!(0, "0", "0")
    );

    assert_eq!(
        parseversion("0:0.0-0.0").unwrap(),
        dpkg_version_object!(0, "0.0", "0.0")
    );

    // Test epoched versions.
    assert_eq!(parseversion("1:0").unwrap(), dpkg_version_object!(1, "0"));

    assert_eq!(parseversion("5:1").unwrap(), dpkg_version_object!(5, "1"));

    // Test multiple hyphens.
    assert_eq!(
        parseversion("0:0-0-0").unwrap(),
        dpkg_version_object!(0, "0-0", "0")
    );

    assert_eq!(
        parseversion("0:0-0-0-0").unwrap(),
        dpkg_version_object!(0, "0-0-0", "0")
    );

    // Test multiple colons.
    assert_eq!(
        parseversion("0:0:0-0").unwrap(),
        dpkg_version_object!(0, "0:0", "0")
    );

    assert_eq!(
        parseversion("0:0:0:0-0").unwrap(),
        dpkg_version_object!(0, "0:0:0", "0")
    );

    // Test multiple hyphens and colons.
    assert_eq!(
        parseversion("0:0:0-0-0").unwrap(),
        dpkg_version_object!(0, "0:0-0", "0")
    );

    assert_eq!(
        parseversion("0:0-0:0-0").unwrap(),
        dpkg_version_object!(0, "0-0:0", "0")
    );
    // Test valid characters in upstream version
    assert_eq!(
        parseversion("0:09azAZ.-+~:-0").unwrap(),
        dpkg_version_object!(0, "09azAZ.-+~:", "0")
    );

    // Test valid characters in revision
    assert_eq!(
        parseversion("0:0-azAZ09.+~").unwrap(),
        dpkg_version_object!(0, "0", "azAZ09.+~")
    );

    // Test version with leading and trailing spaces.
    assert_eq!(
        parseversion("  	0:0-1").unwrap(),
        dpkg_version_object!(0, "0", "1")
    );
    assert_eq!(
        parseversion("0:0-1	  ").unwrap(),
        dpkg_version_object!(0, "0", "1")
    );
    assert_eq!(
        parseversion("	  0:0-1  	").unwrap(),
        dpkg_version_object!(0, "0", "1")
    );

    // Test empty version.
    assert_eq!(parseversion("").unwrap_err(), Error::EmptyVersion);
    assert_eq!(parseversion("  ").unwrap_err(), Error::EmptyVersion);

    // Test empty upstream version after epoch.
    assert_eq!(parseversion("0:").unwrap_err(), Error::NothingAfterColon);

    // Test empty epoch in version.
    assert_eq!(
        parseversion(":1.0").unwrap_err(),
        Error::InvalidEpoch("".to_string())
    );

    // Test empty revision in version.
    assert_eq!(parseversion("1.0-").unwrap_err(), Error::EmptyRevision);

    // Test version with embedded spaces.
    assert_eq!(parseversion("0:0 0-1").unwrap_err(), Error::EmbeddedSpaces);

    // Test version with negative epoch.
    assert_eq!(
        parseversion("-1:0-1").unwrap_err(),
        Error::InvalidEpoch("-1".to_string())
    );

    // Test version with huge epoch.
    assert_eq!(
        parseversion("999999999999999999999999:0-1").unwrap_err(),
        Error::InvalidEpoch("999999999999999999999999".to_string())
    );

    // Test invalid characters in epoch.
    assert_eq!(
        parseversion("a:0-0").unwrap_err(),
        Error::InvalidEpoch("a".to_string())
    );
    assert_eq!(
        parseversion("A:0-0").unwrap_err(),
        Error::InvalidEpoch("A".to_string())
    );

    // Test invalid empty upstream version.
    assert_eq!(parseversion("-0").unwrap_err(), Error::EmptyVersionNumber);
    assert_eq!(parseversion("0:-0").unwrap_err(), Error::EmptyVersionNumber);

    // Test upstream version not starting with a digit.
    assert_eq!(
        parseversion("0:abc3-0").unwrap_err(),
        Error::NonDigitVersion
    );

    // Test invalid characters in upstream version.
    let invalid_chars = "!#@$%&/|\\<>()[]{};,_=*^'";
    for c in invalid_chars.chars() {
        assert_eq!(
            parseversion(&format!("0:0{c}-0")).unwrap_err(),
            Error::InvalidCharacterInVersion
        );
    }

    // Test invalid characters in revision.
    assert_eq!(
        parseversion("0:0-0:0").unwrap_err(),
        Error::InvalidCharacterInRevision
    );
    let invalid_chars = "!#@$%&/|\\<>()[]{}:;,_=*^'";
    for c in invalid_chars.chars() {
        assert_eq!(
            parseversion(&format!("0:0-{c}")).unwrap_err(),
            Error::InvalidCharacterInRevision
        );
    }
}

// n.b. these tests are original and not ported from dpkg
#[test]
fn test_version_display() {
    assert_eq!(parseversion("1.0.0").unwrap().to_string(), "1.0.0",);
    assert_eq!(parseversion("1.0.0-1").unwrap().to_string(), "1.0.0-1",);
    assert_eq!(parseversion("1:1.0.0-1").unwrap().to_string(), "1:1.0.0-1",);

    // zero epoch is dropped
    assert_eq!(parseversion("0:1.0.0-1").unwrap().to_string(), "1.0.0-1",);
}