tamer: src::asg::graph::object::pkg::name: New module

This introduces, but does not yet integrate, `CanonicalName`, which not only
represents canonicalized package names, but handles namespec resolution.

The term "namespec" is motivated by Git's use of *spec (e.g. refspec)
referring to various ways of specifying a particular object.  Names look
like paths, and are derived from them, but they _are not paths_.  Their
resolution is a purely lexical operation, and they include a number of
restrictions to simplify their clarity and handling.  I expect them to
evolve more in the future, and I've had ideas to do so for quite some time.

In particular, resolving packages in this way and then loading the from the
filesystem relative to the project root will ensure that
traversing (conceptually) to a parent directory will not operate
unintuitively with symlinks.  The path will always resolve unambigiously.

(With that said, if the symlink is to a shared directory with different
directory structures, that doesn't solve the compilation problem---we'll
have to move object files into a project-specific build directory to handle
that.)

Span Slicing
------------
Okay, it's worth commenting on the horridity of the path name slicing that
goes on here.  Care has been taken to ensure that spans will be able to be
properly sliced in all relevant contexts, and there are plenty of words
devoted to that in the documentation committed here.

But there is a more fundamental problem here that I regret not having solved
earlier, because I don't have the time for it right now: while we do have
SPair, it makes no guarantees that the span associated with the corresponding
SymbolId is actually the span that matches the original source lexeme.  In
fact, it's often not.

This is a problem when we want to slice up a symbol in an SPair and produce
a sensible span.  If it _is_ a source lexeme with its original span, that's
no problem.  But if it's _not_, then the two are not in sync, and slicing up
the span won't produce something that actually makes sense to the user.  Or,
worse (or maybe it's not worse?), it may cause a panic if the slicing is out
of bounds.

The solution in the future might be to store explicitly the state of an
SPair, or call it Lexeme, or something, so that we know the conditions under
which slicing is safe.  If I ever have time for that in this project.

But the result of the lack of a proper abstraction really shows here: this
is some of the most confusing code in TAMER, and it's really not doing
anything all that complicated.  It is disproportionately confusing.

DEV-13162
main
Mike Gerwitz 2023-05-04 12:28:08 -04:00
parent e3a68aaf9e
commit a9d0f43684
5 changed files with 862 additions and 1 deletions

View File

@ -14,7 +14,7 @@ cd "$(dirname "$0")"
#
# So, to be clear:
# do _not_ do something like `grep -rl 'object_rel!' ../src/asg`.
find ../src/asg/graph/object/ -name '*.rs' \
find ../src/asg/graph/object/ -maxdepth 1 -name '*.rs' \
-a \! -name 'rel.rs' \
-a \! -name 'test.rs' \
| xargs awk -f asg-ontviz.awk

View File

@ -31,6 +31,8 @@ use std::fmt::Display;
#[cfg(doc)]
use super::ObjectKind;
mod name;
#[derive(Debug, PartialEq, Eq)]
pub struct Pkg(Span, PathSpec);

View File

@ -0,0 +1,850 @@
// 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 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}\""
))
}
}

View File

@ -66,6 +66,12 @@
// Collecting interators into existing objects.
// Can be done manually in a more verbose way.
#![feature(iter_collect_into)]
// Concise and descriptive.
// Can be done manually in a more verbose way.
#![feature(str_split_remainder)]
// Concise and descriptive.
// Can be done manually in a more verbose way.
#![feature(iter_intersperse)]
// Used for const params like `&'static str` in `crate::fmt`.
// If this is not stabalized,
// then we can do without by changing the abstraction;

View File

@ -718,6 +718,9 @@ pub mod st {
L_TPLP_VALUES: str "@values@",
FW_SLASH: str "/",
FW_SLASH_DOT: str "/.",
CC_ANY_OF: cid "anyOf",
U_TRUE: cid "TRUE",