Skip to content

Commit 2dc1d07

Browse files
authored
Fix AnimationTargetId collisions (#22876)
## Objective Fixes #22842. TLDR: `AnimationTargetId::from_iter` wrongly returns the same id for `["ab"]` and `["a", "b"]`. ## Solution As suggested in the issue, add the length of each string to the hash. The original suggestion used `to_be_bytes`, but I used `to_le_bytes` on the assumption that most users are targeting LE and this will be more efficient. - There's probably a way to break it with maliciously crafted names, but I can't imagine a case where that's a problem. - I considered hashing `0xffu8` after each string instead of the length. - That would be more efficient, and should be safe since it's not a valid UTF8 character. - But I wasn't quite confident enough. - I also refactored `AnimationTargetId::from_names` to reuse `AnimationTargetId::from_iter`. ## Testing ```sh cargo test -p bevy_animation cargo run --example animated_mesh cargo run --example animated_mesh_events cargo run --example animation_masks ```
1 parent dd6eb2a commit 2dc1d07

File tree

2 files changed

+58
-7
lines changed

2 files changed

+58
-7
lines changed

crates/bevy_animation/src/lib.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,13 +1263,7 @@ impl AnimationTargetId {
12631263
/// Typically, this will be the path from the animation root to the
12641264
/// animation target (e.g. bone) that is to be animated.
12651265
pub fn from_names<'a>(names: impl Iterator<Item = &'a Name>) -> Self {
1266-
let mut blake3 = blake3::Hasher::new();
1267-
blake3.update(ANIMATION_TARGET_NAMESPACE.as_bytes());
1268-
for name in names {
1269-
blake3.update(name.as_bytes());
1270-
}
1271-
let hash = blake3.finalize().as_bytes()[0..16].try_into().unwrap();
1272-
Self(*uuid::Builder::from_sha1_bytes(hash).as_uuid())
1266+
AnimationTargetId::from_iter(names.map(Name::as_str))
12731267
}
12741268

12751269
/// Creates a new [`AnimationTargetId`] by hashing a single name.
@@ -1287,6 +1281,9 @@ impl<T: AsRef<str>> FromIterator<T> for AnimationTargetId {
12871281
let mut blake3 = blake3::Hasher::new();
12881282
blake3.update(ANIMATION_TARGET_NAMESPACE.as_bytes());
12891283
for str in iter {
1284+
// Include the string's length in the hash. This avoids ["ab"] and
1285+
// ["a", "b"] returning the same id.
1286+
blake3.update(&str.as_ref().len().to_le_bytes());
12901287
blake3.update(str.as_ref().as_bytes());
12911288
}
12921289
let hash = blake3.finalize().as_bytes()[0..16].try_into().unwrap();
@@ -1664,4 +1661,47 @@ mod tests {
16641661
Box::new(ActiveAnimation::default()),
16651662
);
16661663
}
1664+
1665+
#[test]
1666+
fn test_animation_target_id() {
1667+
let paths = &[
1668+
vec![],
1669+
vec![""],
1670+
vec!["", ""],
1671+
vec!["a"],
1672+
vec!["a", "a"],
1673+
vec!["a", "b"],
1674+
vec!["b", "a"],
1675+
vec!["aa"],
1676+
vec!["ab"],
1677+
vec!["ba"],
1678+
vec!["abc"],
1679+
vec!["a", "b", "c"],
1680+
vec!["ab", "c"],
1681+
vec!["a", "bc"],
1682+
];
1683+
1684+
// Test that different paths map to a different `AnimationTargetId`.
1685+
for (li, lp) in paths.iter().enumerate() {
1686+
for rp in paths.iter().skip(li + 1) {
1687+
let lt = AnimationTargetId::from_iter(lp.iter());
1688+
let rt = AnimationTargetId::from_iter(rp.iter());
1689+
1690+
assert!(lt != rt, "{:?} and {:?} collided. {:?} ", lp, rp, lt);
1691+
}
1692+
}
1693+
1694+
// Test that `from_iter` is equivalent to `from_names`.
1695+
for str_path in paths {
1696+
let name_path = str_path.iter().map(|&s| Name::from(s)).collect::<Vec<_>>();
1697+
1698+
assert_eq!(
1699+
AnimationTargetId::from_iter(str_path),
1700+
AnimationTargetId::from_names(name_path.iter()),
1701+
"{:?} {:?}",
1702+
str_path,
1703+
&name_path
1704+
);
1705+
}
1706+
}
16671707
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: "`AnimationTargetId` algorithm changes"
3+
pull_requests: [22876]
4+
---
5+
6+
The algorithm used to calculate `AnimationTargetId` has changed. This fixes a
7+
[bug](https://github.com/bevyengine/bevy/issues/22842) where different joint
8+
hierarchies could mistakenly be assigned the same id.
9+
10+
If you have serialized data containing `AnimationTargetId` values then these
11+
will need to be recalculated.

0 commit comments

Comments
 (0)