toe-beans 0.12.0

DHCP library, client, and server
Documentation
use crate::v4::error::Result;
use jiff::Zoned;
use mac_address::MacAddress;
use std::fmt::Display;
use std::net::Ipv4Addr;
use std::str::FromStr;

/// How much space is used by each lease in the leases file.
pub(super) const PAGE_SIZE: usize = 200;

#[derive(Clone, Debug, PartialEq)]
pub(super) enum LeaseStatus {
    Offered,
    Acked,
}

impl Display for LeaseStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LeaseStatus::Offered => write!(f, "Offered"),
            LeaseStatus::Acked => write!(f, "Acked"),
        }
    }
}

impl From<&str> for LeaseStatus {
    fn from(value: &str) -> Self {
        match value {
            "Offered" => Self::Offered,
            "Acked" => Self::Acked,
            _ => panic!("Unknown value used for LeaseStatus"),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub(super) struct Lease {
    /// What line in the leases file this lease occupies.
    /// Where 0 indicates it has not been assigned a line yet.
    pub(super) line: u32,
    /// What ip address was assigned
    pub(super) ip: Ipv4Addr,
    /// Whether the lease is offered or acked
    pub(super) status: LeaseStatus,
    /// Whether the lease is on the static leases list
    pub(super) is_static: bool,
    /// A UTC time with time zone for when the lease was assigned/extended.
    pub(super) when: Zoned,
}

impl Lease {
    #[inline]
    pub(super) fn new(ip: Ipv4Addr, status: LeaseStatus, is_static: bool) -> Self {
        Self {
            line: 0,
            ip,
            status,
            is_static,
            when: Zoned::now(),
        }
    }

    /// Get back the entry that maps the mac address to the lease
    /// The passed string must exactly match the expected format.
    pub(super) fn from_string(string: String, line: u32) -> Result<(MacAddress, Lease)> {
        let fields: Vec<&str> = string.split(',').collect();

        if fields.len() != 5 {
            return Err("Invalid number of fields in lease file");
        }

        // UNWRAP checked length above
        let fields: [&str; 5] = fields.try_into().unwrap();
        let [mac_str, ip_str, status_str, static_str, when_str] = fields;

        // split returns a str with PAGE_SIZE null bytes at the beginning.
        let owner = MacAddress::from_str(mac_str.trim_start_matches('\0'))
            .expect("Problem parsing lease mac address");

        let ip = Ipv4Addr::from_str(ip_str).expect("Problem parsing lease ip address");
        let status = LeaseStatus::from(status_str);
        let is_static = static_str
            .parse()
            .expect("Problem parsing lease is_static boolean");

        // The last value might have trailing spaces
        let when = Zoned::from_str(when_str.trim_end()).expect("Problem parsing lease time");

        let lease = Self {
            line,
            ip,
            status,
            is_static,
            when,
        };
        Ok((owner, lease))
    }

    /// Resets the time that this was considered leased.
    #[inline]
    pub(super) fn extend(&mut self) {
        self.when = Zoned::now();
    }

    /// If the elapsed time since the assignment of the lease exceeds
    /// the allowed lease time then the lease is expired.
    pub(super) fn is_expired(&self, lease_time: u32) -> bool {
        let elapsed = self.when.duration_until(&Zoned::now());
        elapsed.as_secs() >= lease_time as i64
    }

    pub(super) fn to_string(&self, owner: MacAddress) -> String {
        // Some parts of this string are variable, but let's assume 100 for now.
        let mut string = String::with_capacity(PAGE_SIZE);

        string.push_str(&owner.to_string());
        string.push(',');
        string.push_str(&self.ip.to_string()); // variable
        string.push(',');
        string.push_str(&self.status.to_string()); // variable
        string.push(',');
        string.push_str(&self.is_static.to_string()); // variable
        string.push(',');
        string.push_str(&self.when.to_string()); // variable

        string
    }
}

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

    #[test]
    fn test_is_expired() {
        let lease = Lease {
            line: 0,
            ip: Ipv4Addr::UNSPECIFIED,
            status: LeaseStatus::Offered,
            is_static: false,
            // acquired lease yesterday
            when: Zoned::yesterday(&Zoned::now()).expect("Problem getting yesterday"),
        };
        let lease_time = 120; // can only acquire lease for 2 minutes

        assert_eq!(lease.is_expired(lease_time), true);
    }

    #[test]
    fn test_not_is_expired() {
        let lease = Lease {
            line: 0,
            ip: Ipv4Addr::UNSPECIFIED,
            status: LeaseStatus::Offered,
            is_static: false,
            // acquired lease yesterday
            when: Zoned::yesterday(&Zoned::now()).expect("Problem getting yesterday"),
        };
        let lease_time = 172800; // can only acquire lease for 48 hours

        assert_eq!(lease.is_expired(lease_time), false);
    }

    #[test]
    fn test_lease_to_string() {
        let lease = Lease {
            line: 0,
            ip: Ipv4Addr::UNSPECIFIED,
            status: LeaseStatus::Offered,
            is_static: false,
            when: jiff::civil::date(2026, 1, 1)
                .at(12, 0, 0, 0)
                .in_tz("America/New_York")
                .unwrap(),
        };
        let mac_address = MacAddress::new([0x01, 0x02, 0x03, 0x0A, 0x0B, 0x0C]);
        let expected_string =
            "01:02:03:0A:0B:0C,0.0.0.0,Offered,false,2026-01-01T12:00:00-05:00[America/New_York]";

        assert_eq!(&lease.to_string(mac_address), expected_string);
    }

    #[test]
    fn test_lease_from_string() {
        let expected_lease = Lease {
            line: 0,
            ip: Ipv4Addr::UNSPECIFIED,
            status: LeaseStatus::Offered,
            is_static: false,
            when: jiff::civil::date(2026, 1, 1)
                .at(12, 0, 0, 0)
                .in_tz("America/New_York")
                .unwrap(),
        };
        let expected_mac_address = MacAddress::new([0x01, 0x02, 0x03, 0x0A, 0x0B, 0x0C]);

        let lease_string =
            "01:02:03:0A:0B:0C,0.0.0.0,Offered,false,2026-01-01T12:00:00-05:00[America/New_York]"
                .to_string();

        let (parsed_mac_address, parsed_lease) =
            Lease::from_string(lease_string, 0).expect("Problem parsing lease from string");

        assert_eq!(parsed_mac_address, expected_mac_address);
        assert_eq!(parsed_lease, expected_lease);
    }
}