use crate::error::Result;
use crate::lsp_helpers::{dot_segment_rejection_error, is_dot_segment};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("path segment {segment:?} is a dot-segment (\".\" or \"..\")")]
pub struct DotSegmentError<'a> {
segment: &'a str,
}
impl<'a> DotSegmentError<'a> {
#[must_use]
pub const fn segment(&self) -> &'a str {
self.segment
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SafePathSegment<'a>(&'a str);
impl<'a> SafePathSegment<'a> {
pub fn new(segment: &'a str) -> std::result::Result<Self, DotSegmentError<'a>> {
if is_dot_segment(segment) {
Err(DotSegmentError { segment })
} else {
Ok(Self(segment))
}
}
pub fn checked_or_reject(
segment: &'a str,
name: &str,
context: &str,
registry: &'static str,
) -> Result<Self> {
Self::new(segment)
.map_err(|_| dot_segment_rejection_error("is_dot_segment", context, name, registry))
}
#[must_use]
pub const fn as_str(&self) -> &'a str {
self.0
}
}
impl std::fmt::Display for SafePathSegment<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SegmentedPathName<'a> {
Single(SafePathSegment<'a>),
Prefixed(SafePathSegment<'a>, SafePathSegment<'a>),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_rejects_bare_dot_dot() {
assert!(SafePathSegment::new("..").is_err());
}
#[test]
fn test_new_rejects_bare_dot() {
assert!(SafePathSegment::new(".").is_err());
}
#[test]
fn test_new_accepts_normal_names() {
assert!(SafePathSegment::new("left-pad").is_ok());
assert!(SafePathSegment::new("../../search").is_ok());
}
#[test]
fn test_error_carries_the_rejected_segment() {
let err = SafePathSegment::new("..").unwrap_err();
assert_eq!(err.segment(), "..");
}
#[test]
fn test_as_str_roundtrips() {
let segment = SafePathSegment::new("express").unwrap();
assert_eq!(segment.as_str(), "express");
}
#[test]
fn test_checked_or_reject_accepts_normal_segment() {
assert!(SafePathSegment::checked_or_reject("express", "express", "ctx", "npm").is_ok());
}
#[test]
fn test_checked_or_reject_rejects_dot_segment() {
let err = SafePathSegment::checked_or_reject("..", "@a/..", "ctx", "npm").unwrap_err();
assert!(matches!(
err,
crate::error::DepsError::PackageNotFound { .. }
));
}
#[test]
fn test_checked_or_reject_error_reports_full_name_not_bare_segment() {
let err = SafePathSegment::checked_or_reject("..", "@a/..", "ctx", "npm").unwrap_err();
let crate::error::DepsError::PackageNotFound { package, .. } = err else {
panic!("expected PackageNotFound, got {err:?}");
};
assert_eq!(package, "@a/..");
}
}