Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/uu/join/src/join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1104,14 +1104,26 @@ fn exec<Sep: Separator>(
Ok(())
}

/// Render a zero-based field index as its one-based equivalent.
///
/// `parse_field_number` clamps an out-of-range field to `usize::MAX`, so the
/// increment has to saturate rather than overflow.
fn one_based(k: usize) -> usize {
k.saturating_add(1)
}

/// Check that keys for both files and for a particular file are not
/// contradictory and return the key index.
fn get_field_number(keys: Option<usize>, key: Option<usize>) -> UResult<usize> {
match (keys, key) {
// Show zero-based field numbers as one-based.
(Some(k1), Some(k2)) if k1 != k2 => Err(USimpleError::new(
1,
translate!("join-error-incompatible-fields", "field1" => (k1 + 1), "field2" => (k2 + 1)),
translate!(
"join-error-incompatible-fields",
"field1" => one_based(k1),
"field2" => one_based(k2)
),
)),
(Some(k), _) | (_, Some(k)) => Ok(k),
(None, None) => Ok(0),
Expand Down
50 changes: 49 additions & 1 deletion src/uucore/src/lib/mods/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,28 @@ pub fn get_message(id: &str) -> String {
/// let message = get_message_with_args("notification", args);
/// println!("{message}");
/// ```
/// The value as an `i64` when Fluent can represent it exactly, else `None`.
#[doc(hidden)]
pub fn exact_fluent_integer(s: &str) -> Option<i64> {
// Fluent stores numbers as f64, which represents integers exactly only up
// to 2^53. Anything beyond that has to travel as a string.
const MAX_EXACT: i64 = 1 << 53;
match s.parse::<i64>() {
Ok(n) if (-MAX_EXACT..=MAX_EXACT).contains(&n) => Some(n),
_ => None,
}
}

/// Whether `s` is a plain decimal integer literal, with an optional sign.
///
/// Used by [`translate!`] to tell an integer that Fluent cannot hold exactly
/// from a genuine float, so the former can bypass Fluent's number type.
#[doc(hidden)]
pub fn is_integer_literal(s: &str) -> bool {
let digits = s.strip_prefix(['-', '+']).unwrap_or(s);
!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
}

pub fn get_message_with_args(id: &str, ftl_args: FluentArgs) -> String {
get_message_internal(id, Some(ftl_args))
}
Expand Down Expand Up @@ -659,8 +681,13 @@ macro_rules! translate {
let mut args = fluent::FluentArgs::new();
$(
let value_str = $value.to_string();
if let Ok(num_val) = value_str.parse::<i64>() {
if let Some(num_val) = $crate::locale::exact_fluent_integer(&value_str) {
args.set($key, num_val);
} else if $crate::locale::is_integer_literal(&value_str) {
// An integer Fluent cannot hold exactly. Its number type is
// f64-backed, so setting it as a number would round it; keep
// the exact decimal string instead.
args.set($key, value_str);
} else if let Ok(float_val) = value_str.parse::<f64>() {
args.set($key, float_val);
} else {
Expand All @@ -678,6 +705,27 @@ pub use translate;

#[cfg(test)]
mod tests {
#[test]
fn integers_beyond_f64_precision_stay_exact() {
// Fluent's number type is f64-backed, so values past 2^53 must travel
// as strings to survive intact.
assert_eq!(exact_fluent_integer("3"), Some(3));
assert_eq!(exact_fluent_integer("-3"), Some(-3));
assert_eq!(exact_fluent_integer("9007199254740992"), Some(1 << 53));
assert_eq!(exact_fluent_integer("9007199254740993"), None);
assert_eq!(exact_fluent_integer("-9007199254740993"), None);
assert_eq!(exact_fluent_integer("18446744073709551615"), None);
assert_eq!(exact_fluent_integer("1.5"), None);

assert!(is_integer_literal("18446744073709551615"));
assert!(is_integer_literal("-7"));
assert!(is_integer_literal("+7"));
assert!(!is_integer_literal("1.5"));
assert!(!is_integer_literal(""));
assert!(!is_integer_literal("-"));
assert!(!is_integer_literal("12a"));
}

use super::*;
use std::env;
use std::fs;
Expand Down
27 changes: 27 additions & 0 deletions tests/by-util/test_join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,30 @@ fn test_locale_collation() {
.stdout_contains("abc:d 2 y")
.stdout_contains("ab:d 1 x");
}

#[test]
fn test_incompatible_fields_reports_exact_field_number() {
// An out-of-range field clamps to usize::MAX, which used to overflow the
// one-based increment. Field numbers past 2^53 also used to be rounded on
// their way through the localization layer.
for (field, expected) in [
("3", "incompatible join fields 3, 5"),
(
"9007199254740993",
"incompatible join fields 9007199254740993, 5",
),
(
"18446744073709551615",
"incompatible join fields 18446744073709551615, 5",
),
(
"99999999999999999999999",
"incompatible join fields 18446744073709551615, 5",
),
] {
new_ucmd!()
.args(&["-j", field, "-1", "5", "/dev/null", "/dev/null"])
.fails()
.stderr_contains(expected);
}
}
Loading