//! Small, dependency-free encoding helpers shared across crates. //! //! v0.5.4: `pct_encode` and `xml_escape` were duplicated in the SAML //! modules and the HTTP layer; they live here now. They're deliberately //! hand-rolled rather than pulling in `percent-encoding` / `url`: the //! unreserved set below is exactly the RFC 3986 set that iPXE's //! `:uristring` modifier and the SAML HTTP-Redirect binding both expect, //! and a general-purpose URL crate escapes a different set. use std::fmt::Write as _; /// Percent-encode `s` per RFC 3986: the unreserved set /// (`A-Z` `a-z` `0-9` `-` `_` `.` `~`) passes through unchanged; every /// other byte becomes `%XX` (uppercase hex). #[must_use] pub fn pct_encode(s: &str) -> String { let mut out = String::with_capacity(s.len()); for b in s.bytes() { match b { b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { out.push(b as char); } _ => { let _ = write!(out, "%{b:02X}"); } } } out } /// Escape the five XML predefined entities so `s` is safe inside element /// text or a double-quoted attribute value. #[must_use] pub fn xml_escape(s: &str) -> String { let mut out = String::with_capacity(s.len()); for c in s.chars() { match c { '&' => out.push_str("&"), '<' => out.push_str("<"), '>' => out.push_str(">"), '"' => out.push_str("""), '\'' => out.push_str("'"), _ => out.push(c), } } out } #[cfg(test)] mod tests { use super::*; #[test] fn pct_encode_unreserved_passthrough_else_hex() { assert_eq!(pct_encode("node-7.lab_1~"), "node-7.lab_1~"); assert_eq!(pct_encode("aa:bb cc/?&="), "aa%3Abb%20cc%2F%3F%26%3D"); assert_eq!(pct_encode(""), ""); } #[test] fn xml_escape_all_five_entities() { assert_eq!(xml_escape("a&b\"d'e"), "a&b<c>"d'e"); assert_eq!(xml_escape("plain text"), "plain text"); } }