gitbutlerapp/gitbutler · info

below u16::MAX

Error message

below u16::MAX

What it means

GitButler short change IDs (first char g-z, then 1-2 chars of 0-9a-z) are packed into a u16 by UintId::from_name. The arithmetic maxes out at 19 + 35*20 + 36*20*36 = 26,639 — far below u16::MAX — because every character is looked up in fixed tables (FIRST_CHARS/SUBSEQUENT_CHARS) before contributing. The usize→u16 TryInto expect documents that invariant; with the current tables it cannot fail, and there is a further debug_assert that result < LIMIT (26,640).

Source

Thrown at crates/but/src/id/id_usage.rs:75

        let mut result: usize = 0;

        let index = Self::FIRST_CHARS.iter().position(|e| e == first_char)?;
        result += index;

        let index = Self::SUBSEQUENT_CHARS
            .iter()
            .position(|e| e == second_char)?;
        result += index * 20;

        if let Some(third_char) = third_char {
            let index = Self::SUBSEQUENT_CHARS
                .iter()
                .position(|e| e == third_char)?;
            result += (index + 1) * 20 * 36;
        }

        let result: u16 = result.try_into().expect("below u16::MAX");
        debug_assert!(
            result < Self::LIMIT,
            "BUG: {result} is beyond limit of {}",
            Self::LIMIT
        );
        Some(Self(result))
    }
}

/// A tracker of which [UintId]s have been used.
#[derive(Clone, Default, Debug)]
pub(crate) struct IdUsage {
    /// A [UintId] is used if it's in this set.
    uint_ids_used: HashSet<UintId>,
    /// A [UintId] is used if it's less than this number.
    next_uint_id: UintId,
}

View on GitHub (pinned to 2497b8007a)

Solutions

  1. Keep FIRST_CHARS, SUBSEQUENT_CHARS, and LIMIT (20*36*37) in sync; if tables grow, widen the intermediate to u32 and re-derive LIMIT
  2. Replace the expect with u16::try_from(result).ok()? — from_name already returns Option for invalid IDs, so overflow should be one more None case
  3. Add a property test iterating all valid 2- and 3-char names asserting from_name returns Some and stays under LIMIT

Example fix

// before
let result: u16 = result.try_into().expect("below u16::MAX");

// after — invalid/too-large input is a None like any other parse failure
let result: u16 = result.try_into().ok()?;
Defensive patterns

Strategy: validation

Validate before calling

// cheap shape check before handing a user-supplied short id to but
fn looks_like_short_id(s: &str) -> bool {
    let b = s.as_bytes();
    (2..=3).contains(&b.len())
        && b"ghijklmnopqrstuvwxyz".contains(&b[0])
        && b[1..].iter().all(|c| b"0123456789abcdefghijklmnopqrstuvwxyz".contains(c))
}

Type guard

fn is_valid_short_id(value: &str) -> bool {
    let b = value.as_bytes();
    matches!(b, [a, rest @ ..] if b"ghijklmnopqrstuvwxyz".contains(a)
        && (rest.len() == 1 || rest.len() == 2)
        && rest.iter().all(|c| b"0123456789abcdefghijklmnopqrstuvwxyz".contains(c)))
}

Prevention

When it happens

Trigger: Parsing any 2-3 character change ID (e.g. resolving `but <cmd> g5x` or a TUI selection) — the normal, non-panicking path. The expect itself only fires if the alphabet tables are enlarged so a packed value could exceed 65,535.

Common situations: None for users. Developers hit it when extending the ID alphabet or LENGTH_LIMIT in crates/but/src/id/id_usage.rs without re-checking the packing math.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17). Data as JSON: /api/errors/565ca8f79a09302f. Report an issue: GitHub.