jamjam 0.3.0

Handles JAM, PCBOARD message bases & QWK packets.
Documentation
use std::{fmt, str::FromStr};

#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EchomailAddress {
    pub zone: u16,
    pub net: u16,
    pub node: u16,
    pub point: u16,
}

impl EchomailAddress {
    pub fn new(zone: u16, net: u16, node: u16, point: u16) -> Self {
        EchomailAddress {
            zone,
            net,
            node,
            point,
        }
    }

    pub fn parse(input: &str) -> Option<Self> {
        let mut state = EchoParser::Zone;
        let mut result = EchomailAddress::default();
        let mut got_number = false;
        for c in input.chars() {
            match state {
                EchoParser::Zone => {
                    if c == ':' {
                        state = EchoParser::Net;
                        if !got_number {
                            return None;
                        }
                        got_number = false;
                        continue;
                    }
                    if c.is_ascii_digit() {
                        if let Some(next) = result
                            .zone
                            .checked_mul(10)
                            .and_then(|z| z.checked_add((c as u8 - b'0') as u16))
                        {
                            result.zone = next;
                        } else {
                            return None;
                        }
                        got_number = true;
                    } else {
                        return None;
                    }
                }
                EchoParser::Net => {
                    if c == '/' {
                        state = EchoParser::Node;
                        if !got_number {
                            return None;
                        }
                        got_number = false;
                        continue;
                    }
                    if c.is_ascii_digit() {
                        if let Some(next) = result
                            .net
                            .checked_mul(10)
                            .and_then(|z| z.checked_add((c as u8 - b'0') as u16))
                        {
                            result.net = next;
                        } else {
                            return None;
                        }
                        got_number = true;
                    } else {
                        return None;
                    }
                }
                EchoParser::Node => {
                    if c == '.' {
                        state = EchoParser::Point;
                        if !got_number {
                            return None;
                        }
                        got_number = false;
                        continue;
                    }

                    if c.is_ascii_digit() {
                        if let Some(next) = result
                            .node
                            .checked_mul(10)
                            .and_then(|z| z.checked_add((c as u8 - b'0') as u16))
                        {
                            result.node = next;
                        } else {
                            return None;
                        }
                        got_number = true;
                    } else {
                        return None;
                    }
                }
                EchoParser::Point => {
                    if c == '.' {
                        return None;
                    }
                    if c.is_ascii_digit() {
                        if let Some(next) = result
                            .point
                            .checked_mul(10)
                            .and_then(|z| z.checked_add((c as u8 - b'0') as u16))
                        {
                            result.point = next;
                        } else {
                            return None;
                        }
                        got_number = true;
                    } else {
                        return None;
                    }
                }
            }
        }

        if got_number && (state == EchoParser::Point || state == EchoParser::Node) {
            Some(result)
        } else {
            None
        }
    }
}

#[derive(Debug, PartialEq)]
enum EchoParser {
    Zone,
    Net,
    Node,
    Point,
}

impl fmt::Display for EchomailAddress {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.point == 0 {
            return write!(f, "{}:{}/{}", self.zone, self.net, self.node);
        }
        write!(f, "{}:{}/{}.{}", self.zone, self.net, self.node, self.point)
    }
}

impl FromStr for EchomailAddress {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s).ok_or_else(|| format!("'{}' is not a zone:net/node[.point] address", s))
    }
}