Final cleanup before hardware testing. No behaviour changes; 248 tests green, clippy clean. #1 AppError newtype (http-api/src/error.rs) with one IntoResponse mapping (NotFound→404, Invalid→400, _→500) + From<core::Error>/From<io::Error>. Converted the clearly-safe handlers (sso_put, unattended_upload, branding_clear) to `?`; intentionally left handlers with bespoke status semantics (Invalid→404 on category, 409 on duplicate share / open upload) explicit so no asserted status changes. #2 figment-based Config::load (defaults → TOML → env). Keeps the historical flat OPENPXE_* names (Unraid/entrypoint compatible) AND adds the nested OPENPXE_SECTION__FIELD form; now covers every field (apply_env had silently skipped unattended_dir + bind addrs). 6 Jail tests prove backward-compat. Removed the hand-rolled apply_env. #3 thiserror 1→2; dropped unused mime/mime_guess/once_cell deps. #4 Re-evaluated: Duration::from_hours/from_mins are stable on the pinned 1.95 toolchain and clippy prefers them — kept the readable form (the "unstable" premise didn't hold; MSRV is intentionally 1.95). #5 insta snapshot of the rendered iPXE menu (version-filtered) + wiremock coverage of the SAML metadata-URL fetch (200 + non-2xx). #6 api_status → typed StatusResponse struct (was a 25-key json! blob) with a full_flow guard test asserting every UI key + the started_at string shape. Deferred the /api/docs typed conversion (lowest value, highest churn, zero functional benefit). #7 pct_encode/xml_escape de-duplicated into openpxe_core::encoding (were copied across app.rs + the SAML modules). No new crates. #8 UploadSessions registry → parking_lot::RwLock (sync, never held across .await); per-session lock stays tokio::Mutex. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
66 lines
2.1 KiB
Rust
66 lines
2.1 KiB
Rust
//! 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<c>\"d'e"), "a&b<c>"d'e");
|
|
assert_eq!(xml_escape("plain text"), "plain text");
|
|
}
|
|
}
|