#![allow(clippy::unwrap_used, clippy::expect_used)]
use tower_lsp_server::ls_types::Uri;
#[must_use]
pub fn test_uri(unix_path: &str) -> Uri {
#[cfg(windows)]
let owned;
#[cfg(windows)]
let path: &str = {
owned = format!("C:{unix_path}");
&owned
};
#[cfg(not(windows))]
let path: &str = unix_path;
Uri::from_file_path(path).expect("test_uri: fixture path must be a valid file URI")
}
pub struct MockMetadata {
name: crate::PackageName,
latest_version: crate::ConcreteVersion,
}
impl MockMetadata {
#[must_use]
pub fn new(
name: impl Into<crate::PackageName>,
latest_version: impl Into<crate::ConcreteVersion>,
) -> Self {
Self {
name: name.into(),
latest_version: latest_version.into(),
}
}
}
impl crate::Metadata for MockMetadata {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn description(&self) -> Option<&str> {
None
}
fn repository(&self) -> Option<&str> {
None
}
fn documentation(&self) -> Option<&str> {
None
}
fn latest_version(&self) -> &crate::ConcreteVersion {
&self.latest_version
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub const ADVERSARIAL_URL_SEGMENTS: &[&str] = &[
".",
"..",
"../../etc/passwd",
"a b",
"a?b=1",
"a#frag",
"%2e%2e",
];
pub fn assert_dot_segment_gated_or_contained(
resolve: impl Fn(&str) -> Option<String>,
expected_host: &str,
expected_path_prefix: &str,
) {
assert_dot_segment_gated_or_contained_transformed(
resolve,
str::to_string,
expected_host,
expected_path_prefix,
);
}
pub fn assert_dot_segment_gated_or_contained_transformed(
resolve: impl Fn(&str) -> Option<String>,
transform: impl Fn(&str) -> String,
expected_host: &str,
expected_path_prefix: &str,
) {
for segment in ADVERSARIAL_URL_SEGMENTS {
let Some(built) = resolve(segment) else {
continue;
};
let parsed = url::Url::parse(&built).unwrap_or_else(|e| {
panic!("adversarial segment {segment:?} produced an unparsable URL {built:?}: {e}")
});
assert_eq!(
parsed.host_str(),
Some(expected_host),
"adversarial segment {segment:?} escaped host: {built}"
);
assert!(
parsed.path().starts_with(expected_path_prefix),
"adversarial segment {segment:?} escaped path prefix {expected_path_prefix:?}: {built}"
);
let expected_fragment = transform(segment);
assert!(
!expected_fragment.is_empty(),
"adversarial segment {segment:?} transformed to an empty fragment but `resolve` \
still returned Some(url) — an empty identifier must be rejected (return `None`) \
before reaching the sink, since an empty survival check would trivially pass \
(\"\".contains(\"\") is always true) and hide a real gate deletion"
);
if expected_fragment == "." || expected_fragment == ".." {
let matched = parsed.path_segments().is_some_and(|mut segs| {
segs.any(|seg| {
urlencoding::decode(seg)
.is_ok_and(|decoded| decoded.starts_with(expected_fragment.as_str()))
})
});
assert!(
matched,
"adversarial segment {segment:?} (expected to survive as {expected_fragment:?}) \
did not survive as its own path segment (silently dropped/collapsed by \
dot-segment normalization?): built url {built}"
);
} else {
let decoded_path = urlencoding::decode(parsed.path()).unwrap_or_else(|e| {
panic!(
"adversarial segment {segment:?}'s URL path failed to percent-decode: {built:?}: {e}"
)
});
assert!(
decoded_path.contains(&expected_fragment),
"adversarial segment {segment:?} (expected to survive as {expected_fragment:?}) \
did not survive intact in the decoded path (silently dropped/collapsed by \
dot-segment normalization?): decoded path {decoded_path:?}, built url {built}"
);
}
}
}
#[cfg(feature = "test-util")]
#[derive(Clone, Default)]
struct CapturingWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
#[cfg(feature = "test-util")]
impl std::io::Write for CapturingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(feature = "test-util")]
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CapturingWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
#[cfg(feature = "test-util")]
fn capturing_subscriber_at(
max_level: tracing::Level,
) -> (CapturingWriter, impl tracing::Subscriber) {
let writer = CapturingWriter::default();
let subscriber = tracing_subscriber::fmt()
.with_writer(writer.clone())
.with_max_level(max_level)
.without_time()
.with_target(false)
.finish();
(writer, subscriber)
}
#[cfg(feature = "test-util")]
fn capturing_subscriber() -> (CapturingWriter, impl tracing::Subscriber) {
capturing_subscriber_at(tracing::Level::INFO)
}
#[cfg(feature = "test-util")]
#[must_use]
pub fn capture_tracing_output(f: impl FnOnce()) -> String {
let (writer, subscriber) = capturing_subscriber();
tracing::subscriber::with_default(subscriber, f);
String::from_utf8(writer.0.lock().unwrap().clone()).expect("tracing output is valid utf8")
}
#[cfg(feature = "test-util")]
#[must_use]
pub fn capture_tracing_output_at(max_level: tracing::Level, f: impl FnOnce()) -> String {
let (writer, subscriber) = capturing_subscriber_at(max_level);
tracing::subscriber::with_default(subscriber, f);
String::from_utf8(writer.0.lock().unwrap().clone()).expect("tracing output is valid utf8")
}
#[cfg(feature = "test-util")]
pub async fn capture_tracing_output_async(fut: impl std::future::Future<Output = ()>) -> String {
let (writer, subscriber) = capturing_subscriber();
let guard = tracing::subscriber::set_default(subscriber);
fut.await;
drop(guard);
String::from_utf8(writer.0.lock().unwrap().clone()).expect("tracing output is valid utf8")
}
#[cfg(feature = "test-util")]
pub async fn capture_tracing_output_async_at(
max_level: tracing::Level,
fut: impl std::future::Future<Output = ()>,
) -> String {
let (writer, subscriber) = capturing_subscriber_at(max_level);
let guard = tracing::subscriber::set_default(subscriber);
fut.await;
drop(guard);
String::from_utf8(writer.0.lock().unwrap().clone()).expect("tracing output is valid utf8")
}
struct StubDependency {
name: crate::PackageName,
}
impl crate::Dependency for StubDependency {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn name_range(&self) -> tower_lsp_server::ls_types::Range {
tower_lsp_server::ls_types::Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
struct StubParseResult {
dependencies: Vec<StubDependency>,
uri: Uri,
}
impl crate::ParseResult for StubParseResult {
fn dependencies(&self) -> Vec<&dyn crate::Dependency> {
self.dependencies
.iter()
.map(|d| d as &dyn crate::Dependency)
.collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &Uri {
&self.uri
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[must_use]
pub fn stub_parse_result_with_dependencies(count: usize) -> Box<dyn crate::ParseResult> {
Box::new(StubParseResult {
dependencies: (0..count)
.map(|i| StubDependency {
name: crate::PackageName::new(format!("dep-{i}")),
})
.collect(),
uri: test_uri("/project/manifest.toml"),
})
}