deb-version7 0.1.1

Parsing and comparing of Debian package versions
Documentation
// Rust port of lib/dpkg/parsehelp.c
//
// Copyright © 1995 Ian Jackson <ijackson@chiark.greenend.org.uk>
// Copyright © 2006-2012 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::{DebVersion, Error};

pub(crate) fn parseversion(string: &str) -> Result<DebVersion, Error> {
    if !is_ascii(string) {
        return Err(Error::NotAscii);
    }

    // Trim leading and trailing space.
    let string = string
        .trim_start_matches(|c: char| c.is_ascii_whitespace())
        .trim_end_matches(|c: char| c.is_ascii_whitespace());

    if string.is_empty() {
        return Err(Error::EmptyVersion);
    }

    if string.chars().any(|c| c.is_ascii_whitespace()) {
        return Err(Error::EmbeddedSpaces);
    }

    let (epoch, string) = if let Some(colon) = string.find(':') {
        let epoch_str = &string[..colon];
        let epoch: u32 = epoch_str
            .parse()
            .map_err(|_| Error::InvalidEpoch(epoch_str.to_string()))?;

        if string[colon + 1..].is_empty() {
            return Err(Error::NothingAfterColon);
        }

        (epoch, &string[colon + 1..])
    } else {
        (0, string)
    };

    let version = string;
    let (revision, version) = if let Some(hyphen) = version.rfind('-') {
        if version[hyphen + 1..].is_empty() {
            return Err(Error::EmptyRevision);
        }
        (Some(&version[hyphen + 1..]), &version[..hyphen])
    } else {
        (None, version)
    };

    if version.is_empty() {
        return Err(Error::EmptyVersionNumber);
    }

    // unwrap: safe because we checked for empty string above
    if !version.chars().next().unwrap().is_ascii_digit() {
        return Err(Error::NonDigitVersion);
    }

    if version.chars().any(|c| is_invalid_char(c, ".-+~:")) {
        return Err(Error::InvalidCharacterInVersion);
    }

    if let Some(revision) = revision {
        if revision.chars().any(|c| is_invalid_char(c, ".+~")) {
            return Err(Error::InvalidCharacterInRevision);
        }
    }

    Ok(DebVersion {
        epoch,
        version: version.to_string(),
        revision: revision.map(|r| r.to_string()),
    })
}

fn is_invalid_char(char: char, set: &str) -> bool {
    !char.is_ascii_digit() && !char.is_ascii_alphanumeric() && !set.contains(char)
}

fn is_ascii(input: &str) -> bool {
    input.chars().all(|c| c.is_ascii())
}