pub use crate::runtime_parser::MAX_STDLIB_COUNT;
pub use crate::runtime_parser::MAX_SKADIS_PANEL_MM;
pub const MAX_NODE_EXPANSION: u64 = 10_000;
pub const MIN_PITCH_MM: f32 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceLimitError {
pub kind: &'static str,
pub limit: u64,
pub requested: u64,
}
impl std::fmt::Display for ResourceLimitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"resource limit exceeded: kind={} limit={} requested={}",
self.kind, self.limit, self.requested
)
}
}
impl std::error::Error for ResourceLimitError {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FloatLimitError {
pub kind: &'static str,
pub limit: f32,
pub value: f32,
pub unit: &'static str,
}
impl std::fmt::Display for FloatLimitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"resource limit exceeded: kind={} limit={} {} value={} {}",
self.kind, self.limit, self.unit, self.value, self.unit
)?;
if self.kind == "rotation_quat_unit" {
write!(
f,
" (rotate_q needs about 8 significant digits per component to land \
inside this tolerance; for a hand-written rotation, use rotate(x, y, z) \
in degrees instead -- rotate_q is meant for a machine-computed exact \
quaternion, not one typed by hand)"
)?;
}
Ok(())
}
}
impl std::error::Error for FloatLimitError {}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum SpecError {
ResourceLimit(ResourceLimitError),
FloatLimit(FloatLimitError),
}
impl From<ResourceLimitError> for SpecError {
fn from(e: ResourceLimitError) -> Self {
Self::ResourceLimit(e)
}
}
impl From<FloatLimitError> for SpecError {
fn from(e: FloatLimitError) -> Self {
Self::FloatLimit(e)
}
}
impl std::fmt::Display for SpecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ResourceLimit(e) => write!(f, "{e}"),
Self::FloatLimit(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for SpecError {}
pub fn checked_product(
a: u32,
b: u32,
limit: u64,
kind: &'static str,
) -> Result<u64, ResourceLimitError> {
let product = u64::from(a) * u64::from(b);
if product > limit {
return Err(ResourceLimitError {
kind,
limit,
requested: product,
});
}
Ok(product)
}
pub fn checked_positive_finite(
v: f32,
floor: f32,
kind: &'static str,
) -> Result<f32, FloatLimitError> {
if !v.is_finite() || v < floor {
return Err(FloatLimitError {
kind,
limit: floor,
value: v,
unit: "mm",
});
}
Ok(v)
}
pub fn checked_bounded(
v: f32,
floor: f32,
ceiling: f32,
kind: &'static str,
) -> Result<f32, FloatLimitError> {
if !(v.is_finite() && v > floor && v <= ceiling) {
return Err(FloatLimitError {
kind,
limit: ceiling,
value: v,
unit: "mm",
});
}
Ok(v)
}
pub fn checked_tpms_fields(scale: f32, thickness: f32) -> Result<(f32, f32), SpecError> {
if !scale.is_finite() || scale <= 0.0 {
return Err(FloatLimitError {
kind: "tpms_scale",
limit: 0.0,
value: scale,
unit: "mm",
}
.into());
}
let thickness = checked_positive_finite(thickness, f32::NEG_INFINITY, "tpms_thickness")?;
Ok((scale, thickness))
}
pub const ROTATION_QUAT_UNIT_TOLERANCE: f32 = 8.0 * f32::EPSILON;
pub fn checked_rotation_quat(x: f32, y: f32, z: f32, w: f32) -> Result<glam::Quat, SpecError> {
if !(x.is_finite() && y.is_finite() && z.is_finite() && w.is_finite()) {
return Err(FloatLimitError {
kind: "rotation_quat_finite",
limit: 0.0,
value: if x.is_finite() {
if y.is_finite() {
if z.is_finite() {
w
} else {
z
}
} else {
y
}
} else {
x
},
unit: "unitless",
}
.into());
}
let mag2 = f64::from(x).mul_add(
f64::from(x),
f64::from(y).mul_add(
f64::from(y),
f64::from(z).mul_add(f64::from(z), f64::from(w) * f64::from(w)),
),
);
let dev = (mag2 - 1.0).abs();
if dev > f64::from(ROTATION_QUAT_UNIT_TOLERANCE) {
#[allow(clippy::cast_possible_truncation)]
let dev_f32 = dev as f32;
return Err(FloatLimitError {
kind: "rotation_quat_unit",
limit: ROTATION_QUAT_UNIT_TOLERANCE,
value: dev_f32,
unit: "|q|^2-1",
}
.into());
}
Ok(glam::Quat::from_xyzw(x, y, z, w))
}
const ROTATION_QUAT_BOUNDARY_INSIDE_W: u32 = 0x3f80_0003;
const ROTATION_QUAT_BOUNDARY_OUTSIDE_W: u32 = 0x3f80_0004;
pub(crate) const ROTATION_QUAT_BOUNDARY_PROBE_INPUTS: &[(f32, f32, f32, f32)] = &[
(0.0, 0.0, 0.0, 1.0),
(0.6, 0.8, 0.0, 0.0),
(
0.0,
0.0,
0.0,
f32::from_bits(ROTATION_QUAT_BOUNDARY_INSIDE_W),
),
(
0.0,
0.0,
0.0,
f32::from_bits(ROTATION_QUAT_BOUNDARY_OUTSIDE_W),
),
(0.0, 0.0, 0.0, 0.0),
(2.0, 0.0, 0.0, 0.0),
(1.0, 1.0, 0.0, 0.0),
];
pub(crate) const TPMS_VALIDITY_PROBE_INPUTS: &[(f32, f32)] = &[
(-0.0, 0.1),
(0.0, 0.1),
(-1.0, 0.1),
(f32::NAN, 0.1),
(f32::INFINITY, 0.1),
(f32::NEG_INFINITY, 0.1),
(-5e-7, 0.1),
(3.0, f32::NAN),
(3.0, f32::INFINITY),
(3.0, f32::NEG_INFINITY),
(1e-6, 0.1),
(3.0, 0.1),
(3.0, -5.0),
(1e-45, 0.1),
(1e-40, 0.1),
(1e-10, 0.1),
(5e-7, 0.1),
];
#[cfg(test)]
mod tests {
use super::{
checked_bounded, checked_positive_finite, checked_product, checked_rotation_quat,
checked_tpms_fields, SpecError, MAX_NODE_EXPANSION, MIN_PITCH_MM,
};
#[test]
fn checked_product_within_limit_is_ok() {
assert_eq!(
checked_product(2, 2, MAX_NODE_EXPANSION, "grid_expansion"),
Ok(4)
);
}
#[test]
fn checked_product_at_the_limit_is_ok() {
assert_eq!(
checked_product(100, 100, 10_000, "grid_expansion"),
Ok(10_000)
);
}
#[test]
fn checked_product_one_over_the_limit_errs() {
let e = checked_product(100, 101, 10_000, "grid_expansion").unwrap_err();
assert_eq!(e.kind, "grid_expansion");
assert_eq!(e.limit, 10_000);
assert_eq!(e.requested, 10_100);
}
#[test]
fn checked_product_cannot_overflow_even_at_u32_max() {
let e =
checked_product(u32::MAX, u32::MAX, MAX_NODE_EXPANSION, "grid_expansion").unwrap_err();
assert_eq!(e.requested, u64::from(u32::MAX) * u64::from(u32::MAX));
}
#[test]
fn checked_positive_finite_above_floor_is_ok() {
assert_eq!(
checked_positive_finite(20.0, MIN_PITCH_MM, "pitch"),
Ok(20.0)
);
}
#[test]
fn checked_positive_finite_exactly_at_floor_is_ok() {
assert_eq!(
checked_positive_finite(MIN_PITCH_MM, MIN_PITCH_MM, "pitch"),
Ok(MIN_PITCH_MM)
);
}
#[test]
#[allow(clippy::float_cmp)] fn checked_positive_finite_zero_errs_and_value_is_literally_zero() {
let e = checked_positive_finite(0.0, MIN_PITCH_MM, "pitch").unwrap_err();
assert_eq!(e.kind, "pitch");
assert_eq!(e.value, 0.0);
}
#[test]
#[allow(clippy::float_cmp)] fn checked_positive_finite_negative_errs_and_value_is_the_literal_negative() {
let e = checked_positive_finite(-5.0, MIN_PITCH_MM, "pitch").unwrap_err();
assert_eq!(e.value, -5.0);
}
#[test]
fn checked_positive_finite_nan_errs_and_value_is_distinguishable_from_negative() {
let e = checked_positive_finite(f32::NAN, MIN_PITCH_MM, "pitch").unwrap_err();
assert!(e.value.is_nan());
assert!(e.to_string().contains("NaN"), "{}", e.to_string());
let neg = checked_positive_finite(-5.0, MIN_PITCH_MM, "pitch").unwrap_err();
assert_ne!(e.to_string(), neg.to_string());
}
#[test]
fn checked_positive_finite_infinite_errs_and_value_is_literally_infinite() {
let e = checked_positive_finite(f32::INFINITY, MIN_PITCH_MM, "pitch").unwrap_err();
assert!(e.value.is_infinite() && e.value > 0.0);
assert!(e.to_string().contains("inf"), "{}", e.to_string());
}
#[test]
fn checked_positive_finite_just_below_floor_errs() {
let below = MIN_PITCH_MM - 0.001;
assert!(checked_positive_finite(below, MIN_PITCH_MM, "pitch").is_err());
}
#[test]
fn checked_bounded_within_range_is_ok() {
assert_eq!(
checked_bounded(1000.0, 0.0, 2000.0, "panel_size"),
Ok(1000.0)
);
}
#[test]
fn checked_bounded_at_the_ceiling_is_ok() {
assert_eq!(
checked_bounded(2000.0, 0.0, 2000.0, "panel_size"),
Ok(2000.0)
);
}
#[test]
fn checked_bounded_at_the_floor_errs() {
assert!(checked_bounded(0.0, 0.0, 2000.0, "panel_size").is_err());
}
#[test]
fn checked_bounded_nan_errs_without_hanging() {
let e = checked_bounded(f32::NAN, 0.0, 2000.0, "panel_size").unwrap_err();
assert_eq!(e.kind, "panel_size");
assert!(e.value.is_nan());
}
#[test]
fn checked_bounded_infinite_errs() {
let e = checked_bounded(f32::INFINITY, 0.0, 2000.0, "panel_size").unwrap_err();
assert!(e.value.is_infinite() && e.value > 0.0);
}
#[test]
#[allow(clippy::float_cmp)] fn checked_bounded_over_ceiling_reports_the_actual_request_not_a_placeholder() {
let e = checked_bounded(50_000.0, 0.0, 2000.0, "panel_size").unwrap_err();
assert_eq!(e.limit, 2000.0);
assert_eq!(e.value, 50_000.0);
}
#[test]
#[allow(clippy::float_cmp)] fn checked_bounded_negative_errs_and_value_is_distinguishable_from_nan() {
let e = checked_bounded(-5.0, 0.0, 2000.0, "panel_size").unwrap_err();
assert_eq!(e.value, -5.0);
let nan = checked_bounded(f32::NAN, 0.0, 2000.0, "panel_size").unwrap_err();
assert_ne!(e.to_string(), nan.to_string());
}
#[test]
fn spec_error_from_resource_limit_error_displays_the_same_text() {
let e = checked_product(1000, 1000, MAX_NODE_EXPANSION, "grid_expansion").unwrap_err();
let spec_err: SpecError = e.into();
assert_eq!(spec_err.to_string(), e.to_string());
}
#[test]
fn spec_error_from_float_limit_error_displays_the_same_text() {
let e = checked_bounded(50_000.0, 0.0, 2000.0, "panel_size").unwrap_err();
let spec_err: SpecError = e.into();
assert_eq!(spec_err.to_string(), e.to_string());
}
#[test]
fn resource_limit_error_display_names_all_three_fields() {
let e = checked_product(1000, 1000, MAX_NODE_EXPANSION, "grid_expansion").unwrap_err();
let s = e.to_string();
assert!(s.contains("grid_expansion"), "{s}");
assert!(s.contains(&MAX_NODE_EXPANSION.to_string()), "{s}");
assert!(s.contains("1000000"), "{s}");
}
#[test]
fn float_limit_error_display_names_kind_and_unit() {
let e = checked_bounded(50_000.0, 0.0, 2000.0, "panel_size").unwrap_err();
let s = e.to_string();
assert!(s.contains("panel_size"), "{s}");
assert!(s.contains("mm"), "{s}");
assert!(s.contains("50000"), "{s}");
}
#[test]
fn checked_tpms_fields_rejects_a_degenerate_scale() {
for scale in [
-0.0,
0.0,
-1.0,
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
-5e-7,
] {
assert!(
checked_tpms_fields(scale, 0.1).is_err(),
"scale={scale} should be rejected"
);
}
}
#[test]
fn checked_tpms_fields_rejects_a_non_finite_thickness() {
for thickness in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert!(
checked_tpms_fields(3.0, thickness).is_err(),
"thickness={thickness} should be rejected"
);
}
assert!(checked_tpms_fields(3.0, -5.0).is_ok());
}
#[test]
fn checked_tpms_fields_accepts_the_threshold_and_ordinary_values() {
assert_eq!(checked_tpms_fields(1e-6, 0.1).unwrap(), (1e-6, 0.1));
assert_eq!(checked_tpms_fields(3.0, 0.1).unwrap(), (3.0, 0.1));
}
#[test]
fn checked_tpms_fields_accepts_any_finite_positive_scale_including_subnormals() {
for scale in [1e-45_f32, 1e-40, 1e-10, 5e-7] {
assert!(scale > 0.0, "test input itself must be positive: {scale}");
assert_eq!(
checked_tpms_fields(scale, 0.1).unwrap(),
(scale, 0.1),
"scale={scale} should be accepted"
);
}
}
#[test]
fn checked_rotation_quat_rejects_non_finite_components() {
for (x, y, z, w) in [
(f32::NAN, 0.0, 0.0, 1.0),
(0.0, f32::INFINITY, 0.0, 1.0),
(0.0, 0.0, f32::NEG_INFINITY, 1.0),
(0.0, 0.0, 0.0, f32::NAN),
] {
assert!(
checked_rotation_quat(x, y, z, w).is_err(),
"({x},{y},{z},{w}) should be rejected (non-finite)"
);
}
}
#[test]
fn checked_rotation_quat_accepts_the_identity_and_an_ordinary_unit_quat() {
let q = checked_rotation_quat(0.0, 0.0, 0.0, 1.0).unwrap();
assert_eq!((q.x, q.y, q.z, q.w), (0.0, 0.0, 0.0, 1.0));
let q = checked_rotation_quat(0.6, 0.8, 0.0, 0.0).unwrap();
assert_eq!((q.x, q.y, q.z, q.w), (0.6, 0.8, 0.0, 0.0));
}
#[test]
fn checked_rotation_quat_rejects_a_clearly_non_unit_quat() {
for (x, y, z, w) in [
(0.0_f32, 0.0, 0.0, 0.0),
(2.0, 0.0, 0.0, 0.0),
(1.0, 1.0, 0.0, 0.0),
] {
assert!(
checked_rotation_quat(x, y, z, w).is_err(),
"({x},{y},{z},{w}) is far from unit and should be rejected"
);
}
}
#[test]
fn checked_rotation_quat_error_message_explains_the_digit_requirement() {
let e = checked_rotation_quat(1.0, 1.0, 0.0, 0.0).unwrap_err();
let s = e.to_string();
assert!(s.contains("significant digits"), "{s}");
assert!(s.contains("rotate(x, y, z)"), "{s}");
}
#[test]
fn checked_rotation_quat_boundary_is_exact() {
let w_inside = f32::from_bits(0x3f80_0003);
let w_outside = f32::from_bits(0x3f80_0004);
assert!(
checked_rotation_quat(0.0, 0.0, 0.0, w_inside).is_ok(),
"w={w_inside:?} (|q|^2-1 just inside tolerance) should be accepted"
);
assert!(
checked_rotation_quat(0.0, 0.0, 0.0, w_outside).is_err(),
"w={w_outside:?} (|q|^2-1 just outside tolerance) should be rejected"
);
}
#[test]
fn checked_rotation_quat_does_not_renormalise() {
let (qx, qy, qz, qw) = (0.0_f32, 0.0, 0.0, f32::from_bits(0x3f80_0003));
let q = checked_rotation_quat(qx, qy, qz, qw).unwrap();
assert_eq!(q.x.to_bits(), qx.to_bits());
assert_eq!(q.y.to_bits(), qy.to_bits());
assert_eq!(q.z.to_bits(), qz.to_bits());
assert_eq!(q.w.to_bits(), qw.to_bits());
}
}