ztmux 3.7.0

A Rust port of tmux — the full terminal multiplexer, server and client
Documentation
/// C `vendor/tmux/compat/strtonum.c:31`: `long long strtonum(const char *numstr, long long minval, long long maxval, const char **errstrp)`
pub unsafe fn strtonum<T>(
    nptr: *const u8,
    minval: T,
    maxval: T,
) -> Result<T, &'static core::ffi::CStr>
where
    T: Into<i64>,
    i64: TryInto<T>,
{
    let minval: i64 = minval.into();
    let maxval: i64 = maxval.into();

    if minval > maxval {
        return Err(c"invalid");
    }

    let buf = unsafe { std::slice::from_raw_parts(nptr, crate::libc::strlen(nptr)) };
    let s = std::str::from_utf8(buf).map_err(|_| c"invalid")?;
    let n = s.trim_start().parse::<i64>().map_err(|_| c"invalid")?;

    if n < minval {
        return Err(c"too small");
    }

    if n > maxval {
        return Err(c"too large");
    }

    match n.try_into() {
        Ok(value) => Ok(value),
        Err(_) => unreachable!("range check above should prevent this case"),
    }
}

pub fn strtonum_<T>(s: &str, minval: T, maxval: T) -> Result<T, &'static core::ffi::CStr>
where
    T: Into<i64>,
    i64: TryInto<T>,
{
    let minval: i64 = minval.into();
    let maxval: i64 = maxval.into();

    if minval > maxval {
        return Err(c"invalid");
    }

    let n = s.trim_start().parse::<i64>().map_err(|_| c"invalid")?;

    if n < minval {
        return Err(c"too small");
    }

    if n > maxval {
        return Err(c"too large");
    }

    match n.try_into() {
        Ok(value) => Ok(value),
        Err(_) => unreachable!("range check above should prevent this case"),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_strtonum_valid() {
        assert_eq!(strtonum_("42", 0i32, 100i32), Ok(42));
    }

    #[test]
    fn test_strtonum_leading_whitespace() {
        // trim_start allows leading spaces.
        assert_eq!(strtonum_("   7", 0i32, 100i32), Ok(7));
    }

    #[test]
    fn test_strtonum_too_small() {
        assert_eq!(strtonum_("-5", 0i32, 100i32), Err(c"too small"));
    }

    #[test]
    fn test_strtonum_too_large() {
        assert_eq!(strtonum_("200", 0i32, 100i32), Err(c"too large"));
    }

    #[test]
    fn test_strtonum_non_numeric() {
        assert_eq!(strtonum_("abc", 0i32, 100i32), Err(c"invalid"));
    }

    #[test]
    fn test_strtonum_inverted_range() {
        // minval > maxval is rejected before parsing.
        assert_eq!(strtonum_("5", 100i32, 0i32), Err(c"invalid"));
    }

    #[test]
    fn test_strtonum_inclusive_boundaries() {
        // Both bounds are inclusive (C strtonum.c:55-58 uses `< minval` /
        // `> maxval`, so equality is accepted).
        assert_eq!(strtonum_("100", 0i32, 100i32), Ok(100));
        assert_eq!(strtonum_("0", 0i32, 100i32), Ok(0));
        assert_eq!(strtonum_("101", 0i32, 100i32), Err(c"too large"));
        assert_eq!(strtonum_("-1", 0i32, 100i32), Err(c"too small"));
    }

    #[test]
    fn test_strtonum_single_point_range() {
        // minval == maxval is a valid one-value window.
        assert_eq!(strtonum_("5", 5i32, 5i32), Ok(5));
        assert_eq!(strtonum_("6", 5i32, 5i32), Err(c"too large"));
        assert_eq!(strtonum_("4", 5i32, 5i32), Err(c"too small"));
    }

    #[test]
    fn test_strtonum_negative_range() {
        assert_eq!(strtonum_("-50", -100i32, 100i32), Ok(-50));
        assert_eq!(strtonum_("-100", -100i32, 100i32), Ok(-100));
        assert_eq!(strtonum_("-101", -100i32, 100i32), Err(c"too small"));
    }

    #[test]
    fn test_strtonum_leading_plus() {
        // Rust's i64 parse accepts an explicit '+' sign, like C strtoll.
        assert_eq!(strtonum_("+42", 0i32, 100i32), Ok(42));
    }

    #[test]
    fn test_strtonum_trailing_garbage() {
        // C strtonum.c:53 rejects a non-empty `*ep`; the Rust port relies on
        // i64 parse, which likewise rejects any trailing byte (digits, letters,
        // or a trailing space, since only leading whitespace is trimmed).
        assert_eq!(strtonum_("42x", 0i32, 100i32), Err(c"invalid"));
        assert_eq!(strtonum_("42 ", 0i32, 100i32), Err(c"invalid"));
        assert_eq!(strtonum_("4 2", 0i32, 100i32), Err(c"invalid"));
    }

    #[test]
    fn test_strtonum_empty_string() {
        // No digits at all -> invalid (C: numstr == ep).
        assert_eq!(strtonum_("", 0i32, 100i32), Err(c"invalid"));
        assert_eq!(strtonum_("   ", 0i32, 100i32), Err(c"invalid"));
    }

    #[test]
    fn test_strtonum_hex_not_accepted() {
        // strtonum parses base 10 only (C strtonum.c:52 passes 10), so "0x1f"
        // stops after the '0' and the trailing "x1f" makes it invalid.
        assert_eq!(strtonum_("0x1f", 0i32, 100i32), Err(c"invalid"));
    }

    #[test]
    fn test_strtonum_full_i64_range() {
        // The generic accumulator is i64; exercise the extreme bounds.
        assert_eq!(strtonum_("9223372036854775807", i64::MIN, i64::MAX), Ok(i64::MAX));
        assert_eq!(strtonum_("-9223372036854775808", i64::MIN, i64::MAX), Ok(i64::MIN));
    }

    #[test]
    fn test_strtonum_overflow_beyond_i64() {
        // DISCREPANCY vs C: a value past LLONG_MAX makes C strtonum return
        // "too large" (strtonum.c:57, ll == LLONG_MAX && errno == ERANGE).
        // The Rust port parses into i64 first, so the overflow surfaces as a
        // parse failure -> "invalid". Asserting ACTUAL Rust behavior.
        assert_eq!(strtonum_("99999999999999999999999", i64::MIN, i64::MAX), Err(c"invalid"));
    }

    #[test]
    fn test_strtonum_ptr_variant() {
        // The pointer-taking `strtonum` (used by the ported C call sites) must
        // agree with the &str helper.
        unsafe {
            assert_eq!(strtonum(crate::c!("55"), 0i32, 100i32), Ok(55));
            assert_eq!(strtonum(crate::c!("999"), 0i32, 100i32), Err(c"too large"));
            assert_eq!(strtonum(crate::c!("nope"), 0i32, 100i32), Err(c"invalid"));
        }
    }
}