Skip to content

Commit 0cf275b

Browse files
committed
Fix rust-lang#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 rust-lang#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.
1 parent 2386e95 commit 0cf275b

File tree

1 file changed

+50
-24
lines changed
  • src/tools/rust-analyzer/crates/rust-analyzer/src

1 file changed

+50
-24
lines changed

src/tools/rust-analyzer/crates/rust-analyzer/src/lib.rs

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,34 @@ fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8;
7979
u8::from(relevance.requires_import),
8080
u8::from(relevance.is_private_editable),
8181
]);
82-
if let Some(type_match) = &relevance.type_match {
83-
let label = match type_match {
84-
CompletionRelevanceTypeMatch::CouldUnify => "could_unify",
85-
CompletionRelevanceTypeMatch::Exact => "exact",
86-
};
87-
hasher.update(label);
82+
83+
match relevance.type_match {
84+
None => hasher.update([0u8]),
85+
Some(CompletionRelevanceTypeMatch::CouldUnify) => hasher.update([1u8]),
86+
Some(CompletionRelevanceTypeMatch::Exact) => hasher.update([2u8]),
8887
}
88+
89+
hasher.update([u8::from(relevance.trait_.is_some())]);
8990
if let Some(trait_) = &relevance.trait_ {
9091
hasher.update([u8::from(trait_.is_op_method), u8::from(trait_.notable_trait)]);
9192
}
92-
if let Some(postfix_match) = &relevance.postfix_match {
93-
let label = match postfix_match {
94-
CompletionRelevancePostfixMatch::NonExact => "non_exact",
95-
CompletionRelevancePostfixMatch::Exact => "exact",
96-
};
97-
hasher.update(label);
93+
94+
match relevance.postfix_match {
95+
None => hasher.update([0u8]),
96+
Some(CompletionRelevancePostfixMatch::NonExact) => hasher.update([1u8]),
97+
Some(CompletionRelevancePostfixMatch::Exact) => hasher.update([2u8]),
9898
}
99+
100+
hasher.update([u8::from(relevance.function.is_some())]);
99101
if let Some(function) = &relevance.function {
100102
hasher.update([u8::from(function.has_params), u8::from(function.has_self_param)]);
101-
let label = match function.return_type {
102-
CompletionRelevanceReturnType::Other => "other",
103-
CompletionRelevanceReturnType::DirectConstructor => "direct_constructor",
104-
CompletionRelevanceReturnType::Constructor => "constructor",
105-
CompletionRelevanceReturnType::Builder => "builder",
103+
let discriminant: u8 = match function.return_type {
104+
CompletionRelevanceReturnType::Other => 0,
105+
CompletionRelevanceReturnType::DirectConstructor => 1,
106+
CompletionRelevanceReturnType::Constructor => 2,
107+
CompletionRelevanceReturnType::Builder => 3,
106108
};
107-
hasher.update(label);
109+
hasher.update([discriminant]);
108110
}
109111
}
110112

@@ -115,35 +117,59 @@ fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8;
115117
u8::from(item.deprecated),
116118
u8::from(item.trigger_call_info),
117119
]);
120+
121+
hasher.update(item.label.primary.len().to_le_bytes());
118122
hasher.update(&item.label.primary);
123+
124+
hasher.update([u8::from(item.label.detail_left.is_some())]);
119125
if let Some(label_detail) = &item.label.detail_left {
126+
hasher.update(label_detail.len().to_le_bytes());
120127
hasher.update(label_detail);
121128
}
129+
130+
hasher.update([u8::from(item.label.detail_right.is_some())]);
122131
if let Some(label_detail) = &item.label.detail_right {
132+
hasher.update(label_detail.len().to_le_bytes());
123133
hasher.update(label_detail);
124134
}
135+
125136
// NB: do not hash edits or source range, as those may change between the time the client sends the resolve request
126137
// and the time it receives it: some editors do allow changing the buffer between that, leading to ranges being different.
127138
//
128139
// Documentation hashing is skipped too, as it's a large blob to process,
129140
// while not really making completion properties more unique as they are already.
130-
hasher.update(item.kind.tag());
141+
142+
let kind_tag = item.kind.tag();
143+
hasher.update(kind_tag.len().to_le_bytes());
144+
hasher.update(kind_tag);
145+
146+
hasher.update(item.lookup.len().to_le_bytes());
131147
hasher.update(&item.lookup);
148+
149+
hasher.update([u8::from(item.detail.is_some())]);
132150
if let Some(detail) = &item.detail {
151+
hasher.update(detail.len().to_le_bytes());
133152
hasher.update(detail);
134153
}
154+
135155
hash_completion_relevance(&mut hasher, &item.relevance);
156+
157+
hasher.update([u8::from(item.ref_match.is_some())]);
136158
if let Some((ref_mode, text_size)) = &item.ref_match {
137-
let prefix = match ref_mode {
138-
CompletionItemRefMode::Reference(Mutability::Shared) => "&",
139-
CompletionItemRefMode::Reference(Mutability::Mut) => "&mut ",
140-
CompletionItemRefMode::Dereference => "*",
159+
let descriminant = match ref_mode {
160+
CompletionItemRefMode::Reference(Mutability::Shared) => 0u8,
161+
CompletionItemRefMode::Reference(Mutability::Mut) => 1u8,
162+
CompletionItemRefMode::Dereference => 2u8,
141163
};
142-
hasher.update(prefix);
164+
hasher.update([descriminant]);
143165
hasher.update(u32::from(*text_size).to_le_bytes());
144166
}
167+
168+
hasher.update(item.import_to_add.len().to_le_bytes());
145169
for import_path in &item.import_to_add {
170+
hasher.update(import_path.len().to_le_bytes());
146171
hasher.update(import_path);
147172
}
173+
148174
hasher.finalize()
149175
}

0 commit comments

Comments
 (0)