threecpio 0.14.0

manage initrd cpio archives
Documentation
// Copyright (C) 2025-2026, Benjamin Drung <bdrung@posteo.de>
// SPDX-License-Identifier: ISC

use std::num::ParseIntError;
use std::ops::{RangeFrom, RangeInclusive, RangeTo};
use std::str::FromStr;

#[derive(Clone, Debug, PartialEq)]
struct Range {
    start: Option<i32>,
    end: Option<i32>,
}

impl Range {
    fn new(start: Option<i32>, end: Option<i32>) -> Self {
        Self { start, end }
    }

    fn contains(&self, item: &i32) -> bool {
        if let Some(start) = self.start {
            if *item < start {
                return false;
            }
        }
        if let Some(end) = self.end {
            if *item > end {
                return false;
            }
        }
        true
    }

    fn has_more(&self, item: &i32) -> bool {
        self.end.is_none_or(|end| end > *item)
    }
}

impl From<RangeInclusive<i32>> for Range {
    fn from(item: RangeInclusive<i32>) -> Self {
        Self {
            start: Some(*item.start()),
            end: Some(*item.end()),
        }
    }
}

impl From<RangeFrom<i32>> for Range {
    fn from(item: RangeFrom<i32>) -> Self {
        Self {
            start: Some(item.start),
            end: None,
        }
    }
}

impl From<RangeTo<i32>> for Range {
    fn from(item: RangeTo<i32>) -> Self {
        Self {
            start: None,
            end: Some(item.end),
        }
    }
}

/// An array of ranges.
///
/// Each range can either be
/// * bounded inclusively below and above (`start-end`),
/// * bounded inclusively below (`start-`), or
/// * bounded exclusively above (`-end`).
#[derive(Clone, Debug, PartialEq)]
pub struct Ranges(Vec<Range>);

impl Ranges {
    #[cfg(test)]
    fn new(ranges: Vec<Range>) -> Self {
        Self(ranges)
    }

    /// Returns `true` if `item` is contained in at least of of the ranges.
    ///
    /// # Examples
    ///
    /// ```
    /// use threecpio::ranges::Ranges;
    ///
    /// assert!(!"3-4".parse::<Ranges>().unwrap().contains(&2));
    /// assert!( "3-4".parse::<Ranges>().unwrap().contains(&3));
    /// assert!( "3-4".parse::<Ranges>().unwrap().contains(&4));
    /// assert!(!"3-4".parse::<Ranges>().unwrap().contains(&5));
    /// ```
    pub fn contains(&self, item: &i32) -> bool {
        for range in &self.0 {
            if range.contains(item) {
                return true;
            }
        }
        false
    }

    /// Returns `true` if `Ranges` contain items higher than `item`.
    ///
    /// # Examples
    ///
    /// ```
    /// use threecpio::ranges::Ranges;
    ///
    /// assert!( "2-4".parse::<Ranges>().unwrap().has_more(&3));
    /// assert!(!"2-4".parse::<Ranges>().unwrap().has_more(&4));
    /// ```
    ///
    /// Ranges bounded inclusively below will cause `has_more` to always
    /// return `true`:
    ///
    /// ```
    /// use threecpio::ranges::Ranges;
    ///
    /// assert!("3-".parse::<Ranges>().unwrap().has_more(&9000));
    /// ```
    pub fn has_more(&self, item: &i32) -> bool {
        for range in &self.0 {
            if range.has_more(item) {
                return true;
            }
        }
        false
    }
}

impl FromStr for Ranges {
    type Err = ParseIntError;

    /// Parses a string `s` to return `Ranges`.
    ///
    /// `s` is made up of one range, or many ranges separated by commas.
    /// Each range can either be
    /// * one single item (`item`),
    /// * bounded inclusively below and above (`start-end`),
    /// * bounded inclusively below (`start-`), or
    /// * bounded exclusively above (`-end`).
    ///
    /// # Examples
    ///
    /// ```
    /// use threecpio::ranges::Ranges;
    ///
    /// assert!("1-3,5,7-".parse::<Ranges>().is_ok());
    /// ```
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut ranges = Vec::new();
        for range_str in s.split(",") {
            if let Some((start, end)) = range_str.split_once("-") {
                let start = if start.is_empty() {
                    None
                } else {
                    Some(start.parse()?)
                };
                let end = if end.is_empty() {
                    None
                } else {
                    Some(end.parse()?)
                };
                ranges.push(Range::new(start, end));
            } else {
                let start = range_str.parse()?;
                ranges.push(Range::new(Some(start), Some(start)));
            }
        }
        Ok(Self(ranges))
    }
}

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

    #[test]
    fn test_parse_ranges_error_single() {
        for s in ["str", "1-str", "str-5"] {
            let got = s.parse::<Ranges>().unwrap_err();
            assert_eq!(got.to_string(), "invalid digit found in string");
        }
    }

    #[test]
    fn test_parse_ranges_single() {
        assert_eq!("3".parse::<Ranges>(), Ok(Ranges::new(vec![(3..=3).into()])))
    }

    #[test]
    fn test_parse_ranges_range() {
        assert_eq!(
            "2-4".parse::<Ranges>(),
            Ok(Ranges::new(vec![(2..=4).into()]))
        )
    }

    #[test]
    fn test_parse_ranges_multiple() {
        assert_eq!(
            "1,3-5".parse::<Ranges>(),
            Ok(Ranges::new(vec![(1..=1).into(), (3..=5).into()]))
        )
    }

    #[test]
    fn test_parse_ranges_open_end() {
        assert_eq!("2-".parse::<Ranges>(), Ok(Ranges::new(vec![(2..).into()])))
    }

    #[test]
    fn test_parse_ranges_open_start() {
        assert_eq!("-4".parse::<Ranges>(), Ok(Ranges::new(vec![(..4).into()])))
    }

    #[test]
    fn test_ranges_contains() {
        let ranges = "1-3,5".parse::<Ranges>().unwrap();
        assert!(ranges.contains(&2));
        assert!(!ranges.contains(&4));
    }

    #[test]
    fn test_ranges_has_more() {
        let ranges = "4-5,7,-2".parse::<Ranges>().unwrap();
        assert!(ranges.has_more(&6));
        assert!(!ranges.has_more(&7));
    }
}