about summary refs log tree commit diff
path: root/src/tools/rust-analyzer
diff options
context:
space:
mode:
authorNathan Vegdahl <cessen@cessen.com>2025-01-29 17:22:13 +0100
committerNathan Vegdahl <cessen@cessen.com>2025-01-29 17:22:13 +0100
commit0cf275b9fe9d610678ded0bfcad262b8552a1b51 (patch)
treeccd290497cce8fa4cc13f0d2bfc7ddf3281bf0d8 /src/tools/rust-analyzer
parent2386e95072a8468df8e35c68c2c3b4b3ac8f1d9f (diff)
downloadrust-0cf275b9fe9d610678ded0bfcad262b8552a1b51.tar.gz
rust-0cf275b9fe9d610678ded0bfcad262b8552a1b51.zip
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.
Diffstat (limited to 'src/tools/rust-analyzer')
-rw-r--r--src/tools/rust-analyzer/crates/rust-analyzer/src/lib.rs74
1 files changed, 50 insertions, 24 deletions
diff --git a/src/tools/rust-analyzer/crates/rust-analyzer/src/lib.rs b/src/tools/rust-analyzer/crates/rust-analyzer/src/lib.rs
index ccffa7a671e..98ba8ab3af0 100644
--- a/src/tools/rust-analyzer/crates/rust-analyzer/src/lib.rs
+++ b/src/tools/rust-analyzer/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()
 }