859 lines
29 KiB
Rust
859 lines
29 KiB
Rust
// Package name
|
||
//
|
||
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
|
||
//
|
||
// This file is part of TAME.
|
||
//
|
||
// This program is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// This program is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU General Public License
|
||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
//! Package name and canonicalization.
|
||
//!
|
||
//! See [`CanonicalName`] for more information.
|
||
|
||
use crate::{
|
||
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
|
||
fmt::{DisplayWrapper, TtQuote},
|
||
parse::{util::SPair, Token},
|
||
span::Span,
|
||
sym::{
|
||
st::raw::{FW_SLASH, FW_SLASH_DOT},
|
||
GlobalSymbolIntern, GlobalSymbolInternBytes, GlobalSymbolResolve,
|
||
},
|
||
};
|
||
use std::{error::Error, fmt::Display};
|
||
|
||
/// Canonical package name.
|
||
///
|
||
/// A _canonical name_ is a package name that serves as a unique
|
||
/// representation of a package---that is,
|
||
/// it should not be possible to identify the same package by more than
|
||
/// one [`CanonicalName`].
|
||
/// In practice,
|
||
/// this means that the name begins with a forward slash;
|
||
/// contains no `..` or `.` as path-like components;
|
||
/// does not contain `//`;
|
||
/// and does not end in a trailing slash.
|
||
/// A canonical name is represented by [`CanonicalName`].
|
||
///
|
||
/// By representing package names in this way,
|
||
/// we are able to index them uniquely in any context in which they may
|
||
/// appear,
|
||
/// ensuring that we are able to uphold the commutative
|
||
/// definition/reference requirements of TAME.
|
||
///
|
||
/// Package names may develop further in the future;
|
||
/// for now,
|
||
/// their names are somewhat informal and usually generated from the
|
||
/// path on the filesystem relative to the project root.
|
||
///
|
||
/// Namespecs
|
||
/// =========
|
||
/// A _namespec_ is intended for package imports,
|
||
/// and represents a transformation that can be applied to a parent
|
||
/// package to produce the name of a package to import.
|
||
///
|
||
/// Namespecs (and package names) are designed to _look_ like paths,
|
||
/// and are indeed derived from paths on the filesystem,
|
||
/// but TAMER does not produce any hierarchy from that appearance.
|
||
/// Namespec resolution is a _purely lexical_ operation that is entirely
|
||
/// distinct from the operating system,
|
||
/// and the resulting name will be used to resolve package lookups
|
||
/// _relative to the project root_.[^symlink]
|
||
///
|
||
/// That is:
|
||
/// the purpose of a namespec is that of _convenience and
|
||
/// maintainability_;
|
||
/// it serves as an alternative to always providing a canonical name as
|
||
/// an import.
|
||
///
|
||
/// - If a namespec contains a leading slash,
|
||
/// then it is _absolute_ and will used verbatim as a
|
||
/// [`CanonicalName`].
|
||
/// All canonical names are therefore absolute namespecs,
|
||
/// providing unambiguous context-free package names.
|
||
///
|
||
/// - Otherwise, a namespec is _relative_ and must be resolved against a
|
||
/// parent [`CanonicalName`].
|
||
/// First,
|
||
/// the final component of the parent name
|
||
/// (the last slash and everything that follows)
|
||
/// is removed,
|
||
/// which intuitively matches the behavior of resolving against
|
||
/// sibling packages in the same directory.
|
||
///
|
||
/// - If the namespec contains any number of leading parent
|
||
/// markers `../`,
|
||
/// they will strip off a portion of [`CanonicalName`] up to and
|
||
/// including the last `/`,
|
||
/// recursively.
|
||
/// So `../quux` applied to `/bar/baz` resolves to `/quux`,
|
||
/// like a relative path on a Unix-like filesystem.
|
||
/// (As previously stated,
|
||
/// `/bar/baz` first becomes `/bar` so that the relative path
|
||
/// resolves as a sibling.)
|
||
///
|
||
/// - Otherwise,
|
||
/// the relative name is simply concatenated with the remaining
|
||
/// parent [`CanonicalName`],
|
||
/// separated by a `/`.
|
||
///
|
||
/// A namespec cannot use more parent markers than there are `/`s in the
|
||
/// parent [`CanonicalName`].
|
||
/// When viewing a name as a path,
|
||
/// this means that a namespec cannot traverse outside of the project
|
||
/// root.
|
||
///
|
||
/// A namespec cannot contain `.` in a context that would be interpreted by
|
||
/// a filesystem to mean the current directory,
|
||
/// since this would allow for multiple [`CanonicalName`]s for a given
|
||
/// package,
|
||
/// which is then not canonical.
|
||
/// Similarly,
|
||
/// a [`CanonicalName`] cannot contain `//`.
|
||
///
|
||
/// [^symlink]: This distinction is important.
|
||
/// Since a namespec always produces a [`CanonicalName`] which is treated
|
||
/// as a path relative to the project root,
|
||
/// paths will act intuitively despite symlinks,
|
||
/// which may otherwise exhibit unintuitive behavior when using `../`.
|
||
///
|
||
/// [`Span`] Characteristics
|
||
/// =========================
|
||
/// At the time of writing,
|
||
/// TAMER does not yet have an abstraction that indicates whether a given
|
||
/// [`SPair`] is an original lexeme tied to its original [`Span`].
|
||
/// It is assumed that [`CanonicalName`] is only ever constructed from
|
||
/// an [`SPair`] with its original [`Span`],
|
||
/// either via [`Self::from_canonical`] or
|
||
/// [`Self::resolve_namespec_rel`].
|
||
///
|
||
/// When a [`CanonicalNameError`] is raised,
|
||
/// its spans will only ever be derived from what it perceives as an
|
||
/// original [`Span`].
|
||
///
|
||
/// A [`CanonicalName`] may be constructed from a relative namespec via
|
||
/// [`Self::resolve_namespec_rel`].
|
||
/// In that case,
|
||
/// the span of the relative namespec is retained in the resulting
|
||
/// [`CanonicalName`].
|
||
/// Since a namespec can only contain parent markers (`..`) at the _head_ of
|
||
/// the namespec,
|
||
/// it will always be the case that the tail of a [`CanonicalName`] has
|
||
/// a proper span.
|
||
/// As long as we only slice up the span as far as we know is valid given
|
||
/// its construction,
|
||
/// we can return a [`Span`] in errors that is constructed from the tail
|
||
/// of the would-be [`CanonicalName`].
|
||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||
pub struct CanonicalName(SPair);
|
||
|
||
impl CanonicalName {
|
||
/// Verify that the provided `name` is in canonical form,
|
||
/// otherwise produce an error.
|
||
///
|
||
/// For more information on canonical names,
|
||
/// see [`Self`].
|
||
pub fn from_canonical(name: SPair) -> Result<Self, CanonicalNameError> {
|
||
let s = name.symbol().lookup_str();
|
||
|
||
// TODO: We shouldn't slice up a string and span separately,
|
||
// since the two may not be directly associated
|
||
// (e.g. the symbol may not be a lexeme matching the associated
|
||
// span if it's generated).
|
||
// See the section "Span Characteristics" above.
|
||
if s.ends_with('/') {
|
||
Err(CanonicalNameError::TrailingSlash(
|
||
name,
|
||
name.span().slice_tail(1),
|
||
))
|
||
} else if s.starts_with('/') {
|
||
Ok(Self(validate_components(name, s)?))
|
||
} else {
|
||
Err(CanonicalNameError::LeadingSlashExpected(
|
||
name,
|
||
// Slash should be inserted _just before_ the beginning of
|
||
// the span.
|
||
// This also works with zero-length spans.
|
||
name.span().slice_head(0),
|
||
))
|
||
}
|
||
}
|
||
|
||
/// Resolve a namespec relative to this canonical name.
|
||
///
|
||
/// For more information on relative paths,
|
||
/// see [`Self`].
|
||
pub fn resolve_namespec_rel(
|
||
&self,
|
||
namespec: SPair,
|
||
) -> Result<Self, CanonicalNameError> {
|
||
let Self(parent) = self;
|
||
|
||
let s = namespec.symbol().lookup_str();
|
||
|
||
if s.starts_with('/') {
|
||
Self::from_canonical(namespec)
|
||
} else {
|
||
let mut parent_parts = parent.symbol().lookup_str().rsplit('/');
|
||
let mut rel_parts = s.split('/').peekable();
|
||
|
||
let mut rm_bytes = 0;
|
||
|
||
// The rightmost component of the parent is eliminated,
|
||
// in much the same way that you'd expect a relative path to
|
||
// be a sibling of any current file.
|
||
parent_parts.next();
|
||
|
||
while rel_parts.peek() == Some(&"..") {
|
||
rel_parts.next();
|
||
|
||
let _ = parent_parts.next().filter(|s| s != &"").ok_or_else(
|
||
|| {
|
||
CanonicalNameError::TooManyParentMarkers(
|
||
namespec,
|
||
namespec.span().slice(rm_bytes, 2),
|
||
*self,
|
||
)
|
||
},
|
||
)?;
|
||
|
||
rm_bytes += 3; // "../"
|
||
// ^ this is a lie if we've ended `rel_parts`
|
||
}
|
||
|
||
let mut new = String::from(parent_parts.remainder().unwrap_or(""));
|
||
|
||
if rel_parts.peek().is_some() {
|
||
new.push('/');
|
||
rel_parts.intersperse("/").collect_into(&mut new);
|
||
}
|
||
|
||
// If we're empty,
|
||
// it's more intuitive to indicate that we went too far
|
||
// relative to the parent rather than throwing an error about
|
||
// an invalid canonical name.
|
||
if new.is_empty() {
|
||
Err(CanonicalNameError::TooManyParentMarkers(
|
||
namespec,
|
||
namespec.span().slice(rm_bytes - 3, 2),
|
||
*self,
|
||
))
|
||
} else {
|
||
Self::from_canonical(SPair(new.intern(), namespec.span()))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Produce an error if the provided name contains a parent marker `..` or
|
||
/// `.` in any of its components
|
||
/// (strings between `/`).
|
||
///
|
||
/// Conceptually,
|
||
/// this catches the equivalent of current and parent paths components,
|
||
/// such as `foo/../bar` and `./foo/bar`.
|
||
///
|
||
/// This will iterate through the entire name,
|
||
/// which is unnecessary work if we came from
|
||
/// [`CanonicalName::resolve_namespec_rel`],
|
||
/// but it's not much work and it's a safer implementation with fewer
|
||
/// conditions.
|
||
///
|
||
/// The purpose of this function is to provide guidance and rationale to the
|
||
/// user,
|
||
/// so it checks for very specific cases even if something more general
|
||
/// could have sufficed.
|
||
/// This produces very specific spans,
|
||
/// which also makes the scanning more challenging.
|
||
fn validate_components(
|
||
name: SPair,
|
||
s: &str,
|
||
) -> Result<SPair, CanonicalNameError> {
|
||
let bytes = s.as_bytes();
|
||
let len = bytes.len();
|
||
|
||
// Was not worth introducing a regex library,
|
||
// but might be worth replacing this if we've since introduced it.
|
||
// Equivalent to `/..(/|$)|/.(/$)|//`,
|
||
// but most of the code is for span slicing and error construction.
|
||
bytes
|
||
.windows(2) // A canonical path will always be at least 2 bytes
|
||
.enumerate()
|
||
// Again, be mindful of "Span Characteristics" above.
|
||
.find_map(|(pos, w)| match (w, bytes.get(pos + 2)) {
|
||
// `..`
|
||
(b"/.", Some(b'.'))
|
||
if matches!(bytes.get(pos + 3), Some(b'/') | None,) =>
|
||
{
|
||
Some(Err::<(), _>(CanonicalNameError::NonLeadingParentMarker(
|
||
name,
|
||
name.span().rslice(len - pos - 1, 2),
|
||
)))
|
||
}
|
||
// `.`
|
||
(b"/.", la @ (Some(b'/') | None)) => {
|
||
let roff = la.is_some() as usize;
|
||
Some(Err(CanonicalNameError::UselessComponent(
|
||
name,
|
||
SPair(
|
||
// ;_;
|
||
bytes[pos + roff..pos + roff + 2]
|
||
.intern_utf8()
|
||
.unwrap_or(FW_SLASH_DOT),
|
||
name.span().rslice(len - pos - roff, 2),
|
||
),
|
||
)))
|
||
}
|
||
(b"//", _) => Some(Err(CanonicalNameError::UselessComponent(
|
||
name,
|
||
SPair(FW_SLASH, name.span().rslice(len - pos - 1, 1)),
|
||
))),
|
||
_ => None,
|
||
})
|
||
.transpose()
|
||
.map(|_| name)
|
||
}
|
||
|
||
impl Display for CanonicalName {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||
match self {
|
||
Self(name) => Display::fmt(name, f),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<CanonicalName> for SPair {
|
||
fn from(value: CanonicalName) -> Self {
|
||
match value {
|
||
CanonicalName(spair) => spair,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<CanonicalName> for Span {
|
||
fn from(value: CanonicalName) -> Self {
|
||
match value {
|
||
CanonicalName(spair) => spair.into(),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
pub enum CanonicalNameError {
|
||
/// Attempted to create a new [`CanonicalName`] using a symbol that was
|
||
/// expected to already be in canonical form,
|
||
/// but was missing a leading slash.
|
||
///
|
||
/// The latter span is the position at which a leading slash was
|
||
/// expected.
|
||
LeadingSlashExpected(SPair, Span),
|
||
|
||
/// Namespecs cannot include trailing slashes since it makes the package
|
||
/// name look like a directory.
|
||
TrailingSlash(SPair, Span),
|
||
|
||
/// A relative namespec attempts to go past the project root because it
|
||
/// includes too many `..` markers relative to a given canonical
|
||
/// name.
|
||
///
|
||
/// The first [`SPair`] represents the failed relative namespec,
|
||
/// the [`Span`] represents the marker that caused the failure,
|
||
/// and the [`CanonicalName`] represents the parent that the namespec
|
||
/// is being resolved against.
|
||
TooManyParentMarkers(SPair, Span, CanonicalName),
|
||
|
||
/// Relative namespecs may only have parent markers (`..`) in a leading
|
||
/// position.
|
||
///
|
||
/// The provided [`Span`] is the position of the marker that is in
|
||
/// error.
|
||
NonLeadingParentMarker(SPair, Span),
|
||
|
||
/// A portion of the namespec can be removed and still result in the
|
||
/// same namespec when interpreted as a path on the filesystem.
|
||
///
|
||
/// This restriction is important to ensure that multiple namespecs
|
||
/// cannot be used to represent the same package,
|
||
/// which would cause package index lookup failures.
|
||
UselessComponent(SPair, SPair),
|
||
}
|
||
|
||
impl Display for CanonicalNameError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||
use CanonicalNameError::*;
|
||
|
||
match self {
|
||
LeadingSlashExpected(name, _) => write!(
|
||
f,
|
||
"non-canonical package name {} where canonical name was expected",
|
||
TtQuote::wrap(name),
|
||
),
|
||
|
||
TrailingSlash(name, _) => write!(
|
||
f,
|
||
"package namespec {} must not include a trailing slash",
|
||
TtQuote::wrap(name),
|
||
),
|
||
|
||
TooManyParentMarkers(name, _, _) => write!(
|
||
f,
|
||
"package namespec {} resolves outside of package root",
|
||
TtQuote::wrap(name),
|
||
),
|
||
|
||
NonLeadingParentMarker(name, _) => write!(
|
||
f,
|
||
"package namespec {} contains parent marker in a \
|
||
non-head position",
|
||
TtQuote::wrap(name),
|
||
),
|
||
|
||
UselessComponent(name, part) => write!(
|
||
f,
|
||
"package namespec {} contains an invalid {}",
|
||
TtQuote::wrap(name),
|
||
TtQuote::wrap(part),
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Error for CanonicalNameError {}
|
||
|
||
impl Diagnostic for CanonicalNameError {
|
||
fn describe(&self) -> Vec<AnnotatedSpan> {
|
||
use CanonicalNameError::*;
|
||
|
||
match self {
|
||
LeadingSlashExpected(_, at) => vec![
|
||
at.error("expected name beginning with '/'"),
|
||
at.help(
|
||
"canonical names begin with '/' and uniquely identify a \
|
||
package relative to a project root",
|
||
),
|
||
],
|
||
|
||
TrailingSlash(_, at) => vec![
|
||
at.error("trailing slash not permitted in package namespec"),
|
||
at.help(
|
||
"a trailing slash makes a package name look like a directory, \
|
||
but a package is always associated with an \
|
||
explicit file",
|
||
)
|
||
],
|
||
|
||
TooManyParentMarkers(_, at, parent) => vec![
|
||
parent.note(format!(
|
||
"parent package is {}",
|
||
TtQuote::wrap(Into::<SPair>::into(*parent))
|
||
)),
|
||
at.error("this marker exceeds the parent package depth"),
|
||
at.help(
|
||
"the marker \"..\" treats the parent package name like a \
|
||
path and moves into parent directories"
|
||
),
|
||
at.help(
|
||
"for both practical and security reasons, \
|
||
you cannot resolve past the project root \
|
||
relative to the parent package"
|
||
),
|
||
],
|
||
|
||
NonLeadingParentMarker(_, at) => vec![
|
||
at.error(
|
||
"parent marker must only appear at the head of a namespec"
|
||
),
|
||
at.help(
|
||
"for example: the namespec `../bar` is valid, \
|
||
but `../foo/../bar` is not permitted"
|
||
),
|
||
at.help(
|
||
"this restriction helps to avoid unnecessarily confusing \
|
||
namespecs"
|
||
)
|
||
],
|
||
|
||
UselessComponent(_, part) => vec![
|
||
part.error("this component is not permitted here"),
|
||
part.help(format!(
|
||
"remove the unnecessary {} from the namespec",
|
||
TtQuote::wrap(part)
|
||
)),
|
||
part.help(
|
||
"since namespecs are mapped to paths on the filesystem, \
|
||
certain strings are not permitted that would \
|
||
otherwise allow the same package to be represented \
|
||
by multiple different namespecs"
|
||
)
|
||
],
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use super::*;
|
||
use crate::span::dummy::{DUMMY_CONTEXT as DC, *};
|
||
|
||
type Sut = CanonicalName;
|
||
|
||
#[test]
|
||
fn slash_prefixed_name_is_canonical() {
|
||
let name = SPair("/foo/bar".into(), S1);
|
||
|
||
assert_eq!(
|
||
name,
|
||
Sut::from_canonical(name)
|
||
.expect("failed to canonicalize name")
|
||
.into()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn from_canonical_fails_with_non_prefixed_name() {
|
||
let a = DC.span(0, 13);
|
||
let b = DC.span(0, 0); // expected location of '/'
|
||
|
||
let name = SPair("not/canonical".into(), a);
|
||
// [-----------]
|
||
// 0 12
|
||
// | A
|
||
// B (zero-length)
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::LeadingSlashExpected(name, b)),
|
||
Sut::from_canonical(name),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn from_canonical_must_not_end_in_trailing_slash() {
|
||
let a = DC.span(0, 16);
|
||
let b = DC.span(15, 1);
|
||
|
||
let name = SPair("/trailing/slash/".into(), a);
|
||
// [--------------]
|
||
// 0 15
|
||
// A |
|
||
// B
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::TrailingSlash(name, b)),
|
||
Sut::from_canonical(name),
|
||
);
|
||
}
|
||
|
||
// Relative namespec without `..`.
|
||
#[test]
|
||
fn resolve_namespec_against_canonical_without_parent() {
|
||
let parent = canonical(SPair("/parent/pkg".into(), S1));
|
||
// ^^^ this is stripped
|
||
let rel = SPair("rel/to/parent".into(), S2);
|
||
|
||
assert_eq!(
|
||
// Note that this assumes the span of the _child_ since it's
|
||
// more specific and what the user almost certainly wants if
|
||
// there is a diagnostic message referencing this name.
|
||
Ok(canonical(SPair("/parent/rel/to/parent".into(), S2))),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_absolute_namespec_overrides_parent() {
|
||
let parent = canonical(SPair("/parent/to/override".into(), S1));
|
||
let rel = SPair("/abs/name".into(), S2);
|
||
|
||
// A canonical name is an absolute namespec.
|
||
#[rustfmt::skip]
|
||
assert_eq!(
|
||
Ok(canonical(rel)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
// Resolving parent markers
|
||
// (e.g. "../foo")
|
||
// is a _purely lexical_ operation.
|
||
#[test]
|
||
fn resolve_namespec_with_leading_parent_markers_and_compatible_parent() {
|
||
let parent = canonical(SPair("/one/two/three".into(), S1));
|
||
// note that the rightmost component ^^^^^
|
||
// will be stripped _before_ processing
|
||
// the first `..`
|
||
|
||
assert_eq!(
|
||
parent.resolve_namespec_rel(SPair("../a/pkg".into(), S2)),
|
||
Ok(canonical(SPair("/one/a/pkg".into(), S2))),
|
||
);
|
||
|
||
assert_eq!(
|
||
parent.resolve_namespec_rel(SPair("../../b/pkg".into(), S3)),
|
||
Ok(canonical(SPair("/b/pkg".into(), S3))),
|
||
);
|
||
|
||
assert_eq!(
|
||
parent.resolve_namespec_rel(SPair("..".into(), S4)),
|
||
Ok(canonical(SPair("/one".into(), S4))),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_namespec_with_leading_parent_markers_too_far() {
|
||
let parent = canonical(SPair("/parent/too/short".into(), S1));
|
||
|
||
let a = DC.span(0, 15);
|
||
let b = DC.span(6, 2);
|
||
let rel = SPair("../../../foo/bar".into(), a);
|
||
// [-----++-------]
|
||
// 0 || 15
|
||
// A ||
|
||
// []
|
||
// 6 `7
|
||
// B
|
||
// `this marker goes one too far
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::TooManyParentMarkers(rel, b, parent)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_namespec_with_leading_parent_markers_yielding_empty() {
|
||
let parent = canonical(SPair("/will/be/empty/pkg".into(), S1));
|
||
|
||
let a = DC.span(0, 8);
|
||
let b = DC.span(6, 2);
|
||
let rel = SPair("../../..".into(), a);
|
||
// [-----+]
|
||
// 0 ||`7
|
||
// A ||
|
||
// []
|
||
// 6 `7
|
||
// B
|
||
// `only goes too far because the result would
|
||
// be `/` which is not a canonical name
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::TooManyParentMarkers(rel, b, parent)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_namespec_with_leading_parent_markers_trailing_slash() {
|
||
let parent = canonical(SPair("/will/be/empty/pkg".into(), S1));
|
||
|
||
let a = DC.span(0, 13);
|
||
let b = DC.span(12, 1);
|
||
let rel = SPair("../../../foo/".into(), a);
|
||
// [-----------]
|
||
// 0 |`12
|
||
// A |
|
||
// B
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::TrailingSlash(
|
||
SPair("/foo/".into(), a),
|
||
b,
|
||
)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_namespec_with_non_head_parent_marker() {
|
||
let parent = canonical(SPair("/foo/pkg".into(), S1));
|
||
|
||
let a = DC.span(0, 10);
|
||
let b = DC.span(4, 2);
|
||
let rel = SPair("bar/../baz".into(), a);
|
||
// [---++---]
|
||
// 0 || 9
|
||
// A ||
|
||
// []
|
||
// 4 `5
|
||
// B
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::NonLeadingParentMarker(
|
||
SPair("/foo/bar/../baz".into(), a),
|
||
b,
|
||
)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
|
||
// The same error should happen for a supposedly-canonical name too.
|
||
let c = DC.span(0, 11);
|
||
let d = DC.span(5, 2);
|
||
let name = SPair("/bar/../baz".into(), c);
|
||
// [----++---]
|
||
// 0 || 10
|
||
// C ||
|
||
// []
|
||
// 4 `5
|
||
// D
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::NonLeadingParentMarker(name, d,)),
|
||
CanonicalName::from_canonical(name),
|
||
);
|
||
}
|
||
|
||
// The above catches `/../`,
|
||
// but make sure we catch `/..$`.
|
||
#[test]
|
||
fn resolve_namespec_with_non_head_parent_marker_at_tail() {
|
||
let parent = canonical(SPair("/foo/pkg".into(), S1));
|
||
|
||
let a = DC.span(0, 6);
|
||
let b = DC.span(4, 2);
|
||
let rel = SPair("bar/..".into(), a);
|
||
// [---+]
|
||
// 0 ||`5
|
||
// A ||
|
||
// []
|
||
// 4 `5
|
||
// B
|
||
|
||
assert_eq!(
|
||
Err(CanonicalNameError::NonLeadingParentMarker(
|
||
SPair("/foo/bar/..".into(), a),
|
||
b,
|
||
)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
// `/./` doesn't have a special lexical meaning for `CanonicalName`,
|
||
// which is the problem---when
|
||
// we go to resolve the name as a path,
|
||
// multiple names would resolve to the same package.
|
||
#[test]
|
||
fn resolve_namespec_with_single_dot() {
|
||
let parent = canonical(SPair("/foo/pkg".into(), S1));
|
||
|
||
let a = DC.span(0, 9);
|
||
let b = DC.span(0, 2);
|
||
let rel = SPair("./bar/baz".into(), a);
|
||
// [+------]
|
||
// 0| 8
|
||
// [] A
|
||
// 1
|
||
// B
|
||
|
||
// "./" can be removed from the above path and still retain the same
|
||
// meaning.
|
||
assert_eq!(
|
||
Err(CanonicalNameError::UselessComponent(
|
||
SPair("/foo/./bar/baz".into(), a),
|
||
SPair("./".into(), b),
|
||
)),
|
||
parent.resolve_namespec_rel(rel),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn canonical_name_with_single_dot_middle() {
|
||
let a = DC.span(0, 10);
|
||
let b = DC.span(5, 2);
|
||
let name = SPair("/foo/./bar".into(), a);
|
||
// [-----++---]
|
||
// 0 || 9
|
||
// A []
|
||
// 5 `6
|
||
// B
|
||
|
||
// "./" can be removed from the above path and still retain the same
|
||
// meaning.
|
||
assert_eq!(
|
||
Err(CanonicalNameError::UselessComponent(
|
||
name,
|
||
SPair("./".into(), b),
|
||
)),
|
||
CanonicalName::from_canonical(name),
|
||
);
|
||
}
|
||
|
||
// This error is phrased as the same problem as the above,
|
||
// but in reality,
|
||
// `.` in a tail position would act as its own package name if we
|
||
// append an extension to it
|
||
// (e.g. `/foo/bar/..xmlo`).
|
||
// Best to flat out reject that.
|
||
#[test]
|
||
fn canonical_name_with_single_dot_end() {
|
||
let a = DC.span(0, 10);
|
||
let b = DC.span(8, 2);
|
||
let name = SPair("/foo/bar/.".into(), a);
|
||
// [--------+]
|
||
// 0 |9
|
||
// A []
|
||
// 8
|
||
// B
|
||
|
||
// "/." can be removed from the above path and still retain the same
|
||
// meaning.
|
||
assert_eq!(
|
||
Err(CanonicalNameError::UselessComponent(
|
||
name,
|
||
SPair("/.".into(), b),
|
||
)),
|
||
CanonicalName::from_canonical(name),
|
||
);
|
||
}
|
||
|
||
// Adjacent slashes are collapsed into a single slash by many Unix-like
|
||
// filesystems,
|
||
// and so are equivalent to `./`.
|
||
#[test]
|
||
fn canonical_name_double_slash() {
|
||
let a = DC.span(0, 9);
|
||
let b = DC.span(5, 1);
|
||
let name = SPair("/foo//bar".into(), a);
|
||
// [-----+--]
|
||
// 0 | 8
|
||
// A ⌷
|
||
// 6
|
||
// B
|
||
|
||
// A "/" can be removed from the above path and still retain the
|
||
// same meaning.
|
||
assert_eq!(
|
||
Err(CanonicalNameError::UselessComponent(
|
||
name,
|
||
SPair("/".into(), b),
|
||
)),
|
||
CanonicalName::from_canonical(name),
|
||
);
|
||
}
|
||
|
||
// Make sure we're not too naive and restrictive in our search.
|
||
#[test]
|
||
fn canonical_name_permits_parent_marker_like_strings() {
|
||
CanonicalName::from_canonical(SPair("/foo/..bar/baz".into(), S1))
|
||
.unwrap();
|
||
CanonicalName::from_canonical(SPair("/foo/bar/baz..".into(), S2))
|
||
.unwrap();
|
||
}
|
||
|
||
fn canonical(name: SPair) -> CanonicalName {
|
||
CanonicalName::from_canonical(name).expect(&format!(
|
||
"broken test case: failed instantiate CanonicalName \
|
||
with \"{name}\""
|
||
))
|
||
}
|
||
}
|