Skip to content

Commit a484d4d

Browse files
committed
fix(uucore): preserve u64 precision in translate! macro
The translate! macro lost precision for u64 > i64::MAX due to f64 fallback. Now parses u64 before f64 to maintain accuracy. Fixes #9294
1 parent 7f4d902 commit a484d4d

File tree

2 files changed

+204
-10
lines changed

2 files changed

+204
-10
lines changed

src/uu/touch/src/touch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ fn pathbuf_from_stdout() -> Result<PathBuf, TouchError> {
817817
0 => {
818818
return Err(TouchError::WindowsStdoutPathError(translate!(
819819
"touch-error-windows-stdout-path-failed",
820-
"code".to_string() =>
820+
"code" =>
821821
format!(
822822
"{}",
823823
// SAFETY: GetLastError is thread-safe and has no documented memory unsafety.

src/uucore/src/lib/mods/locale.rs

Lines changed: 203 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,58 @@ pub fn get_message_with_args(id: &str, ftl_args: FluentArgs) -> String {
332332
get_message_internal(id, Some(ftl_args))
333333
}
334334

335+
/// Helper function to safely set a FluentArgs value, preserving precision for large integers.
336+
///
337+
/// This function handles the limitation of fluent-rs where numeric values are stored as f64,
338+
/// which can only safely represent integers in the range [-2^53+1, 2^53-1]. For values
339+
/// outside this range, they are passed as strings to preserve exact precision.
340+
///
341+
/// # Arguments
342+
///
343+
/// * `args` - The FluentArgs to add the value to
344+
/// * `key` - The argument key name
345+
/// * `value_str` - The string representation of the value
346+
///
347+
/// # Implementation Notes
348+
///
349+
/// This is a workaround for <https://github.com/projectfluent/fluent-rs/issues/337>
350+
/// Once fluent-rs supports full i64/u64 precision, this can be simplified.
351+
///
352+
/// The function tries parsing in this order:
353+
/// 1. i64: For signed integers
354+
/// 2. u64: For unsigned integers (only if i64 parsing fails)
355+
/// 3. f64: For actual floating-point numbers
356+
/// 4. String: For non-numeric values or to preserve precision
357+
#[inline]
358+
pub fn safe_set_fluent_arg<'a>(args: &mut FluentArgs<'a>, key: &'a str, value_str: &'a str) {
359+
const F64_SAFE_INTEGER_MAX: u64 = (1_u64 << 53) - 1;
360+
361+
if let Ok(num_val) = value_str.parse::<i64>() {
362+
// Signed integer - check absolute value against safe range
363+
if num_val.unsigned_abs() <= F64_SAFE_INTEGER_MAX {
364+
args.set(key, num_val);
365+
} else {
366+
// Beyond safe range - pass as string to preserve precision
367+
args.set(key, value_str.to_string());
368+
}
369+
} else if let Ok(unsigned_val) = value_str.parse::<u64>() {
370+
// Unsigned integer - check against safe range
371+
if unsigned_val <= F64_SAFE_INTEGER_MAX {
372+
// Safe to convert to i64 for FluentArgs
373+
args.set(key, unsigned_val as i64);
374+
} else {
375+
// Beyond safe range - pass as string to preserve precision
376+
args.set(key, value_str.to_string());
377+
}
378+
} else if let Ok(float_val) = value_str.parse::<f64>() {
379+
// Actual floating-point number
380+
args.set(key, float_val);
381+
} else {
382+
// Not a number - pass as string
383+
args.set(key, value_str.to_string());
384+
}
385+
}
386+
335387
/// Function to detect system locale from environment variables
336388
fn detect_system_locale() -> Result<LanguageIdentifier, LocalizationError> {
337389
let locale_str = std::env::var("LANG")
@@ -529,17 +581,12 @@ macro_rules! translate {
529581
// Case 2: Message ID with key-value arguments
530582
($id:expr, $($key:expr => $value:expr),+ $(,)?) => {
531583
{
584+
// Use helper function to handle large integer precision safely.
585+
// This reduces macro expansion bloat while preserving precision.
532586
let mut args = fluent::FluentArgs::new();
533587
$(
534-
let value_str = $value.to_string();
535-
if let Ok(num_val) = value_str.parse::<i64>() {
536-
args.set($key, num_val);
537-
} else if let Ok(float_val) = value_str.parse::<f64>() {
538-
args.set($key, float_val);
539-
} else {
540-
// Keep as string if not a number
541-
args.set($key, value_str);
542-
}
588+
let value_string = $value.to_string();
589+
$crate::locale::safe_set_fluent_arg(&mut args, $key, &value_string);
543590
)+
544591
$crate::locale::get_message_with_args($id, args)
545592
}
@@ -1388,6 +1435,152 @@ invalid-syntax = This is { $missing
13881435
.join()
13891436
.unwrap();
13901437
}
1438+
1439+
#[test]
1440+
fn test_translate_macro_big_number_precision() {
1441+
std::thread::spawn(|| {
1442+
let temp_dir = create_test_locales_dir();
1443+
let locale = LanguageIdentifier::from_str("en-US").unwrap();
1444+
1445+
init_test_localization(&locale, temp_dir.path()).unwrap();
1446+
1447+
// Test with i64::MAX - should preserve precision
1448+
let result = translate!("welcome", "name" => i64::MAX);
1449+
assert!(
1450+
result.contains("9223372036854775807"),
1451+
"Expected i64::MAX (9223372036854775807) to be preserved, got: {result}"
1452+
);
1453+
assert!(
1454+
!result.contains("9223372036854776000"),
1455+
"Should not have precision loss, but got: {result}"
1456+
);
1457+
1458+
// Test with number within safe range
1459+
let result_safe = translate!("welcome", "name" => 1000);
1460+
assert!(result_safe.contains("1000"));
1461+
1462+
// Test with number at safe boundary
1463+
const F64_SAFE_MAX: i64 = (1_i64 << 53) - 1;
1464+
let result_boundary = translate!("welcome", "name" => F64_SAFE_MAX);
1465+
assert!(result_boundary.contains(&F64_SAFE_MAX.to_string()));
1466+
1467+
// Test with number just beyond safe boundary
1468+
let beyond_safe = F64_SAFE_MAX + 1;
1469+
let result_beyond = translate!("welcome", "name" => beyond_safe);
1470+
assert!(result_beyond.contains(&beyond_safe.to_string()));
1471+
})
1472+
.join()
1473+
.unwrap();
1474+
}
1475+
1476+
#[test]
1477+
fn test_translate_macro_preserves_existing_behavior() {
1478+
std::thread::spawn(|| {
1479+
let temp_dir = create_test_locales_dir();
1480+
let locale = LanguageIdentifier::from_str("en-US").unwrap();
1481+
1482+
init_test_localization(&locale, temp_dir.path()).unwrap();
1483+
1484+
// Test string values still work
1485+
let result_str = translate!("welcome", "name" => "Alice");
1486+
assert_eq!(result_str, "Welcome, Alice!");
1487+
1488+
// Test small numbers still work
1489+
let result_num = translate!("count-items", "count" => 5);
1490+
assert_eq!(result_num, "You have 5 items");
1491+
1492+
// Test with multiple arguments
1493+
let count = 10;
1494+
let result_multi = translate!(
1495+
"count-items",
1496+
"count" => count
1497+
);
1498+
assert!(result_multi.contains("10"));
1499+
})
1500+
.join()
1501+
.unwrap();
1502+
}
1503+
1504+
#[test]
1505+
fn test_translate_macro_u64_precision() {
1506+
std::thread::spawn(|| {
1507+
let temp_dir = create_test_locales_dir();
1508+
let locale = LanguageIdentifier::from_str("en-US").unwrap();
1509+
1510+
init_test_localization(&locale, temp_dir.path()).unwrap();
1511+
1512+
// Test u64::MAX - should preserve precision
1513+
let result = translate!("welcome", "name" => u64::MAX);
1514+
assert!(
1515+
result.contains("18446744073709551615"),
1516+
"Expected u64::MAX (18446744073709551615) to be preserved, got: {result}"
1517+
);
1518+
assert!(
1519+
!result.contains("18446744073709552000"),
1520+
"Should not have precision loss, but got: {result}"
1521+
);
1522+
1523+
// Test i64::MAX + 1 as u64 - should preserve precision
1524+
let big_u64 = i64::MAX as u64 + 1;
1525+
let result2 = translate!("welcome", "name" => big_u64);
1526+
assert!(
1527+
result2.contains("9223372036854775808"),
1528+
"Expected i64::MAX+1 (9223372036854775808) to be preserved, got: {result2}"
1529+
);
1530+
assert!(
1531+
!result2.contains("9223372036854776000"),
1532+
"Should not have precision loss, but got: {result2}"
1533+
);
1534+
1535+
// Test u64 within safe range - should work as number
1536+
let safe_u64: u64 = 1000;
1537+
let result_safe = translate!("welcome", "name" => safe_u64);
1538+
assert!(result_safe.contains("1000"));
1539+
1540+
// Test u64 at boundary of safe range
1541+
const F64_SAFE_MAX: u64 = (1_u64 << 53) - 1;
1542+
let result_boundary = translate!("welcome", "name" => F64_SAFE_MAX);
1543+
assert!(result_boundary.contains(&F64_SAFE_MAX.to_string()));
1544+
1545+
// Test u64 just beyond safe boundary
1546+
let beyond_safe = F64_SAFE_MAX + 1;
1547+
let result_beyond = translate!("welcome", "name" => beyond_safe);
1548+
assert!(
1549+
result_beyond.contains(&beyond_safe.to_string()),
1550+
"Expected {beyond_safe} to be preserved, got: {result_beyond}"
1551+
);
1552+
})
1553+
.join()
1554+
.unwrap();
1555+
}
1556+
1557+
#[test]
1558+
fn test_translate_macro_negative_large_numbers() {
1559+
std::thread::spawn(|| {
1560+
let temp_dir = create_test_locales_dir();
1561+
let locale = LanguageIdentifier::from_str("en-US").unwrap();
1562+
1563+
init_test_localization(&locale, temp_dir.path()).unwrap();
1564+
1565+
// Test i64::MIN - should preserve precision
1566+
let result = translate!("welcome", "name" => i64::MIN);
1567+
assert!(
1568+
result.contains("-9223372036854775808"),
1569+
"Expected i64::MIN to be preserved, got: {result}"
1570+
);
1571+
1572+
// Test large negative number beyond safe range
1573+
const F64_SAFE_MAX: i64 = (1_i64 << 53) - 1;
1574+
let large_negative = -F64_SAFE_MAX - 1;
1575+
let result2 = translate!("welcome", "name" => large_negative);
1576+
assert!(
1577+
result2.contains(&large_negative.to_string()),
1578+
"Expected {large_negative} to be preserved, got: {result2}"
1579+
);
1580+
})
1581+
.join()
1582+
.unwrap();
1583+
}
13911584
}
13921585

13931586
#[cfg(all(test, not(debug_assertions)))]
@@ -1414,3 +1607,4 @@ mod fhs_tests {
14141607
assert_eq!(result, share_dir);
14151608
}
14161609
}
1610+

0 commit comments

Comments
 (0)