Skip to main content

markdown_compiler/content/
path.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
7#[serde(transparent)]
8pub struct LogicalAssetPath(String);
9
10impl LogicalAssetPath {
11    pub fn parse(value: &str) -> Result<Self, LogicalTreePathError> {
12        validate_portable_path(value)?;
13        let mut components = value.split('/');
14        if components.next() != Some("assets") || components.next().is_none() {
15            return Err(LogicalTreePathError::WrongAssetNamespace);
16        }
17        Ok(Self(value.to_owned()))
18    }
19
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23}
24
25impl fmt::Display for LogicalAssetPath {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        formatter.write_str(self.as_str())
28    }
29}
30
31#[derive(Clone, Copy, Debug, Deserialize, Eq, Error, PartialEq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum LogicalTreePathError {
34    #[error("logical path must not be empty")]
35    Empty,
36    #[error("logical path must be relative")]
37    Absolute,
38    #[error("logical path contains an unsupported component")]
39    UnsupportedComponent,
40    #[error("logical path contains an encoded or platform-specific separator")]
41    EncodedTraversal,
42    #[error("logical path exceeds its byte limit")]
43    TooLong,
44    #[error("logical asset path must start with assets/ and name an entry")]
45    WrongAssetNamespace,
46}
47
48fn validate_portable_path(value: &str) -> Result<(), LogicalTreePathError> {
49    if value.is_empty() {
50        return Err(LogicalTreePathError::Empty);
51    }
52    if value.starts_with('/') || value.starts_with('\\') {
53        return Err(LogicalTreePathError::Absolute);
54    }
55    if value.contains(['%', '\\']) {
56        return Err(LogicalTreePathError::EncodedTraversal);
57    }
58    if !value.split('/').all(is_portable_component) {
59        return Err(LogicalTreePathError::UnsupportedComponent);
60    }
61    Ok(())
62}
63
64fn is_portable_component(component: &str) -> bool {
65    !component.is_empty()
66        && component != "."
67        && component != ".."
68        && component
69            .bytes()
70            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
71}