systemprompt_identifiers/
path.rs1use crate::error::IdValidationError;
7use crate::{DbValue, ToDbValue};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
12#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
13#[cfg_attr(feature = "sqlx", sqlx(transparent))]
14#[serde(transparent)]
15pub struct ValidatedFilePath(String);
16
17impl ValidatedFilePath {
18 pub fn try_new(value: impl Into<String>) -> Result<Self, IdValidationError> {
19 let value = value.into();
20 if value.is_empty() {
21 return Err(IdValidationError::empty("ValidatedFilePath"));
22 }
23 if value.contains('\0') {
24 return Err(IdValidationError::invalid(
25 "ValidatedFilePath",
26 "cannot contain null bytes",
27 ));
28 }
29 for component in value.split(['/', '\\']) {
30 if component == ".." {
31 return Err(IdValidationError::invalid(
32 "ValidatedFilePath",
33 "cannot contain '..' path traversal",
34 ));
35 }
36 let lower = component.to_lowercase();
37 if lower.contains("%2e%2e") || lower.contains("%2e.") || lower.contains(".%2e") {
38 return Err(IdValidationError::invalid(
39 "ValidatedFilePath",
40 "cannot contain encoded path traversal sequences",
41 ));
42 }
43 }
44 let lower_value = value.to_lowercase();
45 if lower_value.contains("%252e") {
46 return Err(IdValidationError::invalid(
47 "ValidatedFilePath",
48 "cannot contain double-encoded path sequences",
49 ));
50 }
51 Ok(Self(value))
52 }
53
54 #[must_use]
55 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58
59 #[must_use]
60 pub fn extension(&self) -> Option<&str> {
61 self.0
62 .rsplit('.')
63 .next()
64 .filter(|_| self.0.contains('.') && !self.0.ends_with('.'))
65 }
66
67 #[must_use]
68 pub fn file_name(&self) -> Option<&str> {
69 self.0.rsplit(['/', '\\']).next().filter(|s| !s.is_empty())
70 }
71}
72
73impl fmt::Display for ValidatedFilePath {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "{}", self.0)
76 }
77}
78
79impl TryFrom<String> for ValidatedFilePath {
80 type Error = IdValidationError;
81
82 fn try_from(s: String) -> Result<Self, Self::Error> {
83 Self::try_new(s)
84 }
85}
86
87impl TryFrom<&str> for ValidatedFilePath {
88 type Error = IdValidationError;
89
90 fn try_from(s: &str) -> Result<Self, Self::Error> {
91 Self::try_new(s)
92 }
93}
94
95impl std::str::FromStr for ValidatedFilePath {
96 type Err = IdValidationError;
97
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
99 Self::try_new(s)
100 }
101}
102
103impl AsRef<str> for ValidatedFilePath {
104 fn as_ref(&self) -> &str {
105 &self.0
106 }
107}
108
109impl<'de> Deserialize<'de> for ValidatedFilePath {
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: serde::Deserializer<'de>,
113 {
114 let s = String::deserialize(deserializer)?;
115 Self::try_new(s).map_err(serde::de::Error::custom)
116 }
117}
118
119impl ToDbValue for ValidatedFilePath {
120 fn to_db_value(&self) -> DbValue {
121 DbValue::String(self.0.clone())
122 }
123}
124
125impl ToDbValue for &ValidatedFilePath {
126 fn to_db_value(&self) -> DbValue {
127 DbValue::String(self.0.clone())
128 }
129}