Files
servo/components/config/pref_util.rs
Euclid Ye b62dc72c73 config: Fix panic when converting convertible PrefValue::Int to u64 (#44079)
The macro `impl_from_pref!` would cause panic in this case.

Testing: Add more test cases to existing UT.
Fixes: #44078

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
2026-04-10 15:51:07 +00:00

173 lines
5.2 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// The types of preference values in Servo.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum PrefValue {
Float(f64),
Int(i64),
UInt(u64),
Str(String),
Bool(bool),
Array(Vec<PrefValue>),
}
impl PrefValue {
/// Parse the `input` string as a preference value. Defaults to a `PrefValue::Str` if the input
/// cannot be parsed as valid value of one of the other types.
pub fn from_booleanish_str(input: &str) -> Self {
match input {
"false" => PrefValue::Bool(false),
"true" => PrefValue::Bool(true),
_ => input
.parse::<i64>()
.map(PrefValue::Int)
.or_else(|_| input.parse::<f64>().map(PrefValue::Float))
.unwrap_or_else(|_| PrefValue::from(input)),
}
}
}
impl TryFrom<&Value> for PrefValue {
type Error = String;
fn try_from(value: &Value) -> Result<Self, Self::Error> {
match value {
Value::Null => Err("Cannot turn null into preference".into()),
Value::Bool(value) => Ok((*value).into()),
Value::Number(number) => number
.as_i64()
.map(Into::into)
.or_else(|| number.as_f64().map(Into::into))
.map(Ok)
.unwrap_or(Err("Could not parse number from JSON".into())),
Value::String(value) => Ok(value.clone().into()),
Value::Array(array) => {
let array = array
.iter()
.map(TryInto::<PrefValue>::try_into)
.collect::<Result<Vec<_>, _>>()?;
Ok(PrefValue::Array(array))
},
Value::Object(_) => Err("Cannot turn object into preference".into()),
}
}
}
macro_rules! impl_pref_from {
($($t: ty => $variant: path,)*) => {
$(
impl From<$t> for PrefValue {
fn from(other: $t) -> Self {
$variant(other.into())
}
}
)+
}
}
macro_rules! impl_from_pref {
($($variant: path => $t: ty,)*) => {
$(
impl TryFrom<PrefValue> for $t {
type Error = String;
fn try_from(other: PrefValue) -> Result<Self, Self::Error> {
match other {
$variant(value) => Ok(value.into()),
_ => Err(format!("Cannot convert {other:?} to {}", std::any::type_name::<$t>())),
}
}
}
)+
}
}
impl_pref_from! {
f64 => PrefValue::Float,
i64 => PrefValue::Int,
u64 => PrefValue::UInt,
String => PrefValue::Str,
&str => PrefValue::Str,
bool => PrefValue::Bool,
}
impl_from_pref! {
PrefValue::Float => f64,
PrefValue::Int => i64,
PrefValue::Str => String,
PrefValue::Bool => bool,
}
// The default generated from `impl_from_pref` would cause panic
// when converting from PrefValue::Int.
impl TryFrom<PrefValue> for u64 {
type Error = String;
fn try_from(other: PrefValue) -> Result<Self, Self::Error> {
match other {
PrefValue::UInt(value) => Ok(value),
PrefValue::Int(value) if value >= 0 => Ok(value as u64),
_ => Err(format!("Cannot convert {other:?} to u64")),
}
}
}
impl From<[f64; 4]> for PrefValue {
fn from(other: [f64; 4]) -> PrefValue {
PrefValue::Array(IntoIterator::into_iter(other).map(|v| v.into()).collect())
}
}
impl From<PrefValue> for [f64; 4] {
fn from(other: PrefValue) -> [f64; 4] {
match other {
PrefValue::Array(values) if values.len() == 4 => {
let values: Vec<f64> = values
.into_iter()
.map(TryFrom::try_from)
.filter_map(Result::ok)
.collect();
if values.len() == 4 {
[values[0], values[1], values[2], values[3]]
} else {
panic!(
"Cannot convert PrefValue to {:?}",
std::any::type_name::<[f64; 4]>()
)
}
},
_ => panic!(
"Cannot convert {:?} to {:?}",
other,
std::any::type_name::<[f64; 4]>()
),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_pref_value_from_str() {
let value = PrefValue::from_booleanish_str("21");
assert_eq!(value, PrefValue::Int(21));
let value = PrefValue::from_booleanish_str("12.5");
assert_eq!(value, PrefValue::Float(12.5));
let value = PrefValue::from_booleanish_str("a string");
assert_eq!(value, PrefValue::Str("a string".into()));
let value = PrefValue::from_booleanish_str("false");
assert_eq!(value, PrefValue::Bool(false));
let value = PrefValue::from_booleanish_str("true");
assert_eq!(value, PrefValue::Bool(true));
}
}