use std::str::FromStr; use serde::Serialize; use thiserror::Error; #[derive(Serialize, Debug)] pub struct Ulp(Option, Option, String); #[derive(Error, Debug)] pub enum ParseUlpError { #[error("incomplete ULP string")] Incomplete, } enum State { Url, Username(char), Password, } impl FromStr for Ulp { type Err = ParseUlpError; fn from_str(s: &str) -> Result { // intermediary let mut state = State::Url; let mut buf = String::new(); let mut port_buf = String::new(); // final let mut saved_url = None; let mut saved_username = None; let saved_password; let mut it = s.chars().peekable(); loop { let c = it.next(); match state { State::Url => match c { Some('|') => { state = State::Username('|'); saved_url = Some(buf.split_off(0)); } Some(':') if !matches!(buf.as_str(), "http" | "https") => { // at this point, we expect either a port // or the username after a colon // -- // we try to determine which it is // take all chars that could be // part of a port while matches!(it.peek(), Some('0'..='9')) { port_buf.push(it.next().unwrap()); } // peek next char for following checks let Some(nc) = it.peek() else { // done + change form: // * buf is username, port_buf is pw saved_password = port_buf; saved_username = Some(buf); break; }; // if it is surely a port, clear port buf and // continue on if matches!(nc, '/' | '|') { buf.push_str(&port_buf); port_buf.clear(); continue; } // if it could be either a port or a username, // keep for later and move on if matches!(nc, ':') && matches!(port_buf.as_str(), "443" | "80") { continue; } // if we didn't match anything yet, // we assume it is part of a username state = State::Username(':'); saved_url = Some(buf.split_off(0)); } Some(c) => buf.push(c), None => return Err(ParseUlpError::Incomplete), }, State::Username(sep) => match c { Some(c) if sep != c => buf.push(c), Some(_) => { state = State::Password; saved_username = Some(buf.split_off(0)); } None => { // done + change form: // * port_buf||url_buf is username, buf is password saved_password = buf; if port_buf.is_empty() { saved_username = saved_url; saved_url = None; } else { saved_username = Some(port_buf); } break; } }, State::Password => { if let Some(c) = c { buf.push(c); } else { // done: // * we can be sure port_buf is part of url if !port_buf.is_empty() { let saved_url = saved_url.as_mut().unwrap(); saved_url.push(':'); saved_url.push_str(&port_buf); } saved_password = buf; break; } } } } Ok(Ulp(saved_url, saved_username, saved_password)) } }