pub const IMPERSONATED_NIX_VERSION: &str = "2.34.7";
pub const LANG_VERSION: i64 = 6;
#[must_use]
pub fn split_version(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut prev_digit: Option<bool> = None;
for ch in s.chars() {
if ch == '.' || ch == '-' {
if !current.is_empty() {
parts.push(std::mem::take(&mut current));
}
prev_digit = None;
} else {
let is_digit = ch.is_ascii_digit();
if let Some(was_digit) = prev_digit
&& is_digit != was_digit
&& !current.is_empty()
{
parts.push(std::mem::take(&mut current));
}
current.push(ch);
prev_digit = Some(is_digit);
}
}
if !current.is_empty() {
parts.push(current);
}
parts
}
#[must_use]
pub fn compare_versions(a: &str, b: &str) -> i64 {
let pa = split_version(a);
let pb = split_version(b);
let max_len = pa.len().max(pb.len());
for i in 0..max_len {
let ca = pa.get(i).map(String::as_str).unwrap_or("");
let cb = pb.get(i).map(String::as_str).unwrap_or("");
let ord = match (ca.parse::<i64>(), cb.parse::<i64>()) {
(Ok(na), Ok(nb)) => na.cmp(&nb),
(Ok(_), Err(_)) => std::cmp::Ordering::Greater,
(Err(_), Ok(_)) => std::cmp::Ordering::Less,
(Err(_), Err(_)) => match (ca, cb) {
("pre", "pre") => std::cmp::Ordering::Equal,
("pre", _) => std::cmp::Ordering::Less,
(_, "pre") => std::cmp::Ordering::Greater,
_ => ca.cmp(cb),
},
};
if ord != std::cmp::Ordering::Equal {
return if ord == std::cmp::Ordering::Less { -1 } else { 1 };
}
}
0
}
#[must_use]
pub fn cppnix_format_json_float(f: f64) -> String {
if !f.is_finite() {
return "null".to_string();
}
if f == 0.0 {
return "0.0".to_string();
}
let sci = format!("{f:e}");
let (mantissa, exp_str) = sci
.split_once('e')
.expect("LowerExp for f64 always emits an exponent");
let exp: i32 = exp_str
.parse()
.expect("LowerExp for f64 always emits a parseable exponent");
if (-4..=14).contains(&exp) {
let fixed = format!("{f}");
if fixed.contains('.') {
fixed
} else {
format!("{fixed}.0")
}
} else {
let (sign, digits) = match exp_str.strip_prefix('-') {
Some(d) => ('-', d),
None => ('+', exp_str),
};
format!("{mantissa}e{sign}{digits:0>2}")
}
}
pub fn nix_json_to_string(value: &serde_json::Value) -> Result<String, serde_json::Error> {
struct NixFloats;
impl serde_json::ser::Formatter for NixFloats {
fn write_f64<W>(&mut self, writer: &mut W, value: f64) -> std::io::Result<()>
where
W: ?Sized + std::io::Write,
{
writer.write_all(cppnix_format_json_float(value).as_bytes())
}
}
let mut buf = Vec::with_capacity(128);
let mut ser = serde_json::Serializer::with_formatter(&mut buf, NixFloats);
serde::Serialize::serialize(value, &mut ser)?;
Ok(String::from_utf8(buf).expect("serde_json emits UTF-8"))
}
#[must_use]
pub fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\n', "
")
}
#[must_use]
pub fn cppnix_format_float(f: f64) -> String {
if f.is_nan() {
return "NaN".to_string();
}
if f.is_infinite() {
return if f > 0.0 { "inf".to_string() } else { "-inf".to_string() };
}
if f == 0.0 {
return "0".to_string();
}
let exp = f.abs().log10().floor() as i32;
if (-4..6).contains(&exp) {
let after_decimal = (5 - exp).max(0) as usize;
let raw = format!("{f:.*}", after_decimal);
if let Some((whole, frac)) = raw.split_once('.') {
let trimmed = frac.trim_end_matches('0');
if trimmed.is_empty() {
whole.to_string()
} else {
format!("{whole}.{trimmed}")
}
} else {
raw
}
} else {
let raw = format!("{f:.5e}");
if let Some((mantissa, exp_part)) = raw.split_once('e') {
let mantissa_trimmed =
if let Some((w, frac)) = mantissa.split_once('.') {
let trimmed = frac.trim_end_matches('0');
if trimmed.is_empty() {
w.to_string()
} else {
format!("{w}.{trimmed}")
}
} else {
mantissa.to_string()
};
let (sign, digits) = match exp_part.strip_prefix('-') {
Some(d) => ('-', d),
None => ('+', exp_part),
};
let exp_part_signed = format!("{sign}{digits:0>2}");
format!("{mantissa_trimmed}e{exp_part_signed}")
} else {
raw
}
}
}
#[must_use]
pub fn parse_drv_name(s: &str) -> (String, String) {
let bytes = s.as_bytes();
for i in (0..bytes.len()).rev() {
if bytes[i] == b'-'
&& i + 1 < bytes.len()
&& bytes[i + 1].is_ascii_digit()
{
return (s[..i].to_string(), s[i + 1..].to_string());
}
}
(s.to_string(), String::new())
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn rc_orders_above_pre() {
assert_eq!(compare_versions("1.0-rc1", "1.0-pre1"), 1);
assert_eq!(compare_versions("1.0-pre1", "1.0-rc1"), -1);
}
#[test]
fn numeric_components_compare_numerically() {
assert_eq!(compare_versions("1.10", "1.2"), 1);
assert_eq!(compare_versions("1.2", "1.10"), -1);
assert_eq!(compare_versions("1.0", "1.0"), 0);
}
#[test]
fn numeric_component_beats_letter_component() {
assert_eq!(compare_versions("1.a", "1.1"), -1); assert_eq!(compare_versions("1.1", "1.a"), 1); assert_eq!(compare_versions("1.0.0", "1.0.a"), 1); assert_eq!(compare_versions("a", "1"), -1); assert_eq!(compare_versions("1", "a"), 1);
assert_eq!(compare_versions("1.1", "1.pre"), 1);
assert_eq!(compare_versions("1.pre", "1.1"), -1);
assert_eq!(compare_versions("1.a", "1.b"), -1);
}
#[test]
fn missing_components_order_below_present_components() {
assert_eq!(compare_versions("1.0", "1.0.0"), -1);
assert_eq!(compare_versions("1.0.0", "1.0"), 1);
}
#[test]
fn pre_below_everything_except_pre() {
assert_eq!(compare_versions("1.0-pre1", "1.0-pre1"), 0);
assert_eq!(compare_versions("1.0-pre1", "1.0"), -1);
assert_eq!(compare_versions("1.0", "1.0-pre1"), 1);
assert_eq!(compare_versions("1.0-pre", "1.0-alpha"), -1);
assert_eq!(compare_versions("1.0-pre", "1.0-beta"), -1);
assert_eq!(compare_versions("1.0-pre", "1.0-rc"), -1);
}
#[test]
fn split_version_basic_shapes() {
assert_eq!(split_version("1.0-rc1"), vec!["1", "0", "rc", "1"]);
assert_eq!(split_version("1.0.0-pre"), vec!["1", "0", "0", "pre"]);
assert_eq!(split_version("2024a"), vec!["2024", "a"]);
}
#[test]
fn cppnix_format_float_known_outputs() {
assert_eq!(cppnix_format_float(1.0 / 3.0), "0.333333");
assert_eq!(cppnix_format_float(3.14159), "3.14159");
assert_eq!(cppnix_format_float(1.5), "1.5");
assert_eq!(cppnix_format_float(3.0), "3");
assert_eq!(cppnix_format_float(0.0), "0");
assert_eq!(cppnix_format_float(-3.14), "-3.14");
assert_eq!(cppnix_format_float(-3.0), "-3");
}
#[test]
fn cppnix_json_float_switches_at_exponent_minus_four_and_fourteen() {
assert_eq!(cppnix_format_json_float(1.5), "1.5");
assert_eq!(cppnix_format_json_float(0.1), "0.1");
assert_eq!(cppnix_format_json_float(0.0001), "0.0001");
assert_eq!(cppnix_format_json_float(0.00015), "0.00015");
assert_eq!(cppnix_format_json_float(3.0e10), "30000000000.0");
assert_eq!(cppnix_format_json_float(1.0e14), "100000000000000.0");
assert_eq!(cppnix_format_json_float(1.0), "1.0");
assert_eq!(cppnix_format_json_float(1_000_000.0), "1000000.0");
assert_eq!(cppnix_format_json_float(123_456_789.0), "123456789.0");
assert_eq!(cppnix_format_json_float(0.00001), "1e-05");
assert_eq!(cppnix_format_json_float(1.0e-6), "1e-06");
assert_eq!(cppnix_format_json_float(2.5e-5), "2.5e-05");
assert_eq!(cppnix_format_json_float(9.9e-5), "9.9e-05");
assert_eq!(cppnix_format_json_float(1.0e15), "1e+15");
assert_eq!(cppnix_format_json_float(1.23e15), "1.23e+15");
assert_eq!(cppnix_format_json_float(1.0e100), "1e+100");
assert_eq!(cppnix_format_json_float(1.0e-100), "1e-100");
assert_eq!(cppnix_format_json_float(1_234_567_890_123_456.0), "1.234567890123456e+15");
assert_eq!(cppnix_format_json_float(0.300_000_000_000_000_04), "0.30000000000000004");
assert_eq!(cppnix_format_json_float(0.0), "0.0");
assert_eq!(cppnix_format_json_float(-0.0), "0.0");
assert_eq!(cppnix_format_json_float(-1.5), "-1.5");
assert_eq!(cppnix_format_json_float(-2.5e-5), "-2.5e-05");
}
#[test]
fn json_and_value_float_formats_are_not_interchangeable() {
for v in [3.0e10, 1.0, 1.0e14, 1_000_000.0] {
assert_ne!(
cppnix_format_json_float(v),
cppnix_format_float(v),
"the JSON and value float formats agree on {v}, so this \
calibration no longer distinguishes them — check whether one \
was made to call the other"
);
}
}
#[test]
fn nix_json_to_string_formats_floats_at_any_depth() {
let v = serde_json::json!({ "a": [1.0e-6, { "b": 2.5e-5 }], "c": 1.0e15 });
assert_eq!(
nix_json_to_string(&v).expect("serialize"),
r#"{"a":[1e-06,{"b":2.5e-05}],"c":1e+15}"#
);
}
#[test]
fn cppnix_format_float_pads_exponent_to_two_digits() {
assert_eq!(cppnix_format_float(123_456_789.0), "1.23457e+08");
assert_eq!(cppnix_format_float(1_000_000.0), "1e+06");
assert_eq!(cppnix_format_float(0.000_01), "1e-05");
assert_eq!(cppnix_format_float(3.0e10), "3e+10");
assert_eq!(cppnix_format_float(1.0e100), "1e+100");
assert_eq!(cppnix_format_float(1.0e-100), "1e-100");
assert_eq!(cppnix_format_float(999_999.0), "999999");
assert_eq!(cppnix_format_float(-123_456_789.0), "-1.23457e+08");
}
#[test]
fn cppnix_format_float_nan_and_infinity() {
assert_eq!(cppnix_format_float(f64::NAN), "NaN");
assert_eq!(cppnix_format_float(f64::INFINITY), "inf");
assert_eq!(cppnix_format_float(f64::NEG_INFINITY), "-inf");
}
#[test]
fn parse_drv_name_recovers_split() {
let (n, v) = parse_drv_name("hello-1.2.3");
assert_eq!(n, "hello");
assert_eq!(v, "1.2.3");
let (n, v) = parse_drv_name("nix-darwin-config");
assert_eq!(n, "nix-darwin-config");
assert_eq!(v, "");
}
proptest! {
#[test]
fn compare_versions_antisymmetric(
a in "[0-9a-z.-]{1,20}",
b in "[0-9a-z.-]{1,20}",
) {
let ab = compare_versions(&a, &b);
let ba = compare_versions(&b, &a);
prop_assert_eq!(ab, -ba);
}
#[test]
fn compare_versions_reflexive(a in "[0-9a-z.-]{1,20}") {
prop_assert_eq!(compare_versions(&a, &a), 0);
}
}
}