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
- Keep FIRST_CHARS, SUBSEQUENT_CHARS, and LIMIT (20*36*37) in sync; if tables grow, widen the intermediate to u32 and re-derive LIMIT
- 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
- 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
- The overflow is unreachable with the shipped alphabets — only reachable if you edit FIRST_CHARS/SUBSEQUENT_CHARS/LIMIT; re-derive the max (20*36*37) when touching them
- from_name returns None for any invalid character; rely on Option handling instead of expecting
- Add a property test over all valid 2-3 char names asserting Some and result < LIMIT
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
- deduping a NonEmpty will never make it empty
- programs was just checked to be non-empty
- change ID is ensured
- target OID must exist when ahead calculation is enabled
- target OID must exist when merge check is enabled
AI-assisted analysis of gitbutlerapp/gitbutler@2497b8007a (2026-08-17).
Data as JSON: /api/errors/565ca8f79a09302f.
Report an issue: GitHub.