Unnamed repository; edit this file 'description' to name the repository.
Fix #19071: ensure `completion_item_hash` serializes items uniquely
Previously it may have been possible for different completion items to produce colliding hashes, not because of the hash but because of how the items were serialized into byte streams for hashing. See #19071 for details. The chances of that happening were low, if it was actually possible at all. Nevertheless, this commit ensures that it definitely can't happen. This commit uses a handful of techniques used to fix this, but they all boil down to "ensure this could be re-parsed". If it's possible to parse to recreate the original item, then by construction there is no chance of two different items getting serialized to identical byte streams.
Nathan Vegdahl 2025-01-29
parent bc10a44 · commit b89adf0
-rw-r--r--crates/rust-analyzer/src/lib.rs74
1 files changed, 50 insertions, 24 deletions
diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs
index ccffa7a671..98ba8ab3af 100644
--- a/crates/rust-analyzer/src/lib.rs
+++ b/crates/rust-analyzer/src/lib.rs
@@ -79,32 +79,34 @@ fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8;
u8::from(relevance.requires_import),
u8::from(relevance.is_private_editable),
]);
- if let Some(type_match) = &relevance.type_match {
- let label = match type_match {
- CompletionRelevanceTypeMatch::CouldUnify => "could_unify",
- CompletionRelevanceTypeMatch::Exact => "exact",
- };
- hasher.update(label);
+
+ match relevance.type_match {
+ None => hasher.update([0u8]),
+ Some(CompletionRelevanceTypeMatch::CouldUnify) => hasher.update([1u8]),
+ Some(CompletionRelevanceTypeMatch::Exact) => hasher.update([2u8]),
}
+
+ hasher.update([u8::from(relevance.trait_.is_some())]);
if let Some(trait_) = &relevance.trait_ {
hasher.update([u8::from(trait_.is_op_method), u8::from(trait_.notable_trait)]);
}
- if let Some(postfix_match) = &relevance.postfix_match {
- let label = match postfix_match {
- CompletionRelevancePostfixMatch::NonExact => "non_exact",
- CompletionRelevancePostfixMatch::Exact => "exact",
- };
- hasher.update(label);
+
+ match relevance.postfix_match {
+ None => hasher.update([0u8]),
+ Some(CompletionRelevancePostfixMatch::NonExact) => hasher.update([1u8]),
+ Some(CompletionRelevancePostfixMatch::Exact) => hasher.update([2u8]),
}
+
+ hasher.update([u8::from(relevance.function.is_some())]);
if let Some(function) = &relevance.function {
hasher.update([u8::from(function.has_params), u8::from(function.has_self_param)]);
- let label = match function.return_type {
- CompletionRelevanceReturnType::Other => "other",
- CompletionRelevanceReturnType::DirectConstructor => "direct_constructor",
- CompletionRelevanceReturnType::Constructor => "constructor",
- CompletionRelevanceReturnType::Builder => "builder",
+ let discriminant: u8 = match function.return_type {
+ CompletionRelevanceReturnType::Other => 0,
+ CompletionRelevanceReturnType::DirectConstructor => 1,
+ CompletionRelevanceReturnType::Constructor => 2,
+ CompletionRelevanceReturnType::Builder => 3,
};
- hasher.update(label);
+ hasher.update([discriminant]);
}
}
@@ -115,35 +117,59 @@ fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8;
u8::from(item.deprecated),
u8::from(item.trigger_call_info),
]);
+
+ hasher.update(item.label.primary.len().to_le_bytes());
hasher.update(&item.label.primary);
+
+ hasher.update([u8::from(item.label.detail_left.is_some())]);
if let Some(label_detail) = &item.label.detail_left {
+ hasher.update(label_detail.len().to_le_bytes());
hasher.update(label_detail);
}
+
+ hasher.update([u8::from(item.label.detail_right.is_some())]);
if let Some(label_detail) = &item.label.detail_right {
+ hasher.update(label_detail.len().to_le_bytes());
hasher.update(label_detail);
}
+
// NB: do not hash edits or source range, as those may change between the time the client sends the resolve request
// and the time it receives it: some editors do allow changing the buffer between that, leading to ranges being different.
//
// Documentation hashing is skipped too, as it's a large blob to process,
// while not really making completion properties more unique as they are already.
- hasher.update(item.kind.tag());
+
+ let kind_tag = item.kind.tag();
+ hasher.update(kind_tag.len().to_le_bytes());
+ hasher.update(kind_tag);
+
+ hasher.update(item.lookup.len().to_le_bytes());
hasher.update(&item.lookup);
+
+ hasher.update([u8::from(item.detail.is_some())]);
if let Some(detail) = &item.detail {
+ hasher.update(detail.len().to_le_bytes());
hasher.update(detail);
}
+
hash_completion_relevance(&mut hasher, &item.relevance);
+
+ hasher.update([u8::from(item.ref_match.is_some())]);
if let Some((ref_mode, text_size)) = &item.ref_match {
- let prefix = match ref_mode {
- CompletionItemRefMode::Reference(Mutability::Shared) => "&",
- CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ",
- CompletionItemRefMode::Dereference => "*",
+ let descriminant = match ref_mode {
+ CompletionItemRefMode::Reference(Mutability::Shared) => 0u8,
+ CompletionItemRefMode::Reference(Mutability::Mut) => 1u8,
+ CompletionItemRefMode::Dereference => 2u8,
};
- hasher.update(prefix);
+ hasher.update([descriminant]);
hasher.update(u32::from(*text_size).to_le_bytes());
}
+
+ hasher.update(item.import_to_add.len().to_le_bytes());
for import_path in &item.import_to_add {
+ hasher.update(import_path.len().to_le_bytes());
hasher.update(import_path);
}
+
hasher.finalize()
}