use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use bytes::{Bytes, BytesMut};
use lsp_types::{Position, Range, TextDocumentContentChangeEvent, Uri};
use tokio_util::codec::{Decoder, Encoder};
use crate::documents::{Document, PositionEncoding};
use crate::transport::envelope;
use crate::transport::framing::ContentLengthCodec;
use crate::uri_key::{UriKey, percent_decode};
const LARGE_INPUT_LIMIT: usize = 65_536;
const URI_INPUT_LIMIT: usize = 4_096;
const LIFECYCLE_INPUT_LIMIT: usize = 16_384;
pub fn envelope(data: &[u8]) {
if data.len() > LARGE_INPUT_LIMIT {
return;
}
let parsed = envelope::parse(Bytes::copy_from_slice(data));
let serialized = envelope::serialize(&parsed).expect("parsed envelope must serialize");
serde_json::from_slice::<serde_json::Value>(&serialized)
.expect("serialized envelope must be valid JSON");
if !matches!(parsed, crate::RawMessage::ProtocolError { .. }) {
let reparsed = envelope::parse(Bytes::from(serialized.clone()));
let reserialized =
envelope::serialize(&reparsed).expect("reparsed envelope must serialize");
assert_eq!(
serialized, reserialized,
"valid envelope serialization is unstable"
);
}
}
pub fn content_length(data: &[u8]) {
if data.len() > LARGE_INPUT_LIMIT {
return;
}
let expanded = expand_visible_crlf(data);
let mut codec = ContentLengthCodec::new(LARGE_INPUT_LIMIT);
let mut input = BytesMut::new();
for byte in expanded {
input.extend_from_slice(&[byte]);
loop {
let decoded = match codec.decode(&mut input) {
Ok(Some(body)) => body,
Ok(None) | Err(_) => break,
};
let mut encoded = BytesMut::new();
codec
.encode(decoded.clone(), &mut encoded)
.expect("a decoded bounded body must re-encode");
let mut verifier = ContentLengthCodec::new(LARGE_INPUT_LIMIT);
assert_eq!(
verifier
.decode(&mut encoded)
.expect("encoded frame decodes"),
Some(decoded)
);
assert!(encoded.is_empty());
}
}
}
pub fn uri_identity(data: &[u8]) {
if data.len() > URI_INPUT_LIMIT {
return;
}
let (left, right) = if let Some(split) = data.iter().position(|byte| *byte == 0) {
(&data[..split], &data[split + 1..])
} else {
(data, data)
};
let left_text = String::from_utf8_lossy(left);
let right_text = String::from_utf8_lossy(right);
let _ = percent_decode(&left_text);
let _ = percent_decode(&right_text);
let (Ok(left_uri), Ok(right_uri)) = (
Uri::from_str(left_text.trim()),
Uri::from_str(right_text.trim()),
) else {
return;
};
let left_key = UriKey::new(&left_uri);
let right_key = UriKey::new(&right_uri);
if left_key == right_key {
assert_eq!(
hash(&left_key),
hash(&right_key),
"equal URI keys hash differently"
);
}
}
pub fn position_conversion(data: &[u8]) {
if data.len() > LARGE_INPUT_LIMIT {
return;
}
let text = String::from_utf8_lossy(data).into_owned();
let uri = Uri::from_str("file:///fuzz.txt").expect("static URI parses");
let document = Document::provider_snapshot(uri, text.clone());
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
for offset in text
.char_indices()
.map(|(offset, _)| offset)
.chain(std::iter::once(text.len()))
{
let position = document
.offset_to_position(encoding, offset)
.expect("in-range character boundary has a position");
if let Some(roundtrip) = document.position_to_offset(encoding, position) {
assert_eq!(offset, roundtrip, "position conversion did not round-trip");
}
}
if data.len() >= 8 {
let position = Position::new(read_u32(&data[..4]), read_u32(&data[4..8]));
let _ = document.position_to_offset(encoding, position);
}
}
}
pub fn incremental_edits(data: &[u8]) {
if data.len() > LARGE_INPUT_LIMIT {
return;
}
let controls = data.get(..9).unwrap_or(data);
let payload = data.get(controls.len()..).unwrap_or_default();
let split = payload
.iter()
.position(|byte| *byte == 0)
.unwrap_or(payload.len());
let initial = String::from_utf8_lossy(&payload[..split]).into_owned();
let replacement =
String::from_utf8_lossy(payload.get(split + 1..).unwrap_or_default()).into_owned();
let uri = Uri::from_str("file:///fuzz.txt").expect("static URI parses");
let mut document = Document::provider_snapshot(uri, initial.clone());
let encoding = if controls.first().copied().unwrap_or_default() & 1 == 0 {
PositionEncoding::Utf8
} else {
PositionEncoding::Utf16
};
let change = if controls.first().copied().unwrap_or_default() % 5 == 0 {
TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: replacement,
}
} else {
TextDocumentContentChangeEvent {
range: Some(Range::new(
position_from(controls.get(1..5).unwrap_or_default()),
position_from(controls.get(5..9).unwrap_or_default()),
)),
range_length: None,
text: replacement,
}
};
let before = document.text();
let result = document.apply_change(encoding, change, LARGE_INPUT_LIMIT);
if result.is_err() {
assert_eq!(
document.text(),
before,
"a rejected edit mutated the document"
);
} else {
assert!(document.text().len() <= LARGE_INPUT_LIMIT);
}
}
pub fn lifecycle_sequences(data: &[u8]) {
if data.len() <= LIFECYCLE_INPUT_LIMIT {
crate::client_endpoint::fuzz_lifecycle_sequence(data);
}
}
fn expand_visible_crlf(data: &[u8]) -> Vec<u8> {
let mut output = Vec::with_capacity(data.len());
let mut index = 0;
while index < data.len() {
if data[index..].starts_with(br"\r") {
output.push(b'\r');
index += 2;
} else if data[index..].starts_with(br"\n") {
output.push(b'\n');
index += 2;
} else {
output.push(data[index]);
index += 1;
}
}
output
}
fn position_from(bytes: &[u8]) -> Position {
let mut padded = [0; 4];
padded[..bytes.len().min(4)].copy_from_slice(&bytes[..bytes.len().min(4)]);
Position::new(
u16::from_le_bytes([padded[0], padded[1]]) as u32,
u16::from_le_bytes([padded[2], padded[3]]) as u32,
)
}
fn read_u32(bytes: &[u8]) -> u32 {
u32::from_le_bytes(bytes.try_into().expect("caller supplies four bytes"))
}
fn hash(value: &impl Hash) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}